Backend suite is the slow gate (1076 passed locally in 22m27s on
fix/ci-workflow-config). Adding pytest-xdist with per-worker DB
isolation drops it to ~4m20s on the 8-core homelab runner. Verified
locally: `pytest -n auto --no-cov` finished in 4m28s real time
(15m19s user — confirms ~5× parallelism).
How it works:
- conftest.py reads `PYTEST_XDIST_WORKER` (set per worker by xdist —
'gw0', 'gw1', …). When set, derives a per-worker DB URL like
`…/resolutionflow_test_gw0`. The base DB stays for serial / master
runs.
- `_ensure_worker_db_exists` runs synchronously at conftest import,
connects to the postgres maintenance DB, and `CREATE DATABASE`s the
worker-suffixed DB if it doesn't exist. Idempotent across runs.
- The "test" safety guard still applies — every worker DB name
contains "test" so the assertion holds.
- The per-test `DROP SCHEMA public CASCADE` now operates on the
worker's isolated DB, no cross-worker race.
CI workflow: backend job switches to `pytest -n auto`. Coverage still
collected (pytest-cov has built-in xdist support).
Adds `pytest-xdist==3.6.1` to requirements-dev.txt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three changes that get PR #150 to a green CI gate:
1. **test_record_decision_persists_and_bumps_state_version** — the
`decision: draft_template` path calls `_extract_template_parameters`
(TemplateExtractionService → AI provider). CI doesn't set
ANTHROPIC_API_KEY/GOOGLE_AI_API_KEY, so the endpoint raised
`RuntimeError: No AI provider configured` and returned 500. The test
isn't exercising the AI integration — patched the extractor with an
AsyncMock returning a minimal valid `{templated_body, parameters}`
dict. Verified locally: the test now passes.
2. **pip + npm caches** in backend, frontend, and e2e jobs. Keyed on
the hash of requirements*.txt / package-lock.json with a runner-os
restore-key fallback. Saves ~30-60s per run on cache hit.
3. **Pytest invocation tightened**:
- Dropped `--cov-report=term-missing` — the custom "Display coverage
summary" step below parses coverage.json and prints the same
module list more concisely. Term-missing dumps every uncovered
line which adds ~5-10s of stdout.
- Added `--maxfail=10` so a structural breakage (fixture explosion,
DB unreachable) bails after 10 errors instead of running the full
25-min suite. Tunable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Capture the backend pytest parallelization work so it survives session
end. Backend suite is currently ~22 min wall-clock for 1076 tests;
xdist with one-DB-per-worker should land in the 3-6 min range on the
homelab Gitea Actions runner.
Also queues two backlog items:
- Frontend lint warnings (23 react-hooks/exhaustive-deps after PR #149)
- Periodic audit of the ResourceWarning filterwarnings added by Codex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates HANDOFF.md, CURRENT_TASK.md, and SESSION_LOG.md so the next
session has accurate resume state. Summary of where things are:
- PR #141 (PSA tickets), PR #147 (FlowPilot Phase 1-9), PR #148 (CI
fixes part 1), PR #149 (CI fixes part 2) all merged to main in this
session.
- Branch protection enabled on main: PR-only, CI / frontend required.
- PR #150 (this branch) is the last CI-config PR — adds
DATABASE_TEST_URL to the workflow and pins upload-artifact to v3.
- Next session: watch #150's CI, merge if green, add CI / backend to
required checks, then start on the 54 real backend test failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two CI-config issues blocking the gate from going green:
1. **Backend tests connect to localhost instead of postgres service.**
conftest.py reads DATABASE_TEST_URL only — DATABASE_URL is intentionally
not consulted (per dab740d's test-DB-isolation hardening — running
pytest with DATABASE_URL set previously dropped the dev DB schema).
The CI workflow only sets DATABASE_URL, so conftest falls back to its
localhost default and every fixture-setup fails with
`OSError: Connect call failed ('127.0.0.1', 5432)` — observed as 638
errors on the latest main run.
Add DATABASE_TEST_URL pointing at the postgres service container.
Same connection string as DATABASE_URL — the test DB and the app DB
are the same physical postgres in CI; conftest's safety assertion is
satisfied by the URL containing "test".
2. **Frontend artifact upload fails on Gitea Actions runner.**
actions/upload-artifact@v4 (and v5) are not supported on Gitea
Actions / GHES — the runner returns
`GHESNotSupportedError: ... not currently supported on GHES`. Lint
itself is now passing (0 errors after PR #149); the job exits 1 only
because the upload step then fails.
Pin upload-artifact + download-artifact to v3, the latest version
compatible with Gitea Actions until they ship v4 support.
After this lands, both backend and frontend CI gates should turn
green — at which point we can also add backend to the required status
checks on main.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The test_db fixture calls Base.metadata.create_all on a fresh test DB.
That only creates tables for models that have been imported (and thus
registered with Base.metadata) by the time the fixture runs.
app.main imports app.core.database (which gives us Base) but does NOT
eagerly import the model modules — most are pulled in lazily inside
scheduler functions (archive_stale_ai_sessions etc.) and route
modules. At fixture-setup time, only the handful of models touched by
those eager imports are on the metadata, so any test that exercises
PSA, network diagrams, ratings, escalations, etc. fails with
\`UndefinedTableError: relation "X" does not exist\` and a cascade of
500s on every endpoint that queries the missing table.
Adding \`from app import models as _models\` (rather than the bare
\`import app.models\` which would shadow the \`app\` FastAPI instance
imported just above) pulls in app/models/__init__.py, which itself
imports every model module — registering all ~60 tables with
Base.metadata before create_all runs.
Verified locally: tests/test_psa_writeback_phase4.py went from
1 failed / 6 errors → 4 failed / 3 passed (the cascading errors were
masking the actual passes).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The new react-hooks lint rule "Calling setState synchronously within an
effect can trigger cascading renders" flagged real anti-patterns in
four spots. Refactored each per the rule's intent (derive during render,
or use useSyncExternalStore for external subscriptions).
1. hooks/useMediaQuery.ts — replaced the useState + useEffect pair with
useSyncExternalStore. That's the canonical React hook for
subscribing to external stores (matchMedia in this case) without
mirroring into local state via an effect. Snapshot/getServerSnapshot
pair preserves the SSR-safe behaviour.
2. components/network/nodes/DeviceNode.tsx — the prop-sync useEffect
that copied nodeData.label into labelValue was redundant.
labelValue is the EDIT BUFFER; while not editing, the displayed
span now reads nodeData.label directly. The buffer is initialized
only when an edit session starts (onDoubleClick).
3. components/network/nodes/GroupNode.tsx — same pattern, same fix.
4. components/dashboard/TicketQueue.tsx — the
setTickets([]) + setLoading(true) + fetchTickets() chain in the
effect was the cascade. Pushed those writes inside fetchTickets
(after the function boundary, so they batch with the eventual
setTickets(result)). Added a request-id ref so a slow first
response can't overwrite a fast second one.
Frontend lint: 20 errors → 0 errors. tsc -b clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five files, all stylistic:
- useFlowPilotSession.ts: typed the axios error shape with a narrow
inline type instead of \`as any\`.
- FlowPilotSessionPage.tsx: same — typed location.state once, then
destructured.
- ScriptBuilderTab.tsx: handleViewScript was a placeholder no-op;
declared the args properly with \`void script; void filename\` so the
signature matches ScriptBuilderChatProps without no-unused-vars
firing.
- TicketsPage.tsx: replaced 8 ternaries-as-statements (\`x ? f() : g()\`)
with proper if/else blocks. Same control flow, satisfies
no-unused-expressions, and reads better in the URL-param update paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
react-refresh/only-export-components fires when a file with the
\`router\` const export also defines a component (the redirect helper).
Moves the small helper to its own file under components/routing/ so
HMR can keep the route-component module hot-reload-eligible.
No behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pytest-asyncio==0.24.0 (added on the FlowPilot branch as part of the
RLS test infra refactor) declares pytest>=8.2 — but requirements-dev.txt
still pinned pytest==7.4.3, so a clean pip install fails with
ResolutionImpossible. CI runners that started from a fresh image would
have refused to install dev deps; the FlowPilot tests passed locally
only because the dev container had a pre-installed pytest 8.x lying
around.
pytest-cov 4.1.0 also needs >= 5.0 to play nicely with pytest 8.
No code changes — pytest 8 is API-compatible with the existing test
suite once the install resolves.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings PR #148 — two pre-existing CI fixes (network_diagrams JSONB
server_default, removed deprecated session-scoped event_loop fixture).
The conftest.py event_loop fix on main is already incorporated in
FlowPilot's b14a16a (RLS-gating commit, which dropped the same fixture
as part of its larger refactor). Kept HEAD's version of the RLS-gating
collection hook; the event_loop fixture removal is identical.
The network_diagram.py fix lands cleanly via auto-merge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. backend/app/models/network_diagram.py — `nodes` and `edges` columns
used `server_default="'[]'"` (a Python string), which SQLAlchemy
wraps in single quotes when generating DDL, producing
`JSONB DEFAULT '''[]'''` — invalid JSON. Switch to
`server_default=text("'[]'::jsonb")` so the literal is passed through
and the table can actually be created. Surfaced on every CI run as
`asyncpg.exceptions.InvalidTextRepresentationError: invalid input
syntax for type json` at fixture setup time, cascading hundreds of
test errors.
2. backend/tests/conftest.py — drop the deprecated session-scoped
`event_loop` fixture. Since pytest-asyncio 0.23+, the plugin manages
the loop itself; redefining it with a session scope but never
`set_event_loop()`-ing it left the loop dangling, so any test that
called `asyncio.run()` (e.g. `test_tasks_are_isolated`) closed the
process loop and broke the next async test in the module —
`test_require_tenant_context_raises_403_when_no_account` was the
visible casualty in the CI logs.
Verified locally:
- `pytest tests/test_uploads.py::test_upload_success` — was setup-error
on `network_diagrams` DDL; now passes.
- `pytest tests/test_tenant_context.py` — was 1 fail / 3 pass; now 4/4.
Both are real bugs, not test infrastructure churn. Pre-existing on
main; not introduced here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings in PR #141 (PSA ticket management) so FlowPilot can ship on top
of a unified main. Two manual conflict resolutions:
1. CLAUDE.md — kept the FlowPilot ai-handoff rewrite (`.ai/`-driven
protocol). The pre-rewrite reference content (CW integration notes,
lessons archive, env vars table) lives in `docs/connectwise/`,
`docs/LESSONS-ARCHIVE.md`, and DEV-ENV.md by design.
2. frontend/src/pages/AssistantChatPage.tsx — both conflict regions
were purely additive. Concatenated FlowPilot's Phase 2-9 state hooks
(facts, activeFix, preview*, scriptPanelOpen, templatizeQueue) with
PSA's spin-off ticket state (linkedTicket, showNewTicket, spinOffHint).
Both modal mounts (TemplatizePrompt, ShortcutsHelpOverlay,
NewTicketModal) kept. All setters wired by either branch are intact.
Verification:
- `tsc -b` clean across the merged tree.
- Browser smoke-test (Session B fixture): Phase 9 ProposalBanner
("Run AI-drafted PowerShell to recover SSL VPN") renders alongside
PSA's new Tickets sidebar icon. Console clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. EscalateInterceptDialog clipped off-screen.
The dialog was positioned with `absolute bottom-full mb-2 left-0`
under the assumption the Escalate button would have room above it.
In practice the button lives in the chat-page action bar near
y≈105, so the 302 px dialog overflows the top of the viewport
and only the last option is visible.
Switch to `top-full mt-2 right-0` — anchors the dialog below the
button and aligns its right edge with the button (avoids overflow
off the right when the button is in the right-side action cluster).
2. TemplateMatchPanel never renders on a fresh session.
`handleApplyFix` for the script_template_id branch only sets
`scriptPanelOpen=true`, but TemplateMatchPanel is mounted inside
`TaskLane.bottomSlot`. On sessions with no questions/facts the
lane defaults closed, so the panel exists in the React tree but
inside an unrendered TaskLane — the user clicks Apply fix and
nothing visibly changes.
Fix: also `setShowTaskLane(true)` in that branch so the lane
opens alongside the panel. The ai_drafted_script branch is fine
(InlineNoTemplateDialog renders in the chat region, not in the
lane), so it's left alone.
Both bugs were latent — they only surface on sessions that haven't
accumulated TaskLane state yet (questions/facts). Fresh sessions
created from the StartSessionInput hide them because the AI's first
turn populates questions and the lane auto-opens. Caught using the
new seed_phase9_qa_fixtures.py harness.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds backend/scripts/seed_phase9_qa_fixtures.py — creates 4 ai_sessions
plus matching session_suggested_fixes that pre-bake the four backend
states the AI orchestrator must produce to mount the five conditional
Phase 9 components:
A. no template, no draft → ChatTabStrip + ScriptBuilderTab
B. ai_drafted_script set → InlineNoTemplateDialog
C. script_template_id set → TemplateMatchPanel
D. applied_at + status=proposed → EscalateInterceptDialog (verify state)
Background: a Phase 9 QA pass against a regular session left these
five components unreached because the AI didn't emit SUGGEST_FIX in
time/at all. Seeding directly bypasses the AI and lets QA exercise
each surface deterministically.
UUIDs are deterministic (uuid5 over a fixed namespace) so re-runs
upsert. Pass --reset to wipe and recreate. Each session gets two
synthetic conversation messages so the chat header's canAct gate
(messages.length >= 2) opens up Resolve/Escalate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ResolutionNotePreview popover renders inside TaskLane's
overflow-y-auto region at the bottom of the lane. On a 720px
viewport with the default question/check list expanded, the
popover lands below the visible scroll position — the engineer
clicks "Preview Resolve note", sees the button label flip to
"Showing", but no preview appears on screen.
Add a useEffect that calls scrollIntoView({block: 'nearest'}) on
the popover's outer div whenever `open` flips to true. block:
'nearest' scrolls just enough to make it visible without yanking
the lane to the top.
Discovered during Phase 9 QA. Reproduced at 1280x720; fix verified
visually in the same QA run (screenshots in
.gstack/qa-reports/phase9-*/).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Discovered during Phase 9 QA: seed_test_users.py was missing the
cancel_at_period_end column in its subscriptions INSERT, but the
column is NOT NULL (added in 016_add_subscription_tables.py).
Result: seed crashed with NotNullViolationError before any users
were created, blocking auth in fresh dev environments.
Pre-existing on main; not introduced by the FlowPilot migration
branch. Default value: false.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per gstack team-mode install: adds a PreToolUse hook that blocks
skill usage when gstack isn't installed globally, so contributors
are prompted to install it. Un-ignores the two required files
(.claude/settings.json, .claude/hooks/check-gstack.sh) while
keeping settings.local.json and other Claude state ignored.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reflect current state: dual-agent migration + Codex review round +
branch cleanup (RLS test gating, Phase 9 docs, .remember/ gitignore,
landing-handoff deletion). Working tree clean, no active task, 3
cleanup commits queued to push.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review findings companion to docs/FlowAssist_Migration/Issues/phase-8-review-issues.md.
Documents the issues addressed by commit 24972e8 (partial-outcome notes
+ per-fix script-builder remount).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Continues the test-isolation work from dab740d. RLS migration tests run
against a policy-installed database and fail in the default create_all
suite, so they need to be opt-in:
- pytest.ini: register `rls` marker.
- conftest.py: auto-deselect test_rls_isolation.py unless
RUN_RLS_TESTS=1. Drops the deprecated session-scoped event_loop
fixture (not needed since pytest-asyncio 0.23+).
- test_rls_isolation.py: tag module with `rls` marker. Replace
hardcoded `patherly_test` DB reference with parsed DATABASE_TEST_URL
(matches conftest.py default `resolutionflow_test`). Updated docstring
command to show RUN_RLS_TESTS=1.
- requirements-dev.txt: bump pytest-asyncio 0.23.0 → 0.24.0 (loop-scope
marker behavior required by the RLS module fixture).
Run the RLS suite with:
RUN_RLS_TESTS=1 DB_APP_ROLE_PASSWORD=... pytest tests/test_rls_isolation.py
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Codex review of the dual-agent handoff migration flagged factual errors
carried over verbatim from the pre-migration CLAUDE.md. All claims
verified against the live code before correction.
PROJECT_CONTEXT.md — SaaS shape:
- Role hierarchy was `super_admin > team_admin > engineer > viewer`,
but `backend/app/core/permissions.py:4` and
`frontend/src/hooks/usePermissions.ts:4` both define it as
`super_admin > owner > engineer > viewer`. The `team_admin` concept
exists separately as an orthogonal team-scoped gate
(`require_team_admin`, `is_team_admin=True` + valid `team_id`), not
a level in the primary hierarchy.
- Dep list was missing `require_account_owner` and `require_team_admin`,
both present in `backend/app/api/deps.py`.
PROJECT_CONTEXT.md — directory tree:
- `api/endpoints/` comment listed 11 routers; `api/router.py` actually
registers 50+. Replaced with a summary that points at `router.py` as
the source of truth instead of trying to maintain a freezing list.
- `services/psa/` comment omitted `exceptions.py` and `ticket_context.py`,
both present in the directory.
CURRENT_TASK.md + TODO.md:
- Replaced `<!-- EXAMPLE -->` placeholders with clearer empty-state
sentinels so a resume agent sees "no real task yet" at a glance
rather than placeholder acceptance criteria that look unresolved.
SESSION_LOG.md updated with a follow-up bullet documenting this pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Split the monolithic CLAUDE.md into a durable handoff system:
- .ai/PROJECT_CONTEXT.md — stable architectural truth (stack, structure,
SaaS shape, ConnectWise, coding standards, frontend patterns, critical
lessons). Ported verbatim from the previous CLAUDE.md.
- .ai/CURRENT_TASK.md — single active task with DoD + out-of-scope.
- .ai/HANDOFF.md — resume point, kept under ~2K tokens.
- .ai/TODO.md — backlog, read only when CURRENT_TASK complete.
- .ai/DECISIONS.md — append-only architectural decision log.
- .ai/SESSION_LOG.md — append-only chronological history.
- .ai/README.md — human-facing explanation of the system.
Root agent files share a byte-identical protocol block (verified via diff):
- CLAUDE.md — primary agent, with GitNexus + gstack tooling and the
Claude Opus 4.7 co-author trailer.
- AGENTS.md — OpenAI Codex resume agent, with grep/rg fallbacks and the
Codex co-author trailer. Steps in when Claude hits session/weekly
limits.
Legacy root-level SESSION-HANDOFF.md deleted — superseded by .ai/HANDOFF.md.
It was a self-describing one-off from the Design System v4 migration and
had no external references.
Supersedes previous CLAUDE.md. Old version recoverable via
`git show pre-ai-handoff:CLAUDE.md` (tag points at commit e110fed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause of the 06:32 AM outage: running 'pytest tests/' inside the
resolutionflow_backend container silently dropped the public schema on
the DEV database. Two layered bugs made this possible; both are fixed.
Bug 1 — env-var lookup in conftest.TEST_DATABASE_URL put DATABASE_URL
(which normally points at the dev/prod DB) ahead of DATABASE_TEST_URL.
When DATABASE_URL is set, pytest used the dev DB as the 'test' DB and
the test_db fixture's DROP SCHEMA public CASCADE wiped it. Fixed:
- Honor only DATABASE_TEST_URL (or the localhost fallback).
- Assert at module load that the DB name contains 'test' — refuses
to run otherwise. Makes future misconfiguration impossible.
Bug 2 — conftest overrode app.dependency_overrides[get_db] but not
get_admin_db. Endpoints using get_admin_db (register, admin routes)
bypassed the test session and hit the real admin DB. Before Bug 1 was
fixed this was hidden because both engines pointed at the same dev DB.
With isolation in place, register started failing 'Email already
registered' because of stale users in the dev DB. Fixed:
- Also override get_admin_db to yield the same test session. RLS is
not enabled in the create_all-managed test schema, so sharing is
safe.
Also adds DATABASE_TEST_URL=resolutionflow_test to docker-compose.dev.yml
so pytest in the container works out of the box.
Verified: 49/50 Phase 8 + 9 tests pass against resolutionflow_test; the
1 failure is the pre-existing Phase 8 Issue #4
(test_record_decision_persists_and_bumps_state_version).
Refs gitea #145 (will update that issue with this as the primary fix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses docs/FlowAssist_Migration/Issues/phase-9-review-issues.md.
Issue #1 (High): "Applied partially" from the escalation intercept silently
dropped because the backend requires notes on applied_partial and the dialog
sent none. The catch was silent and the UI advanced into the conclude flow
as if the outcome were recorded.
- EscalateInterceptDialog now has a two-step flow: clicking the partial
choice reveals a notes textarea (autofocused, required non-empty) plus
Back / "Record partial & escalate" buttons.
- onChoose signature extended to (choice, notes?).
- handleInterceptChoice passes notes to patchOutcome; on failure it
surfaces a toast and does NOT advance to the conclude modal, so the
intercept stays open for retry.
Issue #2 (Medium/High): ScriptBuilderTab kept local state across active-fix
changes within the same pilot session, so a stale draft could PATCH against
a newer fix.id. Added key={activeFix.id} on the mount — forces a clean
remount per fix; backend get-or-create (keyed on user+ai_session_id) still
returns the same session row, which is the intended resume-on-refresh
semantic; but messages/editorBuffer/latestScript local state resets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Handoff + migration spec incorrectly claimed Phase 9 added a new
parent_pilot_session_id FK. The implementation reuses the existing
ai_session_id column; the migration only adds the origin discriminator
+ partial unique index. Also: ScriptBuilderTab wraps ScriptBuilderChat
and ScriptBodyEditor (Monaco), not "ScriptBuilderChat in ephemeral
mode" — there is no ephemeral mode on the presentational component.
Applies applied_at call-site specifics: handleScriptDecision stamps
on one_off/draft_template, TemplateMatchPanel stamps on onMarkRun,
Script Builder tab Submit does not stamp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marks open items #1 (NoTemplateDialog narrow-lane) and #3 (Tabbed
Script Builder) as resolved. Records the applied_at semantics
correction as shipped. Final Phase 9 row added to the 'What shipped'
table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Phase 9 §5. Before: banner Apply click stamped applied_at
regardless of whether the engineer had committed to running anything,
starting the Verifying timer prematurely. After:
- handleApplyFix no longer calls applyFix(). It just routes to the
right surface (TemplateMatchPanel / InlineNoTemplateDialog / Script
Builder tab).
- handleScriptDecision stamps applied_at for one_off + draft_template
(both labels are 'Run now, …' — the click is the declaration).
build_template does not stamp.
- TemplateMatchPanel's new 'I ran this' button calls applyFix via a
new onMarkRun prop.
- Script Builder tab Submit does not stamp (a draft is not a run).
No backend change — the /apply endpoint is unchanged. Only call sites
move.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the three new components into AssistantChatPage:
- ChatTabStrip renders when the active fix needs a script drafted.
- ScriptBuilderTab sits alongside chat via display:none toggling so
chat scroll position + builder state both persist.
- InlineNoTemplateDialog replaces the task-lane bottomSlot render for
the drafted-script evaluation case; three cards finally fit.
- Banner Apply routing updated: no-draft/no-template → Script Builder
tab; drafted → InlineNoTemplateDialog; template → unchanged path.
applyFix() call site moves land in the next task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the gap Phase 8 final review flagged. When a fix is in
applied_partial state and the engineer escalates, the intercept no
longer forces them to approximate with didn't-work/worked/never-applied.
AssistantChatPage's handleInterceptChoice (Task 13) already dispatches
to patchOutcome for any FixOutcome value, so no handler change is
needed — the type already supports applied_partial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate and Copy alone don't declare a run — the engineer can walk
away after copying. Phase 9 §5 defines an explicit run-declaration
affordance so applied_at only stamps on the engineer's positive
commitment. Wiring from AssistantChatPage lands in Task 13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slide-up wrapper around the existing NoTemplateDialog for rendering
in the chat region above the composer (parallel to ProposalBanner).
The chat region's width lets grid-cols-3 finally work as intended.
No change to NoTemplateDialog itself; decision callbacks and card
copy stay identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owns the inline Script Builder session lifecycle:
- Get-or-create (origin='pilot_inline', ai_session_id) on mount.
- Renders ScriptBuilderChat in AI mode and CodeModeEditor (Monaco) in
'Write it myself' mode. Mode toggles via display:none so buffer and
messages persist across switches.
- Submit → sessionSuggestedFixesApi.patchScript; emits onScriptDrafted
to parent, which refreshes the fix and hides the tab strip.
- Relays in-progress state to the parent via onProgressChange for the
ChatTabStrip's indicator dot.
ScriptBuilderChat is untouched (stays presentational). Persistence
semantics live on the controller, not the display component.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-tab strip for the chat region. Parent controls mounting (strip only
appears when the fix needs a script drafted). Indicator dot signals
in-progress draft state. Tab switching via onChange callback; parent
handles display:none toggling so tab contents preserve state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sessionSuggestedFixesApi.patchScript(sessionId, fixId, script, params?)
hits the new PATCH /script endpoint.
scriptBuilder.createSession accepts an optional options bag with
origin + aiSessionId, defaulting to standalone when omitted so legacy
callers stay behavior-preserving.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Called by the inline Script Builder tab on Submit. Writes
ai_drafted_script + ai_drafted_parameters to the fix without stamping
applied_at (a draft is not an application — that's §5 of the Phase 9
spec). Bumps state_version so Resolve/Escalate preview bundles
regenerate.
409 on terminal fix status. 404 on wrong session. 422 on empty script.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /script-builder/sessions now supports origin='pilot_inline':
- Requires ai_session_id; validates it against current user ownership.
- Get-or-create: returns existing row for (user, ai_session_id) pair.
- Partial unique index on the DB backs the invariant; races resolve to
the single winner row.
list_sessions + count_user_sessions default-scope to origin='standalone'
so inline scratch sessions don't pollute the /script-builder dashboard
or count against the 5-session cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the DB column added in the prior migration. App-level default
is 'standalone' so existing callers of ScriptBuilderSession(...) work
without code changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 9 prep. Adds:
- origin VARCHAR(20) NOT NULL with CHECK ('standalone' | 'pilot_inline')
- invariant: pilot_inline rows must have ai_session_id
- partial unique index on (user_id, ai_session_id) WHERE origin='pilot_inline'
— backs get-or-create idempotency for the inline Script Builder tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Frontend scriptBuilder API client inventory now matches the backend
schema: createSession accepts BOTH origin and ai_session_id (both
required together for inline callers, both omitted for standalone).
- 'If template -> unchanged' sharpened: render location is unchanged,
but run stamping moves into the panel's new 'I ran this' action per
the §5 apply lifecycle correction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>