- Blocker: FlowProposal can't link an l1_walk_session (source_session_id is
NOT NULL FK→ai_sessions, UI links /pilot). Add nullable l1_session_id +
exactly-one CHECK + read-only walked-path link for L1-sourced proposals.
- High: flow_matching_engine matches published flows only; scope match pass
to flows, defer proposal-matching.
- High: notification system is FlowPilot-shaped; enumerate the 3 changes for
l1.session.escalated (VALID_EVENTS, link+body builder, explicit engineer
recipients). Engineer-visible surface is the primary handoff.
- Medium: match before category gate so authored flows aren't blocked.
- Medium: define normalize_walked_path → valid tree with root id, unexplored
branches as needs_review stubs.
- Medium: category write auth needs owner/admin, not engineer; add
require_account_owner_or_admin dep.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brainstormed design for real-time AI tree building when no KB/flow matches.
Overrides the original "no empty-KB build" rule: build from generic L1
knowledge under a layered safety model (classification gate, constrained
generation, per-node validation with a hard floor, standing disclaimer).
Approach C — dedicated ai_tree_builder + match_or_build orchestrator,
reusing flow_matching_engine and the knowledge_flywheel proposal pipeline.
Scope: streaming node-by-node builder, admin-configurable categories,
flywheel capture of resolved trees, minimum escalation handoff (notify +
engineer surface). KB ingestion/connectors, PSA reassign, escalation
package, and AI chat handoff deferred to later phases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 1 ships internal-only. Escalation handoff, AI tree builder, KB connectors deferred to Phase 2A (spec in progress). All checks green incl. e2e on 890cb80.
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>
Backend boot verified in local PR env. AI_KB_CONVERT_STRUCTURED_OUTPUT flag remains False by default; behavior on prod unchanged until staging-validated flip.
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>
Record the 3c finding: Anthropic structured outputs apply only to flat-array
generate_json outputs (kb_conversion). ai_fix and knowledge_flywheel flow-gen
emit recursive/nested decision trees that the "no recursive schemas" limit
excludes; their fence-strippers stay. Documents the deferred kb-only
_try_repair_json removal pending staging validation of the
AI_KB_CONVERT_STRUCTURED_OUTPUT flag.
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>
Stop crashed-process core dumps (core.144926, etc.) from showing up as
untracked noise / being committed by accident.
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>
Harden the Anthropic provider and lay the groundwork for schema-constrained
JSON, optimizing the existing claude-sonnet-4-6 / claude-haiku-4-5 usage
(no model changes).
ai_provider.py:
- _extract_text_from_response replaces fragile response.content[0].text:
skips non-text leading blocks (e.g. thinking), returns the first text
block, logs an anthropic.stop_reason warning on max_tokens/refusal
(truncation now observable), and raises ValueError on a no-text response.
- generate_json gains an optional `schema` param. Anthropic wires it to
output_config.format (structured outputs); schema=None preserves the exact
prior call for every existing caller. Gemini accepts-and-ignores it.
kb_conversion_service.py:
- TROUBLESHOOTING_SCHEMA / PROCEDURAL_SCHEMA + _schema_for_target_type(),
modelled as a strict superset of every field the prompts emit.
- convert_document passes the schema only when the new
AI_KB_CONVERT_STRUCTURED_OUTPUT setting is True (default False). The
_try_repair_json fallback stays as belt-and-suspenders.
Tests: 14 provider + 7 schema, TDD (red-green). Live constrained-decoding
smoke-test still required before enabling the flag in production.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sessions are account-scoped (per spec §7.9), not user-scoped, to support
team coverage. Comment-only fix surfaced by final review.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per spec §5.6.1, audit rows are written at session terminal events
(resolve, escalate, escalate_without_walk). log_audit gains an optional
acting_as parameter that propagates the session's acting_as tag
('l1_coverage' for engineer coverers, null for native L1 users).
Final code review flagged this as Important — column existed but was
never populated. Four new integration tests cover all three paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Full backend suite (1325/1325 passing, xdist) + L1-specific tests
(57/57) + L1 RLS tests (8/8) + frontend build (tsc clean, vite clean)
+ migration roundtrip results. Per-line checklist against spec §15.
Known Phase 2/3 items explicitly deferred per plan scope section.
fix(test): RLS fixture users INSERT missing NOT NULL columns
test_l1_rls.py and test_rls_isolation.py seeded users without the
five NOT NULL columns added in prior migrations (is_super_admin,
is_team_admin, is_service_account, must_change_password, timezone).
Also adds DROP SCHEMA before alembic upgrade in _ensure_rls_schema
to prevent DuplicateTable errors when create_all tables are present.
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>
Adds 8 synchronous psycopg2-based tests that connect as resolutionflow_app
and verify the tenant_isolation RLS policies (USING + WITH CHECK) on the two
new L1 Phase 1 tables block cross-tenant reads and reject cross-tenant INSERTs.
Uses psycopg2 (not asyncpg) to avoid the conftest pytest_runtest_teardown hook
that closes the asyncio event loop after every test — incompatible with
module-scoped asyncpg fixtures in pytest-asyncio 0.24.
conftest.py: extends _RLS_TEST_FILES set to include test_l1_rls.py so it is
excluded from the default create_all test suite (requires RUN_RLS_TESTS=1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
flip_stale_sessions flips L1WalkSession.status from 'active' to
'abandoned' for rows where last_step_at is older than 24h. Preserves the
row for audit; removes it from the L1 dashboard's 'Resume in progress'
widget. Runs hourly via APScheduler with max_instances=1 (Lesson 1).
Uses the admin session factory (no RLS context at startup).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mounts /api/v1/l1/* with require_l1_or_coverage on every route. Intake
creates an internal ticket and starts a flow OR adhoc session (PSA queue
merge follows in Phase 2). Step/notes/resolve/escalate delegate to
l1_session_service. escalate-without-walk creates an immediately-
escalated session for the BuildAbortedNoKB path.
ValueError from services → 400. Cross-account session access → 404.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
resolve: sets status=resolved, helpful, resolution_notes, resolved_at;
flips FlowProposal.validated_by_outcome on helpful=True proposal walks;
closes linked internal ticket. PSA close is a Phase 2 stub.
escalate: marks session + internal ticket as escalated. PSA reassign
deferred to Phase 2.
escalate_without_walk: creates an immediately-escalated adhoc session
with no walked_path, used by the BuildAbortedNoKB → Escalate path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
record_step appends to walked_path JSONB and advances current_node_id
on flow/proposal walks; refuses adhoc sessions. update_notes replaces
walk_notes (used by adhoc walks for debounced autosave); 256KB size cap
to prevent unbounded JSONB growth. Both reject non-active sessions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three start_* functions creating L1WalkSession rows with appropriate
session_kind and target id. Engineers acting in L1 mode get
acting_as='l1_coverage' for audit; native l1_tech users get acting_as=None.
step/notes (T13) and resolve/escalate (T14) extend this file next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
T11 review caught that get_ticket was the one function without the *, marker
all other functions in the module use. One-line fix, no caller impact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
create_ticket, update_status (sets resolved_at on resolve), get_ticket,
list_tickets_for_account (status filter, account-scoped), promote_to_psa.
Used by L1 intake when account has no PSA integration configured.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner-only endpoint to toggle can_cover_l1 on an engineer user. 422 if target
role is not engineer (owners/super_admins already see L1 surface; viewers/
l1_techs don't need this flag). 404 for cross-account targets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Returns {engineer: SeatCheckResult, l1_tech: SeatCheckResult} for the
authenticated engineer's account. Powers the SeatCounterWidget UI in the
admin/users + account/users surfaces. Engineer+ access only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- oauth.py: use status.HTTP_402_PAYMENT_REQUIRED constant (was raw 402)
- accounts.py bulk-invite: catch HTTPException separately to preserve
structured detail dict in failed-row error (was stringified repr,
unparseable by clients)
- Add bulk-invite per-row 402 test verifying structured error preserved
T8 code review identified these as Important issues. Functional change is
the bulk-invite fix; clients can now parse seat-limit errors from bulk
responses. 13/13 seat-enforcement tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For engineer + l1_tech roles, check_seat_available is called at each
mutation point. Returns 402 Payment Required with structured detail
{code: 'seat_limit_exceeded', role, current, limit, upgrade_url} when
seats are full. Grandfathering: existing over-seated accounts keep
existing users; only new mutations are blocked.
Also updates AccountInviteCreate and AccountRoleUpdate schemas to
accept l1_tech as a valid role value.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Shared helper used by invite, accept-invite, and role-change endpoints
(integrated in T8). Counts active users by role against role-specific
seat limit on subscription (engineer → seat_limit, l1_tech → l1_seat_limit).
None limit = unlimited.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aligns the model with the migration (T6 review caught: migration creates
ix_l1_walk_sessions_last_step_at but model annotation was missing, causing
schema drift if Base.metadata.create_all is used in tests).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tenant-scoped fallback ticket model for accounts without PSA integration.
Tracks customer-name, problem-statement, status lifecycle (open/walking/
resolved/escalated), and optional links to flow/proposal/ai_session/
assigned engineer + PSA promotion ID. Account-scoped RLS policy uses
app.current_account_id session setting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds source (NOT NULL, backfilled to 'manual_draft'), linked_ticket_id,
linked_ticket_kind, validated_by_outcome columns. CHECK constraints on
source values and linked_ticket_kind values. walked_path lives on the
new l1_walk_sessions table (Task 6) — NOT on FlowProposal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds users.can_cover_l1, accounts.l1_seats_purchased, subscriptions.l1_seat_limit,
audit_logs.acting_as. Rotates the users.account_role CHECK constraint to include
'l1_tech' (was: 'owner', 'admin', 'engineer', 'viewer').
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>