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,161 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { realpathSync } from "node:fs";
3
+ import { projectTaskStatus } from "../runtime/assurance/coordinator";
4
+ import { readTaskRecordRaw } from "../runtime/kernel/storage";
5
+ import type { PhaseEntry } from "../runtime/assurance/phase_entry";
6
+ import { recordParentRepair } from "../runtime/unattended/batch_git";
7
+ import { readAnyBatchRunState } from "../runtime/unattended/batch_state";
8
+ import { bindRun, decideRunContinuation, observeParentRun, parkFailedCriticalGate, releasesRunBinding, scopedSourceDigest,
9
+ type RunContinuationState, type ReviewContinuationState } from "../runtime/assurance/run_continuation";
10
+
11
+ type SettleBoundary = {
12
+ outcome: "completed" | "aborted" | "error";
13
+ continue: boolean;
14
+ context: { canContinue: boolean; pendingMessages: unknown[] };
15
+ };
16
+ type ContinuationBoundaryResult = { continue?: boolean; entries?: Array<{ type: "custom_message"; customType: string; display: boolean; content: string }> } | void;
17
+
18
+ /** Continuation is session memory, never a capability or durable task owner. */
19
+ export function registerPiRunContinuation(pi: ExtensionAPI, ports: {
20
+ entry(text: string, root: string): PhaseEntry | null;
21
+ }): { selectedParent(root: string, sessionId: string, slug: string): boolean } {
22
+ let entered = false;
23
+ let state: RunContinuationState | undefined;
24
+ let hostFailure: string | undefined;
25
+ const failedGates = new Set<string>();
26
+ let reviewRecovery: string | undefined;
27
+ let review: ReviewContinuationState = "unobserved";
28
+ let reviewerPrompt: string | undefined;
29
+ let reviewerCallId: string | undefined;
30
+ const calls = new Map<string, { name: string; task_id?: string; op?: string; initiative_slug?: string }>();
31
+ const reset = () => { entered = false; state = undefined; hostFailure = undefined; reviewRecovery = undefined;
32
+ review = "unobserved"; reviewerPrompt = undefined; reviewerCallId = undefined; calls.clear(); };
33
+ pi.on("session_start", () => { failedGates.clear(); reset(); });
34
+ pi.on("session_shutdown", () => { failedGates.clear(); reset(); });
35
+ pi.on("input", (event, ctx) => {
36
+ if (event.source === "extension") return;
37
+ const entry = ports.entry(event.text, ctx.cwd);
38
+ if (releasesRunBinding(event.text, entry === "plan" ? "planner" : entry)) { reset(); return; }
39
+ if (entry === "run") { failedGates.clear(); reset(); entered = true; }
40
+ });
41
+ pi.on("tool_call", (event) => {
42
+ const gateInput = event.input as { task_id?: unknown; initiative_slug?: unknown; action?: { op?: unknown } };
43
+ const gate = `${event.toolName}:${gateInput.task_id ?? gateInput.initiative_slug ?? ""}:${gateInput.action?.op ?? ""}`;
44
+ if (failedGates.has(gate)) return { block: true, reason: "This native gate failed or was cancelled; enter imm-run explicitly before retrying" };
45
+ if (!entered) return;
46
+ if (event.toolName === "imm_canary_enrollment" && state?.binding.kind === "parent" &&
47
+ state.binding.blocked_native_tasks?.includes(String(event.input.task_id)))
48
+ return { block: true, reason: "This critical gate is parked; continue independent work or resolve its same-Host recovery and enter imm-run explicitly" };
49
+ if (event.toolName === "Agent" && reviewerPrompt &&
50
+ (event.input.subagent_type === "Review" || event.input.prompt === reviewerPrompt)) {
51
+ if (event.input.prompt !== reviewerPrompt || event.input.subagent_type !== "Review" || event.input.run_in_background !== false)
52
+ return { block: true, reason: "Kernel Review requires the complete exact agent_params from advance_assurance: preserve prompt bytes, subagent_type Review, and run_in_background false. No reviewer was dispatched." };
53
+ if (reviewerCallId) { review = "rejected"; return { block: true, reason: "Reserved Reviewer already dispatched; wait for its result and submit the verdict before any new dispatch" }; }
54
+ reviewerCallId = event.toolCallId; review = "running"; return;
55
+ }
56
+ if (!["imm_canary_enrollment", "imm_kernel_canary", "start_unattended_batch"].includes(event.toolName)) return;
57
+ const input = event.input as { task_id?: unknown; initiative_slug?: unknown; action?: { op?: unknown } };
58
+ calls.set(event.toolCallId, { name: event.toolName,
59
+ task_id: typeof input.task_id === "string" ? input.task_id : undefined,
60
+ initiative_slug: typeof input.initiative_slug === "string" ? input.initiative_slug : undefined,
61
+ op: typeof input.action?.op === "string" ? input.action.op : undefined });
62
+ });
63
+ pi.on("tool_result", async (event, ctx) => {
64
+ if (entered && event.toolName === "Agent" && event.toolCallId === reviewerCallId) review = "returned";
65
+ const call = calls.get(event.toolCallId); calls.delete(event.toolCallId);
66
+ if (!entered || !call) return;
67
+ if (call.name === "start_unattended_batch") {
68
+ const result = event.details as { state?: string; batch_id?: string | null; plan_digest?: string; final_verification_commands?: string[] } | undefined;
69
+ if (!event.isError && result?.state === "native_parent" && typeof result.plan_digest === "string" && call.initiative_slug) {
70
+ if (!state || state.binding.kind !== "parent") state = bindRun({ kind: "parent", workspace: realpathSync(ctx.cwd),
71
+ session_id: ctx.sessionManager.getSessionId(), initiative_slug: call.initiative_slug, batch_id: null,
72
+ plan_digest: result.plan_digest, final_verification: result.final_verification_commands });
73
+ return;
74
+ }
75
+ if (event.isError || result?.state !== "started" || !result.batch_id || !call.initiative_slug) {
76
+ failedGates.add(`${call.name}:${call.initiative_slug ?? ""}:`);
77
+ hostFailure = "Batch native operation did not apply; inspect the same-Host recovery"; return;
78
+ }
79
+ const record = readAnyBatchRunState(ctx.cwd, result.batch_id);
80
+ if (!record || record.initiative_slug !== call.initiative_slug) { hostFailure = "Batch result has no matching persisted identity"; return; }
81
+ if (!state || state.binding.kind !== "parent") state = bindRun({ kind: "parent", workspace: realpathSync(ctx.cwd),
82
+ session_id: ctx.sessionManager.getSessionId(), initiative_slug: record.initiative_slug, batch_id: record.batch_id, plan_digest: record.plan_digest });
83
+ return;
84
+ }
85
+ if (!call.task_id) return;
86
+ const details = event.details as { state?: unknown; reason?: unknown; recovery_action?: unknown; next_action?: unknown;
87
+ agent_params?: { prompt?: unknown } } | undefined;
88
+ if ((event.isError || ["blocked", "cancelled", "rejected", "failed"].includes(String(details?.state))) &&
89
+ (call.name === "imm_canary_enrollment" || ["request_authorization", "approve_breaking_intent_revision", "request_stop"].includes(call.op ?? ""))) {
90
+ failedGates.add(`${call.name}:${call.task_id}:${call.op ?? ""}`);
91
+ hostFailure = typeof details?.reason === "string" ? details.reason : "Native authority operation failed or was cancelled";
92
+ if (call.name === "imm_canary_enrollment" && state && await parkFailedCriticalGate(state.binding, call.task_id)) hostFailure = undefined;
93
+ return;
94
+ }
95
+ if (call.op === "advance_assurance" && !event.isError && details?.state === "review_ready") {
96
+ if (typeof details.agent_params?.prompt === "string" && details.agent_params.prompt !== reviewerPrompt) {
97
+ reviewerPrompt = details.agent_params.prompt; reviewerCallId = undefined; review = "ready";
98
+ }
99
+ reviewRecovery = undefined;
100
+ }
101
+ if (call.op === "submit_review") {
102
+ if (event.isError) {
103
+ review = "rejected";
104
+ const action = details?.recovery_action ?? details?.next_action;
105
+ reviewRecovery = typeof action === "string" ? action : undefined;
106
+ } else {
107
+ review = "unobserved"; reviewerPrompt = undefined; reviewerCallId = undefined; reviewRecovery = undefined;
108
+ }
109
+ }
110
+ if (call.name === "imm_canary_enrollment" && !event.isError && details?.state === "completed" && state?.binding.kind === "parent")
111
+ await recordParentRepair(ctx.cwd, state.binding.batch_id, call.task_id, state.binding.initiative_slug);
112
+ if (state || event.isError) return;
113
+ const status = await projectTaskStatus(ctx.cwd, call.task_id);
114
+ if (status.error || status.claim?.task_id !== call.task_id || status.projection.lifecycle !== "active" || !status.projection.run_id) return;
115
+ state = bindRun({ kind: "task", workspace: realpathSync(ctx.cwd), session_id: ctx.sessionManager.getSessionId(),
116
+ task_id: call.task_id, run_id: status.projection.run_id });
117
+ });
118
+ // shortcut: optional workspace SDK types predate Pi 1.2; remove this declaration when that peer reaches 1.2 (owner: Immune-Brain).
119
+ const boundaryApi = pi as ExtensionAPI & { on(event: "agent_before_settle", handler: (event: SettleBoundary, ctx: ExtensionContext) => Promise<ContinuationBoundaryResult>): void };
120
+ boundaryApi.on("agent_before_settle", async (event, ctx) => {
121
+ if (!entered || !state) return;
122
+ if (event.outcome !== "completed" || !event.context.canContinue) { reset(); return; }
123
+ if (event.continue || event.context.pendingMessages.length) return;
124
+ try {
125
+ if (state.binding.kind === "parent") {
126
+ const decision = decideRunContinuation(state, { ...await observeParentRun(state.binding,
127
+ ctx.sessionManager.getLeafId() ?? "empty-session", review, reviewRecovery), host_failure: hostFailure });
128
+ state = decision.state;
129
+ if (decision.action === "settled" || decision.action === "unbound") { reset(); return; }
130
+ if (decision.action === "wait") return;
131
+ if (decision.action === "pause") { reset(); return { entries: [{ type: "custom_message", customType: "immune-brain-run-paused", display: true, content: `Parent paused: ${decision.reason}. ${decision.next_action ?? ""}` }] }; }
132
+ return { continue: true, entries: [{ type: "custom_message", customType: "immune-brain-run-continuation", display: false,
133
+ content: `Parent remains incomplete. ${decision.next_action}. Continue the authorized Loop and batch; individual Slice settlement does not complete the Parent.` }] };
134
+ }
135
+ const taskId = state.binding.task_id;
136
+ const status = await projectTaskStatus(ctx.cwd, taskId);
137
+ const record = await readTaskRecordRaw(ctx.cwd, taskId);
138
+ if (!record.record && status.projection.lifecycle === "active") throw new Error("bound run has no live TaskRecord");
139
+ const decision = decideRunContinuation(state, { kind: "task", workspace: realpathSync(ctx.cwd),
140
+ session_id: ctx.sessionManager.getSessionId(), boundary_id: ctx.sessionManager.getLeafId() ?? "empty-session",
141
+ status, source_digest: record.record ? scopedSourceDigest(ctx.cwd, record.record.intent_snapshot.scope_hint) : "terminal",
142
+ review, recovery_action: reviewRecovery, host_failure: hostFailure });
143
+ state = decision.state;
144
+ if (decision.action === "settled" || decision.action === "unbound") { reset(); return; }
145
+ if (decision.action === "wait") return;
146
+ if (decision.action === "pause") {
147
+ reset();
148
+ return { entries: [{ type: "custom_message" as const, customType: "immune-brain-run-paused", display: true,
149
+ content: `Run paused: ${decision.reason}${decision.next_action ? `. Recovery: ${decision.next_action}` : ""}` }] };
150
+ }
151
+ return { continue: true, entries: [{ type: "custom_message" as const, customType: "immune-brain-run-continuation", display: false,
152
+ content: `Explicit imm-run remains bound to task ${taskId}, run ${state.binding.kind === "task" ? state.binding.run_id : ""}. Kernel settlement is incomplete. ${decision.next_action}. Continue the authorized Loop; report completion only after authoritative settlement.` }] };
153
+ } catch (error) {
154
+ reset();
155
+ return { entries: [{ type: "custom_message" as const, customType: "immune-brain-run-paused", display: true,
156
+ content: `Run paused because continuation facts could not be verified: ${error instanceof Error ? error.message : String(error)}` }] };
157
+ }
158
+ });
159
+ return { selectedParent: (root, sessionId, slug) => entered && state?.binding.kind === "parent" &&
160
+ state.binding.workspace === realpathSync(root) && state.binding.session_id === sessionId && state.binding.initiative_slug === slug };
161
+ }
@@ -52,6 +52,27 @@ cite the acceptance whose principle it violates as `acceptance_id` and
52
52
  to be written into the acceptance text; a missing literal clause is not itself a
53
53
  finding and does not call for an Intent revision.
54
54
 
55
+ ## Trigger and Threat Model
56
+
57
+ `evidence.trigger` must read "concrete input or state → observable wrong
58
+ result". A finding that cannot name the wrong result is not blocking. Judge
59
+ against the threat model and supported environment declared in the Spec or
60
+ TaskIntent goal: a defect that needs an attacker, input source, repository
61
+ format, encoding, or platform outside that declaration is advisory. When
62
+ nothing is declared, assume inputs come from the repository's own tooling and
63
+ the user's trusted workspace. `security_boundary` means a security, data-loss,
64
+ or authority boundary the change actually crosses.
65
+
66
+ From Review round 3 the manifest carries `prior_review_commit` and
67
+ `delta_paths`: the paths the latest rework changed. Review that delta and its
68
+ direct interactions. A blocking finding is admitted only when at least one
69
+ `caller_chain` entry names a path in `delta_paths`, or `violated.kind` is
70
+ `security_boundary`; an edge case that already existed in round 1 is advisory
71
+ unless it crosses a security, data-loss, or authority boundary. A verdict that
72
+ blocks outside the delta is rejected for resubmission. When the manifest carries
73
+ `delta_skipped` instead, no delta exists for the round and this rule does not
74
+ apply.
75
+
55
76
  ## Invariant and Evidence Coverage
56
77
 
57
78
  Judge whether the change actually closes the invariant its acceptance names,
@@ -101,7 +122,7 @@ contract: `contract`, `role`, `task_id`, `snapshot_digest`, `decision` (`pass`
101
122
  or `rework`), and for `pass` include `approval` (`kind`, `authority_role`,
102
123
  `summary`), for `rework` include `findings` (`id`, `kind`, `acceptance_id`,
103
124
  `summary`, `evidence`). Every rework finding's `evidence` must carry
104
- `trigger` (the concrete inputs or state that reach the defect), a non-empty
125
+ `trigger` (the concrete input or state that reaches the defect and the observable wrong result it produces), a non-empty
105
126
  `caller_chain` (ordered repository paths or symbols), and `violated`
106
127
  (`kind`: `acceptance` or `security_boundary`, `ref`: the acceptance id or
107
128
  boundary name). Do not invent fields, and never send an anchor yourself: the
@@ -110,7 +131,10 @@ violated.ref, caller_chain}`, so an identical claim keeps one stable identity
110
131
  across review rounds while a different call chain is a different claim. A
111
132
  passing review carries no blocking findings; non-blocking notes may ride along as
112
133
  `kind: "advisory"` findings, and every finding, advisory included, needs the
113
- same machine-checkable evidence. If the
134
+ same machine-checkable evidence. Advisory notes are archived on a pass and never
135
+ become Executor work. A rework carries only `kind: "blocking"` findings, at
136
+ least one: the coordinator rejects a rework with an advisory finding or without
137
+ a blocking one, so submit `pass` with `inspected_paths` when nothing blocks. If the
114
138
  checkpoint is `awaiting_user_successor_decision`, stop without dispatch; only
115
139
  a literal user may invoke `--approve-successor`.
116
140
 
@@ -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