immune-brain 3.6.9 → 4.0.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 +3 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7587 -5049
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +27 -25
- package/plugins/immune-brain/dist/imm-planner.md +34 -27
- package/plugins/immune-brain/dist/imm-review-retro.md +2 -2
- package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
- package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
- package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
- package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
- package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
- package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
- package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
- package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
- package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
- package/plugins/immune-brain/runtime/plan_core.ts +27 -65
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
- package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
- package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
- package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
- package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
- package/plugins/immune-brain/bin/imm-retired +0 -4
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
- package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
- package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
- package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan parsing, normalization,
|
|
2
|
+
* Plan parsing, normalization, and validation.
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { resolve, relative, sep } from "node:path";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
6
|
import process from "node:process";
|
|
8
|
-
import { stableStringify } from "./canonical_json";
|
|
9
7
|
|
|
10
8
|
// ── errors ───────────────────────────────────────────────────────────
|
|
11
9
|
|
|
@@ -48,18 +46,18 @@ const ROADMAP_SLICE_REQUIRED_FIELDS: Array<[key: string, label: string]> = [
|
|
|
48
46
|
|
|
49
47
|
// ── plan parsing helpers ─────────────────────────────────────────────
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
function hasPlaceholder(value: string): boolean {
|
|
52
50
|
const text = value || "";
|
|
53
51
|
return Boolean(
|
|
54
52
|
PLACEHOLDER_RE.test(text) || text.includes("<") || text.includes(">"),
|
|
55
53
|
);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
function isActionStepResult(value: string): boolean {
|
|
59
57
|
return ACTION_RESULT_RE.test((value || "").trim());
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
function parseDependsOn(rawValue: string): number[] {
|
|
63
61
|
const value = rawValue.trim();
|
|
64
62
|
if (value.toLowerCase() === "none") return [];
|
|
65
63
|
const deps: number[] = [];
|
|
@@ -81,7 +79,7 @@ export function parseDependsOn(rawValue: string): number[] {
|
|
|
81
79
|
return deps;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
function parseStepScope(rawValue: string): string[] {
|
|
85
83
|
const codeSpans = [...rawValue.matchAll(/`([^`\n]+)`/g)].map((match) =>
|
|
86
84
|
match[1].trim(),
|
|
87
85
|
);
|
|
@@ -110,7 +108,7 @@ export function parseStepScope(rawValue: string): string[] {
|
|
|
110
108
|
return paths;
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
function parseBrainstormManifestItems(rawValue: string): string[] {
|
|
114
112
|
return rawValue
|
|
115
113
|
? rawValue
|
|
116
114
|
.split(/[;,]/)
|
|
@@ -119,7 +117,7 @@ export function parseBrainstormManifestItems(rawValue: string): string[] {
|
|
|
119
117
|
: [];
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
|
|
120
|
+
function parseBrainstormTraceRow(
|
|
123
121
|
stripped: string,
|
|
124
122
|
): Record<string, string> | null {
|
|
125
123
|
if (!stripped.startsWith("|") || !stripped.endsWith("|")) return null;
|
|
@@ -137,7 +135,7 @@ export function parseBrainstormTraceRow(
|
|
|
137
135
|
return { item, status, target, reason };
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
|
|
138
|
+
function parseDiscoveryCache(
|
|
141
139
|
rawValue: string,
|
|
142
140
|
): Array<{ path: string; reason: string }> {
|
|
143
141
|
const value = (rawValue || "").trim();
|
|
@@ -157,7 +155,7 @@ export function parseDiscoveryCache(
|
|
|
157
155
|
return entries;
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
|
|
158
|
+
function validateParallelProbeShape(
|
|
161
159
|
probe: Record<string, unknown>,
|
|
162
160
|
): Record<string, unknown> {
|
|
163
161
|
if (typeof probe !== "object" || probe === null) {
|
|
@@ -179,7 +177,7 @@ export function validateParallelProbeShape(
|
|
|
179
177
|
return probe;
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
|
|
180
|
+
function parseParallelProbeEntry(
|
|
183
181
|
rawEntry: string,
|
|
184
182
|
): Record<string, unknown> {
|
|
185
183
|
const probe: Record<string, unknown> = {};
|
|
@@ -205,7 +203,7 @@ export function parseParallelProbeEntry(
|
|
|
205
203
|
return validateParallelProbeShape(probe);
|
|
206
204
|
}
|
|
207
205
|
|
|
208
|
-
|
|
206
|
+
function parseParallelProbes(
|
|
209
207
|
rawValue: string,
|
|
210
208
|
): Array<Record<string, unknown>> {
|
|
211
209
|
const value = (rawValue || "").trim();
|
|
@@ -237,7 +235,7 @@ function parseSpecReference(rawValue: string): string {
|
|
|
237
235
|
return (rawValue || "").trim().replace(/^`|`$/g, "");
|
|
238
236
|
}
|
|
239
237
|
|
|
240
|
-
|
|
238
|
+
interface ParsedSpecDesign {
|
|
241
239
|
path: string;
|
|
242
240
|
exists: boolean;
|
|
243
241
|
design_risk: string | null;
|
|
@@ -340,7 +338,7 @@ function parseReferencedSpec(
|
|
|
340
338
|
|
|
341
339
|
// ── plan parsing ─────────────────────────────────────────────────────
|
|
342
340
|
|
|
343
|
-
|
|
341
|
+
interface ParsedStep {
|
|
344
342
|
number: number;
|
|
345
343
|
result: string | null;
|
|
346
344
|
verification: string | null;
|
|
@@ -353,7 +351,7 @@ export interface ParsedStep {
|
|
|
353
351
|
agent_hint: string | null;
|
|
354
352
|
}
|
|
355
353
|
|
|
356
|
-
|
|
354
|
+
interface ParsedRoadmapPhase {
|
|
357
355
|
title: string;
|
|
358
356
|
acceptance_criteria_present: boolean;
|
|
359
357
|
acceptance_criteria: string[];
|
|
@@ -361,14 +359,14 @@ export interface ParsedRoadmapPhase {
|
|
|
361
359
|
promotion_criteria: string[];
|
|
362
360
|
}
|
|
363
361
|
|
|
364
|
-
|
|
362
|
+
interface PlanValidationWarning {
|
|
365
363
|
code: string;
|
|
366
364
|
message: string;
|
|
367
365
|
phase?: string;
|
|
368
366
|
field?: string;
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
|
|
369
|
+
interface ParsedPlan {
|
|
372
370
|
path: string;
|
|
373
371
|
summary: string | null;
|
|
374
372
|
task: Record<string, string>;
|
|
@@ -378,7 +376,7 @@ export interface ParsedPlan {
|
|
|
378
376
|
steps: ParsedStep[];
|
|
379
377
|
}
|
|
380
378
|
|
|
381
|
-
|
|
379
|
+
function parsePlan(path: string): ParsedPlan {
|
|
382
380
|
const lines = readFileSync(path, "utf-8").split("\n");
|
|
383
381
|
let summary: string | null = null;
|
|
384
382
|
const steps: ParsedStep[] = [];
|
|
@@ -533,9 +531,7 @@ export function parsePlan(path: string): ParsedPlan {
|
|
|
533
531
|
};
|
|
534
532
|
}
|
|
535
533
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
export interface NormalizedStep {
|
|
534
|
+
interface NormalizedStep {
|
|
539
535
|
number: number;
|
|
540
536
|
step_id: string;
|
|
541
537
|
result: string;
|
|
@@ -548,17 +544,16 @@ export interface NormalizedStep {
|
|
|
548
544
|
test_scenarios?: string[] | null;
|
|
549
545
|
}
|
|
550
546
|
|
|
551
|
-
|
|
547
|
+
interface NormalizedPlan {
|
|
552
548
|
plan_path: string;
|
|
553
549
|
summary: string;
|
|
554
550
|
task: Record<string, string>;
|
|
555
551
|
steps: NormalizedStep[];
|
|
556
552
|
}
|
|
557
553
|
|
|
558
|
-
|
|
559
|
-
export type CompounderPolicy = "optional" | "required";
|
|
554
|
+
type WorkflowProfile = "standard" | "strict";
|
|
560
555
|
|
|
561
|
-
|
|
556
|
+
function workflowProfileForTask(
|
|
562
557
|
task: Record<string, string> | null | undefined,
|
|
563
558
|
): WorkflowProfile {
|
|
564
559
|
return task?.workflow_profile?.trim().toLowerCase() === "standard"
|
|
@@ -566,40 +561,7 @@ export function workflowProfileForTask(
|
|
|
566
561
|
: "strict";
|
|
567
562
|
}
|
|
568
563
|
|
|
569
|
-
|
|
570
|
-
task: Record<string, string> | null | undefined,
|
|
571
|
-
): CompounderPolicy {
|
|
572
|
-
if (task?.compounder?.trim().toLowerCase() === "optional") return "optional";
|
|
573
|
-
return "required";
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
export function buildSignaturePayload(normalizedPlan: NormalizedPlan): string {
|
|
577
|
-
// Keep cross-runtime parity: the legacy Plan signature predates Scope.
|
|
578
|
-
const payload = {
|
|
579
|
-
summary: normalizedPlan.summary,
|
|
580
|
-
task: normalizedPlan.task,
|
|
581
|
-
steps: normalizedPlan.steps.map((step) => ({
|
|
582
|
-
number: step.number,
|
|
583
|
-
step_id: step.step_id,
|
|
584
|
-
result: step.result,
|
|
585
|
-
verification: step.verification,
|
|
586
|
-
test_scenarios: step.test_scenarios,
|
|
587
|
-
discovery_cache: step.discovery_cache,
|
|
588
|
-
parallel_probes: step.parallel_probes,
|
|
589
|
-
depends_on: step.depends_on,
|
|
590
|
-
agent_hint: step.agent_hint,
|
|
591
|
-
})),
|
|
592
|
-
};
|
|
593
|
-
return stableStringify(payload);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
export function buildPlanSignature(normalizedPlan: NormalizedPlan): string {
|
|
597
|
-
return createHash("sha256")
|
|
598
|
-
.update(buildSignaturePayload(normalizedPlan))
|
|
599
|
-
.digest("hex");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
export function normalizePlan(
|
|
564
|
+
function normalizePlan(
|
|
603
565
|
plan: ParsedPlan,
|
|
604
566
|
projectRoot?: string,
|
|
605
567
|
): NormalizedPlan {
|
|
@@ -623,7 +585,7 @@ export function normalizePlan(
|
|
|
623
585
|
};
|
|
624
586
|
}
|
|
625
587
|
|
|
626
|
-
|
|
588
|
+
function normalizePlanPath(
|
|
627
589
|
planPath: string,
|
|
628
590
|
projectRoot?: string,
|
|
629
591
|
): string {
|
|
@@ -845,7 +807,7 @@ function validatePlanContract(task: Record<string, string>): string[] {
|
|
|
845
807
|
return errors;
|
|
846
808
|
}
|
|
847
809
|
|
|
848
|
-
|
|
810
|
+
function validatePlan(plan: ParsedPlan): {
|
|
849
811
|
errors: string[];
|
|
850
812
|
warnings: PlanValidationWarning[];
|
|
851
813
|
} {
|
|
@@ -954,7 +916,7 @@ export function validatePlan(plan: ParsedPlan): {
|
|
|
954
916
|
return { errors, warnings };
|
|
955
917
|
}
|
|
956
918
|
|
|
957
|
-
|
|
919
|
+
interface OriginCoverageProjection {
|
|
958
920
|
applicable: boolean;
|
|
959
921
|
declared_items: number;
|
|
960
922
|
mapped_items: number;
|
|
@@ -964,7 +926,7 @@ export interface OriginCoverageProjection {
|
|
|
964
926
|
complete: boolean;
|
|
965
927
|
}
|
|
966
928
|
|
|
967
|
-
|
|
929
|
+
interface PlanValidationProjection extends NormalizedPlan {
|
|
968
930
|
warnings: PlanValidationWarning[];
|
|
969
931
|
origin_coverage: OriginCoverageProjection;
|
|
970
932
|
}
|
|
@@ -975,7 +937,7 @@ const REASON_REQUIRED_STATUSES = new Set([
|
|
|
975
937
|
"deferred",
|
|
976
938
|
]);
|
|
977
939
|
|
|
978
|
-
|
|
940
|
+
function deriveOriginCoverage(
|
|
979
941
|
plan: ParsedPlan,
|
|
980
942
|
): OriginCoverageProjection {
|
|
981
943
|
const declared = [
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "
|
|
2
|
+
export const PLUGIN_VERSION = "4.0.0" as const;
|
|
@@ -38,6 +38,8 @@ boundary name). Do not invent fields, and never send an anchor yourself: the
|
|
|
38
38
|
Kernel derives it as the sha256 of the canonical `{violated.kind,
|
|
39
39
|
violated.ref, caller_chain}`, so an identical claim keeps one stable identity
|
|
40
40
|
across review rounds while a different call chain is a different claim. A
|
|
41
|
-
passing review
|
|
41
|
+
passing review carries no blocking findings; non-blocking notes may ride along as
|
|
42
|
+
`kind: "advisory"` findings, and every finding, advisory included, needs the
|
|
43
|
+
same machine-checkable evidence. If the
|
|
42
44
|
checkpoint is `awaiting_user_successor_decision`, stop without dispatch; only
|
|
43
45
|
a literal user may invoke `--approve-successor`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Internal role: executor
|
|
2
2
|
|
|
3
3
|
You are the Immune-Brain Executor role inside Loop. Implement exactly the
|
|
4
|
-
enrolled TaskIntent acceptance and `scope_hint` (or one accepted
|
|
5
|
-
same-boundary follow-up) in the current Parent conversation.
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
enrolled TaskIntent acceptance and `scope_hint` envelope (or one accepted
|
|
5
|
+
same-boundary follow-up) in the current Parent conversation. New helpers or
|
|
6
|
+
tests inside an approved directory or glob do not require a revision. Keep
|
|
7
|
+
every edit inside the authorized envelope; do not stage unrelated user files. Do not discover or load a Pi Skill.
|
|
8
8
|
|
|
9
9
|
Before handoff, run the permitted diagnostic checks and return commands and
|
|
10
10
|
outcomes to the Parent as structured diagnostic evidence. The read-only Loop
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Staging a TaskIntent sidecar is one operation on both Hosts: capture the
|
|
2
|
+
// sidecar bytes together with its Git index entry, and restore both. Claude used
|
|
3
|
+
// to stop at `git update-index`; Pi re-read the bytes and the index and failed
|
|
4
|
+
// closed on any mismatch. The stricter branch is now the only one, so a restore
|
|
5
|
+
// that silently left either side inconsistent cannot pass as success.
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
export interface StagedIntentSnapshot {
|
|
12
|
+
/** Repository-relative path of the staged sidecar. */
|
|
13
|
+
path: string;
|
|
14
|
+
bytes: Buffer;
|
|
15
|
+
index_state: Buffer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Capture the sidecar bytes and the exact index entry the restore must reproduce. */
|
|
19
|
+
export function captureStagedIntent(root: string, relativePath: string): StagedIntentSnapshot {
|
|
20
|
+
return {
|
|
21
|
+
path: relativePath,
|
|
22
|
+
bytes: readFileSync(join(root, relativePath)),
|
|
23
|
+
index_state: execFileSync("git", ["ls-files", "--stage", "-z", "--", relativePath], {
|
|
24
|
+
cwd: root,
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Restore the sidecar and its index entry, then verify both. A restore that
|
|
32
|
+
* leaves either the bytes or the index different from the snapshot throws.
|
|
33
|
+
*/
|
|
34
|
+
export function restoreStagedIntent(root: string, snapshot: StagedIntentSnapshot): void {
|
|
35
|
+
writeFileSync(join(root, snapshot.path), snapshot.bytes);
|
|
36
|
+
execFileSync("git", ["update-index", "--force-remove", "--", snapshot.path], {
|
|
37
|
+
cwd: root,
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
});
|
|
40
|
+
if (snapshot.index_state.length > 0) {
|
|
41
|
+
execFileSync("git", ["update-index", "-z", "--index-info"], {
|
|
42
|
+
cwd: root,
|
|
43
|
+
input: snapshot.index_state,
|
|
44
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const restoredBytes = readFileSync(join(root, snapshot.path));
|
|
48
|
+
if (!restoredBytes.equals(snapshot.bytes)) {
|
|
49
|
+
throw new Error("failed to restore prior intent bytes");
|
|
50
|
+
}
|
|
51
|
+
const restoredIndex = execFileSync("git", ["ls-files", "--stage", "-z", "--", snapshot.path], {
|
|
52
|
+
cwd: root,
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
});
|
|
55
|
+
if (!restoredIndex.equals(snapshot.index_state)) {
|
|
56
|
+
throw new Error("failed to restore prior intent git index entry");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Invariant: The runtime never creates, switches, or deletes a Git worktree,
|
|
4
4
|
// never pushes a ref, and never opens or updates a pull request.
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import {
|
|
8
8
|
constants,
|
|
9
9
|
closeSync,
|
|
@@ -11,15 +11,15 @@ import {
|
|
|
11
11
|
lstatSync,
|
|
12
12
|
mkdirSync,
|
|
13
13
|
openSync,
|
|
14
|
-
readFileSync,
|
|
15
14
|
realpathSync,
|
|
16
15
|
renameSync,
|
|
17
16
|
rmSync,
|
|
18
17
|
writeFileSync,
|
|
19
18
|
} from "node:fs";
|
|
20
19
|
import { dirname, join } from "node:path";
|
|
21
|
-
import { readAuditTaskPair, readSecureProjectFile } from "../kernel/storage";
|
|
22
|
-
import { pathMatchesScope } from "../workspace_scope";
|
|
20
|
+
import { localRunId, readAuditTaskPair, readSecureProjectFile } from "../kernel/storage";
|
|
21
|
+
import { captureGitTaskRevisionSnapshot, pathMatchesScope } from "../workspace_scope";
|
|
22
|
+
import { expectedBatchHead, findExistingActiveBatch, findSettledBatchRecord } from "./batch_preflight";
|
|
23
23
|
|
|
24
24
|
export type BatchGitPreflightRejectReason =
|
|
25
25
|
| "batch_branch_exists"
|
|
@@ -66,7 +66,8 @@ const DEFAULT_GIT_ENV = {
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Preflight before any child is enrolled: requires clean working tree,
|
|
69
|
-
* committed HEAD, and
|
|
69
|
+
* committed HEAD, and an absent batch branch or the current branch of a settled
|
|
70
|
+
* run whose commit lineage is preserved.
|
|
70
71
|
* Verifies root is the top-level repository root (Finding 5).
|
|
71
72
|
* Explicitly passes --untracked-files=all so repo config cannot bypass dirty check (Finding 2).
|
|
72
73
|
* Disables Git hooks during checkout (Finding 2).
|
|
@@ -163,6 +164,17 @@ export function runBatchGitPreflight(input: {
|
|
|
163
164
|
{ stdio: ["ignore", "ignore", "ignore"] },
|
|
164
165
|
);
|
|
165
166
|
if (branchCheck.status === 0) {
|
|
167
|
+
const settled = findSettledBatchRecord(root, initiativeSlug);
|
|
168
|
+
const currentBranch = spawnSync("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
|
|
169
|
+
// Reuse only this initiative's current, settled branch. Never move or reset
|
|
170
|
+
// a ref; the new authorization binds the current HEAD, including newly
|
|
171
|
+
// committed candidate intents, and the old run's evidence stays intact.
|
|
172
|
+
if (
|
|
173
|
+
findExistingActiveBatch(root, initiativeSlug) === null &&
|
|
174
|
+
settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch &&
|
|
175
|
+
headResult.stdout.trim() === baseHead &&
|
|
176
|
+
spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", expectedBatchHead(settled), baseHead]).status === 0
|
|
177
|
+
) return { ok: true, branch };
|
|
166
178
|
return {
|
|
167
179
|
ok: false,
|
|
168
180
|
reason: "batch_branch_exists",
|
|
@@ -442,7 +454,11 @@ export async function commitBatchChild(input: {
|
|
|
442
454
|
const { root, taskId, batchId, expectedHead, branch: expectedBranch } = input;
|
|
443
455
|
|
|
444
456
|
// 1. Verify Kernel reports that child done via immutable terminal audit pair (Finding 2)
|
|
445
|
-
|
|
457
|
+
// The committed local run owns this task's evidence: another worktree's run
|
|
458
|
+
// directory for the same task is not this batch's audit pair. Settlement
|
|
459
|
+
// clears the active run, so the lookup includes terminal runs.
|
|
460
|
+
const localRun = localRunId(root, taskId);
|
|
461
|
+
const auditPair = readAuditTaskPair(root, taskId, localRun ?? undefined);
|
|
446
462
|
if (!auditPair) {
|
|
447
463
|
throw new Error(`cannot commit child ${taskId}: task is not settled done (audit pair missing)`);
|
|
448
464
|
}
|
|
@@ -532,6 +548,19 @@ export async function commitBatchChild(input: {
|
|
|
532
548
|
spawnSync("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
533
549
|
throw new Error("dirty_outside_scope");
|
|
534
550
|
}
|
|
551
|
+
const record = auditPair.record as {
|
|
552
|
+
contract?: string;
|
|
553
|
+
git_base_head?: string;
|
|
554
|
+
attestations?: Array<{ kind: string; diff_hash: string }>;
|
|
555
|
+
};
|
|
556
|
+
const qa = [...(record.attestations ?? [])].reverse().find((item) => item.kind === "qa");
|
|
557
|
+
if (record.contract === "assurance_kernel/task_record/v4" && qa?.diff_hash) {
|
|
558
|
+
if (!record.git_base_head) throw new Error("batch commit requires a TaskRecord v4 git_base_head");
|
|
559
|
+
const captured = captureGitTaskRevisionSnapshot(root, scopeHint, record.git_base_head, taskId);
|
|
560
|
+
const digest = `sha256:${createHash("sha256").update(JSON.stringify(captured)).digest("hex")}`;
|
|
561
|
+
if (digest !== qa.diff_hash)
|
|
562
|
+
throw new Error("batch commit drifted from the reviewed delivery identity");
|
|
563
|
+
}
|
|
535
564
|
|
|
536
565
|
// 6. Commit with structured message, Immune-Brain-Batch trailer, and all hooks disabled (Finding 1)
|
|
537
566
|
const goalFirstLine = goal.trim().split(/\r?\n/)[0]?.trim() || taskId;
|
|
@@ -730,7 +759,8 @@ export async function lookupBatchCommit(input: {
|
|
|
730
759
|
}
|
|
731
760
|
|
|
732
761
|
// Verify terminal audit exists and is settled done
|
|
733
|
-
const
|
|
762
|
+
const adoptedRun = localRunId(root, taskId);
|
|
763
|
+
const auditPair = readAuditTaskPair(root, taskId, adoptedRun ?? undefined);
|
|
734
764
|
if (!auditPair) {
|
|
735
765
|
throw new Error(`batch_head_lineage_broken: adopted commit lacks terminal audit pair for ${taskId}`);
|
|
736
766
|
}
|
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
TaskIntentObservationError,
|
|
8
8
|
} from "../kernel/intent";
|
|
9
9
|
import { readTaskRecordRaw } from "../kernel/storage";
|
|
10
|
+
import { inspectSpecBinding, type SpecBindingInspection } from "../kernel/spec_binding";
|
|
10
11
|
import type {
|
|
11
12
|
BatchPlan,
|
|
12
13
|
BatchPlanBudget,
|
|
13
14
|
BatchPlanChild,
|
|
15
|
+
BatchPlanChildReason,
|
|
14
16
|
BatchPlanDigestChild,
|
|
15
17
|
InitiativeObservationReader,
|
|
16
18
|
ProjectBatchPlanInput,
|
|
@@ -20,6 +22,21 @@ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
|
20
22
|
const DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1_000;
|
|
21
23
|
const DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Enrollment refuses a child whose scope_hint cannot name its bound Spec pair,
|
|
27
|
+
* so the plan excludes the same children here, before a confirmation offers
|
|
28
|
+
* them and before an Executor writes their implementation. The reason carries a
|
|
29
|
+
* stable code plus every path the intent must add. Advisory only: this mints,
|
|
30
|
+
* alters, and consumes no authority.
|
|
31
|
+
*/
|
|
32
|
+
function specBindingReason(inspection: Extract<SpecBindingInspection, { ok: false }>): BatchPlanChildReason {
|
|
33
|
+
// One shared branch renders the concrete paths of every refusal that carries
|
|
34
|
+
// them — a `binding_missing` whose declared halves never paired included.
|
|
35
|
+
// `binding_incomplete` always carries paths, so it has no entry of its own.
|
|
36
|
+
if (inspection.missing.length > 0) return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
|
|
37
|
+
return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
function compareIds(left: string, right: string): number {
|
|
24
41
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
25
42
|
}
|
|
@@ -153,10 +170,33 @@ export async function projectBatchPlan(
|
|
|
153
170
|
const intentPath = `docs/plans/${task.task_id}.intent.json`;
|
|
154
171
|
try {
|
|
155
172
|
const read = observeTaskIntent(root, task.task_id, intentPath);
|
|
173
|
+
if (read.intent.risk === "critical") {
|
|
174
|
+
children.push({
|
|
175
|
+
...child,
|
|
176
|
+
status: "needs_human",
|
|
177
|
+
reason: "critical",
|
|
178
|
+
intent_path: read.intent_ref.path,
|
|
179
|
+
intent_revision: read.intent_ref.revision,
|
|
180
|
+
intent_content_hash: read.content_hash,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const binding = inspectSpecBinding(read.intent);
|
|
185
|
+
if (!binding.ok) {
|
|
186
|
+
children.push({
|
|
187
|
+
...child,
|
|
188
|
+
status: "needs_human",
|
|
189
|
+
reason: specBindingReason(binding),
|
|
190
|
+
intent_path: read.intent_ref.path,
|
|
191
|
+
intent_revision: read.intent_ref.revision,
|
|
192
|
+
intent_content_hash: read.content_hash,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
156
196
|
children.push({
|
|
157
197
|
...child,
|
|
158
|
-
status:
|
|
159
|
-
reason:
|
|
198
|
+
status: "enrollable",
|
|
199
|
+
reason: null,
|
|
160
200
|
intent_path: read.intent_ref.path,
|
|
161
201
|
intent_revision: read.intent_ref.revision,
|
|
162
202
|
intent_content_hash: read.content_hash,
|