immune-brain 5.3.0 → 5.5.1
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 +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +13 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16130 -12734
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +57 -16
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +21 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +1 -7
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +8 -15
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +13 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -0
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -12,16 +12,26 @@ import {
|
|
|
12
12
|
mkdirSync,
|
|
13
13
|
openSync,
|
|
14
14
|
realpathSync,
|
|
15
|
+
readdirSync,
|
|
15
16
|
renameSync,
|
|
16
17
|
rmSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from "node:fs";
|
|
19
20
|
import { dirname, join } from "node:path";
|
|
21
|
+
import { auditEvidencePaths, auditRunRecordPath, auditRunTerminalProofPath } from "../kernel/storage_paths";
|
|
20
22
|
import { readSettledTaskEvidence, readAuditTaskPair, readSecureProjectFile } from "../kernel/storage";
|
|
21
|
-
import { serializeTaskTombstone, type TaskTombstone } from "../kernel/backend_claim";
|
|
23
|
+
import { serializeTaskTombstone, readTaskTombstone, type TaskTombstone } from "../kernel/backend_claim";
|
|
22
24
|
import { captureGitTaskRevisionSnapshot, pathMatchesScope } from "../workspace_scope";
|
|
23
|
-
import {
|
|
25
|
+
import { readNativeParentFinalReport, writeNativeParentFinalReport, readAnyBatchRunState, isLaneBatchRecord, writeBatchRunState, writeBatchLaneRunState, type AnyBatchRunStateRecord } from "./batch_state";
|
|
26
|
+
import { changeIdentity, identitiesEqual } from "./batch_integration";
|
|
27
|
+
import { runFinalVerification } from "./batch_final_verification";
|
|
28
|
+
import type { InitiativeObservationReader } from "./types";
|
|
29
|
+
import { readTaskIntent, observeEnrollmentTaskIntent } from "../kernel/intent";
|
|
30
|
+
import { classifyBatchLineage, expectedBatchHead, readActiveClaimTaskId as findActiveClaimForCriticalIntegration, findExistingActiveBatch, findSettledBatchRecord } from "./batch_preflight";
|
|
24
31
|
export { classifyBatchLineage, type BatchLineage } from "./batch_preflight";
|
|
32
|
+
import { observeConsolidatedDeliveries, prepareDeliveryConsolidation, verifyConsolidatedCommit, consolidationDigest,
|
|
33
|
+
parseDeliveryConsolidation, type DeliveryConsolidation, type ConsolidationReceipt } from "./parent_delivery_consolidation";
|
|
34
|
+
import { readParentConsolidationReceipt, writeParentConsolidationReceipt } from "./batch_state";
|
|
25
35
|
|
|
26
36
|
export type BatchGitPreflightRejectReason =
|
|
27
37
|
| "batch_branch_exists"
|
|
@@ -36,6 +46,7 @@ export type BatchGitPreflightResult =
|
|
|
36
46
|
| { ok: false; reason: BatchGitPreflightRejectReason; message: string };
|
|
37
47
|
|
|
38
48
|
export interface BatchRunnerGitPort {
|
|
49
|
+
retainLaneEvidence?: typeof retainIntegratedLaneEvidence;
|
|
39
50
|
preflight(input: {
|
|
40
51
|
root: string;
|
|
41
52
|
initiative_slug: string;
|
|
@@ -123,7 +134,7 @@ export function runBatchGitPreflight(input: {
|
|
|
123
134
|
return {
|
|
124
135
|
ok: false,
|
|
125
136
|
reason: "dirty_working_tree",
|
|
126
|
-
message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${
|
|
137
|
+
message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${scopeRefusal("unsupported_index_flags", flaggedPreflight).message}`,
|
|
127
138
|
};
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -240,7 +251,17 @@ function hasBoundaryWhitespace(path: string): boolean {
|
|
|
240
251
|
return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
|
|
241
252
|
}
|
|
242
253
|
|
|
243
|
-
function
|
|
254
|
+
function scopeRefusal(cause: string, entries: Array<{ path: string; flags?: string[] }>): Error {
|
|
255
|
+
const included: typeof entries = [];
|
|
256
|
+
for (const entry of entries.slice(0, 24)) {
|
|
257
|
+
const candidate = { cause, entries: [...included, entry], omitted: entries.length - included.length - 1 };
|
|
258
|
+
if (Buffer.byteLength(`dirty_outside_scope: ${JSON.stringify(candidate)}`) > 4096) break;
|
|
259
|
+
included.push(entry);
|
|
260
|
+
}
|
|
261
|
+
return new Error(`dirty_outside_scope: ${JSON.stringify({ cause, entries: included, omitted: entries.length - included.length })}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getUnsupportedIndexFlags(root: string): Array<{ path: string; flags: string[] }> {
|
|
244
265
|
const result = spawnSync("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
|
|
245
266
|
encoding: "utf8",
|
|
246
267
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -249,12 +270,15 @@ function getUnsupportedIndexFlags(root: string): string[] {
|
|
|
249
270
|
throw new Error("failed to inspect index flags via git ls-files -v");
|
|
250
271
|
}
|
|
251
272
|
const entries = result.stdout.split("\0").filter((e) => e.length > 0);
|
|
252
|
-
const flagged: string[] = [];
|
|
273
|
+
const flagged: Array<{ path: string; flags: string[] }> = [];
|
|
253
274
|
for (const entry of entries) {
|
|
254
275
|
const tag = entry[0];
|
|
255
276
|
// Reject h (assume-unchanged), S (skip-worktree), and s (both flags combined) (Finding 1)
|
|
256
277
|
if (tag === "h" || tag === "S" || tag === "s") {
|
|
257
|
-
flagged.push(entry.slice(2)
|
|
278
|
+
flagged.push({ path: entry.slice(2), flags: [
|
|
279
|
+
...(tag === "h" || tag === "s" ? ["assume-unchanged"] : []),
|
|
280
|
+
...(tag === "S" || tag === "s" ? ["skip-worktree"] : []),
|
|
281
|
+
] });
|
|
258
282
|
}
|
|
259
283
|
}
|
|
260
284
|
return flagged;
|
|
@@ -464,7 +488,7 @@ function requireMatchingSettledAudit(
|
|
|
464
488
|
export async function commitBatchChild(input: {
|
|
465
489
|
root: string;
|
|
466
490
|
taskId: string;
|
|
467
|
-
batchId: string;
|
|
491
|
+
batchId: string | null;
|
|
468
492
|
expectedHead: string;
|
|
469
493
|
branch?: string;
|
|
470
494
|
intentPath?: string;
|
|
@@ -536,7 +560,7 @@ export async function commitBatchChild(input: {
|
|
|
536
560
|
// 3b. Reject unsupported index flags before scope inspection (Finding 2)
|
|
537
561
|
const flaggedCommit = getUnsupportedIndexFlags(root);
|
|
538
562
|
if (flaggedCommit.length > 0) {
|
|
539
|
-
throw
|
|
563
|
+
throw scopeRefusal("unsupported_index_flags", flaggedCommit);
|
|
540
564
|
}
|
|
541
565
|
|
|
542
566
|
// 4. Identify changed paths and verify they are strictly within scope envelope + .imm/audit/<task-id>/** (Finding 2 & 4)
|
|
@@ -544,7 +568,7 @@ export async function commitBatchChild(input: {
|
|
|
544
568
|
const outsideScope = changedPaths.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
|
|
545
569
|
if (outsideScope.length > 0) {
|
|
546
570
|
// Nothing is committed
|
|
547
|
-
throw
|
|
571
|
+
throw scopeRefusal("changed_paths", outsideScope.map(path => ({ path })));
|
|
548
572
|
}
|
|
549
573
|
|
|
550
574
|
// 5. Stage only paths needing an update, preserving already-staged deletions/renames (Finding 2)
|
|
@@ -574,7 +598,7 @@ export async function commitBatchChild(input: {
|
|
|
574
598
|
const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
|
|
575
599
|
if (stagedOutside.length > 0) {
|
|
576
600
|
spawnSync("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
577
|
-
throw
|
|
601
|
+
throw scopeRefusal("staged_paths", stagedOutside.map(path => ({ path })));
|
|
578
602
|
}
|
|
579
603
|
const record = authoritative as {
|
|
580
604
|
contract?: string;
|
|
@@ -592,7 +616,7 @@ export async function commitBatchChild(input: {
|
|
|
592
616
|
|
|
593
617
|
// 6. Commit with structured message, Immune-Brain-Batch trailer, and all hooks disabled (Finding 1)
|
|
594
618
|
const goalFirstLine = goal.trim().split(/\r?\n/)[0]?.trim() || taskId;
|
|
595
|
-
const commitMessage = `imm(${taskId}): ${goalFirstLine}\n\
|
|
619
|
+
const commitMessage = `imm(${taskId}): ${goalFirstLine}\n\n${batchId === null ? `Immune-Brain-Task: ${taskId}` : `Immune-Brain-Batch: ${batchId}`}\n`;
|
|
596
620
|
|
|
597
621
|
const commitResult = spawnSync(
|
|
598
622
|
"git",
|
|
@@ -634,11 +658,11 @@ export async function commitBatchChild(input: {
|
|
|
634
658
|
const committedDelta = getCommittedDeltaPaths(root);
|
|
635
659
|
const deltaOutside = committedDelta.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
|
|
636
660
|
if (deltaOutside.length > 0) {
|
|
637
|
-
throw
|
|
661
|
+
throw scopeRefusal("committed_paths", deltaOutside.map(path => ({ path })));
|
|
638
662
|
}
|
|
639
663
|
|
|
640
664
|
// 9. Persist durable commit evidence before returning (Finding 1)
|
|
641
|
-
writeBatchCommitEvidence(root, { batchId, taskId, commit: newHead, parentHead: expectedHead });
|
|
665
|
+
writeBatchCommitEvidence(root, { batchId: batchId ?? "native-task", taskId, commit: newHead, parentHead: expectedHead });
|
|
642
666
|
|
|
643
667
|
return { commit: newHead };
|
|
644
668
|
}
|
|
@@ -653,12 +677,13 @@ export async function commitBatchChild(input: {
|
|
|
653
677
|
export async function lookupBatchCommit(input: {
|
|
654
678
|
root: string;
|
|
655
679
|
taskId: string;
|
|
656
|
-
batchId: string;
|
|
680
|
+
batchId: string | null;
|
|
657
681
|
expectedHead?: string;
|
|
658
682
|
branch?: string;
|
|
683
|
+
lane?: { path: string; base_head: string; lane_commit: string; integrated_commit: string };
|
|
659
684
|
}): Promise<{ commit: string } | null> {
|
|
660
685
|
const { root, taskId, batchId, expectedHead, branch: expectedBranch } = input;
|
|
661
|
-
const FORMAT =
|
|
686
|
+
const FORMAT = `%H%x00%(trailers:key=${batchId === null ? "Immune-Brain-Task" : "Immune-Brain-Batch"},valueonly)%x00%an%x00%ae%x00%s`;
|
|
662
687
|
const subjectPrefix = `imm(${taskId}):`;
|
|
663
688
|
const parseEntry = (entry: string) => {
|
|
664
689
|
const parts = entry.split("\0");
|
|
@@ -676,14 +701,35 @@ export async function lookupBatchCommit(input: {
|
|
|
676
701
|
// evidence so a later commit whose goal text mentions the marker can never
|
|
677
702
|
// shadow it, independent of grep dialect config. The fixed-strings scan
|
|
678
703
|
// below remains only as a discovery fallback for evidence-less commits.
|
|
679
|
-
const
|
|
704
|
+
const retained = readBatchCommitEvidence(root, batchId ?? "native-task", taskId);
|
|
705
|
+
const evidence = retained ?? (input.lane ? readBatchCommitEvidence(input.lane.path, batchId ?? "native-task", taskId) : null);
|
|
706
|
+
if (input.lane && retained && retained.commit !== input.lane.integrated_commit)
|
|
707
|
+
throw new Error("batch_head_lineage_broken: integrated candidate differs from retained delivery");
|
|
708
|
+
if (input.lane && !retained) {
|
|
709
|
+
const lane = input.lane;
|
|
710
|
+
if (!evidence || evidence.commit !== lane.lane_commit || evidence.parent_head !== lane.base_head)
|
|
711
|
+
throw new Error("batch_head_lineage_broken: Lane delivery lacks matching production evidence");
|
|
712
|
+
const common = (path: string) => {
|
|
713
|
+
const result = spawnSync("git", ["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8" });
|
|
714
|
+
if (result.status !== 0) throw new Error("batch_head_lineage_broken: unreadable Lane repository");
|
|
715
|
+
return realpathSync(result.stdout.trim());
|
|
716
|
+
};
|
|
717
|
+
const sourceParent = spawnSync("git", ["-C", root, "rev-parse", `${lane.lane_commit}^@`], { encoding: "utf8" });
|
|
718
|
+
const candidateParent = spawnSync("git", ["-C", root, "rev-parse", `${lane.integrated_commit}^@`], { encoding: "utf8" });
|
|
719
|
+
const parents = candidateParent.stdout.trim().split(/\s+/).filter(Boolean);
|
|
720
|
+
if (common(lane.path) !== common(root) || sourceParent.status !== 0 || sourceParent.stdout.trim() !== lane.base_head ||
|
|
721
|
+
candidateParent.status !== 0 || parents.length !== 1 ||
|
|
722
|
+
!identitiesEqual(changeIdentity(root, lane.base_head, lane.lane_commit), changeIdentity(root, parents[0]!, lane.integrated_commit)))
|
|
723
|
+
throw new Error("batch_head_lineage_broken: integrated candidate differs from produced Lane delivery");
|
|
724
|
+
}
|
|
725
|
+
const recordedCommit = input.lane?.integrated_commit ?? evidence?.commit;
|
|
680
726
|
type Candidate = { commit: string; trailer: string; authorName: string; subject: string };
|
|
681
727
|
let candidates: Candidate[] = [];
|
|
682
728
|
let evidenceBacked = false;
|
|
683
|
-
if (evidence &&
|
|
729
|
+
if (evidence && recordedCommit) {
|
|
684
730
|
const direct = spawnSync(
|
|
685
731
|
"git",
|
|
686
|
-
["-C", root, "log", "-n", "1", `--format=${FORMAT}`,
|
|
732
|
+
["-C", root, "log", "-n", "1", `--format=${FORMAT}`, recordedCommit, "--"],
|
|
687
733
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
688
734
|
);
|
|
689
735
|
if (direct.status !== 0) {
|
|
@@ -691,7 +737,7 @@ export async function lookupBatchCommit(input: {
|
|
|
691
737
|
}
|
|
692
738
|
if (direct.stdout?.trim()) {
|
|
693
739
|
const parsed = parseEntry(direct.stdout);
|
|
694
|
-
if (parsed.commit ===
|
|
740
|
+
if (parsed.commit === recordedCommit) {
|
|
695
741
|
candidates = [parsed];
|
|
696
742
|
evidenceBacked = true;
|
|
697
743
|
}
|
|
@@ -725,7 +771,7 @@ export async function lookupBatchCommit(input: {
|
|
|
725
771
|
|
|
726
772
|
let match: Candidate | null = null;
|
|
727
773
|
for (const candidate of candidates) {
|
|
728
|
-
if (!candidate.commit || candidate.trailer !== batchId) continue;
|
|
774
|
+
if (!candidate.commit || candidate.trailer !== (batchId ?? taskId)) continue;
|
|
729
775
|
if (!candidate.subject.startsWith(subjectPrefix)) continue;
|
|
730
776
|
match = candidate;
|
|
731
777
|
break;
|
|
@@ -736,7 +782,7 @@ export async function lookupBatchCommit(input: {
|
|
|
736
782
|
// Finding 1: Require durable commit evidence persisted by commitBatchChild
|
|
737
783
|
// Rejects forged in-scope external commits created during interruption
|
|
738
784
|
if (!evidenceBacked) {
|
|
739
|
-
const evidenceForMatch = readBatchCommitEvidence(root, batchId, taskId);
|
|
785
|
+
const evidenceForMatch = readBatchCommitEvidence(root, batchId ?? "native-task", taskId);
|
|
740
786
|
if (!evidenceForMatch || evidenceForMatch.commit !== commit) {
|
|
741
787
|
throw new Error(
|
|
742
788
|
`batch_head_lineage_broken: commit ${commit} lacks durable batch runner production evidence`,
|
|
@@ -772,6 +818,13 @@ export async function lookupBatchCommit(input: {
|
|
|
772
818
|
throw new Error(`batch_head_lineage_broken: adopted commit lacks terminal audit pair for ${taskId}`);
|
|
773
819
|
}
|
|
774
820
|
requireMatchingSettledAudit(auditPair, adopted);
|
|
821
|
+
const auditPaths = adopted.runId === null ? auditEvidencePaths(root, taskId) :
|
|
822
|
+
{ record: auditRunRecordPath(taskId, adopted.runId), proof: auditRunTerminalProofPath(taskId, adopted.runId) };
|
|
823
|
+
for (const path of [auditPaths.record, auditPaths.proof]) {
|
|
824
|
+
const committed = spawnSync("git", ["-C", root, "show", `${commit}:${path}`], { encoding: "utf8" });
|
|
825
|
+
if (committed.status !== 0 || committed.stdout !== readSecureProjectFile(root, path))
|
|
826
|
+
throw new Error(`batch_head_lineage_broken: committed terminal evidence differs for ${taskId}`);
|
|
827
|
+
}
|
|
775
828
|
const authoritative = adopted.runId !== null ? adopted.record : auditPair.record;
|
|
776
829
|
const lifecycle =
|
|
777
830
|
"lifecycle" in authoritative ? authoritative.lifecycle : (authoritative as { phase: string }).phase;
|
|
@@ -779,61 +832,366 @@ export async function lookupBatchCommit(input: {
|
|
|
779
832
|
throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
|
|
780
833
|
}
|
|
781
834
|
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
)
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
835
|
+
// Every adopted delivery must be in the current history, even without a predecessor assertion.
|
|
836
|
+
const currentHeadResult = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
837
|
+
const currentHead = currentHeadResult.stdout.trim();
|
|
838
|
+
if (currentHeadResult.status !== 0 || (currentHead !== commit &&
|
|
839
|
+
spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", commit, currentHead], { stdio: "ignore" }).status !== 0))
|
|
840
|
+
throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
|
|
841
|
+
const parentsResult = spawnSync("git", ["-C", root, "rev-parse", `${commit}^@`], { encoding: "utf8" });
|
|
842
|
+
const parents = parentsResult.stdout.trim().split(/\s+/).filter(Boolean);
|
|
843
|
+
if (parentsResult.status !== 0 || parents.length !== 1 || (retained && parents[0] !== retained.parent_head) ||
|
|
844
|
+
(expectedHead !== undefined && parents[0] !== expectedHead))
|
|
845
|
+
throw new Error(`batch_head_lineage_broken: adopted commit parent ${parents.join(",")} does not match expected_head ${expectedHead ?? "single-parent delivery"}`);
|
|
846
|
+
const scopeHint = Array.isArray(authoritative.intent_snapshot.scope_hint)
|
|
847
|
+
? authoritative.intent_snapshot.scope_hint.filter((s): s is string => typeof s === "string") : [];
|
|
848
|
+
const deltaResult = spawnSync("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", parents[0]!, commit],
|
|
849
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
850
|
+
if (deltaResult.status !== 0) throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
|
|
851
|
+
const outside = deltaResult.stdout.split("\0").filter(p => p.length > 0 && !isPathAllowedForChild(p, taskId, scopeHint));
|
|
852
|
+
if (outside.length > 0)
|
|
853
|
+
throw new Error(`batch_head_lineage_broken: adopted commit contains out-of-scope changes: ${outside.join(", ")}`);
|
|
854
|
+
|
|
855
|
+
return { commit };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Retain verified production evidence before the Lane can be released. */
|
|
859
|
+
export async function retainIntegratedLaneEvidence(input: Parameters<typeof lookupBatchCommit>[0] & {
|
|
860
|
+
lane: { path: string; base_head: string; lane_commit: string; integrated_commit: string };
|
|
861
|
+
}): Promise<void> {
|
|
862
|
+
const landed = await lookupBatchCommit(input);
|
|
863
|
+
if (!landed) throw new Error("batch_head_lineage_broken: missing integrated Lane delivery");
|
|
864
|
+
const parent = spawnSync("git", ["-C", input.root, "rev-parse", `${landed.commit}^`], { encoding: "utf8" });
|
|
865
|
+
if (parent.status !== 0) throw new Error("batch_head_lineage_broken: unreadable integrated parent");
|
|
866
|
+
writeBatchCommitEvidence(input.root, { batchId: input.batchId ?? "native-task", taskId: input.taskId,
|
|
867
|
+
commit: landed.commit, parentHead: parent.stdout.trim() });
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Default Git port using the real repository Git operations. */
|
|
871
|
+
export interface ParentCompletionObservation {
|
|
872
|
+
complete: boolean;
|
|
873
|
+
integrated_task_ids: string[];
|
|
874
|
+
missing_task_ids: string[];
|
|
875
|
+
final_passed: boolean;
|
|
876
|
+
head: string;
|
|
877
|
+
reason: string | null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Remote closure and batch `completed` are observations; neither proves delivery. */
|
|
881
|
+
export async function observeParentCompletion(root: string, initiativeSlug: string,
|
|
882
|
+
requiredTaskIds: readonly string[]): Promise<ParentCompletionObservation> {
|
|
883
|
+
if (!requiredTaskIds.length || new Set(requiredTaskIds).size !== requiredTaskIds.length)
|
|
884
|
+
throw new Error("Parent completion requires a non-empty, unique required Task list");
|
|
885
|
+
const headResult = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
886
|
+
if (headResult.status !== 0) throw new Error("Parent completion requires a Git HEAD");
|
|
887
|
+
const head = headResult.stdout.trim();
|
|
888
|
+
const directory = join(root, ".imm", "state", "batches");
|
|
889
|
+
const records: AnyBatchRunStateRecord[] = [];
|
|
890
|
+
if (existsSync(directory)) for (const file of readdirSync(directory).sort()) {
|
|
891
|
+
if (!file.endsWith(".json")) continue;
|
|
892
|
+
const candidate = JSON.parse(readSecureProjectFile(root, `.imm/state/batches/${file}`));
|
|
893
|
+
if (!["assurance_kernel/batch_run_state/v1", "assurance_kernel/batch_run_state/v2"].includes(candidate?.contract)
|
|
894
|
+
|| candidate.initiative_slug !== initiativeSlug) continue;
|
|
895
|
+
const record = readAnyBatchRunState(root, file.slice(0, -5));
|
|
896
|
+
if (!record) throw new Error("Parent batch record disappeared during observation");
|
|
897
|
+
records.push(record);
|
|
898
|
+
}
|
|
899
|
+
const native = readNativeParentFinalReport(root, initiativeSlug);
|
|
900
|
+
const allTaskIds = [...new Set([...requiredTaskIds, ...records.flatMap(record => record.repair_tasks?.map(task => task.task_id) ?? []),
|
|
901
|
+
...(native?.repair_tasks?.map(task => task.task_id) ?? [])])];
|
|
902
|
+
const consolidation = await observeConsolidatedDeliveries(root, initiativeSlug, requiredTaskIds);
|
|
903
|
+
const consolidatedIds = consolidation?.envelope.runs.map(run => run.task_id) ?? [];
|
|
904
|
+
const integrated: string[] = [];
|
|
905
|
+
for (const taskId of allTaskIds) {
|
|
906
|
+
if (consolidatedIds.includes(taskId)) { integrated.push(taskId); continue; }
|
|
907
|
+
const settled = readSettledTaskEvidence(root, taskId);
|
|
908
|
+
if (!settled || settled.proof.terminal_lifecycle !== "done") continue;
|
|
909
|
+
const current = readTaskIntent(root, taskId, settled.record.intent_ref.path);
|
|
910
|
+
if (current.content_hash !== settled.record.intent_ref.content_hash) continue;
|
|
911
|
+
const nativeCommit = await lookupBatchCommit({ root, taskId, batchId: null });
|
|
912
|
+
if (nativeCommit) { integrated.push(taskId); continue; }
|
|
913
|
+
for (const record of records) {
|
|
914
|
+
const child = isLaneBatchRecord(record) ? record.children.find(child => child.task_id === taskId) : null;
|
|
915
|
+
const lane = (child?.state === "integrated" || child?.state === "released") && child.commit && child.lane?.lane_commit
|
|
916
|
+
? { ...child.lane, lane_commit: child.lane.lane_commit, integrated_commit: child.commit } : undefined;
|
|
917
|
+
const commit = await lookupBatchCommit({ root, taskId, batchId: record.batch_id, branch: record.branch, lane })
|
|
918
|
+
?? await lookupBatchCommit({ root, taskId, batchId: `${record.batch_id}.critical`, branch: record.branch })
|
|
919
|
+
?? await lookupBatchCommit({ root, taskId, batchId: `${record.batch_id}.repair`, branch: record.branch });
|
|
920
|
+
if (commit) { integrated.push(taskId); break; }
|
|
808
921
|
}
|
|
922
|
+
}
|
|
923
|
+
const missing = allTaskIds.filter(taskId => !integrated.includes(taskId));
|
|
924
|
+
const finalPassed = (consolidation ? native?.consolidation_digest === consolidation.digest : !readParentConsolidationReceipt(root, initiativeSlug)) && ((native?.final_verification.passed === true && native.final_verification.head === head &&
|
|
925
|
+
JSON.stringify([...native.task_ids].sort()) === JSON.stringify([...requiredTaskIds].sort())) || records.some(record => {
|
|
926
|
+
const final = record.final_verification_result;
|
|
927
|
+
return record.batch_state === "completed" && final?.passed === true && final.head === head;
|
|
928
|
+
}));
|
|
929
|
+
return { complete: missing.length === 0 && finalPassed, integrated_task_ids: integrated,
|
|
930
|
+
missing_task_ids: missing, final_passed: finalPassed, head,
|
|
931
|
+
reason: missing.length ? `Required delivery is absent: ${missing.join(", ")}`
|
|
932
|
+
: finalPassed ? null : "Final verification has not passed on the integrated Git HEAD" };
|
|
933
|
+
}
|
|
809
934
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
935
|
+
/** A critical-only/delivered-only Parent needs native task gates and final observations, never an empty grant. */
|
|
936
|
+
export async function observeNativeParent(root: string, initiativeSlug: string, readInitiative?: InitiativeObservationReader, blockedNativeTasks: readonly string[] = []) {
|
|
937
|
+
if (findExistingActiveBatch(root, initiativeSlug)) return null;
|
|
938
|
+
const settled = findSettledBatchRecord(root, initiativeSlug);
|
|
939
|
+
// Retirement preserves history but no longer owns the Parent's remaining delivery.
|
|
940
|
+
if (settled && settled.batch_state !== "superseded") return null;
|
|
941
|
+
const parent = await (readInitiative ?? (await import("./batch_plan")).observeInitiative)(root, initiativeSlug);
|
|
942
|
+
// An empty Parent is not a native Parent; the batch path reports its own empty-set refusal.
|
|
943
|
+
if (parent.tasks.length === 0) return null;
|
|
944
|
+
const intents = parent.tasks.map(task => ({ task, intent: readTaskTombstone(root, task.task_id)
|
|
945
|
+
? readTaskIntent(root, task.task_id) : observeEnrollmentTaskIntent(root, task.task_id) }));
|
|
946
|
+
if (intents.some(({ task, intent }) => intent.intent.risk !== "critical" && !readSettledTaskEvidence(root, task.task_id))) return null;
|
|
947
|
+
const native = readNativeParentFinalReport(root, initiativeSlug);
|
|
948
|
+
const proof = await observeParentCompletion(root, initiativeSlug, parent.tasks.map(task => task.task_id));
|
|
949
|
+
const planDigest = `sha256:${createHash("sha256").update(JSON.stringify(intents.map(({ task, intent }) =>
|
|
950
|
+
({ task_id: task.task_id, blocked_by: task.blocked_by, intent: intent.content_hash })))).digest("hex")}`;
|
|
951
|
+
const activeTask = findActiveClaimForCriticalIntegration(root);
|
|
952
|
+
const action = activeTask ? "drive" : proof.complete ? "complete" :
|
|
953
|
+
parent.tasks.some(task => readSettledTaskEvidence(root, task.task_id)?.proof.terminal_lifecycle === "done" && !proof.integrated_task_ids.includes(task.task_id)) ||
|
|
954
|
+
native?.repair_tasks?.some(task => readSettledTaskEvidence(root, task.task_id)?.proof.terminal_lifecycle === "done" && !proof.integrated_task_ids.includes(task.task_id)) ? "integrate" : "enroll";
|
|
955
|
+
const ready = parent.tasks.find(task => !blockedNativeTasks.includes(task.task_id) && !readSettledTaskEvidence(root, task.task_id) &&
|
|
956
|
+
task.blocked_by.every(id => proof.integrated_task_ids.includes(id)));
|
|
957
|
+
const originalDelivered = parent.tasks.every(task => proof.integrated_task_ids.includes(task.task_id));
|
|
958
|
+
const taskId = activeTask ?? ready?.task_id ?? null;
|
|
959
|
+
if (activeTask && !parent.tasks.some(task => task.task_id === activeTask) && !native?.repair_tasks?.some(task => task.task_id === activeTask))
|
|
960
|
+
throw new Error("Parent entry has another task's active Kernel owner");
|
|
961
|
+
return { state: "native_parent" as const, initiative_slug: initiativeSlug, plan_digest: planDigest, batch_id: null,
|
|
962
|
+
action: action === "enroll" && originalDelivered ? native?.final_verification.passed === false ? "repair" : "verify" : action,
|
|
963
|
+
task_id: taskId, proof, parent, native, next_action: action === "drive" ? `Drive active task ${taskId} from Kernel status` :
|
|
964
|
+
action === "integrate" ? `Re-enter start_unattended_batch for ${initiativeSlug} to integrate settled native deliveries` :
|
|
965
|
+
ready ? `Invoke native single-task Enrollment for ${ready.task_id}; no ordinary BatchAuthorization is needed` :
|
|
966
|
+
originalDelivered ? "Run recorded Parent final verification; diagnose failure and prepare a stable original-goal repair candidate before its fresh native Enrollment" :
|
|
967
|
+
blockedNativeTasks.length ? `Native gates parked for ${blockedNativeTasks.join(", ")}; resolve their same-Host recovery and enter imm-run explicitly` :
|
|
968
|
+
"No dependency-ready required task remains; report concrete blocked predecessors" };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** What a native Parent run is about to do; the Host renders it in its own native confirmation. */
|
|
972
|
+
export interface NativeParentEffects {
|
|
973
|
+
initiative_slug: string;
|
|
974
|
+
plan_digest: string;
|
|
975
|
+
children: Array<{ task_id: string; slice_id: string }>;
|
|
976
|
+
/** Settled deliveries that will be committed before the final commands run. */
|
|
977
|
+
integrate_task_ids: string[];
|
|
978
|
+
final_commands: string[];
|
|
979
|
+
delivery_consolidation?: ConsolidationReceipt;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export type NativeParentRefusal = { state: "rejected" | "cancelled" | "blocked"; reason: string; recovery_action: string };
|
|
983
|
+
|
|
984
|
+
/** Resolves null once the literal user accepted exactly these effects, otherwise the Host's refusal envelope. */
|
|
985
|
+
export type NativeParentGate = (effects: NativeParentEffects) => Promise<NativeParentRefusal | null>;
|
|
986
|
+
|
|
987
|
+
export async function advanceNativeParent(root: string, initiativeSlug: string, commands?: string[], readInitiative?: InitiativeObservationReader,
|
|
988
|
+
tracker?: import("./batch_runner").BatchTrackerPort, gate?: NativeParentGate, consolidationInput?: DeliveryConsolidation) {
|
|
989
|
+
let facts = await observeNativeParent(root, initiativeSlug, readInitiative);
|
|
990
|
+
if (!facts) {
|
|
991
|
+
if (consolidationInput) throw new Error("delivery consolidation is available only for a native Parent");
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
let consolidatedNow = false;
|
|
995
|
+
const selection = parseDeliveryConsolidation(consolidationInput);
|
|
996
|
+
let receipt = readParentConsolidationReceipt(root, initiativeSlug);
|
|
997
|
+
if (receipt && selection && JSON.stringify(receipt.envelope.selection) !== JSON.stringify(selection))
|
|
998
|
+
throw new Error("consolidation source/final run selection changed");
|
|
999
|
+
if (selection && !receipt?.commit) {
|
|
1000
|
+
if (facts.action === "drive" || facts.proof.missing_task_ids.some(id => !selection.source_runs.some(source => source.task_id === id)))
|
|
1001
|
+
throw new Error("consolidation must cover every remaining original Parent delivery with no active owner");
|
|
1002
|
+
const finalCommands = commands ?? receipt?.envelope.final_commands;
|
|
1003
|
+
if (!finalCommands?.length) throw new Error("consolidation requires explicit final verification commands");
|
|
1004
|
+
const currentHead = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).stdout.trim();
|
|
1005
|
+
const recovering = receipt && receipt.envelope.head !== currentHead;
|
|
1006
|
+
if (recovering) {
|
|
1007
|
+
receipt = { ...receipt!, commit: currentHead };
|
|
1008
|
+
await verifyConsolidatedCommit(root, receipt);
|
|
1009
|
+
if (facts.plan_digest !== receipt.envelope.plan_digest || JSON.stringify(finalCommands) !== JSON.stringify(receipt.envelope.final_commands))
|
|
1010
|
+
throw new Error("consolidation recovery plan/commands changed");
|
|
1011
|
+
} else {
|
|
1012
|
+
const envelope = await prepareDeliveryConsolidation(root, initiativeSlug, facts.plan_digest,
|
|
1013
|
+
facts.parent.tasks.map(task => task.task_id), selection, finalCommands);
|
|
1014
|
+
const prepared: ConsolidationReceipt = { contract: "assurance_kernel/parent_delivery_consolidation/v1",
|
|
1015
|
+
envelope, digest: consolidationDigest(envelope), commit: null };
|
|
1016
|
+
if (receipt && receipt.digest !== prepared.digest) throw new Error("pending consolidation changed; preserve receipt and diagnose drift");
|
|
1017
|
+
receipt = prepared;
|
|
821
1018
|
}
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1019
|
+
const accepted = receipt!;
|
|
1020
|
+
const refused = gate ? await gate({ initiative_slug: initiativeSlug, plan_digest: accepted.digest,
|
|
1021
|
+
children: facts.parent.tasks.map(task => ({ task_id: task.task_id, slice_id: task.slice_id })),
|
|
1022
|
+
integrate_task_ids: accepted.envelope.runs.map(run => run.task_id), final_commands: finalCommands,
|
|
1023
|
+
delivery_consolidation: accepted }) : { state: "rejected" as const,
|
|
1024
|
+
reason: "Parent delivery consolidation requires native confirmation", recovery_action: "retry through a fresh native gate in the current Host" };
|
|
1025
|
+
if (refused) return refused;
|
|
1026
|
+
const fresh = await observeNativeParent(root, initiativeSlug, readInitiative);
|
|
1027
|
+
if (!fresh || fresh.plan_digest !== accepted.envelope.plan_digest || fresh.action === "drive")
|
|
1028
|
+
throw new Error("Parent plan/owner changed during consolidation confirmation");
|
|
1029
|
+
const confirmedHead = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
1030
|
+
if (confirmedHead.status !== 0 || confirmedHead.stdout.trim() !== (accepted.commit ?? accepted.envelope.head))
|
|
1031
|
+
throw new Error("consolidation HEAD changed during confirmation");
|
|
1032
|
+
if (recovering) await verifyConsolidatedCommit(root, accepted);
|
|
1033
|
+
else {
|
|
1034
|
+
const rechecked = await prepareDeliveryConsolidation(root, initiativeSlug, fresh.plan_digest,
|
|
1035
|
+
fresh.parent.tasks.map(task => task.task_id), selection, finalCommands);
|
|
1036
|
+
if (consolidationDigest(rechecked) !== accepted.digest) throw new Error("consolidation HEAD/tree/identity changed during confirmation");
|
|
1037
|
+
writeParentConsolidationReceipt(root, accepted);
|
|
1038
|
+
const committed = await commitBatchChild({ root, taskId: selection.task_id, batchId: null,
|
|
1039
|
+
expectedHead: accepted.envelope.head, branch: accepted.envelope.branch });
|
|
1040
|
+
receipt = { ...accepted, commit: committed.commit };
|
|
1041
|
+
await verifyConsolidatedCommit(root, receipt);
|
|
1042
|
+
}
|
|
1043
|
+
writeParentConsolidationReceipt(root, receipt!);
|
|
1044
|
+
// Final commands run only after the approved composed commit and all source proofs were verified.
|
|
1045
|
+
const final = await runFinalVerification(root, finalCommands);
|
|
1046
|
+
writeNativeParentFinalReport(root, { contract: "assurance_kernel/parent_final_report/v1", initiative_slug: initiativeSlug,
|
|
1047
|
+
task_ids: facts.parent.tasks.map(task => task.task_id), final_verification: final,
|
|
1048
|
+
consolidation_digest: receipt!.digest, repair_tasks: facts.native?.repair_tasks });
|
|
1049
|
+
consolidatedNow = true;
|
|
1050
|
+
facts = await observeNativeParent(root, initiativeSlug, readInitiative);
|
|
1051
|
+
if (!facts) throw new Error("Parent disappeared after delivery consolidation");
|
|
1052
|
+
}
|
|
1053
|
+
if (receipt && !receipt.commit && !selection)
|
|
1054
|
+
throw new Error("pending delivery consolidation requires its exact source/final run selection through a fresh native entry");
|
|
1055
|
+
const recordedCommands = facts.native?.final_verification.results.map(result => result.command)
|
|
1056
|
+
?? (receipt?.commit ? receipt.envelope.final_commands : undefined);
|
|
1057
|
+
if (commands && recordedCommands && JSON.stringify(commands) !== JSON.stringify(recordedCommands))
|
|
1058
|
+
throw new Error("Parent final commands changed; start a fresh explicit Parent entry");
|
|
1059
|
+
// New settled-delivery commits need confirmation even without commands; recorded commands
|
|
1060
|
+
// authorize their replay, not integration of a later delivery.
|
|
1061
|
+
if (facts.action === "integrate" || (commands?.length && !recordedCommands)) {
|
|
1062
|
+
const integrateIds = [...facts.parent.tasks.map(task => task.task_id), ...(facts.native?.repair_tasks?.map(task => task.task_id) ?? [])]
|
|
1063
|
+
.filter(taskId => readSettledTaskEvidence(root, taskId)?.proof.terminal_lifecycle === "done" && !facts!.proof.integrated_task_ids.includes(taskId));
|
|
1064
|
+
const missing = facts.proof.missing_task_ids;
|
|
1065
|
+
const runsNow = (["verify", "repair"].includes(facts.action) && missing.length === 0) ||
|
|
1066
|
+
(facts.action === "integrate" && missing.every(taskId => integrateIds.includes(taskId)));
|
|
1067
|
+
if (facts.action === "integrate" || runsNow) {
|
|
1068
|
+
const refused: NativeParentRefusal | null = gate ? await gate({ initiative_slug: initiativeSlug, plan_digest: facts.plan_digest,
|
|
1069
|
+
children: facts.parent.tasks.map(task => ({ task_id: task.task_id, slice_id: task.slice_id })),
|
|
1070
|
+
integrate_task_ids: facts.action === "integrate" ? integrateIds : [],
|
|
1071
|
+
final_commands: runsNow ? recordedCommands ?? commands ?? [] : [] }) :
|
|
1072
|
+
{ state: "rejected", reason: "Parent delivery integration and final verification require native confirmation before execution",
|
|
1073
|
+
recovery_action: "retry through a fresh native gate in the current Host" };
|
|
1074
|
+
if (refused) return refused;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (facts.action === "integrate") {
|
|
1078
|
+
for (const taskId of [...facts.parent.tasks.map(task => task.task_id), ...(facts.native?.repair_tasks?.map(task => task.task_id) ?? [])]) {
|
|
1079
|
+
if (readSettledTaskEvidence(root, taskId)?.proof.terminal_lifecycle !== "done" || facts.proof.integrated_task_ids.includes(taskId)) continue;
|
|
1080
|
+
const prior = await lookupBatchCommit({ root, taskId, batchId: null });
|
|
1081
|
+
if (!prior) await commitBatchChild({ root, taskId, batchId: null, expectedHead: spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).stdout.trim() });
|
|
828
1082
|
}
|
|
1083
|
+
facts = await observeNativeParent(root, initiativeSlug, readInitiative);
|
|
1084
|
+
if (!facts) throw new Error("Parent plan changed while integrating native deliveries");
|
|
829
1085
|
}
|
|
1086
|
+
if (!consolidatedNow && ["verify", "repair"].includes(facts.action) && facts.proof.missing_task_ids.length === 0) {
|
|
1087
|
+
const finalCommands = recordedCommands ?? commands;
|
|
1088
|
+
if (finalCommands) {
|
|
1089
|
+
const final = await runFinalVerification(root, finalCommands);
|
|
1090
|
+
writeNativeParentFinalReport(root, { contract: "assurance_kernel/parent_final_report/v1", initiative_slug: initiativeSlug,
|
|
1091
|
+
task_ids: facts.parent.tasks.map(task => task.task_id), final_verification: final, repair_tasks: facts.native?.repair_tasks,
|
|
1092
|
+
...(receipt?.commit ? { consolidation_digest: receipt.digest } : {}) });
|
|
1093
|
+
facts = await observeNativeParent(root, initiativeSlug, readInitiative);
|
|
1094
|
+
if (!facts) throw new Error("Parent plan changed during final verification");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (facts.action === "complete" && tracker) {
|
|
1098
|
+
const last = facts.parent.tasks.at(-1)!;
|
|
1099
|
+
const terminal = readTaskTombstone(root, last.task_id);
|
|
1100
|
+
if (terminal?.terminal_lifecycle === "done") {
|
|
1101
|
+
try {
|
|
1102
|
+
const sync = await tracker.markTerminal(root, { task_id: last.task_id, phase: "done", terminal_event_id: terminal.terminal_event_id });
|
|
1103
|
+
if (sync.status === "failed") facts.next_action = "Local Parent completed; retry tracker synchronization on this Host";
|
|
1104
|
+
} catch { facts.next_action = "Local Parent completed; retry tracker synchronization on this Host"; }
|
|
1105
|
+
} else facts.next_action = "Local Parent completed; recover exact terminal evidence before tracker synchronization";
|
|
1106
|
+
}
|
|
1107
|
+
const { parent, native, ...observation } = facts;
|
|
1108
|
+
return { ...observation, final_verification: native?.final_verification,
|
|
1109
|
+
final_verification_commands: native?.final_verification.results.map(result => result.command) ?? commands };
|
|
1110
|
+
}
|
|
830
1111
|
|
|
831
|
-
|
|
1112
|
+
/** Called only after observing a successful native Enrollment in a bound Parent. */
|
|
1113
|
+
export async function recordParentRepair(root: string, batchId: string | null, taskId: string, initiativeSlug?: string): Promise<void> {
|
|
1114
|
+
const batch = batchId === null ? null : readAnyBatchRunState(root, batchId);
|
|
1115
|
+
const native = batchId === null && initiativeSlug ? readNativeParentFinalReport(root, initiativeSlug) : null;
|
|
1116
|
+
const container = batch ?? native;
|
|
1117
|
+
if (!container || (batch ? batch.final_verification_result?.passed : native?.final_verification.passed) !== false) return;
|
|
1118
|
+
if (batch) {
|
|
1119
|
+
const parent = await (await import("./batch_plan")).observeInitiative(root, batch.initiative_slug);
|
|
1120
|
+
if (parent.tasks.some(child => child.task_id === taskId)) return;
|
|
1121
|
+
} else if (native!.task_ids.includes(taskId)) return;
|
|
1122
|
+
const status = await (await import("../assurance/coordinator")).projectTaskStatus(root, taskId);
|
|
1123
|
+
if (status.error || status.claim?.task_id !== taskId || status.projection.lifecycle !== "active" || !status.projection.run_id)
|
|
1124
|
+
throw new Error("repair native Enrollment has no exact active run identity");
|
|
1125
|
+
if (container.repair_tasks?.some(task => task.task_id === taskId && task.run_id === status.projection.run_id)) return;
|
|
1126
|
+
if (container.repair_tasks?.some(task => task.task_id === taskId)) throw new Error("Parent repair task was replaced by another run");
|
|
1127
|
+
container.repair_tasks = [...(container.repair_tasks ?? []), { task_id: taskId, run_id: status.projection.run_id }];
|
|
1128
|
+
if (batch) { if (isLaneBatchRecord(batch)) writeBatchLaneRunState(root, batch); else writeBatchRunState(root, batch); }
|
|
1129
|
+
else writeNativeParentFinalReport(root, native!);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Native critical tasks retain their own authority and consume no batch child slot. */
|
|
1133
|
+
export async function integrateSettledCriticalSlices(root: string, initiativeSlug: string,
|
|
1134
|
+
readInitiative?: InitiativeObservationReader): Promise<void> {
|
|
1135
|
+
let record = findExistingActiveBatch(root, initiativeSlug);
|
|
1136
|
+
if (record?.corrupt) return; // The existing preflight renders the structured refusal; no integration occurs.
|
|
1137
|
+
let batch = record && !record.corrupt ? record.record : findSettledBatchRecord(root, initiativeSlug);
|
|
1138
|
+
if (!batch || !["running", "completed"].includes(batch.batch_state)) return;
|
|
1139
|
+
batch = readAnyBatchRunState(root, batch.batch_id);
|
|
1140
|
+
if (!batch) throw new Error("Parent batch disappeared before critical integration");
|
|
1141
|
+
if (findActiveClaimForCriticalIntegration(root)) return;
|
|
1142
|
+
const parent = await (readInitiative ?? (await import("./batch_plan")).observeInitiative)(root, initiativeSlug);
|
|
1143
|
+
const persist = () => { batch = isLaneBatchRecord(batch!) ? writeBatchLaneRunState(root, batch!) : writeBatchRunState(root, batch!); };
|
|
1144
|
+
for (const child of [...parent.tasks, ...(batch.repair_tasks ?? []).map(task => ({ task_id: task.task_id }))]) {
|
|
1145
|
+
if (batch.children.some(candidate => candidate.task_id === child.task_id)) continue;
|
|
1146
|
+
const repair = batch.repair_tasks?.find(task => task.task_id === child.task_id);
|
|
1147
|
+
const nativeBatchId = `${batch.batch_id}.${repair ? "repair" : "critical"}`;
|
|
1148
|
+
const settled = readSettledTaskEvidence(root, child.task_id);
|
|
1149
|
+
if (!settled || settled.proof.terminal_lifecycle !== "done" || (!repair && settled.record.intent_snapshot.risk !== "critical")) continue;
|
|
1150
|
+
if (repair && settled.runId !== repair.run_id) throw new Error("repair settlement belongs to a different enrolled run");
|
|
1151
|
+
const intent = readTaskIntent(root, child.task_id, settled.record.intent_ref.path);
|
|
1152
|
+
if (intent.content_hash !== settled.record.intent_ref.content_hash) throw new Error("critical settled Intent changed before integration");
|
|
1153
|
+
const existing = await lookupBatchCommit({ root, taskId: child.task_id, batchId: nativeBatchId, branch: batch.branch });
|
|
1154
|
+
if (existing) {
|
|
1155
|
+
const live = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
1156
|
+
if (live.status !== 0) throw new Error("cannot verify recovered critical integration HEAD");
|
|
1157
|
+
if (live.stdout.trim() === existing.commit && expectedBatchHead(batch) !== existing.commit) {
|
|
1158
|
+
const predecessor = spawnSync("git", ["-C", root, "rev-parse", `${existing.commit}^`], { encoding: "utf8" });
|
|
1159
|
+
if (predecessor.status !== 0 || predecessor.stdout.trim() !== expectedBatchHead(batch))
|
|
1160
|
+
throw new Error("recovered critical integration does not extend the exact Parent HEAD");
|
|
1161
|
+
batch = { ...batch, adopted_heads: [...(batch.adopted_heads ?? []), { from: expectedBatchHead(batch), to: existing.commit }] };
|
|
1162
|
+
delete batch.final_verification_result; persist();
|
|
1163
|
+
}
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const head = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
1167
|
+
if (head.status !== 0) throw new Error("cannot read native integration HEAD");
|
|
1168
|
+
if (head.stdout.trim() !== expectedBatchHead(batch)) {
|
|
1169
|
+
const lineage = classifyBatchLineage({ root, branch: batch.branch ?? "", expectedHead: expectedBatchHead(batch),
|
|
1170
|
+
childCommits: batch.commits, batchId: batch.batch_id });
|
|
1171
|
+
if (lineage.kind === "broken") throw new Error(lineage.message);
|
|
1172
|
+
if (lineage.kind === "fast_forward") {
|
|
1173
|
+
batch = { ...batch, adopted_heads: [...(batch.adopted_heads ?? []), { from: expectedBatchHead(batch), to: lineage.head }] };
|
|
1174
|
+
delete batch.final_verification_result; persist();
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const from = head.stdout.trim();
|
|
1178
|
+
const { commit } = await commitBatchChild({ root, taskId: child.task_id, batchId: nativeBatchId, expectedHead: from, branch: batch.branch });
|
|
1179
|
+
batch = { ...batch, adopted_heads: [...(batch.adopted_heads ?? []), { from, to: commit }] };
|
|
1180
|
+
delete batch.final_verification_result;
|
|
1181
|
+
persist();
|
|
1182
|
+
}
|
|
1183
|
+
if (batch.batch_state === "completed" && batch.final_verification?.length && !batch.final_verification_result) {
|
|
1184
|
+
const proof = await observeParentCompletion(root, parent.initiative_id, parent.tasks.map(child => child.task_id));
|
|
1185
|
+
if (!proof.missing_task_ids.length) {
|
|
1186
|
+
batch.final_verification_result = await runFinalVerification(root, batch.final_verification);
|
|
1187
|
+
persist();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
832
1190
|
}
|
|
833
1191
|
|
|
834
|
-
/** Default Git port using the real repository Git operations. */
|
|
835
1192
|
export function createDefaultBatchGitPort(): BatchRunnerGitPort {
|
|
836
1193
|
return {
|
|
1194
|
+
retainLaneEvidence: retainIntegratedLaneEvidence,
|
|
837
1195
|
preflight: runBatchGitPreflight,
|
|
838
1196
|
commitChild: (root, taskId, batchId, head, branch, intentPath) =>
|
|
839
1197
|
commitBatchChild({ root, taskId, batchId, expectedHead: head, branch, intentPath }),
|