immune-brain 5.1.0 → 5.1.2

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 (54) hide show
  1. package/package.json +1 -1
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +37 -5
  4. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +230 -10
  5. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
  6. package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -1
  7. package/plugins/immune-brain/dist/claude/mcp-server.mjs +8712 -7410
  8. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  9. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
  10. package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
  11. package/plugins/immune-brain/dist/imm-planner.md +41 -5
  12. package/plugins/immune-brain/dist/imm-run.md +123 -9
  13. package/plugins/immune-brain/dist/role-prompts/code-review.md +25 -0
  14. package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
  15. package/plugins/immune-brain/hooks/hooks.json +22 -1
  16. package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
  17. package/plugins/immune-brain/runtime/assurance/coordinator.ts +31 -4
  18. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
  19. package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
  20. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
  21. package/plugins/immune-brain/runtime/claude/interaction.ts +15 -0
  22. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +120 -59
  23. package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
  24. package/plugins/immune-brain/runtime/claude/mcp_server.ts +142 -31
  25. package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
  26. package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
  27. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
  28. package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
  29. package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
  30. package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
  31. package/plugins/immune-brain/runtime/kernel/runtime_contracts.json +7 -0
  32. package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
  33. package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
  34. package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
  35. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
  36. package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
  37. package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
  38. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  39. package/plugins/immune-brain/runtime/prompts/code-review.md +25 -0
  40. package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
  41. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +1 -1
  42. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
  43. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
  44. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
  45. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +447 -37
  46. package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
  47. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +33 -16
  48. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +16 -3
  49. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +44 -0
  50. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
  51. package/plugins/immune-brain/runtime/unattended/batch_state.ts +199 -6
  52. package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
  53. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
  54. package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -1
@@ -12,8 +12,11 @@ import {
12
12
  type NativeConfirmationPort,
13
13
  } from "./interaction";
14
14
  import { ClaudeReviewHost, FileHookEventLog, parseHookStdin } from "./review_host";
15
+ import { activeTaskHookOutput, laneGuardHookOutput } from "./lane_guard";
15
16
  import { ClaudeRuntime, type ToolMeta } from "./kernel_ports";
16
- import { parseLaneOffers, parseMaxParallel, resolveLaneParallel } from "../unattended/batch_lanes";
17
+ import { parseLaneInstruction, parseLaneOffers, parseMaxParallel, resolveLaneParallel } from "../unattended/batch_lanes";
18
+ import { parseFinalVerification } from "../unattended/batch_final_verification";
19
+ import { parseLaneRevision, REVISION_DELEGATION_TEXT } from "../unattended/batch_delegation";
17
20
  import type { AssuranceCoordinatorPorts } from "../assurance/coordinator";
18
21
 
19
22
  export const MCP_PROTOCOL_VERSION = "2025-06-18";
@@ -22,12 +25,13 @@ export const TOOLS = [
22
25
  { name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
23
26
  { name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
24
27
  { name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
25
- { name: "submit_review", description: "Submit the Parent-mediated Review verdict bound to the correlated receipt.", privileged: false },
28
+ { name: "submit_review", description: "Submit the Review verdict; omit the verdict to apply the correlated reviewer receipt, or relay the reviewer's structured verdict for fingerprint comparison.", privileged: false },
26
29
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
27
30
  { name: "revise_intent", description: "Apply a compatible TaskIntent revision.", privileged: false },
28
31
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
29
32
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
30
33
  { name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
34
+ { name: "retire_stale_batch", description: "Retire an Initiative's parked or drifted batch record to the terminal superseded state after native confirmation.", privileged: true },
31
35
  { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
32
36
  { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
33
37
  { name: "refute_finding", description: "Refute one open finding by binding the fresh passing QA attestation that contradicts it.", privileged: false },
@@ -40,20 +44,42 @@ export function listMcpTools() {
40
44
  inputSchema: {
41
45
  type: "object",
42
46
  properties: {
43
- ...(tool.name === "start_unattended_batch"
47
+ ...(tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch"
44
48
  ? {
45
49
  initiative_slug: { type: "string" },
46
- max_parallel: { type: "integer", minimum: 1 },
47
- lane_offers: {
48
- type: "array",
49
- items: {
50
- type: "object",
51
- properties: { task_id: { type: "string" }, path: { type: "string" } },
52
- required: ["task_id", "path"],
53
- additionalProperties: false,
54
- },
55
- },
56
- }
50
+ ...(tool.name === "start_unattended_batch"
51
+ ? {
52
+ max_parallel: { type: "integer", minimum: 1 },
53
+ lane_offers: {
54
+ type: "array",
55
+ items: {
56
+ type: "object",
57
+ properties: { task_id: { type: "string" }, path: { type: "string" } },
58
+ required: ["task_id", "path"],
59
+ additionalProperties: false,
60
+ },
61
+ },
62
+ final_verification: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 },
63
+ lane_instruction: {
64
+ type: "object",
65
+ properties: {
66
+ task_id: { type: "string" },
67
+ kind: { type: "string", enum: ["instruction", "correction"] },
68
+ text: { type: "string" },
69
+ session_state: { type: "string", enum: ["idle", "working", "blocked", "done"] },
70
+ },
71
+ required: ["task_id", "text", "session_state"],
72
+ additionalProperties: false,
73
+ },
74
+ lane_revision: {
75
+ type: "object",
76
+ properties: { task_id: { type: "string" }, next_intent: { type: "object" } },
77
+ required: ["task_id", "next_intent"],
78
+ additionalProperties: false,
79
+ },
80
+ }
81
+ : {}),
82
+ }
57
83
  : {
58
84
  task_id: { type: "string" },
59
85
  ...(tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {}),
@@ -63,17 +89,15 @@ export function listMcpTools() {
63
89
  ...(tool.name === "refute_finding" ? { finding_id: { type: "string" }, attestation_id: { type: "string" } } : {}),
64
90
  }),
65
91
  },
66
- required: tool.name === "start_unattended_batch"
92
+ required: tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch"
67
93
  ? ["initiative_slug"]
68
- : tool.name === "submit_review"
69
- ? ["task_id", "verdict"]
70
- : tool.name === "resolve_finding"
71
- ? ["task_id", "finding_id"]
72
- : tool.name === "refute_finding"
73
- ? ["task_id", "finding_id", "attestation_id"]
74
- : tool.name === "revise_intent"
75
- ? ["task_id", "next_intent"]
76
- : ["task_id"],
94
+ : tool.name === "resolve_finding"
95
+ ? ["task_id", "finding_id"]
96
+ : tool.name === "refute_finding"
97
+ ? ["task_id", "finding_id", "attestation_id"]
98
+ : tool.name === "revise_intent"
99
+ ? ["task_id", "next_intent"]
100
+ : ["task_id"],
77
101
  },
78
102
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
79
103
  }));
@@ -150,12 +174,38 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
150
174
  };
151
175
  const laneOffers = parseLaneOffers(args.lane_offers);
152
176
  // A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
153
- const maxParallel = resolveLaneParallel(options.cwd ?? process.cwd(), initiativeSlug, parseMaxParallel(args.max_parallel), laneOffers);
177
+ const laneInstruction = parseLaneInstruction(args.lane_instruction);
178
+ const laneRevision = parseLaneRevision(args.lane_revision);
179
+ const maxParallel = resolveLaneParallel(options.cwd ?? process.cwd(), initiativeSlug, parseMaxParallel(args.max_parallel), laneOffers, laneInstruction ?? laneRevision);
180
+ const finalVerification = parseFinalVerification(args.final_verification);
154
181
  return runtime.startUnattendedBatch(initiativeSlug, toolMeta, {
155
182
  ...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
156
183
  ...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
184
+ ...(finalVerification !== undefined ? { final_verification: finalVerification } : {}),
185
+ ...(laneInstruction !== undefined ? { lane_instruction: laneInstruction } : {}),
186
+ ...(laneRevision !== undefined ? { lane_revision: laneRevision } : {}),
157
187
  });
158
188
  }
189
+ if (name === "retire_stale_batch") {
190
+ const initiativeSlug = String(args.initiative_slug ?? "");
191
+ if (!initiativeSlug) throw new Error("initiative_slug is required");
192
+ if ("native_decision" in args) throw new Error("native_decision cannot be supplied in tool arguments");
193
+ if (!negotiatedVersion) throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
194
+ if (!negotiatedInteractive) {
195
+ throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
196
+ }
197
+ const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
198
+ if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
199
+ const toolMeta: ToolMeta = {
200
+ sessionId: meta.sessionId ?? connectionId,
201
+ toolCallId: meta.toolCallId ?? `call-${name}`,
202
+ taskId: initiativeSlug,
203
+ initiativeSlug,
204
+ interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
205
+ signal: meta.signal,
206
+ };
207
+ return runtime.retireStaleBatch(initiativeSlug, toolMeta);
208
+ }
159
209
  const taskId = String(args.task_id ?? "");
160
210
  if (!taskId) throw new Error("task_id is required");
161
211
  if ("native_decision" in args) throw new Error("native_decision cannot be supplied in tool arguments");
@@ -183,8 +233,8 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
183
233
  if (name === "enroll") return runtime.enroll(taskId, toolMeta);
184
234
  if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal, toolMeta);
185
235
  if (name === "submit_review") {
186
- if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
187
- return runtime.submitReview(taskId, args.verdict, toolMeta);
236
+ // ADR 0017: an absent verdict is the receipt-bound submission.
237
+ return runtime.submitReview(taskId, Object.hasOwn(args, "verdict") ? args.verdict : undefined, toolMeta);
188
238
  }
189
239
  if (name === "revise_intent") {
190
240
  if (!Object.hasOwn(args, "next_intent")) throw new Error("next_intent is required");
@@ -381,6 +431,34 @@ async function writeReply(output: Writable, reply: JsonRpc): Promise<void> {
381
431
  }
382
432
 
383
433
  export function elicitationParams(input: NativeConfirmationInput) {
434
+ if (input.operation === "retire_stale_batch") {
435
+ const d = input.batchDisposition;
436
+ const details = [
437
+ `Operation: retire_stale_batch`,
438
+ `Initiative: ${input.initiativeSlug}`,
439
+ d ? `Batch: ${d.batch_id}` : null,
440
+ d ? `Batch state: ${d.batch_state}` : null,
441
+ d ? `Batch branch: ${d.batch_branch}` : null,
442
+ d ? `Plan digest: ${d.plan_digest}` : null,
443
+ d ? `Recorded child commits: ${d.recorded_commits}` : null,
444
+ d
445
+ ? `Children (${d.children.length}):\n${d.children
446
+ .map(
447
+ (child) =>
448
+ ` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`,
449
+ )
450
+ .join("\n")}`
451
+ : null,
452
+ "Retiring writes this record to the terminal superseded state. Child states, recorded commits and the batch branch are preserved as evidence; nothing is deleted and no batch trailer or approval is manufactured.",
453
+ "A child without a commit was never executed by this batch: it carries no delivery evidence, and the retired authorization is never reused for a revised plan.",
454
+ "After retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative.",
455
+ ].filter(Boolean);
456
+ return {
457
+ mode: "form",
458
+ message: `Retire this stale batch record for ${input.initiativeSlug}?\n\n${details.join("\n")}`,
459
+ requestedSchema: { type: "object", properties: {} },
460
+ };
461
+ }
384
462
  if (input.operation === "start_unattended_batch") {
385
463
  const b = input.batchDetails;
386
464
  const details = [
@@ -394,11 +472,25 @@ export function elicitationParams(input: NativeConfirmationInput) {
394
472
  b?.lane_mode ? `Lane mode: max_parallel=${b.lane_mode.max_parallel}` : null,
395
473
  b?.lane_mode ? `Parallel groups (${b.lane_mode.parallel_groups.length}):\n${b.lane_mode.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}` : null,
396
474
  b?.lane_mode && b.lane_mode.serialized.length > 0 ? `Serialized by overlapping scope (${b.lane_mode.serialized.length}):\n${b.lane_mode.serialized.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : null,
475
+ b?.lane_mode?.final_verification ? `Final verification: ${b.lane_mode.final_verification.join("; ")}` : null,
476
+ b?.lane_mode?.offer_revision_delegation ? REVISION_DELEGATION_TEXT : null,
397
477
  ].filter(Boolean);
398
478
  return {
399
479
  mode: "form",
400
480
  message: `Authorize this unattended batch run for ${input.initiativeSlug}?\n\n${details.join("\n")}`,
401
- requestedSchema: { type: "object", properties: {} },
481
+ requestedSchema: b?.lane_mode?.offer_revision_delegation
482
+ ? {
483
+ type: "object",
484
+ properties: {
485
+ delegate_in_lane_revisions: {
486
+ type: "boolean",
487
+ title: "Delegate in-envelope Lane Intent revisions to the batch coordinator",
488
+ description: REVISION_DELEGATION_TEXT,
489
+ default: false,
490
+ },
491
+ },
492
+ }
493
+ : { type: "object", properties: {} },
402
494
  };
403
495
  }
404
496
  const details = [
@@ -476,13 +568,19 @@ export async function serveStdio(options: {
476
568
  if (action !== "accept" && action !== "decline" && action !== "cancel") {
477
569
  throw new NativeAuthorityError("correlation_missing", "MCP elicitation returned an invalid action");
478
570
  }
571
+ let delegateRevisions = false;
479
572
  if (action === "accept") {
480
573
  const content = (result as { content?: unknown }).content;
481
- if (typeof content !== "object" || content === null || Array.isArray(content) || Object.keys(content).length !== 0) {
574
+ const offered = input.operation === "start_unattended_batch" && input.batchDetails?.lane_mode?.offer_revision_delegation === true;
575
+ const keys = typeof content === "object" && content !== null && !Array.isArray(content) ? Object.keys(content) : null;
576
+ const valid = keys !== null && (keys.length === 0 ||
577
+ (offered && keys.length === 1 && keys[0] === "delegate_in_lane_revisions" && typeof (content as Record<string, unknown>).delegate_in_lane_revisions === "boolean"));
578
+ if (!valid) {
482
579
  throw new NativeAuthorityError("correlation_missing", "MCP elicitation accept content did not match the requested schema");
483
580
  }
581
+ delegateRevisions = offered && (content as Record<string, unknown>).delegate_in_lane_revisions === true;
484
582
  }
485
- return { decision: action, requestId };
583
+ return { decision: action, requestId, ...(delegateRevisions ? { delegateRevisions: true } : {}) };
486
584
  } finally {
487
585
  pending.delete(requestId);
488
586
  if (abortListener) input.signal?.removeEventListener("abort", abortListener);
@@ -635,7 +733,20 @@ async function runHook(): Promise<void> {
635
733
  const lines: string[] = [];
636
734
  const rl = createInterface({ input: stdin });
637
735
  for await (const line of rl) lines.push(line);
638
- const event = parseHookStdin(lines.join("\n"));
736
+ const raw = lines.join("\n");
737
+ let payload: Record<string, unknown> | null = null;
738
+ try { payload = JSON.parse(raw) as Record<string, unknown>; } catch { payload = null; }
739
+ if (payload?.hook_event_name === "PreToolUse") {
740
+ const decision = laneGuardHookOutput(payload);
741
+ if (decision) process.stdout.write(`${decision}\n`);
742
+ return;
743
+ }
744
+ if (payload?.hook_event_name === "UserPromptSubmit") {
745
+ const notice = await activeTaskHookOutput(payload);
746
+ if (notice) process.stdout.write(`${notice}\n`);
747
+ return;
748
+ }
749
+ const event = parseHookStdin(raw);
639
750
  if (!event) return;
640
751
  new FileHookEventLog().append(event);
641
752
  }
@@ -8,6 +8,7 @@ import {
8
8
  } from "node:fs";
9
9
  import { execFileSync } from "node:child_process";
10
10
  import { resolve } from "node:path";
11
+ import { projectScopeOverlap } from "../unattended/batch_schedule";
11
12
  import process from "node:process";
12
13
  import { fileURLToPath } from "node:url";
13
14
  import {
@@ -858,6 +859,57 @@ function runIntentAuthor(args: string[], root: string): KernelExecution {
858
859
  * TaskRecord, claim, Ledger, Git index, or session write. Returns a stable
859
860
  * bounded projection.
860
861
  */
862
+ /**
863
+ * Read-only: compare the `scope_hint` of two or more TaskIntents of one
864
+ * Initiative and report every overlapping entry pair plus the start waves the
865
+ * Batch Plan would use. Optional `--blocked-by a=b,c` edges order the waves the
866
+ * way the Initiative's dependencies do. Writes nothing.
867
+ */
868
+ function runIntentOverlap(args: string[], root: string): KernelExecution {
869
+ const usage = "Run imm-kernel intent overlap <path> <path>... [--blocked-by <task-id>=<task-id>,...] --json.";
870
+ const reject = (message: string): KernelExecution => ({
871
+ result: errorResult("invalid_command", message, 2),
872
+ journal: journalFor("intent", null, "rejected", "invalid_command", null, usage),
873
+ });
874
+ const paths: string[] = [];
875
+ const blockedBy = new Map<string, string[]>();
876
+ for (let index = 0; index < args.length; index += 1) {
877
+ const arg = args[index]!;
878
+ if (arg === "--json") continue;
879
+ if (arg === "--blocked-by") {
880
+ const edge = args[++index] ?? "";
881
+ const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)=([A-Za-z0-9._,-]+)$/.exec(edge);
882
+ if (!match) return reject(`invalid --blocked-by edge: ${edge}`);
883
+ blockedBy.set(match[1]!, match[2]!.split(",").filter(Boolean));
884
+ continue;
885
+ }
886
+ if (arg.startsWith("-")) return reject(`unsupported intent overlap option: ${arg}`);
887
+ paths.push(arg);
888
+ }
889
+ if (paths.length < 2) return reject("imm-kernel intent overlap requires at least two paths");
890
+ const children: Array<{ task_id: string; blocked_by: string[]; scope_hint: string[] }> = [];
891
+ for (const path of paths) {
892
+ let intent: { task_id?: unknown; scope_hint?: unknown };
893
+ try {
894
+ intent = JSON.parse(readFileSync(resolve(root, path), "utf8"));
895
+ } catch (error) {
896
+ return { result: errorResult("source_invalid", `cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`, 1), journal: journalFor("intent", null, "rejected", "source_read_failed", null, usage) };
897
+ }
898
+ if (typeof intent.task_id !== "string" || !Array.isArray(intent.scope_hint) || intent.scope_hint.some((entry) => typeof entry !== "string"))
899
+ return { result: errorResult("source_invalid", `${path} has no task_id and string scope_hint`, 1), journal: journalFor("intent", null, "rejected", "source_invalid", null, usage) };
900
+ children.push({ task_id: intent.task_id, blocked_by: [], scope_hint: intent.scope_hint as string[] });
901
+ }
902
+ const known = new Set(children.map((child) => child.task_id));
903
+ for (const child of children) child.blocked_by = (blockedBy.get(child.task_id) ?? []).filter((id) => known.has(id));
904
+ let report: ReturnType<typeof projectScopeOverlap>;
905
+ try {
906
+ report = projectScopeOverlap(children);
907
+ } catch (error) {
908
+ return reject(error instanceof Error ? error.message : String(error));
909
+ }
910
+ return { result: jsonResult(report, 0), journal: journalFor("intent", null, "ok", "command_ok", null, null) };
911
+ }
912
+
861
913
  function runIntentValidate(args: string[], root: string): KernelExecution {
862
914
  const nonFlags = args.filter((arg) => !arg.startsWith("-"));
863
915
  if (nonFlags.length !== 1)
@@ -1173,10 +1225,11 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
1173
1225
  const sub = args[1] ?? "";
1174
1226
  if (sub === "author") return runIntentAuthor(args.slice(2), root);
1175
1227
  if (sub === "validate") return runIntentValidate(args.slice(2), root);
1228
+ if (sub === "overlap") return runIntentOverlap(args.slice(2), root);
1176
1229
  return {
1177
1230
  result: errorResult(
1178
1231
  "invalid_command",
1179
- "usage: imm-kernel intent author <path> --stdin --json | validate <path> --json",
1232
+ "usage: imm-kernel intent author <path> --stdin --json | validate <path> --json | overlap <path> <path>... [--blocked-by <task-id>=<task-id>,...] --json",
1180
1233
  2,
1181
1234
  ),
1182
1235
  journal: journalFor(