feat(recovery,context): tier-2 intent-holder arbiter + remaining-delta pinning + surfaced signal events

- recovery: two-tier repair ladder — owner then arbiter (recovery stage recast
  as intent-holder, holds initial intent, reconciles cross-file contract disputes)
  with independent INTENT_ROUTE_BUDGET; RecoveryRoutingTest coverage
- context: remaining-delta checklist pinning (RemainingDelta{Pinning,Entry}Test)
- surface write-only events (session name, quality signals) into stats/browse
- parseToolArguments lenient-Json fallback for bare-key drift
- tools: ChildProcess seam
This commit is contained in:
2026-07-11 23:56:52 +04:00
parent 3d5e05c1fb
commit 15248cae8a
55 changed files with 1932 additions and 231 deletions
@@ -27,9 +27,25 @@ private const val RECOVERY_PROMPT =
"You are the recovery stage. An upstream stage failed a verification gate it could not fix " +
"(it lacked write access). The failure ticket — the failing stage, the gate, and its exact " +
"output — is in your context under \"## Recovery ticket\". Read the gate output, inspect the " +
"relevant files (file_read / list_dir), and make the minimal edits that fix the reported " +
"cause. Do not re-scaffold or rewrite working code. When done, control returns to the stage " +
"that failed so its gate re-runs."
"relevant files (file_read / list_dir), then FIX the reported cause. You MUST make the " +
"change before finishing — investigation alone does not resolve the ticket, and handing " +
"back without an edit only re-triggers the same failure.\n" +
"Fix the actual cause the gate reports, whatever form it takes:\n" +
"- Broken code / config → edit it with file_write.\n" +
"- A MISSING file the build needs (e.g. package.json, tsconfig.json, a build/entry file " +
"the gate can't find) → produce it. A build gate failing with \"no such file\" is fixed by " +
"creating that file. If a whole project skeleton is missing, prefer the package manager's " +
"own generator via shell — e.g. `npm create vite@latest <dir> -- --template react-ts` — " +
"which writes a correct, complete file set (always pass the template/preset arg it " +
"requires; the run is non-interactive so there's no prompt to fall back on). Fall back to " +
"writing the files directly with file_write when no generator fits. `npm install` is " +
"available to fetch deps once package.json exists. Only bare remote runners (`npx`, " +
"`bunx`, `pnpx`) are blocked; use `npm create` instead.\n" +
"Do not gratuitously rewrite code that already works — touch only what the gate blames. But " +
"\"minimal\" means minimal to actually pass the gate, not zero.\n" +
"When the cause is repaired, finish with a short resolution summary that mirrors the ticket: " +
"the failing stage and gate, the root cause you found, and the exact files created/edited " +
"and commands run. Then control returns to the stage that failed so its gate re-runs."
// Freestyle plans don't specify a per-stage token budget, so without this every compiled stage
// inherits StageConfig's 4096 default — 1/8th of what the static role_pipeline execution stages
@@ -118,7 +134,13 @@ class ExecutionPlanCompiler(
StageId(s.id) to StageConfig(
produces = listOf(TypedArtifactSlot(name = ArtifactId(s.produces), kind = kind)),
needs = s.needs.map { ArtifactId(it) }.toSet(),
allowedTools = s.tools.toSet(),
// Every stage gets the full registered tool universe. The architect's per-stage
// `tools` pick was advisory and mis-scoped stages (e.g. an edit-shaped stage granted
// file_write but not file_edit → forced full-file rewrites; a repair stage unable to
// touch a file it must fix). writeManifest still jails WHICH paths a stage may write,
// so widening the tool grant does not widen write authority. Falls back to the
// architect's pick only when no universe was supplied (bare-constructor unit tests).
allowedTools = knownTools.ifEmpty { s.tools.toSet() },
writeManifest = writeManifest,
// Option A: the concrete (non-glob) subset of the declared writes are expected files
// the contract gate will require to exist. Globs (src/**) are write-permission scopes,
@@ -144,7 +166,7 @@ class ExecutionPlanCompiler(
null
} else {
StageId(RECOVERY_STAGE) to StageConfig(
allowedTools = setOf("file_write", "shell"),
allowedTools = knownTools.ifEmpty { setOf("file_write", "file_edit", "shell") },
tokenBudget = DEFAULT_STAGE_TOKEN_BUDGET,
generationConfig = DEFAULT_STAGE_GENERATION,
metadata = mapOf("role" to "recovery", "promptInline" to RECOVERY_PROMPT),