immune-brain 4.4.1 → 4.6.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 (51) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
  7. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
  8. package/plugins/immune-brain/dist/BASELINE.md +18 -1
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +1401 -1101
  10. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -0
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +21 -0
  12. package/plugins/immune-brain/dist/imm-loop.md +52 -6
  13. package/plugins/immune-brain/dist/imm-planner.md +102 -13
  14. package/plugins/immune-brain/dist/role-prompts/code-review.md +19 -0
  15. package/plugins/immune-brain/dist/role-prompts/qa.md +13 -1
  16. package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
  17. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
  18. package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
  19. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
  20. package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
  21. package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
  22. package/plugins/immune-brain/runtime/github_issue_tracker.ts +956 -634
  23. package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
  24. package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
  25. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
  26. package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
  27. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
  28. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
  29. package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
  30. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
  31. package/plugins/immune-brain/runtime/kernel/storage.ts +8 -3
  32. package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
  33. package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
  34. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  35. package/plugins/immune-brain/runtime/prompts/code-review.md +19 -0
  36. package/plugins/immune-brain/runtime/prompts/qa.md +13 -1
  37. package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
  38. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
  39. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +152 -46
  40. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
  41. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +193 -0
  42. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +112 -122
  43. package/plugins/immune-brain/runtime/unattended/batch_state.ts +57 -16
  44. package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
  45. package/plugins/immune-brain/runtime/v4_runtime.ts +54 -0
  46. package/plugins/immune-brain/runtime/workspace_scope.ts +75 -12
  47. package/plugins/immune-brain/skills/BASELINE.md +18 -1
  48. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +5 -0
  49. package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -2
  50. package/plugins/immune-brain/skills/imm-planner/SKILL.md +10 -1
  51. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
@@ -176,6 +176,15 @@ async function runKernelCli(args: string[], root: string): Promise<{
176
176
  // status --json, and the explicit audit command. All other kernel
177
177
  // subcommands (readiness, journal, migrate) are retired.
178
178
  const sub = args[0] ?? "";
179
+ // The canonical author shape is discoverable before any error probing: a
180
+ // caller who does not know the destination/stdin contract reads it here
181
+ // instead of discovering it through a failed call.
182
+ if (sub === "intent" && (args[1] === "--help" || args[1] === "help" || args[1] === undefined))
183
+ return {
184
+ stdout: `${intentAuthorHelp()}\n`,
185
+ stderr: "",
186
+ returncode: 0,
187
+ };
179
188
  // The explicit claimless storage-layout migration is reachable here: a
180
189
  // worktree still on the retired file store has no other entry point, and
181
190
  // every mutating subcommand stays fail-closed behind it.
@@ -224,6 +233,51 @@ async function runCli(command: string, args: string[], root: string): Promise<{
224
233
  };
225
234
  }
226
235
 
236
+ /**
237
+ * The canonical TaskIntent author contract, printed by `imm-kernel intent` and
238
+ * `imm-kernel intent --help`. It states destination, stdin, and the strict v2
239
+ * descriptor shape before a caller has to discover them by failing a call.
240
+ */
241
+ export function intentAuthorHelp(): string {
242
+ return [
243
+ "imm-kernel intent author <destination> --stdin --json",
244
+ " destination: docs/plans/<task-id>.intent.json (created exclusively; never overwritten)",
245
+ " stdin: the complete candidate TaskIntent JSON on file descriptor 0",
246
+ " stdout: { valid, enrollment_ready, intent_ref, git_base_initialization_required, ... }",
247
+ "",
248
+ "imm-kernel intent validate <path> --json",
249
+ "",
250
+ "Candidate shape (assurance_kernel/task_intent/v1):",
251
+ JSON.stringify(
252
+ {
253
+ contract: "assurance_kernel/task_intent/v1",
254
+ task_id: "<task-id>",
255
+ owner: "user",
256
+ goal: "<one closable outcome>",
257
+ risk: "routine|material|critical",
258
+ revision: 1,
259
+ scope_hint: ["<every file the slice touches>"],
260
+ acceptance: [
261
+ {
262
+ id: "<ID>",
263
+ assertion: "<observable invariant>",
264
+ verification: JSON.stringify({
265
+ contract: "assurance_kernel/verification_descriptor/v2",
266
+ command: { executable: "bun", argv: ["test", "tests/<focused>.test.ts"], cwd: ".", timeout_ms: 180000, max_output_bytes: 196608 },
267
+ environment: { prepare: null, writable_paths: [] },
268
+ }),
269
+ },
270
+ ],
271
+ },
272
+ null,
273
+ 2,
274
+ ),
275
+ "",
276
+ "Unknown fields are rejected; the descriptor parser is strict v2 and never infers a runner.",
277
+ "A v1 descriptor is historical-only and needs an explicit Intent revision before execution.",
278
+ ].join("\n");
279
+ }
280
+
227
281
  async function main(argv: string[]): Promise<number> {
228
282
  const mode = argv[0];
229
283
  const root = process.cwd();
@@ -19,11 +19,13 @@ export interface GitWorkspaceSnapshot {
19
19
  }
20
20
 
21
21
  function git(root: string, args: string[]): string | null {
22
- const result = spawnSync("git", ["-C", root, ...args], {
22
+ const diff = args[0] === "diff";
23
+ const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
24
+ const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
23
25
  encoding: "utf8",
24
26
  stdio: ["ignore", "pipe", "pipe"],
25
27
  });
26
- return result.status === 0 ? result.stdout : null;
28
+ return result.status === 0 || (diff && result.status === 1) ? result.stdout : null;
27
29
  }
28
30
 
29
31
  function splitNull(value: string): string[] {
@@ -135,7 +137,9 @@ function fileFingerprint(root: string, relativePath: string): string {
135
137
  }
136
138
 
137
139
  function dirtyPaths(root: string): string[] | null {
138
- const tracked = git(root, ["diff", "--name-only", "-z", "HEAD", "--"]);
140
+ // --no-renames, like the task-path derivations: rename detection would list
141
+ // only the destination and hide the source path's deletion.
142
+ const tracked = git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD", "--"]);
139
143
  const untracked = git(root, [
140
144
  "ls-files",
141
145
  "--others",
@@ -217,12 +221,14 @@ export function setGitTaskSnapshotTestHook(hook: () => void): () => void {
217
221
  }
218
222
 
219
223
  function gitBytes(root: string, args: string[]): Buffer {
220
- const result = spawnSync("git", ["-C", root, ...args], {
224
+ const diff = args[0] === "diff";
225
+ const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
226
+ const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
221
227
  encoding: null,
222
228
  stdio: ["ignore", "pipe", "pipe"],
223
229
  maxBuffer: 8 * 1024 * 1024,
224
230
  });
225
- if (result.status !== 0 || !Buffer.isBuffer(result.stdout)) {
231
+ if ((result.status !== 0 && !(diff && result.status === 1)) || !Buffer.isBuffer(result.stdout)) {
226
232
  throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
227
233
  }
228
234
  return result.stdout;
@@ -386,6 +392,44 @@ function taskPathMatchesScope(path: string, scope: string[]): boolean {
386
392
  return scope.some((scopePath) => pathMatchesScope(path, scopePath));
387
393
  }
388
394
 
395
+ /**
396
+ * A staged in-scope path that still equals the Enrollment baseline predates the
397
+ * task. Dropping it silently let QA attest workspace bytes that the Review
398
+ * revision did not carry, so it is a hard stop instead: reverting and reapplying
399
+ * yields the same bytes, and only a fresh Enrollment from a clean scope recovers.
400
+ * The task's own planning sidecar is exempt, as it is in dirtyScopePaths:
401
+ * Enrollment requires it tracked, so it is routinely staged in the baseline.
402
+ */
403
+ function assertNoPreEnrollmentScopeChanges(root: string, scopedStagedPaths: readonly string[], taskId?: string): void {
404
+ const baseline = readEnrollmentBaseline(root);
405
+ if (!baseline) return;
406
+ const current = captureGitWorkspaceSnapshot(root);
407
+ if (!current) throw new Error("enrollment baseline cannot be compared because Git is unavailable");
408
+ const withheld = [...new Set(scopedStagedPaths)]
409
+ .filter((path) => !isOwnPlanningSidecar(path, taskId))
410
+ .filter((path) => baseline.dirty_files[path] !== undefined && baseline.dirty_files[path] === current.dirty_files[path])
411
+ .sort(comparePaths);
412
+ if (withheld.length > 0)
413
+ throw new Error(
414
+ `task scope contains staged changes that predate Enrollment and cannot become task work: ${withheld.join(", ")}; stop the task and re-enroll from a clean scope`,
415
+ );
416
+ }
417
+
418
+ /**
419
+ * In-scope paths that are already staged, modified or untracked relative to
420
+ * HEAD. Enrollment refuses these: the baseline would record them as user dirt
421
+ * and they could never be delivered by the task.
422
+ */
423
+ export function dirtyScopePaths(projectRoot: string, scopeHint: unknown, taskId?: string): string[] {
424
+ const root = realpathSync(resolve(projectRoot));
425
+ const scope = assertCanonicalTaskScope(scopeHint);
426
+ const paths = dirtyPaths(root);
427
+ if (!paths) throw new Error("cannot inspect the task scope outside a committed Git workspace");
428
+ return paths.filter(
429
+ (path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && taskPathMatchesScope(path, scope),
430
+ );
431
+ }
432
+
389
433
  function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTaskSnapshot {
390
434
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
391
435
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
@@ -421,12 +465,10 @@ function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTa
421
465
  if (uncommittedInScope.length > 0)
422
466
  throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
423
467
 
424
- const baseline = readEnrollmentBaseline(root);
425
- const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
426
468
  const taskPaths = [...new Set(stagedPaths)]
427
469
  .filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
428
- .filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined))
429
470
  .sort(comparePaths);
471
+ assertNoPreEnrollmentScopeChanges(root, taskPaths, taskId);
430
472
  const stagedFiles: Record<string, GitTaskIndexEntry> = {};
431
473
  for (const path of taskPaths) {
432
474
  const current = indexEntry(root, path);
@@ -555,10 +597,8 @@ function taskRevisionSnapshotOnce(
555
597
  "untracked task revision paths",
556
598
  );
557
599
  assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
558
- const baseline = readEnrollmentBaseline(root);
559
- const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
560
- const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
561
- .filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined));
600
+ const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope));
601
+ assertNoPreEnrollmentScopeChanges(root, scopedStagedPaths, taskId);
562
602
  const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
563
603
  const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
564
604
  assertNoCaseFoldCollisions(
@@ -631,6 +671,29 @@ export function taskRevisionIdentity(
631
671
  };
632
672
  }
633
673
 
674
+ /** The same v4 identity for an immutable delivered commit, without touching the index. */
675
+ export function taskCommitRevisionIdentity(projectRoot: string, scopeHint: unknown, baseHead: string, commit: string): GitTaskDiffIdentity {
676
+ const requested = resolve(projectRoot);
677
+ if (lstatSync(requested).isSymbolicLink()) throw new Error("task commit root must be a real directory");
678
+ const root = realpathSync(requested), scope = assertCanonicalTaskScope(scopeHint);
679
+ if (!GIT_OBJECT_ID.test(baseHead) || !GIT_OBJECT_ID.test(commit)) throw new Error("invalid task commit identity");
680
+ if (gitRequired(root, ["cat-file", "-t", baseHead], "unreadable task base") !== "commit" ||
681
+ gitRequired(root, ["cat-file", "-t", commit], "unreadable task commit") !== "commit" ||
682
+ git(root, ["merge-base", "--is-ancestor", baseHead, commit]) === null) throw new Error("invalid task commit ancestry");
683
+ const paths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", baseHead, commit, "--"]), "task commit paths")
684
+ .filter(path => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).sort(comparePaths);
685
+ assertNoCaseFoldCollisions(paths, "task commit paths");
686
+ const changed_paths: Record<string, GitTaskIndexEntry> = {};
687
+ for (const path of paths) {
688
+ const current = headEntry(root, commit, path), base = headEntry(root, baseHead, path);
689
+ changed_paths[path] = { status: !base ? "added" : !current ? "deleted" : "modified",
690
+ mode: current?.mode ?? null, oid: current?.oid ?? null, base_mode: base?.mode ?? null, base_oid: base?.oid ?? null };
691
+ }
692
+ const snapshot: GitTaskRevisionSnapshot = { kind: "git-task-revision-v1", repository_root: root, base_head: baseHead,
693
+ base_tree: gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "unreadable task base tree"), scope, changed_paths };
694
+ return { diff_hash: hashTaskSnapshot(snapshot), changed_paths: paths };
695
+ }
696
+
634
697
  /** The single v4 freshness identity shared by QA, Review, authorization, and completion. */
635
698
  export function taskRevisionDiffHash(
636
699
  projectRoot: string,
@@ -9,6 +9,20 @@
9
9
  - Stage only explicit task-owned paths. This staging authority does not grant
10
10
  commit, push, broad staging, or authority over pre-existing user changes.
11
11
  Never use `git add .` or `git add -A` in a dirty worktree.
12
+ - Grant effects separately: staging, commit, push, publication, and outbound
13
+ model/data calls are distinct operations. Recovering a stage-only instruction
14
+ restores staging and nothing more; commit, push, and publication each keep
15
+ their own existing grant. An explicit approval already covering the same
16
+ operation, target, and impact — including a valid batch capability — stays
17
+ usable without another gate.
18
+ - Before a new outbound effect, check the project's existing research channel and
19
+ data scope. A public article, a mock, a documentation example, or a local
20
+ read-only database gives no authority to call an arbitrary provider or send new
21
+ data; authorize only a genuinely new channel/data/effect delta. Distinguish
22
+ documentation, mock, real-channel, and model-quality evidence when reporting.
23
+ - These text rules are instruction contracts, not a hard bash sandbox. They
24
+ guide behavior and are checked as contracts; they do not technically prevent an
25
+ arbitrary shell command, so authority gates and review remain the real boundary.
12
26
  - Do not create, switch, or delete Git worktrees; operate only in the Host launch directory.
13
27
  - Record reproducible evidence before reporting closure.
14
28
  - Required verification must pass before reporting completion; disclosing a gap is not a substitute. Autonomously diagnose, repair, and rerun failing conventional local checks within the authorized scope; never delete, skip, or weaken a valid check to manufacture a pass. If a required check remains failing or cannot run, report the work as incomplete with the concrete blocker.
@@ -81,7 +95,6 @@ non-Kernel operations, an explicit user approval already covering the same
81
95
  operation, target, and impact is sufficient; ask again only for a material delta.
82
96
  A generic continuation or configured preference is not blanket authorization.
83
97
  Mandatory native gates and hash-bound manifest approvals still apply.
84
-
85
98
  Privileged effects include:
86
99
 
87
100
  - publish, release, deployment, or remote-system mutation;
@@ -91,6 +104,10 @@ Privileged effects include:
91
104
  override; and
92
105
  - external writes whose target or impact cannot be safely reversed locally.
93
106
 
107
+ An existing grant for one of these never inherits another: a stage-only
108
+ instruction stays stage-only, a commit is not push or publication authority,
109
+ and a valid batch capability commits only its own scope-bounded child.
110
+
94
111
  Routine Managed enrollment uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation. Explicit Plan-only requests stop with candidate artifacts and do not invoke Enrollment; execution-bearing requests open the native gate directly without chat pre-confirmation. Enrollment validates intent, Git ownership, scope, workspace claim, and final authority preconditions without executing acceptance descriptors; deterministic QA executes them after implementation. The routine task proceeds from that single confirmation through enrollment, execution, and QA without a second human stop. Do not request confirmation for local in-scope edits, local verification, ordinary Direct rework, scoped diff review, or completion reporting. Managed evidence, QA, Review, and completion authority remain governed by their Managed contracts; R2 does not weaken them. Managed native-authority failures fail closed with one stable reason and exactly one same-Host recovery action; never offer a Pi, Direct Path, cross-Host/worktree, unmanaged, or automatic-retry fallback.
95
112
 
96
113
  For an unborn symbolic Git HEAD, the same Enrollment confirmation discloses an empty root commit. After approval, the Host creates that commit without staging project content or changing the index/worktree, then revalidates ownership and rehearses Enrollment against the new base. Git identity uses existing environment/config values with an explicit automation fallback, never config writes. Decline or pre-initialization cancellation leaves HEAD unborn; if later Enrollment fails or is cancelled, report the remaining commit. Invalid Git refs are errors, not initialization candidates.
@@ -28,3 +28,8 @@ When framing discusses later execution, describe Enrollment only as the
28
28
  current Host's native gate. A failed Managed authority interaction stays
29
29
  fail-closed: report one same-Host recovery action, never another Host,
30
30
  worktree, or unmanaged implementation.
31
+
32
+ Every sourced branch closes as resolved, explicitly excluded, explicitly
33
+ deferred, or dependent-blocked with its blocking node named, including for a
34
+ clear zero-question request; a confirmed outcome is never conserved as a
35
+ smaller deliverable.
@@ -11,7 +11,9 @@ ordinary host input never resumes a Managed owner implicitly.
11
11
 
12
12
  Mandatory constraints before any action: verify the active backend claim,
13
13
  TaskIntent, and TaskRecord via `imm_kernel_canary` `status` first; invalid or
14
- contradictory projections fail closed. All Managed authority gates use the
14
+ contradictory projections fail closed. Resume only from those authority facts:
15
+ a compaction heading, HANDOFF summary, or Issue state is prose, never authority.
16
+ All Managed authority gates use the
15
17
  current Host's native interaction. A failed gate stays fail-closed and
16
18
  reports one same-Host recovery action; never suggest another Host, worktree,
17
19
  or unmanaged implementation as a fallback.
@@ -24,5 +26,5 @@ or all references as an entry prerequisite.
24
26
  - common: [Shared Guards](../../dist/BASELINE.md#shared-guards), [Workflow Activation](../../dist/BASELINE.md#workflow-activation), [Host Confirmation Boundary](../../dist/BASELINE.md#host-confirmation-boundary), [Kernel Canary Routing and Authority](../../dist/imm-loop.md#kernel-canary-routing-and-authority)
25
27
  - unattended batch run, or any question about whether `imm-loop` starts one: [Unattended Batch Opt-In](../../dist/imm-loop.md#unattended-batch-opt-in)
26
28
  - steady execution: [Verification and Local Recovery](../../dist/BASELINE.md#verification-and-local-recovery), [Execution Loop](../../dist/imm-loop.md#execution-loop), [Observable Output](../../dist/imm-loop.md#observable-output)
27
- - rework, scope expansion, breaking revision, user decision, stop, interruption or unknown state before any action: [Decisions and Recovery](../../dist/imm-loop.md#decisions-and-recovery), [Failure Output](../../dist/imm-loop.md#failure-output); re-read `status`, then the pending obligation
29
+ - rework, scope expansion, breaking revision, user decision, stop, interruption or unknown state before any action: [Decisions and Recovery](../../dist/imm-loop.md#decisions-and-recovery), [Failure Output](../../dist/imm-loop.md#failure-output); re-read `status`, then the pending obligation. Rework submits the verdict before editing, and an uncertain interruption resumes from exact task/run authority facts rather than a summary.
28
30
  - review or post-settlement learning: [Review and Learning](../../dist/imm-loop.md#review-and-learning)
@@ -11,7 +11,10 @@ host-native.
11
11
 
12
12
  Mandatory constraints before any action: Planner writes a TaskIntent, and a Spec
13
13
  only for complex work; it never implements, overwrites an enrolled TaskIntent, or
14
- grants execution authority — only the native Enrollment gate can.
14
+ grants execution authority — only the native Enrollment gate can. Before
15
+ handoff it maps every upstream item and required phase exactly once and reports
16
+ each Slice result plus the remaining gap; a plan that conserves a smaller
17
+ deliverable than the confirmed outcome fails that check instead of passing.
15
18
 
16
19
  Section routes - load a section's instructions only when its branch applies.
17
20
  Read each linked heading body up to the next heading; nested sections and
@@ -40,3 +43,9 @@ that include execution invoke the current Host's native Enrollment gate
40
43
  directly, without chat pre-confirmation. Native-gate failure stays fail-closed
41
44
  in that Host: report its reason and one retry action only; never suggest
42
45
  another Host, worktree, or unmanaged implementation as a fallback.
46
+
47
+ Before that handoff, confirm complete mapping of every upstream item and
48
+ required phase with each Slice's result and the remaining gap; reject a plan
49
+ that conserved only part of the confirmed outcome, and ask again only for a
50
+ newly evidenced material delta rather than reconfirming an unchanged clear
51
+ request or an already-confirmed decision.
@@ -1,33 +0,0 @@
1
- // A native confirmation is bounded on both Hosts. Claude bounded its MCP
2
- // elicitation with IMMUNE_BRAIN_BATCH_TIMEOUT_MS and Pi waited indefinitely; the
3
- // bound, its default, and the "this deadline expired rather than the user
4
- // cancelling" distinction are shared here. Each Host still owns its transport:
5
- // this module only produces the signal to hand it and reports expiry.
6
-
7
- export const CONFIRMATION_TIMEOUT_DEFAULT_MS = 60_000;
8
- export const CONFIRMATION_TIMEOUT_ENV = "IMMUNE_BRAIN_BATCH_TIMEOUT_MS";
9
-
10
- export interface ConfirmationDeadline {
11
- /** The caller's signal, combined with this deadline. */
12
- signal: AbortSignal;
13
- /** True when this deadline fired and the caller's own signal did not. */
14
- timedOut(): boolean;
15
- clear(): void;
16
- }
17
-
18
- export function startConfirmationDeadline(input: {
19
- env?: Record<string, string | undefined>;
20
- signal?: AbortSignal;
21
- }): ConfirmationDeadline {
22
- const configured = Number(input.env?.[CONFIRMATION_TIMEOUT_ENV]);
23
- const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : CONFIRMATION_TIMEOUT_DEFAULT_MS;
24
- const controller = new AbortController();
25
- const timer = setTimeout(() => {
26
- controller.abort(new Error("native confirmation timed out waiting for user interaction"));
27
- }, timeoutMs);
28
- return {
29
- signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
30
- timedOut: () => controller.signal.aborted && !input.signal?.aborted,
31
- clear: () => clearTimeout(timer),
32
- };
33
- }