immune-brain 5.1.2 → 5.3.0

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.
Files changed (34) hide show
  1. package/README.md +13 -6
  2. package/README.zh-CN.md +12 -5
  3. package/package.json +1 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +14 -2
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +73 -0
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +15 -2
  8. package/plugins/immune-brain/agents/immune-brain-reviewer.md +5 -0
  9. package/plugins/immune-brain/dist/BASELINE.md +6 -2
  10. package/plugins/immune-brain/dist/claude/mcp-server.mjs +429 -140
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +3 -3
  12. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -1
  13. package/plugins/immune-brain/dist/imm-planner.md +33 -37
  14. package/plugins/immune-brain/dist/imm-run.md +14 -3
  15. package/plugins/immune-brain/dist/imm-test-prune.md +233 -0
  16. package/plugins/immune-brain/dist/registry.yaml +9 -0
  17. package/plugins/immune-brain/dist/role-prompts/code-review.md +5 -0
  18. package/plugins/immune-brain/dist/role-prompts/executor.md +8 -0
  19. package/plugins/immune-brain/hooks/hooks.json +10 -1
  20. package/plugins/immune-brain/runtime/assurance/alias_file.ts +24 -0
  21. package/plugins/immune-brain/runtime/assurance/phase_entry.ts +133 -0
  22. package/plugins/immune-brain/runtime/claude/interaction.ts +5 -1
  23. package/plugins/immune-brain/runtime/claude/lane_guard.ts +197 -3
  24. package/plugins/immune-brain/runtime/claude/mcp_server.ts +21 -3
  25. package/plugins/immune-brain/runtime/claude/review_host.ts +6 -6
  26. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  27. package/plugins/immune-brain/runtime/prompts/code-review.md +5 -0
  28. package/plugins/immune-brain/runtime/prompts/executor.md +8 -0
  29. package/plugins/immune-brain/skills/BASELINE.md +6 -2
  30. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +6 -4
  31. package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -7
  32. package/plugins/immune-brain/skills/imm-run/SKILL.md +2 -1
  33. package/plugins/immune-brain/skills/imm-test-prune/SKILL.md +28 -0
  34. package/plugins/immune-brain/skills/registry.yaml +9 -0
@@ -39,10 +39,115 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
39
39
  }
40
40
 
41
41
  // plugins/immune-brain/runtime/plugin_version.ts
42
- var PLUGIN_VERSION = "5.1.2";
42
+ var PLUGIN_VERSION = "5.3.0";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
46
+
47
+ // plugins/immune-brain/runtime/assurance/phase_entry.ts
48
+ var RUN_ENTRY_REQUIRED = "run_entry_required";
49
+ var PHASE_ENTRY_REQUIRED = "phase_entry_required";
50
+ var PHASE_ENTRY_RECOVERY = {
51
+ [RUN_ENTRY_REQUIRED]: "ask the user to enter imm-run in the current Host",
52
+ [PHASE_ENTRY_REQUIRED]: "ask the user to enter the Skill themselves in the current Host"
53
+ };
54
+ var PACKAGED = {
55
+ "imm-planner": "plan",
56
+ "imm-brainstorm": "plan",
57
+ "imm-run": "run"
58
+ };
59
+ var ALIAS_TARGETS = { ...PACKAGED, "imm-loop": "run" };
60
+ var PREFIXES = ["immune-brain:", "skill:"];
61
+ var ALIAS_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
62
+ var SKILL_MENTION = /(?<![A-Za-z0-9_-])imm-(?:planner|brainstorm|run|loop)(?![A-Za-z0-9_-])/g;
63
+ var EXPANDED = /^<command-message>[^<]*<\/command-message>\s*<command-name>\/([^\s<]+)<\/command-name>/;
64
+ var RAW = /^\/(\S+)/;
65
+ function stripPrefix(name) {
66
+ for (const prefix of PREFIXES)
67
+ if (name.startsWith(prefix))
68
+ return name.slice(prefix.length);
69
+ return name;
70
+ }
71
+ function packagedPhaseSkill(name) {
72
+ const trimmed = name.trim();
73
+ const bare = stripPrefix(trimmed.startsWith("/") ? trimmed.slice(1) : trimmed);
74
+ return Object.hasOwn(PACKAGED, bare) ? bare : null;
75
+ }
76
+ function recognizePhaseEntry(text, readAlias) {
77
+ const command = (RAW.exec(text) ?? EXPANDED.exec(text))?.[1];
78
+ if (!command)
79
+ return null;
80
+ const packaged = packagedPhaseSkill(command);
81
+ if (packaged)
82
+ return PACKAGED[packaged];
83
+ if (!ALIAS_NAME.test(command))
84
+ return null;
85
+ let body;
86
+ try {
87
+ body = readAlias(command);
88
+ } catch {
89
+ return null;
90
+ }
91
+ if (!body)
92
+ return null;
93
+ const targets = new Set(body.match(SKILL_MENTION) ?? []);
94
+ if (targets.size !== 1)
95
+ return null;
96
+ return ALIAS_TARGETS[[...targets][0]] ?? null;
97
+ }
98
+ var NO_PHASE_ENTRY = { run_live: false };
99
+ function applyPrompt(state, entry) {
100
+ if (entry === "run")
101
+ return { run_live: true };
102
+ if (entry === "plan")
103
+ return { run_live: false };
104
+ return state;
105
+ }
106
+ function phaseEntryLogLine(entry) {
107
+ return `${JSON.stringify({ v: 1, entry })}
108
+ `;
109
+ }
110
+ function reducePhaseEntryLog(text) {
111
+ let state = NO_PHASE_ENTRY;
112
+ for (const line of text.split(`
113
+ `)) {
114
+ if (!line)
115
+ continue;
116
+ let record;
117
+ try {
118
+ record = JSON.parse(line);
119
+ } catch {
120
+ return null;
121
+ }
122
+ if (!record || typeof record !== "object")
123
+ return null;
124
+ const { v, entry } = record;
125
+ if (v !== 1 || entry !== null && entry !== "plan" && entry !== "run")
126
+ return null;
127
+ state = applyPrompt(state, entry);
128
+ }
129
+ return state;
130
+ }
131
+ function refuse(reason_code, detail) {
132
+ return { ok: false, reason_code, detail, recovery_action: PHASE_ENTRY_RECOVERY[reason_code] };
133
+ }
134
+ function admitPhaseOperation(operation, state, context) {
135
+ if (operation.kind === "tool") {
136
+ if (state?.run_live)
137
+ return { ok: true };
138
+ return refuse(RUN_ENTRY_REQUIRED, `${operation.name} needs a literal-user imm-run entry in this session`);
139
+ }
140
+ if (operation.skill !== "imm-run")
141
+ return { ok: true };
142
+ if (state?.run_live)
143
+ return { ok: true };
144
+ return refuse(PHASE_ENTRY_REQUIRED, "imm-run is entered by the user, not by a model Skill call");
145
+ }
146
+ function phaseRefusalMessage(refusal) {
147
+ return `${refusal.reason_code}: ${refusal.detail}; recovery: ${refusal.recovery_action}`;
148
+ }
149
+
150
+ // plugins/immune-brain/runtime/claude/interaction.ts
46
151
  var PRIVILEGED_OPERATIONS = [
47
152
  "enroll",
48
153
  "request_authorization",
@@ -57,7 +162,8 @@ var RECOVERY_ACTIONS = {
57
162
  user_cancelled: "wait for a fresh literal-user request",
58
163
  correlation_missing: "retry through a fresh native gate in the current Host",
59
164
  unsupported_host: "upgrade to a supported Claude Code version and retry in the current Host",
60
- workspace_changed: "review the current workspace and retry through a fresh native gate"
165
+ workspace_changed: "review the current workspace and retry through a fresh native gate",
166
+ ...PHASE_ENTRY_RECOVERY
61
167
  };
62
168
 
63
169
  class NativeAuthorityError extends Error {
@@ -684,6 +790,11 @@ function parseHookStdin(raw) {
684
790
  return null;
685
791
  }
686
792
 
793
+ // plugins/immune-brain/runtime/claude/lane_guard.ts
794
+ import { lstatSync as lstatSync10, rmSync as rmSync5 } from "node:fs";
795
+ import { homedir, tmpdir as tmpdir2 } from "node:os";
796
+ import { join as join9 } from "node:path";
797
+
687
798
  // plugins/immune-brain/runtime/unattended/lane_workspace.ts
688
799
  import { spawnSync } from "node:child_process";
689
800
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -6350,7 +6461,7 @@ var oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
6350
6461
  var id = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6351
6462
  var batchAuthor = process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch";
6352
6463
  var decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6353
- function refuse() {
6464
+ function refuse2() {
6354
6465
  throw new Error("plan_digest mismatch: batch plan reconfirmation is not eligible");
6355
6466
  }
6356
6467
  function matchesLocalProof(localJson, exported) {
@@ -6359,7 +6470,7 @@ function matchesLocalProof(localJson, exported) {
6359
6470
  function git2(root, args) {
6360
6471
  const r = spawnSync6("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
6361
6472
  if (r.status !== 0)
6362
- refuse();
6473
+ refuse2();
6363
6474
  return decode(r.stdout);
6364
6475
  }
6365
6476
  function ownUnpersistedBatchHead(root, record, head) {
@@ -6410,23 +6521,23 @@ function ownUnpersistedBatchHead(root, record, head) {
6410
6521
  var confirmations = new Map;
6411
6522
  function retainReconfirmation(nonce, snapshot) {
6412
6523
  if (confirmations.has(nonce))
6413
- refuse();
6524
+ refuse2();
6414
6525
  confirmations.set(nonce, snapshot);
6415
6526
  }
6416
6527
  function takeReconfirmation(nonce) {
6417
6528
  const found = confirmations.get(nonce);
6418
6529
  confirmations.delete(nonce);
6419
6530
  if (!found)
6420
- refuse();
6531
+ refuse2();
6421
6532
  return found;
6422
6533
  }
6423
6534
  async function captureBatchReconfirmation(root, record, children) {
6424
6535
  if (!id.test(record.batch_id) || !oid.test(record.base_head) || isTerminalBatchState(record.batch_state) || record.commits.length || record.children.some((c) => c.commit !== null || c.state === "committed") || record.children.length !== children.length || !children.length)
6425
- refuse();
6536
+ refuse2();
6426
6537
  const assertNoCommitEvidence = () => {
6427
6538
  for (const child of record.children) {
6428
6539
  if (!id.test(child.task_id))
6429
- refuse();
6540
+ refuse2();
6430
6541
  const path = batchCommitEvidencePath(record.batch_id, child.task_id);
6431
6542
  let missing = false;
6432
6543
  try {
@@ -6435,17 +6546,17 @@ async function captureBatchReconfirmation(root, record, children) {
6435
6546
  if (error instanceof Error && error.message === `source_missing: ${path}`)
6436
6547
  missing = true;
6437
6548
  else
6438
- refuse();
6549
+ refuse2();
6439
6550
  }
6440
6551
  if (!missing)
6441
- refuse();
6552
+ refuse2();
6442
6553
  }
6443
6554
  };
6444
6555
  assertNoCommitEvidence();
6445
6556
  const statePath = batchStatePath(record.batch_id);
6446
6557
  const stateBytes = readSecureProjectBytes(root, statePath);
6447
6558
  if (JSON.stringify(JSON.parse(decode(stateBytes))) !== JSON.stringify(record))
6448
- refuse();
6559
+ refuse2();
6449
6560
  const files = new Map;
6450
6561
  const capture = (path) => {
6451
6562
  const bytes = readSecureProjectBytes(root, path);
@@ -6457,14 +6568,14 @@ async function captureBatchReconfirmation(root, record, children) {
6457
6568
  let scope = null;
6458
6569
  const workspace = readWorkspaceStateRaw(root);
6459
6570
  if (workspace.state.current_working !== null)
6460
- refuse();
6571
+ refuse2();
6461
6572
  for (const [index, child] of children.entries()) {
6462
6573
  const previous = record.children[index];
6463
6574
  if (!id.test(child.task_id) || child.task_id !== previous.task_id || child.slice_id !== previous.slice_id || JSON.stringify(child.blocked_by) !== JSON.stringify(previous.blocked_by) || child.intent_path !== `docs/plans/${child.task_id}.intent.json`)
6464
- refuse();
6575
+ refuse2();
6465
6576
  const old = parseTaskIntentV1(JSON.parse(git2(root, ["show", `${record.base_head}:${child.intent_path}`])));
6466
6577
  if (old.task_id !== child.task_id)
6467
- refuse();
6578
+ refuse2();
6468
6579
  const oldHash = canonicalIntentHash(old);
6469
6580
  oldChildren.push({ task_id: child.task_id, blocked_by: [...child.blocked_by], intent_path: child.intent_path, intent_revision: old.revision, intent_content_hash: oldHash });
6470
6581
  const currentBytes = capture(child.intent_path);
@@ -6472,46 +6583,46 @@ async function captureBatchReconfirmation(root, record, children) {
6472
6583
  const currentHash = canonicalIntentHash(current);
6473
6584
  const tracked = readTaskIntent(root, child.task_id, child.intent_path);
6474
6585
  if (currentHash !== tracked.content_hash || currentHash !== child.intent_content_hash || current.revision !== child.intent_revision)
6475
- refuse();
6586
+ refuse2();
6476
6587
  if (currentHash === oldHash)
6477
6588
  continue;
6478
6589
  if (scope !== null || previous.state === "pending" || previous.state === "skipped_blocked" || current.revision <= old.revision || current.goal !== old.goal || current.owner !== old.owner)
6479
- refuse();
6590
+ refuse2();
6480
6591
  const settled = readSettledRunEvidence(root, child.task_id);
6481
6592
  if (!settled || settled.lifecycle !== "done")
6482
- refuse();
6593
+ refuse2();
6483
6594
  const run = settled.run_id;
6484
6595
  if (!id.test(run) || settled.enrollment_event_id !== enrollmentEventIdFor(child.task_id, settled.created_at) || Date.parse(settled.created_at) > Date.parse(record.updated_at))
6485
- refuse();
6596
+ refuse2();
6486
6597
  const pair = readAuditTaskPair(root, child.task_id, run);
6487
6598
  if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4")
6488
- refuse();
6599
+ refuse2();
6489
6600
  const r = pair.record;
6490
6601
  if (r.lifecycle !== "done" || r.intent_ref.content_hash !== currentHash || canonicalIntentHash(r.intent_snapshot) !== currentHash || r.git_base_head !== record.base_head || pair.proof.terminal_lifecycle !== "done")
6491
- refuse();
6602
+ refuse2();
6492
6603
  const terminal = r.history.at(-1);
6493
6604
  if (terminal?.type !== "complete" || terminal.from_state !== "active:frozen" || terminal.to_state !== "done:frozen" || terminal.id !== pair.proof.terminal_event_id)
6494
- refuse();
6605
+ refuse2();
6495
6606
  const recordPath = auditRunRecordPath(child.task_id, run);
6496
6607
  const proofPath = auditRunTerminalProofPath(child.task_id, run);
6497
6608
  if (!capture(recordPath).equals(Buffer.from(settled.record_json)))
6498
- refuse();
6609
+ refuse2();
6499
6610
  const capturedProof = JSON.parse(decode(capture(proofPath)));
6500
6611
  if (!matchesLocalProof(settled.proof_json, capturedProof) || !matchesLocalProof(settled.proof_json, pair.proof))
6501
- refuse();
6612
+ refuse2();
6502
6613
  const identity = taskDeliveryIdentity(root, r);
6503
6614
  const projection = await projectAssurance(root, child.task_id, (cwd, task) => {
6504
6615
  if (task.contract !== "assurance_kernel/task_record/v4" || !task.git_base_head)
6505
- refuse();
6616
+ refuse2();
6506
6617
  return taskDeliveryIdentity(cwd, task);
6507
6618
  });
6508
6619
  if (projection.error || projection.claim || projection.projection.run_id !== run || projection.projection.lifecycle !== "done" || !projection.projection.completion_ready || projection.projection.intent_content_hash !== currentHash || projection.projection.diff_hash !== identity.diff_hash || !projection.projection.fresh_approval_kinds.includes("qa") || projection.projection.risk !== "routine" && !projection.projection.fresh_approval_kinds.includes("review"))
6509
- refuse();
6620
+ refuse2();
6510
6621
  scope = r.intent_snapshot.scope_hint;
6511
6622
  changed.push({ task: child.task_id, run, identity: JSON.stringify(identity), settled: JSON.stringify(settled) });
6512
6623
  }
6513
6624
  if (computeBatchPlanDigest(oldChildren) !== record.plan_digest || changed.length !== 1 || scope === null)
6514
- refuse();
6625
+ refuse2();
6515
6626
  const confirmedScope = scope;
6516
6627
  const owner = changed[0];
6517
6628
  const status = () => git2(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
@@ -6520,42 +6631,42 @@ async function captureBatchReconfirmation(root, record, children) {
6520
6631
  for (const entry of entries) {
6521
6632
  const code = entry.slice(0, 2), path = entry.slice(3);
6522
6633
  if (code[1] !== " " || !["A", "M", "D"].includes(code[0]) || !path.startsWith(`${auditRunDirPath(owner.task, owner.run)}/`) && !confirmedScope.some((s) => pathMatchesScope(path, s)))
6523
- refuse();
6634
+ refuse2();
6524
6635
  }
6525
6636
  const flagsBytes = git2(root, ["ls-files", "-v"]);
6526
6637
  const flags = flagsBytes.split(`
6527
6638
  `).filter(Boolean);
6528
6639
  if (flags.some((line) => line[0] === "S" || line[0] !== line[0]?.toUpperCase()))
6529
- refuse();
6640
+ refuse2();
6530
6641
  const head = () => git2(root, ["rev-parse", "HEAD"]).trim();
6531
6642
  const branch = () => git2(root, ["symbolic-ref", "--short", "HEAD"]).trim();
6532
6643
  if (head() !== record.base_head || branch() !== record.branch)
6533
- refuse();
6644
+ refuse2();
6534
6645
  const assertDeliveryUnchanged = () => {
6535
6646
  for (const c of changed) {
6536
6647
  let settled;
6537
6648
  try {
6538
6649
  settled = readSettledRunEvidence(root, c.task);
6539
6650
  } catch {
6540
- refuse();
6651
+ refuse2();
6541
6652
  }
6542
6653
  if (!settled || settled.run_id !== c.run || JSON.stringify(settled) !== c.settled)
6543
- refuse();
6654
+ refuse2();
6544
6655
  const pair = readAuditTaskPair(root, c.task, c.run);
6545
6656
  if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || !pair.record.git_base_head)
6546
- refuse();
6657
+ refuse2();
6547
6658
  const diff = taskDeliveryIdentity(root, pair.record);
6548
6659
  if (JSON.stringify(diff) !== c.identity)
6549
- refuse();
6660
+ refuse2();
6550
6661
  }
6551
6662
  };
6552
6663
  const assertUnchanged = () => {
6553
6664
  assertNoCommitEvidence();
6554
6665
  if (!readSecureProjectBytes(root, statePath).equals(stateBytes) || head() !== record.base_head || branch() !== record.branch || status() !== statusBytes || git2(root, ["ls-files", "-v"]) !== flagsBytes || readWorkspaceStateRaw(root).revision !== workspace.revision)
6555
- refuse();
6666
+ refuse2();
6556
6667
  for (const [path, bytes] of files)
6557
6668
  if (!readSecureProjectBytes(root, path).equals(bytes))
6558
- refuse();
6669
+ refuse2();
6559
6670
  assertDeliveryUnchanged();
6560
6671
  };
6561
6672
  assertUnchanged();
@@ -9012,8 +9123,38 @@ async function activeTaskNotice(root) {
9012
9123
  ].join(" ");
9013
9124
  }
9014
9125
 
9126
+ // plugins/immune-brain/runtime/assurance/alias_file.ts
9127
+ import { closeSync as closeSync7, constants as constants5, fstatSync as fstatSync5, openSync as openSync7, readSync as readSync2 } from "node:fs";
9128
+ var MAX_ALIAS_BYTES = 64 * 1024;
9129
+ function readAliasFile(path, maxBytes = MAX_ALIAS_BYTES) {
9130
+ let fd;
9131
+ try {
9132
+ fd = openSync7(path, constants5.O_RDONLY | constants5.O_NONBLOCK);
9133
+ } catch {
9134
+ return null;
9135
+ }
9136
+ try {
9137
+ const stat = fstatSync5(fd);
9138
+ if (!stat.isFile() || stat.size > maxBytes)
9139
+ return null;
9140
+ const buf = Buffer.alloc(maxBytes + 1);
9141
+ const read = readSync2(fd, buf, 0, buf.length, 0);
9142
+ return read > maxBytes ? null : buf.toString("utf8", 0, read);
9143
+ } catch {
9144
+ return null;
9145
+ } finally {
9146
+ closeSync7(fd);
9147
+ }
9148
+ }
9149
+
9015
9150
  // plugins/immune-brain/runtime/claude/lane_guard.ts
9016
9151
  var FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
9152
+ var GATED_MCP_TOOL = /^mcp__.*immune-brain.*__(enroll|start_unattended_batch)$/;
9153
+ function denyPreToolUse(reason) {
9154
+ return JSON.stringify({
9155
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason }
9156
+ });
9157
+ }
9017
9158
  function laneGuardHookOutput(payload) {
9018
9159
  if (payload.hook_event_name !== "PreToolUse" || !FILE_TOOLS.has(String(payload.tool_name)))
9019
9160
  return null;
@@ -9022,9 +9163,7 @@ function laneGuardHookOutput(payload) {
9022
9163
  const refusal = laneWriteRefusal(cwd, input.file_path ?? input.notebook_path);
9023
9164
  if (!refusal)
9024
9165
  return null;
9025
- return JSON.stringify({
9026
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: refusal }
9027
- });
9166
+ return denyPreToolUse(refusal);
9028
9167
  }
9029
9168
  async function activeTaskHookOutput(payload) {
9030
9169
  if (payload.hook_event_name !== "UserPromptSubmit")
@@ -9035,12 +9174,152 @@ async function activeTaskHookOutput(payload) {
9035
9174
  return null;
9036
9175
  return JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: notice } });
9037
9176
  }
9177
+ function entryLogPath(sessionId, root) {
9178
+ return join9(cacheDir(root), `${sessionHash(sessionId)}.entry`);
9179
+ }
9180
+ function admissionPath(toolUseId, root) {
9181
+ return join9(cacheDir(root), `${sessionHash(toolUseId)}.admit`);
9182
+ }
9183
+ function sessionIdOf(payload) {
9184
+ return typeof payload.session_id === "string" ? payload.session_id : "";
9185
+ }
9186
+ function cwdOf(payload) {
9187
+ return typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
9188
+ }
9189
+ function claudeAliasReader(cwd, home) {
9190
+ return (name) => {
9191
+ for (const dir of [join9(cwd, ".claude", "commands"), join9(home, ".claude", "commands")]) {
9192
+ const body = readAliasFile(join9(dir, `${name}.md`));
9193
+ if (body !== null)
9194
+ return body;
9195
+ }
9196
+ return null;
9197
+ };
9198
+ }
9199
+ function readPhaseEntryState(sessionId, root = tmpdir2(), read = readPrivate) {
9200
+ if (!sessionId)
9201
+ return null;
9202
+ const path = entryLogPath(sessionId, root);
9203
+ try {
9204
+ lstatSync10(path);
9205
+ } catch (error) {
9206
+ return error.code === "ENOENT" ? NO_PHASE_ENTRY : null;
9207
+ }
9208
+ if (!ensurePrivateDir(cacheDir(root)))
9209
+ return null;
9210
+ let text;
9211
+ try {
9212
+ text = read(path);
9213
+ } catch {
9214
+ return null;
9215
+ }
9216
+ return text === undefined ? null : reducePhaseEntryLog(text);
9217
+ }
9218
+ function dropPrivate(path) {
9219
+ try {
9220
+ const stat = lstatSync10(path);
9221
+ if (ownedByUs(stat) && stat.isFile())
9222
+ rmSync5(path, { force: true });
9223
+ } catch {}
9224
+ }
9225
+ function appendPhaseEntry(sessionId, entry, root, locations) {
9226
+ const path = entryLogPath(sessionId, root);
9227
+ let recorded = false;
9228
+ try {
9229
+ recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry));
9230
+ } catch {}
9231
+ if (!recorded)
9232
+ dropPrivate(path);
9233
+ return recorded;
9234
+ }
9235
+ function recordPhaseEntryPrompt(payload, locations = {}) {
9236
+ if (payload.hook_event_name !== "UserPromptSubmit")
9237
+ return false;
9238
+ const sessionId = sessionIdOf(payload);
9239
+ if (!sessionId)
9240
+ return false;
9241
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
9242
+ const entry = recognizePhaseEntry(prompt, claudeAliasReader(cwdOf(payload), locations.home ?? homedir()));
9243
+ return appendPhaseEntry(sessionId, entry, locations.root ?? tmpdir2(), locations);
9244
+ }
9245
+ function gatedOperation(payload) {
9246
+ const toolName = String(payload.tool_name ?? "");
9247
+ if (toolName === "Skill") {
9248
+ const input = payload.tool_input ?? {};
9249
+ const skill = typeof input.skill === "string" ? packagedPhaseSkill(input.skill) : null;
9250
+ return skill ? { kind: "skill", skill } : null;
9251
+ }
9252
+ const mcp = GATED_MCP_TOOL.exec(toolName);
9253
+ return mcp ? { kind: "tool", name: mcp[1] } : null;
9254
+ }
9255
+ function phaseEntryGateHookOutput(payload, locations = {}) {
9256
+ if (payload.hook_event_name !== "PreToolUse")
9257
+ return null;
9258
+ const operation = gatedOperation(payload);
9259
+ if (!operation)
9260
+ return null;
9261
+ const root = locations.root ?? tmpdir2();
9262
+ if (operation.kind === "skill" && operation.skill !== "imm-run") {
9263
+ let activeClaim = false;
9264
+ try {
9265
+ activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null;
9266
+ } catch {}
9267
+ const sessionId = sessionIdOf(payload);
9268
+ if (!activeClaim && sessionId)
9269
+ appendPhaseEntry(sessionId, "plan", root, locations);
9270
+ return null;
9271
+ }
9272
+ let state = null;
9273
+ try {
9274
+ state = readPhaseEntryState(sessionIdOf(payload), root, locations.read);
9275
+ } catch {}
9276
+ const admission = admitPhaseOperation(operation, state);
9277
+ if (!admission.ok)
9278
+ return denyPreToolUse(phaseRefusalMessage(admission));
9279
+ if (operation.kind === "skill")
9280
+ return null;
9281
+ const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : "";
9282
+ let recorded = false;
9283
+ try {
9284
+ recorded = toolUseId !== "" && (locations.append ?? appendPrivate)(admissionPath(toolUseId, root), cacheDir(root), `${JSON.stringify({ v: 1, operation: operation.name })}
9285
+ `);
9286
+ } catch {}
9287
+ if (recorded)
9288
+ return null;
9289
+ return denyPreToolUse(`${RUN_ENTRY_REQUIRED}: the admission for this ${operation.name} call could not be recorded; recovery: ${PHASE_ENTRY_RECOVERY[RUN_ENTRY_REQUIRED]}`);
9290
+ }
9291
+ function consumePhaseAdmission(toolUseId, operation, root = tmpdir2()) {
9292
+ if (!toolUseId || !ensurePrivateDir(cacheDir(root)))
9293
+ return false;
9294
+ const path = admissionPath(toolUseId, root);
9295
+ let text;
9296
+ try {
9297
+ text = readPrivate(path);
9298
+ } catch {
9299
+ return false;
9300
+ }
9301
+ if (text === undefined)
9302
+ return false;
9303
+ dropPrivate(path);
9304
+ const lines = text.split(`
9305
+ `).filter(Boolean);
9306
+ if (lines.length === 0)
9307
+ return false;
9308
+ return lines.every((line) => {
9309
+ try {
9310
+ const record = JSON.parse(line);
9311
+ return record?.v === 1 && record.operation === operation;
9312
+ } catch {
9313
+ return false;
9314
+ }
9315
+ });
9316
+ }
9038
9317
 
9039
9318
  // plugins/immune-brain/runtime/assurance/verdict_authority.ts
9040
9319
  import { randomUUID as randomUUID7 } from "node:crypto";
9041
9320
  import { execFileSync as execFileSync4 } from "node:child_process";
9042
9321
  import { existsSync as existsSync9 } from "node:fs";
9043
- import { join as join12, resolve as resolve12 } from "node:path";
9322
+ import { join as join13, resolve as resolve12 } from "node:path";
9044
9323
 
9045
9324
  // plugins/immune-brain/runtime/kernel/application.ts
9046
9325
  function applyTaskAction(input) {
@@ -9698,15 +9977,15 @@ import { execFileSync as execFileSync2 } from "node:child_process";
9698
9977
  import { createHash as createHash14 } from "node:crypto";
9699
9978
  import {
9700
9979
  mkdtempSync,
9701
- rmSync as rmSync5,
9980
+ rmSync as rmSync6,
9702
9981
  writeFileSync as writeFileSync6,
9703
9982
  statSync as statSync3,
9704
9983
  readFileSync as readFileSync10,
9705
9984
  realpathSync as realpathSync8,
9706
9985
  chmodSync
9707
9986
  } from "node:fs";
9708
- import { tmpdir as tmpdir2 } from "node:os";
9709
- import { join as join9 } from "node:path";
9987
+ import { tmpdir as tmpdir3 } from "node:os";
9988
+ import { join as join10 } from "node:path";
9710
9989
  var MAX_REVIEW_BUNDLE_BYTES = 2 * 1024 * 1024;
9711
9990
  function bundleDigest(bundle) {
9712
9991
  return `sha256:${createHash14("sha256").update(JSON.stringify(bundle)).digest("hex")}`;
@@ -9930,9 +10209,9 @@ function publishReviewRevision(root, snapshot, diffHash, taskId) {
9930
10209
  throw new Error("review revision base has invalid identity");
9931
10210
  if (!REVISION_DIFF_HASH.test(diffHash))
9932
10211
  throw new Error("review revision diff hash has invalid identity");
9933
- const indexDirectory = mkdtempSync(join9(tmpdir2(), "imm-review-index-"));
10212
+ const indexDirectory = mkdtempSync(join10(tmpdir3(), "imm-review-index-"));
9934
10213
  try {
9935
- const indexFile = join9(indexDirectory, "index");
10214
+ const indexFile = join10(indexDirectory, "index");
9936
10215
  const env = { GIT_INDEX_FILE: indexFile };
9937
10216
  gitEvidence(root, ["read-tree", snapshot.base_tree], env);
9938
10217
  for (const [path, entry] of Object.entries(snapshot.changed_paths)) {
@@ -9982,7 +10261,7 @@ function publishReviewRevision(root, snapshot, diffHash, taskId) {
9982
10261
  diff_hash: diffHash
9983
10262
  };
9984
10263
  } finally {
9985
- rmSync5(indexDirectory, { recursive: true, force: true });
10264
+ rmSync6(indexDirectory, { recursive: true, force: true });
9986
10265
  }
9987
10266
  }
9988
10267
  function publishInput(root, input) {
@@ -10025,19 +10304,19 @@ function captureReviewManifest(root, input) {
10025
10304
  return manifest;
10026
10305
  }
10027
10306
  function writeNativeReviewEvidence(payload) {
10028
- const rawDirectory = mkdtempSync(join9(tmpdir2(), "imm-canary-native-review-"));
10307
+ const rawDirectory = mkdtempSync(join10(tmpdir3(), "imm-canary-native-review-"));
10029
10308
  try {
10030
10309
  const directory = realpathSync8(rawDirectory);
10031
10310
  chmodSync(directory, 493);
10032
- const path = join9(directory, "evidence.json");
10311
+ const path = join10(directory, "evidence.json");
10033
10312
  writeFileSync6(path, JSON.stringify(payload), { encoding: "utf8", mode: 420, flag: "wx" });
10034
10313
  assertReviewArtifact(path);
10035
10314
  return {
10036
10315
  path,
10037
- remove: () => rmSync5(directory, { recursive: true, force: true })
10316
+ remove: () => rmSync6(directory, { recursive: true, force: true })
10038
10317
  };
10039
10318
  } catch (error) {
10040
- rmSync5(rawDirectory, { recursive: true, force: true });
10319
+ rmSync6(rawDirectory, { recursive: true, force: true });
10041
10320
  throw error;
10042
10321
  }
10043
10322
  }
@@ -10062,8 +10341,8 @@ import { createHash as createHash17, randomUUID as randomUUID6 } from "node:cryp
10062
10341
  // plugins/immune-brain/runtime/assurance/verification.ts
10063
10342
  import { createHash as createHash15, randomBytes } from "node:crypto";
10064
10343
  import { execFileSync as execFileSync3, spawn as spawn2 } from "node:child_process";
10065
- import { accessSync, constants as constants5, readFileSync as readFileSync11, readdirSync as readdirSync5, realpathSync as realpathSync9, statSync as statSync4 } from "node:fs";
10066
- import { delimiter, isAbsolute as isAbsolute5, join as join10, relative as relative5, resolve as resolve11, sep as sep6 } from "node:path";
10344
+ import { accessSync, constants as constants6, readFileSync as readFileSync11, readdirSync as readdirSync5, realpathSync as realpathSync9, statSync as statSync4 } from "node:fs";
10345
+ import { delimiter, isAbsolute as isAbsolute5, join as join11, relative as relative5, resolve as resolve11, sep as sep6 } from "node:path";
10067
10346
  class VerificationAbortedError extends Error {
10068
10347
  constructor() {
10069
10348
  super("fixed verification aborted");
@@ -10099,9 +10378,9 @@ function verificationPath() {
10099
10378
  }
10100
10379
  function toolPath(name, path) {
10101
10380
  for (const dir of path.split(delimiter).filter(isAbsolute5)) {
10102
- const candidate = join10(dir, name);
10381
+ const candidate = join11(dir, name);
10103
10382
  try {
10104
- accessSync(candidate, constants5.X_OK);
10383
+ accessSync(candidate, constants6.X_OK);
10105
10384
  if (statSync4(candidate).isFile())
10106
10385
  return candidate;
10107
10386
  } catch {}
@@ -10111,7 +10390,7 @@ function toolPath(name, path) {
10111
10390
  function identity(path) {
10112
10391
  try {
10113
10392
  const real = realpathSync9(path);
10114
- accessSync(real, constants5.X_OK);
10393
+ accessSync(real, constants6.X_OK);
10115
10394
  const stat = statSync4(real);
10116
10395
  if (!stat.isFile())
10117
10396
  throw new Error("not a file");
@@ -10208,7 +10487,7 @@ async function runFixedVerification(root, command, frozen, options) {
10208
10487
  };
10209
10488
  const procEnviron = (pid) => {
10210
10489
  try {
10211
- return readFileSync11(join10(procRoot, String(pid), "environ"), "utf8");
10490
+ return readFileSync11(join11(procRoot, String(pid), "environ"), "utf8");
10212
10491
  } catch {
10213
10492
  return null;
10214
10493
  }
@@ -10216,7 +10495,7 @@ async function runFixedVerification(root, command, frozen, options) {
10216
10495
  const procSession = (entry) => {
10217
10496
  let stat;
10218
10497
  try {
10219
- stat = readFileSync11(join10(procRoot, entry, "stat"), "utf8");
10498
+ stat = readFileSync11(join11(procRoot, entry, "stat"), "utf8");
10220
10499
  } catch (error) {
10221
10500
  if (error.code === "ENOENT")
10222
10501
  return null;
@@ -10427,7 +10706,7 @@ function createInvocationRegistry() {
10427
10706
  // plugins/immune-brain/runtime/role_prompt_bridge.ts
10428
10707
  import { createHash as createHash16 } from "node:crypto";
10429
10708
  import { existsSync as existsSync8, readFileSync as readFileSync12 } from "node:fs";
10430
- import { join as join11, dirname as dirname8 } from "node:path";
10709
+ import { join as join12, dirname as dirname8 } from "node:path";
10431
10710
  import { fileURLToPath } from "node:url";
10432
10711
  var RUNTIME_DIR = dirname8(fileURLToPath(import.meta.url));
10433
10712
  var INTERNAL_ROLE_PROMPTS = {
@@ -10489,14 +10768,14 @@ function roleSpec(role) {
10489
10768
  }
10490
10769
  function rolePromptSearchDirs(moduleDir) {
10491
10770
  return [
10492
- join11(moduleDir, "..", "dist", "role-prompts"),
10493
- join11(moduleDir, "..", "role-prompts")
10771
+ join12(moduleDir, "..", "dist", "role-prompts"),
10772
+ join12(moduleDir, "..", "role-prompts")
10494
10773
  ];
10495
10774
  }
10496
10775
  function loadRolePrompt(role) {
10497
10776
  const spec = roleSpec(role);
10498
10777
  for (const dir of rolePromptSearchDirs(RUNTIME_DIR)) {
10499
- const path = join11(dir, spec.file);
10778
+ const path = join12(dir, spec.file);
10500
10779
  if (existsSync8(path))
10501
10780
  return readFileSync12(path, "utf8");
10502
10781
  }
@@ -11769,7 +12048,7 @@ function stagePlanningArtifactTransition(root, record) {
11769
12048
  intentArchive,
11770
12049
  ...specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []
11771
12050
  ];
11772
- const paths = candidates.filter((path) => existsSync9(join12(root, path)) || execFileSync4("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
12051
+ const paths = candidates.filter((path) => existsSync9(join13(root, path)) || execFileSync4("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
11773
12052
  if (paths.length === 0)
11774
12053
  return;
11775
12054
  execFileSync4("git", ["add", "--", ...paths], {
@@ -11865,7 +12144,7 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
11865
12144
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
11866
12145
  import { existsSync as existsSync14, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
11867
12146
  import { execFileSync as execFileSync9 } from "node:child_process";
11868
- import { join as join21 } from "node:path";
12147
+ import { join as join22 } from "node:path";
11869
12148
 
11870
12149
  // plugins/immune-brain/runtime/kernel/enrollment_authority.ts
11871
12150
  var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
@@ -12239,9 +12518,9 @@ import { createHash as createHash19 } from "node:crypto";
12239
12518
  // plugins/immune-brain/runtime/assurance/delivery_workspace.ts
12240
12519
  import { execFileSync as execFileSync6 } from "node:child_process";
12241
12520
  import { createHash as createHash18 } from "node:crypto";
12242
- import { chmodSync as chmodSync2, lstatSync as lstatSync10, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync13, readlinkSync as readlinkSync2, realpathSync as realpathSync10, rmSync as rmSync6 } from "node:fs";
12243
- import { tmpdir as tmpdir3 } from "node:os";
12244
- import { dirname as dirname9, isAbsolute as isAbsolute6, join as join13, parse, relative as relative6, resolve as resolve13, sep as sep7 } from "node:path";
12521
+ import { chmodSync as chmodSync2, lstatSync as lstatSync11, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync13, readlinkSync as readlinkSync2, realpathSync as realpathSync10, rmSync as rmSync7 } from "node:fs";
12522
+ import { tmpdir as tmpdir4 } from "node:os";
12523
+ import { dirname as dirname9, isAbsolute as isAbsolute6, join as join14, parse, relative as relative6, resolve as resolve13, sep as sep7 } from "node:path";
12245
12524
 
12246
12525
  class DeliveryWorkspaceError extends Error {
12247
12526
  constructor(message) {
@@ -12253,7 +12532,7 @@ function removeTaskOwnedTree(root) {
12253
12532
  const makeDirectoriesWritable = (path) => {
12254
12533
  let stat;
12255
12534
  try {
12256
- stat = lstatSync10(path);
12535
+ stat = lstatSync11(path);
12257
12536
  } catch (error) {
12258
12537
  if (error.code === "ENOENT")
12259
12538
  return;
@@ -12263,10 +12542,10 @@ function removeTaskOwnedTree(root) {
12263
12542
  return;
12264
12543
  chmodSync2(path, stat.mode & 4095 | 448);
12265
12544
  for (const name of readdirSync6(path))
12266
- makeDirectoriesWritable(join13(path, name));
12545
+ makeDirectoriesWritable(join14(path, name));
12267
12546
  };
12268
12547
  makeDirectoriesWritable(root);
12269
- rmSync6(root, { recursive: true, force: true });
12548
+ rmSync7(root, { recursive: true, force: true });
12270
12549
  }
12271
12550
  var GIT_OBJECT_ID5 = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
12272
12551
  function isolatedGitEnv(extra = {}) {
@@ -12314,9 +12593,9 @@ function resolvedSymlinkTarget(path) {
12314
12593
  current = dirname9(current);
12315
12594
  continue;
12316
12595
  }
12317
- const next = join13(current, part);
12596
+ const next = join14(current, part);
12318
12597
  try {
12319
- if (!lstatSync10(next).isSymbolicLink()) {
12598
+ if (!lstatSync11(next).isSymbolicLink()) {
12320
12599
  current = next;
12321
12600
  continue;
12322
12601
  }
@@ -12337,8 +12616,8 @@ function resolvedSymlinkTarget(path) {
12337
12616
  function assertNoEscapingSymlinks(root, dir = root) {
12338
12617
  const realRoot = realpathSync10(root);
12339
12618
  for (const name of readdirSync6(dir)) {
12340
- const path = join13(dir, name);
12341
- const stat = lstatSync10(path);
12619
+ const path = join14(dir, name);
12620
+ const stat = lstatSync11(path);
12342
12621
  if (stat.isSymbolicLink()) {
12343
12622
  assertInside(realRoot, resolvedSymlinkTarget(path), `symlink ${relative6(root, path)}`);
12344
12623
  } else if (stat.isDirectory()) {
@@ -12347,41 +12626,41 @@ function assertNoEscapingSymlinks(root, dir = root) {
12347
12626
  }
12348
12627
  }
12349
12628
  function writeDeliveryTree(sourceRoot, snapshot) {
12350
- const indexDirectory = mkdtempSync2(join13(tmpdir3(), "imm-delivery-index-"));
12629
+ const indexDirectory = mkdtempSync2(join14(tmpdir4(), "imm-delivery-index-"));
12351
12630
  try {
12352
12631
  git5(sourceRoot, ["read-tree", snapshot.base_tree], {
12353
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12354
- GIT_DIR: join13(sourceRoot, ".git")
12632
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12633
+ GIT_DIR: join14(sourceRoot, ".git")
12355
12634
  });
12356
12635
  for (const [path, entry] of Object.entries(snapshot.changed_paths)) {
12357
12636
  if (entry.oid && entry.mode) {
12358
12637
  git5(sourceRoot, ["update-index", "--add", "--cacheinfo", `${entry.mode},${entry.oid},${path}`], {
12359
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12360
- GIT_DIR: join13(sourceRoot, ".git")
12638
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12639
+ GIT_DIR: join14(sourceRoot, ".git")
12361
12640
  });
12362
12641
  } else {
12363
12642
  git5(sourceRoot, ["update-index", "--force-remove", "--", path], {
12364
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12365
- GIT_DIR: join13(sourceRoot, ".git")
12643
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12644
+ GIT_DIR: join14(sourceRoot, ".git")
12366
12645
  });
12367
12646
  }
12368
12647
  }
12369
12648
  const next = git5(sourceRoot, ["write-tree"], {
12370
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12371
- GIT_DIR: join13(sourceRoot, ".git")
12649
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12650
+ GIT_DIR: join14(sourceRoot, ".git")
12372
12651
  });
12373
12652
  assertTree(next);
12374
12653
  return next;
12375
12654
  } finally {
12376
- rmSync6(indexDirectory, { recursive: true, force: true });
12655
+ rmSync7(indexDirectory, { recursive: true, force: true });
12377
12656
  }
12378
12657
  }
12379
12658
  function workspaceFiles(root, dir = root, result = Object.create(null)) {
12380
12659
  if (dir === root)
12381
- result["."] = `${lstatSync10(root).mode & 4095}:directory`;
12660
+ result["."] = `${lstatSync11(root).mode & 4095}:directory`;
12382
12661
  for (const name of readdirSync6(dir).sort()) {
12383
- const path = join13(dir, name), rel = relative6(root, path);
12384
- const stat = lstatSync10(path);
12662
+ const path = join14(dir, name), rel = relative6(root, path);
12663
+ const stat = lstatSync11(path);
12385
12664
  if (stat.isSymbolicLink())
12386
12665
  result[rel] = `link:${stat.mode & 4095}:${readlinkSync2(path)}`;
12387
12666
  else if (stat.isDirectory()) {
@@ -12408,7 +12687,7 @@ function assertDeliveryClean(root, tree, seal, writablePaths = []) {
12408
12687
  }
12409
12688
  if (seal === "[]") {
12410
12689
  for (const name of readdirSync6(root)) {
12411
- const stat = lstatSync10(join13(root, name));
12690
+ const stat = lstatSync11(join14(root, name));
12412
12691
  if (!stat.isDirectory() || name !== ".git")
12413
12692
  throw new DeliveryWorkspaceError("delivery workspace was contaminated");
12414
12693
  }
@@ -12430,18 +12709,18 @@ function assertDeliveryClean(root, tree, seal, writablePaths = []) {
12430
12709
  if (!permitted)
12431
12710
  throw new DeliveryWorkspaceError("delivery undeclared output was contaminated");
12432
12711
  if (bytes.startsWith("link:")) {
12433
- assertInside(realpathSync10(root), resolvedSymlinkTarget(join13(root, path)), "generated symlink");
12712
+ assertInside(realpathSync10(root), resolvedSymlinkTarget(join14(root, path)), "generated symlink");
12434
12713
  }
12435
12714
  }
12436
12715
  }
12437
12716
  function materializeDeliveryWorkspace(sourceRoot, tree) {
12438
12717
  assertTree(tree);
12439
- const dest = mkdtempSync2(join13(tmpdir3(), "imm-delivery-"));
12718
+ const dest = mkdtempSync2(join14(tmpdir4(), "imm-delivery-"));
12440
12719
  const cleanup = () => removeTaskOwnedTree(dest);
12441
12720
  try {
12442
12721
  mkdirSync7(dest, { recursive: true });
12443
12722
  const commit = git5(sourceRoot, ["commit-tree", tree, "-m", `delivery ${tree}`], {
12444
- GIT_DIR: join13(sourceRoot, ".git"),
12723
+ GIT_DIR: join14(sourceRoot, ".git"),
12445
12724
  GIT_AUTHOR_NAME: "Immune-Brain Assurance",
12446
12725
  GIT_AUTHOR_EMAIL: "assurance@immune-brain.local",
12447
12726
  GIT_AUTHOR_DATE: "1970-01-01T00:00:00 +0000",
@@ -12469,8 +12748,8 @@ function materializeDeliveryWorkspace(sourceRoot, tree) {
12469
12748
 
12470
12749
  // plugins/immune-brain/runtime/assurance/qa.ts
12471
12750
  import { mkdtempSync as mkdtempSync3, realpathSync as realpathSync11 } from "node:fs";
12472
- import { tmpdir as tmpdir4 } from "node:os";
12473
- import { join as join14, sep as sep8 } from "node:path";
12751
+ import { tmpdir as tmpdir5 } from "node:os";
12752
+ import { join as join15, sep as sep8 } from "node:path";
12474
12753
 
12475
12754
  // plugins/immune-brain/runtime/assurance/qa_findings.ts
12476
12755
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -12537,7 +12816,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
12537
12816
  let delivery, home;
12538
12817
  try {
12539
12818
  delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
12540
- home = mkdtempSync3(join14(tmpdir4(), "imm-qa-home-"));
12819
+ home = mkdtempSync3(join15(tmpdir5(), "imm-qa-home-"));
12541
12820
  return { delivery, home, deliveryPrefix: `${realpathSync11(delivery.root)}${sep8}` };
12542
12821
  } catch {
12543
12822
  let reason = "delivery_unavailable";
@@ -12705,11 +12984,11 @@ function deriveAuthorizationOperation(input) {
12705
12984
  // plugins/immune-brain/runtime/staged_intent.ts
12706
12985
  import { execFileSync as execFileSync7 } from "node:child_process";
12707
12986
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
12708
- import { join as join15 } from "node:path";
12987
+ import { join as join16 } from "node:path";
12709
12988
  function captureStagedIntent(root, relativePath) {
12710
12989
  return {
12711
12990
  path: relativePath,
12712
- bytes: readFileSync14(join15(root, relativePath)),
12991
+ bytes: readFileSync14(join16(root, relativePath)),
12713
12992
  index_state: execFileSync7("git", ["ls-files", "--stage", "-z", "--", relativePath], {
12714
12993
  cwd: root,
12715
12994
  stdio: ["ignore", "pipe", "pipe"]
@@ -12717,7 +12996,7 @@ function captureStagedIntent(root, relativePath) {
12717
12996
  };
12718
12997
  }
12719
12998
  function restoreStagedIntent(root, snapshot) {
12720
- writeFileSync7(join15(root, snapshot.path), snapshot.bytes);
12999
+ writeFileSync7(join16(root, snapshot.path), snapshot.bytes);
12721
13000
  execFileSync7("git", ["update-index", "--force-remove", "--", snapshot.path], {
12722
13001
  cwd: root,
12723
13002
  stdio: ["ignore", "pipe", "pipe"]
@@ -12729,7 +13008,7 @@ function restoreStagedIntent(root, snapshot) {
12729
13008
  stdio: ["pipe", "ignore", "pipe"]
12730
13009
  });
12731
13010
  }
12732
- const restoredBytes = readFileSync14(join15(root, snapshot.path));
13011
+ const restoredBytes = readFileSync14(join16(root, snapshot.path));
12733
13012
  if (!restoredBytes.equals(snapshot.bytes)) {
12734
13013
  throw new Error("failed to restore prior intent bytes");
12735
13014
  }
@@ -12745,24 +13024,24 @@ function restoreStagedIntent(root, snapshot) {
12745
13024
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
12746
13025
  import { spawnSync as spawnSync14 } from "node:child_process";
12747
13026
  import { existsSync as existsSync13 } from "node:fs";
12748
- import { join as join20 } from "node:path";
13027
+ import { join as join21 } from "node:path";
12749
13028
 
12750
13029
  // plugins/immune-brain/runtime/unattended/batch_git.ts
12751
13030
  import { spawnSync as spawnSync10 } from "node:child_process";
12752
13031
  import { createHash as createHash20, randomUUID as randomUUID9 } from "node:crypto";
12753
13032
  import {
12754
- constants as constants6,
12755
- closeSync as closeSync7,
13033
+ constants as constants7,
13034
+ closeSync as closeSync8,
12756
13035
  existsSync as existsSync10,
12757
- lstatSync as lstatSync11,
13036
+ lstatSync as lstatSync12,
12758
13037
  mkdirSync as mkdirSync8,
12759
- openSync as openSync7,
13038
+ openSync as openSync8,
12760
13039
  realpathSync as realpathSync12,
12761
13040
  renameSync as renameSync5,
12762
- rmSync as rmSync7,
13041
+ rmSync as rmSync8,
12763
13042
  writeFileSync as writeFileSync8
12764
13043
  } from "node:fs";
12765
- import { dirname as dirname10, join as join16 } from "node:path";
13044
+ import { dirname as dirname10, join as join17 } from "node:path";
12766
13045
  var DEFAULT_GIT_ENV = {
12767
13046
  ...process.env,
12768
13047
  GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch",
@@ -12974,15 +13253,15 @@ function getCommittedDeltaPaths(root) {
12974
13253
  return delta.stdout.split("\x00").filter((p) => p.length > 0);
12975
13254
  }
12976
13255
  function commitEvidencePath(batchId, taskId) {
12977
- return join16(".imm", "state", "batches", "commits", `${batchId}-${taskId}.json`);
13256
+ return join17(".imm", "state", "batches", "commits", `${batchId}-${taskId}.json`);
12978
13257
  }
12979
13258
  function ensureSecureDirectory3(root, relativePath) {
12980
13259
  const segments = relativePath.split("/").filter(Boolean);
12981
13260
  let current = root;
12982
13261
  for (const segment of segments) {
12983
- current = join16(current, segment);
13262
+ current = join17(current, segment);
12984
13263
  if (existsSync10(current)) {
12985
- const stats = lstatSync11(current);
13264
+ const stats = lstatSync12(current);
12986
13265
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
12987
13266
  throw new Error(`${segment} exists but is not a real directory`);
12988
13267
  }
@@ -12993,15 +13272,15 @@ function ensureSecureDirectory3(root, relativePath) {
12993
13272
  return current;
12994
13273
  }
12995
13274
  function writeFileAtomically2(root, relativePath, bytes) {
12996
- const target = join16(root, relativePath);
13275
+ const target = join17(root, relativePath);
12997
13276
  const targetDir = dirname10(target);
12998
13277
  ensureSecureDirectory3(root, dirname10(relativePath));
12999
- const stats = lstatSync11(targetDir);
13278
+ const stats = lstatSync12(targetDir);
13000
13279
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
13001
13280
  throw new Error(`${dirname10(relativePath)} is not a real directory`);
13002
13281
  }
13003
13282
  if (existsSync10(target)) {
13004
- const targetStats = lstatSync11(target);
13283
+ const targetStats = lstatSync12(target);
13005
13284
  if (targetStats.isSymbolicLink()) {
13006
13285
  throw new Error(`${relativePath} is a symlink`);
13007
13286
  }
@@ -13009,17 +13288,17 @@ function writeFileAtomically2(root, relativePath, bytes) {
13009
13288
  const tempPath = `${target}.${randomUUID9()}.tmp`;
13010
13289
  let fd = null;
13011
13290
  try {
13012
- fd = openSync7(tempPath, constants6.O_WRONLY | constants6.O_CREAT | constants6.O_EXCL, 384);
13291
+ fd = openSync8(tempPath, constants7.O_WRONLY | constants7.O_CREAT | constants7.O_EXCL, 384);
13013
13292
  writeFileSync8(fd, bytes, "utf8");
13014
- closeSync7(fd);
13293
+ closeSync8(fd);
13015
13294
  fd = null;
13016
13295
  renameSync5(tempPath, target);
13017
13296
  } finally {
13018
13297
  if (fd !== null)
13019
- closeSync7(fd);
13298
+ closeSync8(fd);
13020
13299
  if (existsSync10(tempPath)) {
13021
13300
  try {
13022
- rmSync7(tempPath);
13301
+ rmSync8(tempPath);
13023
13302
  } catch {}
13024
13303
  }
13025
13304
  }
@@ -13039,7 +13318,7 @@ function writeBatchCommitEvidence(root, evidence) {
13039
13318
  }
13040
13319
  function readBatchCommitEvidence(root, batchId, taskId) {
13041
13320
  const path = commitEvidencePath(batchId, taskId);
13042
- const fullPath = join16(root, path);
13321
+ const fullPath = join17(root, path);
13043
13322
  if (!existsSync10(fullPath))
13044
13323
  return null;
13045
13324
  try {
@@ -13314,11 +13593,11 @@ function createDefaultBatchGitPort() {
13314
13593
  import { spawnSync as spawnSync13 } from "node:child_process";
13315
13594
  import { createHash as createHash21 } from "node:crypto";
13316
13595
  import { existsSync as existsSync12, realpathSync as realpathSync13 } from "node:fs";
13317
- import { isAbsolute as isAbsolute7, join as join19 } from "node:path";
13596
+ import { isAbsolute as isAbsolute7, join as join20 } from "node:path";
13318
13597
 
13319
13598
  // plugins/immune-brain/runtime/kernel/runtime_contracts.ts
13320
13599
  import { existsSync as existsSync11, readFileSync as readFileSync15 } from "node:fs";
13321
- import { dirname as dirname11, join as join17, resolve as resolve14 } from "node:path";
13600
+ import { dirname as dirname11, join as join18, resolve as resolve14 } from "node:path";
13322
13601
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13323
13602
  var RUNTIME_CONTRACTS_MANIFEST = "runtime_contracts.json";
13324
13603
  var RUNTIME_CONTRACTS = {
@@ -13338,10 +13617,10 @@ function runningRuntimeSource() {
13338
13617
  }
13339
13618
  function readRuntimeContracts(runtimeDir) {
13340
13619
  const candidates = [
13341
- join17(runtimeDir, RUNTIME_CONTRACTS_MANIFEST),
13342
- join17(runtimeDir, "kernel", RUNTIME_CONTRACTS_MANIFEST),
13343
- join17(runtimeDir, "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST),
13344
- join17(runtimeDir, "plugins", "immune-brain", "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST)
13620
+ join18(runtimeDir, RUNTIME_CONTRACTS_MANIFEST),
13621
+ join18(runtimeDir, "kernel", RUNTIME_CONTRACTS_MANIFEST),
13622
+ join18(runtimeDir, "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST),
13623
+ join18(runtimeDir, "plugins", "immune-brain", "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST)
13345
13624
  ];
13346
13625
  const path = candidates.find((candidate) => existsSync11(candidate));
13347
13626
  if (!path)
@@ -13412,7 +13691,7 @@ async function runFinalVerification(root, commands, options = {}) {
13412
13691
 
13413
13692
  // plugins/immune-brain/runtime/unattended/batch_delegation.ts
13414
13693
  import { execFileSync as execFileSync8 } from "node:child_process";
13415
- import { join as join18 } from "node:path";
13694
+ import { join as join19 } from "node:path";
13416
13695
  import { writeFileSync as writeFileSync9 } from "node:fs";
13417
13696
  var REVISION_DELEGATION_TEXT = "Optional, off by default: also let the batch coordinator approve a Lane child's breaking Intent revisions that stay inside that child's authorized TaskIntent (acceptance changes, a narrower scope_hint). Widening scope, changing risk, goal or the bound Spec still needs your own confirmation. The grant ends with this batch.";
13418
13697
  var DELEGATED_ACTOR_ID = "batch-coordinator";
@@ -13463,7 +13742,7 @@ function applyDelegatedIntentRevision(input) {
13463
13742
  const nextRef = { path: `docs/plans/${taskId}.intent.json`, content_hash: nextHash };
13464
13743
  const snapshot = captureStagedIntent(root, prior.intent_ref.path);
13465
13744
  try {
13466
- writeFileSync9(join18(root, prior.intent_ref.path), `${JSON.stringify(nextIntent, null, 2)}
13745
+ writeFileSync9(join19(root, prior.intent_ref.path), `${JSON.stringify(nextIntent, null, 2)}
13467
13746
  `);
13468
13747
  execFileSync8("git", ["add", "--", prior.intent_ref.path], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
13469
13748
  const prepared = await projectAssurance(root, taskId, taskDeliveryIdentity);
@@ -13832,7 +14111,7 @@ function laneRuntimeDir(root, explicit) {
13832
14111
  if (explicit)
13833
14112
  return explicit;
13834
14113
  try {
13835
- const manifest = JSON.parse(readFileSync16(join19(root, "package.json"), "utf8"));
14114
+ const manifest = JSON.parse(readFileSync16(join20(root, "package.json"), "utf8"));
13836
14115
  return manifest.name === "immune-brain" ? root : null;
13837
14116
  } catch {
13838
14117
  return null;
@@ -14140,7 +14419,7 @@ function finalizeLane(root, record, reason, nextAction, extra = {}) {
14140
14419
  writeBatchRunReport(root, report);
14141
14420
  return report;
14142
14421
  }
14143
- function refuse2(input, persisted, code, detail) {
14422
+ function refuse3(input, persisted, code, detail) {
14144
14423
  const base = persisted && isLaneBatchRecord(persisted) ? persisted : prepareBatchLaneRunState({
14145
14424
  batch_id: input.batch_id,
14146
14425
  initiative_slug: input.initiative_slug,
@@ -14229,12 +14508,12 @@ function isLineageBreak(message) {
14229
14508
  }
14230
14509
  async function runLaneBatch(input, persisted) {
14231
14510
  if (persisted && !isLaneBatchRecord(persisted))
14232
- return refuse2(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
14511
+ return refuse3(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
14233
14512
  if (persisted && input.max_parallel !== undefined && input.max_parallel !== persisted.max_parallel)
14234
- return refuse2(input, persisted, PARALLEL_MISMATCH, `the recorded batch runs with max_parallel ${persisted.max_parallel}`);
14513
+ return refuse3(input, persisted, PARALLEL_MISMATCH, `the recorded batch runs with max_parallel ${persisted.max_parallel}`);
14235
14514
  const limit = persisted?.max_parallel ?? input.max_parallel ?? 1;
14236
14515
  if (!input.children.length)
14237
- return refuse2(input, persisted, "batch_plan_empty", "batch plan is empty");
14516
+ return refuse3(input, persisted, "batch_plan_empty", "batch plan is empty");
14238
14517
  const git = serialGitOf(input);
14239
14518
  const lanes = laneGitOf(input);
14240
14519
  let record;
@@ -14281,11 +14560,11 @@ async function runLaneBatch(input, persisted) {
14281
14560
  } else {
14282
14561
  const invalid = validateNewAuthorization(input);
14283
14562
  if (invalid)
14284
- return refuse2(input, null, "batch_authorization_invalid", invalid);
14563
+ return refuse3(input, null, "batch_authorization_invalid", invalid);
14285
14564
  const contractRefusal = laneRuntimeContractRefusal(input.root, input.executor_runtime);
14286
14565
  if (contractRefusal) {
14287
14566
  const [code, ...detail] = contractRefusal.split(": ");
14288
- return refuse2(input, null, code, detail.join(": "));
14567
+ return refuse3(input, null, code, detail.join(": "));
14289
14568
  }
14290
14569
  const preflight = await git.preflight({
14291
14570
  root: input.root,
@@ -14293,7 +14572,7 @@ async function runLaneBatch(input, persisted) {
14293
14572
  base_head: input.base_head
14294
14573
  });
14295
14574
  if (!preflight.ok)
14296
- return refuse2(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
14575
+ return refuse3(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
14297
14576
  record = prepareBatchLaneRunState({
14298
14577
  batch_id: input.batch_id,
14299
14578
  initiative_slug: input.initiative_slug,
@@ -14357,7 +14636,7 @@ async function runLaneBatch(input, persisted) {
14357
14636
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "released" } : c);
14358
14637
  persist();
14359
14638
  }
14360
- const lineage = existsSync12(join19(input.root, ".git")) ? classifyBatchLineage({
14639
+ const lineage = existsSync12(join20(input.root, ".git")) ? classifyBatchLineage({
14361
14640
  root: input.root,
14362
14641
  branch: record.branch ?? "",
14363
14642
  expectedHead: expectedBatchHead(record),
@@ -14808,7 +15087,7 @@ function failPersistedLineage(root, existing, message) {
14808
15087
  return writeBatchRunState(root, record);
14809
15088
  }
14810
15089
  function reconcileLineage(root, record) {
14811
- if (!existsSync13(join20(root, ".git")))
15090
+ if (!existsSync13(join21(root, ".git")))
14812
15091
  return { record, failure: null };
14813
15092
  const expected = expectedBatchHead(record);
14814
15093
  const lineage = classifyBatchLineage({
@@ -14831,7 +15110,7 @@ function reconcileLineage(root, record) {
14831
15110
  };
14832
15111
  }
14833
15112
  async function validatePersistedRun(input, record) {
14834
- if (!record.commits.length && record.children[0]?.state === "settled" && existsSync13(join20(input.root, ".git"))) {
15113
+ if (!record.commits.length && record.children[0]?.state === "settled" && existsSync13(join21(input.root, ".git"))) {
14835
15114
  const head = spawnSync14("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
14836
15115
  const live = head.stdout.trim();
14837
15116
  const expected = expectedBatchHead(record);
@@ -14861,7 +15140,7 @@ async function validatePersistedRun(input, record) {
14861
15140
  const git = batchGitPortOf(input);
14862
15141
  const evidence = await git.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch);
14863
15142
  if (!evidence || evidence.commit !== child.commit) {
14864
- if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync13(join20(input.root, ".git"))) {
15143
+ if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync13(join21(input.root, ".git"))) {
14865
15144
  const reach = spawnSync14("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
14866
15145
  if (reach.status !== 0) {
14867
15146
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
@@ -15301,7 +15580,7 @@ async function driveInterruptedChild(input, child) {
15301
15580
  const planChild = input.children.find((c) => c.task_id === child.task_id);
15302
15581
  const intentPath = planChild?.intent_path ?? undefined;
15303
15582
  const doCommit = async () => batchGitPortOf(input).commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath);
15304
- if (existing && !record.commits.length && existsSync13(join20(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
15583
+ if (existing && !record.commits.length && existsSync13(join21(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
15305
15584
  throw new Error("first unpersisted batch commit provenance is invalid");
15306
15585
  const adopted = existing ?? await doCommit().catch((error) => {
15307
15586
  const message = error instanceof Error ? error.message : String(error);
@@ -15832,7 +16111,7 @@ class ClaudeRuntime {
15832
16111
  if (!slug)
15833
16112
  return result;
15834
16113
  try {
15835
- if (existsSync14(join21(this.cwd, ".imm", "audit", taskId)))
16114
+ if (existsSync14(join22(this.cwd, ".imm", "audit", taskId)))
15836
16115
  execFileSync9("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
15837
16116
  const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
15838
16117
  return { ...result, batch };
@@ -15929,7 +16208,7 @@ class ClaudeRuntime {
15929
16208
  throw new Error("approve_breaking_intent_revision requires next_intent");
15930
16209
  const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
15931
16210
  const nextIntentRef = nextIntent ? { path: `docs/plans/${nextIntent.task_id}.intent.json`, content_hash: nextIntentHash } : undefined;
15932
- const sidecar = nextIntent ? join21(this.cwd, priorIntent.intent_ref.path) : undefined;
16211
+ const sidecar = nextIntent ? join22(this.cwd, priorIntent.intent_ref.path) : undefined;
15933
16212
  const stagedSnapshot = sidecar ? captureStagedIntent(this.cwd, priorIntent.intent_ref.path) : undefined;
15934
16213
  const restoreStagedIntent2 = () => {
15935
16214
  if (!stagedSnapshot)
@@ -16038,7 +16317,7 @@ class ClaudeRuntime {
16038
16317
  const { app } = await this.authority();
16039
16318
  const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
16040
16319
  const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
16041
- const sidecar = join21(ctx.cwd, priorIntent.intent_ref.path);
16320
+ const sidecar = join22(ctx.cwd, priorIntent.intent_ref.path);
16042
16321
  const priorBytes = operation.op === "revise_intent" ? readFileSync17(sidecar) : null;
16043
16322
  const priorStaged = priorBytes !== null ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : null;
16044
16323
  try {
@@ -16442,6 +16721,12 @@ function createMcpRuntime(options = {}) {
16442
16721
  let negotiatedVersion;
16443
16722
  let negotiatedInteractive = false;
16444
16723
  const connectionId = randomUUID10();
16724
+ const consumeAdmission = options.consumePhaseAdmission ?? ((toolCallId, operation) => consumePhaseAdmission(toolCallId, operation));
16725
+ const requireRunEntry = (operation, toolCallId) => {
16726
+ if (consumeAdmission(toolCallId, operation))
16727
+ return;
16728
+ throw new NativeAuthorityError("run_entry_required", `${operation} needs a literal-user imm-run entry in this session`);
16729
+ };
16445
16730
  return {
16446
16731
  runtime,
16447
16732
  host,
@@ -16471,6 +16756,7 @@ function createMcpRuntime(options = {}) {
16471
16756
  const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
16472
16757
  if (!probe.ok)
16473
16758
  throw new NativeAuthorityError("unsupported_host", probe.reason);
16759
+ requireRunEntry(name, meta.toolCallId ?? `call-${name}`);
16474
16760
  const toolMeta = {
16475
16761
  sessionId: meta.sessionId ?? connectionId,
16476
16762
  toolCallId: meta.toolCallId ?? `call-${name}`,
@@ -16538,8 +16824,10 @@ function createMcpRuntime(options = {}) {
16538
16824
  interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
16539
16825
  signal: meta.signal
16540
16826
  };
16541
- if (name === "enroll")
16827
+ if (name === "enroll") {
16828
+ requireRunEntry(name, meta.toolCallId ?? `call-${name}`);
16542
16829
  return runtime.enroll(taskId, toolMeta);
16830
+ }
16543
16831
  if (name === "advance_assurance")
16544
16832
  return runtime.advance(taskId, toolMeta.signal, toolMeta);
16545
16833
  if (name === "submit_review") {
@@ -17046,13 +17334,14 @@ async function runHook() {
17046
17334
  payload = null;
17047
17335
  }
17048
17336
  if (payload?.hook_event_name === "PreToolUse") {
17049
- const decision = laneGuardHookOutput(payload);
17337
+ const decision = laneGuardHookOutput(payload) ?? phaseEntryGateHookOutput(payload);
17050
17338
  if (decision)
17051
17339
  process.stdout.write(`${decision}
17052
17340
  `);
17053
17341
  return;
17054
17342
  }
17055
17343
  if (payload?.hook_event_name === "UserPromptSubmit") {
17344
+ recordPhaseEntryPrompt(payload);
17056
17345
  const notice = await activeTaskHookOutput(payload);
17057
17346
  if (notice)
17058
17347
  process.stdout.write(`${notice}