fix(kernel,workflow): five fixes from the 954da1a9 post-mortem (#705,#706,#709,#710,#712)

The run died on a build gate running the wrong toolchain and burned 43% of its
tool calls on repeats, rejections and failures. Five fixes, each traceable to a
measured cost in docs/audits/2026-08-11-session-954da1a9-postmortem.md.

#705 build gate toolchain scope. The gate is armed session-scoped but read the
toolchain stage-scoped, so a reviewer stage that wrote nothing fell back to the
flat `build` alias and ran ./gradlew assemble on an all-frontend session. It now
falls back to the session's own manifest first. 32.5 min, 33% of that run.

#706 action ledger. L2 keeps ten conversation entries, so a stage past round
five has no memory of what it tried; 29% of tool calls were byte-identical
repeats. One pinned line per call (tool, target, outcome) with repeats collapsed
to a count, ~3k tokens for a whole run.

#709 plan-compile lint for blocked runners. Plans prescribed `npx tailwindcss
init -p`, rejected by the shell denylist at every attempt. Rejected at compile
time now, where the architect can still rewrite the step.

#710 near-greedy sampling on tool-call rounds. temperature 1.0 on argv emitted
`./gradlew_`, `npm_prefix=frontend`, `create_vite@latest`. Prose rounds keep the
operator's sampling.

#712 auto-approve manifest-contained writes. 94 approvals, all APPROVED, no
steering, 19 min. A write inside the declared manifest already proved its
containment by getting past ManifestContainmentRule. DENY mode still denies.

Tests: core:kernel 133, infrastructure:workflow 99, testing:integration 176,
testing:deterministic 79, all green. detekt clean (no new findings).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dVqqci5H5b3s6xzv6Lojq
This commit is contained in:
2026-08-11 22:15:49 +04:00
parent 700f59ef0d
commit d892587420
12 changed files with 674 additions and 7 deletions
@@ -59,7 +59,11 @@ class DefaultContextPackBuilder(
// the model a progress signal that outlives the truncation which otherwise wipes its memory.
// "retryFeedback" carries WHY the last attempt failed + the files already written this stage; if
// truncation evicts it the model cold-starts on the same wrong idea every turn (the write-loop rot).
private val neverDropSourceTypes = setOf("steeringNote", "eventHistory", "factSheet", "remainingDelta", "retryFeedback")
// "actionLedger" is the stage's one-line-per-tool-call history (#706). L2 keeps only the last
// ten conversation entries, so without it a stage past round five re-issues calls it already
// made; the ledger is the memory that survives that eviction.
private val neverDropSourceTypes =
setOf("steeringNote", "eventHistory", "factSheet", "remainingDelta", "retryFeedback", "actionLedger")
private companion object {
const val CHARS_PER_TOKEN = 4