Files
correx/docs/sprints/2026-07-23.md
T

249 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_analysis` seam 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 (`NoEligibleProvider` on 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: `recoveryTicket` is 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`). `awaitDiagnostics` waits for one push per URI then a *quiescent*
period anchored to the last server publication (`awaitAll` + `awaitQuiet`), not a 750ms timer
started at `didOpen`. Kills the half-loaded-project phantom unresolved-import.
- **#311 ✅** (commit `f61864ff`). `LspDiagnostic.tags` (lowercased `DiagnosticTag` names) is carried
from LSP4J through the event; `SessionOrchestratorGates2` gates on `severity == error && !isLint`.
A `noUnusedLocals` tsconfig 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) — `autoGateStages` is every
write-declaring stage **plus** `terminalStageId(plan)`, so a non-writing review terminal is
gated and `runExecutionGate` promotes it to PROJECT off the real `FileWritten` manifest. The
2026-07-18 evidence was stale.
- The hole that remained: any stage declaring `build_expectation: project|tests` zeroed 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.
**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`). `ContextAssembledEvent` records the injected manifest —
`{sourceType, sourceId, tokenEstimate, layer, role}` per entry, no content (that stays in CAS). Scope
check confirmed nothing existing carried it; `ContextTruncatedEvent` only reports drop counts. Emitted
at **all four** `contextPackBuilder.build` sites, 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.
`detectRepeatedToolFailure` windows its fold to events after the most recent `FailureTicketOpenedEvent`
naming 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``fileRepairOutcomes`
correlates each `FileWrittenEvent` with the next `LspDiagnosticsCompletedEvent` per path, feeding both
consumers off one fold: the in-recovery guard (a path rewritten 3x without clearing opens
`FailureTicketOpened(gate=recovery_loop_break, escalated=true)` and fails terminally instead of
looping) and the ledger annotation in `buildRetryFeedbackEntry`, still `EntryRole.USER` with #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 distinct `unchecked` state; 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 its `RetryAttemptedEvent` occurrence, delivery derived by
folding prior `ContextAssembledEvent` manifests (`sourceType="unconfirmedFix"`, `sourceId=classKey`) —
no new state, pure fold (invariant #9). *Within one stage entry:* the guard alone was not enough —
`unconfirmedFixEntries` is called once at stage entry and its result folded into `accumulatedEntries`,
which every `pushBack` rebuild 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: `classKey` already derives from the current retry's
own class, and routing dead-ends carry `gate="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.
- **#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-checking `refreshedHealth` rather than the TTL cache) before `NoEligibleProvider`. A crash-plus-
restart no longer collapses a retryable failure into a session kill.
- **#300 ✅** (commit `bf362527`). Made health *gating* instead of reactive: new
`InferenceRouter.reportFailure(providerId, reason)` (default no-op) writes `Unavailable` straight
into the health cache, bypassing `healthCheck()`/TTL, and `SessionOrchestrator.kt:454` calls it from
the inference catch when `isConnectionLevelFailure(e)`. The next `route()` 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.md` requires inspecting
the whole decision surface and batching *all* operator-only questions after inspection (no
stop-at-first-uncertainty), and `analyst_freestyle.md` makes the `dod` artifact 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`) — `SqliteObservationStore` was a second unsynchronized SQLite writer holding a fact
the log already carries (`FileWrittenEvent.path` + `postImageHash` + a pure `describe().render()`
over CAS bytes), i.e. an invariant-#1/#8 break. `descriptorMemo` keeps 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` — the `unconfirmed`/`falsified` states
below hard promotion, matched reactively on the retry's own `classKey`, 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_plan` only,
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_plan` or `discovery`), 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.
- **#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` (commit 514aeae7).
- **#313** ✅ Implementation (commit a4f6cf05, `./gradlew check` green). Ten entries re-roled
SYSTEM→USER; trailing repair-mandate slot now emits exactly one mandate by precedence
(`recoveryTicket > retryFeedback > groundingFeedback > rejectionFeedback`) with `remainingDelta`
appended, so the slot stays scarce as members were added.
- **Bug fixed en route:** the renderer's `layer == L0` clause 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. |