Mounts L1EscalationsSection on EscalationQueuePage (Finding 2a — it was never
rendered) and renders the correct fields: step.question ?? step.text, timeAgo,
and the session problem_text (Finding 2b). ProposalDetail gates the /pilot link
on source_session_id and shows an L1-source block for l1_session_id-sourced
proposals (Finding 3 — was a broken /pilot/null link). Collapses the three
near-identical intake handlers into one runIntake: "Use this flow" now passes
near_miss.flow_id (Finding 4 — it previously re-suggested forever) and a
navigate guard prevents /l1/walk/undefined; out_of_scope gains a "Walk it
ad-hoc" button (Finding 5). Aligns L1-category permissions to owner+admin:
usePermissions.canManageAccount includes account admins, User.account_role TS
type gains 'admin', and a new ProtectedRoute requireAccountManager guard fronts
the route (Finding 7). Drops the unused NextNodeRequest.acknowledged field.
tsc -b + eslint + vite build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- flow-proposal.ts: source_session_id nullable + add l1_session_id (matches backend
FlowProposalSummary).
- ProposalDetail.tsx: render an 'AI L1 walk (outcome-validated)' note when
l1_session_id is set instead of the /pilot/{source_session_id} link; fall back to
the link for ai_session-sourced proposals.
- New L1EscalationsSection.tsx (GET /l1/escalations) — expandable rows with walked-path
summary; renders nothing if empty. Mounted below the FlowPilot queue on
EscalationQueuePage. tsc -b + eslint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New owner-gated pages/account/L1CategoriesPage.tsx: checkbox list of available
categories toggling enabled via l1Api.getCategories/setCategories, plus a read-only
'always excluded (safety)' hard-floor list. Registered lazy route /account/l1-categories
(ProtectedRoute requiredRole=owner) and an 'L1 AI build categories' card in the
AccountSettingsPage owner section. tsc -b + eslint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ad9c4c8 committed with TSC_EXIT=2 (I batched the commit with its own failing
verification). Two regressions, now fixed and tsc -b + eslint verified (TSC=0,
ESLINT=0):
- L1WalkTreeVariant.tsx: the ai_build JSX branch referenced isAiBuild/node/
nodeLoading/nodeError/advanceNode/isTerminalNode that were never declared (the
import + state Edits had silently failed). Add the import (useEffect/useCallback,
TreeNode) and the state/effect/advanceNode/isTerminalNode block.
- L1Dashboard.tsx: had reverted to the original (no dispatch). Re-add outcome
dispatch as minimal edits on the real page (matched/build->walker; suggest->
use-flow/build-new; out_of_scope->escalate-without-walk).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tasks 14 (df7150f) and 15 (f483196) were committed with broken TypeScript (I
misread eslint EXIT=0 as 'tsc clean'). Corrections:
- L1Dashboard: revert the speculative rewrite (it imported a non-existent
StartWalkPanel and dropped the real PageMeta/greeting/inputs layout). Re-apply
outcome dispatch as a MINIMAL edit on the real page — handleStart branches on
outcome (matched/build -> walker; suggest -> use-flow/build-new; out_of_scope ->
escalate-without-walk), preserving the original structure.
- L1WalkTreeVariant: revert the rewrite (it imported a non-existent WalkModals and
changed the props contract, breaking L1WalkPage). Re-apply on the real component:
keep {session,onSessionUpdate,onDone} + ResolveModal/EscalateModal + header +
transcript sidebar; add an ai_build branch that walks nodes via /next-node (passing
node_text), a disclaimer banner, and terminal -> existing resolve/escalate modals.
flow/proposal keep the Phase-1 synthetic path.
Verified: tsc -b EXIT=0 + eslint EXIT=0 (whole-project typecheck). L1WalkPage
unchanged (already routes ai_build -> tree variant).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
L1WalkTreeVariant drives ai_build sessions node-by-node through POST /next-node:
fetch first node on mount, render question (yes/no) / instruction (acknowledge),
pass node_text on each advance; terminal nodes (resolved/escalate/needs_review)
hand off to the existing Resolve/Escalate modals. Standing AI disclaimer banner on
ai_build walks. L1WalkPage routes ai_build to the tree variant. Published flow/
proposal keep the Phase-1 stub. tsc -b + eslint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two regressions surfaced by running the L1 e2e suite against current main
(which carries PR #174's /home routing migration):
1. L1 post-login redirect keyed off `pathname === '/'`, but the authed index
moved to /home in #174 — so L1 users landed on the engineer dashboard
instead of /l1. Replace the ad-hoc '/' and /pilot|/assistant checks with a
single allowlist: l1_tech users may only reach /l1*, /guides, /account,
/change-password; everything else (incl. /home, /pilot, /trees/*,
/escalations) bounces to /l1. Runs before the requiredRole check so L1
users never trip the engineer-route role logic.
2. Rail nav Links exposed only the truncated shortLabel as their accessible
name (title= is not an accessible-name source when visible text exists), so
the "L1 Workspace" coverage-engineer link was unreachable by role+name. Add
aria-label={item.label} for an accurate accessible name on every rail link.
Fixes all 3 failing cases in e2e/l1-workspace.spec.ts. tsc + eslint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frontend CI failed on @typescript-eslint/no-explicit-any in three L1
post-review fix sites. Replace `(err as any).response...` with the
codebase's established structural cast
`(err as { response?: { data?: { detail?: string } } })`, matching
TicketPickerModal / FolderEditModal / ProceduralEditorPage. The
AccountSettingsPage 402 handler gets the fuller seat-limit detail shape.
tsc clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The post-login redirect pushes l1_tech users from / to /l1, but a
bookmark, browser back, or direct URL still landed L1 users on /pilot,
where the page tried to POST /api/v1/ai-sessions and got 403. Frontend
swallowed that as a generic 'Failed to start AI conversation' toast.
Add a route-level redirect in ProtectedRoute so L1 users hitting /pilot
or /assistant bounce to /l1 — turns the backend 403 into a clean UX path
that matches the spec's intent (L1 = walker, engineer = pilot).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Recover and commit the landing-page redesign that had been sitting
uncommitted in the working tree: refreshed dark palette (adjusted
--lp-bg-alt, electric-blue accent), Atkinson Hyperlegible Next display
+ body type, and editorial hero/section layout in LandingPage.tsx, with
the matching font preload in index.html.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catches the structured detail from the seat-enforcement 402 and surfaces
a clear toast with current/limit counts instead of a silent failure.
Modal-with-upgrade-link is a v2 polish — Phase 1 just ships a toast.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Final review flagged silent failure on intake error. Adds a toast with
the backend detail message (or fallback) on catch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
l1-workspace.spec.ts covers:
- L1 user lands on /l1, intakes a problem, takes notes (autosave), resolves
- L1 cannot access /pilot, /trees/new, /escalations (route guards)
- Engineer with can_cover_l1 sees the L1 Workspace nav + coverage banner
- escalate-without-walk path via direct API call returns escalated session
Seed script adds l1@resolutionflow.example.com (l1_tech) and
engineer-coverage@resolutionflow.example.com (engineer + can_cover_l1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
L1DraftsPage is a Phase 1 placeholder (AI drafts arrive in Phase 2).
L1TicketsPage replaces the stub with a status-filterable internal-tickets
queue. L1CoverageBanner renders inside L1RouteGuard so every /l1/* page
shows it for engineer-coverers (hidden for native L1). SeatCounterWidget
+ /api/seats.ts surface engineer + L1 seat usage from the /accounts/me/
seats endpoint (T9).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The session variant that Phase 1 L1 users actually hit (intake creates
adhoc sessions when no flow_id is provided). Single-pane note-taking
surface with 300ms-debounced autosave to walk_notes. Shares header
shape + Resolve/Escalate modals with the tree variant. Splits the
notes textarea by paragraph and persists each as a structured
AdhocNote entry. Stops saving once status leaves 'active'.
L1WalkPage now dispatches both variants.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the T20 stub. WalkPage dispatches by session_kind:
- 'flow' / 'proposal' → L1WalkTreeVariant (this commit)
- 'adhoc' → placeholder until T23
L1WalkTreeVariant: sticky header with back link + AI-built badge +
persistent Escalate/Resolve buttons; two-pane body (current step
yes/no card on left, walked-path transcript on right). ResolveModal
and EscalateModal extracted to shared WalkModals.tsx (T23 reuses).
Phase 1 caveat: this surface isn't reached by user-driven intake
(which creates adhoc sessions only). It's exercised via direct URL
or integration tests until Phase 2 wires match_or_build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the T20 stub. L1 dashboard renders greeting, "Describe the
problem" intake card (autofocus textarea, optional customer fields,
primary "Start walk" CTA), open-tickets queue (Phase 1: display-only),
and a "Resume in progress" widget listing the L1's active sessions
ordered by last_step_at DESC. Empty-state card shows on accounts with
no queue + no active sessions (first-run nudge to upload KB or auth flows).
Adds /api/l1.ts (full L1 API client surface) and /types/l1.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
L1RouteGuard wraps the new routes and redirects users without
canUseL1Surface back to /. Page components are stubs in this task
(real UI in T21-T24): L1Dashboard, L1WalkPage, L1DraftsPage,
L1TicketsPage.
Routes: /l1, /l1/walk/:sessionId, /l1/drafts, /l1/tickets — all gated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
L1 users see a focused sidebar with only their L1 surfaces (Workspace,
Tickets, My Drafts, Guides, Account). Engineers with can_cover_l1
(plus owners/super_admins) get an appended "L1 Workspace" entry in
their existing sidebar. ProtectedRoute redirects L1 users from / to /l1
on login.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 'l1_tech' to the AccountRole union, includes can_cover_l1 on the User
type, and exposes isL1Tech / canCoverL1 / canUseL1Surface /
canUseEngineerSurface from usePermissions. Existing isEngineer/isOwner/
etc. flags unchanged in semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The original public-landing routing refactor migrated WelcomeRouter,
WelcomeStep1, and WelcomeStep2 post-onboarding redirects to /home, but
left four sites still pointing at the old / + query-string destinations:
- WelcomeStep3 `completeWizardAndExit` (Send invites)
- WelcomeStep3 `handleSkipStep` (Skip)
- VerifyEmailPage post-verify auto-redirect (`setTimeout`)
- VerifyEmailPage success-state "Go to dashboard" Link
These all worked by accident because PublicLanding redirects authed
users from / to /home — so users still landed on the dashboard, but
through an unnecessary mount-and-redirect flicker, and the
`?welcome=true` / `?verified=1` query markers got dropped on the way.
Drop both query markers — neither is read anywhere in the codebase
(grepped frontend/src; the dashboard's onboarding UX is driven by
`getOnboardingStatus`, not URL state). Carrying dead URL params
just invites future "is this load-bearing?" investigations.
Test stubs in WelcomeStep3.test.tsx and VerifyEmailPage.test.tsx
moved from `<Route path="/">` to `<Route path="/home">` so the
assertions verify the new destination instead of accidentally matching
the old one (the previous stubs masked the partial migration).
Out of scope: AcceptInvitePage and OAuthCallbackPage still use
`?welcome=teammate`, but that one carries an explicit "decoded by the
dashboard in Task 41" annotation and may be wired up later, so left
untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-refactor, WelcomeRouter and the Step1/Step2 "Skip-the-rest" handlers
navigate to /home, but the MemoryRouter test stubs still mounted the
"dashboard" marker at /. Update the stub routes (and matching it() titles)
so the assertions resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Playwright specs still asserted the pre-refactor URLs and failed on CI:
- auth.spec.ts expected post-login to land at `/`; now `/home`.
- public.spec.ts expected unauth redirect to `/landing`; now `/`.
- public.spec.ts's landing-loads test navigated to `/landing` (a stale-
bookmark redirect); point it directly at `/`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stripe's compliance crawler fetches the apex URL without executing JS and
declined live-mode review when `https://resolutionflow.com/` returned the
empty SPA shell that redirected to /landing client-side. Restructure the
router so / serves LandingPage directly:
- `/` → new `PublicLanding` wrapper (LandingPage for anon; Navigate to
/home for authed users so there's no marketing-frame flicker).
- Authed tree converted to a path-less layout route with absolute child
paths. QuickStartPage moves to `/home`; all other children
(`/trees`, `/pilot`, `/admin/*`, `/account/*`, etc.) keep their URLs.
- `/landing` kept as a one-release stale-bookmark redirect to /.
- `ProtectedRoute` unauth redirect flipped /landing → /; `state.from`
preserved for post-login return.
Reference updates:
- Post-login / post-onboarding destinations → /home: OAuthCallbackPage
(incl. `?welcome=teammate` query), WelcomeStep1/2/3 dismiss-rest,
AssistantChatPage post-escalate, WelcomeRouter completion/dismiss
redirects, VerifyEmailPage's three "Go to dashboard" links.
- Authed chrome → /home: TopBar logo, AppLayout mobile nav + drawer
logo, CommandPalette Dashboard entry.
- Dashboard onboarding → /home: NextStepCard `ran_session.ctaPath`,
SetupChecklist `ran_session.path`, SessionHistoryPage empty-state CTA.
- Public back-links → /: TermsPage, PrivacyPage, PoliciesPage,
ContactPage, PromotionsPage, PublicTemplatesPage (header + footer).
SharedSessionPage's `to="/"` left as-is — now correctly lands anon
visitors on the public landing.
Crawlability:
- New `frontend/public/robots.txt` allowlisting public pages and
disallowing the authed app.
- New `frontend/public/sitemap.xml` for /, /pricing, /contact-sales,
/contact, /templates, /terms, /privacy, /policies, /promotions.
- `PageMeta` gains an `og:url` (defaults to `window.location.href`) and
flips `twitter:card` to `summary_large_image` when an `ogImage` is
passed.
Tests:
- `AppLayout.test.tsx` updated to mount at `/home`.
- New `ProtectedRoute.test.tsx` asserts unauthenticated `/home`
redirects to `/` (not `/landing`) and preserves origin in `state.from`.
If Stripe's crawler still cannot see the site after this (zero-JS
crawler), the documented next escalation is server-side prerendering of
public routes via `vite-plugin-ssg`. Out of scope here.
Plan: docs/plans/2026-05-13-public-landing-routing-refactor.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picking a real PSA in /welcome/step-2 now swaps the primary action from a
single "Continue" + a tiny "Connect now →" link into an explicit choice:
"Connect <PSA> now" (saves primary_psa and routes to /account/integrations)
or "Connect later" (saves primary_psa and continues to step 3). The old
link never actually persisted primary_psa before navigating — that's now
fixed. "No PSA yet" and no-selection states keep the original single
Continue button. Skip-this-step and Skip-the-rest are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI surfaced react-hooks/set-state-in-effect on the synchronous
setState(computeState(token)) inside the useEffect body. The earlier
shape mirrored token -> state via an effect, which is exactly the
"you might not need an effect" pattern React 19's eslint rule now
flags.
Switch to derived state: compute during render, use a useReducer
tick to force re-render on the 30s cadence (so relative timestamps
stay current even when token props don't change). Same observable
behavior, no cascading renders.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "Start a session" CTAs on the NextStepCard and SetupChecklist used to
Link-navigate, which left the user on the same page (the Start Session
input lives on the dashboard) without any visible response. Replace those
CTAs with a custom window-event dispatch (FOCUS_START_SESSION_EVENT) that
the StartSessionInput listens for: scroll the input into view, focus the
textarea, and pulse a ring for 900ms so the click feels intentional. The
NextStepCard also locally hides itself after firing so the user isn't
double-prompted while typing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eighth commit in the session-expiration-policy series. Surfaces all
the owner controls and user-facing expiry UX that the prior commits
plumbed through, designed end-to-end via /plan-design-review (initial
4/10 -> final 9/10; 7 decisions locked in the plan).
Backend additions:
- accounts/me/security GET response gains active_users: list of
{user_id, name, email, last_login_at} for users in this account
with at least one un-revoked refresh token. Joined query on
refresh_tokens + users, distinct, ordered by last_login desc.
Drives the Active Sessions section.
Frontend additions:
- api/accountSecurity.ts: typed client for GET/PATCH/revoke-sessions.
- hooks/useAuthSessionExpiry.ts: reads idle/absolute expiry from the
auth store, returns warning ('none'|'soon'|'now') + reason
('idle'|'absolute') so consumers can pick the right UX for the
closer window. Re-evaluates every 30s.
- components/common/SessionExpiryToast.tsx: top-of-app notice that
fires at T-5min. Idle case: warning-amber tone, [Stay signed in]
button hits authApi.refresh() and updates the store on success.
Absolute case: info-cyan tone, [Sign in now] link to /login (no
recoverable action). Dismissable, doesn't re-fire after dismissal.
- components/account/RevokeSessionsModal.tsx: confirmation modal for
the two bulk-revoke scopes. Title, body, and confirm-label vary by
scope; danger-styled confirm button.
- pages/account/AccountSecuritySettingsPage.tsx: the main page.
Header (Shield icon), intro, Policy card with Strict/Standard/Custom
radios + always-visible-disabled Custom inputs (idle/absolute
minutes) with inline validation, Save button + emerald success ping,
info note about 'applies at next login'. Active sessions card with
count-aware copy, list of {name, email, last-login-ago} rows
(caller tagged '(you)'), two buttons — 'except me' hidden when
count=1, 'sign me out and everyone else' uses danger-tinted styling.
- pages/AccountSettingsPage.tsx: 'Session security' row added to the
owner-only settings list.
- router.tsx: /account/security route, owner-gated via ProtectedRoute.
- pages/LoginPage.tsx: cyan info-tone banner above form when
?reason=session_expired is in the URL.
- components/layout/AppLayout.tsx: mounts <SessionExpiryToast />.
Scope=all bulk-revoke UX (the most jarring moment): on success,
toast.success(N sessions), 1.5s delay, then clear localStorage +
useAuthStore.logout() + window.location='/login' (no banner — the
owner just did this).
Backend tests: existing 22/22 still green plus the GET test now
asserts active_users is present + non-empty after login. Frontend:
tsc clean, authStore test 2/2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Seventh commit in the session-expiration-policy series. Wires the
backend taxonomy from commit 2 through to the frontend so users see
the right page (calm banner vs plain logout) when the refresh path
fails for different reasons.
- types/auth.ts: Token gains idle_expires_at + absolute_expires_at
(Optional ISO 8601 strings). The next commit adds the
useAuthSessionExpiry hook that reads these.
- api/auth.ts: OAuthCallbackResponse mirrors the same two fields.
- api/client.ts: refresh-failure handler now branches on the response
detail. session_expired_idle and session_expired_absolute both
redirect to /login?reason=session_expired (commit 8 adds the
banner that reads the query param); any other detail (most
commonly invalid_refresh_token) goes to plain /login. The bare
redirect is guarded against re-firing when the user is already on
/login. The refresh-success path now forwards the two new fields
into setTokens so the store stays current as the session ages.
- pages/OAuthCallbackPage.tsx: setTokens({...}) spreads
idle_expires_at + absolute_expires_at from the OAuth response.
No new tests — authStore.test still 2/2, tsc clean. The
useAuthSessionExpiry hook and the SessionExpiryToast that consume
the new fields land in commit 8 alongside the AccountSecurity page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the three legal/contact pages needed for Stripe live-mode site review: /policies (consolidated customer policies — refunds, cancellation, legal restrictions, promotions), /contact (phone (470) 949-4131 + support/sales/billing/security inboxes), /promotions (stub satisfying §6.2 cross-ref).
Extracts the existing landing footer into components/common/MarketingFooter.tsx and mounts it on /pricing and /contact-sales so all four legal links are reachable from every marketing surface.
Privacy and Terms closing sections updated to point at /contact + /policies; stale hello@ mailto removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Michael Chihlas <michael@resolutionflow.com>
Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
8612042 dropped the static "Account Management" heading in favor of the
account name (rendered as a dynamic h1). Switch the smoke test to the
"Settings" SectionLabel — a stable h2 that survives the redesign.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /subscription endpoint returns usage as {tree_count, session_count_this_month}
without user_count, so the Seats UsageRow rendered as " / ∞" (blank current value).
The TS type declared user_count: number, hiding this API/type drift; the old
card-stack design hid it visually because each stat had its own border. The new
flat layout surfaced the gap.
Owners get a fallback to members.length (already fetched). Non-owners can't
fetch members and don't need seat-count info, so the row hides entirely for
them. Verified live: owner now sees Seats 2 / ∞.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The index page had ~12 distinct card surfaces with three places of
nested cards-inside-cards, against PRODUCT.md's "elevation = lighter
surface + border" + "nested cards are always wrong" rules. Branding
appeared twice, Display Code lived in Identity but does invite work,
and Preferences got a full card for one dropdown.
Single column, max-w-3xl, no card chrome. Sections separated by
border-t rules + mono-uppercase section labels (existing house style):
- Header: inline-editable name + plan/status/owner/member-count info
line. No card.
- Plan & usage: renewal date right-aligned in section header, three
thin progress rows replace the 4-card usage stat grid, upgrade
CTAs right-aligned at bottom.
- People (owner-only): invite form, unified members + pending invites
list, display code as a quiet "share to invite during signup" line.
Non-owners see a one-line "managed by your admin" instead of a card.
- Settings: dense route list (icon + title + summary + status pill +
chevron). Profile above a thin divider; team-admin rows below,
owner-gated. Branding row carries the Included/Plan-gated pill.
Support & Feedback as a dim link at the bottom.
- Account actions: plain rows. Owner: Transfer + Delete. Non-owner:
Leave. Destructive labels colored, no red box-of-doom.
Drops: Access & Security card (filler), Preferences card,
Settings Areas link grid, billing-card branding-status duplicate,
SettingsLinkCard helper. Default export format moves to Profile
Settings where it belongs (personal preference, not account).
856 -> 710 lines on the index. tsc, eslint, vite build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Sidebar: kill the drifting railGroups + sections dual definition.
Single source of truth (workItems / libraryItems / footerItems)
rendered in both pinned and rail modes; pin/unpin is a width and
label affordance, not an IA switch. Hairline divider replaces
section labels. Guides moves to the footer alongside Account.
Renames: Home -> Dashboard, History -> Sessions, Insights -> Analytics.
- CURRENT-STATE.md: log PR #158 (session impeccable pass + tasklane
keyboard flow) under "Recently shipped".
- PRODUCT.md: design-context source of truth (users, brand, aesthetic);
sibling to DESIGN-SYSTEM.md.
- skills-lock.json: lock /impeccable + /documentation-writer skill
versions so other sessions reproduce the same tooling state.
- Drop stale .impeccable.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace 15 feature-dump guides with 43 problem-oriented how-tos grouped
under 10 categories. Drop Maintenance Flows / AI Assistant / Flow Assist
Sparkles — those surfaces no longer exist post-FlowPilot pivot. Rename
Step Library → Solutions Library throughout. Correct every "click X in
the sidebar" reference to match live labels (Home, History, Tickets,
Flows, Scripts, Data, Acct).
Schema: add `category: CategoryId` and optional `relatedSlugs` to Guide;
new Category type and `categories` const drive hub ordering. GuidesHubPage
renders category sections (auto-hides empty); GuideDetailPage renders a
related-guides footer when set; GuideCard drops the misleading "N sections"
subtitle.
Fix step.tip markdown rendering — `**bold**` rendered literally because
tip used plain text instead of the same regex replacement used on
instruction.
14 net-new how-tos for FlowPilot-era surfaces with no prior coverage:
tasklane keyboard flow, view-what-we-know, ask-AI mid-session,
pause-and-leave, resolve, record-fix-outcome, escalate (Escalation
Mode), post-docs-to-ticket, send-client-update, build-script-from-scratch,
open-suggested-flow, pin-a-flow, invite-teammate.
Browser-verified against engineer + owner test users (sidebar labels,
account sub-pages, pilot-screen header buttons, Tasks panel, integration
form). tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small ergonomic fixes after the impeccable pass:
- TaskLane keyboard hints (⏎ submit · ⇧⏎ newline) under each open input
were rendered at text-muted-foreground/70, just shy of legible at a
glance. Drop the /70 opacity modifier so they read at full muted weight
on first look without becoming visually loud.
- 12 sites across the session screen had explicit font-sans utilities,
but the body default is already IBM Plex Sans (via --font-sans in
index.css and Tailwind v4's default-sans binding). None of the call
sites sit inside a font-heading or font-mono cascade, so every
font-sans there was a no-op. Drop them. ConcludeSessionModal also had
three "text-xs font-sans text-xs" triplets — drop both the redundant
font-sans and the doubled text-xs in one pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ParameterizationPreview.tokenize() matched highlight values via raw
seg.text.startsWith(value, cursor) with no word-boundary check and no
minimum length. A param value like "D" (e.g. a drive letter) lit up every
capital D in the script body — Get-ADUser, Add-Type, Disable- all rendered
as proposed-parameter pills.
Add a word-boundary guard: a candidate match is only accepted if either
side of the match either falls at start/end of the segment, OR the
adjacent character is non-alphanumeric. The guard is conditional on
whether the value itself starts/ends with a word char, so values that
begin or end in punctuation (e.g. "D:\\Folder") still match cleanly when
they sit next to whitespace or punctuation.
Surfaced 2026-05-01 while testing the suggested-fix flow with a real
PowerShell script.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Multi-step UX refactor of the assistant chat session screen, run via the
$impeccable skill. Heuristic score moved 24/40 → 33/40 (+9), with the biggest
gains on Aesthetic & Minimalist (1→3), Consistency & Standards (1→3), and
Recognition Rather Than Recall (2→4).
Distill — chat region:
- Remove the "Suggested checks" chip strip + selected-chip detail card; the
TaskLane is the single canonical home for "what to do next"
- Add an inline Next steps · N pending cue above the latest action-bearing
AI bubble (anchors attention without duplicating the lane's items)
- Link banner ↔ script-panel lifecycle: collapsing or dismissing the
ProposalBanner now also hides the InlineNoTemplateDialog / TemplateMatchPanel
- Drop backdrop-blur on the handoff-context overlay (DESIGN-SYSTEM hard rule)
Quieter — drop decoration overshoot:
- Remove 3px side stripes on TaskLane done cards, all 6 ProposalBanner modes,
WhatWeKnowItem fact rows
- Drop bg-gradient surfaces on WhatWeKnow + every ProposalBanner mode
- Drop 2px accent borderTop on the TaskLane header
- Replace bordered avatar boxes in banners with inline state-colored icons
- Each surface now uses a single decoration channel (top border + inline icon)
Layout:
- Header consolidates to Resolve + Escalate + ⋯ kebab; Context, New Ticket,
Update Ticket, Pause now live behind the kebab on desktop, with feature
parity in the existing mobile overflow menu
- Messages column anchors to max-w-3xl mx-auto to match the composer
- Chat bubbles drop from rounded-2xl to rounded-xl for vocabulary alignment
Typeset:
- Unify text sizing from 14 distinct sizes (with sub-pixel oddities and
rem/px duplicates) to a 5-step scale: 10px / 11px / text-xs / 13px / text-sm
WhatWeKnow collapsible:
- Header is now a toggle; section body hides when collapsed
- Auto-collapses on first render when facts ≥ 5 so Questions / Diagnostic
Checks stay above the fold
- Engineer's choice persists in sessionStorage per session and beats the
auto-collapse heuristic on subsequent renders
- key=activeChatId on both render sites resets state cleanly across sessions
Polish:
- Split MessageCircleQuestion into Pencil (question Answer CTA, write
affordance) + HelpCircle (per-check Explain toggle, universal help icon) —
same icon for two different jobs was a discoverability bug
- Drop redundant text-xs from font-sans text-[0.625rem] / text-[0.6875rem]
double-class definitions; the more-specific size always wins
TaskLane keyboard flow:
- Enter submits and auto-advances to the next pending task; Shift+Enter
inserts a newline (consistent across question and action textareas — paste
events don't fire keydown, so paste-then-Enter still works as expected)
- Esc cancels (same as the Cancel button)
- After the last pending task is submitted, focus moves to the Send Responses
button so the engineer can fire the whole batch with one more keystroke
- Subtle hint row under each open input teaches the shortcut
Type-check, lint, and build all clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Page-level Resolve patches applied_pending → applied_success before
opening the resolution flow, so resolved sessions don't carry a
provisional pending fix.
- Page-level Escalate intercept now catches applied_pending in addition
to verifying/partial; intercept copy generalized from "Verifying state"
to "still needs an outcome."
- PendingBanner gains a Dismiss action, matching the PR body and the
backend's allowed pending → dismissed transition.
- resolution_note_generator and escalation_package_generator system
prompts no longer include real-looking pending examples (anti-parrot
guardrail compliance).
Verified via Docker: prompt anti-parrot 2/2, suggested-fix outcome suite
21/21, frontend tsc -b clean, npm run build clean.
Co-Authored-By: Codex <noreply@openai.com>
Engineer applies a fix but can't verify yet (waiting on client power-cycle,
AD replication, async sync). Today the verifying banner forces a synchronous
verdict (worked / didn't / partial) — anything else means leaving the banner
stale or guessing wrong. This adds a fourth outcome that parks the fix in a
non-terminal "Awaiting verification" state with a reason ("waiting on what?")
and exposes it on the chat-anchored banner so the engineer doesn't lose track.
Backend
- New non-terminal status `applied_pending` parallel to `applied_partial`.
- New `pending_reason` column (nullable Text) — the "what are you waiting on?"
prose, mirrors `partial_notes`. Required when outcome=applied_pending.
- Outcome endpoint allows pending in/out transitions; pending stamps
applied_at but NOT verified_at (it's parked, not verified).
- Resolution-note + escalation-package prompts handle the new status:
resolution note frames the fix as provisional; escalation package surfaces
pending verification as the leading hypothesis with reference to what's
being waited on.
- Migration: add column + extend status CHECK constraint.
Frontend
- New `BannerMode = 'pending'` + `PendingBanner` component (info-tone,
parallel to PartialBanner) with worked / didn't / update-reason actions.
- VerifyingBanner overflow menu adds "Waiting to verify…".
- Nudge banner's "Still checking" button now actually records pending with
a reason, instead of just silencing for the session.
- AssistantChatPage banner-mode derivation maps applied_pending → 'pending'.
Tests: 4 new integration tests covering pending notes requirement, reason
storage + applied_at/verified_at semantics, pending→success transition,
and pending_reason update on re-PATCH.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codex review pass on the escalation wedge. Reworks claim_session from
read-then-write to a conditional UPDATE so two seniors racing can't both
win, blocks the original engineer from claiming their own handoff, and
filters self-escalated sessions out of the dashboard escalation queue.
Also preassigns the handoff UUID before flush so the compatibility
escalation_package payload carries it. Removes legacy frontend pickup
state (claiming, handleStartHere) that broke tsc --noEmit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bundles four fixes from the live debugging session:
1. AssistantChatPage: replace urlSessionId === activeChatId gate with a
loadedChatIdsRef. After 8914391 made activeChatId initialize from
urlSessionId, the gate short-circuited fresh mounts and selectChat
never fired. Symptom: senior picks up an escalation, lands on a blank
chat surface with no conversation history and no sidebar entry. Fix
also adds loadChats() in handleStartHere so the picked-up session
appears in the sidebar (its escalated_to_id is null pre-claim, so
listSessions doesn't return it until claim_session sets it).
2. config: bump ESCALATION_AI_ASSESSMENT_TIMEOUT_SECONDS 15s → 45s.
Sonnet was hitting tail latency at 15s in the field, leaving the
magic-moment placeholder permanent. Background-task architecture
(e8ba74e) means this no longer blocks the user; it's just the budget
before publishing has_assessment=false. NOTE: live test still shows
assessment not populating — see HANDOFF for the consolidation plan
that supersedes this.
3. Enter-to-submit: chat-input convention (Enter submits, Shift+Enter
inserts newline) on the escalate-flow forms. RichTextInput gains an
optional onSubmit prop; EscalateModal wires it to handleSubmit;
ConcludeSessionModal gets the same handler on its plain textarea.
4. PendingEscalations: each row is now expandable. Click row body to
reveal the engineer's escalation reason, step count on record,
confidence tier, and PSA ticket number. Pick Up still clicks through
directly. Single-expand-at-a-time keeps the dashboard compact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>