End-to-end through the real endpoint+service stack (only the AI boundary mocked:
match_or_build outcome + ai_tree_builder.generate_next_node). Asserts the captured
FlowProposal is outcome-validated with l1_session_id set / source_session_id null
and tree root 'n1' (meta entry skipped); and that escalate notifies the account's
engineers and the session surfaces in GET /l1/escalations. 2 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /accounts/me/l1-categories (require_l1_or_above) returns enabled + available
+ hard_floor; PATCH (require_account_owner_or_admin) sets the enabled set, dropping
unknown/hard-floored keys via l1_category_service. New L1CategoriesResponse/Update
schemas. 6 API tests green (incl. engineer + l1_tech write both 403); test_accounts
regression 36 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
An earlier anchor-edit silently failed, so POST /sessions/{id}/next-node and
GET /escalations were never added (they 404'd). Add both, anchored on the real
/escalate-without-walk route.
Phase-1 test_l1_endpoints tests used POST /intake to create adhoc setup sessions,
but Phase 2A intake now dispatches via match_or_build (build/matched/suggest/
out_of_scope — never adhoc). Add a _create_adhoc_session service helper and route
the step/notes/resolve/escalate/cross-account setup through it; rewrite
test_intake_adhoc as test_intake_build_creates_ai_build_session (mocked outcome).
All green: test_l1_endpoints + test_l1_api_ai_build = 25 passed; full Phase 2A
backend service/unit/model suite = 56 passed; notification suite = 18 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
L1WalkSession has no escalated_at column (only started_at/last_step_at/resolved_at
+ escalation_reason[_category]). The /escalations endpoint and its test referenced
escalated_at, which would AttributeError at query time / TypeError at construction.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KNOWN-RED (handoff): test_escalations_forbidden_for_l1_tech passes; the intake/
next-node tests still 403 'L1 access required' despite the DB role persisting as
l1_tech (verified) and get_current_user reading role from the DB. The identical
register->promote->subscribe->login helper works in test_l1_endpoints.py, so this
is a test-harness/auth interaction needing interactive debugging in a clean shell.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- /intake now runs match_or_build (matched/suggest/out_of_scope/build); build
seeds the classified category as a hidden meta walked_path entry, matched starts
a flow session, suggest/out_of_scope return prompt data with no session.
- New POST /sessions/{id}/next-node (threads node_text to advance_ai_build) and
GET /escalations (engineer-or-above) for the handoff queue.
- New IntakeResponse(outcome=...)/NextNodeRequest/NextNodeResponse schemas and
require_account_owner_or_admin dep.
- Reconcile Phase-1 intake tests to the new contract (mock match_or_build); add
test_l1_api_ai_build.py covering build/out_of_scope/suggest/next-node/escalations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove the weaker shadowing copies of the two T9 tests so the stronger
originals (which seed an engineer and assert eng.id in target_user_ids,
plus proposal_type/match_keywords) actually run.
- _resolve_recipients: treat an explicit empty target_user_ids as 'no
recipients' instead of falling back to the default owner/admin set.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add test_resolve_ai_build_creates_outcome_validated_proposal and
test_escalate_notifies_engineers to cover the already-committed
Task 9 implementation (flywheel FlowProposal creation on resolve,
notify() call on escalate). Adapts fixture pattern to test_db +
_make_internal_ticket as required by the T9 spec.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Teaches l1_walk_sessions a new session_kind='ai_build' for AI-generated
decision-tree walks. FK shape matches adhoc: both flow_id and
flow_proposal_id must be NULL. Drops and recreates the two affected CHECK
constraints (session_kind allowlist + target_consistency). Migration
beca7464b6b4 chains from b3358ba0e48c.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`git push --mirror` pushes everything under refs/* including refs/pull/*,
which GitHub rejects with "deny updating a hidden ref" — GitHub manages
its own refs/pull/N/head namespace and won't let outside pushers touch it.
Switching to `--all --prune --force` + `--tags --prune --force` scopes the
push to refs/heads/* and refs/tags/* only (same as the original lines)
while keeping --prune so branch/tag deletions still propagate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`.mcp.json` is per-machine MCP server config (e.g. the GitHub MCP block
added during today's session). It references local env vars for auth
rather than embedding secrets, but the file itself is workstation-specific
— what servers a contributor connects depends on which MCPs they've set
up locally.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Today's cleanup surfaced 14 branches that existed on GitHub but had
been deleted on Gitea — the previous `--all --force` + `--tags --force`
pair pushes refs but never deletes missing ones, so the mirror drifted
over time.
Switching to `git push --mirror` (equivalent to --all --tags --prune
--force) makes the GitHub side a true reflection of Gitea: branch and
tag deletes propagate automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the 2026-05-29 decision to derive admin plan dropdown + validation
from the plan_limits table rather than hand-duplicating the allow-list across
6+ sites. Triggered by the prod "AI sessions down" report that traced to the
admin dropdown still offering the dead 'team' slug. Adds the matching backlog
entry to TODO.md with duplication sites enumerated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>