immune-brain 5.2.0 → 5.5.1

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 (77) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
  8. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
  9. package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
  10. package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
  11. package/plugins/immune-brain/dist/BASELINE.md +17 -1
  12. package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
  13. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
  14. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
  15. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  16. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  17. package/plugins/immune-brain/dist/imm-planner.md +63 -19
  18. package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
  19. package/plugins/immune-brain/dist/imm-run.md +89 -344
  20. package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
  21. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
  22. package/plugins/immune-brain/hooks/hooks.json +22 -2
  23. package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
  24. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
  25. package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
  26. package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
  27. package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
  28. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
  29. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
  30. package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
  31. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
  32. package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
  33. package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
  36. package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
  37. package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
  38. package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
  40. package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
  41. package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
  42. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
  43. package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
  44. package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
  45. package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
  46. package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
  47. package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
  48. package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
  49. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
  50. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
  51. package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
  52. package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
  53. package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
  54. package/plugins/immune-brain/runtime/plan_core.ts +15 -8
  55. package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
  56. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  57. package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
  58. package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
  59. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
  60. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
  61. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
  62. package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
  63. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
  64. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
  65. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
  66. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
  67. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
  68. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
  69. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
  70. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
  71. package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
  72. package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
  73. package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
  74. package/plugins/immune-brain/skills/BASELINE.md +17 -1
  75. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
  76. package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
  77. package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
@@ -0,0 +1,248 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, lstatSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { projectTaskStatus } from "../assurance/coordinator";
6
+ import { readTaskIntent } from "../kernel/intent";
7
+ import { readAuditTaskPair, readSecureProjectFile, readSettledTaskEvidence, revisionForContent } from "../kernel/storage";
8
+ import { auditRunRecordPath, auditRunTerminalProofPath } from "../kernel/storage_paths";
9
+ import { captureGitTaskRevisionSnapshot, pathMatchesScope } from "../workspace_scope";
10
+ import { readParentConsolidationReceipt } from "./batch_state";
11
+ import { parseFinalVerification } from "./batch_final_verification";
12
+
13
+ export interface DeliveryRun { task_id: string; run_id: string }
14
+ export interface DeliveryConsolidation extends DeliveryRun { source_runs: DeliveryRun[] }
15
+ export const consolidationSchema = {
16
+ type: "object", additionalProperties: false, required: ["task_id", "run_id", "source_runs"],
17
+ properties: {
18
+ task_id: { type: "string" }, run_id: { type: "string" },
19
+ source_runs: { type: "array", minItems: 1, maxItems: 16, items: {
20
+ type: "object", additionalProperties: false, required: ["task_id", "run_id"],
21
+ properties: { task_id: { type: "string" }, run_id: { type: "string" } },
22
+ } },
23
+ },
24
+ } as const;
25
+
26
+ function object(value: unknown, keys: string[]): Record<string, unknown> {
27
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
28
+ Object.keys(value).sort().join() !== [...keys].sort().join()) throw new Error("invalid delivery_consolidation shape");
29
+ return value as Record<string, unknown>;
30
+ }
31
+ function run(value: unknown): DeliveryRun {
32
+ const raw = object(value, ["task_id", "run_id"]);
33
+ if (typeof raw.task_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(raw.task_id) ||
34
+ typeof raw.run_id !== "string" || !/^run-[A-Za-z0-9][A-Za-z0-9._-]{0,123}$/.test(raw.run_id))
35
+ throw new Error("invalid consolidation task/run identity");
36
+ return { task_id: raw.task_id, run_id: raw.run_id };
37
+ }
38
+ export function parseDeliveryConsolidation(value: unknown): DeliveryConsolidation | undefined {
39
+ if (value === undefined) return undefined;
40
+ const raw = object(value, ["task_id", "run_id", "source_runs"]);
41
+ const final = run({ task_id: raw.task_id, run_id: raw.run_id });
42
+ if (!Array.isArray(raw.source_runs) || !raw.source_runs.length || raw.source_runs.length > 16)
43
+ throw new Error("consolidation requires 1–16 source runs");
44
+ const sources = raw.source_runs.map(run).sort((a, b) => a.task_id.localeCompare(b.task_id));
45
+ if (new Set([final.task_id, ...sources.map(source => source.task_id)]).size !== sources.length + 1)
46
+ throw new Error("duplicate/self consolidation selection");
47
+ return { ...final, source_runs: sources };
48
+ }
49
+ export function consolidationDigest(value: unknown): string {
50
+ return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
51
+ }
52
+ function git(root: string, argv: string[]): string {
53
+ const result = spawnSync("git", ["-C", root, ...argv], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
54
+ if (result.status !== 0) throw new Error(`consolidation Git inspection failed: ${argv[0]}`);
55
+ return result.stdout;
56
+ }
57
+ export interface ConsolidationEnvelope {
58
+ initiative_slug: string;
59
+ plan_digest: string;
60
+ selection: DeliveryConsolidation;
61
+ parent_tasks: Array<{ task_id: string; intent_hash: string }>;
62
+ branch: string;
63
+ head: string;
64
+ final_commands: string[];
65
+ /** All source approvals are historical; only the final run approves the composed bytes. */
66
+ runs: Array<{ task_id: string; run_id: string; record_hash: string; proof_hash: string;
67
+ intent_hash: string; review_revision: unknown }>;
68
+ delivery_diff_hash: string;
69
+ manifest: Array<{ path: string; mode: string | null; oid: string | null }>;
70
+ }
71
+ export interface ConsolidationReceipt {
72
+ contract: "assurance_kernel/parent_delivery_consolidation/v1";
73
+ envelope: ConsolidationEnvelope;
74
+ digest: string;
75
+ commit: string | null;
76
+ }
77
+
78
+ export function parseConsolidationReceipt(value: unknown): ConsolidationReceipt {
79
+ const raw = object(value, ["contract", "envelope", "digest", "commit"]);
80
+ if (raw.contract !== "assurance_kernel/parent_delivery_consolidation/v1" ||
81
+ (typeof raw.commit !== "string" && raw.commit !== null) ||
82
+ (typeof raw.commit === "string" && !/^[a-f0-9]{40,64}$/.test(raw.commit))) throw new Error("invalid consolidation receipt");
83
+ const e = object(raw.envelope, ["initiative_slug", "plan_digest", "selection", "parent_tasks", "branch", "head",
84
+ "final_commands", "runs", "delivery_diff_hash", "manifest"]);
85
+ const selection = parseDeliveryConsolidation(e.selection)!;
86
+ if (JSON.stringify(selection) !== JSON.stringify(e.selection) || typeof e.initiative_slug !== "string" ||
87
+ !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(e.initiative_slug) || typeof e.branch !== "string" || !e.branch ||
88
+ typeof e.head !== "string" || !/^[a-f0-9]{40,64}$/.test(e.head) ||
89
+ !Array.isArray(e.parent_tasks) || !e.parent_tasks.length || !Array.isArray(e.runs) ||
90
+ !Array.isArray(e.manifest) || !e.manifest.length || !Array.isArray(e.final_commands) ||
91
+ e.final_commands.some(c => typeof c !== "string") || typeof raw.digest !== "string" ||
92
+ raw.digest !== consolidationDigest(e)) throw new Error("consolidation receipt digest/shape mismatch");
93
+ const hash = (value: unknown) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
94
+ if (!hash(e.plan_digest) || !hash(e.delivery_diff_hash) || !parseFinalVerification(e.final_commands)?.length)
95
+ throw new Error("invalid consolidation proof/commands");
96
+ const expectedRuns = [selection, ...selection.source_runs].map(({ task_id, run_id }) => ({ task_id, run_id }));
97
+ if (e.runs.length !== expectedRuns.length) throw new Error("consolidation run contributions missing");
98
+ for (let index = 0; index < e.runs.length; index++) {
99
+ const binding = object(e.runs[index], ["task_id", "run_id", "record_hash", "proof_hash", "intent_hash", "review_revision"]);
100
+ if (JSON.stringify(run({ task_id: binding.task_id, run_id: binding.run_id })) !== JSON.stringify(expectedRuns[index]) ||
101
+ !hash(binding.record_hash) || !hash(binding.proof_hash) || !hash(binding.intent_hash))
102
+ throw new Error("consolidation run contribution binding mismatch");
103
+ }
104
+ const parentIds = e.parent_tasks.map(value => {
105
+ const task = object(value, ["task_id", "intent_hash"]);
106
+ if (typeof task.task_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(task.task_id) || !hash(task.intent_hash))
107
+ throw new Error("invalid consolidation Parent binding");
108
+ return task.task_id;
109
+ });
110
+ if (new Set(parentIds).size !== parentIds.length) throw new Error("duplicate consolidation Parent contribution");
111
+ const paths = e.manifest.map(value => {
112
+ const entry = object(value, ["path", "mode", "oid"]);
113
+ if (typeof entry.path !== "string" || !entry.path || entry.path.startsWith("/") || entry.path.includes("\\") ||
114
+ entry.path.split("/").some(part => !part || part === "." || part === "..") ||
115
+ (entry.mode !== "100644" && entry.mode !== "100755" && entry.mode !== null) ||
116
+ (entry.oid !== null && (typeof entry.oid !== "string" || !/^[a-f0-9]{40,64}$/.test(entry.oid))) ||
117
+ ((entry.mode === null) !== (entry.oid === null))) throw new Error("invalid consolidation file identity");
118
+ return entry.path;
119
+ });
120
+ if (new Set(paths).size !== paths.length || JSON.stringify(paths) !== JSON.stringify([...paths].sort()))
121
+ throw new Error("consolidation manifest is not canonical");
122
+ return raw as unknown as ConsolidationReceipt;
123
+ }
124
+
125
+ async function runEvidence(root: string, identity: DeliveryRun) {
126
+ const settled = readSettledTaskEvidence(root, identity.task_id);
127
+ if (!settled || settled.runId !== identity.run_id || settled.proof.terminal_lifecycle !== "done")
128
+ throw new Error(`consolidation requires the exact settled done run: ${identity.task_id}`);
129
+ const record = settled.record;
130
+ if (record.contract !== "assurance_kernel/task_record/v4") throw new Error("consolidation requires v4 immutable Git delivery records");
131
+ const pair = readAuditTaskPair(root, identity.task_id, identity.run_id);
132
+ if (!pair || JSON.stringify(pair.record) !== JSON.stringify(settled.record) || JSON.stringify(pair.proof) !== JSON.stringify(settled.proof))
133
+ throw new Error(`consolidation source audit mismatch: ${identity.task_id}`);
134
+ const current = readTaskIntent(root, identity.task_id, settled.record.intent_ref.path);
135
+ if (current.content_hash !== settled.record.intent_ref.content_hash) throw new Error("consolidation Intent changed");
136
+ const status = await projectTaskStatus(root, identity.task_id);
137
+ if (status.error || status.projection.run_id !== identity.run_id || !status.projection.completion_ready)
138
+ throw new Error(`consolidation run has no completion proof: ${identity.task_id}`);
139
+ const review = [...record.attestations].reverse().find(a => a.kind === "review")?.review_revision ?? null;
140
+ if (review) {
141
+ if (git(root, ["rev-parse", `${review.review_commit}^{tree}`]).trim() !== review.review_tree ||
142
+ git(root, ["rev-parse", `${review.review_commit}^@`]).trim() !== review.base_head)
143
+ throw new Error("consolidation immutable Review revision mismatch");
144
+ }
145
+ return { settled: { ...settled, record }, binding: { task_id: identity.task_id, run_id: identity.run_id,
146
+ record_hash: revisionForContent(readSecureProjectFile(root, auditRunRecordPath(identity.task_id, identity.run_id))),
147
+ proof_hash: revisionForContent(readSecureProjectFile(root, auditRunTerminalProofPath(identity.task_id, identity.run_id))),
148
+ intent_hash: current.content_hash, review_revision: review } };
149
+ }
150
+
151
+ /** Read-only preparation: no index, association, receipt, or Git ref writes before the native gate. */
152
+ export async function prepareDeliveryConsolidation(root: string, initiativeSlug: string, planDigest: string,
153
+ parentTaskIds: string[], selection: DeliveryConsolidation, commands: string[]): Promise<ConsolidationEnvelope> {
154
+ selection = parseDeliveryConsolidation(selection)!;
155
+ const all = [selection, ...selection.source_runs];
156
+ if (!selection.source_runs.some(source => parentTaskIds.includes(source.task_id)))
157
+ throw new Error("consolidation must select an original Parent contribution");
158
+ const sources = [];
159
+ for (const identity of all) sources.push(await runEvidence(root, identity));
160
+ const final = sources[0]!.settled.record;
161
+ if (final.intent_snapshot.risk === "routine" || !sources[0]!.binding.review_revision || !final.git_base_head)
162
+ throw new Error("final consolidation requires fresh QA and independent material/critical Review");
163
+ const snapshot = captureGitTaskRevisionSnapshot(root, final.intent_snapshot.scope_hint, final.git_base_head, selection.task_id);
164
+ const diffHash = consolidationDigest(snapshot);
165
+ if (!final.attestations.some(a => a.kind === "qa" && a.diff_hash === diffHash && a.task_revision === final.intent_snapshot.revision &&
166
+ a.intent_content_hash === final.intent_ref.content_hash && a.acceptance_results.length === final.intent_snapshot.acceptance.length &&
167
+ a.acceptance_results.every(r => r.status === "passed")) ||
168
+ !final.attestations.some(a => a.kind === "review" && a.diff_hash === diffHash && a.review_revision &&
169
+ a.task_revision === final.intent_snapshot.revision && a.intent_content_hash === final.intent_ref.content_hash))
170
+ throw new Error("final composed bytes do not match fresh QA/Review");
171
+ const finalScope = final.intent_snapshot.scope_hint;
172
+ for (const source of sources.slice(1)) {
173
+ const record = source.settled.record;
174
+ if (!record.git_base_head) throw new Error("source run has no Git delivery base");
175
+ const review = source.binding.review_revision;
176
+ if (!review) throw new Error("source run has no immutable Review delivery");
177
+ for (const path of git(root, ["diff", "--name-only", "--no-renames", "-z", record.git_base_head, review.review_commit]).split("\0").filter(Boolean))
178
+ if (!finalScope.some(scope => pathMatchesScope(path, scope))) throw new Error(`source contribution is not covered by final Review scope: ${path}`);
179
+ }
180
+ const auditPaths = new Set(all.flatMap(identity => [auditRunRecordPath(identity.task_id, identity.run_id), auditRunTerminalProofPath(identity.task_id, identity.run_id)]));
181
+ const dirty = [...new Set([...git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD"]).split("\0"),
182
+ ...git(root, ["ls-files", "--others", "--exclude-standard", "-z"]).split("\0")].filter(Boolean))].sort();
183
+ const unstaged = git(root, ["diff", "--name-only", "-z"]).split("\0").filter(Boolean);
184
+ if (unstaged.some(path => !auditPaths.has(path))) throw new Error("consolidation has unstaged delivery drift");
185
+ const manifest = dirty.map(path => {
186
+ if (!finalScope.some(scope => pathMatchesScope(path, scope)) && !auditPaths.has(path)) throw new Error(`dirty_outside_scope: ${path}`);
187
+ if (!existsSync(join(root, path))) return { path, mode: null, oid: null };
188
+ const stat = lstatSync(join(root, path));
189
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("consolidation only accepts regular delivery files");
190
+ const oid = git(root, ["hash-object", "--", path]).trim();
191
+ const staged = git(root, ["ls-files", "--stage", "--", path]);
192
+ const mode = staged ? staged.split(" ")[0]! : stat.mode & 0o111 ? "100755" : "100644";
193
+ return { path, mode, oid };
194
+ });
195
+ const parentTasks = parentTaskIds.map(task_id => ({ task_id, intent_hash: readTaskIntent(root, task_id).content_hash }));
196
+ return { initiative_slug: initiativeSlug, plan_digest: planDigest, selection, parent_tasks: parentTasks,
197
+ branch: git(root, ["symbolic-ref", "--short", "HEAD"]).trim(), head: git(root, ["rev-parse", "HEAD"]).trim(),
198
+ final_commands: commands, runs: sources.map(source => source.binding), delivery_diff_hash: diffHash, manifest };
199
+ }
200
+
201
+ /** Verify committed content against the accepted manifest, not against a historical source bundle. */
202
+ export async function verifyConsolidatedCommit(root: string, receipt: ConsolidationReceipt): Promise<void> {
203
+ parseConsolidationReceipt(receipt);
204
+ const { envelope: e, commit } = receipt;
205
+ if (!commit) throw new Error("consolidation commit receipt is pending");
206
+ // Recovery must preserve the whole checkout, not just the reviewed scope snapshot.
207
+ if (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]).length)
208
+ throw new Error("consolidation recovery requires a clean workspace");
209
+ if (git(root, ["ls-files", "-v", "-z"]).split("\0").some(entry => entry && !entry.startsWith("H ")))
210
+ throw new Error("consolidation recovery rejects hidden index flags");
211
+ const message = git(root, ["show", "-s", "--format=%B", commit]);
212
+ if (!message.startsWith(`imm(${e.selection.task_id}): `) ||
213
+ !message.split("\n").includes(`Immune-Brain-Task: ${e.selection.task_id}`))
214
+ throw new Error("consolidation commit lacks the selected final task identity");
215
+ if (git(root, ["rev-parse", `${commit}^@`]).trim() !== e.head ||
216
+ git(root, ["symbolic-ref", "--short", "HEAD"]).trim() !== e.branch ||
217
+ spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", commit, "HEAD"], { stdio: "ignore" }).status !== 0)
218
+ throw new Error("consolidation commit lineage mismatch");
219
+ const paths = git(root, ["diff-tree", "--no-commit-id", "--no-renames", "--name-only", "-r", "-z", e.head, commit]).split("\0").filter(Boolean).sort();
220
+ if (JSON.stringify(paths) !== JSON.stringify(e.manifest.map(entry => entry.path))) throw new Error("consolidation commit manifest differs");
221
+ for (const entry of e.manifest) {
222
+ const tree = git(root, ["ls-tree", "-z", commit, "--", entry.path]);
223
+ const identity = tree ? tree.split("\t")[0]!.split(" ") : null;
224
+ if ((identity?.[0] ?? null) !== entry.mode || (identity?.[2] ?? null) !== entry.oid)
225
+ throw new Error("consolidation committed bytes/mode mismatch");
226
+ }
227
+ for (const identity of e.runs) {
228
+ const current = (await runEvidence(root, identity)).binding;
229
+ if (JSON.stringify(current) !== JSON.stringify(identity)) throw new Error("consolidation source proof changed");
230
+ for (const path of [auditRunRecordPath(identity.task_id, identity.run_id), auditRunTerminalProofPath(identity.task_id, identity.run_id)])
231
+ if (git(root, ["show", `${commit}:${path}`]) !== readSecureProjectFile(root, path)) throw new Error("committed source audit mismatch");
232
+ }
233
+ for (const parent of e.parent_tasks)
234
+ if (readTaskIntent(root, parent.task_id).content_hash !== parent.intent_hash) throw new Error("consolidated Parent Intent changed");
235
+ const final = readSettledTaskEvidence(root, e.selection.task_id)!.record;
236
+ if (final.contract !== "assurance_kernel/task_record/v4") throw new Error("final consolidation record changed contract");
237
+ const live = captureGitTaskRevisionSnapshot(root, final.intent_snapshot.scope_hint, final.git_base_head, e.selection.task_id);
238
+ if (consolidationDigest(live) !== e.delivery_diff_hash) throw new Error("consolidated delivery changed after commit");
239
+ }
240
+
241
+ export async function observeConsolidatedDeliveries(root: string, slug: string, requiredIds: readonly string[]) {
242
+ const receipt = readParentConsolidationReceipt(root, slug);
243
+ if (!receipt || !receipt.commit) return null;
244
+ if (JSON.stringify(receipt.envelope.parent_tasks.map(task => task.task_id).sort()) !== JSON.stringify([...requiredIds].sort()))
245
+ throw new Error("consolidation Parent contribution set changed");
246
+ await verifyConsolidatedCommit(root, receipt);
247
+ return receipt;
248
+ }
@@ -62,9 +62,15 @@ function enrollmentBaselinePath(root: string): string {
62
62
  return join(root, ".imm/state/enrollment-baseline.json");
63
63
  }
64
64
 
65
- export function writeEnrollmentBaseline(root: string): void {
65
+ /**
66
+ * Records the dirt that predates the task. `claimedPaths` are the in-scope
67
+ * paths the confirmed Enrollment gate listed: they are left out, so later
68
+ * snapshots treat them as task work instead of withholding them.
69
+ */
70
+ export function writeEnrollmentBaseline(root: string, claimedPaths: readonly string[] = []): void {
66
71
  const snapshot = captureGitWorkspaceSnapshot(root);
67
72
  if (!snapshot) return;
73
+ for (const path of claimedPaths) delete snapshot.dirty_files[path];
68
74
  const path = enrollmentBaselinePath(root);
69
75
  mkdirSync(dirname(path), { recursive: true });
70
76
  writeFileSync(path, `${JSON.stringify(snapshot)}\n`);
@@ -220,13 +226,14 @@ export function setGitTaskSnapshotTestHook(hook: () => void): () => void {
220
226
  };
221
227
  }
222
228
 
223
- function gitBytes(root: string, args: string[]): Buffer {
229
+ function gitBytes(root: string, args: string[], env?: NodeJS.ProcessEnv): Buffer {
224
230
  const diff = args[0] === "diff";
225
231
  const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
226
232
  const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
227
233
  encoding: null,
228
234
  stdio: ["ignore", "pipe", "pipe"],
229
235
  maxBuffer: 8 * 1024 * 1024,
236
+ ...(env ? { env } : {}),
230
237
  });
231
238
  if ((result.status !== 0 && !(diff && result.status === 1)) || !Buffer.isBuffer(result.stdout)) {
232
239
  throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
@@ -282,7 +289,7 @@ function decodeIndexFlaggedPaths(bytes: Buffer, label: string): Array<{ path: st
282
289
  const record = bytes.subarray(start, index);
283
290
  if (record.length < 3 || record[1] !== 0x20) throw new Error(`${label} contains a malformed entry`);
284
291
  const tag = String.fromCharCode(record[0]);
285
- if (tag === "h" || tag === "S") {
292
+ if (tag === "h" || tag === "S" || tag === "s") {
286
293
  flagged.push({
287
294
  path: decodeCanonicalGitPath(record.subarray(2), label),
288
295
  flag: tag === "h" ? "assume-unchanged" : "skip-worktree",
@@ -394,9 +401,10 @@ function taskPathMatchesScope(path: string, scope: string[]): boolean {
394
401
 
395
402
  /**
396
403
  * 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.
404
+ * task and was not listed by the confirmed Enrollment gate. Dropping it silently
405
+ * let QA attest workspace bytes that the Review revision did not carry, so it is
406
+ * a hard stop instead: reverting and reapplying yields the same bytes, and only
407
+ * a fresh Enrollment that lists the path, or starts from a clean scope, recovers.
400
408
  * The task's own planning sidecar is exempt, as it is in dirtyScopePaths:
401
409
  * Enrollment requires it tracked, so it is routinely staged in the baseline.
402
410
  */
@@ -411,14 +419,15 @@ function assertNoPreEnrollmentScopeChanges(root: string, scopedStagedPaths: read
411
419
  .sort(comparePaths);
412
420
  if (withheld.length > 0)
413
421
  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`,
422
+ `task scope contains staged changes that predate Enrollment and cannot become task work: ${withheld.join(", ")}; stop the task and enroll again so the Enrollment gate lists them, or from a clean scope`,
415
423
  );
416
424
  }
417
425
 
418
426
  /**
419
427
  * 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.
428
+ * HEAD. The Enrollment gate lists them; the ones it listed are claimed as task
429
+ * work, and any other one refuses Enrollment, because the baseline would record
430
+ * it as user dirt and the task could never deliver it.
422
431
  */
423
432
  export function dirtyScopePaths(projectRoot: string, scopeHint: unknown, taskId?: string): string[] {
424
433
  const root = realpathSync(resolve(projectRoot));
@@ -430,7 +439,7 @@ export function dirtyScopePaths(projectRoot: string, scopeHint: unknown, taskId?
430
439
  );
431
440
  }
432
441
 
433
- function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTaskSnapshot {
442
+ function taskSnapshotOnce(root: string, scope: string[], taskId?: string, tolerated?: string[]): GitTaskSnapshot {
434
443
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
435
444
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
436
445
  if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
@@ -462,8 +471,10 @@ function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTa
462
471
  const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])]
463
472
  .filter((path) => taskPathMatchesScope(path, scope))
464
473
  .sort(comparePaths);
465
- if (uncommittedInScope.length > 0)
466
- throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
474
+ if (uncommittedInScope.length > 0) {
475
+ if (!tolerated) throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
476
+ tolerated.splice(0, tolerated.length, ...uncommittedInScope);
477
+ }
467
478
 
468
479
  const taskPaths = [...new Set(stagedPaths)]
469
480
  .filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
@@ -495,6 +506,7 @@ export function captureGitTaskSnapshot(
495
506
  projectRoot: string,
496
507
  scopeHint: unknown,
497
508
  taskId?: string,
509
+ tolerated?: string[],
498
510
  ): GitTaskSnapshot {
499
511
  const requestedRoot = resolve(projectRoot);
500
512
  const requestedStat = lstatSync(requestedRoot);
@@ -502,9 +514,9 @@ export function captureGitTaskSnapshot(
502
514
  throw new Error("task snapshot root must be a real directory");
503
515
  const root = realpathSync(requestedRoot);
504
516
  const scope = assertCanonicalTaskScope(scopeHint);
505
- const before = taskSnapshotOnce(root, scope, taskId);
517
+ const before = taskSnapshotOnce(root, scope, taskId, tolerated);
506
518
  gitTaskSnapshotTestHook?.();
507
- const after = taskSnapshotOnce(root, scope, taskId);
519
+ const after = taskSnapshotOnce(root, scope, taskId, tolerated);
508
520
  if (JSON.stringify(after) !== JSON.stringify(before))
509
521
  throw new Error("Git task snapshot changed while being captured");
510
522
  return before;
@@ -561,6 +573,7 @@ function taskRevisionSnapshotOnce(
561
573
  scope: string[],
562
574
  baseHead: string,
563
575
  taskId?: string,
576
+ tolerated?: string[],
564
577
  ): GitTaskRevisionSnapshot {
565
578
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
566
579
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
@@ -606,8 +619,10 @@ function taskRevisionSnapshotOnce(
606
619
  "Git task revision paths",
607
620
  );
608
621
  const drift = [...new Set([...scopedUnstagedPaths, ...scopedUntrackedPaths])].sort(comparePaths);
609
- if (drift.length > 0)
610
- throw new Error(`task scope contains unstaged or untracked changes: ${drift.join(", ")}`);
622
+ if (drift.length > 0) {
623
+ if (!tolerated) throw new Error(`task scope contains unstaged or untracked changes: ${drift.join(", ")}`);
624
+ tolerated.splice(0, tolerated.length, ...drift);
625
+ }
611
626
 
612
627
  const changed = [...new Set(scopedStagedPaths)].sort(comparePaths);
613
628
  const changedPaths: Record<string, GitTaskIndexEntry> = {};
@@ -640,6 +655,7 @@ export function captureGitTaskRevisionSnapshot(
640
655
  scopeHint: unknown,
641
656
  baseHead: unknown,
642
657
  taskId?: string,
658
+ tolerated?: string[],
643
659
  ): GitTaskRevisionSnapshot {
644
660
  const requestedRoot = resolve(projectRoot);
645
661
  const requestedStat = lstatSync(requestedRoot);
@@ -650,9 +666,9 @@ export function captureGitTaskRevisionSnapshot(
650
666
  throw new Error("task revision base must be a Git commit id");
651
667
  const scope = assertCanonicalTaskScope(scopeHint);
652
668
  const normalizedBase = baseHead.toLowerCase();
653
- const before = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
669
+ const before = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId, tolerated);
654
670
  gitTaskSnapshotTestHook?.();
655
- const after = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
671
+ const after = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId, tolerated);
656
672
  if (JSON.stringify(after) !== JSON.stringify(before))
657
673
  throw new Error("Git task revision changed while being captured");
658
674
  return before;
@@ -705,6 +721,36 @@ export function taskDeliveryIdentity(
705
721
  return taskDiffIdentity(projectRoot, record.intent_snapshot.scope_hint, record.task_id);
706
722
  }
707
723
 
724
+ /**
725
+ * The delivery identity for a read-only status projection. Unstaged or
726
+ * untracked in-scope paths do not refuse it: the identity is the staged one and
727
+ * the paths are returned for a warning. Nothing may be frozen from this value;
728
+ * every snapshot that is frozen goes through `taskDeliveryIdentity`, which
729
+ * still refuses those paths.
730
+ */
731
+ export function taskStatusIdentity(
732
+ projectRoot: string,
733
+ record: TaskDeliveryRecord,
734
+ ): GitTaskDiffIdentity & { unstaged_scope_paths: string[] } {
735
+ const unstaged: string[] = [];
736
+ const scope = record.intent_snapshot.scope_hint;
737
+ if (record.contract === "assurance_kernel/task_record/v4") {
738
+ if (!record.git_base_head) throw new Error("TaskRecord v4 is missing git_base_head");
739
+ const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scope, record.git_base_head, record.task_id, unstaged);
740
+ return {
741
+ diff_hash: hashTaskSnapshot(snapshot),
742
+ changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths),
743
+ unstaged_scope_paths: unstaged,
744
+ };
745
+ }
746
+ const snapshot = captureGitTaskSnapshot(projectRoot, scope, record.task_id, unstaged);
747
+ return {
748
+ diff_hash: hashTaskSnapshot(snapshot),
749
+ changed_paths: Object.keys(snapshot.staged_files).sort(comparePaths),
750
+ unstaged_scope_paths: unstaged,
751
+ };
752
+ }
753
+
708
754
  /** The same v4 identity for an immutable delivered commit, without touching the index. */
709
755
  export function taskCommitRevisionIdentity(projectRoot: string, scopeHint: unknown, baseHead: string, commit: string): GitTaskDiffIdentity {
710
756
  const requested = resolve(projectRoot);
@@ -989,3 +1035,65 @@ export function assertChangedFilesWithinScope(
989
1035
  );
990
1036
  }
991
1037
  }
1038
+
1039
+ function gitBlobId(body: Buffer, length: number): string {
1040
+ const hash = createHash(length === 64 ? "sha256" : "sha1");
1041
+ hash.update(`blob ${body.length}\0`);
1042
+ hash.update(body);
1043
+ return hash.digest("hex");
1044
+ }
1045
+
1046
+ /**
1047
+ * In-scope tracked paths whose working-directory content differs from `tree`.
1048
+ * Bytes are compared directly, never through the index stat cache, so a file a
1049
+ * command rewrote with identical content is not a difference. Paths outside the
1050
+ * scope are the user's environment and are not read.
1051
+ */
1052
+ // A hosting process that carries one of these would compare another repository
1053
+ // or index instead of the task's working directory.
1054
+ const GIT_REPOSITORY_BINDINGS = [
1055
+ "GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY",
1056
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_COMMON_DIR", "GIT_NAMESPACE", "GIT_PREFIX",
1057
+ ];
1058
+
1059
+ export function inScopeTrackedDrift(projectRoot: string, tree: string, scopeHint: unknown): string[] {
1060
+ const root = resolve(projectRoot);
1061
+ const env = { ...process.env };
1062
+ for (const name of GIT_REPOSITORY_BINDINGS) delete env[name];
1063
+ if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(tree)) throw new Error("frozen tree has invalid identity");
1064
+ const scope = Array.isArray(scopeHint) ? scopeHint.filter((entry): entry is string => typeof entry === "string") : [];
1065
+ const inScope = (path: string) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope);
1066
+ const expected = new Map<string, { mode: string; oid: string }>();
1067
+ for (const entry of gitBytes(root, ["ls-tree", "-r", "-z", tree], env).toString("utf8").split("\0")) {
1068
+ const tab = entry.indexOf("\t");
1069
+ if (tab < 0) continue;
1070
+ const [mode, , oid] = entry.slice(0, tab).split(" ");
1071
+ const path = entry.slice(tab + 1);
1072
+ if (mode && oid && inScope(path)) expected.set(path, { mode, oid });
1073
+ }
1074
+ const tracked = splitNull(gitBytes(root, ["ls-files", "-z"], env).toString("utf8")).filter(inScope);
1075
+ const regular: string[] = [];
1076
+ const drift = new Set<string>();
1077
+ for (const path of new Set([...expected.keys(), ...tracked])) {
1078
+ const want = expected.get(path);
1079
+ let stat;
1080
+ try { stat = lstatSync(join(root, path)); } catch { stat = null; }
1081
+ if (!want || !stat) { if (want || stat) drift.add(path); continue; }
1082
+ // A submodule is pinned by the index, not by working-directory bytes.
1083
+ if (want.mode === "160000") continue;
1084
+ if (stat.isSymbolicLink()) {
1085
+ if (want.mode !== "120000" || gitBlobId(Buffer.from(readlinkSync(join(root, path))), want.oid.length) !== want.oid) drift.add(path);
1086
+ } else if (!stat.isFile() || want.mode === "120000" || (want.mode === "100755") !== ((stat.mode & 0o100) !== 0)) drift.add(path);
1087
+ else regular.push(path);
1088
+ }
1089
+ if (regular.length) {
1090
+ // `--stdin-paths` applies the same clean filters as `git add` would.
1091
+ const hashed = spawnSync("git", ["--no-optional-locks", "-C", root, "hash-object", "--stdin-paths"], {
1092
+ input: `${regular.join("\n")}\n`, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 8 * 1024 * 1024, env,
1093
+ });
1094
+ const ids = hashed.status === 0 ? hashed.stdout.split("\n").filter(Boolean) : [];
1095
+ if (ids.length !== regular.length) throw new Error("Git task snapshot command failed: git hash-object");
1096
+ regular.forEach((path, index) => { if (ids[index] !== expected.get(path)!.oid) drift.add(path); });
1097
+ }
1098
+ return [...drift].sort(comparePaths);
1099
+ }
@@ -15,6 +15,12 @@
15
15
  their own existing grant. An explicit approval already covering the same
16
16
  operation, target, and impact — including a valid batch capability — stays
17
17
  usable without another gate.
18
+ - A precondition commit is the one commit the runtime makes without a separate
19
+ grant. When an `imm-run` precondition would fail only because paths are
20
+ uncommitted, the runtime commits that precondition's committable set, an
21
+ explicit path list, silently and continues. It runs Git hooks normally and
22
+ grants no push, publication, history rewrite, or commit of any other path. A
23
+ model never makes this commit itself and never asks the user to make it.
18
24
  - Before a new outbound effect, check the project's existing research channel and
19
25
  data scope. A public article, a mock, a documentation example, or a local
20
26
  read-only database gives no authority to call an arbitrary provider or send new
@@ -55,11 +61,19 @@ framing does not load that execution guidance.
55
61
 
56
62
  Ordinary host input stays host-native and does not run natural-language Managed
57
63
  routing. A new Managed workflow starts only from explicit `imm-brainstorm`,
58
- `imm-planner`, or `imm-run` Skill entry.
64
+ `imm-planner`, or `imm-run` Skill entry. For `imm-planner` and `imm-brainstorm`
65
+ the user's request for that planning or clarification, by slash command or in
66
+ natural language, is such an entry; a request that asks for neither stays
67
+ host-native. `imm-run` starts only from a literal-user entry. A model-entered
68
+ Planner or Brainstorm leaves the session's run entry unchanged.
59
69
 
60
70
  1. **Continue an existing Managed owner explicitly**: an active Assurance
61
71
  projection, TaskIntent, TaskRecord, reviewer `follow_up`, or other nonterminal
62
72
  Managed owner remains authoritative; the user resumes it with `imm-run`.
73
+ Once that entry binds this session, ordinary status questions and in-scope
74
+ clarification retain automatic response continuation. Explicit cancel, planning
75
+ entry or session replacement releases the binding; Host continuation metadata
76
+ remains non-authoritative.
63
77
  2. **Start explicitly**: the selected Immune-Brain Skill owns its planning or
64
78
  coordination work. It creates only requested artifacts and their required
65
79
  parent directories. Explicit Planner entry also owns absent routing-policy
@@ -112,6 +126,8 @@ Routine Managed enrollment is the first step of an explicit `imm-run` entry and
112
126
 
113
127
  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.
114
128
 
129
+ Before Enrollment prepares its binding, the Host commits the Enrollment committable set when an untracked TaskIntent would otherwise fail it. The set is the task's own TaskIntent and the Spec its binding names. In-scope changes that already exist are not committed: the Enrollment gate lists them and only the listed paths become task work on confirmation, so a dirty scope is never claimed without that list being shown. The commit is silent, needs no separate confirmation, and the native confirmation binds the HEAD that results. When no precondition fails, no commit is made. Dirty paths outside the set are left as they are and do not block Enrollment. With an unborn HEAD the empty root commit comes first and this commit follows it. A commit Git refuses fails Enrollment with Git's message and is never retried with hooks bypassed; if Enrollment is declined, cancelled or fails afterwards, report the remaining commit.
130
+
115
131
  ## Parallel Read-Only Dispatch
116
132
 
117
133
  State mutations, step activations, QA decisions, and plan switches remain
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: imm-brainstorm
3
- description: Use when the user explicitly requests Immune-Brain requirement clarification.
3
+ description: Use when the user asks to clarify or explore requirements before Immune-Brain planning.
4
4
  ---
5
5
 
6
6
  # Immune-Brain: Brainstorm
7
7
 
8
8
  Use [`../../dist/imm-brainstorm.md`](../../dist/imm-brainstorm.md) as the
9
- canonical contract index, not a whole-document read. Explicit entry only: ordinary host questions do not
10
- start this workflow.
9
+ canonical contract index, not a whole-document read. Starts on the user's request
10
+ to clarify or explore requirements, by slash command or in natural language;
11
+ ordinary host questions do not start this workflow.
11
12
 
12
13
  Mandatory constraints before any action: Brainstorm is read-only — no code,
13
14
  test, or runtime edits; no Spec, Plan, or workflow-state writes. All modes
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  name: imm-planner
3
- description: Use when the user explicitly requests Immune-Brain Spec and TaskIntent planning.
3
+ description: Use when the user asks for an Immune-Brain Spec, Plan, or TaskIntent for a repository change.
4
4
  ---
5
5
 
6
6
  # Immune-Brain: Planner
7
7
 
8
8
  Use [`../../dist/imm-planner.md`](../../dist/imm-planner.md) as the canonical contract
9
- index, not a whole-document read. Explicit entry only: ordinary host requests stay
9
+ index, not a whole-document read. Starts on the user's request for a Spec, Plan
10
+ or TaskIntent, by slash command or in natural language; other host requests stay
10
11
  host-native.
11
12
 
12
13
  Mandatory constraints before any action: Planner writes a TaskIntent, and a Spec
@@ -42,6 +43,11 @@ own routes instead of these stages.
42
43
  - retirement design: [Retirement Completion Contract](../../dist/imm-planner.md#retirement-completion-contract)
43
44
  - optional research dispatch: [Research Dispatch](../../dist/imm-planner.md#research-dispatch)
44
45
 
46
+ A Spec with external input declares its threat model and supported environment,
47
+ acceptance enumerates concrete negative cases rather than absolute
48
+ any-malformed-input clauses, and descriptors name focused test files, never whole
49
+ suites.
50
+
45
51
  Every request stops after candidate TaskIntent validation, plus Spec validation when the work is complex; Plan-only requests stop there too. The Planner
46
52
  never invokes the current Host's native Enrollment gate, even when the request also asks for
47
53
  execution: report the candidate and name a literal-user `imm-run` entry as the
@@ -7,12 +7,16 @@ description: Use when the user explicitly requests execution or resumption of an
7
7
 
8
8
  Use [`../../dist/imm-run.md`](../../dist/imm-run.md) as the canonical contract
9
9
  index, not a whole-document read, in the current host conversation. Explicit entry only:
10
- ordinary host input never resumes a Managed owner implicitly.
10
+ ordinary host input never creates a Managed execution binding. Status questions
11
+ and in-scope clarification retain an already-bound run.
11
12
 
12
13
  Mandatory constraints before any action: verify the active backend claim,
13
14
  TaskIntent, and TaskRecord via `imm_kernel_canary` `status` first; invalid or
14
- contradictory projections fail closed. With no active claim, the first step
15
- is the native Enrollment gate for the validated candidate, invoked once. Resume only from those authority facts:
15
+ contradictory projections fail closed. A Parent Issue selects its whole planned
16
+ batch; load the batch route and open native Batch Authorization directly. A Child
17
+ Issue selects only its TaskIntent, verifies predecessor settlement and integration,
18
+ then opens single-task Enrollment. With no active claim, a standalone validated
19
+ candidate opens native Enrollment once. Resume only from those authority facts:
16
20
  a compaction heading, HANDOFF summary, or Issue state is prose, never authority.
17
21
  All Managed authority gates use the
18
22
  current Host's native interaction. A failed gate stays fail-closed and
@@ -25,11 +29,11 @@ references load only under their own condition. Never read the whole contract
25
29
  or all references as an entry prerequisite.
26
30
 
27
31
  - 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-run.md#kernel-canary-routing-and-authority)
28
- - unattended batch run, or any question about whether `imm-run` starts one: [Unattended Batch Opt-In](../../dist/imm-run.md#unattended-batch-opt-in)
29
- - parallel batch (`max_parallel`, `lane_offers`, Lane admission or integration): [Parallel Batch Opt-In](../../dist/imm-run.md#parallel-batch-opt-in)
30
- - a lane-mode report carries an `executor` handoff, or a Lane's Executor Host session has ended: [Lane Executor Supervision](../../dist/imm-run.md#lane-executor-supervision)
31
- - that Parent runs inside Herdr (`HERDR_ENV=1`): [Herdr Lane Tabs](../../dist/imm-run.md#herdr-lane-tabs)
32
- - a lane-mode batch is about to start, to resolve the repository's parallelism, Executor Host, model and effort: [Lane Preferences](../../dist/imm-run.md#lane-preferences)
32
+ - Parent Issue, unattended batch run, or any question about whether `imm-run` starts one: [Unattended Batch Opt-In](../../dist/imm-run-batch.md#unattended-batch-opt-in)
33
+ - parallel batch (`max_parallel`, `lane_offers`, Lane admission or integration): [Parallel Batch Opt-In](../../dist/imm-run-batch.md#parallel-batch-opt-in)
34
+ - a lane-mode report carries an `executor` handoff, or a Lane's Executor Host session has ended: [Lane Executor Supervision](../../dist/imm-run-batch.md#lane-executor-supervision)
35
+ - that Parent runs inside Herdr (`HERDR_ENV=1`): [Herdr Lane Tabs](../../dist/imm-run-batch.md#herdr-lane-tabs)
36
+ - a lane-mode batch is about to start, to resolve the repository's parallelism, Executor Host, model and effort: [Lane Preferences](../../dist/imm-run-batch.md#lane-preferences)
33
37
  - steady execution: [Verification and Local Recovery](../../dist/BASELINE.md#verification-and-local-recovery), [Execution Loop](../../dist/imm-run.md#execution-loop), [Observable Output](../../dist/imm-run.md#observable-output)
34
38
  - rework, scope expansion, breaking revision, user decision, stop, interruption or unknown state before any action: [Decisions and Recovery](../../dist/imm-run.md#decisions-and-recovery), [Failure Output](../../dist/imm-run.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.
35
39
  - review or post-settlement learning: [Review and Learning](../../dist/imm-run.md#review-and-learning)