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.
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +37 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +230 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8712 -7410
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
- package/plugins/immune-brain/dist/imm-planner.md +41 -5
- package/plugins/immune-brain/dist/imm-run.md +123 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +25 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +22 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +31 -4
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
- package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +15 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +120 -59
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +142 -31
- package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
- package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.json +7 -0
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +25 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +447 -37
- package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +33 -16
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +44 -0
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +199 -6
- package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
- 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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 === "
|
|
69
|
-
? ["task_id", "
|
|
70
|
-
: tool.name === "
|
|
71
|
-
? ["task_id", "finding_id"]
|
|
72
|
-
: tool.name === "
|
|
73
|
-
? ["task_id", "
|
|
74
|
-
:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(
|