immune-brain 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Immune-Brain agent skill system",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Immune-Brain Claude Code Host: native Enrollment, QA, Review, and Kernel settlement.",
5
5
  "author": {
6
6
  "name": "Immune-Brain Team"
@@ -55,7 +55,11 @@ framing does not load that execution guidance.
55
55
 
56
56
  Ordinary host input stays host-native and does not run natural-language Managed
57
57
  routing. A new Managed workflow starts only from explicit `imm-brainstorm`,
58
- `imm-planner`, or `imm-run` Skill entry.
58
+ `imm-planner`, or `imm-run` Skill entry. For `imm-planner` and `imm-brainstorm`
59
+ the user's request for that planning or clarification, by slash command or in
60
+ natural language, is such an entry; a request that asks for neither stays
61
+ host-native. `imm-run` starts only from a literal-user entry. A model-entered
62
+ Planner outside an enrolled task ends the session's run entry.
59
63
 
60
64
  1. **Continue an existing Managed owner explicitly**: an active Assurance
61
65
  projection, TaskIntent, TaskRecord, reviewer `follow_up`, or other nonterminal
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
39
39
  }
40
40
 
41
41
  // plugins/immune-brain/runtime/plugin_version.ts
42
- var PLUGIN_VERSION = "5.2.0";
42
+ var PLUGIN_VERSION = "5.3.0";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
@@ -95,14 +95,13 @@ function recognizePhaseEntry(text, readAlias) {
95
95
  return null;
96
96
  return ALIAS_TARGETS[[...targets][0]] ?? null;
97
97
  }
98
- var NO_PHASE_ENTRY = { prompt_seq: 0, run_live: false, run_prompt_seq: 0, plan_prompt_seq: 0 };
98
+ var NO_PHASE_ENTRY = { run_live: false };
99
99
  function applyPrompt(state, entry) {
100
- const prompt_seq = state.prompt_seq + 1;
101
100
  if (entry === "run")
102
- return { ...state, prompt_seq, run_live: true, run_prompt_seq: prompt_seq };
101
+ return { run_live: true };
103
102
  if (entry === "plan")
104
- return { prompt_seq, run_live: false, run_prompt_seq: 0, plan_prompt_seq: prompt_seq };
105
- return { ...state, prompt_seq };
103
+ return { run_live: false };
104
+ return state;
106
105
  }
107
106
  function phaseEntryLogLine(entry) {
108
107
  return `${JSON.stringify({ v: 1, entry })}
@@ -138,17 +137,11 @@ function admitPhaseOperation(operation, state, context) {
138
137
  return { ok: true };
139
138
  return refuse(RUN_ENTRY_REQUIRED, `${operation.name} needs a literal-user imm-run entry in this session`);
140
139
  }
141
- const latest = state !== null && state.prompt_seq > 0 ? state.prompt_seq : -1;
142
- if (operation.skill === "imm-run") {
143
- if (state?.run_live && state.run_prompt_seq === latest)
144
- return { ok: true };
145
- return refuse(PHASE_ENTRY_REQUIRED, "imm-run is entered by the user, not by a model Skill call");
146
- }
147
- if (state && state.plan_prompt_seq === latest)
140
+ if (operation.skill !== "imm-run")
148
141
  return { ok: true };
149
- if (operation.skill === "imm-planner" && state?.run_live && context.activeClaim)
142
+ if (state?.run_live)
150
143
  return { ok: true };
151
- return refuse(PHASE_ENTRY_REQUIRED, `${operation.skill} is entered by the user, not by a model Skill call`);
144
+ return refuse(PHASE_ENTRY_REQUIRED, "imm-run is entered by the user, not by a model Skill call");
152
145
  }
153
146
  function phaseRefusalMessage(refusal) {
154
147
  return `${refusal.reason_code}: ${refusal.detail}; recovery: ${refusal.recovery_action}`;
@@ -9229,20 +9222,25 @@ function dropPrivate(path) {
9229
9222
  rmSync5(path, { force: true });
9230
9223
  } catch {}
9231
9224
  }
9225
+ function appendPhaseEntry(sessionId, entry, root, locations) {
9226
+ const path = entryLogPath(sessionId, root);
9227
+ let recorded = false;
9228
+ try {
9229
+ recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry));
9230
+ } catch {}
9231
+ if (!recorded)
9232
+ dropPrivate(path);
9233
+ return recorded;
9234
+ }
9232
9235
  function recordPhaseEntryPrompt(payload, locations = {}) {
9233
9236
  if (payload.hook_event_name !== "UserPromptSubmit")
9234
9237
  return false;
9235
9238
  const sessionId = sessionIdOf(payload);
9236
9239
  if (!sessionId)
9237
9240
  return false;
9238
- const root = locations.root ?? tmpdir2();
9239
9241
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
9240
9242
  const entry = recognizePhaseEntry(prompt, claudeAliasReader(cwdOf(payload), locations.home ?? homedir()));
9241
- const path = entryLogPath(sessionId, root);
9242
- const recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry));
9243
- if (!recorded)
9244
- dropPrivate(path);
9245
- return recorded;
9243
+ return appendPhaseEntry(sessionId, entry, locations.root ?? tmpdir2(), locations);
9246
9244
  }
9247
9245
  function gatedOperation(payload) {
9248
9246
  const toolName = String(payload.tool_name ?? "");
@@ -9261,15 +9259,21 @@ function phaseEntryGateHookOutput(payload, locations = {}) {
9261
9259
  if (!operation)
9262
9260
  return null;
9263
9261
  const root = locations.root ?? tmpdir2();
9262
+ if (operation.kind === "skill" && operation.skill !== "imm-run") {
9263
+ let activeClaim = false;
9264
+ try {
9265
+ activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null;
9266
+ } catch {}
9267
+ const sessionId = sessionIdOf(payload);
9268
+ if (!activeClaim && sessionId)
9269
+ appendPhaseEntry(sessionId, "plan", root, locations);
9270
+ return null;
9271
+ }
9264
9272
  let state = null;
9265
9273
  try {
9266
9274
  state = readPhaseEntryState(sessionIdOf(payload), root, locations.read);
9267
9275
  } catch {}
9268
- let activeClaim = false;
9269
- try {
9270
- activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null;
9271
- } catch {}
9272
- const admission = admitPhaseOperation(operation, state, { activeClaim });
9276
+ const admission = admitPhaseOperation(operation, state);
9273
9277
  if (!admission.ok)
9274
9278
  return denyPreToolUse(phaseRefusalMessage(admission));
9275
9279
  if (operation.kind === "skill")
@@ -1,6 +1,6 @@
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: Brainstormer
@@ -1,6 +1,6 @@
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
@@ -9,10 +9,13 @@ This skill adheres to the **[BASELINE.md](BASELINE.md)**.
9
9
 
10
10
  ## Managed Request Routing
11
11
 
12
- `imm-planner` is entered explicitly by the user for a clear repository mutation.
13
- Ordinary host input does not invoke this Skill through natural-language routing.
12
+ `imm-planner` starts on the user's request for a Spec, Plan, or TaskIntent for a
13
+ repository change, by slash command or in natural language. A request that asks
14
+ for none of these stays host-native, and `imm-run` starts only from a
15
+ literal-user entry. A model-entered Planner outside an enrolled task ends the
16
+ session's run entry, so Enrollment needs a fresh `imm-run` entry.
14
17
  An active Assurance projection remains authoritative and is resumed only through
15
- an explicit `imm-run` entry; explicit Planner entry owns planning only.
18
+ an explicit `imm-run` entry; Planner entry owns planning only.
16
19
 
17
20
  Planner output remains non-authoritative. Planner creates or validates a
18
21
  candidate Spec/TaskIntent and never invokes the native Enrollment gate;
@@ -429,7 +432,7 @@ it, and structural Enrollment readiness is not proof that a descriptor executed.
429
432
  - **Planning granularity**: Keep a coherent outcome together when acceptance, risk, rollback, and authority can settle together. Use the TaskIntent decomposition rules below for independent outcomes. File count, tokens, compactions, elapsed time, and review rounds are evidence for judgment, not universal gates.
430
433
  - **Historical artifacts**: v3 prose Plan mutation is retired. `imm-plan` is a read-only validator for archived Plans; create no new Roadmap, Phase, successor Plan, or State Ledger.
431
434
  - **Risk-Triggered Exploration**: Before authoring a TaskIntent, resolve only unknowns that could change scope, design, or verification using targeted read-only probes. Internal `arch-explorer` and explicit-lens `advisory-reviewer` roles return evidence without writing the Spec, TaskIntent, or workflow state. Stop probing when outcome, scope, and verification are concrete. Do not use retired Plan termination flags to classify discovery failures.
432
- - **Simplicity**: Apply the BASELINE Workflow Activation gate first. Non-mutating host-native work creates no Planner artifact. Explicit Immune-Brain Skill entry starts this Planner phase; ordinary host input does not invoke it through natural-language routing. Create one coherent outcome instead of expanding ceremony.
435
+ - **Simplicity**: Apply the BASELINE Workflow Activation gate first. Non-mutating host-native work creates no Planner artifact. The user's request for a Spec, Plan, or TaskIntent, by slash command or in natural language, starts this Planner phase; other host input stays host-native. Create one coherent outcome instead of expanding ceremony.
433
436
  - **Design-Depth Classification**: Classify change design risk with the smallest sufficient tier: **Low risk** (copy, configuration, trivial rename, or contained local fix) may omit a separate Technical Design; **Medium risk** (non-trivial single-module behavior or internal contract) records affected components, decisions, invariants, failure behavior, and verification implications; **High risk** (cross-module/API/data-flow/state-machine, security, migration, concurrency, architecture ownership, cross-runtime/package-contract, or persisted-state work) records boundaries, interfaces or flow, alternatives, invariants, rollback/compatibility, and verification implications. Medium and High risk require Technical Design in the Spec. Do not classify a change as Low risk when it has a contract, ownership, security, persistence, compatibility, or multi-component concern. Every new or revised Spec records `**Design risk**: Low|Medium|High` with an adjacent rationale.
434
437
  - **Design-view selection**: For Medium and High risk, select every materially relevant technical-design view from architecture layers, service/component interfaces, data flow, state transitions, and temporal sequence. Record a short `Design views` statement naming the selected views and why any omitted view cannot affect the design. Do not write empty architecture, interface, data-flow, state, or sequence sections. Low risk remains concise and may omit Technical Design. When a selected view is recorded, also record its required decision content: architecture layers need layer responsibilities, dependency direction, ownership, and prohibited coupling; service/component interfaces need inputs, outputs, errors, compatibility/versioning, and caller/callee ownership; data flow needs source, transformations, validation, destination, and failure handling; state transitions need states, legal transitions, trigger, invariant, terminal ownership, and recovery; temporal sequence needs ordered interactions, authority at each point, interruption behavior, and idempotency.
435
438
  - **Technical Design Authority**: The Spec is the single Technical Design baseline. TaskIntent acceptance and scope reference the applicable design decisions or invariants without copying Technical Design prose. If discovery invalidates the baseline, stop execution and return to Planner to update the Spec and decide whether `replan` is required. TaskIntent and Initiative text do not duplicate Technical Design prose or become a prose Plan substitute.
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Observe Claude Code Review and session lifecycle for Immune-Brain, keep a batch Lane's file edits inside its Lane, require a literal-user phase entry before Enrollment, batch start or a phase Skill, and announce an active Managed task without resuming it. Hooks never mint authority.",
2
+ "description": "Observe Claude Code Review and session lifecycle for Immune-Brain, keep a batch Lane's file edits inside its Lane, require a literal-user phase entry before Enrollment, batch start or the imm-run Skill, and announce an active Managed task without resuming it. Hooks never mint authority.",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -63,23 +63,16 @@ export function recognizePhaseEntry(text: string, readAlias: AliasReader): Phase
63
63
  }
64
64
 
65
65
  export interface PhaseEntryState {
66
- /** Count of literal prompts observed in the session. */
67
- prompt_seq: number;
68
66
  run_live: boolean;
69
- /** The prompt that recorded the live run entry, or 0. */
70
- run_prompt_seq: number;
71
- /** The prompt that recorded the latest plan entry, or 0. */
72
- plan_prompt_seq: number;
73
67
  }
74
68
 
75
- export const NO_PHASE_ENTRY: PhaseEntryState = { prompt_seq: 0, run_live: false, run_prompt_seq: 0, plan_prompt_seq: 0 };
69
+ export const NO_PHASE_ENTRY: PhaseEntryState = { run_live: false };
76
70
 
77
71
  /** One literal prompt: a run entry goes live, a plan entry revokes it. */
78
72
  export function applyPrompt(state: PhaseEntryState, entry: PhaseEntry | null): PhaseEntryState {
79
- const prompt_seq = state.prompt_seq + 1;
80
- if (entry === "run") return { ...state, prompt_seq, run_live: true, run_prompt_seq: prompt_seq };
81
- if (entry === "plan") return { prompt_seq, run_live: false, run_prompt_seq: 0, plan_prompt_seq: prompt_seq };
82
- return { ...state, prompt_seq };
73
+ if (entry === "run") return { run_live: true };
74
+ if (entry === "plan") return { run_live: false };
75
+ return state;
83
76
  }
84
77
 
85
78
  export function phaseEntryLogLine(entry: PhaseEntry | null): string {
@@ -115,26 +108,23 @@ function refuse(reason_code: typeof RUN_ENTRY_REQUIRED | typeof PHASE_ENTRY_REQU
115
108
 
116
109
  /**
117
110
  * Whether the session's entry state admits one operation. `state` is null when
118
- * the store could not be read, which refuses everything.
111
+ * the store could not be read, which refuses `enroll`, `start_unattended_batch`
112
+ * and the `imm-run` Skill. The Planner and Brainstorm Skills are never refused.
113
+ * `context` is accepted for Host callers; admission no longer depends on it.
119
114
  */
120
115
  export function admitPhaseOperation(
121
116
  operation: PhaseOperation,
122
117
  state: PhaseEntryState | null,
123
- context: { activeClaim: boolean },
118
+ context?: { activeClaim: boolean },
124
119
  ): PhaseAdmission {
120
+ void context;
125
121
  if (operation.kind === "tool") {
126
122
  if (state?.run_live) return { ok: true };
127
123
  return refuse(RUN_ENTRY_REQUIRED, `${operation.name} needs a literal-user imm-run entry in this session`);
128
124
  }
129
- const latest = state !== null && state.prompt_seq > 0 ? state.prompt_seq : -1;
130
- if (operation.skill === "imm-run") {
131
- if (state?.run_live && state.run_prompt_seq === latest) return { ok: true };
132
- return refuse(PHASE_ENTRY_REQUIRED, "imm-run is entered by the user, not by a model Skill call");
133
- }
134
- if (state && state.plan_prompt_seq === latest) return { ok: true };
135
- // Enrolled Intent Revision: the Loop routes to the Planner inside a run.
136
- if (operation.skill === "imm-planner" && state?.run_live && context.activeClaim) return { ok: true };
137
- return refuse(PHASE_ENTRY_REQUIRED, `${operation.skill} is entered by the user, not by a model Skill call`);
125
+ if (operation.skill !== "imm-run") return { ok: true };
126
+ if (state?.run_live) return { ok: true };
127
+ return refuse(PHASE_ENTRY_REQUIRED, "imm-run is entered by the user, not by a model Skill call");
138
128
  }
139
129
 
140
130
  /** The single-line message both Hosts return for a refusal. */
@@ -6,8 +6,10 @@
6
6
  //
7
7
  // Phase entry gate: UserPromptSubmit records which literal prompt entered
8
8
  // planning or execution; PreToolUse denies `enroll`, `start_unattended_batch`
9
- // and the three phase Skills without a matching entry, and otherwise leaves an
10
- // admission the tool handler consumes by tool-use id.
9
+ // and the `imm-run` Skill without a live run entry, and otherwise leaves an
10
+ // admission the tool handler consumes by tool-use id. A model Skill call for
11
+ // `imm-planner` or `imm-brainstorm` is never denied; with no active claim it
12
+ // revokes the run entry.
11
13
  import { lstatSync, rmSync } from "node:fs";
12
14
  import { homedir, tmpdir } from "node:os";
13
15
  import { join } from "node:path";
@@ -25,6 +27,7 @@ import {
25
27
  reducePhaseEntryLog,
26
28
  RUN_ENTRY_REQUIRED,
27
29
  type AliasReader,
30
+ type PhaseEntry,
28
31
  type PhaseEntryState,
29
32
  type PhaseOperation,
30
33
  } from "../assurance/phase_entry";
@@ -126,22 +129,29 @@ function dropPrivate(path: string): void {
126
129
  }
127
130
 
128
131
  /**
129
- * UserPromptSubmit: record whether this literal prompt entered a phase. Every
130
- * prompt is recorded so "the latest prompt" is decidable later. A prompt that
131
- * could not be recorded drops the log: an earlier entry must not outlive a
132
- * prompt the store never saw, so the session is back to no entry.
132
+ * Appends one entry line to the session log. A line that could not be written
133
+ * drops the log: an earlier entry must not outlive a line the store never saw,
134
+ * so the session is back to no entry.
135
+ */
136
+ function appendPhaseEntry(sessionId: string, entry: PhaseEntry | null, root: string, locations: PhaseEntryLocations): boolean {
137
+ const path = entryLogPath(sessionId, root);
138
+ let recorded = false;
139
+ try { recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry)); } catch { /* an unwritten line drops the log */ }
140
+ if (!recorded) dropPrivate(path);
141
+ return recorded;
142
+ }
143
+
144
+ /**
145
+ * UserPromptSubmit: record whether this literal prompt entered a phase. A
146
+ * prompt that could not be recorded drops the log.
133
147
  */
134
148
  export function recordPhaseEntryPrompt(payload: Record<string, unknown>, locations: PhaseEntryLocations = {}): boolean {
135
149
  if (payload.hook_event_name !== "UserPromptSubmit") return false;
136
150
  const sessionId = sessionIdOf(payload);
137
151
  if (!sessionId) return false;
138
- const root = locations.root ?? tmpdir();
139
152
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
140
153
  const entry = recognizePhaseEntry(prompt, claudeAliasReader(cwdOf(payload), locations.home ?? homedir()));
141
- const path = entryLogPath(sessionId, root);
142
- const recorded = (locations.append ?? appendPrivate)(path, cacheDir(root), phaseEntryLogLine(entry));
143
- if (!recorded) dropPrivate(path);
144
- return recorded;
154
+ return appendPhaseEntry(sessionId, entry, locations.root ?? tmpdir(), locations);
145
155
  }
146
156
 
147
157
  function gatedOperation(payload: Record<string, unknown>): PhaseOperation | null {
@@ -156,20 +166,27 @@ function gatedOperation(payload: Record<string, unknown>): PhaseOperation | null
156
166
  }
157
167
 
158
168
  /**
159
- * PreToolUse for a gated tool: deny without a matching entry; for the two MCP
160
- * tools, otherwise write the admission the handler consumes. Any other tool
161
- * answers null and runs as configured.
169
+ * PreToolUse for a gated tool: deny without a live run entry; for the two MCP
170
+ * tools, otherwise write the admission the handler consumes. A Planner or
171
+ * Brainstorm Skill call is never denied; with no active claim it records a plan
172
+ * entry first, which revokes the run entry. Any other tool answers null and
173
+ * runs as configured.
162
174
  */
163
175
  export function phaseEntryGateHookOutput(payload: Record<string, unknown>, locations: PhaseEntryLocations = {}): string | null {
164
176
  if (payload.hook_event_name !== "PreToolUse") return null;
165
177
  const operation = gatedOperation(payload);
166
178
  if (!operation) return null;
167
179
  const root = locations.root ?? tmpdir();
180
+ if (operation.kind === "skill" && operation.skill !== "imm-run") {
181
+ let activeClaim = false;
182
+ try { activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null; } catch { /* unreadable authority is no claim */ }
183
+ const sessionId = sessionIdOf(payload);
184
+ if (!activeClaim && sessionId) appendPhaseEntry(sessionId, "plan", root, locations);
185
+ return null;
186
+ }
168
187
  let state: PhaseEntryState | null = null;
169
188
  try { state = readPhaseEntryState(sessionIdOf(payload), root, locations.read); } catch { /* an unreadable store refuses */ }
170
- let activeClaim = false;
171
- try { activeClaim = readActiveClaimTaskId(cwdOf(payload)) !== null; } catch { /* unreadable authority is no claim */ }
172
- const admission = admitPhaseOperation(operation, state, { activeClaim });
189
+ const admission = admitPhaseOperation(operation, state);
173
190
  if (!admission.ok) return denyPreToolUse(phaseRefusalMessage(admission));
174
191
  if (operation.kind === "skill") return null;
175
192
  const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : "";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/plugin_versioning.ts from the root package.json.
2
- export const PLUGIN_VERSION = "5.2.0" as const;
2
+ export const PLUGIN_VERSION = "5.3.0" as const;
@@ -55,7 +55,11 @@ framing does not load that execution guidance.
55
55
 
56
56
  Ordinary host input stays host-native and does not run natural-language Managed
57
57
  routing. A new Managed workflow starts only from explicit `imm-brainstorm`,
58
- `imm-planner`, or `imm-run` Skill entry.
58
+ `imm-planner`, or `imm-run` Skill entry. For `imm-planner` and `imm-brainstorm`
59
+ the user's request for that planning or clarification, by slash command or in
60
+ natural language, is such an entry; a request that asks for neither stays
61
+ host-native. `imm-run` starts only from a literal-user entry. A model-entered
62
+ Planner outside an enrolled task ends the session's run entry.
59
63
 
60
64
  1. **Continue an existing Managed owner explicitly**: an active Assurance
61
65
  projection, TaskIntent, TaskRecord, reviewer `follow_up`, or other nonterminal
@@ -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