Completes ProposalBanner's state machine. AIConfirming (accent-blue)
surfaces the AI's [FIX_OUTCOME] proposal with one-click accept; Nudge
is the compact passive-prompt variant for post-apply chats; Collapsed
is the 28px expand-hint strip.
Adds onSilenceNudge prop so the parent can silence the nudge without
collapsing it (Task 11 wires this). Removes the last three stale
eslint-disable-next-line comments — all sub-components now use props.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verifying: amber pulse animation, confidence pill becomes 'Applied Xm ago',
three actions (overflow for Mark partial, Didn't work, It worked). window.prompt
used for the partial notes + failure reason inputs — good-enough v1 pending
an inline composer.
Partial: cyan-toned to signal 'parked, outcome unknown', shows saved notes
inline, Finish it / Didn't work / It worked actions.
Adds pulse-amber to @theme animations alongside slide-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New component that will replace the task-lane SuggestedFix card. Docks
above the chat composer with a 320ms slide-up animation. This commit
implements only the Proposed state (Tasks 8 & 9 fill Verifying, Partial,
AI-confirming, Nudge, Collapsed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends SessionSuggestedFix with outcome fields (status, applied_at,
verified_at, partial_notes, failure_reason, ai_outcome_proposal) and
adds a patchOutcome method hitting the new backend endpoint.
FixStatus (5 values) + FixOutcome (4 writable values) mirror the
backend Pydantic types and the DB check constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tells the AI when + how to emit the [FIX_OUTCOME] marker that Task 4's
parser consumes. Placeholder-only per the anti-parrot pattern — no
literal UUIDs, outcomes, or reasons that could leak into unrelated
sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AI emits [FIX_OUTCOME] when the engineer indicates in chat that a
prior suggested fix worked, didn't work, or was partially applied. The
marker writes to session_suggested_fixes.ai_outcome_proposal (JSONB),
which the frontend surfaces as a "confirm outcome?" banner. The status
column is only updated when the engineer clicks confirm (via PATCH
/outcome endpoint from Task 3).
Placeholder-only system prompt wiring comes in Task 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records engineer-reported outcome (applied_success|applied_failed|
applied_partial|dismissed). Enforces transition rules (partial → success/
failed allowed; terminal outcomes return 409) and notes requirements
(applied_partial requires notes).
Sets verified_at on success/failure, stamps applied_at if not already
set (handles the case where the AI [FIX_OUTCOME] marker fires before
the engineer clicks Apply).
Also fixes pre-existing test-infrastructure bug: network_diagram.py used
bare string server_default="'[]'" for JSONB columns, which asyncpg
rejects during test schema creation. Changed to text("'[]'::jsonb") to
match the pattern used by script_template.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds FixStatus literal (5 values matching the DB check constraint),
extends SessionSuggestedFixResponse with outcome fields, and introduces
SessionSuggestedFixOutcomeRequest for the PATCH /outcome endpoint coming
in Task 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8 prep for the fix outcome banner. Adds:
- status (proposed|applied_success|applied_failed|applied_partial|dismissed)
- applied_at, verified_at (timestamps)
- partial_notes, failure_reason (engineer-provided context)
- ai_outcome_proposal (JSONB for AI [FIX_OUTCOME] marker payloads)
Backfills status='dismissed' from user_decision='dismissed'. status is
orthogonal to user_decision — outcome (did the fix work?) vs script-path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each lane section (What we know, Questions, Diagnostic Checks, Suggested
fix) had its own `position: sticky; top: 0` header. As the engineer
scrolled past a section, that section's header would pin until the
section's bottom edge cleared the viewport, producing an "orphaned"
label floating over unrelated content below. Headers now scroll with
their content — in a 340px-wide lane the affordance was negative value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WhatWeKnow shows a "synthesizing" indicator + skeleton pulse while the
chat cycle is in-flight; task-lane header mirrors the signal with a
"thinking" pip so engineers know the AI is still working.
- Quiet-state hint when the lane is open (facts exist) but no open
questions, checks, or active fix — keeps the surface from looking
"finished" when the AI is about to follow up.
- Keyboard shortcuts: ⌘↵/Ctrl+↵ send in the composer (plain Enter still
sends), ⌘G toggles the Script Generator panel for the active fix,
`?` opens a new ShortcutsHelpOverlay listing all bindings. ⌘K palette
was already wired in TopBar.
- Responsive: below 1200px the task lane collapses to a bottom drawer
with a backdrop + a floating "Tasks ●" toggle button. TaskLane now
takes a `variant: 'side' | 'drawer'` prop; drawer variant drops the
resize handle and uses the shared slide-in-bottom animation.
- Build hygiene: fixed a pre-existing TS error in confirm-post error
handling (duplicate `response` type keys) and an unused-import warning
in TemplatizePrompt.
Verified: `npx tsc -b` and `npm run build` both clean against the dev
stack; Vite HMR applied each change without errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop on the Phase 5 "Run now, templatize after resolve" path.
After a session resolves, drafts queued by the three-option dialog surface
as a modal that lets the engineer review the AI-proposed parameterization
and either save as a reusable team template or skip. A "don't ask again"
toggle writes to account_settings.preferences so the next resolve won't
pop the modal.
Backend:
- /api/v1/draft-templates:
* GET — list account drafts (pending_only default true; pass false for
audit view including accepted/rejected)
* GET /{id} — single draft
* POST /{id}/accept — promotes to a new script_templates row with
source_session_id / source_user_id / source_ticket_ref populated
(drives the Script Library "generated from CW #X · resolved by Y"
provenance chip). Draft flips to status=accepted,
promoted_template_id set, resolved_at stamped. 409 on re-accept /
already-rejected. 400 on unknown category_id.
* POST /{id}/reject — flips to status=rejected. 409 on re-reject.
- /api/v1/accounts/me/preferences (GET/PATCH) — thin wrapper over
AccountSettings.get_setting/set_setting. PATCH merges keys into the
JSONB column, preserving existing keys the client didn't touch.
Used by the "Don't ask again for this team" checkbox
(templatize_prompt_enabled=false) and, forward-looking, by
cw_resolved_status_id / cw_escalated_status_id from Phase 4.
- 13 tests: list filter, accept with/without edited_body, provenance
copy-through, reject, 409 on re-accept / re-reject, 400 on unknown
category, prefs round-trip with merge semantics.
Frontend:
- src/components/pilot/script/TemplatizePrompt.tsx — modal showing the
drafted script with proposed parameters in the Phase 5
ParameterizationPreview, editable name/category/description, an
individual-parameter remove button, and the "don't ask again" opt-out.
Accept posts to /draft-templates/{id}/accept + optionally PATCHes
preferences. Skip posts /reject.
- src/api/draftTemplates.ts — typed client plus accountPreferencesApi.
- AssistantChatPage: after a successful Resolve (external OR local),
fetches preferences + pending drafts for the session and queues the
modal one draft at a time. Escalate does not trigger this flow.
- Sidebar: Scripts nav shows the pending-draft count as a badge. Fetched
independently of the main sidebar stats so endpoint flakes don't
break the rest of the sidebar.
Verified live 2026-04-22: seed two drafts → GET sees both pending →
accept draft A (template created, provenance CW #99123 populated) →
reject draft B → pending count drops → PATCH opt-out → GET confirms
persistence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: sidebar showed "User mjones got locked out … 0 messages" but the
conversation pane was rendering 2 messages from a different chat. The
task lane content matched what was displayed (so the AI was fine post-
prompt-sweep) — the leak was purely UI: messages from the previous chat
stayed on screen until the new chat's getSession returned.
selectChat resetSessionDerivedState() then awaits getSession before
calling setMessages(detail.conversation_messages). Between the reset
and that await, the prior chat's messages remain visible. handleNewChat
already had an explicit setMessages([]) call so it was unaffected;
selectChat did not.
Folded setMessages([]) into resetSessionDerivedState so any new chat-
switch entry point gets the wipe for free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "AI parrots example content from system prompt" bug bit us twice in
one day across two different prompt sites. Patching individual prompts
is treating the symptom; this commit makes the rule structural.
Audit + sanitize:
- assistant_chat_service.ASSISTANT_SYSTEM_PROMPT — already cleaned in
prior commits, but the [FORK] schema still had literal "Brief reason"
/ "Short name" / "One sentence" placeholders. Replaced with
<angle-bracket> placeholders. Anti-parrot rule itself rewritten to
describe the failure mode abstractly instead of naming "jsmith" so
the rule no longer trips the guardrail (and so the model doesn't
see "jsmith" as a token at all).
- ai_chat_service.py — removed three concrete-example offenders:
"Get-Service ADSync" command literal, the "DC01 server_name" intake
form payload (in two places), and the inline interview demos using
"Azure AD Sync failures" / "Exchange Online mailbox migration".
Replaced with technology-neutral schema descriptions.
- ai_tree_generator_service.BRANCH_DETAIL_SYSTEM_PROMPT — replaced the
fully-fleshed DNS troubleshooting tree (with literal Dnscache /
ipconfig / google.com / Start-Service) with a placeholder schema
showing only ID-linkage shape.
- kb_conversion_service.PROCEDURAL_SYSTEM_PROMPT — replaced the worked
Server Manager + DC01 example payload with a placeholder schema.
Guardrail (tests/test_prompt_anti_parrot.py):
- Imports every module under app/services/ and app/core/ and walks
every uppercase string constant ending in _PROMPT, _SCHEMA,
_PROTOCOL, _FORMAT, or _CONTEXT.
- test 1: known-leaked-token list (jsmith, DC01, ADSync, Dnscache,
google.com, "Outlook keeps", "Teams drops") must not appear in any
prompt constant. Add to the list when a new leak shows up in prod —
the list IS the audit trail.
- test 2: marker blocks ([QUESTIONS], [ACTIONS], [SUGGEST_FIX], etc.)
must contain placeholders only. Distinguishes JSON keys (followed
by ':', allowed) from JSON values (followed by ',' / ']' / '}',
must be <placeholder>); allows pipe-separated enum types
(text|password|select) and a small set of fixed enum values
(question, diagnostic_check, decision, action, ...). Verified by
feeding the test a known-bad block — caught it correctly.
Documented the rule in CLAUDE.md → AI / FlowPilot lessons, naming
the test as the enforcement point so future contributors know how to
extend it (add to the known-leaked list when a new leak surfaces).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The system prompt had a "Complete example of a correct first response"
section with a specific Outlook/WiFi/jsmith scenario plus literal JSON
payloads in [QUESTIONS], [ACTIONS], [SUGGEST_FIX], and [PROMOTE]
markers. The model was emitting those literal strings (the same
WiFi/laptop questions, the same "Clear cached credentials" suggested
fix, the same "OWA login confirmed for jsmith" promote) on EVERY
unrelated chat — making the task lane look like it was leaking previous-
session data when in fact the AI was just reciting the prompt examples.
Replaced literal example content with `<placeholder>` schemas. Added an
explicit ANTI-PARROT RULE in the FINAL REMINDER section calling out
that the angle-bracket placeholders show SHAPE, not CONTENT, with
concrete examples of the failure mode (printer ticket → don't ask
about Outlook; user not named jsmith → don't name jsmith).
Same scrub applied to the FORK section's "Outlook AND Teams dropping"
and the worked fork-flow example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes from the Phase 5 shakedown:
1. Stale lane data leaking across chats. handleNewChat, sendPrefill, and
handleResumeNew were each missed when Phase 3/5 added activeFix,
previewKind, previewData, and scriptPanelOpen — only selectChat reset
the full set. Result: starting a new chat while a Suggested Fix card
was active showed the previous session's fix card (and any open
preview/script panel) until the next backend refresh swept it.
Consolidated all four entry points behind a single
resetSessionDerivedState() helper so adding new lane state in future
phases only requires touching one place.
2. CommandPalette TDZ on cold load. SCRIPTS_INLINE_QUICK_ACTION (line 66)
referenced PILOT_INLINE_SCRIPT_PATH declared at line 94 — module-level
evaluation hit the use before the declaration. Browser blanked with
"Cannot access 'PILOT_INLINE_SCRIPT_PATH' before initialization".
Moved the path const above its first use; also extracted
PILOT_INLINE_SCRIPT_EVENT into a tiny @/lib/pilotEvents module so
AssistantChatPage doesn't import the palette component just to read a
string — that mixed-export pattern broke Fast Refresh ("consistent
components exports") and added an unnecessary import edge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the SuggestedFix card to an inline panel that handles both cases:
template-matched fixes open the Script Library generator with parameters
pre-filled from session context; un-matched fixes open the three-option
dialog (one_off / draft_template / build_template). The decision endpoint
records the path choice with side effects: draft_template persists a
draft_templates row via a Sonnet-driven TemplateExtractionService;
build_template returns a redirect to the Script Builder; one_off just
records the choice.
Backend:
- TemplateExtractionService: drafts a parameter schema from a concrete
rendered script. Conservative by default ("prefer fewer parameters").
Round-trip-validates that templated_body only references declared
parameters; missing-key mismatch falls back to the original script
with no params. LLM/parse failures fall back identically — the
engineer can still create a draft and refine in the post-resolve
prompt (Phase 6).
- /suggested-fixes/{fix_id}/decision side effects:
* one_off → returns rendered_script (engineer's edited version or the
fix's ai_drafted_script verbatim)
* draft_template → same + creates draft_templates row with extracted
params, returns draft_template_id
* build_template → returns redirect_path=/scripts/builder?from_session=
&fix= so the frontend can navigate to the builder pre-loaded
- 400 when a non-template fix has no ai_drafted_script (template-matched
fixes take the dedicated /scripts/generate path, not this endpoint).
- 12 tests: TemplateExtractionService parse + fallback paths, all four
decision branches, edited_script override, missing-script 400.
Frontend:
- src/components/pilot/script/{TemplateMatchPanel, NoTemplateDialog,
ParameterizationPreview}.tsx — inline panels rendered in the task
lane's bottom slot when the engineer clicks a SuggestedFix card.
- TemplateMatchPanel: loads template via /scripts/templates/{id},
pre-fills params from fix.ai_drafted_parameters with cyan "from
session" tags, generates via existing /scripts/generate (already
bumps state_version on ai_session_id from Phase 3). 404 falls back
with a clear message instead of erroring.
- NoTemplateDialog: shows the AI-drafted script with proposed parameter
values highlighted in amber via ParameterizationPreview; three option
cards with the middle (draft_template) flagged Recommended; inline
edit on the script body before deciding.
- SuggestedFix card now clickable: onActivate toggles the inline panel.
- AssistantChatPage: scriptPanelOpen state + handleScriptDecision that
navigates on build_template and toasts on the other paths. Active fix
changes auto-close the panel so engineers don't act on stale state.
- Cmd+K → "Open inline Script Generator" palette entry surfaces only on
/pilot/:id routes; fires a window event the chat page subscribes to.
No Resolve shortcut added per Section 14 decision (browser ⌘R conflict).
Verified 2026-04-22 against the dev stack:
- one_off / draft_template / build_template all return the right shape
with real Sonnet TemplateExtractionService for the draft path.
- Conservative extraction confirmed: cmdkey + Restart-Process script
yielded zero proposed parameters as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the preview popover's Confirm & post action to ConnectWise (and,
via the provider pattern, any future PSA). Adds the parallel Escalate
flow with the handoff-oriented five-section markdown. Sessions without a
linked PSA ticket resolve/escalate locally — markdown stored, status
flipped, nothing posted externally.
Backend:
- EscalationPackageGeneratorService: Sonnet, five sections (Problem /
What we've confirmed / What we've tried / Current hypothesis /
Suggested next steps). Shares the preview_cache with a separate KIND
so Resolve and Escalate previews for the same state coexist.
- PSAWritebackService: post_resolution_note (RESOLUTION note type,
customer-visible), post_escalation_package (INTERNAL_ANALYSIS,
handoff for the next engineer only), transition_ticket_status with
mandatory re-fetch verification. PSAStatusVerificationError surfaces
loudly when CW silently rejects a status change — the
ConnectWise anti-pattern CLAUDE.md flags.
- Endpoints:
* POST /ai-sessions/{id}/escalation-package/preview
* POST /ai-sessions/{id}/resolution-note/post
* POST /ai-sessions/{id}/escalation-package/post
Outcomes: "resolved" / "escalated" with external_id + verified status,
"resolved_local" / "escalated_local" when no PSA linked.
- Target CW status IDs live in account_settings.preferences
(cw_resolved_status_id, cw_escalated_status_id). When unset, the post
proceeds without a status transition — response includes a
status_transition_skipped_reason rather than silently erroring.
- 7 tests: local-only path, PSA happy path with verified transition,
status verification failure → 502, skipped transition when
unconfigured, 409 on already-resolved re-post, escalate parallel path,
internal-analysis note type enforced.
Frontend:
- ResolutionNotePreview now kind-parameterized ('resolve' | 'escalate')
with inline edit + Confirm & post. Preview loads from the matching
backend endpoint; posting calls the matching endpoint; outcome toast
surfaces the verified CW status or the local-only result.
- AssistantChatPage: previewKind state replaces previewOpen; two toggle
buttons (Preview Resolve note / Escalate instead) in the lane's bottom
slot. handleConfirmPost dispatches by kind.
Verified 2026-04-22:
- Local-only Resolve + Escalate round-trip against the dev stack.
- Live Sonnet escalation-package preview; cache hit on repeat call
with no state change (separate cache kind from resolution-note).
- PSA post + status-verification paths covered by mocked-provider pytest
cases. Live CW round-trip pending a test CW instance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refreshSessionDerived's dep array referenced refreshActiveFix and
schedulePreviewRefresh before they were declared. React evaluates
useCallback deps synchronously during render, so the page blew up with
"Cannot access 'refreshActiveFix' before initialization" before a single
render completed. Moved the three leaf helpers above the aggregator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the AI-proposed resolution path and the inline preview of the
markdown that will be posted to the customer ticket on Resolve. The
preview is keyed on (session_id, ai_sessions.state_version) so back-to-
back fetches against unchanged state hit an in-process cache instead
of paying for a Sonnet call.
Backend:
- preview_cache: in-process LRU keyed on (kind, session_id, state_version).
No TTL — state_version is the source of truth. Soft-cap 5000 entries.
- unified_chat_service: [SUGGEST_FIX] parser (last-block-wins, JSON
payload, confidence clamped 0-100), supersession persistence (sets
superseded_at on prior active row), atomic state_version bump.
- ResolutionNoteGeneratorService: pulls session, facts, active fix, and
redacted script_generations into a structured input bundle for Sonnet;
produces the four-section markdown (Problem / What we confirmed /
Root cause / Resolution). Sensitive script parameters redacted via
ScriptTemplateEngine.redact_sensitive driven by the template's
parameters_schema.
- /api/v1/ai-sessions/{id}/suggested-fixes/active — 200 with the active
fix or 404.
- /api/v1/ai-sessions/{id}/suggested-fixes/{fix_id}/decision — records
one_off / draft_template / build_template / dismissed; dismiss
supersedes; bumps state_version. 409 on dismissing an already-
superseded fix.
- /api/v1/ai-sessions/{id}/resolution-note/preview — generates or returns
cached markdown; from_cache flag in payload signals cache hit.
- scripts.py POST /generate now bumps state_version on the linked
ai_session_id when present (third source of preview-cache invalidation
per Section 5.5).
- ASSISTANT_SYSTEM_PROMPT documents [SUGGEST_FIX] (when to/not to emit,
format, supersession semantics).
- 12 tests covering the parser (well-formed, last-wins, malformed,
confidence clamping), supersession + state_version invariant, all
decision branches, preview cache hit-on-no-change + miss-after-write.
Frontend:
- src/components/pilot/sections/SuggestedFix.tsx — amber-accented card
with confidence badge; dismiss action wired to the decision endpoint.
- src/components/pilot/ResolutionNotePreview.tsx — popover with refresh,
loading state, cached/fresh indicator, ticket-ref display.
- src/api/sessionSuggestedFixes.ts — typed client; getActive normalizes
404 to null so callers don't have to special-case.
- TaskLane gains suggestedFixSlot + bottomSlot props (rendered after
Diagnostic Checks; bottomSlot anchors the Resolve action).
- AssistantChatPage: refreshSessionDerived helper batches fact + fix
refresh; fact mutations and chat sends both schedule a 500ms-debounced
preview refresh per the Section 5.5 spec.
Verified end-to-end against the dev stack with a real Sonnet call:
- /active 404 → fact create → preview generates four-section markdown
grounded only in provided facts → second preview call hits cache
(from_cache=true, no LLM call) → fact write 2 → cache miss, regenerates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the load-bearing structural feature of the FlowPilot migration: a
"What we know" panel that holds confirmed facts for a session, fed by AI
[PROMOTE] markers and engineer-added notes. Facts feed the resolution
note preview (Phase 3) and survive across turns via stable UUIDs assigned
to pending_task_lane items.
Backend:
- FactSynthesisService: create/update/soft-delete facts with atomic
state_version bumps; LLM-backed synthesize_from_question/check on the
fact_synthesis (Haiku) action tier per Section 6.6.
- /api/v1/ai-sessions/{id}/facts CRUD + /facts/promote (proposed_text or
via synthesis). PATCH returns 403 for question/diagnostic_check facts
(edit the source item instead, Section 7.3).
- unified_chat_service: [PROMOTE] marker parser (JSON-block per Section
8.1 spec drift note), stable-UUID assignment for pending_task_lane
questions/actions preserved by exact text/label match across turns.
- ASSISTANT_SYSTEM_PROMPT: documents [PROMOTE] format, when to/not to
emit, hallucination guardrails, source_ref handling.
- 17 tests covering parser, stable IDs, service validation, CRUD,
editability rule, both promote modes, 422 null-synthesis path,
state_version invariant.
Frontend:
- src/components/pilot/sections/{WhatWeKnow,WhatWeKnowItem,AddNoteButton}
— green-gradient section above Questions, dashed-circle check, inline
edit/delete gated by the server's editable flag.
- TaskLane gains a whatWeKnowSlot prop (existing assistant/ folder kept
per the doc's "rename is opportunistic" guidance).
- AssistantChatPage fetches facts on selectChat and refetches after each
chat send (so [PROMOTE]-synthesized facts appear immediately); auto-
opens the lane when facts exist.
Verification: end-to-end smoke against the local docker stack confirms
all five endpoints (list/create/patch/delete/promote) plus the 403
editability rule. pytest suite verifies the same with mocked LLM. Live
[PROMOTE] flow remains untested until used in the UI — the marker shape
is covered by parser tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gate 1 complete on Proxmox dev host (docker-01):
- Alembic at f07010f17b01 (single head); downgrade/upgrade roundtrip clean.
- Phase 0 prompt-cache verified: direct provider probe shows
cache_create=5398 → cache_read=5398 across two calls; chat path emitted
two anthropic.cache events 55s apart on a real FlowPilot session.
- Frontend npm run build clean (57.63s, no TS errors, no stale
FlowPilotSessionPage imports).
- /assistant/:id → /pilot/:id redirect fires correctly and session detail
loads (GET /api/v1/ai-sessions/<id> 200); a blank-until-click UX polish
will be tracked separately.
- Dashboard session-tile dispatcher routes to /pilot/:id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docker-compose.dev.yml: drop Traefik/dev.resolutionflow.com labels, expose
backend:8000 and frontend:5173 directly; swap relative bind mounts for
${REPO_ROOT}/... so compose works when driven from inside a code-server
container with the host Docker socket mounted; default POSTGRES_PORT to
5433 host-side; add explicit uvicorn/npm run dev commands; add
ENABLE_MCP_MICROSOFT_LEARN and docker-01/Tailscale CORS origins.
- frontend/vite.config.ts: replace dev.resolutionflow.com with
allowedHosts=['docker-01', '.ts.net', 'localhost'] for direct-port access
over the private network.
- DEV-ENV.md: add Section 11 reference topology for the homelab Proxmox +
code-server Option B setup, plus troubleshooting entries for the
REPO_ROOT-empty-mount trap and the Vite allowedHosts rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trim from 570 → 264 lines. Archived lessons and fixes-in-code remain in
docs/LESSONS-ARCHIVE.md; CLAUDE.md now only carries what a fresh session
can't derive from the repo state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained status snapshot for picking up Phase 0 + Phase 1 work
after the Proxmox dev-environment move. Lists what is done, what is
owed (the Gate 1 verification checklist), known drift, and the
recommended order of operations after the move.
Explicitly ephemeral — the doc instructs the reader to delete it once
Gate 1 verification has passed. Durable dev-env setup lives in
DEV-ENV.md; this file covers only the "where is the work right now"
handoff for this specific migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version was tightly coupled to the Hostinger VPS at
46.202.92.250 — hardcoded IP, Traefik/Let's-Encrypt assumption,
specific Docker-volume paths. Rewriting ahead of the Proxmox migration
so a fresh clone on any Linux host (LXC, VM, bare metal, VPS) can
stand up a working dev environment without pre-baked assumptions about
topology.
Structural changes:
- Introduces Option A (all-in-one host) / Option B (Docker Compose) /
Option C (split services) topology choice up front, so readers
commit to one shape before touching commands.
- Adds a "per-host configuration" template the reader fills in once
(DEV_HOST, POSTGRES_PORT, SECRET_KEY, API keys), referenced by name
throughout the rest of the doc. No more hardcoded IPs.
- Adds an explicit verification section (Section 6) with concrete
expected outcomes: alembic head, reversibility, prompt-cache hit,
frontend build, /assistant→/pilot redirect, dispatcher routing, CORS.
- References the Phase 0 TODO(phase0-verify) in ai_provider.py and
the expected alembic head (f07010f17b01) as of the current branch.
- Adds a troubleshooting section pulling in CLAUDE.md lessons that
bite people repeatedly: stale Vite env vars, RLS policy violations,
EACCES on dist/, multi-head alembic state, invisible cache misses.
- Documents the structured log events the backend emits
(anthropic.cache, mcp.turn, mcp.fallback) so readers know what to
grep for during verification.
Deliberately excluded:
- Production deployment (lives in CLAUDE.md Deployment section).
- Reverse-proxy configuration (whatever the reader prefers).
- code-server install specifics (Docker vs LXC vs native is reader's
choice; once running, this doc applies).
- Proxmox-specific instructions — the doc is host-agnostic so it
survives the next migration as well.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses the pre-existing dual-surface setup (AssistantChatPage at /assistant,
FlowPilotSessionPage at /pilot) into a single chat-primary surface per
architectural claim #1 of FLOWPILOT-MIGRATION.md.
Router changes (frontend/src/router.tsx):
- /pilot and /pilot/:sessionId now render AssistantChatPage.
- /assistant redirects permanently to /pilot via <Navigate replace>.
- /assistant/:sessionId redirects to /pilot/:sessionId preserving the ID
via an AssistantSessionRedirect helper that reads the param.
- FlowPilotSessionPage is no longer imported or mounted. Per the
beta-history-disposable decision, the file stays on disk for reference
but is unreachable; delete once nothing else in the tree imports it.
Dispatcher de-branching — previously these sites routed by session_type
(chat -> /assistant, otherwise -> /pilot). All now unconditionally go to
/pilot/:id since session_type is no longer used for frontend routing:
- components/dashboard/ActiveFlowPilotSessions.tsx
- components/dashboard/RecentFlowPilotSessions.tsx
- components/flowpilot/AISessionListItem.tsx
(keeps isChat for icon selection, but linkTo is unconditional)
User-facing label + navigation updates:
- components/layout/CommandPalette.tsx: "AI Assistant" palette entry
becomes "FlowPilot" pointing to /pilot; the sparkles quick-action also
routes to /pilot.
- components/dashboard/StartSessionInput.tsx: both navigate() call sites
now go to /pilot instead of /assistant.
- lib/routePrefetch.ts: prefetch entry for AssistantChatPage keyed to
/pilot (the real surface) rather than /assistant (now redirect-only).
Preserved intentionally (not user-facing routes):
- Backend /assistant/retention API path and the assistantChatApi module
name — those are internal API and module identifiers, not SPA routes.
- src/components/assistant/* and src/types/assistant-chat — TypeScript
module paths, not routes.
- Sidebar.tsx — no top-level AI entry existed to rename; /pilot is
already in the History group's matchPaths. Whether FlowPilot deserves
its own rail entry is a future UX decision, not Phase 1 scope.
- FlowPilotAnalyticsPage at /analytics/flowpilot — analytics for the
unified product, not guided-only, per the agreed Q16 interpretation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backs the schema added in 210d310 with SQLAlchemy 2.0 models.
- SessionFact: "What we know" facts with polymorphic source_ref pointing
at task-lane item UUIDs inside ai_sessions.pending_task_lane (not a FK
per Section 4.2).
- SessionSuggestedFix: AI-proposed resolutions with supersession tracking
and the full user_decision state machine.
- DraftTemplate: post-resolve templatization queue with promotion to
script_templates.
- AccountSettings: per-account JSONB preferences grab-bag with async
classmethod helpers — get_setting(db, account_id, key, default) reads
without creating, set_setting(db, account_id, key, value) upserts via
Postgres ON CONFLICT + jsonb `||` merge so existing keys are preserved.
Lazy row creation matches the Phase 1 design.
Column additions on existing models to mirror the migration:
- AISession: resolution_note_* / escalation_package_* / state_version
(the preview-cache-invalidation counter consumed by Phase 3).
- ScriptTemplate: source_session_id / source_user_id / source_ticket_ref
(provenance for templates promoted from DraftTemplate).
All four new models registered in app.models.__init__ and __all__.
TYPE_CHECKING-guarded relationship imports throughout, matching the
repo's existing model style.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the backing store for the FlowPilot unified session surface, per
the FLOWPILOT-MIGRATION.md Phase 1 deliverable. Descends from production
head 074 (add_network_diagrams_table).
New tables (all tenant-scoped, all RLS-enabled + forced):
- session_facts — "What we know" facts. source_ref is a polymorphic
pointer to a task-lane item inside ai_sessions.pending_task_lane
(no DB-level FK; integrity enforced at service layer per Section 4.2
of the design doc). Soft-delete via deleted_at; active-facts partial
index excludes deleted rows.
- session_suggested_fixes — AI-proposed resolutions. One active per
session at a time (supersession tracked via superseded_at; partial
index on (session_id) WHERE superseded_at IS NULL powers the
"find active fix" query).
- draft_templates — scripts pending post-resolve templatization.
Partial index on (account_id) WHERE status='pending' supports the
"N scripts ready to review" Script Library badge.
- account_settings — new per-account table with JSONB preferences
grab-bag. Rows created lazily on first write; get_setting returns
default when no row exists.
Column additions on ai_sessions:
- resolution_note_markdown / posted_at / external_id
- escalation_package_markdown / posted_at / external_id
- state_version (INTEGER NOT NULL DEFAULT 0) — incremented atomically
by any write that invalidates the resolution note preview cache
per Section 5.5. Phase 3 consumes this.
Column additions on script_templates:
- source_session_id, source_user_id, source_ticket_ref — powers the
"generated from CW #X · resolved by Y · used N times" provenance
chip in the Script Library.
RLS pattern matches the repo convention (074 / network_diagrams is the
nearest template): ENABLE + FORCE, USING + WITH CHECK on
`account_id = app.current_account_id`. Downgrade is reversible —
drops in the inverse order of creation so FK dependencies unwind.
No runtime verification from code-server; migration apply + downgrade
will be verified on the new dev environment per the standing deferral.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Significant rewrite of FLOWPILOT-MIGRATION.md after post-Codex plan review
and the Phase 0 in-flight audit. Archives the pre-rewrite version as
FLOWPILOT-MIGRATION-v1.md and keeps the Codex review under
CODEX-FlowAssist-Migration-PLAN.md for traceability.
Substantive changes that affect implementation:
- Section 0.1 adds a spec-drift note listing corrections integrated into
this revision (API namespace, task-lane item UUIDs, account_settings
creation, missing /tickets/ai-parse endpoint).
- Section 2 adds "Task lane item ID" terminology — stable UUID assigned
to items inside ai_sessions.pending_task_lane so session_facts.source_ref
has something reliable to point to.
- Section 4.1 adds ai_sessions.state_version (INTEGER NOT NULL DEFAULT 0)
and escalation_package_external_id. state_version drives preview cache
invalidation; incremented atomically on writes to facts / suggested
fixes / script_generations.
- Section 4.6 creates account_settings as a new table with JSONB
preferences column, lazy row creation, and a promotion rule for when a
setting should graduate to a typed column.
- Section 5 namespaces all session-scoped routes under
/api/v1/ai-sessions/{id}/... to match the existing codebase pattern.
- Section 5.5 documents the preview caching strategy (state_version
keyed, 500ms client debounce, Redis planned).
- Section 6.6 adds per-service MCP capability flags alongside the model
tier flags.
- Section 7.1 makes the /assistant -> /pilot redirect include the
session-deep-link path and preserve the session ID.
- Section 8.2 adds supersession semantics for [SUGGEST_FIX] markers.
- Section 9 Phase 1 now explicitly includes account_settings and
state_version; Phase 3 uses state_version-keyed caching; Phase 5
mentions MCP inheritance via chat_call_cached wrapper.
- Section 11 adds a dedicated test plan (migrations, backend, frontend,
manual QA).
- Section 14 captures the eight planning decisions made during the
Phase 0 conversation so they are traceable.
No code changes in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the chat caller to a name that signals its actual purpose, and
factors the reusable cached-system-block + cached-history + cache-usage-log
primitives out to app.core.ai_provider so they can be shared with the
provider-generic path without pulling MCP/beta/images into the abstract
interface.
Helpers added to ai_provider.py:
- `build_anthropic_chat_messages(history, new_message, images, format_reminder)`
— owns: copy history, apply cache_control to last history message,
append format reminder to new message, render images as multimodal blocks.
Anthropic-shaped by design; do not call from Gemini paths.
chat_call_cached keeps exactly the concerns that are unique to the one
MCP/beta/multimodal chat caller:
- Anthropic beta endpoint invocation
- Microsoft Learn MCP server wiring (ENABLE_MCP_MICROSOFT_LEARN)
- Retry-without-MCP fallback
- Format-reminder content string (declared as module constant)
- Phase 0.5 telemetry (mcp.turn, mcp.fallback)
Documents in the module docstring AND at the function site that this is
the ONE MCP/beta chat caller and should not become the general provider
path. MCP/beta/images are features of exactly one optional Anthropic beta
endpoint; routing them through AnthropicProvider would leak a provider-
specific concern into the abstract interface that also serves Gemini.
Behavior change: chat_call_cached now reuses the singleton AnthropicProvider
HTTP client via `_get_anthropic_client(...)` instead of instantiating a new
`anthropic.AsyncAnthropic(...)` per call. Matches the provider's own pattern
and avoids burning connections per-turn. No user-visible difference.
No runtime verification from code-server. TODO(phase0-verify) in
ai_provider.py tracks the cache-hit verification owed on the new dev env.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps each static system prompt in a single-block list so Phase 0.1's
AnthropicProvider applies cache_control: ephemeral automatically (policy α,
first block gets marked when no caller-authored cache_control is present).
Call sites:
- ai_tree_generator.scaffold_branches: SCAFFOLD_SYSTEM_PROMPT (~1k tokens)
- ai_tree_generator.generate_branch_detail: BRANCH_DETAIL_SYSTEM_PROMPT
(~2.5k tokens with few-shot example); retries inside the same function
re-read the cached block instead of paying full input cost on each attempt
- kb_conversion.convert_document: TROUBLESHOOTING or PROCEDURAL prompt
(each caches independently by text content)
- ai_fix.generate_fixes: FIX_SYSTEM_PROMPT on first attempt + corrective retry
- script_builder.send_message: SYSTEM_PROMPT_TEMPLATE (per-session language
substitution — same-language sessions share cache entries)
Each edit includes an inline comment explaining why the block is cacheable
(stable-constant, retry-reuse, per-language variant) so a future dev can
see the intent at the cache_control marker site.
script_builder history caching deliberately deferred — per Phase 0.1
decision (option i), AnthropicProvider does not automatically cache the
message list. If script_builder's growing 20-message history turns out
to be a visible cost driver via the anthropic.cache telemetry, route
that caller through the 0.4 chat wrapper which handles history caching.
No runtime verification from code-server; cache-hit behavior will be
confirmed against the new dev environment when it's up, per the inline
TODO(phase0-verify) in ai_provider.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /tickets/ai-parse endpoint named in Phase 0.2 does not exist in the
codebase (verified: zero matches for ai-parse/ai_parse across endpoints,
services, models, and all branches/commit messages). integrations.py:557
is get_ticket_statuses — a CW passthrough with no AI call.
Adding a block-quoted note under the 0.2 deliverable that flags the
drift, records the cached-system-block pattern to apply when the endpoint
is built, and instructs the next editor to remove the note once applied.
No implementation change this commit — guidance only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Widens AIProvider.generate_json / generate_text / generate_text_stream
signatures to accept `system_prompt: str | list[SystemBlock]`:
- `str` (the existing call shape): passes through uncached, unchanged
behavior. Every existing caller stays on the uncached path — no silent
behavior change.
- `list[SystemBlock]`: enables Anthropic prompt caching via structured
system blocks. Caller-authored `cache_control` is honored verbatim
(policy α); if no block carries it, the provider applies
`cache_control: {"type": "ephemeral"}` to the first block only.
Gemini ignores cache_control and concatenates list entries into one
system string — the widened signature is strictly additive on that path.
Adds `anthropic.cache` structured-log telemetry: on every Anthropic
response (streaming included, via `stream.get_final_message()`), logs
`cache_read_input_tokens` and `cache_creation_input_tokens`. Telemetry
failure in streaming is swallowed so the user-facing stream never breaks.
Verification deferred: cannot run from code-server (no Python, no DB,
no dev env). TODO(phase0-verify) left inline in the module docstring.
First verification task on the new dev environment is to hit any
FlowPilot endpoint twice within 5 minutes and confirm the second call
shows cache_read_input_tokens > 0 in the `anthropic.cache` log event.
If verification fails, that's a debug task on the new env — not a
blocker for continuing Phase 0.2/0.3/0.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emits structured `mcp.turn` log events on every Anthropic-path chat turn,
capturing whether MCP was wired in (mcp_available), whether the model
actually invoked an MCP tool (mcp_invoked), which tool names fired,
and whether the silent retry-without-MCP fallback was triggered.
Adds a separate `mcp.fallback` event with error type/message for
fallback occurrences.
Establishes baseline data for deciding whether MCP investment is earning
its keep before Phase 2+ expands the product footprint. Scope: the one
MCP-using code path (`_call_anthropic_cached`) — not a general
instrumentation layer.
No new dependencies, no schema changes, no behavior change. Standard
library `logging` is the sink; PostHog is not wired on the backend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the locked FlowPilot migration design onto the branch that will
implement it. Includes the annotated target UI mockups (primary session
view + three Script Generator integration states) and the superseded
FLOWPILOT-AND-RESOLUTIONASSIST.md for historical reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous implementation PATCHed the `resources` string directly, which CW
silently ignores because `resources` is a server-derived read-only field (it's
populated from schedule entries of type/id=4, not freely writable).
Per CW docs (openapi line 70949): "Please use the
/schedule/entries?conditions=type/id=4 AND objectId={id} endpoint".
Behavior per spec:
- No owner + assign user → set owner (existing behavior kept)
- Has owner + assign different user → POST /schedule/entries with type/id=4,
member, objectId; owner untouched
- User already assigned (owner or schedule entry) → idempotent no-op
- Remove owner → clear owner (existing behavior kept)
- Remove co-assignee → DELETE /schedule/entries/{entry_id}
- list_resources now merges owner + schedule-entry members, deduped by id
Required CW security role permission on the API member:
- Service > Resource Scheduling > Add/Inquire/Delete
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous `resources`-string PATCH was silently ignored by CW — the
`resources` field is server-derived from the ticket's owner + schedule
entries, not freely writable. Status PATCH could also silently no-op
when a cross-board status id was sent.
- add_resource: when the ticket is unassigned, set the `owner`
MemberReference (the canonical writable primary-assignee field).
If already owned by someone else, append the identifier to the
`resources` co-assignee string best-effort.
- remove_resource: clear `owner` (with remove→replace:null fallback) if
the target is the current owner, otherwise strip from `resources`.
- list_resources: merge owner + resources string, deduped by member id,
so the UI reflects both single-owner and multi-resource assignments.
- update_ticket_status: verify CW applied the status by comparing the
response body's status.id — raises PSAError with a clear message when
CW silently rejects the change (e.g., status invalid for ticket's
board), instead of reporting spurious success.
- Frontend: surface the backend error detail in the toast so users see
the real reason instead of a generic "Failed to update" message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status update was returning only new_status (string) and the parent list's
onStatusUpdated only set status_name. The <select> was bound to status_id,
which never changed — so it visually reverted to the old status even though
the PATCH succeeded.
- Backend: include new_status_id in the status-update response.
- Panel: own currentStatusId/currentStatusName state so the select reflects
the change immediately and survives stale parent snapshots.
- Parent list: update status_id on both the row and selectedTicket so the
list row stays in sync when the panel stays open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Status filter: aggregate statuses across all boards (deduped by name)
when no board is selected. Backend accepts status_name and filters by
status/name so the same status matches across boards.
- Resource assignment: CW has no /service/tickets/{id}/members endpoint —
assignees live in the ticket's comma-separated `resources` string field.
Rewrote list/add/remove to read/PATCH that field via member identifier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Apply company_id filter in CW search_tickets conditions (was silently ignored)
- Sanitize query string to strip single quotes before CW condition interpolation
- Add psaError state to TicketsPage for permissions error surfacing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GET /boards/{board_id}/statuses endpoint — direct board-to-statuses lookup
without ticket roundabout; used by filter bar and new ticket form
- Fix TicketsPage and NewTicketModal to call getBoardStatuses(board_id) instead
of misusing getTicketStatuses(ticket_id) with a board_id value
- Fix list_members auth: was require_account_owner (owner/super_admin only) —
changed to require_engineer_or_admin so engineers can see member list for
ticket assignment
- list_members: return [] on PSAError instead of 502 (Lesson 111 pattern)
- get_ticket_statuses: return [] on PSAError instead of 502
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- list_resources: return [] on PSAError instead of 502 — stops global interceptor
toast when CW API key lacks ticket members permission (Lesson 111)
- list_boards/list_priorities: add warning logging so Railway logs reveal the
root cause when CW permissions are missing
- TicketsPage: derive board options from ticket search results when listBoards
returns empty (CW permissions fallback)
- TicketFilterBar: replace assignment <select> with searchable member picker —
fixed options (All/Mine/Unassigned) + text-filtered member dropdown
- TicketQueue: remove Load More / infinite scroll; page now exists at /tickets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>