voidmachine 5.0.3 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "VoidCorp universal craftsman skills (void-tdd, void-typescript-strict, void-hexagonal-architecture, void-domain-driven-design, void-functional, void-code-review, void-api-and-interface-design, void-source-driven-development, void-context, void-learn, void-decide, void-claude-md, void-observability, void-security-guidance, ...) plus 12 PreToolUse enforcement hooks (tdd-guard, no-any, protect-sensitive-files, block-dangerous-bash, no-ai-design-slop, ...) and PostToolUse/SessionStart lifecycle hooks (incl. trim-large-output token frugality), five read-only critics including doctrine-critic, native architecture/security/QA specialists, and the void-graph, void-doctor and void-audit gestures, which are skills like everything else.",
5
5
  "author": {
6
6
  "name": "VoidCorp",
@@ -5,7 +5,7 @@
5
5
  "source": {
6
6
  "kind": "adapter",
7
7
  "version": "3.0.0-alpha.1",
8
- "rootHash": "sha256:69ada8c7a668574252b442c0bb1cde65d4d878973c9c59cc86871a147393bba9"
8
+ "rootHash": "sha256:56012433234f5e7d537066868543df205e92f817003384489c7e698778a49ab2"
9
9
  },
10
10
  "nodes": [
11
11
  {
@@ -3551,7 +3551,7 @@
3551
3551
  }
3552
3552
  ],
3553
3553
  "id": "skill:void-autopilot",
3554
- "lines": 394,
3554
+ "lines": 395,
3555
3555
  "name": "void-autopilot",
3556
3556
  "owner": "folpe",
3557
3557
  "pack": null,
@@ -3560,7 +3560,7 @@
3560
3560
  "codex"
3561
3561
  ],
3562
3562
  "source": "packages/core/skills/void-autopilot/SKILL.md",
3563
- "staticTokens": 6967,
3563
+ "staticTokens": 7051,
3564
3564
  "type": "skill"
3565
3565
  }
3566
3566
  },
@@ -4340,7 +4340,7 @@
4340
4340
  "codex"
4341
4341
  ],
4342
4342
  "source": "packages/core/skills/void-implement/SKILL.md",
4343
- "staticTokens": 5664,
4343
+ "staticTokens": 5800,
4344
4344
  "type": "skill"
4345
4345
  }
4346
4346
  },
@@ -4677,7 +4677,7 @@
4677
4677
  }
4678
4678
  ],
4679
4679
  "id": "skill:void-orchestrate",
4680
- "lines": 311,
4680
+ "lines": 327,
4681
4681
  "name": "void-orchestrate",
4682
4682
  "owner": "folpe",
4683
4683
  "pack": null,
@@ -4686,7 +4686,7 @@
4686
4686
  "codex"
4687
4687
  ],
4688
4688
  "source": "packages/core/skills/void-orchestrate/SKILL.md",
4689
- "staticTokens": 4402,
4689
+ "staticTokens": 4673,
4690
4690
  "type": "skill"
4691
4691
  }
4692
4692
  },
@@ -5373,7 +5373,7 @@
5373
5373
  }
5374
5374
  ],
5375
5375
  "id": "skill:void-verify",
5376
- "lines": 184,
5376
+ "lines": 203,
5377
5377
  "name": "void-verify",
5378
5378
  "owner": "folpe",
5379
5379
  "pack": null,
@@ -5382,7 +5382,7 @@
5382
5382
  "codex"
5383
5383
  ],
5384
5384
  "source": "packages/core/skills/void-verify/SKILL.md",
5385
- "staticTokens": 2267,
5385
+ "staticTokens": 2552,
5386
5386
  "type": "skill"
5387
5387
  }
5388
5388
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "harnessVersion": "5.0.3",
3
+ "harnessVersion": "5.0.4",
4
4
  "capabilities": [
5
5
  {
6
6
  "id": "skill:pack-mobile/void-eas-build-profile",
@@ -1703,7 +1703,7 @@
1703
1703
  }
1704
1704
  ],
1705
1705
  "id": "skill:void-autopilot",
1706
- "lines": 394,
1706
+ "lines": 395,
1707
1707
  "name": "void-autopilot",
1708
1708
  "owner": "folpe",
1709
1709
  "pack": null,
@@ -1712,7 +1712,7 @@
1712
1712
  "codex"
1713
1713
  ],
1714
1714
  "source": "packages/core/skills/void-autopilot/SKILL.md",
1715
- "staticTokens": 6967,
1715
+ "staticTokens": 7051,
1716
1716
  "type": "skill"
1717
1717
  },
1718
1718
  {
@@ -2204,7 +2204,7 @@
2204
2204
  "codex"
2205
2205
  ],
2206
2206
  "source": "packages/core/skills/void-implement/SKILL.md",
2207
- "staticTokens": 5664,
2207
+ "staticTokens": 5800,
2208
2208
  "type": "skill"
2209
2209
  },
2210
2210
  {
@@ -2415,7 +2415,7 @@
2415
2415
  }
2416
2416
  ],
2417
2417
  "id": "skill:void-orchestrate",
2418
- "lines": 311,
2418
+ "lines": 327,
2419
2419
  "name": "void-orchestrate",
2420
2420
  "owner": "folpe",
2421
2421
  "pack": null,
@@ -2424,7 +2424,7 @@
2424
2424
  "codex"
2425
2425
  ],
2426
2426
  "source": "packages/core/skills/void-orchestrate/SKILL.md",
2427
- "staticTokens": 4402,
2427
+ "staticTokens": 4673,
2428
2428
  "type": "skill"
2429
2429
  },
2430
2430
  {
@@ -2859,7 +2859,7 @@
2859
2859
  }
2860
2860
  ],
2861
2861
  "id": "skill:void-verify",
2862
- "lines": 184,
2862
+ "lines": 203,
2863
2863
  "name": "void-verify",
2864
2864
  "owner": "folpe",
2865
2865
  "pack": null,
@@ -2868,7 +2868,7 @@
2868
2868
  "codex"
2869
2869
  ],
2870
2870
  "source": "packages/core/skills/void-verify/SKILL.md",
2871
- "staticTokens": 2267,
2871
+ "staticTokens": 2552,
2872
2872
  "type": "skill"
2873
2873
  }
2874
2874
  ],
@@ -11093,6 +11093,7 @@ function executeContextContinuity(rawInput, root, runtime3, now) {
11093
11093
 
11094
11094
  init_define_VOID_SYNTAX_WORKER_IDENTITY();
11095
11095
  import { spawnSync as spawnSync2 } from "node:child_process";
11096
+ import { realpathSync as realpathSync5 } from "node:fs";
11096
11097
  import { homedir as homedir2 } from "node:os";
11097
11098
  import { join as join12 } from "node:path";
11098
11099
 
@@ -12984,6 +12985,39 @@ function metadataCommands(snapshot, event, sequence, contextPercent, workspaceMi
12984
12985
  return commands;
12985
12986
  }
12986
12987
 
12988
+ init_define_VOID_SYNTAX_WORKER_IDENTITY();
12989
+ var discoverySchema = object2({ result: object2({ process_info: object2({
12990
+ pane_id: string2(),
12991
+ foreground_processes: array(object2({
12992
+ pid: number2().check(int(), _gte(2))
12993
+ })).check(_minLength(1), _maxLength(256))
12994
+ }) }) });
12995
+ function foreground(output, pane) {
12996
+ try {
12997
+ const value = JSON.parse(output.toString("utf8"));
12998
+ if (record3(value)?.["error"] !== void 0) return void 0;
12999
+ const result = discoverySchema.safeParse(value);
13000
+ if (!result.success || result.data.result.process_info.pane_id !== pane) return void 0;
13001
+ return result.data.result.process_info.foreground_processes.map((item) => item.pid);
13002
+ } catch {
13003
+ return void 0;
13004
+ }
13005
+ }
13006
+ function proveOwnership(pids, call) {
13007
+ let pid = process.pid;
13008
+ const visited = /* @__PURE__ */ new Set();
13009
+ for (let depth = 0; depth < 64; depth++) {
13010
+ if (visited.has(pid)) return "parent-cycle";
13011
+ visited.add(pid);
13012
+ if (pids.includes(pid)) return void 0;
13013
+ const parent = call("ps", ["-o", "ppid=", "-p", String(pid)]).toString("utf8").trim();
13014
+ if (!/^[0-9]+$/.test(parent) || !Number.isSafeInteger(Number(parent))) return "invalid-parent";
13015
+ pid = Number(parent);
13016
+ if (pid <= 1) return "ownership-unproven";
13017
+ }
13018
+ return "parent-depth-exceeded";
13019
+ }
13020
+
12987
13021
  init_define_VOID_SYNTAX_WORKER_IDENTITY();
12988
13022
  import { createHash as createHash4 } from "node:crypto";
12989
13023
  import { closeSync as closeSync3, constants as constants4, fstatSync as fstatSync3, lstatSync as lstatSync4, opendirSync, openSync as openSync3, readSync as readSync3, realpathSync as realpathSync4 } from "node:fs";
@@ -13294,9 +13328,9 @@ function executeHerdrMetadata(raw, root, env, runtime3, run = runCommand) {
13294
13328
  };
13295
13329
  try {
13296
13330
  const common = call("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"]).trim();
13331
+ const inventory = () => paneList.parse(JSON.parse(call("herdr", ["pane", "list"]))).result.panes;
13297
13332
  const snapshot = readOrchestra(join12(env["HOME"] ?? homedir2(), ".local/state/orchestra"), common, root, () => {
13298
- const response = paneList.parse(JSON.parse(call("herdr", ["pane", "list"])));
13299
- return response.result.panes.flatMap((pane) => typeof pane.label === "string" && typeof pane.cwd === "string" ? [{
13333
+ return inventory().flatMap((pane) => typeof pane.label === "string" && typeof pane.cwd === "string" ? [{
13300
13334
  pane_id: pane.pane_id,
13301
13335
  workspace_id: pane.workspace_id,
13302
13336
  label: pane.label,
@@ -13304,7 +13338,29 @@ function executeHerdrMetadata(raw, root, env, runtime3, run = runCommand) {
13304
13338
  ...pane.tokens === void 0 ? {} : { tokens: pane.tokens }
13305
13339
  }] : []);
13306
13340
  });
13307
- if (snapshot === void 0) return { status: "skipped", details: { reason: "no-orchestra-mission" } };
13341
+ if (snapshot === void 0) {
13342
+ if (event !== "SessionStart") return { status: "skipped", details: { reason: "no-orchestra-mission" } };
13343
+ const paneId = env["HERDR_PANE_ID"];
13344
+ if (!paneId || !env["HERDR_SOCKET_PATH"]) throw Error("missing-herdr-context");
13345
+ const panes = inventory();
13346
+ const matches2 = panes.filter((pane2) => pane2.pane_id === paneId);
13347
+ const pane = matches2[0];
13348
+ if (matches2.length !== 1 || !pane || !pane.cwd || realpathSync5(pane.cwd) !== realpathSync5(root)) {
13349
+ throw Error("Herdr caller pane missing or foreign worktree");
13350
+ }
13351
+ if (pane.label && panes.filter((other) => other.label === pane.label && typeof other.cwd === "string" && realpathSync5(other.cwd) === realpathSync5(root)).length !== 1) {
13352
+ throw Error("ambiguous Herdr coordinator identity");
13353
+ }
13354
+ const pids = foreground(Buffer.from(call("herdr", ["pane", "process-info", "--pane", paneId])), paneId);
13355
+ if (pids === void 0) throw Error("invalid Herdr process discovery");
13356
+ const denied = proveOwnership(pids, (command, args) => Buffer.from(call(command, args)));
13357
+ if (denied !== void 0) throw Error(denied);
13358
+ return { status: "ok", details: { host: "herdr", mission: "absent" }, output: { hookSpecificOutput: {
13359
+ hookEventName: "SessionStart",
13360
+ additionalContext: `Validated Herdr caller: ${paneId}. No Orchestra mission selected.
13361
+ Use void-orchestrate before delegating a ticket: register the mission, create or reuse its worker tab, and launch the worker once. Revalidate live identity before acting; this hook never launches workers.`
13362
+ } } };
13363
+ }
13308
13364
  const workspace = snapshot.coordinator ? record3(record3(JSON.parse(call("herdr", ["workspace", "get", snapshot.pane.workspace_id])))?.["result"]) : void 0;
13309
13365
  const workspaceMission = record3(record3(workspace?.["workspace"])?.["tokens"])?.["mission"];
13310
13366
  const percent = measureHerdrContext(input, root, runtime3, now);
@@ -13341,8 +13397,17 @@ Read mission.md and the current brief/report before resuming; Herdr IDs are hint
13341
13397
  };
13342
13398
  } catch (error) {
13343
13399
  const reason = (error instanceof Error ? error.message : String(error)).slice(0, 500);
13344
- return { status: "degraded", details: { reason }, diagnostic: `herdr-metadata: ${reason}
13345
- ` };
13400
+ return {
13401
+ status: "degraded",
13402
+ details: { reason },
13403
+ diagnostic: `herdr-metadata: ${reason}
13404
+ `,
13405
+ ...event === "SessionStart" ? { output: { hookSpecificOutput: {
13406
+ hookEventName: "SessionStart",
13407
+ additionalContext: `Herdr discovery refused: ${reason}.
13408
+ Do not act on this host until resolved. Native delegation independent of that action may continue.`
13409
+ } } } : {}
13410
+ };
13346
13411
  }
13347
13412
  }
13348
13413
 
@@ -13356,41 +13421,10 @@ var runCommand2 = (command, args, options) => {
13356
13421
  if (result.error !== void 0 || result.status !== 0) throw Error("process-failed");
13357
13422
  return result.stdout;
13358
13423
  };
13359
- var discoverySchema = object2({ result: object2({ process_info: object2({
13360
- pane_id: string2(),
13361
- foreground_processes: array(object2({
13362
- pid: number2().check(int(), _gte(2))
13363
- })).check(_minLength(1), _maxLength(256))
13364
- }) }) });
13365
13424
  function refused(reason) {
13366
13425
  return { status: "degraded", details: { reason }, diagnostic: `herdr-session: ${reason}
13367
13426
  ` };
13368
13427
  }
13369
- function foreground(output, pane) {
13370
- try {
13371
- const value = JSON.parse(output.toString("utf8"));
13372
- if (record3(value)?.["error"] !== void 0) return void 0;
13373
- const result = discoverySchema.safeParse(value);
13374
- if (!result.success || result.data.result.process_info.pane_id !== pane) return void 0;
13375
- return result.data.result.process_info.foreground_processes.map((item) => item.pid);
13376
- } catch {
13377
- return void 0;
13378
- }
13379
- }
13380
- function proveOwnership(pids, call) {
13381
- let pid = process.pid;
13382
- const visited = /* @__PURE__ */ new Set();
13383
- for (let depth = 0; depth < 64; depth++) {
13384
- if (visited.has(pid)) return "parent-cycle";
13385
- visited.add(pid);
13386
- if (pids.includes(pid)) return void 0;
13387
- const parent = call("ps", ["-o", "ppid=", "-p", String(pid)]).toString("utf8").trim();
13388
- if (!/^[0-9]+$/.test(parent) || !Number.isSafeInteger(Number(parent))) return "invalid-parent";
13389
- pid = Number(parent);
13390
- if (pid <= 1) return "ownership-unproven";
13391
- }
13392
- return "parent-depth-exceeded";
13393
- }
13394
13428
  function nativeHook(env) {
13395
13429
  const path2 = join13(
13396
13430
  env["CODEX_HOME"] || join13(env["HOME"] || homedir3(), ".codex"),
@@ -13453,7 +13487,7 @@ function executeHerdrSession(input, env, runtime3, run = runCommand2, clock = ()
13453
13487
 
13454
13488
  init_define_VOID_SYNTAX_WORKER_IDENTITY();
13455
13489
  import { randomUUID } from "node:crypto";
13456
- import { lstatSync as lstatSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync3, realpathSync as realpathSync5, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "node:fs";
13490
+ import { lstatSync as lstatSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync3, realpathSync as realpathSync6, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "node:fs";
13457
13491
  import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute7, join as join16 } from "node:path";
13458
13492
 
13459
13493
  init_define_VOID_SYNTAX_WORKER_IDENTITY();
@@ -13512,20 +13546,20 @@ function claimedPath(root, sessionId) {
13512
13546
  if (!within(runs, path2) || lstatSync5(dirname6(path2), { throwIfNoEntry: false })?.isDirectory() !== true) {
13513
13547
  return void 0;
13514
13548
  }
13515
- return within(realpathSync5(runs), realpathSync5(dirname6(path2))) ? path2 : void 0;
13549
+ return within(realpathSync6(runs), realpathSync6(dirname6(path2))) ? path2 : void 0;
13516
13550
  } catch {
13517
13551
  return void 0;
13518
13552
  }
13519
13553
  }
13520
13554
  function waitingRun(root, cwd, now) {
13521
13555
  try {
13522
- const here = realpathSync5(cwd);
13556
+ const here = realpathSync6(cwd);
13523
13557
  const directory = join16(root, "agents", "pending");
13524
13558
  return readdirSync3(directory).filter((name) => !name.startsWith(".")).slice(0, 64).some((name) => {
13525
13559
  const marker = record3(JSON.parse(readFileSync11(join16(directory, name), "utf8")));
13526
13560
  const createdAt = marker?.["createdAt"];
13527
13561
  const runCwd = marker?.["cwd"];
13528
- return typeof createdAt === "number" && now - createdAt < PARKING_WINDOW_MS && typeof runCwd === "string" && realpathSync5(runCwd) === here;
13562
+ return typeof createdAt === "number" && now - createdAt < PARKING_WINDOW_MS && typeof runCwd === "string" && realpathSync6(runCwd) === here;
13529
13563
  });
13530
13564
  } catch {
13531
13565
  return false;
@@ -13953,7 +13987,7 @@ import { createHash as createHash5 } from "node:crypto";
13953
13987
  import {
13954
13988
  lstatSync as lstatSync7,
13955
13989
  mkdirSync as mkdirSync5,
13956
- realpathSync as realpathSync6,
13990
+ realpathSync as realpathSync7,
13957
13991
  writeFileSync as writeFileSync4
13958
13992
  } from "node:fs";
13959
13993
  import { join as join18, relative as relative7 } from "node:path";
@@ -14024,11 +14058,11 @@ ${errors}
14024
14058
 
14025
14059
  function safeOutputDirectory(root) {
14026
14060
  try {
14027
- const canonicalRoot = realpathSync6(root);
14061
+ const canonicalRoot = realpathSync7(root);
14028
14062
  const directory = voidMachinePath(root, "outputs");
14029
14063
  mkdirSync5(directory, { recursive: true, mode: 448 });
14030
14064
  const info = lstatSync7(directory);
14031
- const canonicalDirectory2 = realpathSync6(directory);
14065
+ const canonicalDirectory2 = realpathSync7(directory);
14032
14066
  if (!info.isDirectory() || info.isSymbolicLink() || !within(canonicalRoot, canonicalDirectory2)) {
14033
14067
  return void 0;
14034
14068
  }
@@ -14064,7 +14098,7 @@ function executeTrim(rawInput, root, env) {
14064
14098
  const hash = createHash5("sha256").update(extracted.text).digest("hex").slice(0, 12);
14065
14099
  const tool = extracted.tool.replaceAll(/[^A-Za-z0-9_]/g, "_").slice(0, 80);
14066
14100
  const file = join18(directory, `${tool}-${process.pid}-${Date.now()}-${hash}.log`);
14067
- const spillPath = relative7(realpathSync6(root), file).replaceAll("\\", "/");
14101
+ const spillPath = relative7(realpathSync7(root), file).replaceAll("\\", "/");
14068
14102
  const plan = planOutputTrim(extracted.text, {
14069
14103
  tool: extracted.tool,
14070
14104
  thresholdBytes,
@@ -14351,13 +14385,13 @@ Resolve before claiming done. This never blocks.
14351
14385
 
14352
14386
  init_define_VOID_SYNTAX_WORKER_IDENTITY();
14353
14387
  import { spawnSync as spawnSync8 } from "node:child_process";
14354
- import { closeSync as closeSync4, existsSync as existsSync8, lstatSync as lstatSync8, openSync as openSync4, readSync as readSync4, realpathSync as realpathSync7 } from "node:fs";
14388
+ import { closeSync as closeSync4, existsSync as existsSync8, lstatSync as lstatSync8, openSync as openSync4, readSync as readSync4, realpathSync as realpathSync8 } from "node:fs";
14355
14389
  import { dirname as dirname8, isAbsolute as isAbsolute10, join as join21, resolve as resolve9 } from "node:path";
14356
14390
  var GIT_TIMEOUT_MS2 = 5e3;
14357
14391
  var GIT_MAX_OUTPUT_BYTES = 1e6;
14358
14392
  function canonical2(path2) {
14359
14393
  try {
14360
- return realpathSync7(resolve9(path2));
14394
+ return realpathSync8(resolve9(path2));
14361
14395
  } catch {
14362
14396
  return resolve9(path2);
14363
14397
  }
@@ -90,8 +90,8 @@ skip a step, merge, or decide that a refusal does not apply to it.
90
90
 
91
91
  **Orchestrator.** Runs the loop. Evaluates no ticket, edits no code, reads no diff.
92
92
 
93
- **Workers**, one per slot. Each carries one ticket from claim to an open pull request, in its own
94
- worktree, by running `void-implement` whole.
93
+ **Workers**, one per slot, are commit-only. Each carries one ticket to a committed candidate
94
+ and proof references in its own worktree; the coordinator owns pushes and pull requests.
95
95
 
96
96
  **Reviewer.** The independent pass of `void-implement`, invoked by ORCH through
97
97
  `void-orchestrate` in a fresh read-only context on the exact head SHA. `autopilot review`
@@ -203,8 +203,8 @@ in flight set it when they create or push their branch and drop it with a merged
203
203
  own settings still count. So start each worktree from
204
204
  `origin/<base>` and never move a local base branch while units are in flight. The baseline
205
205
  recorded at `assign` is written once: a second `--before` for the same ticket is refused, so a unit
206
- cannot re-record the state it left as the state it found. It is compared by the worker before it
207
- pushes, with `autopilot fingerprint --after <ticket>`, and again by the kernel before it arms a
206
+ cannot re-record the state it left as the state it found. It is compared by the coordinator before it
207
+ publishes, with `autopilot fingerprint --after <ticket>`, and again by the kernel before it arms a
208
208
  merge. A changed or missing baseline sends the ticket to a human, unpublished; only that person
209
209
  deletes the record.
210
210
 
@@ -273,21 +273,22 @@ It runs `void-implement` whole in that worktree, and starts its mission with the
273
273
  mission what it has done: each specialist result through `mission specialist-event`, each proof
274
274
  through `mission verify`, a committed candidate through `mission writer-event`. The journal accepts
275
275
  only these typed events, no free note; what is not yet one of them does not survive a respawn, and
276
- uncommitted edits survive only in the worktree. When its proofs are green it runs
277
- `autopilot fingerprint --after <ticket>`, pushes its own branch, opens one pull request towards the
278
- base, ready for review rather than as a draft, and moves the ticket to In Review. The reviewer the
279
- orchestrator invokes is that cycle's independent review; it reviews ready pull requests only, so a
280
- draft waits.
276
+ uncommitted edits survive only in the worktree. During development, the coordinator may create or reuse a draft pull request
277
+ for the branch; drafts are permitted for development, never for merge admission. After the worker
278
+ returns its commit and green proofs, the coordinator runs `autopilot fingerprint --after <ticket>`,
279
+ pushes, and must mark the same pull request ready (create one only if absent), then move the ticket
280
+ to In Review. The independent reviewer examines ready pull requests only; an unfinished draft waits.
281
281
  Blocking findings come back as a hand-back and are corrected as a batch, per `void-implement`.
282
282
 
283
283
  On a hand-back the worker reads the reason: failing checks, blocking findings, a conflict, or a
284
284
  base that moved. It updates its branch by merging the base into it,
285
- never by rewriting pushed history, re-runs its proofs, and pushes again.
285
+ never by rewriting pushed history, re-runs its proofs, and returns its commit to the coordinator.
286
+ The coordinator updates the same PR; the worker does not create another one.
286
287
 
287
288
  May: run every `void-implement` pass whose predicate fires, run its own gates, apply a migration in
288
- dev/local only, push its own branch without force, and open or update its own pull request.
289
+ dev/local only, and return its commits and proofs to the coordinator.
289
290
 
290
- May not: enable auto-merge, merge anything, post or record a verdict, run `autopilot review`, move a ticket to Done, close or cancel a ticket, touch another ticket's branch or worktree,
291
+ May not: enable auto-merge, merge anything, post or record a verdict, run `autopilot review`, push, create or change pull requests, move a ticket to Done, close or cancel a ticket, touch another ticket's branch or worktree,
291
292
  prune the mission journals, or write the git state the repository shares -- `refs/stash`, tags,
292
293
  notes, remotes, the repository config.
293
294
 
@@ -332,7 +333,7 @@ head, then sends the ticket to a human. An ejected head whose own checks fail go
332
333
  worker as failing checks. A pull request in conflict with the base goes back to its worker. The
333
334
  worker classifies the conflict as the typed `conflict` judgment on the conflicting head, posted
334
335
  through `autopilot judgment conflict-class`: `mechanical` it resolves, re-runs
335
- its proofs and pushes; `semantic` -- two intents that disagree -- it leaves alone, and the kernel
336
+ its proofs and returns its commit for coordinator publication; `semantic` -- two intents that disagree -- it leaves alone, and the kernel
336
337
  sends the ticket to a human. A worker never picks a side of a semantic conflict to keep the loop
337
338
  moving.
338
339
 
@@ -102,7 +102,7 @@ Run in order. Each pass names the skill it composes and the predicate that fires
102
102
  3. **Source grounding** (ALWAYS; the source depends on what the change touches). Read the relevant sources before the first line, per the Anti-rustine section of `PHILOSOPHY.md`. For a third-party configuration, schema or call signature, compose `void-source-driven-development`: official documentation **of the installed version**, never an upgrade by default. For internal code, read the owning contract, types, source and project docs of what you are about to change. Assumed semantics produce bugs that cost hours to find, and the assumption is invisible in the diff — which is why this runs before the writing rather than at review. Cite a source briefly when it helps understand or record a choice; a missing citation never blocks on its own.
103
103
  4. **Migration safety** (IF it changes a DB schema or ships a migration). Compose `void-migrations` (and `void-drizzle-migration-safe` on a Drizzle/Postgres stack): zero-downtime, two-phase, batched backfill, locking analysis. **Once the migration is generated and safety-reviewed, apply it to the dev/local database before the TDD and E2E passes run** — otherwise those tests execute against a stale schema and prove nothing about the new shape. **This cycle only ever applies to dev/local; production migrations run through CI / GitHub Actions on merge, never from a worker or this session** (see the `void-migrations` anti-rule). A schema change must never reach the rest of the cycle without this pass.
104
104
  5. **Convene the panel** (ALWAYS once, in `team` mode). With `panel.provider: orchestrator`, consume ORCH's supplied panel and never launch another. Otherwise call `mission dispatch --json`: its first `invoke-specialists` action is at `stage: 'pre-implementation'`, before code. Compose `void-orchestrate` for native invocation. Pass the envelope's runtime, agentName, missionId, contextPack, reviewSubject and reviewScope unchanged; record actual starts and complete results using `mission specialist-event`. Reconcile an existing invocation before dispatch; uncertain identity means no launch. Keep at most four simultaneous agents, wait in bounded slices, and never fail an agent merely because a wait timed out. Findings brief the single writer; a concrete blocked verdict is resolved before implementation. Later controller stages reuse this transport and never replay preparation. Missing required capability is diagnosed, never fabricated.
105
- 6. **TDD implementation** (ALWAYS). The single lead writer composes `void-tdd` + `void-testing`. Red, green, refactor. Unit tests for the behavior, green before moving on. Review specialists remain read-only.
105
+ 6. **TDD implementation** (ALWAYS). During development, the coordinator may create or reuse a draft pull request for the branch. Keep work-in-progress PRs in draft and run focused local checks; do not wait for full remote CI per iteration. `void-verify` owns the transition to complete integration checks and its portable CI template. The single lead writer composes `void-tdd` + `void-testing`. Red, green, refactor. Unit tests for the behavior, green before moving on. Review specialists remain read-only.
106
106
  7. **Async + idempotency** (IF it sends email, calls an external side-effecting API, enqueues a job, or mints a single-use token). Compose `void-async-safety`: idempotency keys, replay/dedup window, bounded retries, single-use enforcement.
107
107
  8. **End-to-end tests** (IF it touches a user-facing flow). Write/extend the E2E suite (Playwright). The path a user actually walks, not just the unit.
108
108
  9. **UX/UI pass** (IF it touches a UI surface). The interface is held to production craft, not just "it renders". Compose `void-frontend-design` (build-time craft) + `void-ui-review` (the audit/critique ceiling: AI-slop test, squint test, interaction-state coverage, technical audit) + `void-accessibility`, across the baseline (BACK+FRONT parity, mobile and desktop, and the states a user actually hits — loading / error / empty). Browser-verified QA (live screenshots) runs via `void-qa` (the claude-in-chrome MCP). A UI ticket is not shippable until this pass is **verified, not assumed**.
@@ -115,7 +115,7 @@ Run in order. Each pass names the skill it composes and the predicate that fires
115
115
  This exists because the passes above have a measured hit rate of zero on a whole class of defect. Six shipped on 2026-08-06 with typecheck, lint, 2,700 tests and five CI checks green: five were caught by using the thing on the real tree, one by the harness itself, none by CI and none by the human merge gate. One of them reported "5 path(s) not extracted" about files it had read, parsed and indexed — the exact lie the feature existed to prevent — and it surfaced by running the command on the repo, after merge. Under autopilot nobody comes back to the previous ticket, so a defect found that way is a defect not found at all.
116
116
  **A fixture is not a dogfood.** The fixture is the shortcut that would have missed every one of the six: the two oversized generated artifacts that exposed the false count existed only in the real tree. **And "the command did not crash" is not an observation** — the output has to be read, and quoted rather than summarised as "works". A surface that genuinely cannot run here (a deploy, a remote service) has its limitation named, never worked around in silence. Keep the run read-only or reversible.
117
117
  14. **Dispose of what you found** (ALWAYS). A cycle surfaces defects that are not this ticket: an incoherent skill, a stale doc, a contract that lies. Do not file them on their merits. Compare each against the unit you are building, judged against the program objective: it either blocks this unit and is fixed here, or it beats this unit and becomes the next one, or it is **dropped**. Write one recap line per finding naming what it lost to. Record only evidence that cannot be reconstructed — a failure seen once, a measurement — with the command that replays it. The admission rules are `void-ticket`'s; this pass is where they fire.
118
- 15. **Ship** (ALWAYS). Compose `void-commit-discipline`, open the PR, attach the PR and verification evidence, then move a tracker-backed ticket to **In Review**. Move it to **Done** only after merge and final verification of the merged state. Commits are **bisectable** — one logical change each, dependency-ordered (infra → domain + tests → edge/UI + tests), each independently valid. Under `void-autopilot` the worker pushes its own branch and opens the PR, but never enables auto-merge, merges, posts the review status or moves the ticket to Done; the loop's reviewer performs the independent review of step 11 on the PR head SHA.
118
+ 15. **Ship** (ALWAYS). Compose `void-commit-discipline`, reuse the existing PR for the branch (create one only if absent), attach verification evidence, and mark the same pull request ready when the candidate meets the review handoff requirements. Then move a tracker-backed ticket to **In Review**. Move it to **Done** only after merge and final verification of the merged state. Commits are **bisectable** — one logical change each, dependency-ordered (infra → domain + tests → edge/UI + tests), each independently valid. Under `void-autopilot` the worker is commit-only: it returns the commit and proof references to the coordinator, who owns pushes, PR creation/readiness and tracker updates. The worker never enables auto-merge, merges, posts the review status or moves the ticket to Done; the loop's reviewer performs the independent review of step 11 on the PR head SHA.
119
119
 
120
120
  ---
121
121
 
@@ -40,6 +40,22 @@ or act on an unrelated project. Native work independent of that action may conti
40
40
  mandatory independent review or required isolation remains a blocker; do not
41
41
  replace it with self-review or silently weaken its requirement.
42
42
 
43
+ ## First ticket and optional worker tabs
44
+
45
+ SessionStart discovers Herdr even before central mission files exist. Its advice
46
+ requires a fresh caller pane at the canonical checkout and process ownership;
47
+ environment variables alone never authorize a host action. Revalidate before launch.
48
+ On a new implementation ticket, reuse the assigned writer if one exists. Otherwise,
49
+ in a validated Herdr session, register the mission and planned worker first, then
50
+ create one background tab in the resolved coordinator workspace with the assigned
51
+ worktree as cwd. Read the installed `herdr tab` schema, preserve focus with
52
+ `--no-focus`, and use the returned root pane for the single agent start below.
53
+ Reattach a matching existing worker instead of opening another tab. The coordinator
54
+ stays in its existing pane. Hooks only discover and advise; they never create tabs,
55
+ launch agents or write central files. Without Herdr, use native delegation normally.
56
+ An invalid host or explicit mission selection refuses only that host action;
57
+ independent native work may continue with the limitation stated.
58
+
43
59
  ## Brief and bound the delegation
44
60
 
45
61
  Use a fresh context by default. Choose a supported fork only when the task needs
@@ -10,3 +10,5 @@ Sources for `void-verify` skill (see `docs/plans/skill-audits/void-verify.md` fo
10
10
  Strategy: distill. Slim skill. Adapted for void-machine's specific signal set (12 items, each mapped to a skill or hook).
11
11
 
12
12
  No new hooks (composes with all existing).
13
+
14
+ - GitHub Actions event, concurrency and merge-queue documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows ; https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency ; https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue
@@ -19,6 +19,25 @@ If you find yourself wanting to skip the checklist "because it's a small change,
19
19
 
20
20
  ---
21
21
 
22
+ ## Local development and integration
23
+
24
+ During development, run the smallest meaningful configured test command against the
25
+ changed behavior, plus affected types and local hooks. Keep the PR in draft; do not
26
+ wait for full remote CI after each edit or commit. Fresh local evidence still gates
27
+ claims about the behavior tested. `commands.testUnit` and `commands.typecheck` in
28
+ `.void/config.json` describe project commands; preserve their argv.
29
+
30
+ When the candidate is committed and independently reviewed, run the applicable final
31
+ checks below and mark the PR ready. Wait for real required CI on that head and, if
32
+ configured, the merge group before integration. Local success cannot replace a required
33
+ server check. Returning to draft cancels obsolete PR work without resetting reviews.
34
+
35
+ For an explicitly requested CI setup or optimization, use the installed
36
+ [executable admission guard and workflow template](references/ci-feedback.md).
37
+ They defer draft validation with an explicit non-success outcome, rerun on readiness,
38
+ and preserve merge queue checks. Never install them or change remote protection merely
39
+ because this skill was invoked.
40
+
22
41
  ## The checklist
23
42
 
24
43
  Read item by item. Do not run from memory. Each item is OBSERVED, not assumed.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // tdd-cover: e2e packages/cli/src/lib/ci-feedback.test.ts
3
+ // An admission refusal is not test evidence. Keep this step inside the required
4
+ // quality job: a skipped required job reports success in GitHub Actions.
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ try {
8
+ const name = process.env.GITHUB_EVENT_NAME;
9
+ if (!['pull_request', 'merge_group', 'push', 'workflow_dispatch'].includes(name)) {
10
+ throw new Error('CI_INVALID_EVENT: unsupported or missing GitHub event.');
11
+ }
12
+ const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
13
+ if (name === 'pull_request') {
14
+ if (typeof event?.pull_request?.draft !== 'boolean') {
15
+ throw new Error('CI_INVALID_EVENT: pull request draft state is missing or malformed.');
16
+ }
17
+ if (event.pull_request.draft) {
18
+ throw new Error('CI_DEFERRED_DRAFT: preparation only; run focused tests locally, then mark ready for review to start integration validation. No tests were run.');
19
+ }
20
+ }
21
+ process.stdout.write('CI_ADMITTED: run the complete integration checks for this revision.\n');
22
+ } catch (error) {
23
+ process.stderr.write(`${error instanceof Error ? error.message : 'CI_INVALID_EVENT: unreadable event.'}\n`);
24
+ process.exitCode = 1;
25
+ }
@@ -0,0 +1,51 @@
1
+ # Fast local feedback, real integration checks
2
+
3
+ The sibling files are shipped with this skill and copied by `init` into the selected
4
+ runtime's skill directory. Resolve them relative to the SKILL.md you actually loaded,
5
+ not from a developer checkout or a global installation.
6
+
7
+ On an explicit CI configuration request:
8
+
9
+ 1. Inspect existing workflows, required check names, package manager, branch names,
10
+ review publication and release dependencies. Preserve their owners. Do not overwrite
11
+ existing workflows with this example or install a second quality workflow.
12
+ 2. Adopt the behavior in `quality.yml` into the existing quality workflow. Copy
13
+ `ci-admission.mjs` to `.github/scripts/ci-admission.mjs`. Keep the admission step
14
+ inside an existing **required** quality job, before dependency installation, without
15
+ `continue-on-error`, `always()` on later quality steps, or a job-level draft skip.
16
+ 3. Set up the project's actual package manager and immutable lockfile installation.
17
+ The example uses npm. For pnpm/Bun use their pinned setup and frozen install.
18
+ Define `ci:verify` in package.json to run the actual complete lint, typecheck, build,
19
+ and test commands appropriate to the project. Do not equate `testUnit` with the
20
+ whole integration suite; discover the project's existing CI contract. Missing
21
+ `ci:verify` fails rather than silently passing. Never use `--if-present` or `true`.
22
+ 4. Preserve remote protection and exact-head checks. Keep the required check names
23
+ stable. Review protection adoption separately if no quality check is required;
24
+ this template alone does not configure GitHub protection. Retain the separate
25
+ `void-enforce` floor and independent review. This template does not replace either.
26
+ 5. Test draft, ready without another commit, new head, return to draft, merge group,
27
+ and manual run. `CI_DEFERRED_DRAFT` is expected non-admission during preparation,
28
+ not a failed test: tests have not run. Readiness runs the full suite even at the
29
+ same SHA. Never treat a skipped required job as successful test evidence.
30
+
31
+ Development runs configured local tests for the changed behavior and affected types.
32
+ There is no network hook or mandatory remote wait per edit. Only obsolete PR runs are
33
+ cancelled. Distinct merge groups, manual runs and release runs must have distinct
34
+ concurrency keys: `cancel-in-progress: false` alone still replaces pending runs in a
35
+ shared group. Keep integration checks before entry to the protected branch; checking
36
+ only after merge permits broken integration.
37
+
38
+ GitHub couples required PR and merge queue checks. Preserve both. Remove a redundant
39
+ post-merge run only after checking that no release, promotion or artifact consumer
40
+ needs it, and that the protected integration path proves the merged result. The
41
+ example has no push trigger; add one where it provides distinct release evidence.
42
+
43
+ Rollback: restore the prior workflow triggers and admission step together. Restoring
44
+ full draft CI costs runner time but does not weaken the floor. Leave protection and
45
+ review evidence unchanged. Existing missions and receipts are not migrated.
46
+
47
+ Sources read for GitHub Actions (hosted service):
48
+ - https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request
49
+ - https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
50
+ - https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue
51
+ - https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-jobs-with-conditions
@@ -0,0 +1,34 @@
1
+ # Copy this template and its sibling ci-admission.mjs into .github/workflows/
2
+ # and .github/scripts/ respectively. Read ci-feedback.md before adoption.
3
+ # Adapt branch names and package-manager setup to the project. Define ci:verify
4
+ # in package.json as its real complete validation command; absence fails closed.
5
+ name: quality
6
+ on:
7
+ pull_request:
8
+ types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
9
+ merge_group:
10
+ types: [checks_requested]
11
+ workflow_dispatch: {}
12
+ permissions:
13
+ contents: read
14
+ concurrency:
15
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
16
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
17
+ jobs:
18
+ quality:
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 20
21
+ steps:
22
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
23
+ with:
24
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
25
+ persist-credentials: false
26
+ - name: Admit integration validation
27
+ run: node .github/scripts/ci-admission.mjs
28
+ - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
29
+ with:
30
+ node-version: 24
31
+ cache: npm
32
+ - run: npm ci
33
+ - name: Run the project integration checks
34
+ run: npm run ci:verify
package/dist/main.js CHANGED
@@ -35768,6 +35768,7 @@ init_esm_shims();
35768
35768
 
35769
35769
  // ../mission-engine/dist/orchestration/review-loop.js
35770
35770
  init_esm_shims();
35771
+ var BOUNDED_REVIEW_BUDGET = { maxRounds: 3, maxCorrectionBatches: 2 };
35771
35772
  var SHA256 = /^sha256:[a-f0-9]{64}$/;
35772
35773
  function validNativeContextId(value) {
35773
35774
  return typeof value === "string" && value.trim().length > 0 && value.length <= 160 && [...value].every((char) => char.charCodeAt(0) >= 32 && (char.charCodeAt(0) < 127 || char.charCodeAt(0) > 159));
@@ -36727,7 +36728,7 @@ function staleDispatchCompletions(events) {
36727
36728
  function specialistId2(value) {
36728
36729
  return /^core:[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
36729
36730
  }
36730
- function validProvenanceRecovery(input2, existing) {
36731
+ function validProvenanceRecovery(input2, existing, afterSeqExclusive = 0) {
36731
36732
  const disposition = input2.request.disposition;
36732
36733
  const observation2 = input2.observation;
36733
36734
  const bindings = observation2.reviewBindings;
@@ -36741,10 +36742,10 @@ function validProvenanceRecovery(input2, existing) {
36741
36742
  const observedSubject = observation2.reviewSubject;
36742
36743
  return bindings.every((binding3) => {
36743
36744
  const original = existing.find((item) => item.event.eventId === binding3.completionEventId);
36744
- if (original === void 0 || !disposition.completionEventIds.includes(binding3.completionEventId) || original.event.subject !== "core:independent-code-reviewer" || original.event.source !== observation2.expectedSource || original.completion.verdict !== "pass" || original.completion.findings.some((finding) => finding.classification !== "advisory") || original.completion.contractVersion !== observation2.contractVersions[original.event.subject] || field4(original.event, "inputHash") !== observation2.currentInputHashes[original.event.subject] || canonicalJsonHash(original.completion) !== binding3.completionHash || !sameReviewSubject(binding3.review, observedSubject) || binding3.review.taskId !== original.event.missionId || binding3.review.writerId !== writerId || binding3.review.provenance.kind !== "native-context" || binding3.review.provenance.contextId !== field4(original.event, "contextId"))
36745
+ if (parseReviewReceipt(binding3.review) === void 0 || original === void 0 || original.event.seq <= afterSeqExclusive || !disposition.completionEventIds.includes(binding3.completionEventId) || original.event.subject !== "core:independent-code-reviewer" || original.event.source !== observation2.expectedSource || original.completion.verdict !== "pass" || original.completion.findings.some((finding) => finding.classification !== "advisory") || original.completion.contractVersion !== observation2.contractVersions[original.event.subject] || field4(original.event, "inputHash") !== observation2.currentInputHashes[original.event.subject] || canonicalJsonHash(original.completion) !== binding3.completionHash || !sameReviewSubject(binding3.review, observedSubject) || binding3.review.taskId !== original.event.missionId || binding3.review.writerId !== writerId || binding3.review.provenance.kind !== "native-context" || binding3.review.provenance.contextId !== field4(original.event, "contextId"))
36745
36746
  return false;
36746
- const requested = events.find((event) => event.seq < original.event.seq && matchesRequest(original.event, event) && field4(event, "contractVersion") === original.completion.contractVersion && `runtime:${String(field4(event, "runtime"))}` === observation2.expectedSource);
36747
- return requested !== void 0 && events.some((event) => event.kind === "specialist.started" && event.source === observation2.expectedSource && event.subject === original.event.subject && event.seq > requested.seq && event.seq < original.event.seq && ["stage", "reviewRound", "inputHash", "contextId"].every((key) => field4(event, key) === field4(original.event, key)));
36747
+ const requested = events.find((event) => event.seq > afterSeqExclusive && event.seq < original.event.seq && matchesRequest(original.event, event) && field4(event, "contractVersion") === original.completion.contractVersion && `runtime:${String(field4(event, "runtime"))}` === observation2.expectedSource);
36748
+ return requested !== void 0 && events.some((event) => event.kind === "specialist.started" && event.source === observation2.expectedSource && event.subject === original.event.subject && event.seq > requested.seq && event.seq < original.event.seq && ["stage", "reviewRound", "inputHash", "contractVersion", "contextId"].every((key) => field4(event, key) === field4(original.event, key)));
36748
36749
  });
36749
36750
  }
36750
36751
  function pendingPreparationClarification(input2, previousRecovery) {
@@ -36795,11 +36796,11 @@ function admitStoppedMission(input2, projectedHistory = input2.stream.events) {
36795
36796
  return refuse2("stale-journal", "Re-observe the journal before requesting recovery");
36796
36797
  if (ambiguousEffects(events))
36797
36798
  return refuse2("ambiguous-effect", "Reconcile the unfinished external effect; recovery cannot replay it");
36799
+ const existing = completions(events).filter((item) => field4(item.event, "stage") === observation2.stage);
36798
36800
  const previousRecovery = events.filter((event) => event.kind === "mission.recovered").at(-1);
36799
- if (previousRecovery !== void 0 && !events.some((event) => event.seq > previousRecovery.seq && event.kind === "lead-writer.completed") && !pendingPreparationClarification(input2, previousRecovery)) {
36801
+ if (previousRecovery !== void 0 && !events.some((event) => event.seq > previousRecovery.seq && event.kind === "lead-writer.completed") && !pendingPreparationClarification(input2, previousRecovery) && !validProvenanceRecovery(input2, existing, previousRecovery.seq)) {
36800
36802
  return refuse2("no-recovery-progress", "Record actual corrective progress before reopening another stopped episode");
36801
36803
  }
36802
- const existing = completions(events).filter((item) => field4(item.event, "stage") === observation2.stage);
36803
36804
  const disposition = request.disposition;
36804
36805
  const replacement = disposition.kind === "preparation-invocation";
36805
36806
  if (replacement && !preparationInvocationReplacement(input2)) {
@@ -36879,7 +36880,7 @@ function admitStoppedMission(input2, projectedHistory = input2.stream.events) {
36879
36880
  requiredSpecialists: required2,
36880
36881
  contractVersions: observation2.contractVersions,
36881
36882
  currentInputHashes: observation2.currentInputHashes,
36882
- maxRounds: observation2.maxRounds
36883
+ ...observation2.reviewPolicy === "bounded-corrections-v1" ? BOUNDED_REVIEW_BUDGET : { maxRounds: observation2.maxRounds }
36883
36884
  };
36884
36885
  const originalReview = reduceReviewLoop({ ...reviewInput, events: projectedHistory });
36885
36886
  if (originalReview.issues.some((issue4) => replacement ? issue4.code !== "reused-context" || !inadmissible.includes(issue4.eventId) : issue4.code !== "wrong-review-round")) {
@@ -36893,7 +36894,7 @@ function admitStoppedMission(input2, projectedHistory = input2.stream.events) {
36893
36894
  return refuse2("incomplete-required-panel", "Every required preparation specialist must have its original matching PASS triplet before recovery");
36894
36895
  }
36895
36896
  const consumedRounds = Math.max(0, ...(replacement ? projectedHistory : projected).filter((event) => ["specialist.completed", "specialist.failed"].includes(event.kind) && field4(event, "stage") === observation2.stage).map((event) => Number(field4(event, "reviewRound"))));
36896
- if (!replacement && disposition.kind !== "review-provenance" && consumedRounds >= observation2.maxRounds)
36897
+ if (!replacement && disposition.kind !== "review-provenance" && consumedRounds >= reviewInput.maxRounds)
36897
36898
  return refuse2("review-budget-exhausted", "The real review budget is exhausted; recovery cannot reset it");
36898
36899
  const consumedCorrectionBatches = projected.filter((event) => event.kind === "lead-writer.completed" && field4(event, "actionKind") === "run-correction").length;
36899
36900
  if (disposition.kind === "review-provenance" && consumedCorrectionBatches > 2) {
@@ -36915,7 +36916,7 @@ function admitStoppedMission(input2, projectedHistory = input2.stream.events) {
36915
36916
  inadmissibleCompletionEventIds: inadmissible,
36916
36917
  roundCorrections,
36917
36918
  consumedRounds,
36918
- remainingRounds: Math.max(0, observation2.maxRounds - consumedRounds),
36919
+ remainingRounds: Math.max(0, reviewInput.maxRounds - consumedRounds),
36919
36920
  ...!replacement && disposition.kind !== "review-provenance" ? {} : {
36920
36921
  consumedCorrectionBatches,
36921
36922
  remainingCorrectionBatches: 2 - consumedCorrectionBatches
@@ -36960,11 +36961,12 @@ function recoveryObservation(value) {
36960
36961
  "currentInputHashes",
36961
36962
  "maxRounds",
36962
36963
  "expectedSource",
36964
+ ...value["reviewPolicy"] === void 0 ? [] : ["reviewPolicy"],
36963
36965
  ...value["resolutionArtifact"] === void 0 ? [] : ["resolutionArtifact"],
36964
36966
  ...value["reviewSubject"] === void 0 ? [] : ["reviewSubject"],
36965
36967
  ...value["reviewBindings"] === void 0 ? [] : ["reviewBindings"],
36966
36968
  ...value["evidenceDependencies"] === void 0 ? [] : ["evidenceDependencies"]
36967
- ]) || !["pre-implementation", "post-implementation"].includes(String(value["stage"])) || !["runtime:codex", "runtime:claude"].includes(String(value["expectedSource"])) || typeof value["maxRounds"] !== "number" || !Number.isSafeInteger(value["maxRounds"]) || value["maxRounds"] < 1 || value["maxRounds"] > 8 || value["contractVersions"] === void 0 || !record10(value["contractVersions"]) || value["currentInputHashes"] === void 0 || !record10(value["currentInputHashes"]))
36969
+ ]) || value["reviewPolicy"] !== void 0 && value["reviewPolicy"] !== "bounded-corrections-v1" || !["pre-implementation", "post-implementation"].includes(String(value["stage"])) || !["runtime:codex", "runtime:claude"].includes(String(value["expectedSource"])) || typeof value["maxRounds"] !== "number" || !Number.isSafeInteger(value["maxRounds"]) || value["maxRounds"] < 1 || value["maxRounds"] > 8 || value["contractVersions"] === void 0 || !record10(value["contractVersions"]) || value["currentInputHashes"] === void 0 || !record10(value["currentInputHashes"]))
36968
36970
  return false;
36969
36971
  return Object.keys(value["contractVersions"]).length <= 64 && Object.keys(value["currentInputHashes"]).length <= 64 && Object.entries(value["contractVersions"]).every(([key, version3]) => specialistId2(key) && typeof version3 === "number" && Number.isSafeInteger(version3) && version3 >= 1 && version3 <= 1e4) && Object.entries(value["currentInputHashes"]).every(([key, hash7]) => specialistId2(key) && typeof hash7 === "string" && /^sha256:[a-f0-9]{64}$/.test(hash7)) && (value["reviewSubject"] === void 0 || isReviewSubject(value["reviewSubject"])) && (value["reviewBindings"] === void 0 || Array.isArray(value["reviewBindings"]) && value["reviewBindings"].length <= 64 && value["reviewBindings"].every(reviewBinding)) && (value["resolutionArtifact"] === void 0 || artifact(value["resolutionArtifact"])) && (value["evidenceDependencies"] === void 0 || record10(value["evidenceDependencies"]) && Object.keys(value["evidenceDependencies"]).length <= 64 && Object.entries(value["evidenceDependencies"]).every(([key, hash7]) => key.length > 0 && key.length <= 500 && typeof hash7 === "string" && /^sha256:[a-f0-9]{64}$/.test(hash7)));
36970
36972
  }
@@ -39070,8 +39072,7 @@ function orchestrateMissionTeam(originalInput) {
39070
39072
  requiredSpecialists: requiredSpecialists(input2.plan, "post-implementation", start.boundedReview),
39071
39073
  contractVersions: contractVersions(input2.plan),
39072
39074
  currentInputHashes: input2.currentInputHashesByStage["post-implementation"],
39073
- maxRounds: start.boundedReview ? 3 : input2.maxReviewRounds,
39074
- ...start.boundedReview ? { maxCorrectionBatches: 2 } : {},
39075
+ ...start.boundedReview ? BOUNDED_REVIEW_BUDGET : { maxRounds: input2.maxReviewRounds },
39075
39076
  evidenceObligations: obligations,
39076
39077
  validProofIds: input2.stream.events.flatMap((event) => {
39077
39078
  if (event.kind !== "evidence.recorded")
@@ -46578,7 +46579,7 @@ function dependencyEntries(pkg) {
46578
46579
  function technology(id3, version3, source2) {
46579
46580
  return Object.freeze({ id: id3, version: version3, sources: Object.freeze([source2]) });
46580
46581
  }
46581
- function packageTechnologies(workspace) {
46582
+ function packageTechnologies(workspace, recipe) {
46582
46583
  const technologies = /* @__PURE__ */ new Map();
46583
46584
  const source2 = workspace.path === "." ? "package.json" : `${workspace.path}/package.json`;
46584
46585
  for (const [dependency, version3] of dependencyEntries(workspace.packageJson)) {
@@ -46591,7 +46592,10 @@ function packageTechnologies(workspace) {
46591
46592
  }
46592
46593
  }
46593
46594
  if (workspace.path === ".") {
46594
- technologies.set("node", technology("node", exactVersion(process.versions.node), "runtime:node"));
46595
+ const declaredNode2 = workspace.packageJson.engines?.node;
46596
+ const projectNode = typeof declaredNode2 === "string" && /^\d+\.\d+\.\d+$/.test(declaredNode2) ? declaredNode2 : null;
46597
+ const projectNodeSource = projectNode === null && typeof declaredNode2 === "string" ? `package.json:engines.node:${declaredNode2}` : "package.json:engines.node";
46598
+ technologies.set("node", recipe === "runtime-v1" ? technology("node", exactVersion(process.versions.node), "runtime:node") : technology("node", projectNode, projectNodeSource));
46595
46599
  const packageManager = workspace.packageJson.packageManager;
46596
46600
  if (packageManager !== void 0) {
46597
46601
  const separator = packageManager.indexOf("@");
@@ -46617,7 +46621,7 @@ function packageTechnologies(workspace) {
46617
46621
  function validChangedFile(file2) {
46618
46622
  return file2.length > 0 && !file2.startsWith("/") && !file2.startsWith("../") && !file2.includes("/../") && !file2.includes("\\") && posix.normalize(file2) === file2;
46619
46623
  }
46620
- function detectProfileInput(root, files) {
46624
+ function detectProfileInput(root, files, recipe = "runtime-v1") {
46621
46625
  if (!files.every(validChangedFile)) {
46622
46626
  throw new Error("PROFILE_INPUT_INVALID: changed files must remain inside the project root");
46623
46627
  }
@@ -46625,7 +46629,7 @@ function detectProfileInput(root, files) {
46625
46629
  const rootWorkspace = workspaces.find((workspace) => workspace.path === ".");
46626
46630
  const projects2 = workspaces.map((workspace) => Object.freeze({
46627
46631
  path: workspace.path,
46628
- technologies: packageTechnologies(workspace)
46632
+ technologies: packageTechnologies(workspace, recipe)
46629
46633
  }));
46630
46634
  if (projects2.length > 1) {
46631
46635
  const rootProject = projects2.find((project) => project.path === ".");
@@ -55863,17 +55867,18 @@ async function createMission(root, input2) {
55863
55867
  );
55864
55868
  await writeMissionMetadata(run2, safeInput, createdAt);
55865
55869
  }
55866
- async function writeMissionControllerPlan(root, missionId, plan2, ticket, baseCommit) {
55870
+ async function writeMissionControllerPlan(root, missionId, plan2, ticket, baseCommit, inputRecipe) {
55867
55871
  const run2 = await existingRunDirectory(root, missionId);
55868
- const routingHash = missionControllerRoutingHash(plan2, ticket, baseCommit);
55872
+ const routingHash = missionControllerRoutingHash(plan2, ticket, baseCommit, inputRecipe);
55869
55873
  const handle3 = await open$1(join(run2, "controller-plan.json"), "wx", 384);
55870
55874
  try {
55871
55875
  await handle3.writeFile(`${JSON.stringify({
55872
- schemaVersion: baseCommit === void 0 ? 1 : 2,
55876
+ schemaVersion: inputRecipe === void 0 ? baseCommit === void 0 ? 1 : 2 : 3,
55873
55877
  routingHash,
55874
55878
  plan: plan2,
55875
55879
  ticket,
55876
- ...baseCommit === void 0 ? {} : { baseCommit }
55880
+ ...baseCommit === void 0 ? {} : { baseCommit },
55881
+ ...inputRecipe === void 0 ? {} : { inputRecipe }
55877
55882
  })}
55878
55883
  `, "utf8");
55879
55884
  } finally {
@@ -55902,28 +55907,38 @@ async function loadMissionControllerPlan(root, missionId) {
55902
55907
  "routingHash",
55903
55908
  "plan",
55904
55909
  "ticket",
55905
- ...parsed.schemaVersion === 2 ? ["baseCommit"] : []
55910
+ ...parsed.schemaVersion === 2 || parsed.schemaVersion === 3 ? ["baseCommit"] : [],
55911
+ ...parsed.schemaVersion === 3 ? ["inputRecipe"] : []
55906
55912
  ])) {
55907
55913
  throw new Error("MISSION_CONTROLLER_PLAN_INVALID: plan envelope is malformed");
55908
55914
  }
55909
55915
  const plan2 = parseMissionSpecialistPlan(parsed.plan);
55910
55916
  const ticket = parseMissionControllerTicket(parsed.ticket);
55911
55917
  const routingHash = typeof parsed.routingHash === "string" ? parsed.routingHash : "";
55912
- const baseCommit = parsed.schemaVersion === 2 ? parsed.baseCommit : void 0;
55913
- if (parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2 || parsed.schemaVersion === 2 && (typeof baseCommit !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(baseCommit)) || !/^sha256:[a-f0-9]{64}$/.test(routingHash)) {
55918
+ const baseCommit = parsed.schemaVersion === 2 || parsed.schemaVersion === 3 ? parsed.baseCommit : void 0;
55919
+ const inputRecipe = parsed.schemaVersion === 3 && parsed.inputRecipe === "project-v1" ? "project-v1" : void 0;
55920
+ if (parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2 && parsed.schemaVersion !== 3 || parsed.schemaVersion === 3 && inputRecipe === void 0 || (parsed.schemaVersion === 2 || parsed.schemaVersion === 3) && (typeof baseCommit !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(baseCommit)) || !/^sha256:[a-f0-9]{64}$/.test(routingHash)) {
55914
55921
  throw new Error("MISSION_CONTROLLER_PLAN_INVALID: plan integrity check failed");
55915
55922
  }
55916
55923
  const base = typeof baseCommit === "string" ? { baseCommit } : {};
55917
- if (missionControllerRoutingHash(plan2, ticket, base.baseCommit) !== routingHash) {
55924
+ if (missionControllerRoutingHash(plan2, ticket, base.baseCommit, inputRecipe) !== routingHash) {
55918
55925
  throw new Error("MISSION_CONTROLLER_PLAN_INVALID: plan integrity check failed");
55919
55926
  }
55920
- return Object.freeze({ plan: plan2, ticket, routingHash, ...base });
55927
+ return Object.freeze({ plan: plan2, ticket, routingHash, ...base, ...inputRecipe === void 0 ? {} : { inputRecipe } });
55921
55928
  }
55922
- function missionControllerRoutingHash(plan2, ticket, baseCommit) {
55929
+ function missionControllerRoutingHash(plan2, ticket, baseCommit, inputRecipe) {
55930
+ if (inputRecipe !== void 0 && (inputRecipe !== "project-v1" || baseCommit === void 0)) {
55931
+ throw new Error("MISSION_CONTROLLER_PLAN_INVALID: project input recipe requires a committed baseline");
55932
+ }
55923
55933
  if (baseCommit !== void 0 && !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(baseCommit)) {
55924
55934
  throw new Error("MISSION_REVIEW_BASE_INVALID: expected a full commit object ID");
55925
55935
  }
55926
- return canonicalJsonHash({ plan: plan2, ticket, ...baseCommit === void 0 ? {} : { baseCommit } });
55936
+ return canonicalJsonHash({
55937
+ plan: plan2,
55938
+ ticket,
55939
+ ...baseCommit === void 0 ? {} : { baseCommit },
55940
+ ...inputRecipe === void 0 ? {} : { inputRecipe }
55941
+ });
55927
55942
  }
55928
55943
  function unknownRecord2(value) {
55929
55944
  return typeof value === "object" && Boolean(value) && !Array.isArray(value);
@@ -56335,6 +56350,7 @@ async function loadProfiles(root, coreProfileDirectory) {
56335
56350
  // src/lib/mission-inputs.ts
56336
56351
  var execFile2 = promisify(execFile);
56337
56352
  var MAX_TICKET_BYTES = 1e5;
56353
+ var MAX_REVIEW_PATCH_BYTES = 16e6;
56338
56354
  async function readTicket(root, ticketPath) {
56339
56355
  const canonicalRoot = await realpath(resolve(root));
56340
56356
  const loaded = await readBoundedProjectFile({
@@ -56390,7 +56406,7 @@ async function missionReviewBase(root) {
56390
56406
  throw new Error("MISSION_REVIEW_BASE_INVALID: a committed Git baseline is required");
56391
56407
  }
56392
56408
  }
56393
- async function captureMissionReviewSubject(root, baseCommit, committed = false) {
56409
+ async function captureMissionReviewSubject(root, baseCommit, committed = false, hashFormat = committed ? "committed" : "legacy") {
56394
56410
  if (baseCommit === void 0) {
56395
56411
  throw new Error(
56396
56412
  "MISSION_REVIEW_BASE_MISSING: legacy mission has no review baseline; preserve its history and start a new mission"
@@ -56430,7 +56446,17 @@ async function captureMissionReviewSubject(root, baseCommit, committed = false)
56430
56446
  "--relative",
56431
56447
  ...range,
56432
56448
  ...paths
56433
- ], options2),
56449
+ ], {
56450
+ ...options2,
56451
+ maxBuffer: MAX_REVIEW_PATCH_BYTES
56452
+ }).catch((error62) => {
56453
+ if (error62 instanceof Error && "code" in error62 && error62.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
56454
+ throw new Error(
56455
+ `MISSION_REVIEW_PATCH_TOO_LARGE: Git review patch exceeds ${MAX_REVIEW_PATCH_BYTES} bytes`
56456
+ );
56457
+ }
56458
+ throw error62;
56459
+ }),
56434
56460
  execFile2("git", [
56435
56461
  "diff",
56436
56462
  "--no-ext-diff",
@@ -56463,7 +56489,7 @@ async function captureMissionReviewSubject(root, baseCommit, committed = false)
56463
56489
  files,
56464
56490
  hash: canonicalJsonHash({
56465
56491
  baseCommit,
56466
- ...reviewedCommit === void 0 ? {} : { reviewedCommit },
56492
+ ...hashFormat === "legacy" ? {} : { reviewedCommit },
56467
56493
  diff: patch.stdout,
56468
56494
  files: files.files
56469
56495
  })
@@ -56537,10 +56563,10 @@ function detectedStack(root, profileInput) {
56537
56563
  status: "known"
56538
56564
  });
56539
56565
  }
56540
- async function planMission(root, ticketPath, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
56541
- return (await planBoundMission(root, ticketPath, generatedAt)).plan;
56566
+ async function planMission(root, ticketPath, generatedAt = (/* @__PURE__ */ new Date()).toISOString(), inputRecipe = "runtime-v1") {
56567
+ return (await planBoundMission(root, ticketPath, generatedAt, void 0, void 0, inputRecipe)).plan;
56542
56568
  }
56543
- async function compileMission(root, ticket, generatedAt, detectedFiles, specialistCatalog) {
56569
+ async function compileMission(root, ticket, generatedAt, detectedFiles, specialistCatalog, inputRecipe = "runtime-v1") {
56544
56570
  const [coreRoot, diff] = await Promise.all([
56545
56571
  findCoreSource(),
56546
56572
  detectedFiles ?? gitFiles(root)
@@ -56550,7 +56576,7 @@ async function compileMission(root, ticket, generatedAt, detectedFiles, speciali
56550
56576
  loadProfiles(root, join(coreRoot, "profiles")),
56551
56577
  specialistCatalog ?? loadSpecialists(coreRoot)
56552
56578
  ]);
56553
- const profileInput = detectProfileInput(root, diff.files);
56579
+ const profileInput = detectProfileInput(root, diff.files, inputRecipe);
56554
56580
  const stack = detectedStack(root, profileInput);
56555
56581
  return compileMissionPlan({
56556
56582
  ticket: { id: ticket.id, title: ticket.title, body: ticket.body },
@@ -56570,9 +56596,9 @@ function controllerTicketBinding(ticket) {
56570
56596
  contentHash: `sha256:${createHash("sha256").update(ticket.body).digest("hex")}`
56571
56597
  });
56572
56598
  }
56573
- async function planBoundMission(root, ticketPath, generatedAt = (/* @__PURE__ */ new Date()).toISOString(), detectedFiles, specialistCatalog) {
56599
+ async function planBoundMission(root, ticketPath, generatedAt = (/* @__PURE__ */ new Date()).toISOString(), detectedFiles, specialistCatalog, inputRecipe = "runtime-v1") {
56574
56600
  const ticket = await readTicket(root, ticketPath);
56575
- const plan2 = await compileMission(root, ticket, generatedAt, detectedFiles, specialistCatalog);
56601
+ const plan2 = await compileMission(root, ticket, generatedAt, detectedFiles, specialistCatalog, inputRecipe);
56576
56602
  return Object.freeze({
56577
56603
  plan: plan2,
56578
56604
  ticket: controllerTicketBinding(ticket)
@@ -67920,7 +67946,7 @@ async function recordSpecialistRequests(root, missionId, envelopes, planHash) {
67920
67946
  init_esm_shims();
67921
67947
 
67922
67948
  // package.json
67923
- var version2 = "5.0.3";
67949
+ var version2 = "5.0.4";
67924
67950
 
67925
67951
  // src/lib/runs/verify.ts
67926
67952
  var MAX_CAPTURE_BYTES = 64 * 1024;
@@ -68430,8 +68456,8 @@ async function migrateMissionSpecialist(roots, missionId, request) {
68430
68456
  throw new Error(`SPECIALIST_CONTRACT_MIGRATION_NATIVE: ${capability.limitations.join("; ")}`);
68431
68457
  }
68432
68458
  const assets = await observeSpecialistMigrationAssets(coreRoot, installRoot, identity2.runtime);
68433
- const subject = await captureMissionReviewSubject(workRoot, stored.baseCommit);
68434
- const live = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject.files);
68459
+ const subject = await captureControllerReviewSubject(workRoot, stored.baseCommit, current.inspected.stream.events);
68460
+ const live = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject.files, void 0, stored.inputRecipe);
68435
68461
  if (live.ticket.path !== stored.ticket.path || live.ticket.contentHash !== stored.ticket.contentHash) {
68436
68462
  throw new Error("MISSION_TICKET_CHANGED: migration cannot replace the bound ticket");
68437
68463
  }
@@ -68440,7 +68466,7 @@ async function migrateMissionSpecialist(roots, missionId, request) {
68440
68466
  canonicalJsonHash({ routing: specialist.proof.inputHash, subject: subject.hash })
68441
68467
  ]));
68442
68468
  const comparisonCatalog = await loadSpecialistMigrationComparisonCatalog(coreRoot, assets.declaration, stored.plan);
68443
- const comparison = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject.files, comparisonCatalog);
68469
+ const comparison = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject.files, comparisonCatalog, stored.inputRecipe);
68444
68470
  const originalHashes = Object.fromEntries(comparison.plan.specialists.map((specialist) => [
68445
68471
  specialist.specialistId,
68446
68472
  canonicalJsonHash({ routing: specialist.proof.inputHash, subject: subject.hash })
@@ -68465,6 +68491,18 @@ async function migrateMissionSpecialist(roots, missionId, request) {
68465
68491
  nativeAgentSha256: assets.nativeAgentSha256
68466
68492
  } };
68467
68493
  }
68494
+ function boundedMissionReview(events) {
68495
+ return events.some((event) => event.kind === "mission.started" && objectField(event.payload, "reviewPolicy") === "bounded-corrections-v1") || events.some((event) => event.kind === "mission.recovered" && objectField(objectField(objectField(event.payload, "request"), "disposition"), "kind") === "review-provenance");
68496
+ }
68497
+ function captureControllerReviewSubject(root, baseCommit, events, requireCommitted = false) {
68498
+ const committedFormat = events.some((event) => event.kind === "mission.started" && objectField(event.payload, "reviewPolicy") === "bounded-corrections-v1");
68499
+ return captureMissionReviewSubject(
68500
+ root,
68501
+ baseCommit,
68502
+ requireCommitted || boundedMissionReview(events),
68503
+ committedFormat ? "committed" : "legacy"
68504
+ );
68505
+ }
68468
68506
  async function recoverStoppedMission(roots, missionId, request) {
68469
68507
  const { workRoot, installRoot } = roots;
68470
68508
  const [stored, current, coreRoot] = await Promise.all([
@@ -68496,8 +68534,13 @@ async function recoverStoppedMission(roots, missionId, request) {
68496
68534
  }
68497
68535
  }
68498
68536
  const implemented = inspected.stream.events.some((event) => event.kind === "lead-writer.completed" && objectField(event.payload, "actionKind") !== "run-preparation-correction");
68499
- const subject = implemented ? await captureMissionReviewSubject(workRoot, stored.baseCommit) : void 0;
68500
- const live = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject?.files);
68537
+ const subject = implemented ? await captureControllerReviewSubject(
68538
+ workRoot,
68539
+ stored.baseCommit,
68540
+ inspected.stream.events,
68541
+ provenanceRecovery
68542
+ ) : void 0;
68543
+ const live = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject?.files, void 0, stored.inputRecipe);
68501
68544
  if (live.ticket.path !== stored.ticket.path || live.ticket.contentHash !== stored.ticket.contentHash) {
68502
68545
  throw new Error("MISSION_TICKET_CHANGED: recovery cannot replace the original ticket");
68503
68546
  }
@@ -68514,7 +68557,7 @@ async function recoverStoppedMission(roots, missionId, request) {
68514
68557
  throw new Error("SPECIALIST_CONTRACT_MIGRATION_INVALID: declared or installed contract changed");
68515
68558
  }
68516
68559
  const comparisonCatalog = await loadSpecialistMigrationComparisonCatalog(coreRoot, assets.declaration, stored.plan);
68517
- const comparison = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject.files, comparisonCatalog);
68560
+ const comparison = await planBoundMission(workRoot, stored.ticket.path, (/* @__PURE__ */ new Date()).toISOString(), subject.files, comparisonCatalog, stored.inputRecipe);
68518
68561
  for (const specialist of comparison.plan.specialists) {
68519
68562
  if (specialist.specialistId !== assets.declaration.specialistId) {
68520
68563
  currentInputHashes[specialist.specialistId] = canonicalJsonHash({
@@ -68526,19 +68569,19 @@ async function recoverStoppedMission(roots, missionId, request) {
68526
68569
  }
68527
68570
  const provenance4 = request.disposition.kind !== "review-provenance" ? void 0 : await readRecoveryReviewBindings(workRoot, request.disposition.resolutionArtifact.path);
68528
68571
  const resolutionArtifact = provenance4?.artifact ?? (request.disposition.kind === "review-blocker" || request.disposition.kind === "command-correction" ? await recoveryResolutionArtifact(workRoot, request.disposition.resolutionArtifact.path) : void 0);
68529
- const committedSubject = provenance4 === void 0 ? void 0 : await captureMissionReviewSubject(workRoot, stored.baseCommit, true);
68530
68572
  const opaqueContextRecovery = request.disposition.kind === "controller-defect" && request.disposition.defect === "opaque-native-context";
68531
68573
  const recoverySpecialists = opaqueContextRecovery || request.disposition.kind === "preparation-invocation" ? migration.plan.specialists.filter((value) => value.state === "applicable" && value.stages?.includes("pre-implementation")) : migration.plan.specialists;
68532
68574
  const observation2 = {
68533
- ...provenance4 === void 0 || committedSubject?.baseCommit === void 0 || committedSubject.reviewedCommit === void 0 ? {} : {
68575
+ ...provenance4 === void 0 || subject?.baseCommit === void 0 || subject.reviewedCommit === void 0 ? {} : {
68534
68576
  reviewBindings: provenance4.bindings,
68535
68577
  reviewSubject: {
68536
68578
  taskId: missionId,
68537
- baseCommit: committedSubject.baseCommit,
68538
- reviewedCommit: committedSubject.reviewedCommit,
68579
+ baseCommit: subject.baseCommit,
68580
+ reviewedCommit: subject.reviewedCommit,
68539
68581
  acceptanceCriteriaHash: stored.ticket.contentHash
68540
68582
  }
68541
68583
  },
68584
+ ...implemented && (provenanceRecovery || boundedMissionReview(inspected.stream.events)) ? { reviewPolicy: "bounded-corrections-v1" } : {},
68542
68585
  stage: implemented ? "post-implementation" : "pre-implementation",
68543
68586
  maxRounds: 2,
68544
68587
  evidenceDependencies: { "git:working-tree": current.project.diffHash },
@@ -68602,13 +68645,14 @@ async function dispatchMissionSpecialists(roots, input2, generatedAt = (/* @__PU
68602
68645
  const inspected = current?.inspected ?? initialInspection;
68603
68646
  requireOpenMission(inspected.stream.events);
68604
68647
  const implemented = inspected.stream.events.some((event) => event.kind === "lead-writer.completed" && isUnknownRecord(event.payload) && event.payload.actionKind !== "run-preparation-correction");
68605
- const boundedReview = inspected.stream.events.some((event) => event.kind === "mission.started" && objectField(event.payload, "reviewPolicy") === "bounded-corrections-v1") || inspected.stream.events.some((event) => event.kind === "mission.recovered" && objectField(objectField(objectField(event.payload, "request"), "disposition"), "kind") === "review-provenance");
68606
- const reviewSubject = implemented ? await captureMissionReviewSubject(workRoot, stored.baseCommit, boundedReview) : void 0;
68648
+ const reviewSubject = implemented ? await captureControllerReviewSubject(workRoot, stored.baseCommit, inspected.stream.events) : void 0;
68607
68649
  const live = await planBoundMission(
68608
68650
  workRoot,
68609
68651
  stored.ticket.path,
68610
68652
  generatedAt,
68611
- reviewSubject?.files
68653
+ reviewSubject?.files,
68654
+ void 0,
68655
+ stored.inputRecipe
68612
68656
  );
68613
68657
  const livePlan = live.plan;
68614
68658
  if (live.ticket.path !== stored.ticket.path || live.ticket.contentHash !== stored.ticket.contentHash) {
@@ -68639,7 +68683,7 @@ async function dispatchMissionSpecialists(roots, input2, generatedAt = (/* @__PU
68639
68683
  throw new Error("SPECIALIST_CONTRACT_MIGRATION_INVALID: declared or installed contract changed");
68640
68684
  }
68641
68685
  const catalog = await loadSpecialistMigrationComparisonCatalog(coreRoot, assets.declaration, stored.plan);
68642
- const comparison = await planBoundMission(workRoot, stored.ticket.path, generatedAt, reviewSubject.files, catalog);
68686
+ const comparison = await planBoundMission(workRoot, stored.ticket.path, generatedAt, reviewSubject.files, catalog, stored.inputRecipe);
68643
68687
  for (const specialist of comparison.plan.specialists) {
68644
68688
  if (specialist.specialistId !== assets.declaration.specialistId) {
68645
68689
  currentInputHashes[specialist.specialistId] = canonicalJsonHash({
@@ -68674,19 +68718,6 @@ async function dispatchMissionSpecialists(roots, input2, generatedAt = (/* @__PU
68674
68718
  reviewedCommit: reviewSubject.reviewedCommit,
68675
68719
  acceptanceCriteriaHash: stored.ticket.contentHash
68676
68720
  };
68677
- if (reviewBinding2 !== void 0) {
68678
- const restored = validatedRecoveredReviewEvents(inspected.stream.events);
68679
- if (!restored.ok) throw new Error(`MISSION_RECOVERY_INVALID: ${restored.reasons.join("; ")}`);
68680
- const hasProvenanceRecovery = inspected.stream.events.some((event) => event.kind === "mission.recovered" && objectField(objectField(objectField(event.payload, "request"), "disposition"), "kind") === "review-provenance");
68681
- if (hasProvenanceRecovery) for (const event of restored.events) {
68682
- if (event.kind !== "specialist.completed") continue;
68683
- const receipt2 = parseSpecialistCompletionValue(objectField(event.payload, "completion"))?.review;
68684
- const hash7 = objectField(event.payload, "inputHash");
68685
- if (receipt2 !== void 0 && sameReviewSubject(receipt2, reviewBinding2) && typeof hash7 === "string") {
68686
- currentInputHashes[event.subject] = hash7;
68687
- }
68688
- }
68689
- }
68690
68721
  const decision = orchestrateMissionTeam({
68691
68722
  ...reviewBinding2 === void 0 ? {} : { reviewSubject: reviewBinding2 },
68692
68723
  plan: stored.plan,
@@ -69010,7 +69041,7 @@ Fix: ${parsed.fix}
69010
69041
  const journal = roots.installRoot;
69011
69042
  try {
69012
69043
  if (parsed.kind === "plan") {
69013
- const plan2 = await planMission(root, parsed.ticketPath);
69044
+ const plan2 = await planMission(root, parsed.ticketPath, void 0, "project-v1");
69014
69045
  process.stdout.write(parsed.json ? `${JSON.stringify(plan2)}
69015
69046
  ` : `${renderPlan(plan2)}
69016
69047
  `);
@@ -69095,7 +69126,7 @@ ${dispatched.envelopes.map((envelope) => `${envelope.agentName} ${envelope.stage
69095
69126
  }
69096
69127
  if (parsed.kind === "start") {
69097
69128
  const baseCommit = parsed.ticketPath === void 0 ? void 0 : await missionReviewBase(root);
69098
- const bound = parsed.ticketPath === void 0 ? void 0 : await planBoundMission(root, parsed.ticketPath);
69129
+ const bound = parsed.ticketPath === void 0 ? void 0 : await planBoundMission(root, parsed.ticketPath, void 0, void 0, void 0, "project-v1");
69099
69130
  const plan2 = bound?.plan;
69100
69131
  const diff = plan2 === void 0 ? await gitFiles(root) : void 0;
69101
69132
  const stack = diff === void 0 ? void 0 : detectedStack(root, detectProfileInput(root, diff.files));
@@ -69114,7 +69145,7 @@ ${dispatched.envelopes.map((envelope) => `${envelope.agentName} ${envelope.stage
69114
69145
  const controllerPlan = plan2 === void 0 ? void 0 : missionSpecialistPlan(plan2);
69115
69146
  const ticketBinding = bound?.ticket;
69116
69147
  const runtimeIdentity = bound === void 0 ? void 0 : coordinatorRuntimeIdentity(process.env);
69117
- const routingHash = controllerPlan === void 0 ? void 0 : ticketBinding === void 0 ? void 0 : missionControllerRoutingHash(controllerPlan, ticketBinding, baseCommit);
69148
+ const routingHash = controllerPlan === void 0 ? void 0 : ticketBinding === void 0 ? void 0 : missionControllerRoutingHash(controllerPlan, ticketBinding, baseCommit, "project-v1");
69118
69149
  await createMission(journal, {
69119
69150
  missionId,
69120
69151
  title: parsed.title,
@@ -69141,7 +69172,8 @@ ${dispatched.envelopes.map((envelope) => `${envelope.agentName} ${envelope.stage
69141
69172
  missionId,
69142
69173
  controllerPlan,
69143
69174
  ticketBinding,
69144
- baseCommit
69175
+ baseCommit,
69176
+ "project-v1"
69145
69177
  );
69146
69178
  if (storedHash !== routingHash) {
69147
69179
  throw new Error("MISSION_CONTROLLER_PLAN_INVALID: routing hash changed while storing");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidmachine",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "void-machine CLI — install an opinionated engineering doctrine on any project; render deterministic project health. Run via `npx voidmachine`; command: void-machine (alias vm).",
5
5
  "license": "MIT",
6
6
  "author": "Folpe @ VoidCorp",
@@ -58,8 +58,8 @@
58
58
  "ws": "8.21.0",
59
59
  "yaml": "^2.5.0",
60
60
  "zod": "^4.6.5",
61
+ "@voidcorp/harness-graph": "5.0.4",
61
62
  "@voidcorp/hook-runner": "0.0.0",
62
- "@voidcorp/harness-graph": "5.0.3",
63
63
  "@voidcorp/mission-engine": "0.0.0",
64
64
  "@voidcorp/void-machine": "0.0.0"
65
65
  },