Commit Graph

616 Commits

Author SHA1 Message Date
claude 4113ee9af4 Merge pull request 'Refresh the failure mandate in-loop when a write lands on a file the failure names' (#2) from task/461-refresh-the-failure-mandate-in-loop-when into master 2026-08-11 12:13:08 +02:00
claude 24b94aab28 feat(kernel): refresh a stale lsp_diagnostics mandate in-loop (#461)
On a gate-repair retry the failure text was frozen for the whole tool loop. The
agent edited the offending file, was told "written successfully", and kept
editing against a diagnostic it may already have cleared — it only found out
after stage_complete, when runPostStageGates re-ran from the top.

refreshLspRetryMandate hangs off the existing wroteThisRound hook: when the
pending retry mandate is an lsp_diagnostics failure and a write landed on a path
that failure names, re-pull diagnostics, record LspDiagnosticsCompletedEvent,
and rebuild the retryFeedback entry. buildRetryFeedbackEntry already renders
per-file freshness from that event, so the ledger flips to "done, leave it"
in-loop with no new message format and no context rebuild.

Two calls worth stating. The pull covers the stage's whole written set, not just
the named paths, because a path absent from the recorded event reads as clean to
fileRepairOutcomes — a partial pull would mark unrelated files repaired. A
skipped pull returns null rather than recording empty diagnostics, for the same
reason: telling the model to leave a still-broken file alone is worse than
leaving the stale text in place.

Scoped to LSP. A tsc or npm run build re-run per write is too expensive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:28:12 +04:00
kami 32020b496a chore: ignore testing/integration/logs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:47:27 +04:00
kami c70b6779a3 fix(context): name emit_artifact in the schema instruction (#416)
The pinned schemaInstruction said only "Respond with a single JSON object
matching this schema", contradicting every role prompt that says to emit via
the emit_artifact tool. Two stated channels for one artifact, and the pinned
one won.

ResponseFormat.Json is built under exactly the condition that offers the tool
(an llmEmitted slot — see emitArtifactTool), so the tool is always available
wherever this instruction renders, and the kernel prefers it: tool-calling
models are more reliable at it and it sidesteps llama.cpp's grammar+tools
incompatibility. The instruction now names it, and keeps raw JSON as what to do
when told to stop calling tools — which is what the tools-less final pass
demands and what the executor already accepts either way
(llmArtifactOverride ?: response.text).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:03:23 +04:00
kami a9196a0037 fix(context): render the stage role prompt as the system prompt (#416)
The role prompt was an L1/USER entry, so PromptRenderer emitted it as a user
turn behind the intent, decision journal, repo map and docs catalog, outranked
by the pinned schemaInstruction it contradicts. It is now L0/SYSTEM and folds
into the leading system message, and it sits directly after systemPrompt in
assembly so it heads the system block rather than trailing schemaEntries.

Extracted buildAgentPromptEntry so both the promptInline and prompt-path
branches build the entry one way. Guard test mutation-verified.

#416's finding 1 was wrong: the role prompt was never evictable. "agentPrompt"
is already in REQUIRED_SOURCE_TYPES, and DefaultContextPackBuilder exempts
REQUIRED entries from pruning at any layer. Layer was never the pinning
mechanism here; message placement was the whole defect.

Also pins groundingFeedback and recoveryTicket, the two feedback types that
were neither REQUIRED nor in neverDropSourceTypes. The recovery stage exists
only because of its ticket, so pruning the ticket left it nothing to repair.

Not done: reconciling the pinned schemaInstruction ("respond with JSON only")
against the role prompt's emit_artifact instruction, and live verification
across analyst/architect/role_pipeline.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:57:58 +04:00
kami d1b84a1a9f fix(gates,tools): stop pinning deleted files; match edit targets across blank lines (#419, #417)
Both defects come from live session fced377e, where scaffold_frontend burned 89
turns without converging.

#419 — the stage output manifest never tracked deletions. A deletion is a
FileWrittenEvent with a null postImageHash, and stageWrittenPaths filtered that
per-event instead of per-path, so a written-then-deleted file stayed in the
manifest forever. The contract gate stamps file_exists on every entry, which made
deleting — and therefore renaming, which is delete plus write — a permanent
contract violation. That deadlocked against the build gate: it ordered "rename it
to use the '.cjs' file extension", the agent complied, and the contract gate then
failed file_exists on the .js it had just been told to remove. Keep the last
mutation per path instead. Fixed in the shared function, so the build-gate
toolchain probe, the review gate, and verification all stop seeing deleted files
too. Moved to its own file to keep the gates file under the detekt function cap.

#417 — file_edit failed 4 of 6 live calls, all "Target not found" with a correct
"did you mean" suggestion attached. flexibleMatch already tolerated indentation
drift but walked target lines against file lines positionally, so one blank line
the model dropped shifted every later index and missed the whole block. Compare
only non-blank lines and map the hit back to real file lines. Ambiguity still
fails rather than picking a match, and reindent keeps handling the writeback.

Guard tests both ways, each mutation-verified against the old logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:23:51 +04:00
kami 5ed8ebd0c5 fix(tui): render CoT once per turn, stop clipping tool output (#415, #414)
The reasoning trace was rendered twice: once as its own "thinking" row on
inference.completed, and again as a ✼ block on the following tool row, whose
Reasoning was copied from Model.lastReasoning. The fallback branch was worse —
lastReasoning is current model state, so every tool row with no Reasoning of its
own (all non-diff rows, all snapshot-restored rows) got the newest trace stamped
on it retroactively. Drop the block, the copy, and the now-dead
RouterEntry.Reasoning / Model.lastReasoning; the standalone row already covers
tool turns.

actionToolText clipped every summary to 48 columns, which cut tool output and —
worse — harness coach text: read-before-write rejections, write blocks, gate
feedback, exactly the messages that say whether the agent was steered or
silently blocked. The action renderer already wraps to panel width, so the clip
was the only single-line ceiling; raise it to 4000 chars.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:46:25 +04:00
kami 45a9fe3369 fix(tui): repaint the background after inner ANSI resets, and guard it
Wrapping already-styled text in a Background() style silently fails: an inner
"\x1b[0m" (glamour emits one per span) resets the background to the *terminal*
default, not to the enclosing lipgloss style, so every cell after the first
reset went transparent and the terminal wallpaper showed through. fillFrame only
pads tail gaps, so it never caught this.

paintBG splits on the reset and re-applies the background per segment, leaving
each segment's own foreground codes intact. Used for the markdown-rendered
router turn, whose old comment claimed the wrapping approach "keeps the opaque
panel" — it did not.

TestFrameHasNoTransparentCells asserts the property directly over every
render-matrix case: after any reset, no printable cell before a background SGR.
Verified non-vacuous — restoring the old line fails it on chat.turn (router).
This is the part that stops the next render site from reintroducing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:22:31 +04:00
kami 8a9935ab6e docs(sprint): record #305 — all three ACR stores traced and closed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 13:41:12 +04:00
kami d52a94e5b2 feat(acr): deliver the prior plan shape to discovery, not only the planner (#305)
Store 3's read side already existed but gated on `produces execution_plan`, so the
one stage #305 is actually about — discovery — still started cold. The gate now keys
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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 13:35:17 +04:00
kami 34895b3d54 docs(sprint): close #299, #300, #260; #305/#261 status calls
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 13:08:45 +04:00
kami 4a730084f1 docs(sprint): record #307, #304, #309, #306
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 23:30:35 +04:00
kami f78c7f15ad fix(kernel): drop the steer-away hint after its first build, not just its first stage entry (#306)
bc5afa51 made unconfirmedFixEntries one-shot per RetryAttemptedEvent by folding
prior ContextAssembledEvent manifests. Correct, but it only guards the call
site — which runs ONCE per stage entry (SessionOrchestratorExecution.kt:250).
The entry it returns is folded into accumulatedEntries, and every pushBack /
tool-round rebuild rebuilds from that same list, so the hint rode along into
every turn of the stage regardless of the delivery fold. That is the reported
symptom: 7 consecutive turns in session d734e1de, one stage entry.

Drop sourceType=="unconfirmedFix" from accumulatedEntries once the first pack
is built and its delivery recorded. The fold still does the across-entries half.

./gradlew check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 23:29:27 +04:00
kami bc5afa51b3 fix(kernel): make the ACR steer-away hint genuinely one-shot per retry (#306)
Root cause: unconfirmedFixEntries() re-derived the hint on every context
build from "the latest RetryAttemptedEvent for this stage," with no guard
on whether that retry occurrence had already been delivered. A single
contradicted retry (e.g. a workflow-routing dead end, gate="stage")
therefore kept re-injecting its "Steer away from a known dead end" block
on every subsequent stage retry/rebuild, even once the model had moved on
to a materially different sub-problem — live evidence: session d734e1de,
stage scaffold_frontend, injected on 7 consecutive turns.

Fix (direction 1 from the three proposed): key delivery to the specific
RetryAttemptedEvent occurrence and check whether it was already delivered
by folding prior ContextAssembledEvent manifests (#307) for
sourceType="unconfirmedFix"/sourceId=classKey recorded after that retry's
own sessionSequence — no new mutable state, replay-safe (invariant #9).
A fresh contradicted retry now injects exactly once; later rebuilds for
the same occurrence stay silent; a NEW RetryAttemptedEvent of the same
class (a real recurrence) earns one fresh injection.

Direction 2 (match the current turn's own classKey, not "latest in
stage") turned out to be already true by construction — classKey already
comes from the single "latest" retry being matched — so the mismatch in
the evidence was purely temporal/sticky, which direction 1 resolves.
Implementing 2 separately would have been redundant; noted rather than
silently dropped.

Direction 3 (tighten the signature so routing and build failures can't
collapse): already structurally true — routing dead-ends
("no transition condition matched...") only ever reach
RetryAttemptedEvent via DefaultRetryCoordinator.shouldRetry(), which
defaults gate="stage", while every other failure path uses .decide()
with its real gate (build/contract/lint/...). Since classKey = "$gate:
$normalizedSignature", the two families can never share a classKey.
Added a regression test locking this in rather than changing the scheme.

Docstring on unconfirmedFixEntries updated to describe the one-shot
mechanism precisely instead of just asserting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 23:12:27 +04:00
kami 9db4e3dd4d fix(kernel): "not yet checked" must never read as "clean" in the repair ledger (#309)
fileRepairOutcomes mapped both "no diagnostic run after this write" and
"diagnostics ran and found nothing" to emptySet(), so resolved was true for a
file that was never actually re-checked. Two consequences, both the inverse of
what #309 is for:

- the ledger told the model "done, leave it" about a file whose last write was
  never verified — the precise mis-signal the tab-keeping exists to prevent;
- the loop-breaker counted unchecked writes as unresolved, so it could kill a
  run terminally on the ABSENCE of evidence rather than on recorded proof the
  rewrites weren't working.

Track it as a distinct `unchecked` state: the ledger says "not re-checked since
the last write — outcome unknown", and the breaker requires !unchecked.

./gradlew check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 23:01:37 +04:00
kami ee69f9becc fix(kernel): recovery-internal same-file loop-breaker + repair ledger (#309)
Recovery runs freestyle for 3h+/800 events thrashing one file (22
rewrites, 9 rewrites of a single component) with no second
FailureTicketOpenedEvent ever firing, terminated only by human CANCEL.
Root cause: recovery is architected as ONE continuous ReAct loop
(deliberately generous maxToolRounds — most rounds are
re-investigation before the write), so the existing cumulative
tool-failure breaker (stage_loop_break, #78/#304) never fires: each
file_edit call itself SUCCEEDS, only the downstream lsp_diagnostics
gate keeps failing on the same file. The per-gate progress-aware
fingerprint is also unreliable here: it hashes the whole failure-reason
string, which can look "different" each round from unrelated
diagnostics elsewhere even while one file's defect never clears.

Fix — two consumers sharing one event-derived data source
(RecoveryFileLoopBreak.kt, FileRepairOutcome/fileRepairOutcomes):
correlates each FileWrittenEvent with the next LspDiagnosticsCompletedEvent
for the same path (invariant #9 — no re-observation, pure fold).

- Guard (recoveryFileLoopBreak, wired into DefaultSessionOrchestratorStep's
  enterStage before the normal gate-retry machinery): once a single path has
  been rewritten recoveryFileRewriteLimit times (default 3) without its
  diagnostic ever going clean, escalateRecoveryLoop opens a
  FailureTicketOpenedEvent (gate=recovery_loop_break, escalated=true, same
  machinery every other escalation uses) and fails the workflow terminally
  instead of looping again. Recovery is the last tier — there's nowhere
  further to route to.
- Ledger (buildRetryFeedbackEntry in ContextFeedback.kt): each
  already-written file in the trailing repair-mandate slot is now annotated
  with whether rewriting it actually moved the diagnostic, e.g.
  "SessionsList.tsx — written 3x. TS6133: present before AND after every
  write. Re-writing has not changed the result. Change the fix or report
  unresolvable." vs "MainLayout.tsx — written 2x. cleared after write 2.
  done, leave it." Existing trailing-slot precedence (recoveryTicket >
  retryFeedback > groundingFeedback > rejectionFeedback, #313) is untouched —
  this only changes retryFeedback's rendered content, still EntryRole.USER.

FileRepairOutcome/fileRepairOutcomes/describeFileRepairOutcome and the
recovery guard functions live in a new RecoveryFileLoopBreak.kt purely to
keep ContextFeedback.kt and DefaultSessionOrchestratorRecovery.kt under
detekt's per-file function-count threshold (11) — no behavioral reason
for the split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 22:58:16 +04:00
kami 2b13f610e1 fix(kernel): reset the stage_loop_break window on recovery entry (#304)
Recovery was dying on a stale failure cap, not on anything recovery
itself did. detectRepeatedToolFailure (stage_loop_break, #78) counted
same-signature ToolExecutionFailedEvents cumulatively across the whole
stage. A stage routed to recovery, retried by the ladder, and routed
again would re-trip the SAME pre-recovery count on its very first
post-recovery round — recovery would burn a full expensive turn and
still fail on retryExhausted=true from stale evidence.

Fix: window the fold to events after the most recent
FailureTicketOpenedEvent naming the stage (mirrors the existing
BuildPrerequisiteBootstrapAttemptedEvent windowing pattern already used
by repeatedBuildCriticalReferenceBlock). A stage returning from a
genuine repair attempt now starts the count clean; it must accumulate
stageFailureLoopLimit (default 6) NEW failures post-recovery before
re-tripping.

This does not weaken RECOVERY_ROUTE_BUDGET/INTENT_ROUTE_BUDGET: those
are charged directly off FailureTicketOpenedEvent by the reducer
(OrchestrationState.recoveryRoutes/recoveryFailureFingerprints),
independent of this fold, and are deliberately left untouched by
TransitionExecutedEvent already. A full ladder round-trip therefore
stays bounded at RECOVERY_ROUTE_BUDGET(2) + INTENT_ROUTE_BUDGET(2)
route-in/route-out cycles, each itself requiring a fresh
stageFailureLoopLimit(6) failures to re-trip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 22:57:54 +04:00
kami 68b5392e15 fix(context): emit the manifest on every context rebuild, not just the first (#307)
c742656e emitted ContextAssembledEvent only at the stage's initial
contextPackBuilder.build. But the question that motivated #307 — "did the ACR
steer-away hint fire on 7 consecutive turns?" — is a per-rebuild question:
unconfirmedFixEntries recomputes on every rebuild, which is exactly the #306
bug. With one event per stage the log still couldn't answer it.

Emit at all four build sites, matching emitContextTruncationIfNeeded's existing
placement: pushBack, the tool-round rebuild, and the tools-less clean emission.

./gradlew check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 22:29:58 +04:00
kami c742656e15 feat(context): emit ContextAssembled manifest event per stage build (#307)
Root cause: context-assembly (unconfirmedFixEntries, promotedConceptEntries,
buildRetryFeedbackEntry, etc.) computes ContextEntry lists at stage-build time
but never records what was actually injected into a turn. The only way to
answer "did hint X fire in session Y" was decoding the CAS prompt artifact and
grepping raw JSON — the event log alone couldn't say.

Scope check: no existing event carries this (ContextTruncatedEvent only
reports drop counts, not the injected set) — added new ContextAssembledEvent
rather than extending an existing one.

Fix: ContextAssembledEvent (core/events/events/ContextEvents.kt) records ONE
manifest per stage's initial ContextPack build — sessionId, stageId,
contextPackId, and a List<ContextManifestEntry> of {sourceType, sourceId,
tokenEstimate, layer, role} pulled from the pack's actual (post-budget)
entries. No entry content — content stays a pure derived projection of the
event log (invariant #9/#6) and already lives in CAS. Registered in
eventModule (Serialization.kt). Emitted via
SessionOrchestrator.emitContextAssembled (SessionOrchestratorRepoContext.kt),
wired at the initial contextPackBuilder.build() call site in
SessionOrchestratorExecution.kt (mirrors emitContextTruncationIfNeeded).

Tests: ContextAssembledEventSerializationTest (round-trip + no-content-leak),
SessionOrchestratorIntegrationTest "stage context build emits
ContextAssembledEvent with a manifest (#307)".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
2026-07-26 22:27:43 +04:00
kami 9b59b7c1aa docs(sprint): record the #40 profile migration
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:39:51 +04:00
kami fb8141d669 chore(profile): split build-gate commands per toolchain (#40 follow-through)
The repo hosts Kotlin at root and a Node app in frontend/, but the profile
still pointed every flat alias at npm — so a Kotlin run's auto-gate would
have run 'npm --prefix frontend run build'. #40 shipped [commands.<toolchain>]
resolution in 6e844ef1; this migrates the profile to actually use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:39:38 +04:00
kami 7cfb4e92d1 fix(profile): declare a setup command so the build gate installs deps first
QA runs clear frontend/ before each run, taking node_modules with it. With
#263 making the terminal build gate actually fire, the gate would fail on
absent dependencies instead of on the code it exists to verify.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:36:40 +04:00
kami a7f5b9902f docs(sprint): correct the #191 note, file the setup-command gap as #314
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:34:13 +04:00
kami 1b3b2f401a docs(sprint): record Goal 1 progress — #310, #311, #263 landed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:31:23 +04:00
kami 867e99d1cb fix(build-gate): a mid-plan declared build never suppresses the terminal floor (#263)
autoGateStages returned an empty set as soon as any stage declared
PROJECT/TESTS, so a plan that builds at stage 3 of 9 had nothing
verifying the six stages written after it. Keep the suppression for the
redundant per-writing-stage gates, always keep the terminal stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:27:09 +04:00
kami f61864ff1d fix(lsp-gate): lint-tagged diagnostics never fail a stage (#311)
tsserver tags TS6133 (unused import) Unnecessary, but a tsconfig with
noUnusedLocals promotes it to error severity — which hard-failed whole
runs no rewrite could clear. Carry LSP DiagnosticTag through the
LspDiagnostic event and gate on untagged errors only; lint diagnostics
stay recorded and visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:16:33 +04:00
kami c32445b8a8 docs(sprint): record the #312/#313 context message-type sweep
Landed out-of-band on 2026-07-26: upstream of Goal 1's gate verdicts and
Goal 3's recovery tickets, both of which deliver through the context
builders it touched. Drops #312 from the deferred table (its stated
dependency on #307 turned out to be unnecessary) and notes that #309 now
lands on top of the re-roled recoveryTicket.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:13:55 +04:00
kami a4f6cf0564 feat(context): role means message type, layer means pinning (#312/#313)
The system block is now for content that does not change during a run.
Anything the run mutates renders as a user message — both because a
mutating system prefix defeats prompt caching and because models
under-weight system-folded content against the trailing user turn.

PromptRenderer: role alone decides the message type; the old
`layer == L0 ||` clause is gone. That clause was silently overriding
role on four packs (InferenceSummarizer, SemanticReviewerImpl,
CapabilityGapReflectorImpl, Talkie session-naming) which are L0+USER
prompts — they were rendering as a system-only request with no user
turn at all.

Re-roled SYSTEM -> USER, all mutable within a run:
  recoveryTicket, remainingDelta, groundingFeedback, rejectionFeedback
  (trailing slot), plus verifiedBaseline, promotedConcept, claimedTask,
  clarificationAnswer, locked steeringNote, factSheet (inline, still
  L0-pinned).
Left SYSTEM (immutable): systemPrompt, operatingGuidance,
schemaInstruction, projectProfile, operatorProfile, agentInstructions,
successfulPlanShape.

Trailing-slot scarcity guard: repairMandateSourceTypes joined ALL
matches, so a recovery stage on a retry with an unmet delta would stack
three competing mandates. Now a precedence list emits exactly one
(recoveryTicket > retryFeedback > groundingFeedback > rejectionFeedback)
with remainingDelta appended as the completion signal.

ContextClassifier keys STATIC on layer alone — L0 means pinned/never
pruned regardless of role, so the re-roled L0 entries don't fall through
to FREEFORM and get token-pruned.

Also fixes a stale ContextFeedbackTest assertion (expected a CAS hash
the producer deliberately stopped emitting) and a stale initialIntent
doc comment claiming L0/SYSTEM where the code says L1/USER.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:10:26 +04:00
kami 514aeae75f docs(audit): context entry role/layer placement sweep (#312)
Audits every ContextEntry producer against PromptRenderer's three real
render destinations (system fold / inline / trailing user slot). Finds
four escalation-grade entries roled SYSTEM and therefore buried in the
leading system message: recoveryTicket, remainingDelta, groundingFeedback,
rejectionFeedback. remainingDelta additionally mutates the cached system
prefix every turn.

Report only, no code changes. Re-role work filed as #313.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:00:36 +04:00
kami a95475be2a fix: anchor LSP diagnostics to server readiness 2026-07-26 10:25:32 +04:00
kami 516af1ca96 feat: tighten freestyle discovery and analyst handoff 2026-07-26 10:22:05 +04:00
kami cf9eecc895 chore(qa): move Ethos design kit out of frontend/ into tracked design-system/
The kit (SKILL.md, ethos.tokens.css, ethos-icons.svg, .oxlintrc.json) lived
inside frontend/ — the exact dir freestyle runs scaffold into. vite refuses a
non-empty target, so every session improvised a stash-aside (frontend_backup/,
frontend_temp/) and left scratch dirs behind. Move the kit to a sibling
design-system/ so frontend/ starts clean, and deliver it via the L0 conventions
channel (project.toml) rather than seeding files in the scaffold target: copy
the token/icon assets in (don't read them), SKILL.md for rules only. Un-ignore
frontend/ so scaffolded output is tracked going forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 21:11:42 +04:00
kami ac4601562a fix(shell): timeout is recoverable + coaches detach, not a workflow kill
A foreground long-running command (npm run dev, vite, a --watch task) hit
the 30s timeout and returned recoverable=false, giving the stage no
transition to take -> WorkflowFailed "no transition condition matched"
(killed session d734e1de). A non-zero exit right below already returns
recoverable=true; a timeout is no more fatal. Return recoverable and coach
the model to start the process detached (nohup ... &) and verify separately,
or run a one-shot that exits. No background-process registry: the model
backgrounds it itself via `&`, which already routes through `sh -c`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 20:38:04 +04:00
kami f5aaa255ef fix(l3): raise repo-map similarity floor 0.5→0.6 (stopgap) (#305)
L3 semantic retrieval does a bad job on the repo map: docs embed a terse symbol-list
descriptor while queries embed prose intent, an asymmetric comparison that collapses all
scores into a ~0.5 noise band (session 459: junk hits at 0.53/0.54, real relevance never
reached). At 0.5 the noise-winners cleared the bar and suppressed the deterministic
repo-map floor. 0.6 sits above the noise ceiling (~0.55) and below the real-signal floor
(0.68) so noise → empty → fall back to the repo map.

Band-aid, not a fix — the real remedy is a prose-shaped repo-file descriptor so query and
document share a representation space. Calibration knob, retune if the embedder changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 18:42:56 +04:00
kami 12775d56da refactor(acr): ACR Store 1 as a disposable memo, not a durable side-store (#305)
The shipped Store 1 persisted repo-file descriptors in a new SqliteObservationStore
— a second, unsynchronized SQLite writer on the event-log DB (no WAL/busy_timeout,
one shared Connection across coroutines) holding a fact the log already carries. That
violates "the event log is the only source of truth" (inv #1/#8): an Observation is
just FileWrittenEvent.path + postImageHash + describe(bytes).render(), and render is a
pure function of content-addressed CAS bytes.

Replace the whole durable apparatus with an in-process memo (descriptorMemo, keyed on
repoRoot+path+contentHash) on the orchestrator, next to the existing artifactContentCache.
Rebuilt from the log on restart, disposable, no external store. Preserves the perf win
(skip CAS read + regex on repeat describes) with none of the concurrency/lock risk.

Deletes: ObservationStore/InMemoryObservationStore/SqliteObservationStore (+ tests),
InfrastructureModule.createObservationStore, the FileReadTool priming path (which never
hit anyway — it keyed on a workspace-relative path while every consumer looks up the
absolute FileWrittenEvent.path), the FileReadConfig/SessionOrchestrator plumbing, and
five orphaned build.gradle deps.

Store 2 (soft-confidence hints) unchanged — it was correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 18:42:47 +04:00
kami 8806de1628 feat(acr): Store 2 soft-confidence hints + Store 1 FileReadTool priming (#305)
Store 2 (Fixes): ConceptCompilerProjection already tracked the
unconfirmed(validatedFixes>=1)/falsified(contradicted)/confirmed(promoted)
lifecycle as data, but only confirmed (>=threshold) clusters were ever
delivered. unconfirmedFixEntries reactively matches the CURRENT retry's
classKey against that state and injects a soft one-shot hint (below
threshold) or steer-away (contradicted) before hard promotion — wired
into stage context build in SessionOrchestratorExecution.kt.

Store 3 (Plan-shapes): already shipped as SessionOrchestratorPlanPatterns.kt
(deterministic keyword-Jaccard over eventStore.allEvents(), no embedder
needed) — confirmed complete, no new code required.

FileReadTool hot-path extension to Store 1: a whole-file read now primes
the cross-session ObservationStore (when wired) the same way the write
path already does, so a later describeCached lookup for the same content
hash is warm even if the file was only read, not written, this session.
2026-07-21 17:55:19 +04:00
kami 5df35879eb feat(acr): ACR Store 1 — content-hash observation cache for repo-file descriptors (#305)
Adds ObservationStore (core:context) keyed on (repoRoot, path), an in-memory default and
a durable SqliteObservationStore (infrastructure:persistence), and wires it into the two
SessionOrchestratorArtifacts call sites that re-derive a SourceDescriptor from CAS bytes
on every call — a hit on matching content hash skips both the CAS read and the regex
extraction. First slice of docs/plans/2026-07-21-acr-knowledge-accretion.md (build order:
observations before fixes/plan-shapes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
2026-07-21 17:37:25 +04:00
kami f08a784432 fix(orchestration): break the no-op-write loop + un-rot retry feedback
A stage (configure_ethos_theme) looped forever re-writing an identical
postcss.config.js: file_write returned "written successfully" on a byte-
identical write, and the retry-feedback entry that says WHY it failed got
truncated out of context every turn, so the model cold-started on the same
wrong idea. CAS hashes leaked into its face from two sides too.

- SandboxedToolExecutor: a write whose result is byte-identical to disk now
  returns "No change: … nothing was written" (+ noop=true) instead of a false
  success signal. Uses the pre-image hashes already captured for reversibility.
- DefaultContextPackBuilder: pin "retryFeedback" in neverDropSourceTypes so the
  failure reason + already-written-files list survives truncation.
- ContextFeedback: drop the opaque "— CAS <hash>" from the retry entry; path only.
- tool_output withheld until the session actually spills over-cap output to CAS
  (StageConfig.ALWAYS_AVAILABLE_READ_TOOLS -> on-demand via sessionHasSpilledOutput),
  so a hash-eating tool isn't advertised to every stage that never spills.
- Wrap 10 long lines in kernel to bring detekt back under maxIssues (99 -> 89).

Tests: infrastructure:tools (+2 no-op cases), core:{transitions,context,kernel} green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:22:39 +04:00
kami 8a60778ca7 merge: integrate tui-vikunja (#295,#296,#298,#265) into HEAD 2026-07-21 11:55:41 +04:00
kami d6155691c8 feat(tui): token metrics aggregation, plan overlay, tool reasoning, clar dismissal
#295 — include narration_llm tokens in changes-panel aggregate count
#296 — execution plan overlay (Ctrl+P in-session)
#298 — surface reasoning/CoT on tool-call turns in output view
#265 — dismiss clarification modal when answered externally (session.resumed)
2026-07-21 11:52:00 +04:00
kami dc5b24e75f fix(events): register WriteScopeGrantedEvent in eventModule (#301 silent-deser trap)
The #301 escalation event shipped unregistered — it would silently fail to
deserialize on replay, dropping the persisted write-scope grant and re-prompting
the same path every session restart (breaks replay invariant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:44:51 +04:00
kami 28d369ebce merge: integrate sonnet-vikunja (#299,#300,#297,#301) into codex handoff HEAD
# Conflicts:
#	core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationTuning.kt
#	examples/workflows/prompts/analyst_freestyle.md
2026-07-21 11:43:05 +04:00
kami 8cc418a381 feat(orchestration): escalate repeated scope/manifest write-block to user approval (#301)
Small models frequently fail to comply with the WRITE_SCOPE/PATH_OUTSIDE_MANIFEST
remediation ("add its path via task_update affected_paths") and instead thrash the
same out-of-scope write call turn after turn, burning the session without ever
completing. After `tuning.escalateScopeAfterN` (default 3) consecutive rejections
of the SAME path for a scope/manifest reason, the orchestrator now reaches back to
the FIRST rejected invocation of that path (its pristine, pre-degraded arguments),
and routes it through the existing approval/pause flow instead of rejecting again.
On approval, a new WriteScopeGrantedEvent widens the effective write manifest for
that path for the rest of the session (mirroring how OutsidePathAccessGrantedEvent
already widens out-of-workspace reads) and the first attempt's write is executed.
On denial, the call is rejected as before. escalateScopeAfterN = 0 preserves the
old hard-block-forever behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeyGFXczJb8RUWGBKmkm6G
2026-07-21 02:22:09 +04:00
kami 6e844ef1e1 fix(build-gate): resolve commands by toolchain (#40) 2026-07-21 02:18:04 +04:00
kami 119f59d637 test(server): bind workspace in contradiction hook fixture (#266) 2026-07-21 02:13:05 +04:00
kami 0af2f200d8 fix(prompts): resolve analyst epic-vs-child ambiguity that burned the full reasoning budget (#297)
Live incident: "Write a web-ui for Correx" resolved to a pre-existing parent
epic (webui-8, with children webui-9..14). The old prompt only said "name a
task that covers this work" and, separately, "after decomposing, name the
single ready child" — it never said what to do when an EXISTING task found
via task_search/task_context is itself a parent. The model oscillated
between naming the epic, re-decomposing, or working a child until it burned
all 16384 reasoning tokens and emitted nothing.

Reworded analyst_freestyle.md's task-framing section so every branch
(existing leaf, existing parent, no task yet + single unit, no task yet +
needs decomposition) converges on one rule: never name a parent/epic, always
name the single ready child — removing the decision entirely rather than
asking the model to make it under ambiguity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeyGFXczJb8RUWGBKmkm6G
2026-07-21 02:05:20 +04:00
kami bf36252736 feat(health): gate routing event-driven on connection drop, not just periodic poll (#300)
HealthMonitor's periodic poll took ~18s to notice a dead provider — long
after a retry had already re-selected it and died. Health needs to GATE
routing, not just log reactively.

- InferenceRouter.reportFailure(providerId, reason): new default-no-op
  method; DefaultInferenceRouter implements it by writing Unavailable
  straight into its health cache, bypassing healthCheck()/TTL entirely.
- SessionOrchestrator.runInference: on a connection-level exception
  (ConnectException/SocketException/IOException or a message matching
  "prematurely closed"/"connection refused"/"connection reset"), call
  reportFailure immediately so the very next route() call — the retry driven
  by #299 — sees the provider as down right away instead of re-selecting it
  and dying again.

Pairs with #299: routing now (1) reacts to a connection drop instantly and
(2) waits/backs off for the capability to recover before declaring terminal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeyGFXczJb8RUWGBKmkm6G
2026-07-21 02:01:35 +04:00
kami c5289420a1 fix(inference): tolerate a briefly-absent provider instead of killing the session (#299)
A transient provider connection drop (e.g. OOM-killed llama.cpp mid-request)
collapsed into a hard NoEligibleProviderException that escaped runInference's
try/catch (the route() call sat outside it), propagated past the stage retry
loop, and landed in ServerModule's session-level catch — failing the WHOLE
session even though the failure was retryable and the provider recovered
seconds later.

- SessionOrchestrator.runInference: wrap router.route() in try/catch so
  routing failures become InferenceResult.Failed and flow through the normal
  retryable/backoff/exhaustion machinery instead of escaping.
- DefaultInferenceRouter.route: distinguish "capability never configured on
  any provider" (fail fast, waiting can't help) from "configured but
  currently unhealthy" (bounded wait/backoff — default 3 attempts x 2s —
  re-checking health before declaring NoEligibleProvider terminal).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeyGFXczJb8RUWGBKmkm6G
2026-07-21 01:57:04 +04:00
kami a2bf976a13 fix(server): unify session workspace root (#266) 2026-07-21 01:48:45 +04:00
kami ae0b23df3f chore: sprint handoffs + Lsp4j runner live-proof
Add per-agent sprint handoff docs (Sonnet/Codex/opencode) mapping the
1-week sprint's two goals to concrete Vikunja tasks. Kept in repo root
(docs/ is gitignored). Includes hanging Lsp4jDiagnosticsRunner change +
its live-proof test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rdo9fe7SujNVeyZA8YkpkD
2026-07-21 00:36:25 +04:00