immune-brain 5.1.2 → 5.2.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 +1 -1
  10. package/plugins/immune-brain/dist/claude/mcp-server.mjs +425 -140
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +2 -2
  12. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -1
  13. package/plugins/immune-brain/dist/imm-planner.md +26 -33
  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 +143 -0
  22. package/plugins/immune-brain/runtime/claude/interaction.ts +5 -1
  23. package/plugins/immune-brain/runtime/claude/lane_guard.ts +180 -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 +1 -1
  30. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +2 -1
  31. package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -5
  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,122 @@ 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.2.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 = { prompt_seq: 0, run_live: false, run_prompt_seq: 0, plan_prompt_seq: 0 };
99
+ function applyPrompt(state, entry) {
100
+ const prompt_seq = state.prompt_seq + 1;
101
+ if (entry === "run")
102
+ return { ...state, prompt_seq, run_live: true, run_prompt_seq: prompt_seq };
103
+ if (entry === "plan")
104
+ return { prompt_seq, run_live: false, run_prompt_seq: 0, plan_prompt_seq: prompt_seq };
105
+ return { ...state, prompt_seq };
106
+ }
107
+ function phaseEntryLogLine(entry) {
108
+ return `${JSON.stringify({ v: 1, entry })}
109
+ `;
110
+ }
111
+ function reducePhaseEntryLog(text) {
112
+ let state = NO_PHASE_ENTRY;
113
+ for (const line of text.split(`
114
+ `)) {
115
+ if (!line)
116
+ continue;
117
+ let record;
118
+ try {
119
+ record = JSON.parse(line);
120
+ } catch {
121
+ return null;
122
+ }
123
+ if (!record || typeof record !== "object")
124
+ return null;
125
+ const { v, entry } = record;
126
+ if (v !== 1 || entry !== null && entry !== "plan" && entry !== "run")
127
+ return null;
128
+ state = applyPrompt(state, entry);
129
+ }
130
+ return state;
131
+ }
132
+ function refuse(reason_code, detail) {
133
+ return { ok: false, reason_code, detail, recovery_action: PHASE_ENTRY_RECOVERY[reason_code] };
134
+ }
135
+ function admitPhaseOperation(operation, state, context) {
136
+ if (operation.kind === "tool") {
137
+ if (state?.run_live)
138
+ return { ok: true };
139
+ return refuse(RUN_ENTRY_REQUIRED, `${operation.name} needs a literal-user imm-run entry in this session`);
140
+ }
141
+ const latest = state !== null && state.prompt_seq > 0 ? state.prompt_seq : -1;
142
+ if (operation.skill === "imm-run") {
143
+ if (state?.run_live && state.run_prompt_seq === latest)
144
+ return { ok: true };
145
+ return refuse(PHASE_ENTRY_REQUIRED, "imm-run is entered by the user, not by a model Skill call");
146
+ }
147
+ if (state && state.plan_prompt_seq === latest)
148
+ return { ok: true };
149
+ if (operation.skill === "imm-planner" && state?.run_live && context.activeClaim)
150
+ return { ok: true };
151
+ return refuse(PHASE_ENTRY_REQUIRED, `${operation.skill} is entered by the user, not by a model Skill call`);
152
+ }
153
+ function phaseRefusalMessage(refusal) {
154
+ return `${refusal.reason_code}: ${refusal.detail}; recovery: ${refusal.recovery_action}`;
155
+ }
156
+
157
+ // plugins/immune-brain/runtime/claude/interaction.ts
46
158
  var PRIVILEGED_OPERATIONS = [
47
159
  "enroll",
48
160
  "request_authorization",
@@ -57,7 +169,8 @@ var RECOVERY_ACTIONS = {
57
169
  user_cancelled: "wait for a fresh literal-user request",
58
170
  correlation_missing: "retry through a fresh native gate in the current Host",
59
171
  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"
172
+ workspace_changed: "review the current workspace and retry through a fresh native gate",
173
+ ...PHASE_ENTRY_RECOVERY
61
174
  };
62
175
 
63
176
  class NativeAuthorityError extends Error {
@@ -684,6 +797,11 @@ function parseHookStdin(raw) {
684
797
  return null;
685
798
  }
686
799
 
800
+ // plugins/immune-brain/runtime/claude/lane_guard.ts
801
+ import { lstatSync as lstatSync10, rmSync as rmSync5 } from "node:fs";
802
+ import { homedir, tmpdir as tmpdir2 } from "node:os";
803
+ import { join as join9 } from "node:path";
804
+
687
805
  // plugins/immune-brain/runtime/unattended/lane_workspace.ts
688
806
  import { spawnSync } from "node:child_process";
689
807
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -6350,7 +6468,7 @@ var oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
6350
6468
  var id = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6351
6469
  var batchAuthor = process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch";
6352
6470
  var decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6353
- function refuse() {
6471
+ function refuse2() {
6354
6472
  throw new Error("plan_digest mismatch: batch plan reconfirmation is not eligible");
6355
6473
  }
6356
6474
  function matchesLocalProof(localJson, exported) {
@@ -6359,7 +6477,7 @@ function matchesLocalProof(localJson, exported) {
6359
6477
  function git2(root, args) {
6360
6478
  const r = spawnSync6("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
6361
6479
  if (r.status !== 0)
6362
- refuse();
6480
+ refuse2();
6363
6481
  return decode(r.stdout);
6364
6482
  }
6365
6483
  function ownUnpersistedBatchHead(root, record, head) {
@@ -6410,23 +6528,23 @@ function ownUnpersistedBatchHead(root, record, head) {
6410
6528
  var confirmations = new Map;
6411
6529
  function retainReconfirmation(nonce, snapshot) {
6412
6530
  if (confirmations.has(nonce))
6413
- refuse();
6531
+ refuse2();
6414
6532
  confirmations.set(nonce, snapshot);
6415
6533
  }
6416
6534
  function takeReconfirmation(nonce) {
6417
6535
  const found = confirmations.get(nonce);
6418
6536
  confirmations.delete(nonce);
6419
6537
  if (!found)
6420
- refuse();
6538
+ refuse2();
6421
6539
  return found;
6422
6540
  }
6423
6541
  async function captureBatchReconfirmation(root, record, children) {
6424
6542
  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();
6543
+ refuse2();
6426
6544
  const assertNoCommitEvidence = () => {
6427
6545
  for (const child of record.children) {
6428
6546
  if (!id.test(child.task_id))
6429
- refuse();
6547
+ refuse2();
6430
6548
  const path = batchCommitEvidencePath(record.batch_id, child.task_id);
6431
6549
  let missing = false;
6432
6550
  try {
@@ -6435,17 +6553,17 @@ async function captureBatchReconfirmation(root, record, children) {
6435
6553
  if (error instanceof Error && error.message === `source_missing: ${path}`)
6436
6554
  missing = true;
6437
6555
  else
6438
- refuse();
6556
+ refuse2();
6439
6557
  }
6440
6558
  if (!missing)
6441
- refuse();
6559
+ refuse2();
6442
6560
  }
6443
6561
  };
6444
6562
  assertNoCommitEvidence();
6445
6563
  const statePath = batchStatePath(record.batch_id);
6446
6564
  const stateBytes = readSecureProjectBytes(root, statePath);
6447
6565
  if (JSON.stringify(JSON.parse(decode(stateBytes))) !== JSON.stringify(record))
6448
- refuse();
6566
+ refuse2();
6449
6567
  const files = new Map;
6450
6568
  const capture = (path) => {
6451
6569
  const bytes = readSecureProjectBytes(root, path);
@@ -6457,14 +6575,14 @@ async function captureBatchReconfirmation(root, record, children) {
6457
6575
  let scope = null;
6458
6576
  const workspace = readWorkspaceStateRaw(root);
6459
6577
  if (workspace.state.current_working !== null)
6460
- refuse();
6578
+ refuse2();
6461
6579
  for (const [index, child] of children.entries()) {
6462
6580
  const previous = record.children[index];
6463
6581
  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();
6582
+ refuse2();
6465
6583
  const old = parseTaskIntentV1(JSON.parse(git2(root, ["show", `${record.base_head}:${child.intent_path}`])));
6466
6584
  if (old.task_id !== child.task_id)
6467
- refuse();
6585
+ refuse2();
6468
6586
  const oldHash = canonicalIntentHash(old);
6469
6587
  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
6588
  const currentBytes = capture(child.intent_path);
@@ -6472,46 +6590,46 @@ async function captureBatchReconfirmation(root, record, children) {
6472
6590
  const currentHash = canonicalIntentHash(current);
6473
6591
  const tracked = readTaskIntent(root, child.task_id, child.intent_path);
6474
6592
  if (currentHash !== tracked.content_hash || currentHash !== child.intent_content_hash || current.revision !== child.intent_revision)
6475
- refuse();
6593
+ refuse2();
6476
6594
  if (currentHash === oldHash)
6477
6595
  continue;
6478
6596
  if (scope !== null || previous.state === "pending" || previous.state === "skipped_blocked" || current.revision <= old.revision || current.goal !== old.goal || current.owner !== old.owner)
6479
- refuse();
6597
+ refuse2();
6480
6598
  const settled = readSettledRunEvidence(root, child.task_id);
6481
6599
  if (!settled || settled.lifecycle !== "done")
6482
- refuse();
6600
+ refuse2();
6483
6601
  const run = settled.run_id;
6484
6602
  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();
6603
+ refuse2();
6486
6604
  const pair = readAuditTaskPair(root, child.task_id, run);
6487
6605
  if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4")
6488
- refuse();
6606
+ refuse2();
6489
6607
  const r = pair.record;
6490
6608
  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();
6609
+ refuse2();
6492
6610
  const terminal = r.history.at(-1);
6493
6611
  if (terminal?.type !== "complete" || terminal.from_state !== "active:frozen" || terminal.to_state !== "done:frozen" || terminal.id !== pair.proof.terminal_event_id)
6494
- refuse();
6612
+ refuse2();
6495
6613
  const recordPath = auditRunRecordPath(child.task_id, run);
6496
6614
  const proofPath = auditRunTerminalProofPath(child.task_id, run);
6497
6615
  if (!capture(recordPath).equals(Buffer.from(settled.record_json)))
6498
- refuse();
6616
+ refuse2();
6499
6617
  const capturedProof = JSON.parse(decode(capture(proofPath)));
6500
6618
  if (!matchesLocalProof(settled.proof_json, capturedProof) || !matchesLocalProof(settled.proof_json, pair.proof))
6501
- refuse();
6619
+ refuse2();
6502
6620
  const identity = taskDeliveryIdentity(root, r);
6503
6621
  const projection = await projectAssurance(root, child.task_id, (cwd, task) => {
6504
6622
  if (task.contract !== "assurance_kernel/task_record/v4" || !task.git_base_head)
6505
- refuse();
6623
+ refuse2();
6506
6624
  return taskDeliveryIdentity(cwd, task);
6507
6625
  });
6508
6626
  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();
6627
+ refuse2();
6510
6628
  scope = r.intent_snapshot.scope_hint;
6511
6629
  changed.push({ task: child.task_id, run, identity: JSON.stringify(identity), settled: JSON.stringify(settled) });
6512
6630
  }
6513
6631
  if (computeBatchPlanDigest(oldChildren) !== record.plan_digest || changed.length !== 1 || scope === null)
6514
- refuse();
6632
+ refuse2();
6515
6633
  const confirmedScope = scope;
6516
6634
  const owner = changed[0];
6517
6635
  const status = () => git2(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
@@ -6520,42 +6638,42 @@ async function captureBatchReconfirmation(root, record, children) {
6520
6638
  for (const entry of entries) {
6521
6639
  const code = entry.slice(0, 2), path = entry.slice(3);
6522
6640
  if (code[1] !== " " || !["A", "M", "D"].includes(code[0]) || !path.startsWith(`${auditRunDirPath(owner.task, owner.run)}/`) && !confirmedScope.some((s) => pathMatchesScope(path, s)))
6523
- refuse();
6641
+ refuse2();
6524
6642
  }
6525
6643
  const flagsBytes = git2(root, ["ls-files", "-v"]);
6526
6644
  const flags = flagsBytes.split(`
6527
6645
  `).filter(Boolean);
6528
6646
  if (flags.some((line) => line[0] === "S" || line[0] !== line[0]?.toUpperCase()))
6529
- refuse();
6647
+ refuse2();
6530
6648
  const head = () => git2(root, ["rev-parse", "HEAD"]).trim();
6531
6649
  const branch = () => git2(root, ["symbolic-ref", "--short", "HEAD"]).trim();
6532
6650
  if (head() !== record.base_head || branch() !== record.branch)
6533
- refuse();
6651
+ refuse2();
6534
6652
  const assertDeliveryUnchanged = () => {
6535
6653
  for (const c of changed) {
6536
6654
  let settled;
6537
6655
  try {
6538
6656
  settled = readSettledRunEvidence(root, c.task);
6539
6657
  } catch {
6540
- refuse();
6658
+ refuse2();
6541
6659
  }
6542
6660
  if (!settled || settled.run_id !== c.run || JSON.stringify(settled) !== c.settled)
6543
- refuse();
6661
+ refuse2();
6544
6662
  const pair = readAuditTaskPair(root, c.task, c.run);
6545
6663
  if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || !pair.record.git_base_head)
6546
- refuse();
6664
+ refuse2();
6547
6665
  const diff = taskDeliveryIdentity(root, pair.record);
6548
6666
  if (JSON.stringify(diff) !== c.identity)
6549
- refuse();
6667
+ refuse2();
6550
6668
  }
6551
6669
  };
6552
6670
  const assertUnchanged = () => {
6553
6671
  assertNoCommitEvidence();
6554
6672
  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();
6673
+ refuse2();
6556
6674
  for (const [path, bytes] of files)
6557
6675
  if (!readSecureProjectBytes(root, path).equals(bytes))
6558
- refuse();
6676
+ refuse2();
6559
6677
  assertDeliveryUnchanged();
6560
6678
  };
6561
6679
  assertUnchanged();
@@ -9012,8 +9130,38 @@ async function activeTaskNotice(root) {
9012
9130
  ].join(" ");
9013
9131
  }
9014
9132
 
9133
+ // plugins/immune-brain/runtime/assurance/alias_file.ts
9134
+ import { closeSync as closeSync7, constants as constants5, fstatSync as fstatSync5, openSync as openSync7, readSync as readSync2 } from "node:fs";
9135
+ var MAX_ALIAS_BYTES = 64 * 1024;
9136
+ function readAliasFile(path, maxBytes = MAX_ALIAS_BYTES) {
9137
+ let fd;
9138
+ try {
9139
+ fd = openSync7(path, constants5.O_RDONLY | constants5.O_NONBLOCK);
9140
+ } catch {
9141
+ return null;
9142
+ }
9143
+ try {
9144
+ const stat = fstatSync5(fd);
9145
+ if (!stat.isFile() || stat.size > maxBytes)
9146
+ return null;
9147
+ const buf = Buffer.alloc(maxBytes + 1);
9148
+ const read = readSync2(fd, buf, 0, buf.length, 0);
9149
+ return read > maxBytes ? null : buf.toString("utf8", 0, read);
9150
+ } catch {
9151
+ return null;
9152
+ } finally {
9153
+ closeSync7(fd);
9154
+ }
9155
+ }
9156
+
9015
9157
  // plugins/immune-brain/runtime/claude/lane_guard.ts
9016
9158
  var FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
9159
+ var GATED_MCP_TOOL = /^mcp__.*immune-brain.*__(enroll|start_unattended_batch)$/;
9160
+ function denyPreToolUse(reason) {
9161
+ return JSON.stringify({
9162
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason }
9163
+ });
9164
+ }
9017
9165
  function laneGuardHookOutput(payload) {
9018
9166
  if (payload.hook_event_name !== "PreToolUse" || !FILE_TOOLS.has(String(payload.tool_name)))
9019
9167
  return null;
@@ -9022,9 +9170,7 @@ function laneGuardHookOutput(payload) {
9022
9170
  const refusal = laneWriteRefusal(cwd, input.file_path ?? input.notebook_path);
9023
9171
  if (!refusal)
9024
9172
  return null;
9025
- return JSON.stringify({
9026
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: refusal }
9027
- });
9173
+ return denyPreToolUse(refusal);
9028
9174
  }
9029
9175
  async function activeTaskHookOutput(payload) {
9030
9176
  if (payload.hook_event_name !== "UserPromptSubmit")
@@ -9035,12 +9181,141 @@ async function activeTaskHookOutput(payload) {
9035
9181
  return null;
9036
9182
  return JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: notice } });
9037
9183
  }
9184
+ function entryLogPath(sessionId, root) {
9185
+ return join9(cacheDir(root), `${sessionHash(sessionId)}.entry`);
9186
+ }
9187
+ function admissionPath(toolUseId, root) {
9188
+ return join9(cacheDir(root), `${sessionHash(toolUseId)}.admit`);
9189
+ }
9190
+ function sessionIdOf(payload) {
9191
+ return typeof payload.session_id === "string" ? payload.session_id : "";
9192
+ }
9193
+ function cwdOf(payload) {
9194
+ return typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
9195
+ }
9196
+ function claudeAliasReader(cwd, home) {
9197
+ return (name) => {
9198
+ for (const dir of [join9(cwd, ".claude", "commands"), join9(home, ".claude", "commands")]) {
9199
+ const body = readAliasFile(join9(dir, `${name}.md`));
9200
+ if (body !== null)
9201
+ return body;
9202
+ }
9203
+ return null;
9204
+ };
9205
+ }
9206
+ function readPhaseEntryState(sessionId, root = tmpdir2(), read = readPrivate) {
9207
+ if (!sessionId)
9208
+ return null;
9209
+ const path = entryLogPath(sessionId, root);
9210
+ try {
9211
+ lstatSync10(path);
9212
+ } catch (error) {
9213
+ return error.code === "ENOENT" ? NO_PHASE_ENTRY : null;
9214
+ }
9215
+ if (!ensurePrivateDir(cacheDir(root)))
9216
+ return null;
9217
+ let text;
9218
+ try {
9219
+ text = read(path);
9220
+ } catch {
9221
+ return null;
9222
+ }
9223
+ return text === undefined ? null : reducePhaseEntryLog(text);
9224
+ }
9225
+ function dropPrivate(path) {
9226
+ try {
9227
+ const stat = lstatSync10(path);
9228
+ if (ownedByUs(stat) && stat.isFile())
9229
+ rmSync5(path, { force: true });
9230
+ } catch {}
9231
+ }
9232
+ function recordPhaseEntryPrompt(payload, locations = {}) {
9233
+ if (payload.hook_event_name !== "UserPromptSubmit")
9234
+ return false;
9235
+ const sessionId = sessionIdOf(payload);
9236
+ if (!sessionId)
9237
+ return false;
9238
+ const root = locations.root ?? tmpdir2();
9239
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
9240
+ const entry = recognizePhaseEntry(prompt, claudeAliasReader(cwdOf(payload), locations.home ?? homedir()));
9241
+ const path = entryLogPath(sessionId, root);
9242
+ const recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry));
9243
+ if (!recorded)
9244
+ dropPrivate(path);
9245
+ return recorded;
9246
+ }
9247
+ function gatedOperation(payload) {
9248
+ const toolName = String(payload.tool_name ?? "");
9249
+ if (toolName === "Skill") {
9250
+ const input = payload.tool_input ?? {};
9251
+ const skill = typeof input.skill === "string" ? packagedPhaseSkill(input.skill) : null;
9252
+ return skill ? { kind: "skill", skill } : null;
9253
+ }
9254
+ const mcp = GATED_MCP_TOOL.exec(toolName);
9255
+ return mcp ? { kind: "tool", name: mcp[1] } : null;
9256
+ }
9257
+ function phaseEntryGateHookOutput(payload, locations = {}) {
9258
+ if (payload.hook_event_name !== "PreToolUse")
9259
+ return null;
9260
+ const operation = gatedOperation(payload);
9261
+ if (!operation)
9262
+ return null;
9263
+ const root = locations.root ?? tmpdir2();
9264
+ let state = null;
9265
+ try {
9266
+ state = readPhaseEntryState(sessionIdOf(payload), root, locations.read);
9267
+ } catch {}
9268
+ let activeClaim = false;
9269
+ try {
9270
+ activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null;
9271
+ } catch {}
9272
+ const admission = admitPhaseOperation(operation, state, { activeClaim });
9273
+ if (!admission.ok)
9274
+ return denyPreToolUse(phaseRefusalMessage(admission));
9275
+ if (operation.kind === "skill")
9276
+ return null;
9277
+ const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : "";
9278
+ let recorded = false;
9279
+ try {
9280
+ recorded = toolUseId !== "" && (locations.append ?? appendPrivate)(admissionPath(toolUseId, root), cacheDir(root), `${JSON.stringify({ v: 1, operation: operation.name })}
9281
+ `);
9282
+ } catch {}
9283
+ if (recorded)
9284
+ return null;
9285
+ return denyPreToolUse(`${RUN_ENTRY_REQUIRED}: the admission for this ${operation.name} call could not be recorded; recovery: ${PHASE_ENTRY_RECOVERY[RUN_ENTRY_REQUIRED]}`);
9286
+ }
9287
+ function consumePhaseAdmission(toolUseId, operation, root = tmpdir2()) {
9288
+ if (!toolUseId || !ensurePrivateDir(cacheDir(root)))
9289
+ return false;
9290
+ const path = admissionPath(toolUseId, root);
9291
+ let text;
9292
+ try {
9293
+ text = readPrivate(path);
9294
+ } catch {
9295
+ return false;
9296
+ }
9297
+ if (text === undefined)
9298
+ return false;
9299
+ dropPrivate(path);
9300
+ const lines = text.split(`
9301
+ `).filter(Boolean);
9302
+ if (lines.length === 0)
9303
+ return false;
9304
+ return lines.every((line) => {
9305
+ try {
9306
+ const record = JSON.parse(line);
9307
+ return record?.v === 1 && record.operation === operation;
9308
+ } catch {
9309
+ return false;
9310
+ }
9311
+ });
9312
+ }
9038
9313
 
9039
9314
  // plugins/immune-brain/runtime/assurance/verdict_authority.ts
9040
9315
  import { randomUUID as randomUUID7 } from "node:crypto";
9041
9316
  import { execFileSync as execFileSync4 } from "node:child_process";
9042
9317
  import { existsSync as existsSync9 } from "node:fs";
9043
- import { join as join12, resolve as resolve12 } from "node:path";
9318
+ import { join as join13, resolve as resolve12 } from "node:path";
9044
9319
 
9045
9320
  // plugins/immune-brain/runtime/kernel/application.ts
9046
9321
  function applyTaskAction(input) {
@@ -9698,15 +9973,15 @@ import { execFileSync as execFileSync2 } from "node:child_process";
9698
9973
  import { createHash as createHash14 } from "node:crypto";
9699
9974
  import {
9700
9975
  mkdtempSync,
9701
- rmSync as rmSync5,
9976
+ rmSync as rmSync6,
9702
9977
  writeFileSync as writeFileSync6,
9703
9978
  statSync as statSync3,
9704
9979
  readFileSync as readFileSync10,
9705
9980
  realpathSync as realpathSync8,
9706
9981
  chmodSync
9707
9982
  } from "node:fs";
9708
- import { tmpdir as tmpdir2 } from "node:os";
9709
- import { join as join9 } from "node:path";
9983
+ import { tmpdir as tmpdir3 } from "node:os";
9984
+ import { join as join10 } from "node:path";
9710
9985
  var MAX_REVIEW_BUNDLE_BYTES = 2 * 1024 * 1024;
9711
9986
  function bundleDigest(bundle) {
9712
9987
  return `sha256:${createHash14("sha256").update(JSON.stringify(bundle)).digest("hex")}`;
@@ -9930,9 +10205,9 @@ function publishReviewRevision(root, snapshot, diffHash, taskId) {
9930
10205
  throw new Error("review revision base has invalid identity");
9931
10206
  if (!REVISION_DIFF_HASH.test(diffHash))
9932
10207
  throw new Error("review revision diff hash has invalid identity");
9933
- const indexDirectory = mkdtempSync(join9(tmpdir2(), "imm-review-index-"));
10208
+ const indexDirectory = mkdtempSync(join10(tmpdir3(), "imm-review-index-"));
9934
10209
  try {
9935
- const indexFile = join9(indexDirectory, "index");
10210
+ const indexFile = join10(indexDirectory, "index");
9936
10211
  const env = { GIT_INDEX_FILE: indexFile };
9937
10212
  gitEvidence(root, ["read-tree", snapshot.base_tree], env);
9938
10213
  for (const [path, entry] of Object.entries(snapshot.changed_paths)) {
@@ -9982,7 +10257,7 @@ function publishReviewRevision(root, snapshot, diffHash, taskId) {
9982
10257
  diff_hash: diffHash
9983
10258
  };
9984
10259
  } finally {
9985
- rmSync5(indexDirectory, { recursive: true, force: true });
10260
+ rmSync6(indexDirectory, { recursive: true, force: true });
9986
10261
  }
9987
10262
  }
9988
10263
  function publishInput(root, input) {
@@ -10025,19 +10300,19 @@ function captureReviewManifest(root, input) {
10025
10300
  return manifest;
10026
10301
  }
10027
10302
  function writeNativeReviewEvidence(payload) {
10028
- const rawDirectory = mkdtempSync(join9(tmpdir2(), "imm-canary-native-review-"));
10303
+ const rawDirectory = mkdtempSync(join10(tmpdir3(), "imm-canary-native-review-"));
10029
10304
  try {
10030
10305
  const directory = realpathSync8(rawDirectory);
10031
10306
  chmodSync(directory, 493);
10032
- const path = join9(directory, "evidence.json");
10307
+ const path = join10(directory, "evidence.json");
10033
10308
  writeFileSync6(path, JSON.stringify(payload), { encoding: "utf8", mode: 420, flag: "wx" });
10034
10309
  assertReviewArtifact(path);
10035
10310
  return {
10036
10311
  path,
10037
- remove: () => rmSync5(directory, { recursive: true, force: true })
10312
+ remove: () => rmSync6(directory, { recursive: true, force: true })
10038
10313
  };
10039
10314
  } catch (error) {
10040
- rmSync5(rawDirectory, { recursive: true, force: true });
10315
+ rmSync6(rawDirectory, { recursive: true, force: true });
10041
10316
  throw error;
10042
10317
  }
10043
10318
  }
@@ -10062,8 +10337,8 @@ import { createHash as createHash17, randomUUID as randomUUID6 } from "node:cryp
10062
10337
  // plugins/immune-brain/runtime/assurance/verification.ts
10063
10338
  import { createHash as createHash15, randomBytes } from "node:crypto";
10064
10339
  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";
10340
+ import { accessSync, constants as constants6, readFileSync as readFileSync11, readdirSync as readdirSync5, realpathSync as realpathSync9, statSync as statSync4 } from "node:fs";
10341
+ import { delimiter, isAbsolute as isAbsolute5, join as join11, relative as relative5, resolve as resolve11, sep as sep6 } from "node:path";
10067
10342
  class VerificationAbortedError extends Error {
10068
10343
  constructor() {
10069
10344
  super("fixed verification aborted");
@@ -10099,9 +10374,9 @@ function verificationPath() {
10099
10374
  }
10100
10375
  function toolPath(name, path) {
10101
10376
  for (const dir of path.split(delimiter).filter(isAbsolute5)) {
10102
- const candidate = join10(dir, name);
10377
+ const candidate = join11(dir, name);
10103
10378
  try {
10104
- accessSync(candidate, constants5.X_OK);
10379
+ accessSync(candidate, constants6.X_OK);
10105
10380
  if (statSync4(candidate).isFile())
10106
10381
  return candidate;
10107
10382
  } catch {}
@@ -10111,7 +10386,7 @@ function toolPath(name, path) {
10111
10386
  function identity(path) {
10112
10387
  try {
10113
10388
  const real = realpathSync9(path);
10114
- accessSync(real, constants5.X_OK);
10389
+ accessSync(real, constants6.X_OK);
10115
10390
  const stat = statSync4(real);
10116
10391
  if (!stat.isFile())
10117
10392
  throw new Error("not a file");
@@ -10208,7 +10483,7 @@ async function runFixedVerification(root, command, frozen, options) {
10208
10483
  };
10209
10484
  const procEnviron = (pid) => {
10210
10485
  try {
10211
- return readFileSync11(join10(procRoot, String(pid), "environ"), "utf8");
10486
+ return readFileSync11(join11(procRoot, String(pid), "environ"), "utf8");
10212
10487
  } catch {
10213
10488
  return null;
10214
10489
  }
@@ -10216,7 +10491,7 @@ async function runFixedVerification(root, command, frozen, options) {
10216
10491
  const procSession = (entry) => {
10217
10492
  let stat;
10218
10493
  try {
10219
- stat = readFileSync11(join10(procRoot, entry, "stat"), "utf8");
10494
+ stat = readFileSync11(join11(procRoot, entry, "stat"), "utf8");
10220
10495
  } catch (error) {
10221
10496
  if (error.code === "ENOENT")
10222
10497
  return null;
@@ -10427,7 +10702,7 @@ function createInvocationRegistry() {
10427
10702
  // plugins/immune-brain/runtime/role_prompt_bridge.ts
10428
10703
  import { createHash as createHash16 } from "node:crypto";
10429
10704
  import { existsSync as existsSync8, readFileSync as readFileSync12 } from "node:fs";
10430
- import { join as join11, dirname as dirname8 } from "node:path";
10705
+ import { join as join12, dirname as dirname8 } from "node:path";
10431
10706
  import { fileURLToPath } from "node:url";
10432
10707
  var RUNTIME_DIR = dirname8(fileURLToPath(import.meta.url));
10433
10708
  var INTERNAL_ROLE_PROMPTS = {
@@ -10489,14 +10764,14 @@ function roleSpec(role) {
10489
10764
  }
10490
10765
  function rolePromptSearchDirs(moduleDir) {
10491
10766
  return [
10492
- join11(moduleDir, "..", "dist", "role-prompts"),
10493
- join11(moduleDir, "..", "role-prompts")
10767
+ join12(moduleDir, "..", "dist", "role-prompts"),
10768
+ join12(moduleDir, "..", "role-prompts")
10494
10769
  ];
10495
10770
  }
10496
10771
  function loadRolePrompt(role) {
10497
10772
  const spec = roleSpec(role);
10498
10773
  for (const dir of rolePromptSearchDirs(RUNTIME_DIR)) {
10499
- const path = join11(dir, spec.file);
10774
+ const path = join12(dir, spec.file);
10500
10775
  if (existsSync8(path))
10501
10776
  return readFileSync12(path, "utf8");
10502
10777
  }
@@ -11769,7 +12044,7 @@ function stagePlanningArtifactTransition(root, record) {
11769
12044
  intentArchive,
11770
12045
  ...specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []
11771
12046
  ];
11772
- const paths = candidates.filter((path) => existsSync9(join12(root, path)) || execFileSync4("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
12047
+ const paths = candidates.filter((path) => existsSync9(join13(root, path)) || execFileSync4("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
11773
12048
  if (paths.length === 0)
11774
12049
  return;
11775
12050
  execFileSync4("git", ["add", "--", ...paths], {
@@ -11865,7 +12140,7 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
11865
12140
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
11866
12141
  import { existsSync as existsSync14, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
11867
12142
  import { execFileSync as execFileSync9 } from "node:child_process";
11868
- import { join as join21 } from "node:path";
12143
+ import { join as join22 } from "node:path";
11869
12144
 
11870
12145
  // plugins/immune-brain/runtime/kernel/enrollment_authority.ts
11871
12146
  var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
@@ -12239,9 +12514,9 @@ import { createHash as createHash19 } from "node:crypto";
12239
12514
  // plugins/immune-brain/runtime/assurance/delivery_workspace.ts
12240
12515
  import { execFileSync as execFileSync6 } from "node:child_process";
12241
12516
  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";
12517
+ 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";
12518
+ import { tmpdir as tmpdir4 } from "node:os";
12519
+ import { dirname as dirname9, isAbsolute as isAbsolute6, join as join14, parse, relative as relative6, resolve as resolve13, sep as sep7 } from "node:path";
12245
12520
 
12246
12521
  class DeliveryWorkspaceError extends Error {
12247
12522
  constructor(message) {
@@ -12253,7 +12528,7 @@ function removeTaskOwnedTree(root) {
12253
12528
  const makeDirectoriesWritable = (path) => {
12254
12529
  let stat;
12255
12530
  try {
12256
- stat = lstatSync10(path);
12531
+ stat = lstatSync11(path);
12257
12532
  } catch (error) {
12258
12533
  if (error.code === "ENOENT")
12259
12534
  return;
@@ -12263,10 +12538,10 @@ function removeTaskOwnedTree(root) {
12263
12538
  return;
12264
12539
  chmodSync2(path, stat.mode & 4095 | 448);
12265
12540
  for (const name of readdirSync6(path))
12266
- makeDirectoriesWritable(join13(path, name));
12541
+ makeDirectoriesWritable(join14(path, name));
12267
12542
  };
12268
12543
  makeDirectoriesWritable(root);
12269
- rmSync6(root, { recursive: true, force: true });
12544
+ rmSync7(root, { recursive: true, force: true });
12270
12545
  }
12271
12546
  var GIT_OBJECT_ID5 = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
12272
12547
  function isolatedGitEnv(extra = {}) {
@@ -12314,9 +12589,9 @@ function resolvedSymlinkTarget(path) {
12314
12589
  current = dirname9(current);
12315
12590
  continue;
12316
12591
  }
12317
- const next = join13(current, part);
12592
+ const next = join14(current, part);
12318
12593
  try {
12319
- if (!lstatSync10(next).isSymbolicLink()) {
12594
+ if (!lstatSync11(next).isSymbolicLink()) {
12320
12595
  current = next;
12321
12596
  continue;
12322
12597
  }
@@ -12337,8 +12612,8 @@ function resolvedSymlinkTarget(path) {
12337
12612
  function assertNoEscapingSymlinks(root, dir = root) {
12338
12613
  const realRoot = realpathSync10(root);
12339
12614
  for (const name of readdirSync6(dir)) {
12340
- const path = join13(dir, name);
12341
- const stat = lstatSync10(path);
12615
+ const path = join14(dir, name);
12616
+ const stat = lstatSync11(path);
12342
12617
  if (stat.isSymbolicLink()) {
12343
12618
  assertInside(realRoot, resolvedSymlinkTarget(path), `symlink ${relative6(root, path)}`);
12344
12619
  } else if (stat.isDirectory()) {
@@ -12347,41 +12622,41 @@ function assertNoEscapingSymlinks(root, dir = root) {
12347
12622
  }
12348
12623
  }
12349
12624
  function writeDeliveryTree(sourceRoot, snapshot) {
12350
- const indexDirectory = mkdtempSync2(join13(tmpdir3(), "imm-delivery-index-"));
12625
+ const indexDirectory = mkdtempSync2(join14(tmpdir4(), "imm-delivery-index-"));
12351
12626
  try {
12352
12627
  git5(sourceRoot, ["read-tree", snapshot.base_tree], {
12353
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12354
- GIT_DIR: join13(sourceRoot, ".git")
12628
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12629
+ GIT_DIR: join14(sourceRoot, ".git")
12355
12630
  });
12356
12631
  for (const [path, entry] of Object.entries(snapshot.changed_paths)) {
12357
12632
  if (entry.oid && entry.mode) {
12358
12633
  git5(sourceRoot, ["update-index", "--add", "--cacheinfo", `${entry.mode},${entry.oid},${path}`], {
12359
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12360
- GIT_DIR: join13(sourceRoot, ".git")
12634
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12635
+ GIT_DIR: join14(sourceRoot, ".git")
12361
12636
  });
12362
12637
  } else {
12363
12638
  git5(sourceRoot, ["update-index", "--force-remove", "--", path], {
12364
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12365
- GIT_DIR: join13(sourceRoot, ".git")
12639
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12640
+ GIT_DIR: join14(sourceRoot, ".git")
12366
12641
  });
12367
12642
  }
12368
12643
  }
12369
12644
  const next = git5(sourceRoot, ["write-tree"], {
12370
- GIT_INDEX_FILE: join13(indexDirectory, "index"),
12371
- GIT_DIR: join13(sourceRoot, ".git")
12645
+ GIT_INDEX_FILE: join14(indexDirectory, "index"),
12646
+ GIT_DIR: join14(sourceRoot, ".git")
12372
12647
  });
12373
12648
  assertTree(next);
12374
12649
  return next;
12375
12650
  } finally {
12376
- rmSync6(indexDirectory, { recursive: true, force: true });
12651
+ rmSync7(indexDirectory, { recursive: true, force: true });
12377
12652
  }
12378
12653
  }
12379
12654
  function workspaceFiles(root, dir = root, result = Object.create(null)) {
12380
12655
  if (dir === root)
12381
- result["."] = `${lstatSync10(root).mode & 4095}:directory`;
12656
+ result["."] = `${lstatSync11(root).mode & 4095}:directory`;
12382
12657
  for (const name of readdirSync6(dir).sort()) {
12383
- const path = join13(dir, name), rel = relative6(root, path);
12384
- const stat = lstatSync10(path);
12658
+ const path = join14(dir, name), rel = relative6(root, path);
12659
+ const stat = lstatSync11(path);
12385
12660
  if (stat.isSymbolicLink())
12386
12661
  result[rel] = `link:${stat.mode & 4095}:${readlinkSync2(path)}`;
12387
12662
  else if (stat.isDirectory()) {
@@ -12408,7 +12683,7 @@ function assertDeliveryClean(root, tree, seal, writablePaths = []) {
12408
12683
  }
12409
12684
  if (seal === "[]") {
12410
12685
  for (const name of readdirSync6(root)) {
12411
- const stat = lstatSync10(join13(root, name));
12686
+ const stat = lstatSync11(join14(root, name));
12412
12687
  if (!stat.isDirectory() || name !== ".git")
12413
12688
  throw new DeliveryWorkspaceError("delivery workspace was contaminated");
12414
12689
  }
@@ -12430,18 +12705,18 @@ function assertDeliveryClean(root, tree, seal, writablePaths = []) {
12430
12705
  if (!permitted)
12431
12706
  throw new DeliveryWorkspaceError("delivery undeclared output was contaminated");
12432
12707
  if (bytes.startsWith("link:")) {
12433
- assertInside(realpathSync10(root), resolvedSymlinkTarget(join13(root, path)), "generated symlink");
12708
+ assertInside(realpathSync10(root), resolvedSymlinkTarget(join14(root, path)), "generated symlink");
12434
12709
  }
12435
12710
  }
12436
12711
  }
12437
12712
  function materializeDeliveryWorkspace(sourceRoot, tree) {
12438
12713
  assertTree(tree);
12439
- const dest = mkdtempSync2(join13(tmpdir3(), "imm-delivery-"));
12714
+ const dest = mkdtempSync2(join14(tmpdir4(), "imm-delivery-"));
12440
12715
  const cleanup = () => removeTaskOwnedTree(dest);
12441
12716
  try {
12442
12717
  mkdirSync7(dest, { recursive: true });
12443
12718
  const commit = git5(sourceRoot, ["commit-tree", tree, "-m", `delivery ${tree}`], {
12444
- GIT_DIR: join13(sourceRoot, ".git"),
12719
+ GIT_DIR: join14(sourceRoot, ".git"),
12445
12720
  GIT_AUTHOR_NAME: "Immune-Brain Assurance",
12446
12721
  GIT_AUTHOR_EMAIL: "assurance@immune-brain.local",
12447
12722
  GIT_AUTHOR_DATE: "1970-01-01T00:00:00 +0000",
@@ -12469,8 +12744,8 @@ function materializeDeliveryWorkspace(sourceRoot, tree) {
12469
12744
 
12470
12745
  // plugins/immune-brain/runtime/assurance/qa.ts
12471
12746
  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";
12747
+ import { tmpdir as tmpdir5 } from "node:os";
12748
+ import { join as join15, sep as sep8 } from "node:path";
12474
12749
 
12475
12750
  // plugins/immune-brain/runtime/assurance/qa_findings.ts
12476
12751
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -12537,7 +12812,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
12537
12812
  let delivery, home;
12538
12813
  try {
12539
12814
  delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
12540
- home = mkdtempSync3(join14(tmpdir4(), "imm-qa-home-"));
12815
+ home = mkdtempSync3(join15(tmpdir5(), "imm-qa-home-"));
12541
12816
  return { delivery, home, deliveryPrefix: `${realpathSync11(delivery.root)}${sep8}` };
12542
12817
  } catch {
12543
12818
  let reason = "delivery_unavailable";
@@ -12705,11 +12980,11 @@ function deriveAuthorizationOperation(input) {
12705
12980
  // plugins/immune-brain/runtime/staged_intent.ts
12706
12981
  import { execFileSync as execFileSync7 } from "node:child_process";
12707
12982
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
12708
- import { join as join15 } from "node:path";
12983
+ import { join as join16 } from "node:path";
12709
12984
  function captureStagedIntent(root, relativePath) {
12710
12985
  return {
12711
12986
  path: relativePath,
12712
- bytes: readFileSync14(join15(root, relativePath)),
12987
+ bytes: readFileSync14(join16(root, relativePath)),
12713
12988
  index_state: execFileSync7("git", ["ls-files", "--stage", "-z", "--", relativePath], {
12714
12989
  cwd: root,
12715
12990
  stdio: ["ignore", "pipe", "pipe"]
@@ -12717,7 +12992,7 @@ function captureStagedIntent(root, relativePath) {
12717
12992
  };
12718
12993
  }
12719
12994
  function restoreStagedIntent(root, snapshot) {
12720
- writeFileSync7(join15(root, snapshot.path), snapshot.bytes);
12995
+ writeFileSync7(join16(root, snapshot.path), snapshot.bytes);
12721
12996
  execFileSync7("git", ["update-index", "--force-remove", "--", snapshot.path], {
12722
12997
  cwd: root,
12723
12998
  stdio: ["ignore", "pipe", "pipe"]
@@ -12729,7 +13004,7 @@ function restoreStagedIntent(root, snapshot) {
12729
13004
  stdio: ["pipe", "ignore", "pipe"]
12730
13005
  });
12731
13006
  }
12732
- const restoredBytes = readFileSync14(join15(root, snapshot.path));
13007
+ const restoredBytes = readFileSync14(join16(root, snapshot.path));
12733
13008
  if (!restoredBytes.equals(snapshot.bytes)) {
12734
13009
  throw new Error("failed to restore prior intent bytes");
12735
13010
  }
@@ -12745,24 +13020,24 @@ function restoreStagedIntent(root, snapshot) {
12745
13020
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
12746
13021
  import { spawnSync as spawnSync14 } from "node:child_process";
12747
13022
  import { existsSync as existsSync13 } from "node:fs";
12748
- import { join as join20 } from "node:path";
13023
+ import { join as join21 } from "node:path";
12749
13024
 
12750
13025
  // plugins/immune-brain/runtime/unattended/batch_git.ts
12751
13026
  import { spawnSync as spawnSync10 } from "node:child_process";
12752
13027
  import { createHash as createHash20, randomUUID as randomUUID9 } from "node:crypto";
12753
13028
  import {
12754
- constants as constants6,
12755
- closeSync as closeSync7,
13029
+ constants as constants7,
13030
+ closeSync as closeSync8,
12756
13031
  existsSync as existsSync10,
12757
- lstatSync as lstatSync11,
13032
+ lstatSync as lstatSync12,
12758
13033
  mkdirSync as mkdirSync8,
12759
- openSync as openSync7,
13034
+ openSync as openSync8,
12760
13035
  realpathSync as realpathSync12,
12761
13036
  renameSync as renameSync5,
12762
- rmSync as rmSync7,
13037
+ rmSync as rmSync8,
12763
13038
  writeFileSync as writeFileSync8
12764
13039
  } from "node:fs";
12765
- import { dirname as dirname10, join as join16 } from "node:path";
13040
+ import { dirname as dirname10, join as join17 } from "node:path";
12766
13041
  var DEFAULT_GIT_ENV = {
12767
13042
  ...process.env,
12768
13043
  GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch",
@@ -12974,15 +13249,15 @@ function getCommittedDeltaPaths(root) {
12974
13249
  return delta.stdout.split("\x00").filter((p) => p.length > 0);
12975
13250
  }
12976
13251
  function commitEvidencePath(batchId, taskId) {
12977
- return join16(".imm", "state", "batches", "commits", `${batchId}-${taskId}.json`);
13252
+ return join17(".imm", "state", "batches", "commits", `${batchId}-${taskId}.json`);
12978
13253
  }
12979
13254
  function ensureSecureDirectory3(root, relativePath) {
12980
13255
  const segments = relativePath.split("/").filter(Boolean);
12981
13256
  let current = root;
12982
13257
  for (const segment of segments) {
12983
- current = join16(current, segment);
13258
+ current = join17(current, segment);
12984
13259
  if (existsSync10(current)) {
12985
- const stats = lstatSync11(current);
13260
+ const stats = lstatSync12(current);
12986
13261
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
12987
13262
  throw new Error(`${segment} exists but is not a real directory`);
12988
13263
  }
@@ -12993,15 +13268,15 @@ function ensureSecureDirectory3(root, relativePath) {
12993
13268
  return current;
12994
13269
  }
12995
13270
  function writeFileAtomically2(root, relativePath, bytes) {
12996
- const target = join16(root, relativePath);
13271
+ const target = join17(root, relativePath);
12997
13272
  const targetDir = dirname10(target);
12998
13273
  ensureSecureDirectory3(root, dirname10(relativePath));
12999
- const stats = lstatSync11(targetDir);
13274
+ const stats = lstatSync12(targetDir);
13000
13275
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
13001
13276
  throw new Error(`${dirname10(relativePath)} is not a real directory`);
13002
13277
  }
13003
13278
  if (existsSync10(target)) {
13004
- const targetStats = lstatSync11(target);
13279
+ const targetStats = lstatSync12(target);
13005
13280
  if (targetStats.isSymbolicLink()) {
13006
13281
  throw new Error(`${relativePath} is a symlink`);
13007
13282
  }
@@ -13009,17 +13284,17 @@ function writeFileAtomically2(root, relativePath, bytes) {
13009
13284
  const tempPath = `${target}.${randomUUID9()}.tmp`;
13010
13285
  let fd = null;
13011
13286
  try {
13012
- fd = openSync7(tempPath, constants6.O_WRONLY | constants6.O_CREAT | constants6.O_EXCL, 384);
13287
+ fd = openSync8(tempPath, constants7.O_WRONLY | constants7.O_CREAT | constants7.O_EXCL, 384);
13013
13288
  writeFileSync8(fd, bytes, "utf8");
13014
- closeSync7(fd);
13289
+ closeSync8(fd);
13015
13290
  fd = null;
13016
13291
  renameSync5(tempPath, target);
13017
13292
  } finally {
13018
13293
  if (fd !== null)
13019
- closeSync7(fd);
13294
+ closeSync8(fd);
13020
13295
  if (existsSync10(tempPath)) {
13021
13296
  try {
13022
- rmSync7(tempPath);
13297
+ rmSync8(tempPath);
13023
13298
  } catch {}
13024
13299
  }
13025
13300
  }
@@ -13039,7 +13314,7 @@ function writeBatchCommitEvidence(root, evidence) {
13039
13314
  }
13040
13315
  function readBatchCommitEvidence(root, batchId, taskId) {
13041
13316
  const path = commitEvidencePath(batchId, taskId);
13042
- const fullPath = join16(root, path);
13317
+ const fullPath = join17(root, path);
13043
13318
  if (!existsSync10(fullPath))
13044
13319
  return null;
13045
13320
  try {
@@ -13314,11 +13589,11 @@ function createDefaultBatchGitPort() {
13314
13589
  import { spawnSync as spawnSync13 } from "node:child_process";
13315
13590
  import { createHash as createHash21 } from "node:crypto";
13316
13591
  import { existsSync as existsSync12, realpathSync as realpathSync13 } from "node:fs";
13317
- import { isAbsolute as isAbsolute7, join as join19 } from "node:path";
13592
+ import { isAbsolute as isAbsolute7, join as join20 } from "node:path";
13318
13593
 
13319
13594
  // plugins/immune-brain/runtime/kernel/runtime_contracts.ts
13320
13595
  import { existsSync as existsSync11, readFileSync as readFileSync15 } from "node:fs";
13321
- import { dirname as dirname11, join as join17, resolve as resolve14 } from "node:path";
13596
+ import { dirname as dirname11, join as join18, resolve as resolve14 } from "node:path";
13322
13597
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13323
13598
  var RUNTIME_CONTRACTS_MANIFEST = "runtime_contracts.json";
13324
13599
  var RUNTIME_CONTRACTS = {
@@ -13338,10 +13613,10 @@ function runningRuntimeSource() {
13338
13613
  }
13339
13614
  function readRuntimeContracts(runtimeDir) {
13340
13615
  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)
13616
+ join18(runtimeDir, RUNTIME_CONTRACTS_MANIFEST),
13617
+ join18(runtimeDir, "kernel", RUNTIME_CONTRACTS_MANIFEST),
13618
+ join18(runtimeDir, "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST),
13619
+ join18(runtimeDir, "plugins", "immune-brain", "runtime", "kernel", RUNTIME_CONTRACTS_MANIFEST)
13345
13620
  ];
13346
13621
  const path = candidates.find((candidate) => existsSync11(candidate));
13347
13622
  if (!path)
@@ -13412,7 +13687,7 @@ async function runFinalVerification(root, commands, options = {}) {
13412
13687
 
13413
13688
  // plugins/immune-brain/runtime/unattended/batch_delegation.ts
13414
13689
  import { execFileSync as execFileSync8 } from "node:child_process";
13415
- import { join as join18 } from "node:path";
13690
+ import { join as join19 } from "node:path";
13416
13691
  import { writeFileSync as writeFileSync9 } from "node:fs";
13417
13692
  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
13693
  var DELEGATED_ACTOR_ID = "batch-coordinator";
@@ -13463,7 +13738,7 @@ function applyDelegatedIntentRevision(input) {
13463
13738
  const nextRef = { path: `docs/plans/${taskId}.intent.json`, content_hash: nextHash };
13464
13739
  const snapshot = captureStagedIntent(root, prior.intent_ref.path);
13465
13740
  try {
13466
- writeFileSync9(join18(root, prior.intent_ref.path), `${JSON.stringify(nextIntent, null, 2)}
13741
+ writeFileSync9(join19(root, prior.intent_ref.path), `${JSON.stringify(nextIntent, null, 2)}
13467
13742
  `);
13468
13743
  execFileSync8("git", ["add", "--", prior.intent_ref.path], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
13469
13744
  const prepared = await projectAssurance(root, taskId, taskDeliveryIdentity);
@@ -13832,7 +14107,7 @@ function laneRuntimeDir(root, explicit) {
13832
14107
  if (explicit)
13833
14108
  return explicit;
13834
14109
  try {
13835
- const manifest = JSON.parse(readFileSync16(join19(root, "package.json"), "utf8"));
14110
+ const manifest = JSON.parse(readFileSync16(join20(root, "package.json"), "utf8"));
13836
14111
  return manifest.name === "immune-brain" ? root : null;
13837
14112
  } catch {
13838
14113
  return null;
@@ -14140,7 +14415,7 @@ function finalizeLane(root, record, reason, nextAction, extra = {}) {
14140
14415
  writeBatchRunReport(root, report);
14141
14416
  return report;
14142
14417
  }
14143
- function refuse2(input, persisted, code, detail) {
14418
+ function refuse3(input, persisted, code, detail) {
14144
14419
  const base = persisted && isLaneBatchRecord(persisted) ? persisted : prepareBatchLaneRunState({
14145
14420
  batch_id: input.batch_id,
14146
14421
  initiative_slug: input.initiative_slug,
@@ -14229,12 +14504,12 @@ function isLineageBreak(message) {
14229
14504
  }
14230
14505
  async function runLaneBatch(input, persisted) {
14231
14506
  if (persisted && !isLaneBatchRecord(persisted))
14232
- return refuse2(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
14507
+ return refuse3(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
14233
14508
  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}`);
14509
+ return refuse3(input, persisted, PARALLEL_MISMATCH, `the recorded batch runs with max_parallel ${persisted.max_parallel}`);
14235
14510
  const limit = persisted?.max_parallel ?? input.max_parallel ?? 1;
14236
14511
  if (!input.children.length)
14237
- return refuse2(input, persisted, "batch_plan_empty", "batch plan is empty");
14512
+ return refuse3(input, persisted, "batch_plan_empty", "batch plan is empty");
14238
14513
  const git = serialGitOf(input);
14239
14514
  const lanes = laneGitOf(input);
14240
14515
  let record;
@@ -14281,11 +14556,11 @@ async function runLaneBatch(input, persisted) {
14281
14556
  } else {
14282
14557
  const invalid = validateNewAuthorization(input);
14283
14558
  if (invalid)
14284
- return refuse2(input, null, "batch_authorization_invalid", invalid);
14559
+ return refuse3(input, null, "batch_authorization_invalid", invalid);
14285
14560
  const contractRefusal = laneRuntimeContractRefusal(input.root, input.executor_runtime);
14286
14561
  if (contractRefusal) {
14287
14562
  const [code, ...detail] = contractRefusal.split(": ");
14288
- return refuse2(input, null, code, detail.join(": "));
14563
+ return refuse3(input, null, code, detail.join(": "));
14289
14564
  }
14290
14565
  const preflight = await git.preflight({
14291
14566
  root: input.root,
@@ -14293,7 +14568,7 @@ async function runLaneBatch(input, persisted) {
14293
14568
  base_head: input.base_head
14294
14569
  });
14295
14570
  if (!preflight.ok)
14296
- return refuse2(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
14571
+ return refuse3(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
14297
14572
  record = prepareBatchLaneRunState({
14298
14573
  batch_id: input.batch_id,
14299
14574
  initiative_slug: input.initiative_slug,
@@ -14357,7 +14632,7 @@ async function runLaneBatch(input, persisted) {
14357
14632
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "released" } : c);
14358
14633
  persist();
14359
14634
  }
14360
- const lineage = existsSync12(join19(input.root, ".git")) ? classifyBatchLineage({
14635
+ const lineage = existsSync12(join20(input.root, ".git")) ? classifyBatchLineage({
14361
14636
  root: input.root,
14362
14637
  branch: record.branch ?? "",
14363
14638
  expectedHead: expectedBatchHead(record),
@@ -14808,7 +15083,7 @@ function failPersistedLineage(root, existing, message) {
14808
15083
  return writeBatchRunState(root, record);
14809
15084
  }
14810
15085
  function reconcileLineage(root, record) {
14811
- if (!existsSync13(join20(root, ".git")))
15086
+ if (!existsSync13(join21(root, ".git")))
14812
15087
  return { record, failure: null };
14813
15088
  const expected = expectedBatchHead(record);
14814
15089
  const lineage = classifyBatchLineage({
@@ -14831,7 +15106,7 @@ function reconcileLineage(root, record) {
14831
15106
  };
14832
15107
  }
14833
15108
  async function validatePersistedRun(input, record) {
14834
- if (!record.commits.length && record.children[0]?.state === "settled" && existsSync13(join20(input.root, ".git"))) {
15109
+ if (!record.commits.length && record.children[0]?.state === "settled" && existsSync13(join21(input.root, ".git"))) {
14835
15110
  const head = spawnSync14("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
14836
15111
  const live = head.stdout.trim();
14837
15112
  const expected = expectedBatchHead(record);
@@ -14861,7 +15136,7 @@ async function validatePersistedRun(input, record) {
14861
15136
  const git = batchGitPortOf(input);
14862
15137
  const evidence = await git.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch);
14863
15138
  if (!evidence || evidence.commit !== child.commit) {
14864
- if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync13(join20(input.root, ".git"))) {
15139
+ if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync13(join21(input.root, ".git"))) {
14865
15140
  const reach = spawnSync14("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
14866
15141
  if (reach.status !== 0) {
14867
15142
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
@@ -15301,7 +15576,7 @@ async function driveInterruptedChild(input, child) {
15301
15576
  const planChild = input.children.find((c) => c.task_id === child.task_id);
15302
15577
  const intentPath = planChild?.intent_path ?? undefined;
15303
15578
  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))
15579
+ if (existing && !record.commits.length && existsSync13(join21(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
15305
15580
  throw new Error("first unpersisted batch commit provenance is invalid");
15306
15581
  const adopted = existing ?? await doCommit().catch((error) => {
15307
15582
  const message = error instanceof Error ? error.message : String(error);
@@ -15832,7 +16107,7 @@ class ClaudeRuntime {
15832
16107
  if (!slug)
15833
16108
  return result;
15834
16109
  try {
15835
- if (existsSync14(join21(this.cwd, ".imm", "audit", taskId)))
16110
+ if (existsSync14(join22(this.cwd, ".imm", "audit", taskId)))
15836
16111
  execFileSync9("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
15837
16112
  const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
15838
16113
  return { ...result, batch };
@@ -15929,7 +16204,7 @@ class ClaudeRuntime {
15929
16204
  throw new Error("approve_breaking_intent_revision requires next_intent");
15930
16205
  const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
15931
16206
  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;
16207
+ const sidecar = nextIntent ? join22(this.cwd, priorIntent.intent_ref.path) : undefined;
15933
16208
  const stagedSnapshot = sidecar ? captureStagedIntent(this.cwd, priorIntent.intent_ref.path) : undefined;
15934
16209
  const restoreStagedIntent2 = () => {
15935
16210
  if (!stagedSnapshot)
@@ -16038,7 +16313,7 @@ class ClaudeRuntime {
16038
16313
  const { app } = await this.authority();
16039
16314
  const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
16040
16315
  const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
16041
- const sidecar = join21(ctx.cwd, priorIntent.intent_ref.path);
16316
+ const sidecar = join22(ctx.cwd, priorIntent.intent_ref.path);
16042
16317
  const priorBytes = operation.op === "revise_intent" ? readFileSync17(sidecar) : null;
16043
16318
  const priorStaged = priorBytes !== null ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : null;
16044
16319
  try {
@@ -16442,6 +16717,12 @@ function createMcpRuntime(options = {}) {
16442
16717
  let negotiatedVersion;
16443
16718
  let negotiatedInteractive = false;
16444
16719
  const connectionId = randomUUID10();
16720
+ const consumeAdmission = options.consumePhaseAdmission ?? ((toolCallId, operation) => consumePhaseAdmission(toolCallId, operation));
16721
+ const requireRunEntry = (operation, toolCallId) => {
16722
+ if (consumeAdmission(toolCallId, operation))
16723
+ return;
16724
+ throw new NativeAuthorityError("run_entry_required", `${operation} needs a literal-user imm-run entry in this session`);
16725
+ };
16445
16726
  return {
16446
16727
  runtime,
16447
16728
  host,
@@ -16471,6 +16752,7 @@ function createMcpRuntime(options = {}) {
16471
16752
  const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
16472
16753
  if (!probe.ok)
16473
16754
  throw new NativeAuthorityError("unsupported_host", probe.reason);
16755
+ requireRunEntry(name, meta.toolCallId ?? `call-${name}`);
16474
16756
  const toolMeta = {
16475
16757
  sessionId: meta.sessionId ?? connectionId,
16476
16758
  toolCallId: meta.toolCallId ?? `call-${name}`,
@@ -16538,8 +16820,10 @@ function createMcpRuntime(options = {}) {
16538
16820
  interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
16539
16821
  signal: meta.signal
16540
16822
  };
16541
- if (name === "enroll")
16823
+ if (name === "enroll") {
16824
+ requireRunEntry(name, meta.toolCallId ?? `call-${name}`);
16542
16825
  return runtime.enroll(taskId, toolMeta);
16826
+ }
16543
16827
  if (name === "advance_assurance")
16544
16828
  return runtime.advance(taskId, toolMeta.signal, toolMeta);
16545
16829
  if (name === "submit_review") {
@@ -17046,13 +17330,14 @@ async function runHook() {
17046
17330
  payload = null;
17047
17331
  }
17048
17332
  if (payload?.hook_event_name === "PreToolUse") {
17049
- const decision = laneGuardHookOutput(payload);
17333
+ const decision = laneGuardHookOutput(payload) ?? phaseEntryGateHookOutput(payload);
17050
17334
  if (decision)
17051
17335
  process.stdout.write(`${decision}
17052
17336
  `);
17053
17337
  return;
17054
17338
  }
17055
17339
  if (payload?.hook_event_name === "UserPromptSubmit") {
17340
+ recordPhaseEntryPrompt(payload);
17056
17341
  const notice = await activeTaskHookOutput(payload);
17057
17342
  if (notice)
17058
17343
  process.stdout.write(`${notice}