Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 KiB
Sprint: "Close the loop" — 2 weeks
Dates: 2026-07-23 → 2026-08-06 Source: Vikunja Correx project (id 4), 30 open tasks reviewed Theme: Real per-stage validation, warm-discovery freestyle runs, robust orchestration under failure
Goal 1 — Real per-stage validation: land the LSP gate + fix build-gate wiring
Why. Multiple freestyle QA runs hit the same wall: the build-gate only fires at the terminal stage, so 8-10 hopeful writes stack before the truth-check, and when it fails there's no budget left (#167 epic calls this the "open-loop" disease). #80 is the filed fix and its deferral condition was observed on 2026-07-18 run 4a41417b — promote now.
Tasks.
- #80 [EPIC] LSP validation gate — real per-stage gate + terminal typecheck replacement. Design finalized in the task body; break into the 5 sub-tasks listed there. Container for the work below.
- #310 ✅ LSP diagnostics runner: anchor read on server readiness, not the 750ms timer (kills phantom unresolved-import).
- #311 ✅ LSP gate: lint-class diagnostics (unused/deprecated) must not fail the run — classify by
DiagnosticTag, not severity.
- #263 ✅ Auto build-gate never fires on real freestyle scaffold — promotion attaches to no reachable stage. Blocks the terminal safety net #80 doesn't replace.
- #267 Verify build-gate actually fires + re-scope #40 (LSP obsoletes the typecheck alias) — one live run settles both. Acceptance gate for the whole goal; do last.
Exit. One live freestyle run where every write stage gets LSP diagnostics scoped to its write blast-radius, terminal build-gate fires, and a stranded-scaffold case goes to recovery instead of WorkflowFailed.
Sequencing.
- Mon: #80 sub-task 1 (cheap floor — populate the existing per-stage
static_analysisseam with file-local one-shots in the freestyle compiler). Same-day ship; de-risks everything below. - Week 1: parallel track on #263 (build-gate wiring).
- Week 2: #80 sub-tasks 2/3/4 — LSP4J wiring, pull-diagnostics, server pool, per-stage integration.
- Week 2 tail: #310, #311 (readiness anchor + DiagnosticTag classification), then #267 as the acceptance live run.
Goal 2 — Make freestyle runs start warm and converge: discovery prompts, impl decomposition, ACR
Why. The kernel executes what it's told; near-term leverage is what it's told. #260/#261 sharpen upstream (discovery + per-feature stages), #305/#306 make run N+1 actually carry forward from the event log — the two halves of the ACR thesis still unimplemented. #297 kills a budget-burning failure mode mid-discovery.
Tasks.
- #260 Freestyle SDLC: prompt-only upgrades — exhaustive discovery prompt + analyst DoD artifact (the contract the rest of the run is judged against).
- #261 Freestyle SDLC: decompose impl stages into features/sub-tasks (structural, deferred — but it's what makes per-stage LSP from Goal 1 actually bound blast-radius).
- #305 ACR: accrete task knowledge externally so discovery starts warm (model-agnostic) — warm-start half of #168's remaining follow-on.
- #306 Sticky ACR steer-away hint: fires every turn + coarse signature collapse — makes delivered concepts usable; without this ACR delivery is noise.
- #297 Analyst CoT indecision loop burns full reasoning budget, emits nothing — recovery hygiene in the stage the goal is sharpening.
Exit. A second freestyle run on a fresh repo where DoD is recorded, impl stages are feature-bounded (so Goal 1's LSP scope is real), and the discovery stage carries accreted task knowledge from a prior run on an adjacent repo.
Sequencing.
- Day 1 (parallel): #260 — files-only, doesn't block on anything.
- Week 1 mid: #297 (analyst CoT fix — same stage family #260 touches).
- Week 2: #261 (impl decomposition — depends on #260's DoD existing to decompose against), #305/#306 (ACR — pairs with the warmed discovery stage).
Goal 3 — Orchestration & recovery robustness: stop the unrecoverable kills and runaway recoveries
Why. Two failure modes currently end runs that shouldn't end: a single provider going down mid-run (#299), and the recovery stage burning its budget against a stale failure-cap it can't clear (#304). Both waste full event logs. #307 is the cheap observability floor that makes the rest debuggable.
Tasks.
- #299 Single provider death → unrecoverable session kill (
NoEligibleProvideron retry) — re-route, don't die. - #300 HealthMonitor detects provider loss ~18s too late (reactive, not gating) — gates #299's recovery path on a fast signal.
- #304 Recovery stage runs expensively then run dies on stale failure-cap — wasted work; the cap must reset on the recovery's own progress.
- #309 Recovery stage: apply same-fingerprint loop-breaker + repair-ledger — the runaway root cause; closes the loop Goal 3 started. Now lands on top of #312/#313:
recoveryTicketis USER-role in the trailing slot at highest precedence, and the loop-breaker is the agreed place to escalate a repeated tool failure out of tool-role (per-failure sites stay tool-role). - #307 Observability: no event records the assembled stage-context manifest — cheap event, makes every above failure diagnosable post-run.
Stretch (if #307 lands fast): #308 Background-process execution + monitor tool for long-running shell commands — unblocks real test gates but not load-bearing for the goals above.
Exit. A live run where provider downtime is logged + recovered around, and a recovery stage that either converges or breaks the loop with a recorded repair ledger rather than dying on a stale cap.
Sequencing.
- Week 1: #307 (manifest event — cheap, unblocks debugging of everything below).
- Week 2: #299 + #300 together (provider-death path), #304 + #309 together (recovery runaway path).
Cross-goal sequencing
| Week | Track A (validation) | Track B (freestyle content) | Track C (robustness) |
|---|---|---|---|
| 1 M | #80 sub1 — static_analysis seam populated | #260 — discovery prompts | #307 — manifest event |
| 1 W-F | #263 — build-gate wiring | #297 — analyst CoT loop | — |
| 2 M | #80 sub2/3/4 — LSP4J + server pool + per-stage | #261 — impl decomposition | #299 + #300 — provider death |
| 2 W | #80 sub4 — blast-radius filter | #305 + #306 — ACR external + sticky hint | #304 + #309 — recovery runaway |
| 2 F | #310, #311, #267 — readiness anchor + tag class + acceptance run | — | — |
Goal 1 progress — 2026-07-26
- #310 ✅ (commit
a95475be).awaitDiagnosticswaits for one push per URI then a quiescent period anchored to the last server publication (awaitAll+awaitQuiet), not a 750ms timer started atdidOpen. Kills the half-loaded-project phantom unresolved-import. - #311 ✅ (commit
f61864ff).LspDiagnostic.tags(lowercasedDiagnosticTagnames) is carried from LSP4J through the event;SessionOrchestratorGates2gates onseverity == error && !isLint. AnoUnusedLocalstsconfig promoting TS6133 to error no longer hard-fails a run that no rewrite could clear. Lint diagnostics stay recorded and visible, just non-gating. Classification by protocol tag, not a TS-code whitelist. Test:core/events/.../LspDiagnosticTest.kt. - #263 ✅ (commit
867e99d1). Two findings on trace:- The reported selection bug was already fixed by
159b3f1e(#277) —autoGateStagesis every write-declaring stage plusterminalStageId(plan), so a non-writing review terminal is gated andrunExecutionGatepromotes it to PROJECT off the realFileWrittenmanifest. The 2026-07-18 evidence was stale. - The hole that remained: any stage declaring
build_expectation: project|testszeroed the whole auto-gate set, so a plan building at stage 3 of 9 had nothing verifying the six stages written after it. A declared build now suppresses only the redundant per-writing-stage gates; the terminal floor always stays. - Left deliberately: the gate chain still short-circuits before the execution gate when the contract gate fails. The stage fails either way — cheap gates first, no COMPLETE-lie.
- The reported selection bug was already fixed by
Goal 1 remaining: #267 — the acceptance live run. Fold the unverified #312/#313 trailing-mandate check into the same run.
Precondition, handled. #191 (dependency resolution before scaffold accept) closed on 2026-07-21:
runSetupCommand runs the profile alias setup before every build gate and #40 resolves it per
toolchain. But setup is operator-declared, and the repo profile didn't declare one — so with
frontend/ cleared before each QA run, the now-firing terminal gate would run npm run build against
an absent node_modules and fail on deps instead of on the code. Added
setup = "npm --prefix frontend install" to .correx/project.toml (install, not ci — a fresh
scaffold has no lockfile). The general case — a workspace whose operator declared no setup — is
#314 (toolchain-default fallback).
#40 follow-through (commit fb8141d6). #40 (toolchain-aware command resolution — this repo hosts
Kotlin at root and a Node app in frontend/, and one flat alias can't serve both) closed on
2026-07-21 with [commands.<toolchain>] support in 6e844ef1. The repo profile was never migrated:
every flat alias still pointed at npm, so a Kotlin run's auto-gate would have run
npm --prefix frontend run build — the latent misfire #40's own body predicted. Profile now carries
[commands.jvm] + [commands.node] with jvm as the flat default. Parsing verified against
ProjectProfileLoader; the gate prefers <toolchain>.<alias> and falls back to flat. #267's "re-scope
#40" is now just the live confirmation.
Goal 3 + Goal 2 progress — 2026-07-26
-
#307 ✅ (commits
c742656e,68b5392e).ContextAssembledEventrecords the injected manifest —{sourceType, sourceId, tokenEstimate, layer, role}per entry, no content (that stays in CAS). Scope check confirmed nothing existing carried it;ContextTruncatedEventonly reports drop counts. Emitted at all fourcontextPackBuilder.buildsites, not just the stage's first: the motivating question ("did the steer-away hint fire on 7 consecutive turns?") is a per-rebuild question, and one event per stage couldn't answer it. Turned out to be the delivery-tracking substrate #306 needed. -
#304 ✅ (commit
2b13f610). Chose option (b) — recovery is a real second chance.detectRepeatedToolFailurewindows its fold to events after the most recentFailureTicketOpenedEventnaming the stage. Route budgets are charged off the ticket event by the reducer, so the reset can't open an infinite route-in/route-out cycle: 2+2 route cycles, each needing 6 fresh failures to re-trip. -
#309 ✅ (commits
ee69f9be,9db4e3dd).RecoveryFileLoopBreak.kt—fileRepairOutcomescorrelates eachFileWrittenEventwith the nextLspDiagnosticsCompletedEventper path, feeding both consumers off one fold: the in-recovery guard (a path rewritten 3x without clearing opensFailureTicketOpened(gate=recovery_loop_break, escalated=true)and fails terminally instead of looping) and the ledger annotation inbuildRetryFeedbackEntry, stillEntryRole.USERwith #313's precedence untouched. Correction landed on top: the fold conflated "no diagnostic run since this write" with "ran clean" — so the ledger said "done, leave it" about unverified files, and the breaker could kill a run on the absence of evidence. Now a distinctuncheckedstate; the breaker requires!unchecked.- Left deliberately: the guard is terminal, not an operator-approval pause. Recovery is the last tier, so there is nowhere to route; it opens the ticket for the record, then fails. Human-in-the-loop there is a follow-up if wanted.
-
#306 ✅ (commits
bc5afa51,f78c7f15). Two halves, and only both together fix the report. Across stage entries: the hint is keyed to itsRetryAttemptedEventoccurrence, delivery derived by folding priorContextAssembledEventmanifests (sourceType="unconfirmedFix",sourceId=classKey) — no new state, pure fold (invariant #9). Within one stage entry: the guard alone was not enough —unconfirmedFixEntriesis called once at stage entry and its result folded intoaccumulatedEntries, which everypushBackrebuild re-uses, so the hint rode into every turn regardless. That is the actual 7-turn symptom; the entry is now dropped once its first pack is built.- Directions 2/3 from the ticket needed no code:
classKeyalready derives from the current retry's own class, and routing dead-ends carrygate="stage"while every other path carries its real gate, so"$gate:$signature"can't collapse them. Locked in with a regression test rather than a rewrite.
- Directions 2/3 from the ticket needed no code:
-
#299 ✅ (commit
c5289420).route()now splits the two cases the old code conflated: a capability nobody was ever configured with still fails fast, but a capability that is configured whose candidates are all currently unhealthy gets a bounded wait (3 × 2s, each round re-checkingrefreshedHealthrather than the TTL cache) beforeNoEligibleProvider. A crash-plus- restart no longer collapses a retryable failure into a session kill. -
#300 ✅ (commit
bf362527). Made health gating instead of reactive: newInferenceRouter.reportFailure(providerId, reason)(default no-op) writesUnavailablestraight into the health cache, bypassinghealthCheck()/TTL, andSessionOrchestrator.kt:454calls it from the inference catch whenisConnectionLevelFailure(e). The nextroute()sees the drop immediately instead of ~18s later. Recovery needs no extra path — TTL expiry or #299's bounded-wait re-check picks the provider back up.
Goal 3 remaining: none. #308 partly pre-empted by ac460156 — a shell timeout is now recoverable
and coaches nohup & detach, so there is no background-process registry to build unless a real gate
needs one.
Goal 2 status calls — 2026-07-27.
- #260 ✅ (commit
516af1ca). Both halves are in the prompts:discovery.mdrequires inspecting the whole decision surface and batching all operator-only questions after inspection (no stop-at-first-uncertainty), andanalyst_freestyle.mdmakes thedodartifact the handoff contract — atomic, yes/no-checkable criteria, plus one criterion carrying the named task into the impl plan and one per material failure path. #3 (architect re-plans on better input) fell out for free. - #305 ✅ (commit
d52a94e5). The ticket body was stale on two of three stores; traced each against the tree before touching anything.- Store 1 shipped durable (
5df35879) and was then deliberately reverted to a memo (12775d56) —SqliteObservationStorewas a second unsynchronized SQLite writer holding a fact the log already carries (FileWrittenEvent.path+postImageHash+ a puredescribe().render()over CAS bytes), i.e. an invariant-#1/#8 break.descriptorMemokeeps the perf win with none of the risk. The "extend to the FileReadTool hot path" follow-up died with it: that path keyed on a workspace-relative path while every consumer looks up the absolute one, so it never hit. - Store 2 was already complete in
unconfirmedFixEntries— theunconfirmed/falsifiedstates below hard promotion, matched reactively on the retry's ownclassKey, made genuinely one-shot by #306. - Store 3 was already built, but deterministically rather than via the embedder: intent
keyword Jaccard over a fold of (initial intent, locked plan, workflow completion). That beats the
planned embedder version on #8/#9 — pure fold, no environment read, so no recorded-retrieval
event is needed at all. Its one real gap was the consumer gate:
produces execution_planonly, so discovery — the stage the whole ticket is named after — still started cold. Now gated on the produced artifact kind being a plan-shape consumer (execution_planordiscovery), with a discovery-specific framing: the prior run's stage list read as a checklist of surfaces this task-family touches, to inspect now rather than at stage 6. - Left deliberately: tool categories in the plan shape (the plan said "stage sequence + tool categories"). The sequence carries the signal; add the tool list only if a live run shows discovery missing tooling it should have anticipated.
- Store 1 shipped durable (
- #261 blocked by #267, correctly. Its own body defers it until the simpler pipeline is proven on a clean end-to-end run — which is the #267 acceptance run. Do #267 first.
Landed out-of-band — context message-type sweep (#312, #313)
Not in the original three goals; pulled in on 2026-07-26 because it is upstream of Goal 1's gate
verdicts and Goal 3's recovery tickets — both deliver their findings through the context builders
this touched. #312 was listed as deferred-behind-#307; that turned out to be unnecessary, placement
is statically determined by PromptRenderer + each producer's role, so no run ground truth was needed.
Rule established: the system block carries only what does not change during a run. Anything the
run mutates is a user message — a mutating system prefix defeats prompt caching, and models
under-weight system-folded content against the trailing user turn. role = which chat message type,
layer = pinning/prune eligibility; those were tangled and are now separate.
- #312 ✅ Audit — report at
docs/audits/2026-07-26-context-role-audit.md(commit514aeae7). - #313 ✅ Implementation (commit
a4f6cf05,./gradlew checkgreen). Ten entries re-roled SYSTEM→USER; trailing repair-mandate slot now emits exactly one mandate by precedence (recoveryTicket > retryFeedback > groundingFeedback > rejectionFeedback) withremainingDeltaappended, so the slot stays scarce as members were added. - Bug fixed en route: the renderer's
layer == L0clause was overriding role on four L0+USER packs —InferenceSummarizer,SemanticReviewerImpl,CapabilityGapReflectorImpl, Talkie session-naming — all four were sending a system-only request with no user turn at all.
Not verified live. Needs one freestyle run to confirm the trailing mandate lands as intended. Fold into the Goal 1 acceptance run (#267) rather than spending a separate run.
Intentionally deferred (seen, not dropped)
| ID | Title | Why out |
|---|---|---|
| #167 / #168 | Closed-loop + ACR design epics | Bodies marked IMPLEMENTED for landed slices; remaining work folded into Goal 1 (#80) and Goal 2 (#305/#306). Keep open as epic containers. |
| #193 | Frontier-parity design-review round | Design/review work, not a 2-week deliverable. Next cycle after Goals land. |
| #31 | Interactive workflow creation TUI/web-ui | Visible polish; not load-bearing for run reliability. Schedule its own sprint. |
| #265 | TUI clarification modal not dismissed on external resolve | TUI cluster; pair with the next TUI sprint. |
| #295 | TUI token usage display for router/talkie | TUI cluster. |
| #296 | TUI execution plan viewer | TUI cluster. |
| #298 | TUI output: show CoT/reasoning on artifact + tool-call turns | TUI cluster. |
| #301 | Escalate repeated scope/manifest write-block to user approval | Falls under Goal 2 once DoD lands; premature now. |
| #302 | Mid-run hard steering (drop inference, inject operator message, restart) | Bigger surface; pairs with the steering-channel design, post-Goal 3 reliability. |
| #303 | Auto-repair collapsed-argv shell calls | Nice-to-have shell hygiene. |
| #25 | Backlog (deferred/spec-level from memory) | Meta-task; verify-against-code before any sub-item is promoted. |