immune-brain 3.6.6 → 3.6.8
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/README.md +110 -51
- package/README.zh-CN.md +153 -49
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +29 -19
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +932 -0
- package/plugins/immune-brain/.pi-extension/package.json +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +149 -4
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +200 -2
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +3404 -227
- package/plugins/immune-brain/dist/imm-loop.md +25 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +9 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +101 -3
- package/plugins/immune-brain/runtime/claude/interaction.ts +16 -3
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +840 -17
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +72 -10
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +19 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +162 -11
- package/plugins/immune-brain/runtime/kernel/refutation.ts +82 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +34 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +202 -9
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +775 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +203 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +1224 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +360 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +60 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +1 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "immune-brain-pi-extension",
|
|
3
3
|
"private": true,
|
|
4
|
-
"description": "Explicit extension entry manifest for the Immune-Brain Pi lifecycle Tools. Helper modules in this directory are library code, not extension factories; listing only the
|
|
4
|
+
"description": "Explicit extension entry manifest for the Immune-Brain Pi lifecycle Tools. Helper modules in this directory are library code, not extension factories; listing only the three factory files here prevents Pi's directory discovery from loading them as extensions.",
|
|
5
5
|
"pi": {
|
|
6
6
|
"extensions": [
|
|
7
7
|
"./imm-canary-enroll.ts",
|
|
8
|
-
"./imm-canary-work.ts"
|
|
8
|
+
"./imm-canary-work.ts",
|
|
9
|
+
"./imm-unattended-batch.ts"
|
|
9
10
|
]
|
|
10
11
|
}
|
|
11
12
|
}
|
|
@@ -55,6 +55,8 @@ export interface TaskRailView {
|
|
|
55
55
|
recovery?: string;
|
|
56
56
|
/** Latest per-descriptor QA fact; rendered only while present. */
|
|
57
57
|
acceptance_progress?: TaskRailAcceptanceProgress;
|
|
58
|
+
/** Optional pipeline milestone progress indicator. */
|
|
59
|
+
pipeline?: boolean;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export interface TaskOverviewEntry {
|
|
@@ -144,7 +146,7 @@ export async function requestAuthorityDialog<T extends string, R = T | undefined
|
|
|
144
146
|
finish = done;
|
|
145
147
|
if (settled || options.signal?.aborted) done(undefined);
|
|
146
148
|
let expanded = false;
|
|
147
|
-
const detailText = new Text(theme.fg("muted", "Details collapsed; press d to expand."), 1, 0);
|
|
149
|
+
const detailText = new Text(theme.fg("muted", "▸ Details collapsed; press d to expand."), 1, 0);
|
|
148
150
|
const selectList = new SelectList(
|
|
149
151
|
options.actions.map((action): SelectItem => ({ ...action })),
|
|
150
152
|
options.actions.length,
|
|
@@ -158,13 +160,27 @@ export async function requestAuthorityDialog<T extends string, R = T | undefined
|
|
|
158
160
|
);
|
|
159
161
|
selectList.onSelect = (item) => complete(item.value as T);
|
|
160
162
|
selectList.onCancel = () => complete(undefined);
|
|
163
|
+
|
|
164
|
+
const affirmativeAction = options.actions.find((a) =>
|
|
165
|
+
["confirm", "authorize", "yes", "accept"].includes(a.value.toLowerCase()),
|
|
166
|
+
);
|
|
167
|
+
const negativeAction = options.actions.find((a) =>
|
|
168
|
+
["cancel", "decline", "no", "reject"].includes(a.value.toLowerCase()),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const affKey = affirmativeAction ? "y/enter" : "enter";
|
|
172
|
+
const negKey = negativeAction ? "n/esc" : "esc";
|
|
173
|
+
const affLabel = affirmativeAction ? affirmativeAction.value : "choose";
|
|
174
|
+
const negLabel = negativeAction ? negativeAction.value : "cancel";
|
|
175
|
+
const hintLine = `d: details | ${affKey}: ${affLabel} | ${negKey}: ${negLabel}`;
|
|
176
|
+
|
|
161
177
|
const container = new Container();
|
|
162
178
|
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
163
179
|
container.addChild(new Text(theme.fg("accent", theme.bold(options.title)), 1, 0));
|
|
164
|
-
container.addChild(new Text(options.summary, 1, 0));
|
|
180
|
+
container.addChild(new Text(formatDialogSummary(options.summary, theme), 1, 0));
|
|
165
181
|
container.addChild(detailText);
|
|
166
182
|
container.addChild(selectList);
|
|
167
|
-
container.addChild(new Text(theme.fg("dim",
|
|
183
|
+
container.addChild(new Text(theme.fg("dim", hintLine), 1, 0));
|
|
168
184
|
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
169
185
|
return {
|
|
170
186
|
render: (width) => container.render(width),
|
|
@@ -172,10 +188,20 @@ export async function requestAuthorityDialog<T extends string, R = T | undefined
|
|
|
172
188
|
handleInput: (data) => {
|
|
173
189
|
if (data === "d" || data === "D") {
|
|
174
190
|
expanded = !expanded;
|
|
175
|
-
detailText.setText(expanded
|
|
191
|
+
detailText.setText(expanded
|
|
192
|
+
? `${theme.fg("muted", "▾ Details:")}\n${options.details}`
|
|
193
|
+
: theme.fg("muted", "▸ Details collapsed; press d to expand."));
|
|
176
194
|
tui.requestRender();
|
|
177
195
|
return;
|
|
178
196
|
}
|
|
197
|
+
if ((data === "y" || data === "Y") && affirmativeAction) {
|
|
198
|
+
complete(affirmativeAction.value as T);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if ((data === "n" || data === "N") && negativeAction) {
|
|
202
|
+
complete(negativeAction.value as T);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
179
205
|
selectList.handleInput(data);
|
|
180
206
|
tui.requestRender();
|
|
181
207
|
},
|
|
@@ -372,6 +398,13 @@ export function renderStructuredResult(
|
|
|
372
398
|
];
|
|
373
399
|
const recovery = recoveryHint(details);
|
|
374
400
|
if (recovery) lines.push(`${theme.fg("muted", "Recovery:")} ${theme.fg("dim", recovery)}`);
|
|
401
|
+
const facts = record(details.facts) ?? record(details.execution_facts);
|
|
402
|
+
if (facts) {
|
|
403
|
+
const factParts = Object.entries(facts)
|
|
404
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
405
|
+
.join(" · ");
|
|
406
|
+
if (factParts) lines.push(`${theme.fg("muted", "Facts:")} ${theme.fg("dim", factParts)}`);
|
|
407
|
+
}
|
|
375
408
|
if (terminal && taskState) lines.push(...renderFinalLines(taskState, theme));
|
|
376
409
|
return new Text(lines.join("\n"), 0, 0);
|
|
377
410
|
}
|
|
@@ -423,6 +456,9 @@ function renderTaskRail(view: TaskRailView, width = 120, theme?: Theme): string[
|
|
|
423
456
|
const lines = [
|
|
424
457
|
`Task ${boundedMiddle(view.task_id, taskIdWidth)} · ${stateFormatted}`,
|
|
425
458
|
];
|
|
459
|
+
if (view.pipeline) {
|
|
460
|
+
lines.push(`${label("Pipeline:")} ${renderPipelineMilestones(view.state, theme)}`);
|
|
461
|
+
}
|
|
426
462
|
if (view.phase) {
|
|
427
463
|
lines.push(`${label("Phase:")} ${body(bounded(view.phase, availableContentWidth))}`);
|
|
428
464
|
}
|
|
@@ -446,6 +482,69 @@ function renderTaskRail(view: TaskRailView, width = 120, theme?: Theme): string[
|
|
|
446
482
|
return lines;
|
|
447
483
|
}
|
|
448
484
|
|
|
485
|
+
export function renderPipelineMilestones(state: TaskRailState, theme?: Theme): string {
|
|
486
|
+
const milestones = [
|
|
487
|
+
{ name: "Plan", key: "plan" },
|
|
488
|
+
{ name: "Exec", key: "exec" },
|
|
489
|
+
{ name: "QA", key: "qa" },
|
|
490
|
+
{ name: "Review", key: "review" },
|
|
491
|
+
] as const;
|
|
492
|
+
|
|
493
|
+
type StepStatus = "done" | "active" | "pending" | "blocked" | "stopped";
|
|
494
|
+
|
|
495
|
+
let statuses: [StepStatus, StepStatus, StepStatus, StepStatus];
|
|
496
|
+
switch (state) {
|
|
497
|
+
case "Planning":
|
|
498
|
+
statuses = ["active", "pending", "pending", "pending"];
|
|
499
|
+
break;
|
|
500
|
+
case "Approval required":
|
|
501
|
+
case "Working":
|
|
502
|
+
statuses = ["done", "active", "pending", "pending"];
|
|
503
|
+
break;
|
|
504
|
+
case "Verifying":
|
|
505
|
+
statuses = ["done", "done", "active", "pending"];
|
|
506
|
+
break;
|
|
507
|
+
case "Reviewing":
|
|
508
|
+
statuses = ["done", "done", "done", "active"];
|
|
509
|
+
break;
|
|
510
|
+
case "Completed":
|
|
511
|
+
statuses = ["done", "done", "done", "done"];
|
|
512
|
+
break;
|
|
513
|
+
case "Blocked":
|
|
514
|
+
statuses = ["done", "blocked", "pending", "pending"];
|
|
515
|
+
break;
|
|
516
|
+
case "Stopped":
|
|
517
|
+
statuses = ["done", "stopped", "pending", "pending"];
|
|
518
|
+
break;
|
|
519
|
+
default:
|
|
520
|
+
statuses = ["pending", "pending", "pending", "pending"];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const renderStep = (name: string, status: StepStatus, index: number): string => {
|
|
524
|
+
const stepNum = index + 1;
|
|
525
|
+
if (!theme) {
|
|
526
|
+
const sym = status === "done" ? "✔" : status === "active" ? "●" : status === "blocked" ? "⚠" : status === "stopped" ? "■" : "○";
|
|
527
|
+
return `[${stepNum}.${name} ${sym}]`;
|
|
528
|
+
}
|
|
529
|
+
switch (status) {
|
|
530
|
+
case "done":
|
|
531
|
+
return `[${stepNum}.${name} ${theme.fg("success", "✔")}]`;
|
|
532
|
+
case "active":
|
|
533
|
+
return `[${stepNum}.${name} ${theme.fg("accent", "●")}]`;
|
|
534
|
+
case "blocked":
|
|
535
|
+
return `[${stepNum}.${name} ${theme.fg("warning", "⚠")}]`;
|
|
536
|
+
case "stopped":
|
|
537
|
+
return `[${stepNum}.${name} ${theme.fg("muted", "■")}]`;
|
|
538
|
+
case "pending":
|
|
539
|
+
default:
|
|
540
|
+
return `[${stepNum}.${name} ${theme.fg("dim", "○")}]`;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const sep = theme ? ` ${theme.fg("dim", "─")} ` : " ─ ";
|
|
545
|
+
return milestones.map((m, i) => renderStep(m.name, statuses[i], i)).join(sep);
|
|
546
|
+
}
|
|
547
|
+
|
|
449
548
|
export function renderTaskOverview(view: TaskOverviewView, width = 120, theme?: Theme): string[] {
|
|
450
549
|
const label = (text: string) => (theme ? theme.fg("muted", text) : text);
|
|
451
550
|
const head = (text: string) => (theme ? theme.fg("accent", theme.bold(text)) : text);
|
|
@@ -456,6 +555,7 @@ export function renderTaskOverview(view: TaskOverviewView, width = 120, theme?:
|
|
|
456
555
|
}
|
|
457
556
|
if (view.active) {
|
|
458
557
|
lines.push(`${label("Active:")} ${formatTaskRailState(view.active.state, theme)} ${bounded(view.active.task_id, 60)}`);
|
|
558
|
+
lines.push(`${label(" Pipeline:")} ${renderPipelineMilestones(view.active.state, theme)}`);
|
|
459
559
|
lines.push(`${label(" Result:")} ${bounded(view.active.result, 100)}`);
|
|
460
560
|
lines.push(`${label(" Next:")} ${bounded(view.active.next, 100)}`);
|
|
461
561
|
} else {
|
|
@@ -501,15 +601,60 @@ function renderFinalLines(taskState: Record<string, unknown>, theme: Theme): str
|
|
|
501
601
|
+ strings(taskState.unresolved_user_decision_ids).length
|
|
502
602
|
+ strings(taskState.replan_required_ids).length;
|
|
503
603
|
const diffHash = string(taskState.diff_hash);
|
|
604
|
+
const divider = theme.fg("dim", "────────────────────────────────────────");
|
|
504
605
|
return [
|
|
606
|
+
divider,
|
|
607
|
+
theme.fg("accent", theme.bold("Final Settlement Summary")),
|
|
505
608
|
`${theme.fg("muted", "Acceptance:")} ${theme.fg(missing === 0 ? "success" : "warning", `${fresh}/${fresh + missing} fresh`)}`,
|
|
506
609
|
`${theme.fg("muted", "QA / Review:")} ${theme.fg("dim", approvals.length > 0 ? approvals.join(", ") : "not recorded")}`,
|
|
507
610
|
`${theme.fg("muted", "Residual blockers:")} ${theme.fg(blockers === 0 ? "dim" : "warning", String(blockers))}`,
|
|
508
611
|
`${theme.fg("muted", "Repository health:")} ${theme.fg("dim", "not assessed")}`,
|
|
509
612
|
`${theme.fg("muted", "Git:")} ${theme.fg("dim", diffHash ? `task diff ${diffHash.slice(0, 15)}` : "not reported")}`,
|
|
613
|
+
divider,
|
|
510
614
|
];
|
|
511
615
|
}
|
|
512
616
|
|
|
617
|
+
function formatDialogSummary(summary: string, theme: Theme): string {
|
|
618
|
+
const lines = summary.split("\n");
|
|
619
|
+
return lines.map((line) => {
|
|
620
|
+
const colonIdx = line.indexOf(":");
|
|
621
|
+
if (colonIdx === -1) return theme.fg("dim", line);
|
|
622
|
+
const key = line.slice(0, colonIdx).trim();
|
|
623
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
624
|
+
let formattedValue = theme.fg("dim", value);
|
|
625
|
+
const lowerKey = key.toLowerCase();
|
|
626
|
+
if (lowerKey === "risk") {
|
|
627
|
+
const isHigh = /high|material|critical/i.test(value);
|
|
628
|
+
formattedValue = isHigh ? theme.fg("warning", theme.bold(value)) : theme.fg("accent", value);
|
|
629
|
+
} else if (lowerKey === "goal") {
|
|
630
|
+
formattedValue = theme.fg("accent", value);
|
|
631
|
+
} else if (lowerKey === "acceptance") {
|
|
632
|
+
formattedValue = theme.fg("success", value);
|
|
633
|
+
}
|
|
634
|
+
return `${theme.fg("muted", `${key}:`)} ${formattedValue}`;
|
|
635
|
+
}).join("\n");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function boundedPath(path: string, max: number): string {
|
|
639
|
+
const width = visibleWidth(path);
|
|
640
|
+
if (width <= max) return path;
|
|
641
|
+
if (!path.includes("/")) return bounded(path, max);
|
|
642
|
+
const parts = path.split("/");
|
|
643
|
+
const fileName = parts.pop() ?? "";
|
|
644
|
+
const fileWidth = visibleWidth(fileName);
|
|
645
|
+
if (fileWidth + 2 >= max) {
|
|
646
|
+
return bounded(fileName, max);
|
|
647
|
+
}
|
|
648
|
+
const remaining = max - fileWidth - 3; // "…/"
|
|
649
|
+
let prefix = "";
|
|
650
|
+
for (const part of parts) {
|
|
651
|
+
const next = prefix ? `${prefix}/${part}` : part;
|
|
652
|
+
if (visibleWidth(next) > remaining) break;
|
|
653
|
+
prefix = next;
|
|
654
|
+
}
|
|
655
|
+
return prefix ? `${prefix}/…/${fileName}` : `…/${fileName}`;
|
|
656
|
+
}
|
|
657
|
+
|
|
513
658
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
514
659
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
515
660
|
? value as Record<string, unknown>
|
|
@@ -174,7 +174,15 @@ export interface TaskRecordRead {
|
|
|
174
174
|
manifest_digest: string;
|
|
175
175
|
};
|
|
176
176
|
}>;
|
|
177
|
-
findings: Array<{
|
|
177
|
+
findings: Array<{
|
|
178
|
+
id: string;
|
|
179
|
+
kind: string;
|
|
180
|
+
status: string;
|
|
181
|
+
summary?: string;
|
|
182
|
+
anchor?: string | null;
|
|
183
|
+
evidence?: unknown;
|
|
184
|
+
counterevidence?: { attestation_id: string; acceptance_id: string } | null;
|
|
185
|
+
}>;
|
|
178
186
|
} | null;
|
|
179
187
|
}
|
|
180
188
|
|
|
@@ -283,7 +291,12 @@ export async function readTaskRecord(
|
|
|
283
291
|
taskId: string,
|
|
284
292
|
): Promise<TaskRecordRead> {
|
|
285
293
|
const mod = await import(/* @vite-ignore */ kernelPath("storage"));
|
|
286
|
-
|
|
294
|
+
// Lock-free read only. `storage.readTaskRecord` wraps this in
|
|
295
|
+
// withKernelStoreLock, which first runs pending-transaction recovery, so a
|
|
296
|
+
// decline, a cancel, or a rejected preflight would otherwise mutate Kernel
|
|
297
|
+
// state before the literal user ever approved anything. Mutating Kernel
|
|
298
|
+
// entrypoints still take their own lock when the batch actually starts.
|
|
299
|
+
return mod.readTaskRecordRaw(root, taskId);
|
|
287
300
|
}
|
|
288
301
|
export async function withKernelStoreLock<T>(root: string, operation: () => T): Promise<T> {
|
|
289
302
|
const mod = await import(/* @vite-ignore */ kernelPath("storage"));
|
|
@@ -403,3 +416,188 @@ export async function deriveAssuranceAuthorization(input: {
|
|
|
403
416
|
const mod = await import(/* @vite-ignore */ kernelPath("assurance_projection"));
|
|
404
417
|
return mod.deriveAssuranceAuthorization(input) as AssuranceAuthorizationReadiness;
|
|
405
418
|
}
|
|
419
|
+
|
|
420
|
+
// --- Batch Authority & Unattended Runner Forwarding ---
|
|
421
|
+
function unattendedPath(module: string): string {
|
|
422
|
+
return `../runtime/unattended/${module}.ts`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export type {
|
|
426
|
+
BatchPlan,
|
|
427
|
+
BatchPlanChild,
|
|
428
|
+
BatchPlanBudget,
|
|
429
|
+
BatchPlanDigestChild,
|
|
430
|
+
InitiativeObservationReader,
|
|
431
|
+
} from "../runtime/unattended/types";
|
|
432
|
+
export type {
|
|
433
|
+
BatchAuthorityRegistry,
|
|
434
|
+
BatchAuthorizationBinding,
|
|
435
|
+
ValidatedBatchAuthorization,
|
|
436
|
+
} from "../runtime/kernel/batch_authority";
|
|
437
|
+
export type {
|
|
438
|
+
BatchRunnerKernelPort,
|
|
439
|
+
StartBatchInput,
|
|
440
|
+
BatchRunReport,
|
|
441
|
+
} from "../runtime/unattended/batch_runner";
|
|
442
|
+
export type {
|
|
443
|
+
BatchRunnerGitPort,
|
|
444
|
+
BatchGitPreflightResult,
|
|
445
|
+
} from "../runtime/unattended/batch_git";
|
|
446
|
+
|
|
447
|
+
export async function projectBatchPlan(
|
|
448
|
+
root: string,
|
|
449
|
+
initiativeSlug: string,
|
|
450
|
+
input: any,
|
|
451
|
+
readInitiative?: any,
|
|
452
|
+
): Promise<any> {
|
|
453
|
+
const mod = await import(/* @vite-ignore */ unattendedPath("batch_plan"));
|
|
454
|
+
return mod.projectBatchPlan(root, initiativeSlug, input, readInitiative);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function computeBatchPlanDigest(children: any): Promise<string> {
|
|
458
|
+
const mod = await import(/* @vite-ignore */ kernelPath("batch_authority"));
|
|
459
|
+
return mod.computeBatchPlanDigest(children);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function createBatchAuthorityRegistry(): Promise<any> {
|
|
463
|
+
const mod = await import(/* @vite-ignore */ kernelPath("batch_authority"));
|
|
464
|
+
return mod.createBatchAuthorityRegistry();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export async function deriveChildEnrollment(root: string, registry: any, input: any): Promise<any> {
|
|
468
|
+
const mod = await import(/* @vite-ignore */ kernelPath("batch_authority"));
|
|
469
|
+
return mod.deriveChildEnrollment(root, registry, input);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function startBatch(input: any): Promise<any> {
|
|
473
|
+
const mod = await import(/* @vite-ignore */ unattendedPath("batch_runner"));
|
|
474
|
+
return mod.startBatch(input);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export async function runBatchGitPreflight(input: any): Promise<any> {
|
|
478
|
+
const mod = await import(/* @vite-ignore */ unattendedPath("batch_git"));
|
|
479
|
+
return mod.runBatchGitPreflight(input);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function readGitHead(root: string): Promise<string> {
|
|
483
|
+
// The Pi extension must reach Kernel prepare only through the shared Enrollment
|
|
484
|
+
// boundary, so this reads HEAD itself rather than importing the Kernel prepare
|
|
485
|
+
// module. The unattended Git module reads HEAD the same way.
|
|
486
|
+
const { spawnSync } = await import("node:child_process");
|
|
487
|
+
const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
488
|
+
if (result.status !== 0) throw new Error(`git rev-parse HEAD is unavailable for ${root}`);
|
|
489
|
+
return result.stdout.trim();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Read the immutable terminal audit record for a settled child.
|
|
494
|
+
*
|
|
495
|
+
* A child reaches Kernel settlement before the batch commits it, and settlement
|
|
496
|
+
* clears the live state record, so the batch resume preflight must resolve the
|
|
497
|
+
* child's authorized scope from the audit pair instead. Read-only: neither the
|
|
498
|
+
* audit pair nor this reader mutates Kernel state.
|
|
499
|
+
*/
|
|
500
|
+
export async function readSettledTaskRecord(
|
|
501
|
+
root: string,
|
|
502
|
+
taskId: string,
|
|
503
|
+
): Promise<{ scope_hint: string[]; intent_path: string | undefined } | null> {
|
|
504
|
+
const mod = await import(/* @vite-ignore */ kernelPath("storage"));
|
|
505
|
+
const pair = mod.readAuditTaskPair(root, taskId);
|
|
506
|
+
if (!pair?.record) return null;
|
|
507
|
+
const record = pair.record as {
|
|
508
|
+
intent_snapshot?: { scope_hint?: string[] };
|
|
509
|
+
intent_ref?: { path?: string };
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
scope_hint: record.intent_snapshot?.scope_hint ?? [],
|
|
513
|
+
intent_path: record.intent_ref?.path,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function pathMatchesScope(path: string, scopePath: string): Promise<boolean> {
|
|
518
|
+
const mod = await import(/* @vite-ignore */ runtimePath("workspace_scope"));
|
|
519
|
+
return mod.pathMatchesScope(path, scopePath);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const GLOBAL_PI_PROGRESSION_KEY = Symbol.for("immune_brain.pi_assurance_progression");
|
|
523
|
+
|
|
524
|
+
export async function getSharedPiProgression(): Promise<any> {
|
|
525
|
+
let progression = (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY];
|
|
526
|
+
if (!progression) {
|
|
527
|
+
const workMod = await import(/* @vite-ignore */ "./imm-canary-work");
|
|
528
|
+
const ports = workMod.createPiAssuranceProgressionPorts();
|
|
529
|
+
const progMod = await import(/* @vite-ignore */ "./pi-canary-assurance-progression");
|
|
530
|
+
progression = new progMod.AssuranceProgression(ports);
|
|
531
|
+
(globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] = progression;
|
|
532
|
+
}
|
|
533
|
+
return progression;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function advancePiTask(root: string, taskId: string): Promise<any> {
|
|
537
|
+
const progression = await getSharedPiProgression();
|
|
538
|
+
const result = await progression.advance(taskId, { cwd: root });
|
|
539
|
+
if (result.state === "completed") return { state: "completed" };
|
|
540
|
+
if (result.state === "stopped") return { state: "stopped" };
|
|
541
|
+
if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary };
|
|
542
|
+
if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, agent_params: result.agent_params };
|
|
543
|
+
if (result.state === "blocked") return { state: "blocked", reason: result.reason };
|
|
544
|
+
return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed" };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export async function projectAssuranceForTask(root: string, taskId: string): Promise<any> {
|
|
548
|
+
const [assuranceMod, scopeMod] = await Promise.all([
|
|
549
|
+
import(/* @vite-ignore */ kernelPath("assurance_projection")),
|
|
550
|
+
import(/* @vite-ignore */ runtimePath("workspace_scope")),
|
|
551
|
+
]);
|
|
552
|
+
const diffSnapshotOf = (r: string, record: any) => {
|
|
553
|
+
if (record.contract === "assurance_kernel/task_record/v4") {
|
|
554
|
+
if (!record.git_base_head)
|
|
555
|
+
throw new Error("TaskRecord v4 is missing git_base_head");
|
|
556
|
+
return scopeMod.taskRevisionIdentity(r, record.intent_snapshot.scope_hint, record.git_base_head);
|
|
557
|
+
}
|
|
558
|
+
return scopeMod.taskDiffIdentity(r, record.intent_snapshot.scope_hint);
|
|
559
|
+
};
|
|
560
|
+
return assuranceMod.projectAssurance(root, taskId, diffSnapshotOf);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function findExistingActiveBatch(root: string, initiativeSlug: string): Promise<any> {
|
|
564
|
+
const { join } = await import("node:path");
|
|
565
|
+
const { existsSync, readdirSync, readFileSync } = await import("node:fs");
|
|
566
|
+
const batchesDir = join(root, ".imm", "state", "batches");
|
|
567
|
+
if (!existsSync(batchesDir)) return null;
|
|
568
|
+
const files = readdirSync(batchesDir);
|
|
569
|
+
for (const file of files) {
|
|
570
|
+
if (!file.endsWith(".json")) continue;
|
|
571
|
+
let record: any;
|
|
572
|
+
try {
|
|
573
|
+
record = JSON.parse(readFileSync(join(batchesDir, file), "utf8"));
|
|
574
|
+
} catch {
|
|
575
|
+
// Unreadable Kernel batch state must fail closed: silently treating the
|
|
576
|
+
// initiative as batchless could authorize a parallel run.
|
|
577
|
+
return { corrupt: true, path: file };
|
|
578
|
+
}
|
|
579
|
+
if (record.contract === "assurance_kernel/batch_run_state/v1" && record.initiative_slug === initiativeSlug) {
|
|
580
|
+
const validStates = new Set([
|
|
581
|
+
"prepared",
|
|
582
|
+
"running",
|
|
583
|
+
"needs_human",
|
|
584
|
+
"completed",
|
|
585
|
+
"budget_stopped",
|
|
586
|
+
"failed",
|
|
587
|
+
"rejected",
|
|
588
|
+
]);
|
|
589
|
+
if (
|
|
590
|
+
typeof record.batch_id !== "string" ||
|
|
591
|
+
typeof record.base_head !== "string" ||
|
|
592
|
+
!Array.isArray(record.children) ||
|
|
593
|
+
!validStates.has(record.batch_state)
|
|
594
|
+
) {
|
|
595
|
+
return { corrupt: true, path: file };
|
|
596
|
+
}
|
|
597
|
+
return record;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|