immune-brain 5.0.0 → 5.1.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 +223 -12
- package/README.zh-CN.md +228 -11
- 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 +18 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +189 -3
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1885 -149
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-planner.md +4 -1
- package/plugins/immune-brain/dist/imm-run.md +198 -3
- package/plugins/immune-brain/dist/role-prompts/code-review.md +10 -0
- package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +22 -4
- package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +16 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +105 -58
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +89 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
- package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
- package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +55 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +10 -0
- package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +2 -2
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +946 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +48 -33
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +13 -1
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +22 -1
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +398 -12
- package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +4 -0
|
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
42
|
-
var PLUGIN_VERSION = "5.
|
|
42
|
+
var PLUGIN_VERSION = "5.1.1";
|
|
43
43
|
|
|
44
44
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
45
45
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -48,7 +48,8 @@ var PRIVILEGED_OPERATIONS = [
|
|
|
48
48
|
"request_authorization",
|
|
49
49
|
"approve_breaking_intent_revision",
|
|
50
50
|
"stop",
|
|
51
|
-
"start_unattended_batch"
|
|
51
|
+
"start_unattended_batch",
|
|
52
|
+
"retire_stale_batch"
|
|
52
53
|
];
|
|
53
54
|
var RECOVERY_ACTIONS = {
|
|
54
55
|
interaction_not_opened: "retry through a fresh native gate in the current Host",
|
|
@@ -732,6 +733,7 @@ var KERNEL_DB_RELATIVE = ".imm/state/kernel.sqlite";
|
|
|
732
733
|
var KERNEL_STORE_SCHEMA_VERSION = 1;
|
|
733
734
|
var FILE_STORE_CLAIM_RELATIVE = ".imm/state/active-claim.json";
|
|
734
735
|
var FILE_STORE_WORKSPACE_RELATIVE = ".imm/state/workspace.json";
|
|
736
|
+
var FILE_STORE_TASKS_RELATIVE = ".imm/state/tasks";
|
|
735
737
|
var FILE_STORE_TRANSACTIONS_RELATIVE = ".imm/state/transactions";
|
|
736
738
|
var BATCH_STATE_RELATIVE = ".imm/state/batches";
|
|
737
739
|
function stateDatabasePath() {
|
|
@@ -790,6 +792,25 @@ function batchCommitEvidencePath(batchId, taskId) {
|
|
|
790
792
|
validateTaskId(taskId);
|
|
791
793
|
return `${BATCH_STATE_RELATIVE}/commits/${batchId}-${taskId}.json`;
|
|
792
794
|
}
|
|
795
|
+
function entryStatus(root, relativePath) {
|
|
796
|
+
const candidate = resolve(root, relativePath);
|
|
797
|
+
let stat;
|
|
798
|
+
try {
|
|
799
|
+
stat = lstatSync2(candidate);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const code = error.code;
|
|
802
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
803
|
+
return "absent";
|
|
804
|
+
return "other";
|
|
805
|
+
}
|
|
806
|
+
if (stat.isSymbolicLink())
|
|
807
|
+
return "symlink";
|
|
808
|
+
if (stat.isFile())
|
|
809
|
+
return "file";
|
|
810
|
+
if (stat.isDirectory())
|
|
811
|
+
return "directory";
|
|
812
|
+
return "other";
|
|
813
|
+
}
|
|
793
814
|
function listEntries(root, relativePath) {
|
|
794
815
|
const candidate = resolve(root, relativePath);
|
|
795
816
|
try {
|
|
@@ -798,6 +819,44 @@ function listEntries(root, relativePath) {
|
|
|
798
819
|
return null;
|
|
799
820
|
}
|
|
800
821
|
}
|
|
822
|
+
function readSmallFile(root, relativePath) {
|
|
823
|
+
const candidate = resolve(root, relativePath);
|
|
824
|
+
try {
|
|
825
|
+
const fd = openSync2(candidate, FS_CONSTANTS.O_RDONLY | (FS_CONSTANTS.O_NOFOLLOW ?? 0));
|
|
826
|
+
try {
|
|
827
|
+
const stat = fstatSync2(fd);
|
|
828
|
+
if (stat.size > 4 * 1024 * 1024)
|
|
829
|
+
throw new Error("file exceeds the inspection read bound");
|
|
830
|
+
return readFileSync(fd, "utf8");
|
|
831
|
+
} finally {
|
|
832
|
+
closeSync2(fd);
|
|
833
|
+
}
|
|
834
|
+
} catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function inspectRetiredStateResidue(root) {
|
|
839
|
+
const tasksStatus = entryStatus(root, FILE_STORE_TASKS_RELATIVE);
|
|
840
|
+
if (tasksStatus !== "absent" && tasksStatus !== "directory")
|
|
841
|
+
throw new Error(`${FILE_STORE_TASKS_RELATIVE} is not a regular directory`);
|
|
842
|
+
const entries = tasksStatus === "absent" ? [] : listEntries(root, FILE_STORE_TASKS_RELATIVE);
|
|
843
|
+
if (entries === null)
|
|
844
|
+
throw new Error(`${FILE_STORE_TASKS_RELATIVE} is unreadable`);
|
|
845
|
+
const workspaceStatus = entryStatus(root, FILE_STORE_WORKSPACE_RELATIVE);
|
|
846
|
+
let owner = null;
|
|
847
|
+
if (workspaceStatus !== "absent") {
|
|
848
|
+
if (workspaceStatus !== "file")
|
|
849
|
+
throw new Error(`${FILE_STORE_WORKSPACE_RELATIVE} is not a regular file`);
|
|
850
|
+
const content = readSmallFile(root, FILE_STORE_WORKSPACE_RELATIVE);
|
|
851
|
+
if (content === null)
|
|
852
|
+
throw new Error(`${FILE_STORE_WORKSPACE_RELATIVE} is unreadable`);
|
|
853
|
+
const raw = JSON.parse(content);
|
|
854
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw) || raw.contract !== "assurance_kernel/workspace/v1" || raw.current_working !== null && (typeof raw.current_working !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(raw.current_working)) || raw.current_working === null && Object.keys(raw).some((key) => !["contract", "current_working"].includes(key)))
|
|
855
|
+
throw new Error(`${FILE_STORE_WORKSPACE_RELATIVE} has invalid workspace content`);
|
|
856
|
+
owner = raw.current_working;
|
|
857
|
+
}
|
|
858
|
+
return { task_entries: entries, workspace_owner: owner };
|
|
859
|
+
}
|
|
801
860
|
|
|
802
861
|
// plugins/immune-brain/runtime/kernel/sqlite_store.ts
|
|
803
862
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -2144,6 +2203,7 @@ function parseApprovalV2(value, index, violations, allowReviewRevision = false)
|
|
|
2144
2203
|
"actor_id",
|
|
2145
2204
|
"summary",
|
|
2146
2205
|
"advisory_findings",
|
|
2206
|
+
"reviewer_verdict_sha256",
|
|
2147
2207
|
...allowReviewRevision ? ["review_revision"] : []
|
|
2148
2208
|
], `record.approvals[${index}]`, violations);
|
|
2149
2209
|
const intentContentHash = stringAt(item.intent_content_hash, `record.approvals[${index}].intent_content_hash`, violations);
|
|
@@ -2164,7 +2224,8 @@ function parseApprovalV2(value, index, violations, allowReviewRevision = false)
|
|
|
2164
2224
|
...allowReviewRevision && item.review_revision !== undefined ? { review_revision: parseReviewRevisionIdentity(item.review_revision, `record.approvals[${index}].review_revision`, violations) } : {},
|
|
2165
2225
|
...item.advisory_findings !== undefined ? {
|
|
2166
2226
|
advisory_findings: arrayAt(item.advisory_findings, `record.approvals[${index}].advisory_findings`, violations).map((entry, advisoryIndex) => parseAdvisoryFinding(entry, `record.approvals[${index}].advisory_findings[${advisoryIndex}]`, violations))
|
|
2167
|
-
} : {}
|
|
2227
|
+
} : {},
|
|
2228
|
+
...item.reviewer_verdict_sha256 !== undefined ? { reviewer_verdict_sha256: stringAt(item.reviewer_verdict_sha256, `record.approvals[${index}].reviewer_verdict_sha256`, violations) } : {}
|
|
2168
2229
|
};
|
|
2169
2230
|
}
|
|
2170
2231
|
function parseAdvisoryFinding(value, path, violations) {
|
|
@@ -2211,6 +2272,7 @@ function parseAttestationV3(value, index, acceptanceIds, violations, allowReview
|
|
|
2211
2272
|
"summary",
|
|
2212
2273
|
"acceptance_results",
|
|
2213
2274
|
"advisory_findings",
|
|
2275
|
+
"reviewer_verdict_sha256",
|
|
2214
2276
|
...allowReviewRevision ? ["review_revision"] : []
|
|
2215
2277
|
], path, violations);
|
|
2216
2278
|
const kind = enumAt(item.kind, APPROVAL_KINDS, `${path}.kind`, violations);
|
|
@@ -2248,6 +2310,11 @@ function parseAttestationV3(value, index, acceptanceIds, violations, allowReview
|
|
|
2248
2310
|
const advisoryFindings = item.advisory_findings === undefined ? undefined : arrayAt(item.advisory_findings, `${path}.advisory_findings`, violations).map((entry, advisoryIndex) => parseAdvisoryFinding(entry, `${path}.advisory_findings[${advisoryIndex}]`, violations));
|
|
2249
2311
|
if (advisoryFindings !== undefined && kind !== "review")
|
|
2250
2312
|
violations.push(`${path}.advisory_findings is only valid on review attestations`);
|
|
2313
|
+
const reviewerVerdictSha256 = item.reviewer_verdict_sha256 === undefined ? undefined : stringAt(item.reviewer_verdict_sha256, `${path}.reviewer_verdict_sha256`, violations);
|
|
2314
|
+
if (reviewerVerdictSha256 !== undefined && kind !== "review")
|
|
2315
|
+
violations.push(`${path}.reviewer_verdict_sha256 is only valid on review attestations`);
|
|
2316
|
+
if (reviewerVerdictSha256 !== undefined && !SHA256_HEX.test(reviewerVerdictSha256))
|
|
2317
|
+
violations.push(`${path}.reviewer_verdict_sha256 must be sha256:<64 hex>`);
|
|
2251
2318
|
return {
|
|
2252
2319
|
id: stringAt(item.id, `${path}.id`, violations),
|
|
2253
2320
|
kind,
|
|
@@ -2259,7 +2326,8 @@ function parseAttestationV3(value, index, acceptanceIds, violations, allowReview
|
|
|
2259
2326
|
summary: stringAt(item.summary, `${path}.summary`, violations),
|
|
2260
2327
|
acceptance_results: acceptanceResults,
|
|
2261
2328
|
...reviewRevision ? { review_revision: reviewRevision } : {},
|
|
2262
|
-
...advisoryFindings !== undefined ? { advisory_findings: advisoryFindings } : {}
|
|
2329
|
+
...advisoryFindings !== undefined ? { advisory_findings: advisoryFindings } : {},
|
|
2330
|
+
...reviewerVerdictSha256 !== undefined ? { reviewer_verdict_sha256: reviewerVerdictSha256 } : {}
|
|
2263
2331
|
};
|
|
2264
2332
|
}
|
|
2265
2333
|
function parseTaskRecordAtVersion(raw, version) {
|
|
@@ -2643,6 +2711,11 @@ function readBoundActiveSpec(root, intent, required = false) {
|
|
|
2643
2711
|
throw error;
|
|
2644
2712
|
}
|
|
2645
2713
|
}
|
|
2714
|
+
function assertValidSpecBinding(intent) {
|
|
2715
|
+
const binding = inspectSpecBinding(intent);
|
|
2716
|
+
if (!binding.ok)
|
|
2717
|
+
throw new KernelInvariantError([binding.message]);
|
|
2718
|
+
}
|
|
2646
2719
|
function inspectSpecBinding(intent) {
|
|
2647
2720
|
const active = intent.scope_hint.filter((path) => ACTIVE_SPEC_RE.test(path));
|
|
2648
2721
|
const archived = intent.scope_hint.filter((path) => ARCHIVED_SPEC_RE.test(path));
|
|
@@ -2696,6 +2769,8 @@ function asTaskDiffSnapshot(value) {
|
|
|
2696
2769
|
return { diff_hash: value.diff_hash, changed_paths: value.changed_paths };
|
|
2697
2770
|
}
|
|
2698
2771
|
function resolveProjectedRisk(intent, changedPaths = []) {
|
|
2772
|
+
if (!inspectSpecBinding(intent).ok)
|
|
2773
|
+
return classifyTaskRisk(changedPaths, intent.risk);
|
|
2699
2774
|
const excluded = ownSidecarPaths(intent);
|
|
2700
2775
|
return classifyTaskRisk(changedPaths.filter((path) => !excluded.has(path)), intent.risk);
|
|
2701
2776
|
}
|
|
@@ -2754,6 +2829,27 @@ function completionDecision(intent, record, currentDiffHash, currentIntentConten
|
|
|
2754
2829
|
};
|
|
2755
2830
|
}
|
|
2756
2831
|
function projectTask(intent, record, currentDiffHash, currentIntentContentHash, changedPaths = []) {
|
|
2832
|
+
if (record.lifecycle === "active" && !inspectSpecBinding(intent).ok) {
|
|
2833
|
+
assertKernelInvariantsV3(intent, record);
|
|
2834
|
+
return {
|
|
2835
|
+
contract: "assurance_kernel/projection/v3",
|
|
2836
|
+
task_id: record.task_id,
|
|
2837
|
+
intent_revision: intent.revision,
|
|
2838
|
+
lifecycle: record.lifecycle,
|
|
2839
|
+
artifact_state: record.artifact_state,
|
|
2840
|
+
blocked: true,
|
|
2841
|
+
next_obligation: "revise_intent",
|
|
2842
|
+
complete: false,
|
|
2843
|
+
fresh_acceptance_ids: [],
|
|
2844
|
+
missing_acceptance_ids: intent.acceptance.map((item) => item.id),
|
|
2845
|
+
stale_attestation_ids: record.attestations.map((item) => item.id),
|
|
2846
|
+
missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)],
|
|
2847
|
+
blocking_finding_ids: record.findings.filter((item) => item.kind === "blocking" && item.status !== "resolved").map((item) => item.id),
|
|
2848
|
+
unresolved_user_decision_ids: record.findings.filter((item) => item.kind === "unresolved_user_decision" && item.status === "open").map((item) => item.id),
|
|
2849
|
+
replan_required_ids: record.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id),
|
|
2850
|
+
independence_violations: []
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2757
2853
|
const decision = completionDecision(intent, record, currentDiffHash, currentIntentContentHash, changedPaths);
|
|
2758
2854
|
const blocked = decision.blocking_finding_ids.length > 0 || decision.unresolved_user_decision_ids.length > 0 || decision.replan_required_ids.length > 0 || decision.independence_violations.length > 0;
|
|
2759
2855
|
let nextObligation = "none";
|
|
@@ -3044,27 +3140,37 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
|
|
|
3044
3140
|
const reviewRevision = approval.review_revision;
|
|
3045
3141
|
if (reviewRevision && approval.kind !== "review")
|
|
3046
3142
|
throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
|
|
3143
|
+
if (approval.reviewer_verdict_sha256 && approval.kind !== "review")
|
|
3144
|
+
throw new KernelInvariantError(["reviewer_verdict_sha256 is only valid on review approvals"]);
|
|
3047
3145
|
if (approval.kind === "review") {
|
|
3048
3146
|
if (!reviewRevision)
|
|
3049
3147
|
throw new KernelInvariantError(["v4 review approval requires review_revision"]);
|
|
3050
3148
|
if (reviewRevision.base_head !== record.git_base_head)
|
|
3051
3149
|
throw new KernelInvariantError(["review_revision.base_head must equal the Enrollment git_base_head"]);
|
|
3052
3150
|
}
|
|
3053
|
-
const {
|
|
3151
|
+
const {
|
|
3152
|
+
review_revision: storedReviewRevision,
|
|
3153
|
+
advisory_findings: storedAdvisoryFindings,
|
|
3154
|
+
reviewer_verdict_sha256: storedReviewerDigest,
|
|
3155
|
+
...approvalWithoutExtras
|
|
3156
|
+
} = approval;
|
|
3054
3157
|
record.attestations.push({
|
|
3055
|
-
...
|
|
3158
|
+
...approvalWithoutExtras,
|
|
3056
3159
|
acceptance_results: approval.kind === "qa" ? record.intent_snapshot.acceptance.map((item) => ({
|
|
3057
3160
|
acceptance_id: item.id,
|
|
3058
3161
|
status: "passed",
|
|
3059
3162
|
summary: `host-attested QA: ${approval.summary}`
|
|
3060
3163
|
})) : [],
|
|
3061
|
-
...storedReviewRevision ? { review_revision: storedReviewRevision } : {}
|
|
3164
|
+
...storedReviewRevision ? { review_revision: storedReviewRevision } : {},
|
|
3165
|
+
...storedAdvisoryFindings ? { advisory_findings: storedAdvisoryFindings } : {},
|
|
3166
|
+
...storedReviewerDigest ? { reviewer_verdict_sha256: storedReviewerDigest } : {}
|
|
3062
3167
|
});
|
|
3063
3168
|
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
3064
3169
|
break;
|
|
3065
3170
|
}
|
|
3066
3171
|
case "revise_intent":
|
|
3067
3172
|
case "approve_breaking_intent_revision": {
|
|
3173
|
+
assertValidSpecBinding(action.next_intent);
|
|
3068
3174
|
if (record.lifecycle !== "active")
|
|
3069
3175
|
throw new KernelInvariantError([
|
|
3070
3176
|
`cannot revise intent while lifecycle is ${record.lifecycle}`
|
|
@@ -3544,10 +3650,16 @@ function isRetiredFileProvablySuperseded(path, db, taskId) {
|
|
|
3544
3650
|
}
|
|
3545
3651
|
function assertNoRetiredFileStore(root, db, taskId) {
|
|
3546
3652
|
const canonical = canonicalRoot2(root);
|
|
3653
|
+
let residue;
|
|
3654
|
+
try {
|
|
3655
|
+
residue = inspectRetiredStateResidue(canonical);
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
throw new KernelStoreSecurityError(`retired file-store residue is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
3658
|
+
}
|
|
3547
3659
|
const retired = [
|
|
3548
3660
|
[".imm/tasks", "pre-cutover task store"],
|
|
3549
3661
|
[".imm/workspace.json", "pre-cutover workspace owner"],
|
|
3550
|
-
[".imm/state/tasks", "task records"]
|
|
3662
|
+
...residue.task_entries.length > 0 ? [[".imm/state/tasks", "task records"]] : []
|
|
3551
3663
|
];
|
|
3552
3664
|
for (const [path, label] of retired) {
|
|
3553
3665
|
if (existsSync2(resolve5(canonical, path)))
|
|
@@ -3559,6 +3671,8 @@ function assertNoRetiredFileStore(root, db, taskId) {
|
|
|
3559
3671
|
];
|
|
3560
3672
|
for (const [path, label] of derived) {
|
|
3561
3673
|
const full = resolve5(canonical, path);
|
|
3674
|
+
if (path === FILE_STORE_WORKSPACE_RELATIVE && residue.workspace_owner === null)
|
|
3675
|
+
continue;
|
|
3562
3676
|
if (!existsSync2(full))
|
|
3563
3677
|
continue;
|
|
3564
3678
|
if (db === undefined || typeof taskId !== "string" || taskId.length === 0)
|
|
@@ -3575,10 +3689,16 @@ function assertNoRetiredFileStore(root, db, taskId) {
|
|
|
3575
3689
|
}
|
|
3576
3690
|
function retiredFileStoreConflict(root, db, taskId) {
|
|
3577
3691
|
const canonical = canonicalRoot2(root);
|
|
3692
|
+
let residue;
|
|
3693
|
+
try {
|
|
3694
|
+
residue = inspectRetiredStateResidue(canonical);
|
|
3695
|
+
} catch (error) {
|
|
3696
|
+
return `retired file-store residue is invalid: ${error instanceof Error ? error.message : String(error)}`;
|
|
3697
|
+
}
|
|
3578
3698
|
const authority = [
|
|
3579
3699
|
[".imm/tasks", "pre-cutover task store"],
|
|
3580
3700
|
[".imm/workspace.json", "pre-cutover workspace owner"],
|
|
3581
|
-
[".imm/state/tasks", "task records"]
|
|
3701
|
+
...residue.task_entries.length > 0 ? [[".imm/state/tasks", "task records"]] : []
|
|
3582
3702
|
];
|
|
3583
3703
|
for (const [path, label] of authority)
|
|
3584
3704
|
if (existsSync2(resolve5(canonical, path)))
|
|
@@ -3590,7 +3710,7 @@ function retiredFileStoreConflict(root, db, taskId) {
|
|
|
3590
3710
|
[FILE_STORE_WORKSPACE_RELATIVE, "workspace owner"]
|
|
3591
3711
|
];
|
|
3592
3712
|
for (const [path, label] of derived)
|
|
3593
|
-
if (existsSync2(resolve5(canonical, path)))
|
|
3713
|
+
if ((path !== FILE_STORE_WORKSPACE_RELATIVE || residue.workspace_owner !== null) && existsSync2(resolve5(canonical, path)))
|
|
3594
3714
|
return `retired file-store authority is present (${label}: ${path}); import it with the supported migration before mutating this worktree`;
|
|
3595
3715
|
}
|
|
3596
3716
|
const transactions = resolve5(canonical, FILE_STORE_TRANSACTIONS_RELATIVE);
|
|
@@ -5222,7 +5342,7 @@ function projectFromRecord(record, recordRevision, workspaceRevision, snapshot)
|
|
|
5222
5342
|
independence_violations: decision.independence_violations,
|
|
5223
5343
|
open_user_decision_count: openUserDecisionCount,
|
|
5224
5344
|
completion_ready: decision.complete,
|
|
5225
|
-
authorization: deriveAssuranceAuthorization({
|
|
5345
|
+
authorization: !inspectSpecBinding(intent).ok ? { state: "none", blocked: "invalid Spec binding requires a valid breaking intent revision" } : deriveAssuranceAuthorization({
|
|
5226
5346
|
next_obligation: decision.next_obligation,
|
|
5227
5347
|
open_user_decision_count: openUserDecisionCount
|
|
5228
5348
|
})
|
|
@@ -5555,6 +5675,7 @@ function createCanaryApplication(registry) {
|
|
|
5555
5675
|
const current = readTaskRecordRaw(input.root, input.task_id);
|
|
5556
5676
|
if (!current.record)
|
|
5557
5677
|
throw new KernelInvariantError([`task ${input.task_id} has no TaskRecord v3`]);
|
|
5678
|
+
assertValidSpecBinding(current.record.intent_snapshot);
|
|
5558
5679
|
const workspace = readWorkspaceStateRaw(input.root);
|
|
5559
5680
|
if (current.record.artifact_state === "frozen")
|
|
5560
5681
|
return { revision: current.revision, record: current.record, workspace };
|
|
@@ -5636,7 +5757,11 @@ function createCanaryApplication(registry) {
|
|
|
5636
5757
|
const diffHash = asTaskDiffSnapshot((input.diffProvider ?? taskDeliveryIdentity)(input.root, snapshot.record)).diff_hash;
|
|
5637
5758
|
if (operation.op === "stop" && !("capability" in operation))
|
|
5638
5759
|
throw new KernelInvariantError(["stop requires user authority capability"]);
|
|
5639
|
-
|
|
5760
|
+
if (operation.op === "approve_breaking_intent_revision")
|
|
5761
|
+
assertValidSpecBinding(operation.next_intent);
|
|
5762
|
+
else
|
|
5763
|
+
assertValidSpecBinding(snapshot.intent_snapshot);
|
|
5764
|
+
const hasBoundSpec = operation.op === "approve_breaking_intent_revision" ? boundSpecPath(operation.next_intent) !== undefined : boundSpecPath(snapshot.intent_snapshot) !== undefined;
|
|
5640
5765
|
if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
|
|
5641
5766
|
throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
|
|
5642
5767
|
const artifactTransition = snapshot.record.artifact_state === "frozen" && (operation.op === "request_rework" || operation.op === "authorize_rework" || operation.op === "approve_breaking_intent_revision") ? transitionFor(input.root, snapshot.record, "restore") : operation.op === "stop" && snapshot.record.artifact_state !== "frozen" ? transitionFor(input.root, snapshot.record, "freeze", true) : undefined;
|
|
@@ -6816,8 +6941,14 @@ var INTERNAL_ROLE_PROMPTS = {
|
|
|
6816
6941
|
file: "compounder.md",
|
|
6817
6942
|
authority: "compounder",
|
|
6818
6943
|
tool_policy: "learning tools"
|
|
6944
|
+
},
|
|
6945
|
+
"lane-steward": {
|
|
6946
|
+
file: "lane-steward.md",
|
|
6947
|
+
authority: "lane-provision",
|
|
6948
|
+
tool_policy: "workspace tools"
|
|
6819
6949
|
}
|
|
6820
6950
|
};
|
|
6951
|
+
var LANE_EXECUTOR_HOSTS = ["claude-code", "pi"];
|
|
6821
6952
|
function roleSpec(role) {
|
|
6822
6953
|
const spec = INTERNAL_ROLE_PROMPTS[role];
|
|
6823
6954
|
if (!spec)
|
|
@@ -7163,8 +7294,9 @@ function parseVerdictEvidence(value, index) {
|
|
|
7163
7294
|
function parseAssuranceVerdict(input, snapshot) {
|
|
7164
7295
|
let raw;
|
|
7165
7296
|
if (typeof input === "string") {
|
|
7166
|
-
const
|
|
7167
|
-
|
|
7297
|
+
const first = input.indexOf("{");
|
|
7298
|
+
const last = input.lastIndexOf("}");
|
|
7299
|
+
const cleaned = first >= 0 && last >= first ? input.slice(first, last + 1) : "";
|
|
7168
7300
|
if (!cleaned)
|
|
7169
7301
|
throw new Error("reviewer returned no strict JSON verdict");
|
|
7170
7302
|
try {
|
|
@@ -7792,10 +7924,10 @@ class AssuranceCoordinator {
|
|
|
7792
7924
|
signal?.removeEventListener("abort", relayExternalAbort);
|
|
7793
7925
|
}
|
|
7794
7926
|
}
|
|
7795
|
-
async submitReview(taskId, ctx, verdictInput) {
|
|
7796
|
-
return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput));
|
|
7927
|
+
async submitReview(taskId, ctx, verdictInput, options) {
|
|
7928
|
+
return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput, options));
|
|
7797
7929
|
}
|
|
7798
|
-
async submitReviewOnce(taskId, ctx, verdictInput) {
|
|
7930
|
+
async submitReviewOnce(taskId, ctx, verdictInput, options) {
|
|
7799
7931
|
const unknown = this.unknownOperations.get(taskId);
|
|
7800
7932
|
if (unknown)
|
|
7801
7933
|
return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
|
|
@@ -7836,6 +7968,9 @@ class AssuranceCoordinator {
|
|
|
7836
7968
|
reservation.verdictCorrectionRequired = true;
|
|
7837
7969
|
return { state: "blocked", code: "verdict_invalid", reason: boundedAssuranceError(error) };
|
|
7838
7970
|
}
|
|
7971
|
+
const receiptError = options?.validateReceipt?.();
|
|
7972
|
+
if (receiptError)
|
|
7973
|
+
return { state: "blocked", reason: receiptError, ...options?.receiptRecoveryAction ? { recovery_action: options.receiptRecoveryAction } : {} };
|
|
7839
7974
|
const invocation = this.openInvocation(taskId);
|
|
7840
7975
|
this.releaseReviewReservation(taskId, reservation);
|
|
7841
7976
|
try {
|
|
@@ -7844,7 +7979,8 @@ class AssuranceCoordinator {
|
|
|
7844
7979
|
snapshot: reservation.snapshot,
|
|
7845
7980
|
verdict,
|
|
7846
7981
|
invocation,
|
|
7847
|
-
actorId: "parent-mediated-review"
|
|
7982
|
+
actorId: "parent-mediated-review",
|
|
7983
|
+
...options?.reviewer_verdict_sha256 ? { reviewer_verdict_sha256: options.reviewer_verdict_sha256 } : {}
|
|
7848
7984
|
});
|
|
7849
7985
|
} catch (error) {
|
|
7850
7986
|
const reason = boundedAssuranceError(error);
|
|
@@ -8137,7 +8273,8 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
|
|
|
8137
8273
|
actor_id: actorId,
|
|
8138
8274
|
summary: verdict.approval.summary,
|
|
8139
8275
|
...snapshot.role === "review" && snapshot.review_revision ? { review_revision: snapshot.review_revision } : {},
|
|
8140
|
-
...snapshot.role === "review" && advisories.length ? { advisory_findings: advisories } : {}
|
|
8276
|
+
...snapshot.role === "review" && advisories.length ? { advisory_findings: advisories } : {},
|
|
8277
|
+
...snapshot.role === "review" && input.reviewer_verdict_sha256 ? { reviewer_verdict_sha256: input.reviewer_verdict_sha256 } : {}
|
|
8141
8278
|
} : undefined;
|
|
8142
8279
|
const op = verdict.decision === "rework" ? "request_rework" : "record_approval";
|
|
8143
8280
|
const payload = approval ? { approval } : { findings };
|
|
@@ -8192,9 +8329,9 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
|
|
|
8192
8329
|
}
|
|
8193
8330
|
|
|
8194
8331
|
// plugins/immune-brain/runtime/claude/kernel_ports.ts
|
|
8195
|
-
import { existsSync as
|
|
8332
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
8196
8333
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
8197
|
-
import { join as
|
|
8334
|
+
import { join as join17 } from "node:path";
|
|
8198
8335
|
|
|
8199
8336
|
// plugins/immune-brain/runtime/kernel/enrollment_authority.ts
|
|
8200
8337
|
var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
|
|
@@ -9483,6 +9620,16 @@ var BATCH_REASONS = Object.freeze({
|
|
|
9483
9620
|
state: "rejected",
|
|
9484
9621
|
reason: (detail) => detail || "batch run rejected",
|
|
9485
9622
|
recovery_action: "delete or rename the conflicting branch, or commit working changes and retry in the current Host"
|
|
9623
|
+
},
|
|
9624
|
+
stale_batch_absent: {
|
|
9625
|
+
state: "rejected",
|
|
9626
|
+
reason: (detail) => `no retirable batch record to dispose: ${detail}`,
|
|
9627
|
+
recovery_action: "start a batch for this Initiative, or inspect the batch records under .imm/state/batches in the current Host"
|
|
9628
|
+
},
|
|
9629
|
+
stale_batch_in_flight: {
|
|
9630
|
+
state: "blocked",
|
|
9631
|
+
reason: (detail) => `a batch child is still mid-flight, so the record cannot be disposed: ${detail}`,
|
|
9632
|
+
recovery_action: "settle or stop the in-flight child through its own task, then dispose the batch in the current Host"
|
|
9486
9633
|
}
|
|
9487
9634
|
});
|
|
9488
9635
|
function batchReason(key, detail = "") {
|
|
@@ -10699,7 +10846,7 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
|
|
|
10699
10846
|
}
|
|
10700
10847
|
|
|
10701
10848
|
// plugins/immune-brain/runtime/unattended/batch_preflight.ts
|
|
10702
|
-
import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as
|
|
10849
|
+
import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync14 } from "node:fs";
|
|
10703
10850
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
10704
10851
|
import { join as join13 } from "node:path";
|
|
10705
10852
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
@@ -10711,6 +10858,9 @@ import { spawnSync as spawnSync6 } from "node:child_process";
|
|
|
10711
10858
|
import { existsSync as existsSync7, mkdirSync as mkdirSync6, openSync as openSync6, closeSync as closeSync6, writeFileSync as writeFileSync6, renameSync as renameSync3, lstatSync as lstatSync9, constants as constants5, rmSync as rmSync6 } from "node:fs";
|
|
10712
10859
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
10713
10860
|
import { dirname as dirname7, join as join12 } from "node:path";
|
|
10861
|
+
function isLaneBatchRecord(record) {
|
|
10862
|
+
return record.contract === "assurance_kernel/batch_run_state/v2";
|
|
10863
|
+
}
|
|
10714
10864
|
var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10715
10865
|
var CHILD_RUN_STATES = new Set([
|
|
10716
10866
|
"pending",
|
|
@@ -10727,7 +10877,8 @@ var BATCH_RUN_STATES = new Set([
|
|
|
10727
10877
|
"completed",
|
|
10728
10878
|
"budget_stopped",
|
|
10729
10879
|
"failed",
|
|
10730
|
-
"rejected"
|
|
10880
|
+
"rejected",
|
|
10881
|
+
"superseded"
|
|
10731
10882
|
]);
|
|
10732
10883
|
var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10733
10884
|
function isCanonicalTimestamp(value) {
|
|
@@ -10808,7 +10959,7 @@ function validateRecordShape(value, batchId) {
|
|
|
10808
10959
|
if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
|
|
10809
10960
|
throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
|
|
10810
10961
|
}
|
|
10811
|
-
if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
|
|
10962
|
+
if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" || record.batch_state === "superseded") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
|
|
10812
10963
|
throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
|
|
10813
10964
|
if (record.batch_state === "completed" && record.children.some((child) => child.state !== "committed"))
|
|
10814
10965
|
throw new Error(`batch run state ${batchId} is completed but a child is not committed`);
|
|
@@ -10902,6 +11053,21 @@ function replaceBatchRunState(root, expected, next, validate) {
|
|
|
10902
11053
|
return stored;
|
|
10903
11054
|
});
|
|
10904
11055
|
}
|
|
11056
|
+
function replaceAnyBatchRunState(root, expected, next, validate) {
|
|
11057
|
+
const path = statePath(next.batch_id);
|
|
11058
|
+
if (isLaneBatchRecord(next))
|
|
11059
|
+
validateLaneRecordShape(next, next.batch_id);
|
|
11060
|
+
else
|
|
11061
|
+
validateRecordShape(next, next.batch_id);
|
|
11062
|
+
return withKernelStoreLock(root, () => {
|
|
11063
|
+
if (!readSecureProjectBytes(root, path).equals(expected))
|
|
11064
|
+
throw new Error("batch state CAS mismatch");
|
|
11065
|
+
validate();
|
|
11066
|
+
const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
|
|
11067
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
11068
|
+
return stored;
|
|
11069
|
+
});
|
|
11070
|
+
}
|
|
10905
11071
|
function writeBatchRunState(root, record) {
|
|
10906
11072
|
const path = statePath(record.batch_id);
|
|
10907
11073
|
validateRecordShape(record, record.batch_id);
|
|
@@ -10918,6 +11084,162 @@ function writeBatchRunState(root, record) {
|
|
|
10918
11084
|
return stored;
|
|
10919
11085
|
});
|
|
10920
11086
|
}
|
|
11087
|
+
var LANE_CHILD_STATES = new Set([
|
|
11088
|
+
"pending",
|
|
11089
|
+
"lane_admitted",
|
|
11090
|
+
"enrolled",
|
|
11091
|
+
"settled",
|
|
11092
|
+
"lane_committed",
|
|
11093
|
+
"integrated",
|
|
11094
|
+
"released",
|
|
11095
|
+
"needs_human",
|
|
11096
|
+
"skipped_blocked"
|
|
11097
|
+
]);
|
|
11098
|
+
var LANE_BOUND_STATES = new Set([
|
|
11099
|
+
"lane_admitted",
|
|
11100
|
+
"enrolled",
|
|
11101
|
+
"settled",
|
|
11102
|
+
"lane_committed",
|
|
11103
|
+
"integrated",
|
|
11104
|
+
"released"
|
|
11105
|
+
]);
|
|
11106
|
+
function validateLaneRecordShape(value, batchId) {
|
|
11107
|
+
if (typeof value !== "object" || value === null)
|
|
11108
|
+
throw new Error(`batch run state ${batchId} is not an object`);
|
|
11109
|
+
const record = value;
|
|
11110
|
+
if (record.contract !== "assurance_kernel/batch_run_state/v2")
|
|
11111
|
+
throw new Error(`batch run state ${batchId} has an unknown contract`);
|
|
11112
|
+
if (record.batch_id !== batchId)
|
|
11113
|
+
throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
|
|
11114
|
+
if (typeof record.plan_digest !== "string" || !record.plan_digest)
|
|
11115
|
+
throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
|
|
11116
|
+
if (typeof record.base_head !== "string" || !record.base_head)
|
|
11117
|
+
throw new Error(`batch run state ${batchId} has an invalid base_head`);
|
|
11118
|
+
if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
|
|
11119
|
+
throw new Error(`batch run state ${batchId} has an invalid branch`);
|
|
11120
|
+
if (!isCanonicalTimestamp(record.confirmation_time))
|
|
11121
|
+
throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
|
|
11122
|
+
if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
|
|
11123
|
+
throw new Error(`batch run state ${batchId} has invalid state timestamps`);
|
|
11124
|
+
if (!Array.isArray(record.children) || record.children.length === 0)
|
|
11125
|
+
throw new Error(`batch run state ${batchId} has no children`);
|
|
11126
|
+
if (!BATCH_RUN_STATES.has(String(record.batch_state)))
|
|
11127
|
+
throw new Error(`batch run state ${batchId} has an invalid batch_state`);
|
|
11128
|
+
if (typeof record.max_parallel !== "number" || !Number.isSafeInteger(record.max_parallel) || record.max_parallel <= 0)
|
|
11129
|
+
throw new Error(`batch run state ${batchId} has an invalid max_parallel`);
|
|
11130
|
+
const budget = record.budget;
|
|
11131
|
+
if (typeof record.budget !== "object" || record.budget === null || typeof budget.max_children !== "number" || !Number.isInteger(budget.max_children) || budget.max_children <= 0 || typeof budget.qa_failure_limit !== "number" || !Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
11132
|
+
throw new Error(`batch run state ${batchId} has an invalid budget`);
|
|
11133
|
+
if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
|
|
11134
|
+
throw new Error(`batch run state ${batchId} has an invalid commits list`);
|
|
11135
|
+
if (record.adopted_heads !== undefined && (!Array.isArray(record.adopted_heads) || record.adopted_heads.some((a) => typeof a !== "object" || a === null || typeof a.from !== "string" || !a.from || typeof a.to !== "string" || !a.to)))
|
|
11136
|
+
throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
|
|
11137
|
+
const seenTaskIds = new Set;
|
|
11138
|
+
for (const child of record.children) {
|
|
11139
|
+
if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
|
|
11140
|
+
throw new Error(`batch run state ${batchId} has an invalid child entry`);
|
|
11141
|
+
if (!LANE_CHILD_STATES.has(String(child.state)))
|
|
11142
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
|
|
11143
|
+
if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
|
|
11144
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
|
|
11145
|
+
if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
|
|
11146
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
|
|
11147
|
+
if (typeof child.qa_failures !== "number" || !Number.isInteger(child.qa_failures) || child.qa_failures < 0)
|
|
11148
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid qa_failures`);
|
|
11149
|
+
const lane = child.lane;
|
|
11150
|
+
if (lane !== null) {
|
|
11151
|
+
if (typeof lane !== "object" || typeof lane.path !== "string" || !lane.path || typeof lane.branch !== "string" || !lane.branch || typeof lane.base_head !== "string" || !lane.base_head || lane.lane_commit !== null && typeof lane.lane_commit !== "string" || lane.run_id !== undefined && lane.run_id !== null && (typeof lane.run_id !== "string" || !lane.run_id))
|
|
11152
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid lane`);
|
|
11153
|
+
}
|
|
11154
|
+
if (LANE_BOUND_STATES.has(child.state) && lane === null)
|
|
11155
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane`);
|
|
11156
|
+
if ((child.state === "pending" || child.state === "skipped_blocked") && lane !== null)
|
|
11157
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} but holds a lane`);
|
|
11158
|
+
const integrated = child.state === "integrated" || child.state === "released";
|
|
11159
|
+
if (integrated && child.commit === null)
|
|
11160
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a commit`);
|
|
11161
|
+
if (!integrated && child.commit !== null)
|
|
11162
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not integrated`);
|
|
11163
|
+
if (integrated && lane?.lane_commit === null)
|
|
11164
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane commit`);
|
|
11165
|
+
if (child.state === "lane_committed" && lane?.lane_commit === null)
|
|
11166
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is lane_committed without a lane commit`);
|
|
11167
|
+
if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
|
|
11168
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
|
|
11169
|
+
if (seenTaskIds.has(child.task_id))
|
|
11170
|
+
throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
|
|
11171
|
+
seenTaskIds.add(child.task_id);
|
|
11172
|
+
}
|
|
11173
|
+
for (const child of record.children) {
|
|
11174
|
+
if (child.commit !== null && !record.commits.includes(child.commit))
|
|
11175
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
|
|
11176
|
+
}
|
|
11177
|
+
const inFlight = (child) => child.state === "lane_admitted" || child.state === "enrolled" || child.state === "settled" || child.state === "lane_committed";
|
|
11178
|
+
if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" || record.batch_state === "superseded") && record.children.some(inFlight))
|
|
11179
|
+
throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
|
|
11180
|
+
if (record.batch_state === "completed" && record.children.some((child) => child.state !== "integrated" && child.state !== "released"))
|
|
11181
|
+
throw new Error(`batch run state ${batchId} is completed but a child is not integrated`);
|
|
11182
|
+
}
|
|
11183
|
+
function prepareBatchLaneRunState(input) {
|
|
11184
|
+
validateBatchId(input.batch_id);
|
|
11185
|
+
return {
|
|
11186
|
+
contract: "assurance_kernel/batch_run_state/v2",
|
|
11187
|
+
batch_id: input.batch_id,
|
|
11188
|
+
initiative_slug: input.initiative_slug,
|
|
11189
|
+
plan_digest: input.plan_digest,
|
|
11190
|
+
base_head: input.base_head,
|
|
11191
|
+
branch: input.branch ?? `imm/${input.initiative_slug}`,
|
|
11192
|
+
confirmation_time: input.confirmation_time,
|
|
11193
|
+
budget: input.budget,
|
|
11194
|
+
max_parallel: input.max_parallel,
|
|
11195
|
+
batch_state: "prepared",
|
|
11196
|
+
children: input.children.map((child) => ({
|
|
11197
|
+
task_id: child.task_id,
|
|
11198
|
+
slice_id: child.slice_id,
|
|
11199
|
+
blocked_by: [...child.blocked_by],
|
|
11200
|
+
state: "pending",
|
|
11201
|
+
reason: null,
|
|
11202
|
+
commit: null,
|
|
11203
|
+
lane: null,
|
|
11204
|
+
qa_failures: 0
|
|
11205
|
+
})),
|
|
11206
|
+
commits: [],
|
|
11207
|
+
created_at: input.now,
|
|
11208
|
+
updated_at: input.now
|
|
11209
|
+
};
|
|
11210
|
+
}
|
|
11211
|
+
function parseBatchLaneRunState(raw, batchId) {
|
|
11212
|
+
validateBatchId(batchId);
|
|
11213
|
+
const parsed = JSON.parse(raw);
|
|
11214
|
+
validateLaneRecordShape(parsed, batchId);
|
|
11215
|
+
return withoutRetiredClock(parsed);
|
|
11216
|
+
}
|
|
11217
|
+
function parseAnyBatchRunState(raw, batchId) {
|
|
11218
|
+
validateBatchId(batchId);
|
|
11219
|
+
const parsed = JSON.parse(raw);
|
|
11220
|
+
if (typeof parsed === "object" && parsed !== null && parsed.contract === "assurance_kernel/batch_run_state/v2")
|
|
11221
|
+
return parseBatchLaneRunState(raw, batchId);
|
|
11222
|
+
return parseBatchRunState(raw, batchId);
|
|
11223
|
+
}
|
|
11224
|
+
function readAnyBatchRunState(root, batchId) {
|
|
11225
|
+
const path = statePath(batchId);
|
|
11226
|
+
if (!existsSync7(join12(root, path)))
|
|
11227
|
+
return null;
|
|
11228
|
+
return parseAnyBatchRunState(readSecureProjectFile(root, path), batchId);
|
|
11229
|
+
}
|
|
11230
|
+
function writeBatchLaneRunState(root, record) {
|
|
11231
|
+
const path = statePath(record.batch_id);
|
|
11232
|
+
validateLaneRecordShape(record, record.batch_id);
|
|
11233
|
+
return withKernelStoreLock(root, () => {
|
|
11234
|
+
const existing = existsSync7(join12(root, path)) ? readSecureProjectFile(root, path) : null;
|
|
11235
|
+
if (existing !== null && existing === canonicalBytes(record))
|
|
11236
|
+
return record;
|
|
11237
|
+
const stored = withoutRetiredClock({ ...record, updated_at: new Date().toISOString() });
|
|
11238
|
+
ensureSecureDirectory2(root, join12(".imm", "state", "batches"));
|
|
11239
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
11240
|
+
return stored;
|
|
11241
|
+
});
|
|
11242
|
+
}
|
|
10921
11243
|
function reportPath(batchId) {
|
|
10922
11244
|
validateBatchId(batchId);
|
|
10923
11245
|
return join12(".imm", "state", "batches", `${batchId}.report.json`);
|
|
@@ -10946,7 +11268,7 @@ function canonicalReportBytes(report) {
|
|
|
10946
11268
|
`;
|
|
10947
11269
|
}
|
|
10948
11270
|
function isTerminalBatchState(state) {
|
|
10949
|
-
return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
|
|
11271
|
+
return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected" || state === "superseded";
|
|
10950
11272
|
}
|
|
10951
11273
|
|
|
10952
11274
|
// plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
|
|
@@ -11173,7 +11495,186 @@ async function captureBatchReconfirmation(root, record, children) {
|
|
|
11173
11495
|
|
|
11174
11496
|
// plugins/immune-brain/runtime/unattended/batch_plan.ts
|
|
11175
11497
|
import { createHash as createHash19 } from "node:crypto";
|
|
11498
|
+
|
|
11499
|
+
// plugins/immune-brain/runtime/local_initiative.ts
|
|
11500
|
+
import { lstatSync as lstatSync10, readFileSync as readFileSync13 } from "node:fs";
|
|
11501
|
+
import { resolve as resolve12 } from "node:path";
|
|
11176
11502
|
var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
11503
|
+
var SLICE_HEADING = /^##\s+([^\s:]+)\s*:/;
|
|
11504
|
+
var BLOCKED_BY = /^blocked by\s*:(.*)$/i;
|
|
11505
|
+
var TASKS = /^tasks\s*:\s*$/i;
|
|
11506
|
+
var BULLET = /^[-*]\s+(.*)$/;
|
|
11507
|
+
function localInitiativePath(initiativeSlug) {
|
|
11508
|
+
return `docs/initiatives/${initiativeSlug}.md`;
|
|
11509
|
+
}
|
|
11510
|
+
function hasLocalInitiative(root, initiativeSlug) {
|
|
11511
|
+
const stat = lstatSync10(resolve12(root, localInitiativePath(initiativeSlug)), { throwIfNoEntry: false });
|
|
11512
|
+
if (!stat)
|
|
11513
|
+
return false;
|
|
11514
|
+
if (!stat.isFile())
|
|
11515
|
+
throw new Error(`${localInitiativePath(initiativeSlug)} must be a regular file`);
|
|
11516
|
+
return true;
|
|
11517
|
+
}
|
|
11518
|
+
function unquote(value) {
|
|
11519
|
+
return value.trim().replace(/^`(.*)`$/, "$1");
|
|
11520
|
+
}
|
|
11521
|
+
function observeLocalInitiative(root, initiativeSlug) {
|
|
11522
|
+
if (!ID_PATTERN2.test(initiativeSlug))
|
|
11523
|
+
throw new Error("initiative_slug is invalid");
|
|
11524
|
+
const path = localInitiativePath(initiativeSlug);
|
|
11525
|
+
if (!hasLocalInitiative(root, initiativeSlug))
|
|
11526
|
+
throw new Error(`Local Initiative ${path} does not exist`);
|
|
11527
|
+
const slices = [];
|
|
11528
|
+
let inTasks = false;
|
|
11529
|
+
let fence = false;
|
|
11530
|
+
for (const raw of readFileSync13(resolve12(root, path), "utf8").split(/\r?\n/)) {
|
|
11531
|
+
const line = raw.trim();
|
|
11532
|
+
if (line.startsWith("```")) {
|
|
11533
|
+
fence = !fence;
|
|
11534
|
+
continue;
|
|
11535
|
+
}
|
|
11536
|
+
if (fence)
|
|
11537
|
+
continue;
|
|
11538
|
+
const heading = SLICE_HEADING.exec(line);
|
|
11539
|
+
if (heading) {
|
|
11540
|
+
slices.push({ slice_id: heading[1], tasks: [], blocked_by: [] });
|
|
11541
|
+
inTasks = false;
|
|
11542
|
+
continue;
|
|
11543
|
+
}
|
|
11544
|
+
const slice = slices.at(-1);
|
|
11545
|
+
if (!slice)
|
|
11546
|
+
continue;
|
|
11547
|
+
if (TASKS.test(line)) {
|
|
11548
|
+
inTasks = true;
|
|
11549
|
+
continue;
|
|
11550
|
+
}
|
|
11551
|
+
const blocked = BLOCKED_BY.exec(line);
|
|
11552
|
+
if (blocked) {
|
|
11553
|
+
slice.blocked_by.push(...blocked[1].split(",").map(unquote).filter(Boolean));
|
|
11554
|
+
inTasks = false;
|
|
11555
|
+
continue;
|
|
11556
|
+
}
|
|
11557
|
+
const bullet = BULLET.exec(line);
|
|
11558
|
+
if (inTasks && bullet)
|
|
11559
|
+
slice.tasks.push(unquote(bullet[1]));
|
|
11560
|
+
else if (line)
|
|
11561
|
+
inTasks = false;
|
|
11562
|
+
}
|
|
11563
|
+
if (!slices.length)
|
|
11564
|
+
throw new Error(`Local Initiative ${path} declares no Slice`);
|
|
11565
|
+
return {
|
|
11566
|
+
contract: "immune_brain/local_initiative_observation/v1",
|
|
11567
|
+
initiative_id: initiativeSlug,
|
|
11568
|
+
path,
|
|
11569
|
+
tasks: slices.map((slice) => {
|
|
11570
|
+
if (slice.tasks.length !== 1)
|
|
11571
|
+
throw new Error(`Local Initiative Slice ${slice.slice_id} must name exactly one Task to run as a batch child`);
|
|
11572
|
+
return { task_id: slice.tasks[0], slice_id: slice.slice_id, blocked_by: slice.blocked_by };
|
|
11573
|
+
})
|
|
11574
|
+
};
|
|
11575
|
+
}
|
|
11576
|
+
|
|
11577
|
+
// plugins/immune-brain/runtime/unattended/batch_schedule.ts
|
|
11578
|
+
var IN_FLIGHT_STATES = new Set([
|
|
11579
|
+
"lane_admitted",
|
|
11580
|
+
"enrolled",
|
|
11581
|
+
"settled",
|
|
11582
|
+
"lane_committed"
|
|
11583
|
+
]);
|
|
11584
|
+
var DONE_STATES = new Set(["committed", "integrated", "released"]);
|
|
11585
|
+
var WILDCARD = /[*?[\]{}!]/;
|
|
11586
|
+
function scopePrefix(entry) {
|
|
11587
|
+
if (typeof entry !== "string")
|
|
11588
|
+
return [];
|
|
11589
|
+
let value = entry.trim().toLowerCase();
|
|
11590
|
+
while (value.startsWith("./"))
|
|
11591
|
+
value = value.slice(2);
|
|
11592
|
+
if (!value || value.startsWith("/") || value.includes("\\"))
|
|
11593
|
+
return [];
|
|
11594
|
+
const wildcard = value.search(WILDCARD);
|
|
11595
|
+
const literal = wildcard === -1 ? value : value.slice(0, value.lastIndexOf("/", wildcard) + 1);
|
|
11596
|
+
const segments = literal.split("/").filter((segment) => segment.length > 0);
|
|
11597
|
+
if (segments.some((segment) => segment === "." || segment === ".."))
|
|
11598
|
+
return [];
|
|
11599
|
+
return segments;
|
|
11600
|
+
}
|
|
11601
|
+
function prefixOverlaps(left, right) {
|
|
11602
|
+
if (!left.length || !right.length)
|
|
11603
|
+
return true;
|
|
11604
|
+
const shared = Math.min(left.length, right.length);
|
|
11605
|
+
for (let index = 0;index < shared; index += 1)
|
|
11606
|
+
if (left[index] !== right[index])
|
|
11607
|
+
return false;
|
|
11608
|
+
return true;
|
|
11609
|
+
}
|
|
11610
|
+
function scopesOverlap(left, right) {
|
|
11611
|
+
if (!left.length || !right.length)
|
|
11612
|
+
return true;
|
|
11613
|
+
const rightPrefixes = right.map(scopePrefix);
|
|
11614
|
+
return left.some((entry) => {
|
|
11615
|
+
const prefix = scopePrefix(entry);
|
|
11616
|
+
return rightPrefixes.some((other) => prefixOverlaps(prefix, other));
|
|
11617
|
+
});
|
|
11618
|
+
}
|
|
11619
|
+
function positiveLimit(maxParallel) {
|
|
11620
|
+
if (maxParallel === Number.POSITIVE_INFINITY)
|
|
11621
|
+
return maxParallel;
|
|
11622
|
+
if (!Number.isSafeInteger(maxParallel) || maxParallel <= 0)
|
|
11623
|
+
throw new Error("max_parallel must be a positive safe integer");
|
|
11624
|
+
return maxParallel;
|
|
11625
|
+
}
|
|
11626
|
+
function startableChildren(children, maxParallel) {
|
|
11627
|
+
const limit = positiveLimit(maxParallel);
|
|
11628
|
+
const done = new Set(children.filter((child) => DONE_STATES.has(child.state)).map((child) => child.task_id));
|
|
11629
|
+
const occupied = children.filter((child) => IN_FLIGHT_STATES.has(child.state));
|
|
11630
|
+
const startable = [];
|
|
11631
|
+
for (const child of children) {
|
|
11632
|
+
if (occupied.length >= limit)
|
|
11633
|
+
break;
|
|
11634
|
+
if (child.state !== "pending")
|
|
11635
|
+
continue;
|
|
11636
|
+
if (!child.blocked_by.every((id) => done.has(id)))
|
|
11637
|
+
continue;
|
|
11638
|
+
if (occupied.some((other) => scopesOverlap(child.scope_hint, other.scope_hint)))
|
|
11639
|
+
continue;
|
|
11640
|
+
startable.push(child.task_id);
|
|
11641
|
+
occupied.push(child);
|
|
11642
|
+
}
|
|
11643
|
+
return startable;
|
|
11644
|
+
}
|
|
11645
|
+
function projectParallelGroups(children) {
|
|
11646
|
+
const remaining = [...children];
|
|
11647
|
+
const placed = new Set;
|
|
11648
|
+
const groups = [];
|
|
11649
|
+
const conflicts = new Map;
|
|
11650
|
+
while (remaining.length) {
|
|
11651
|
+
const ready = remaining.filter((child) => child.blocked_by.every((id) => placed.has(id)));
|
|
11652
|
+
if (!ready.length)
|
|
11653
|
+
throw new Error("Initiative Task dependencies must form an acyclic graph");
|
|
11654
|
+
const wave = [];
|
|
11655
|
+
for (const child of ready) {
|
|
11656
|
+
const overlapped = wave.filter((member) => scopesOverlap(child.scope_hint, member.scope_hint));
|
|
11657
|
+
if (overlapped.length) {
|
|
11658
|
+
const known = conflicts.get(child.task_id) ?? new Set;
|
|
11659
|
+
for (const member of overlapped)
|
|
11660
|
+
known.add(member.task_id);
|
|
11661
|
+
conflicts.set(child.task_id, known);
|
|
11662
|
+
continue;
|
|
11663
|
+
}
|
|
11664
|
+
wave.push(child);
|
|
11665
|
+
}
|
|
11666
|
+
groups.push(wave.map((child) => child.task_id));
|
|
11667
|
+
for (const child of wave) {
|
|
11668
|
+
placed.add(child.task_id);
|
|
11669
|
+
remaining.splice(remaining.indexOf(child), 1);
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
const scopeConflicts = children.filter((child) => conflicts.has(child.task_id)).map((child) => ({ task_id: child.task_id, overlaps_with: [...conflicts.get(child.task_id)].sort() }));
|
|
11673
|
+
return { parallel_groups: groups, scope_conflicts: scopeConflicts };
|
|
11674
|
+
}
|
|
11675
|
+
|
|
11676
|
+
// plugins/immune-brain/runtime/unattended/batch_plan.ts
|
|
11677
|
+
var ID_PATTERN3 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
11177
11678
|
var DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
11178
11679
|
function specBindingReason(inspection) {
|
|
11179
11680
|
if (inspection.missing.length > 0)
|
|
@@ -11197,12 +11698,14 @@ function timestamp(value, name) {
|
|
|
11197
11698
|
throw new Error(`${name} must be an ISO timestamp`);
|
|
11198
11699
|
return { milliseconds, iso: new Date(milliseconds).toISOString() };
|
|
11199
11700
|
}
|
|
11701
|
+
var observeInitiative = async (root, initiativeSlug) => hasLocalInitiative(root, initiativeSlug) ? observeLocalInitiative(root, initiativeSlug) : observeGithubInitiative(root, initiativeSlug);
|
|
11200
11702
|
function normalizeObservation(value, initiativeSlug) {
|
|
11201
|
-
|
|
11703
|
+
const local = value?.contract === "immune_brain/local_initiative_observation/v1";
|
|
11704
|
+
if (!value || !local && value.contract !== "immune_brain/github_initiative_observation/v1")
|
|
11202
11705
|
throw new Error("tracker returned an invalid Initiative observation contract");
|
|
11203
11706
|
if (value.initiative_id !== initiativeSlug)
|
|
11204
11707
|
throw new Error("tracker returned an observation for another Initiative");
|
|
11205
|
-
if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
|
|
11708
|
+
if (!local && (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0))
|
|
11206
11709
|
throw new Error("tracker returned an invalid Initiative Issue number");
|
|
11207
11710
|
if (!Array.isArray(value.tasks))
|
|
11208
11711
|
throw new Error("tracker returned an invalid Initiative Task list");
|
|
@@ -11211,13 +11714,14 @@ function normalizeObservation(value, initiativeSlug) {
|
|
|
11211
11714
|
const tasks = value.tasks.map((task, index) => {
|
|
11212
11715
|
if (!task || typeof task !== "object")
|
|
11213
11716
|
throw new Error(`tracker Task ${index} is invalid`);
|
|
11214
|
-
if (!
|
|
11717
|
+
if (!ID_PATTERN3.test(task.task_id))
|
|
11215
11718
|
throw new Error(`tracker Task ${index} has an invalid task_id`);
|
|
11216
|
-
if (!
|
|
11719
|
+
if (!ID_PATTERN3.test(task.slice_id))
|
|
11217
11720
|
throw new Error(`tracker Task ${index} has an invalid slice_id`);
|
|
11218
|
-
|
|
11721
|
+
const issueNumber = task.issue_number;
|
|
11722
|
+
if (!local && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0))
|
|
11219
11723
|
throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
|
|
11220
|
-
if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !
|
|
11724
|
+
if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN3.test(id)))
|
|
11221
11725
|
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
11222
11726
|
if (taskIds.has(task.task_id))
|
|
11223
11727
|
throw new Error(`tracker returned duplicate Task ${task.task_id}`);
|
|
@@ -11273,8 +11777,8 @@ function budget(input, enrollableCount) {
|
|
|
11273
11777
|
const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
11274
11778
|
return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
|
|
11275
11779
|
}
|
|
11276
|
-
async function projectBatchPlan(root, initiativeSlug, input, readInitiative =
|
|
11277
|
-
if (!
|
|
11780
|
+
async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeInitiative) {
|
|
11781
|
+
if (!ID_PATTERN3.test(initiativeSlug))
|
|
11278
11782
|
throw new Error("initiative_slug is invalid");
|
|
11279
11783
|
if (!input || typeof input !== "object")
|
|
11280
11784
|
throw new Error("batch plan input is required");
|
|
@@ -11283,6 +11787,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
|
|
|
11283
11787
|
const order = dependencyOrder(trackerObservation);
|
|
11284
11788
|
const closures = dependencyClosures(order);
|
|
11285
11789
|
const children = [];
|
|
11790
|
+
const scopeHints = new Map;
|
|
11286
11791
|
for (const task of order) {
|
|
11287
11792
|
const blockedBy = closures.get(task.task_id);
|
|
11288
11793
|
const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
|
|
@@ -11324,6 +11829,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
|
|
|
11324
11829
|
});
|
|
11325
11830
|
continue;
|
|
11326
11831
|
}
|
|
11832
|
+
scopeHints.set(task.task_id, [...read.intent.scope_hint]);
|
|
11327
11833
|
children.push({
|
|
11328
11834
|
...child,
|
|
11329
11835
|
status: "enrollable",
|
|
@@ -11369,7 +11875,12 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
|
|
|
11369
11875
|
children,
|
|
11370
11876
|
enrollable,
|
|
11371
11877
|
plan_digest: planDigest,
|
|
11372
|
-
budget: budget(input, enrollable.length)
|
|
11878
|
+
budget: budget(input, enrollable.length),
|
|
11879
|
+
...projectParallelGroups(enrollable.map((child) => ({
|
|
11880
|
+
task_id: child.task_id,
|
|
11881
|
+
blocked_by: child.blocked_by,
|
|
11882
|
+
scope_hint: scopeHints.get(child.task_id)
|
|
11883
|
+
})))
|
|
11373
11884
|
};
|
|
11374
11885
|
}
|
|
11375
11886
|
|
|
@@ -11399,8 +11910,8 @@ function findResumableBatchSlugForTask(root, taskId) {
|
|
|
11399
11910
|
if (!file.endsWith(".json"))
|
|
11400
11911
|
continue;
|
|
11401
11912
|
try {
|
|
11402
|
-
const record = JSON.parse(
|
|
11403
|
-
if (record?.contract !== "assurance_kernel/batch_run_state/v1")
|
|
11913
|
+
const record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
|
|
11914
|
+
if (record?.contract !== "assurance_kernel/batch_run_state/v1" && record?.contract !== "assurance_kernel/batch_run_state/v2")
|
|
11404
11915
|
continue;
|
|
11405
11916
|
if (record.batch_state !== "running" && record.batch_state !== "needs_human")
|
|
11406
11917
|
continue;
|
|
@@ -11421,31 +11932,23 @@ function findExistingActiveBatch(root, initiativeSlug) {
|
|
|
11421
11932
|
continue;
|
|
11422
11933
|
let record;
|
|
11423
11934
|
try {
|
|
11424
|
-
record = JSON.parse(
|
|
11935
|
+
record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
|
|
11425
11936
|
} catch {
|
|
11426
11937
|
return { corrupt: true, path: file };
|
|
11427
11938
|
}
|
|
11428
11939
|
const candidate = record;
|
|
11429
|
-
if (candidate?.contract !== "assurance_kernel/batch_run_state/v1")
|
|
11940
|
+
if (candidate?.contract !== "assurance_kernel/batch_run_state/v1" && candidate?.contract !== "assurance_kernel/batch_run_state/v2")
|
|
11430
11941
|
continue;
|
|
11431
11942
|
if (candidate.initiative_slug !== initiativeSlug)
|
|
11432
11943
|
continue;
|
|
11433
|
-
const validStates = new Set(
|
|
11434
|
-
"prepared",
|
|
11435
|
-
"running",
|
|
11436
|
-
"needs_human",
|
|
11437
|
-
"completed",
|
|
11438
|
-
"budget_stopped",
|
|
11439
|
-
"failed",
|
|
11440
|
-
"rejected"
|
|
11441
|
-
]);
|
|
11944
|
+
const validStates = new Set(BATCH_RUN_STATES);
|
|
11442
11945
|
if (typeof candidate.batch_id !== "string" || typeof candidate.base_head !== "string" || !Array.isArray(candidate.children) || !validStates.has(candidate.batch_state)) {
|
|
11443
11946
|
return { corrupt: true, path: file };
|
|
11444
11947
|
}
|
|
11445
11948
|
if (isTerminalBatchState(candidate.batch_state))
|
|
11446
11949
|
continue;
|
|
11447
11950
|
try {
|
|
11448
|
-
const record =
|
|
11951
|
+
const record = readAnyBatchRunState(root, file.slice(0, -5));
|
|
11449
11952
|
if (record)
|
|
11450
11953
|
return { corrupt: false, record };
|
|
11451
11954
|
} catch {}
|
|
@@ -11463,12 +11966,12 @@ function findSettledBatchRecord(root, initiativeSlug) {
|
|
|
11463
11966
|
continue;
|
|
11464
11967
|
let record;
|
|
11465
11968
|
try {
|
|
11466
|
-
record = JSON.parse(
|
|
11969
|
+
record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
|
|
11467
11970
|
} catch {
|
|
11468
11971
|
continue;
|
|
11469
11972
|
}
|
|
11470
11973
|
const candidate = record;
|
|
11471
|
-
if (candidate?.contract !== "assurance_kernel/batch_run_state/v1")
|
|
11974
|
+
if (candidate?.contract !== "assurance_kernel/batch_run_state/v1" && candidate?.contract !== "assurance_kernel/batch_run_state/v2")
|
|
11472
11975
|
continue;
|
|
11473
11976
|
if (candidate.initiative_slug !== initiativeSlug)
|
|
11474
11977
|
continue;
|
|
@@ -11612,6 +12115,8 @@ async function projectPlanSurface(input) {
|
|
|
11612
12115
|
let recoveryChildren = [];
|
|
11613
12116
|
let planDigest;
|
|
11614
12117
|
let excluded = [];
|
|
12118
|
+
let parallelGroups = [];
|
|
12119
|
+
let scopeConflicts = [];
|
|
11615
12120
|
const riskByTask = new Map;
|
|
11616
12121
|
let budget;
|
|
11617
12122
|
if (isResuming && existingBatch) {
|
|
@@ -11645,7 +12150,7 @@ async function projectPlanSurface(input) {
|
|
|
11645
12150
|
}
|
|
11646
12151
|
}
|
|
11647
12152
|
riskByTask.set(c.task_id, read.intent?.risk ?? "material");
|
|
11648
|
-
const isDone =
|
|
12153
|
+
const isDone = ["committed", "settled", "integrated", "released"].includes(c.state);
|
|
11649
12154
|
return {
|
|
11650
12155
|
task_id: c.task_id,
|
|
11651
12156
|
slice_id: c.slice_id,
|
|
@@ -11680,6 +12185,8 @@ async function projectPlanSurface(input) {
|
|
|
11680
12185
|
if (!plan.enrollable.length)
|
|
11681
12186
|
return { ok: false, key: "empty_enrollable_set", detail: "" };
|
|
11682
12187
|
budget = plan.budget;
|
|
12188
|
+
parallelGroups = plan.parallel_groups;
|
|
12189
|
+
scopeConflicts = plan.scope_conflicts;
|
|
11683
12190
|
const enrollableChildById = new Map(plan.enrollable.map((c) => [c.task_id, c]));
|
|
11684
12191
|
recoveryChildren = plan.children.filter((c) => c.status === "enrollable").map((c) => {
|
|
11685
12192
|
const digestChild = enrollableChildById.get(c.task_id);
|
|
@@ -11705,7 +12212,9 @@ async function projectPlanSurface(input) {
|
|
|
11705
12212
|
plan_digest: planDigest,
|
|
11706
12213
|
recovery_children: recoveryChildren,
|
|
11707
12214
|
risk_by_task: Object.fromEntries([...riskByTask.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)),
|
|
11708
|
-
excluded
|
|
12215
|
+
excluded,
|
|
12216
|
+
parallel_groups: parallelGroups,
|
|
12217
|
+
scope_conflicts: scopeConflicts
|
|
11709
12218
|
}
|
|
11710
12219
|
};
|
|
11711
12220
|
}
|
|
@@ -11785,11 +12294,11 @@ async function projectBatchPreflight(options) {
|
|
|
11785
12294
|
if (!planSurface.ok)
|
|
11786
12295
|
return reject(planSurface.key, planSurface.detail);
|
|
11787
12296
|
let reconfirmation;
|
|
11788
|
-
if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
|
|
12297
|
+
if (activeRecord && !isLaneBatchRecord(activeRecord) && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
|
|
11789
12298
|
try {
|
|
11790
12299
|
reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
|
|
11791
12300
|
} catch {
|
|
11792
|
-
return reject("plan_projection_failed", "batch plan reconfirmation is not eligible");
|
|
12301
|
+
return reject("plan_projection_failed", activeRecord.commits.length ? `batch plan changed after ${activeRecord.commits.length} recorded child commit(s). The old authorization cannot execute the revised plan. Preserve the batch record, report, commit evidence and audit; inspect each remaining child's Kernel run before an explicit imm-run handoff. Do not re-enroll settled children or manufacture batch trailers. To close the parked record explicitly, dispose it with the retire_stale_batch Tool for this Initiative; a child that is still mid-flight must settle first. This rejection neither terminates the old batch nor authorizes the revised plan.` : "batch plan reconfirmation is not eligible");
|
|
11793
12302
|
}
|
|
11794
12303
|
}
|
|
11795
12304
|
const projection = {
|
|
@@ -11802,7 +12311,9 @@ async function projectBatchPreflight(options) {
|
|
|
11802
12311
|
plan_digest: planSurface.surface.plan_digest,
|
|
11803
12312
|
recovery_children: planSurface.surface.recovery_children,
|
|
11804
12313
|
risk_by_task: planSurface.surface.risk_by_task,
|
|
11805
|
-
excluded: planSurface.surface.excluded
|
|
12314
|
+
excluded: planSurface.surface.excluded,
|
|
12315
|
+
parallel_groups: planSurface.surface.parallel_groups,
|
|
12316
|
+
scope_conflicts: planSurface.surface.scope_conflicts
|
|
11806
12317
|
};
|
|
11807
12318
|
if (reconfirmation)
|
|
11808
12319
|
Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
|
|
@@ -11849,7 +12360,7 @@ async function authorizeBatch(options) {
|
|
|
11849
12360
|
const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11850
12361
|
if (branchBefore.status !== 0)
|
|
11851
12362
|
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
11852
|
-
const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
|
|
12363
|
+
const ownsUnpersistedHead = isResuming && existingBatch !== null && !isLaneBatchRecord(existingBatch) && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
|
|
11853
12364
|
const fastForwardsRecordedHead = isResuming && existingBatch !== null && existingBatch.branch === batchBranch && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && classifyBatchLineage({
|
|
11854
12365
|
root,
|
|
11855
12366
|
branch: batchBranch,
|
|
@@ -11884,7 +12395,9 @@ async function authorizeBatch(options) {
|
|
|
11884
12395
|
risk: projection.risk_by_task[child.task_id] ?? "material",
|
|
11885
12396
|
status: child.status
|
|
11886
12397
|
})),
|
|
11887
|
-
excluded: projection.excluded
|
|
12398
|
+
excluded: projection.excluded,
|
|
12399
|
+
parallel_groups: projection.parallel_groups,
|
|
12400
|
+
scope_conflicts: projection.scope_conflicts
|
|
11888
12401
|
};
|
|
11889
12402
|
let requestId = null;
|
|
11890
12403
|
if (!reuseAuthorization) {
|
|
@@ -11951,9 +12464,9 @@ async function authorizeBatch(options) {
|
|
|
11951
12464
|
}
|
|
11952
12465
|
|
|
11953
12466
|
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
11954
|
-
import { spawnSync as
|
|
11955
|
-
import { existsSync as
|
|
11956
|
-
import { join as
|
|
12467
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
12468
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
12469
|
+
import { join as join16 } from "node:path";
|
|
11957
12470
|
|
|
11958
12471
|
// plugins/immune-brain/runtime/unattended/batch_git.ts
|
|
11959
12472
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
@@ -11962,7 +12475,7 @@ import {
|
|
|
11962
12475
|
constants as constants6,
|
|
11963
12476
|
closeSync as closeSync7,
|
|
11964
12477
|
existsSync as existsSync9,
|
|
11965
|
-
lstatSync as
|
|
12478
|
+
lstatSync as lstatSync11,
|
|
11966
12479
|
mkdirSync as mkdirSync7,
|
|
11967
12480
|
openSync as openSync7,
|
|
11968
12481
|
realpathSync as realpathSync11,
|
|
@@ -12190,7 +12703,7 @@ function ensureSecureDirectory3(root, relativePath) {
|
|
|
12190
12703
|
for (const segment of segments) {
|
|
12191
12704
|
current = join14(current, segment);
|
|
12192
12705
|
if (existsSync9(current)) {
|
|
12193
|
-
const stats =
|
|
12706
|
+
const stats = lstatSync11(current);
|
|
12194
12707
|
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
12195
12708
|
throw new Error(`${segment} exists but is not a real directory`);
|
|
12196
12709
|
}
|
|
@@ -12204,12 +12717,12 @@ function writeFileAtomically2(root, relativePath, bytes) {
|
|
|
12204
12717
|
const target = join14(root, relativePath);
|
|
12205
12718
|
const targetDir = dirname8(target);
|
|
12206
12719
|
ensureSecureDirectory3(root, dirname8(relativePath));
|
|
12207
|
-
const stats =
|
|
12720
|
+
const stats = lstatSync11(targetDir);
|
|
12208
12721
|
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
12209
12722
|
throw new Error(`${dirname8(relativePath)} is not a real directory`);
|
|
12210
12723
|
}
|
|
12211
12724
|
if (existsSync9(target)) {
|
|
12212
|
-
const targetStats =
|
|
12725
|
+
const targetStats = lstatSync11(target);
|
|
12213
12726
|
if (targetStats.isSymbolicLink()) {
|
|
12214
12727
|
throw new Error(`${relativePath} is a symlink`);
|
|
12215
12728
|
}
|
|
@@ -12518,40 +13031,958 @@ function createDefaultBatchGitPort() {
|
|
|
12518
13031
|
};
|
|
12519
13032
|
}
|
|
12520
13033
|
|
|
12521
|
-
// plugins/immune-brain/runtime/unattended/
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
13034
|
+
// plugins/immune-brain/runtime/unattended/batch_lanes.ts
|
|
13035
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
13036
|
+
import { existsSync as existsSync10, realpathSync as realpathSync12 } from "node:fs";
|
|
13037
|
+
import { isAbsolute as isAbsolute6, join as join15 } from "node:path";
|
|
13038
|
+
|
|
13039
|
+
// plugins/immune-brain/runtime/unattended/batch_integration.ts
|
|
13040
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
13041
|
+
class BatchIntegrationError extends Error {
|
|
13042
|
+
reason;
|
|
13043
|
+
constructor(reason, message) {
|
|
13044
|
+
super(message);
|
|
13045
|
+
this.reason = reason;
|
|
13046
|
+
this.name = "BatchIntegrationError";
|
|
13047
|
+
}
|
|
12529
13048
|
}
|
|
12530
|
-
|
|
12531
|
-
|
|
13049
|
+
var COMMITTER_ENV = {
|
|
13050
|
+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "Immune-Brain Batch",
|
|
13051
|
+
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "immune-brain@local"
|
|
13052
|
+
};
|
|
13053
|
+
function git5(root, args, extra = {}) {
|
|
13054
|
+
const result = spawnSync9("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", ...args], {
|
|
13055
|
+
encoding: "utf8",
|
|
13056
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13057
|
+
input: extra.input,
|
|
13058
|
+
env: { ...process.env, ...COMMITTER_ENV, ...extra.env ?? {} }
|
|
13059
|
+
});
|
|
13060
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
12532
13061
|
}
|
|
12533
|
-
function
|
|
12534
|
-
|
|
13062
|
+
function gitOut(root, args, what) {
|
|
13063
|
+
const result = git5(root, args);
|
|
13064
|
+
if (result.status !== 0)
|
|
13065
|
+
throw new Error(`${what}: ${result.stderr.trim() || "git failed"}`);
|
|
13066
|
+
return result.stdout;
|
|
12535
13067
|
}
|
|
12536
|
-
function
|
|
12537
|
-
const
|
|
12538
|
-
const
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
13068
|
+
function changeIdentity(root, from, to) {
|
|
13069
|
+
const raw = gitOut(root, ["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-ext-diff", "--no-commit-id", from, to], `failed to inspect change ${from}..${to}`);
|
|
13070
|
+
const tokens = raw.split("\x00");
|
|
13071
|
+
const identity = new Map;
|
|
13072
|
+
for (let index = 0;index < tokens.length; index += 1) {
|
|
13073
|
+
const meta = tokens[index];
|
|
13074
|
+
if (!meta.startsWith(":"))
|
|
13075
|
+
continue;
|
|
13076
|
+
const [, newMode, , newSha] = meta.slice(1).split(" ");
|
|
13077
|
+
const path = tokens[index + 1];
|
|
13078
|
+
if (path === undefined || !newMode || !newSha)
|
|
13079
|
+
throw new Error(`unreadable change record for ${from}..${to}`);
|
|
13080
|
+
identity.set(path, `${newMode} ${newSha}`);
|
|
13081
|
+
index += 1;
|
|
12547
13082
|
}
|
|
12548
|
-
|
|
13083
|
+
return identity;
|
|
12549
13084
|
}
|
|
12550
|
-
function
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
13085
|
+
function identitiesEqual(left, right) {
|
|
13086
|
+
if (left.size !== right.size)
|
|
13087
|
+
return false;
|
|
13088
|
+
for (const [path, value] of left)
|
|
13089
|
+
if (right.get(path) !== value)
|
|
13090
|
+
return false;
|
|
13091
|
+
return true;
|
|
13092
|
+
}
|
|
13093
|
+
function singleParent(root, commit) {
|
|
13094
|
+
const parents = gitOut(root, ["rev-parse", `${commit}^@`], `failed to read parents of ${commit}`).split(/\s+/).filter(Boolean);
|
|
13095
|
+
if (parents.length !== 1)
|
|
13096
|
+
throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane commit ${commit} has ${parents.length} parents, expected exactly one`);
|
|
13097
|
+
return parents[0];
|
|
13098
|
+
}
|
|
13099
|
+
function isAncestor(root, ancestor, descendant) {
|
|
13100
|
+
return git5(root, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
|
|
13101
|
+
}
|
|
13102
|
+
function buildCandidate(input) {
|
|
13103
|
+
const { root, batch_head: head, lane_base: base, lane_commit: laneCommit } = input;
|
|
13104
|
+
if (head === base)
|
|
13105
|
+
return laneCommit;
|
|
13106
|
+
const merged = git5(root, ["merge-tree", "--write-tree", `--merge-base=${base}`, head, laneCommit]);
|
|
13107
|
+
if (merged.status === 1)
|
|
13108
|
+
throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: lane commit ${laneCommit} does not apply cleanly onto batch head ${head}`);
|
|
13109
|
+
if (merged.status !== 0)
|
|
13110
|
+
throw new Error(`failed to build integration candidate: ${merged.stderr.trim() || "git merge-tree failed"}`);
|
|
13111
|
+
const tree = merged.stdout.split(`
|
|
13112
|
+
`)[0]?.trim();
|
|
13113
|
+
if (!tree)
|
|
13114
|
+
throw new Error("failed to build integration candidate: no tree produced");
|
|
13115
|
+
const message = gitOut(root, ["log", "-n", "1", "--format=%B", laneCommit], "failed to read lane commit message");
|
|
13116
|
+
const author = gitOut(root, ["log", "-n", "1", "--format=%an%x00%ae%x00%aI", laneCommit], "failed to read lane commit author").trim().split("\x00");
|
|
13117
|
+
const created = git5(root, ["commit-tree", tree, "-p", head, "-F", "-"], {
|
|
13118
|
+
input: message,
|
|
13119
|
+
env: {
|
|
13120
|
+
GIT_AUTHOR_NAME: author[0] ?? COMMITTER_ENV.GIT_COMMITTER_NAME,
|
|
13121
|
+
GIT_AUTHOR_EMAIL: author[1] ?? COMMITTER_ENV.GIT_COMMITTER_EMAIL,
|
|
13122
|
+
...author[2] ? { GIT_AUTHOR_DATE: author[2] } : {}
|
|
13123
|
+
}
|
|
13124
|
+
});
|
|
13125
|
+
const candidate = created.stdout.trim();
|
|
13126
|
+
if (created.status !== 0 || !candidate)
|
|
13127
|
+
throw new Error(`failed to build integration candidate: ${created.stderr.trim() || "git commit-tree failed"}`);
|
|
13128
|
+
return candidate;
|
|
13129
|
+
}
|
|
13130
|
+
function prepareLaneCandidate(input) {
|
|
13131
|
+
const { root, branch, batch_head: head, lane_base: base, lane_commit: laneCommit } = input;
|
|
13132
|
+
const currentBranch = git5(root, ["symbolic-ref", "--short", "HEAD"]);
|
|
13133
|
+
if (currentBranch.status !== 0 || currentBranch.stdout.trim() !== branch)
|
|
13134
|
+
throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: current branch ${currentBranch.stdout.trim()} does not match expected branch ${branch}`);
|
|
13135
|
+
const currentHead = git5(root, ["rev-parse", "HEAD"]).stdout.trim();
|
|
13136
|
+
if (currentHead !== head)
|
|
13137
|
+
throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: current HEAD ${currentHead} does not match expected batch head ${head}`);
|
|
13138
|
+
if (singleParent(root, laneCommit) !== base)
|
|
13139
|
+
throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane commit ${laneCommit} does not descend from lane base ${base}`);
|
|
13140
|
+
if (!isAncestor(root, base, head))
|
|
13141
|
+
throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane base ${base} is not an ancestor of batch head ${head}`);
|
|
13142
|
+
const candidate = buildCandidate({ root, batch_head: head, lane_base: base, lane_commit: laneCommit });
|
|
13143
|
+
if (!identitiesEqual(changeIdentity(root, base, laneCommit), changeIdentity(root, head, candidate)))
|
|
13144
|
+
throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: candidate ${candidate} does not carry the lane commit ${laneCommit} change`);
|
|
13145
|
+
return candidate;
|
|
13146
|
+
}
|
|
13147
|
+
function fastForwardToCandidate(root, candidate) {
|
|
13148
|
+
const moved = git5(root, ["merge", "--ff-only", "--quiet", candidate]);
|
|
13149
|
+
if (moved.status !== 0)
|
|
13150
|
+
throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: batch branch could not fast-forward to ${candidate}: ${moved.stderr.trim() || "git merge failed"}`);
|
|
13151
|
+
const landed = git5(root, ["rev-parse", "HEAD"]).stdout.trim();
|
|
13152
|
+
if (landed !== candidate)
|
|
13153
|
+
throw new Error(`batch branch landed on ${landed}, expected ${candidate}`);
|
|
13154
|
+
return { commit: candidate };
|
|
13155
|
+
}
|
|
13156
|
+
function commitsBetween(root, from, to) {
|
|
13157
|
+
return new Set(gitOut(root, ["rev-list", `${from}..${to}`], `failed to list commits ${from}..${to}`).split(/\s+/).filter(Boolean));
|
|
13158
|
+
}
|
|
13159
|
+
function readCandidateAcceptance(root, candidate, check) {
|
|
13160
|
+
if (!check.intent_path)
|
|
13161
|
+
throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: ${check.task_id} has no intent to rerun`);
|
|
13162
|
+
const shown = git5(root, ["show", `${candidate}:${check.intent_path}`]);
|
|
13163
|
+
if (shown.status !== 0)
|
|
13164
|
+
throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: intent ${check.intent_path} of ${check.task_id} is absent from candidate ${candidate}`);
|
|
13165
|
+
try {
|
|
13166
|
+
const intent = parseTaskIntentV1(JSON.parse(shown.stdout));
|
|
13167
|
+
return intent.acceptance.map((item) => ({ id: `${check.task_id}:${item.id}`, assertion: item.assertion, verification: item.verification }));
|
|
13168
|
+
} catch (error) {
|
|
13169
|
+
throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: intent of ${check.task_id} is unreadable in candidate ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
13170
|
+
}
|
|
13171
|
+
}
|
|
13172
|
+
async function verifyCandidateDescriptors(input) {
|
|
13173
|
+
const { root, candidate } = input;
|
|
13174
|
+
const since = commitsBetween(root, input.lane_base, input.batch_head);
|
|
13175
|
+
const checks = [input.child, ...input.siblings.filter((sibling) => sibling.commit !== null && since.has(sibling.commit))];
|
|
13176
|
+
const acceptance = checks.flatMap((check) => readCandidateAcceptance(root, candidate, check));
|
|
13177
|
+
const descriptors = new Map;
|
|
13178
|
+
try {
|
|
13179
|
+
for (const item of acceptance)
|
|
13180
|
+
descriptors.set(item.id, parseVerificationDescriptor(item.verification));
|
|
13181
|
+
} catch (error) {
|
|
13182
|
+
throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: a verification descriptor is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
13183
|
+
}
|
|
13184
|
+
if (acceptance.length === 0)
|
|
13185
|
+
throw new BatchIntegrationError("batch_integration_check_failed", "batch_integration_check_failed: no descriptor to rerun");
|
|
13186
|
+
const tree = gitOut(root, ["rev-parse", `${candidate}^{tree}`], `failed to read the tree of ${candidate}`).trim();
|
|
13187
|
+
const snapshot = {
|
|
13188
|
+
contract: "assurance_kernel/assurance_snapshot/v2",
|
|
13189
|
+
task_id: input.child.task_id,
|
|
13190
|
+
run_id: null,
|
|
13191
|
+
role: "qa",
|
|
13192
|
+
record_revision: "",
|
|
13193
|
+
workspace_revision: "",
|
|
13194
|
+
intent_revision: 0,
|
|
13195
|
+
intent_content_hash: "",
|
|
13196
|
+
diff_hash: "",
|
|
13197
|
+
lifecycle: "active",
|
|
13198
|
+
artifact_state: "frozen",
|
|
13199
|
+
risk: "material",
|
|
13200
|
+
fresh_acceptance_ids: [],
|
|
13201
|
+
missing_acceptance_ids: [],
|
|
13202
|
+
stale_attestation_ids: [],
|
|
13203
|
+
acceptance,
|
|
13204
|
+
dirty_files: [],
|
|
13205
|
+
review_bundle_digest: null,
|
|
13206
|
+
review_revision: {
|
|
13207
|
+
contract: "assurance_kernel/review_revision_identity/v1",
|
|
13208
|
+
base_head: input.lane_base,
|
|
13209
|
+
review_commit: candidate,
|
|
13210
|
+
review_tree: tree,
|
|
13211
|
+
manifest_digest: ""
|
|
13212
|
+
},
|
|
13213
|
+
root
|
|
13214
|
+
};
|
|
13215
|
+
let verdict;
|
|
13216
|
+
try {
|
|
13217
|
+
verdict = await runDeterministicQa(snapshot, descriptors, { signal: input.signal });
|
|
13218
|
+
} catch (error) {
|
|
13219
|
+
if (error instanceof VerificationAbortedError)
|
|
13220
|
+
throw error;
|
|
13221
|
+
throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: the descriptor rerun could not complete: ${error instanceof Error ? error.message : String(error)}`);
|
|
13222
|
+
}
|
|
13223
|
+
if (verdict.decision !== "pass")
|
|
13224
|
+
throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: ${(verdict.findings ?? []).map((finding) => finding.acceptance_id).join(", ") || "a descriptor"} failed on candidate ${candidate}`);
|
|
13225
|
+
}
|
|
13226
|
+
async function integrateGuardedLaneCommit(input) {
|
|
13227
|
+
const candidate = prepareLaneCandidate(input);
|
|
13228
|
+
await verifyCandidateDescriptors({
|
|
13229
|
+
root: input.root,
|
|
13230
|
+
candidate,
|
|
13231
|
+
lane_base: input.lane_base,
|
|
13232
|
+
batch_head: input.batch_head,
|
|
13233
|
+
child: input.child,
|
|
13234
|
+
siblings: input.siblings,
|
|
13235
|
+
signal: input.signal
|
|
13236
|
+
});
|
|
13237
|
+
return fastForwardToCandidate(input.root, candidate);
|
|
13238
|
+
}
|
|
13239
|
+
function findIntegratedCandidate(input) {
|
|
13240
|
+
const { root, task_id: taskId, batch_id: batchId, from_head: from, lane_base: base, lane_commit: laneCommit } = input;
|
|
13241
|
+
const listed = git5(root, [
|
|
13242
|
+
"log",
|
|
13243
|
+
"--fixed-strings",
|
|
13244
|
+
`--grep=imm(${taskId}):`,
|
|
13245
|
+
"--format=%x01%H%x00%(trailers:key=Immune-Brain-Batch,valueonly)%x00%s",
|
|
13246
|
+
`${from}..HEAD`
|
|
13247
|
+
]);
|
|
13248
|
+
if (listed.status !== 0)
|
|
13249
|
+
throw new Error(`failed to search for integrated ${taskId}: ${listed.stderr.trim()}`);
|
|
13250
|
+
const lane = changeIdentity(root, base, laneCommit);
|
|
13251
|
+
for (const record of listed.stdout.split("\x01")) {
|
|
13252
|
+
const [commit, trailer, subject] = record.split("\x00");
|
|
13253
|
+
if (!commit || trailer?.trim() !== batchId || !subject?.trim().startsWith(`imm(${taskId}):`))
|
|
13254
|
+
continue;
|
|
13255
|
+
const parent = git5(root, ["rev-parse", `${commit}^`]).stdout.trim();
|
|
13256
|
+
if (parent && identitiesEqual(lane, changeIdentity(root, parent, commit)))
|
|
13257
|
+
return commit;
|
|
13258
|
+
}
|
|
13259
|
+
return null;
|
|
13260
|
+
}
|
|
13261
|
+
|
|
13262
|
+
// plugins/immune-brain/runtime/unattended/batch_lanes.ts
|
|
13263
|
+
var PARALLEL_MISMATCH = "batch_parallel_mismatch";
|
|
13264
|
+
var TASK_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
13265
|
+
var MAX_LANE_OFFERS = 64;
|
|
13266
|
+
var MAX_PATH_LENGTH = 4096;
|
|
13267
|
+
function resolveLaneParallel(root, initiativeSlug, maxParallel, laneOffers) {
|
|
13268
|
+
if (maxParallel !== undefined || laneOffers === undefined)
|
|
13269
|
+
return maxParallel;
|
|
13270
|
+
const lookup = findExistingActiveBatch(root, initiativeSlug);
|
|
13271
|
+
if (lookup === null || lookup.corrupt || !isLaneBatchRecord(lookup.record))
|
|
13272
|
+
throw new Error("lane_offers requires max_parallel unless a recorded lane batch is being resumed");
|
|
13273
|
+
return lookup.record.max_parallel;
|
|
13274
|
+
}
|
|
13275
|
+
function parseMaxParallel(value) {
|
|
13276
|
+
if (value === undefined || value === null)
|
|
13277
|
+
return;
|
|
13278
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
|
|
13279
|
+
throw new Error("invalid max_parallel: expected a positive integer");
|
|
13280
|
+
return value;
|
|
13281
|
+
}
|
|
13282
|
+
function parseLaneOffers(value) {
|
|
13283
|
+
if (value === undefined || value === null)
|
|
13284
|
+
return;
|
|
13285
|
+
if (!Array.isArray(value))
|
|
13286
|
+
throw new Error("invalid lane_offers: expected an array");
|
|
13287
|
+
if (value.length > MAX_LANE_OFFERS)
|
|
13288
|
+
throw new Error(`invalid lane_offers: at most ${MAX_LANE_OFFERS} offers`);
|
|
13289
|
+
const seen = new Set;
|
|
13290
|
+
return value.map((entry) => {
|
|
13291
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
13292
|
+
throw new Error("invalid lane_offers: each offer must be an object");
|
|
13293
|
+
const { task_id: taskId, path, ...rest } = entry;
|
|
13294
|
+
if (Object.keys(rest).length > 0)
|
|
13295
|
+
throw new Error("invalid lane_offers: unknown offer field");
|
|
13296
|
+
if (typeof taskId !== "string" || !TASK_ID_PATTERN2.test(taskId))
|
|
13297
|
+
throw new Error("invalid lane_offers: task_id is not a valid task id");
|
|
13298
|
+
if (typeof path !== "string" || !path || path.length > MAX_PATH_LENGTH || path.includes("\x00") || !isAbsolute6(path))
|
|
13299
|
+
throw new Error("invalid lane_offers: path must be an absolute path");
|
|
13300
|
+
if (seen.has(taskId))
|
|
13301
|
+
throw new Error(`invalid lane_offers: duplicate offer for ${taskId}`);
|
|
13302
|
+
seen.add(taskId);
|
|
13303
|
+
return { task_id: taskId, path };
|
|
13304
|
+
});
|
|
13305
|
+
}
|
|
13306
|
+
function laneBranchName(initiativeSlug, taskId) {
|
|
13307
|
+
return `imm-lane/${initiativeSlug}/${taskId}`;
|
|
13308
|
+
}
|
|
13309
|
+
function gitRead(root, args) {
|
|
13310
|
+
const result = spawnSync10("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13311
|
+
return { status: result.status, stdout: (result.stdout ?? "").trim() };
|
|
13312
|
+
}
|
|
13313
|
+
function commonDir(root) {
|
|
13314
|
+
const result = gitRead(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
13315
|
+
if (result.status !== 0 || !result.stdout)
|
|
13316
|
+
return null;
|
|
13317
|
+
try {
|
|
13318
|
+
return realpathSync12(result.stdout);
|
|
13319
|
+
} catch {
|
|
13320
|
+
return null;
|
|
13321
|
+
}
|
|
13322
|
+
}
|
|
13323
|
+
function resolveRealPath(path) {
|
|
13324
|
+
try {
|
|
13325
|
+
return realpathSync12(path);
|
|
13326
|
+
} catch {
|
|
13327
|
+
return null;
|
|
13328
|
+
}
|
|
13329
|
+
}
|
|
13330
|
+
function createDefaultLaneGitPort() {
|
|
13331
|
+
return {
|
|
13332
|
+
resolveRoot: (root) => resolveRealPath(root) ?? root,
|
|
13333
|
+
inspectLane(coordinatorRoot, lanePath) {
|
|
13334
|
+
const real = existsSync10(lanePath) ? resolveRealPath(lanePath) : null;
|
|
13335
|
+
const absent = {
|
|
13336
|
+
exists: real !== null,
|
|
13337
|
+
real_path: real,
|
|
13338
|
+
same_repository: false,
|
|
13339
|
+
is_worktree_root: false,
|
|
13340
|
+
branch: null,
|
|
13341
|
+
head: null,
|
|
13342
|
+
clean: false,
|
|
13343
|
+
active_claim_task_id: null
|
|
13344
|
+
};
|
|
13345
|
+
if (!real)
|
|
13346
|
+
return absent;
|
|
13347
|
+
const laneCommon = commonDir(real);
|
|
13348
|
+
const coordinatorCommon = commonDir(coordinatorRoot);
|
|
13349
|
+
if (!laneCommon || !coordinatorCommon || laneCommon !== coordinatorCommon)
|
|
13350
|
+
return absent;
|
|
13351
|
+
const top = gitRead(real, ["rev-parse", "--show-toplevel"]);
|
|
13352
|
+
const topReal = top.status === 0 ? resolveRealPath(top.stdout) : null;
|
|
13353
|
+
const branch = gitRead(real, ["symbolic-ref", "--short", "HEAD"]);
|
|
13354
|
+
const head = gitRead(real, ["rev-parse", "HEAD"]);
|
|
13355
|
+
const status = gitRead(real, ["status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"]);
|
|
13356
|
+
let activeClaim = null;
|
|
13357
|
+
try {
|
|
13358
|
+
activeClaim = readActiveClaimTaskId(real);
|
|
13359
|
+
} catch {
|
|
13360
|
+
activeClaim = "unreadable";
|
|
13361
|
+
}
|
|
13362
|
+
return {
|
|
13363
|
+
exists: true,
|
|
13364
|
+
real_path: real,
|
|
13365
|
+
same_repository: true,
|
|
13366
|
+
is_worktree_root: topReal === real,
|
|
13367
|
+
branch: branch.status === 0 ? branch.stdout : null,
|
|
13368
|
+
head: head.status === 0 ? head.stdout : null,
|
|
13369
|
+
clean: status.status === 0 && status.stdout === "",
|
|
13370
|
+
active_claim_task_id: activeClaim
|
|
13371
|
+
};
|
|
13372
|
+
},
|
|
13373
|
+
integrate: integrateGuardedLaneCommit,
|
|
13374
|
+
findIntegrated: findIntegratedCandidate,
|
|
13375
|
+
auditReachable({ root, head, task_id }) {
|
|
13376
|
+
const listing = gitRead(root, ["ls-tree", "-r", "--name-only", head, "--", `.imm/audit/${task_id}`]);
|
|
13377
|
+
if (listing.status !== 0)
|
|
13378
|
+
return false;
|
|
13379
|
+
const files = listing.stdout.split(`
|
|
13380
|
+
`);
|
|
13381
|
+
const runDirs = new Set(files.map((file) => file.slice(0, file.lastIndexOf("/"))));
|
|
13382
|
+
return [...runDirs].some((dir) => files.includes(`${dir}/task-record.json`) && files.includes(`${dir}/terminal-proof.json`));
|
|
13383
|
+
}
|
|
13384
|
+
};
|
|
13385
|
+
}
|
|
13386
|
+
function decideLaneAdmission(input) {
|
|
13387
|
+
const { facts } = input;
|
|
13388
|
+
if (!facts.exists || !facts.real_path || !facts.same_repository || !facts.is_worktree_root)
|
|
13389
|
+
return "batch_lane_foreign_repository";
|
|
13390
|
+
if (facts.real_path === input.coordinator_real_path)
|
|
13391
|
+
return "batch_lane_is_coordinator";
|
|
13392
|
+
if (facts.branch !== input.expected_branch)
|
|
13393
|
+
return "batch_lane_branch_mismatch";
|
|
13394
|
+
if (facts.head !== input.batch_head)
|
|
13395
|
+
return "batch_lane_base_mismatch";
|
|
13396
|
+
if (!facts.clean)
|
|
13397
|
+
return "batch_lane_dirty";
|
|
13398
|
+
if (facts.active_claim_task_id !== null || input.bound_paths.includes(facts.real_path))
|
|
13399
|
+
return "batch_lane_occupied";
|
|
13400
|
+
return null;
|
|
13401
|
+
}
|
|
13402
|
+
function laneGitOf(input) {
|
|
13403
|
+
return input.git?.lane ?? createDefaultLaneGitPort();
|
|
13404
|
+
}
|
|
13405
|
+
function serialGitOf(input) {
|
|
13406
|
+
return input.git ?? defaultSerialGit();
|
|
13407
|
+
}
|
|
13408
|
+
function releaseHandoffs(input, record, lanes) {
|
|
13409
|
+
const lineage = classifyBatchLineage({
|
|
13410
|
+
root: input.root,
|
|
13411
|
+
branch: record.branch ?? "",
|
|
13412
|
+
expectedHead: expectedBatchHead(record),
|
|
13413
|
+
childCommits: record.commits,
|
|
13414
|
+
batchId: record.batch_id
|
|
13415
|
+
});
|
|
13416
|
+
if (lineage.kind === "broken")
|
|
13417
|
+
return [];
|
|
13418
|
+
const handoffs = [];
|
|
13419
|
+
for (const child of record.children) {
|
|
13420
|
+
if (child.state !== "integrated" || !child.lane)
|
|
13421
|
+
continue;
|
|
13422
|
+
const facts = lanes.inspectLane(input.root, child.lane.path);
|
|
13423
|
+
if (!facts.exists || !facts.same_repository || !facts.is_worktree_root || !facts.clean || facts.branch !== child.lane.branch || facts.active_claim_task_id !== null)
|
|
13424
|
+
continue;
|
|
13425
|
+
if (!lanes.auditReachable({ root: input.root, head: lineage.head, task_id: child.task_id }))
|
|
13426
|
+
continue;
|
|
13427
|
+
handoffs.push({ role: "lane-steward", action: "release", task_id: child.task_id, lane_branch: child.lane.branch });
|
|
13428
|
+
}
|
|
13429
|
+
return handoffs;
|
|
13430
|
+
}
|
|
13431
|
+
function defaultSerialGit() {
|
|
13432
|
+
return createDefaultBatchGitPort();
|
|
13433
|
+
}
|
|
13434
|
+
var IN_FLIGHT = new Set(["lane_admitted", "enrolled", "settled", "lane_committed"]);
|
|
13435
|
+
function dependentsOf(record, taskId) {
|
|
13436
|
+
return record.children.filter((child) => child.blocked_by.includes(taskId));
|
|
13437
|
+
}
|
|
13438
|
+
function skipDependents(record, taskId, reason) {
|
|
13439
|
+
const skip = new Set([taskId]);
|
|
13440
|
+
const queue = [taskId];
|
|
13441
|
+
while (queue.length > 0) {
|
|
13442
|
+
const current = queue.shift();
|
|
13443
|
+
for (const dependent of dependentsOf(record, current)) {
|
|
13444
|
+
if (skip.has(dependent.task_id))
|
|
13445
|
+
continue;
|
|
13446
|
+
skip.add(dependent.task_id);
|
|
13447
|
+
queue.push(dependent.task_id);
|
|
13448
|
+
}
|
|
13449
|
+
}
|
|
13450
|
+
record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
|
|
13451
|
+
}
|
|
13452
|
+
function consumedSlots(record) {
|
|
13453
|
+
return record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
|
|
13454
|
+
}
|
|
13455
|
+
function scopeOfChild(root, child) {
|
|
13456
|
+
try {
|
|
13457
|
+
return [...readTaskIntent(root, child.task_id, child.intent_path ?? undefined).intent.scope_hint];
|
|
13458
|
+
} catch {
|
|
13459
|
+
return ["**"];
|
|
13460
|
+
}
|
|
13461
|
+
}
|
|
13462
|
+
function scheduleView(input, record) {
|
|
13463
|
+
return record.children.map((child) => ({
|
|
13464
|
+
task_id: child.task_id,
|
|
13465
|
+
state: child.state,
|
|
13466
|
+
blocked_by: child.blocked_by,
|
|
13467
|
+
scope_hint: scopeOfChild(input.root, input.children.find((c) => c.task_id === child.task_id) ?? { task_id: child.task_id, intent_path: null })
|
|
13468
|
+
}));
|
|
13469
|
+
}
|
|
13470
|
+
var LANE_NEEDS_HUMAN_NEXT_ACTION = "The lane batch has stopped on a parked child and its Lane is kept. Resolve the child by hand; continuing needs a new Batch Authorization.";
|
|
13471
|
+
var TERMINAL_NEXT_ACTIONS = {
|
|
13472
|
+
completed: "The batch integrated every enrollable child; review the commits and the tracker.",
|
|
13473
|
+
budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
|
|
13474
|
+
failed: "A lineage failure stopped the batch; inspect the failing child and the branch state.",
|
|
13475
|
+
rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
|
|
13476
|
+
superseded: "The plan moved past this record and an explicit disposition retired it; it grants no handoff and no child delivery.",
|
|
13477
|
+
needs_human: LANE_NEEDS_HUMAN_NEXT_ACTION,
|
|
13478
|
+
running: "The batch is still running; no terminal report is due yet.",
|
|
13479
|
+
prepared: "The batch is prepared but not started."
|
|
13480
|
+
};
|
|
13481
|
+
function laneReport(record, reason, nextAction, extra = {}) {
|
|
13482
|
+
return {
|
|
13483
|
+
contract: "assurance_kernel/batch_run_report/v1",
|
|
13484
|
+
batch_id: record.batch_id,
|
|
13485
|
+
initiative_slug: record.initiative_slug,
|
|
13486
|
+
batch_state: record.batch_state,
|
|
13487
|
+
max_parallel: record.max_parallel,
|
|
13488
|
+
children: record.children,
|
|
13489
|
+
commits: record.commits,
|
|
13490
|
+
reason,
|
|
13491
|
+
handoffs: extra.handoffs ?? [],
|
|
13492
|
+
...extra.refusals?.length ? { lane_refusals: extra.refusals } : {},
|
|
13493
|
+
next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
|
|
13494
|
+
created_at: record.updated_at
|
|
13495
|
+
};
|
|
13496
|
+
}
|
|
13497
|
+
function laneRejectionReport(input, record, reason, nextAction) {
|
|
13498
|
+
const handoffs = releaseHandoffs(input, record, laneGitOf(input));
|
|
13499
|
+
return laneReport({ ...record, batch_state: "rejected" }, reason, handoffs.length ? `${nextAction}. Check each release handoff with the Lane Steward.` : nextAction, { handoffs });
|
|
13500
|
+
}
|
|
13501
|
+
function finalizeLane(root, record, reason, nextAction, extra = {}) {
|
|
13502
|
+
const report = laneReport(record, reason, nextAction, extra);
|
|
13503
|
+
if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human")
|
|
13504
|
+
writeBatchRunReport(root, report);
|
|
13505
|
+
return report;
|
|
13506
|
+
}
|
|
13507
|
+
function refuse2(input, persisted, code, detail) {
|
|
13508
|
+
const base = persisted && isLaneBatchRecord(persisted) ? persisted : prepareBatchLaneRunState({
|
|
13509
|
+
batch_id: input.batch_id,
|
|
13510
|
+
initiative_slug: input.initiative_slug,
|
|
13511
|
+
children: persisted ? [] : input.children,
|
|
13512
|
+
plan_digest: input.plan_digest,
|
|
13513
|
+
base_head: input.base_head,
|
|
13514
|
+
confirmation_time: input.confirmation_time,
|
|
13515
|
+
budget: input.budget,
|
|
13516
|
+
max_parallel: input.max_parallel ?? 1,
|
|
13517
|
+
now: input.now
|
|
13518
|
+
});
|
|
13519
|
+
return laneReport({ ...base, batch_state: "rejected" }, `${code}: ${detail}`, "Correct the lane-mode parameters and call start_unattended_batch again.");
|
|
13520
|
+
}
|
|
13521
|
+
function assertPlanMatches(input, record) {
|
|
13522
|
+
const plan = input.registry.children(input.capability);
|
|
13523
|
+
if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
|
|
13524
|
+
const expected = plan[index];
|
|
13525
|
+
return child.task_id !== expected.task_id || JSON.stringify(child.blocked_by) !== JSON.stringify(expected.blocked_by);
|
|
13526
|
+
}))
|
|
13527
|
+
throw new Error("plan_digest mismatch: persisted children do not match the authorized plan");
|
|
13528
|
+
}
|
|
13529
|
+
function validateNewAuthorization(input) {
|
|
13530
|
+
try {
|
|
13531
|
+
const authorized = input.kernel.validateBatchAuthorization({
|
|
13532
|
+
registry: input.registry,
|
|
13533
|
+
capability: input.capability,
|
|
13534
|
+
binding: {
|
|
13535
|
+
batch_id: input.batch_id,
|
|
13536
|
+
plan_digest: input.plan_digest,
|
|
13537
|
+
base_head: input.base_head,
|
|
13538
|
+
initiative_slug: input.initiative_slug,
|
|
13539
|
+
budget: input.budget
|
|
13540
|
+
}
|
|
13541
|
+
});
|
|
13542
|
+
if (authorized.issued_at !== input.confirmation_time)
|
|
13543
|
+
return "the parked batch requires a fresh literal-user confirmation";
|
|
13544
|
+
const children = input.children.map((child) => {
|
|
13545
|
+
const { intent_path, intent_revision, intent_content_hash } = child;
|
|
13546
|
+
if (intent_path === null || intent_revision === null || intent_content_hash === null)
|
|
13547
|
+
throw new Error(`batch child ${child.task_id} has no complete intent identity`);
|
|
13548
|
+
return { ...child, intent_path, intent_revision, intent_content_hash };
|
|
13549
|
+
});
|
|
13550
|
+
if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
|
|
13551
|
+
return "batch run input does not match the authorized plan or base_head";
|
|
13552
|
+
return null;
|
|
13553
|
+
} catch (error) {
|
|
13554
|
+
return error instanceof Error ? error.message : String(error);
|
|
13555
|
+
}
|
|
13556
|
+
}
|
|
13557
|
+
function rehydrateConsumption(input, record) {
|
|
13558
|
+
const { issued_at: _issuedAt, ...binding } = input.kernel.validateBatchAuthorization({
|
|
13559
|
+
registry: input.registry,
|
|
13560
|
+
capability: input.capability,
|
|
13561
|
+
binding: {
|
|
13562
|
+
batch_id: record.batch_id,
|
|
13563
|
+
plan_digest: record.plan_digest,
|
|
13564
|
+
base_head: record.base_head,
|
|
13565
|
+
initiative_slug: record.initiative_slug,
|
|
13566
|
+
budget: record.budget
|
|
13567
|
+
}
|
|
13568
|
+
});
|
|
13569
|
+
for (const child of record.children) {
|
|
13570
|
+
if (child.state === "pending" || child.state === "skipped_blocked" || child.state === "lane_admitted")
|
|
13571
|
+
continue;
|
|
13572
|
+
if (!input.registry.isChildConsumed(input.capability, child.task_id))
|
|
13573
|
+
input.registry.consumeChild(input.capability, binding, child.task_id);
|
|
13574
|
+
}
|
|
13575
|
+
}
|
|
13576
|
+
function failLineage(root, record, message) {
|
|
13577
|
+
const next = {
|
|
13578
|
+
...record,
|
|
13579
|
+
batch_state: "failed",
|
|
13580
|
+
children: record.children.map((child) => IN_FLIGHT.has(child.state) ? { ...child, state: "needs_human", reason: message } : child)
|
|
13581
|
+
};
|
|
13582
|
+
for (const child of next.children)
|
|
13583
|
+
if (child.state === "needs_human")
|
|
13584
|
+
skipDependents(next, child.task_id, `dependency ${child.task_id} parked`);
|
|
13585
|
+
return finalizeLane(root, writeBatchLaneRunState(root, next), message, "");
|
|
13586
|
+
}
|
|
13587
|
+
function park(record, taskId, reason) {
|
|
13588
|
+
record.children = record.children.map((child) => child.task_id === taskId ? { ...child, state: "needs_human", reason } : child);
|
|
13589
|
+
skipDependents(record, taskId, `dependency ${taskId} parked`);
|
|
13590
|
+
}
|
|
13591
|
+
function isLineageBreak(message) {
|
|
13592
|
+
return message.includes("batch_head_lineage_broken");
|
|
13593
|
+
}
|
|
13594
|
+
async function runLaneBatch(input, persisted) {
|
|
13595
|
+
if (persisted && !isLaneBatchRecord(persisted))
|
|
13596
|
+
return refuse2(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
|
|
13597
|
+
if (persisted && input.max_parallel !== undefined && input.max_parallel !== persisted.max_parallel)
|
|
13598
|
+
return refuse2(input, persisted, PARALLEL_MISMATCH, `the recorded batch runs with max_parallel ${persisted.max_parallel}`);
|
|
13599
|
+
const limit = persisted?.max_parallel ?? input.max_parallel ?? 1;
|
|
13600
|
+
if (!input.children.length)
|
|
13601
|
+
return refuse2(input, persisted, "batch_plan_empty", "batch plan is empty");
|
|
13602
|
+
const git = serialGitOf(input);
|
|
13603
|
+
const lanes = laneGitOf(input);
|
|
13604
|
+
let record;
|
|
13605
|
+
if (persisted) {
|
|
13606
|
+
record = persisted;
|
|
13607
|
+
assertPlanMatches(input, record);
|
|
13608
|
+
if (isTerminalBatchState(record.batch_state))
|
|
13609
|
+
return finalizeLane(input.root, record, `terminal state already reached: ${record.batch_state}`, "", {
|
|
13610
|
+
handoffs: record.batch_state === "completed" ? releaseHandoffs(input, record, lanes) : []
|
|
13611
|
+
});
|
|
13612
|
+
if (record.batch_state === "needs_human") {
|
|
13613
|
+
const invalid = validateNewAuthorization(input);
|
|
13614
|
+
if (invalid || Date.parse(input.confirmation_time) <= Date.parse(record.confirmation_time))
|
|
13615
|
+
return laneReport(record, invalid ?? "the parked batch requires a fresh literal-user confirmation", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
13616
|
+
const resumed = [];
|
|
13617
|
+
for (const child of record.children) {
|
|
13618
|
+
if (child.state !== "needs_human") {
|
|
13619
|
+
resumed.push(child);
|
|
13620
|
+
continue;
|
|
13621
|
+
}
|
|
13622
|
+
if (!child.lane?.run_id)
|
|
13623
|
+
return laneReport(record, "parked Lane has no recorded run identity; inspect it without re-enrolling", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
13624
|
+
const facts = lanes.inspectLane(input.root, child.lane.path);
|
|
13625
|
+
const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
|
|
13626
|
+
if (!facts.exists || !facts.same_repository || !facts.is_worktree_root || facts.branch !== child.lane.branch || fresh.error !== null || fresh.projection.run_id !== child.lane.run_id || fresh.projection.open_user_decision_count > 0 || fresh.projection.replan_required_ids.length > 0)
|
|
13627
|
+
return laneReport(record, "parked Lane identity or decision is unresolved", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
13628
|
+
const state = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready ? child.lane.lane_commit ? "lane_committed" : "settled" : fresh.projection.lifecycle === "active" && fresh.claim?.task_id === child.task_id && (facts.active_claim_task_id === null || facts.active_claim_task_id === child.task_id) ? "enrolled" : null;
|
|
13629
|
+
if (!state)
|
|
13630
|
+
return laneReport(record, "parked child has no recoverable owned run", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
13631
|
+
resumed.push({ ...child, state, reason: null, qa_failures: 0 });
|
|
13632
|
+
}
|
|
13633
|
+
record = {
|
|
13634
|
+
...record,
|
|
13635
|
+
batch_state: "running",
|
|
13636
|
+
confirmation_time: input.confirmation_time,
|
|
13637
|
+
children: resumed.map((child) => child.state === "skipped_blocked" ? { ...child, state: "pending", reason: null } : child)
|
|
13638
|
+
};
|
|
13639
|
+
}
|
|
13640
|
+
} else {
|
|
13641
|
+
const invalid = validateNewAuthorization(input);
|
|
13642
|
+
if (invalid)
|
|
13643
|
+
return refuse2(input, null, "batch_authorization_invalid", invalid);
|
|
13644
|
+
const preflight = await git.preflight({
|
|
13645
|
+
root: input.root,
|
|
13646
|
+
initiative_slug: input.initiative_slug,
|
|
13647
|
+
base_head: input.base_head
|
|
13648
|
+
});
|
|
13649
|
+
if (!preflight.ok)
|
|
13650
|
+
return refuse2(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
|
|
13651
|
+
record = prepareBatchLaneRunState({
|
|
13652
|
+
batch_id: input.batch_id,
|
|
13653
|
+
initiative_slug: input.initiative_slug,
|
|
13654
|
+
children: input.children,
|
|
13655
|
+
plan_digest: input.plan_digest,
|
|
13656
|
+
base_head: input.base_head,
|
|
13657
|
+
confirmation_time: input.confirmation_time,
|
|
13658
|
+
budget: input.budget,
|
|
13659
|
+
max_parallel: limit,
|
|
13660
|
+
now: input.now
|
|
13661
|
+
});
|
|
13662
|
+
}
|
|
13663
|
+
const persist = () => {
|
|
13664
|
+
record = writeBatchLaneRunState(input.root, record);
|
|
13665
|
+
};
|
|
13666
|
+
const resumedPark = persisted?.batch_state === "needs_human" && record.batch_state === "running";
|
|
13667
|
+
if (record.batch_state === "prepared") {
|
|
13668
|
+
record.batch_state = "running";
|
|
13669
|
+
persist();
|
|
13670
|
+
}
|
|
13671
|
+
for (const child of record.children) {
|
|
13672
|
+
if (child.state !== "lane_committed" || !child.lane?.lane_commit)
|
|
13673
|
+
continue;
|
|
13674
|
+
const found = lanes.findIntegrated({
|
|
13675
|
+
root: input.root,
|
|
13676
|
+
task_id: child.task_id,
|
|
13677
|
+
batch_id: record.batch_id,
|
|
13678
|
+
from_head: expectedBatchHead(record),
|
|
13679
|
+
lane_base: child.lane.base_head,
|
|
13680
|
+
lane_commit: child.lane.lane_commit
|
|
13681
|
+
});
|
|
13682
|
+
if (!found)
|
|
13683
|
+
continue;
|
|
13684
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "integrated", commit: found } : c);
|
|
13685
|
+
record.commits = [...record.commits, found];
|
|
13686
|
+
persist();
|
|
13687
|
+
}
|
|
13688
|
+
for (const child of record.children) {
|
|
13689
|
+
if (child.state !== "integrated" || !child.lane)
|
|
13690
|
+
continue;
|
|
13691
|
+
if (lanes.inspectLane(input.root, child.lane.path).exists)
|
|
13692
|
+
continue;
|
|
13693
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "released" } : c);
|
|
13694
|
+
persist();
|
|
13695
|
+
}
|
|
13696
|
+
const lineage = existsSync10(join15(input.root, ".git")) ? classifyBatchLineage({
|
|
13697
|
+
root: input.root,
|
|
13698
|
+
branch: record.branch ?? "",
|
|
13699
|
+
expectedHead: expectedBatchHead(record),
|
|
13700
|
+
childCommits: record.commits,
|
|
13701
|
+
batchId: record.batch_id
|
|
13702
|
+
}) : { kind: "equal", head: expectedBatchHead(record) };
|
|
13703
|
+
if (lineage.kind === "broken")
|
|
13704
|
+
return failLineage(input.root, record, lineage.message);
|
|
13705
|
+
if (lineage.kind === "fast_forward") {
|
|
13706
|
+
record = writeBatchLaneRunState(input.root, {
|
|
13707
|
+
...record,
|
|
13708
|
+
adopted_heads: [...record.adopted_heads ?? [], { from: expectedBatchHead(record), to: lineage.head }]
|
|
13709
|
+
});
|
|
13710
|
+
}
|
|
13711
|
+
try {
|
|
13712
|
+
rehydrateConsumption(input, record);
|
|
13713
|
+
} catch (error) {
|
|
13714
|
+
return failLineage(input.root, record, error instanceof Error ? error.message : String(error));
|
|
13715
|
+
}
|
|
13716
|
+
if (resumedPark)
|
|
13717
|
+
persist();
|
|
13718
|
+
const refusals = [];
|
|
13719
|
+
let parkedMessage = null;
|
|
13720
|
+
const reviewOpen = [];
|
|
13721
|
+
const parkChild = (taskId, reason) => {
|
|
13722
|
+
park(record, taskId, reason);
|
|
13723
|
+
parkedMessage ??= reason;
|
|
13724
|
+
persist();
|
|
13725
|
+
};
|
|
13726
|
+
const enrollInLane = async (taskId) => {
|
|
13727
|
+
const lane = record.children.find((c) => c.task_id === taskId).lane;
|
|
13728
|
+
try {
|
|
13729
|
+
await input.kernel.enrollTask({
|
|
13730
|
+
root: lane.path,
|
|
13731
|
+
task_id: taskId,
|
|
13732
|
+
batch: { registry: input.registry, capability: input.capability, binding: { batch_id: record.batch_id, expected_head: lane.base_head } }
|
|
13733
|
+
});
|
|
13734
|
+
const fresh = await input.kernel.projectTask(lane.path, taskId);
|
|
13735
|
+
if (fresh.error !== null || !fresh.projection.run_id || fresh.claim?.task_id !== taskId)
|
|
13736
|
+
throw new Error("enrolled Lane has no observable owned run");
|
|
13737
|
+
record.children = record.children.map((c) => c.task_id === taskId ? { ...c, state: "enrolled", reason: null, lane: { ...lane, run_id: fresh.projection.run_id } } : c);
|
|
13738
|
+
persist();
|
|
13739
|
+
return null;
|
|
13740
|
+
} catch (error) {
|
|
13741
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13742
|
+
parkChild(taskId, message);
|
|
13743
|
+
if (!isLineageBreak(message))
|
|
13744
|
+
return null;
|
|
13745
|
+
record.batch_state = "failed";
|
|
13746
|
+
persist();
|
|
13747
|
+
return finalizeLane(input.root, record, message, "", { refusals });
|
|
13748
|
+
}
|
|
13749
|
+
};
|
|
13750
|
+
for (const child of [...record.children]) {
|
|
13751
|
+
if (child.state !== "lane_admitted" && child.state !== "enrolled" || !child.lane)
|
|
13752
|
+
continue;
|
|
13753
|
+
let fresh;
|
|
13754
|
+
try {
|
|
13755
|
+
fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
|
|
13756
|
+
} catch {
|
|
13757
|
+
fresh = { error: "unreadable" };
|
|
13758
|
+
}
|
|
13759
|
+
if (fresh.error !== null) {
|
|
13760
|
+
parkChild(child.task_id, "batch_lane_lost");
|
|
13761
|
+
continue;
|
|
13762
|
+
}
|
|
13763
|
+
if (child.lane.run_id && fresh.projection.run_id !== child.lane.run_id) {
|
|
13764
|
+
parkChild(child.task_id, "batch_lane_run_mismatch");
|
|
13765
|
+
continue;
|
|
13766
|
+
}
|
|
13767
|
+
const holdsClaim = fresh.claim !== null && fresh.claim.task_id === child.task_id;
|
|
13768
|
+
if (child.state === "lane_admitted") {
|
|
13769
|
+
if (holdsClaim) {
|
|
13770
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing lane claim after interruption", lane: { ...child.lane, run_id: fresh.projection.run_id } } : c);
|
|
13771
|
+
persist();
|
|
13772
|
+
} else {
|
|
13773
|
+
const failed = await enrollInLane(child.task_id);
|
|
13774
|
+
if (failed)
|
|
13775
|
+
return failed;
|
|
13776
|
+
}
|
|
13777
|
+
continue;
|
|
13778
|
+
}
|
|
13779
|
+
if (fresh.projection.lifecycle === "done" && fresh.projection.completion_ready) {
|
|
13780
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
|
|
13781
|
+
persist();
|
|
13782
|
+
continue;
|
|
13783
|
+
}
|
|
13784
|
+
if (!holdsClaim) {
|
|
13785
|
+
parkChild(child.task_id, "batch_lane_lost");
|
|
13786
|
+
continue;
|
|
13787
|
+
}
|
|
13788
|
+
if (!input.kernel.ownsTaskClaim(child.task_id)) {
|
|
13789
|
+
parkChild(child.task_id, "claim held by another batch");
|
|
13790
|
+
continue;
|
|
13791
|
+
}
|
|
13792
|
+
if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen")
|
|
13793
|
+
continue;
|
|
13794
|
+
const terminal = await input.kernel.advanceTask(child.lane.path, child.task_id);
|
|
13795
|
+
if (terminal.state === "completed") {
|
|
13796
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null, qa_failures: 0 } : c);
|
|
13797
|
+
persist();
|
|
13798
|
+
} else if (terminal.state === "review_ready") {
|
|
13799
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
|
|
13800
|
+
persist();
|
|
13801
|
+
reviewOpen.push(child.task_id);
|
|
13802
|
+
} else if (terminal.state === "rework" && terminal.operation === "qa") {
|
|
13803
|
+
const failures = child.qa_failures + 1;
|
|
13804
|
+
if (failures >= record.budget.qa_failure_limit) {
|
|
13805
|
+
const reason = "QA failure limit reached";
|
|
13806
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason, qa_failures: failures } : c);
|
|
13807
|
+
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
13808
|
+
parkedMessage ??= reason;
|
|
13809
|
+
persist();
|
|
13810
|
+
continue;
|
|
13811
|
+
}
|
|
13812
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, qa_failures: failures } : c);
|
|
13813
|
+
persist();
|
|
13814
|
+
} else if (terminal.state === "rework" || terminal.environment_failure || terminal.state === "review_preparation_failed" || terminal.recovery?.category === "repair") {
|
|
13815
|
+
continue;
|
|
13816
|
+
} else {
|
|
13817
|
+
const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
|
|
13818
|
+
parkChild(child.task_id, reason);
|
|
13819
|
+
}
|
|
13820
|
+
}
|
|
13821
|
+
for (const child of [...record.children]) {
|
|
13822
|
+
if (child.state !== "settled" || !child.lane)
|
|
13823
|
+
continue;
|
|
13824
|
+
const planChild = input.children.find((c) => c.task_id === child.task_id);
|
|
13825
|
+
let laneCommit;
|
|
13826
|
+
try {
|
|
13827
|
+
const existing = await git.lookupBatchCommit(child.lane.path, child.task_id, record.batch_id, child.lane.base_head, child.lane.branch);
|
|
13828
|
+
laneCommit = existing?.commit ?? (await git.commitChild(child.lane.path, child.task_id, record.batch_id, child.lane.base_head, child.lane.branch, planChild?.intent_path ?? undefined)).commit;
|
|
13829
|
+
} catch (error) {
|
|
13830
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13831
|
+
parkChild(child.task_id, message);
|
|
13832
|
+
if (!isLineageBreak(message))
|
|
13833
|
+
continue;
|
|
13834
|
+
record.batch_state = "failed";
|
|
13835
|
+
persist();
|
|
13836
|
+
return finalizeLane(input.root, record, message, "");
|
|
13837
|
+
}
|
|
13838
|
+
record.children = record.children.map((c) => c.task_id === child.task_id && c.lane ? { ...c, state: "lane_committed", lane: { ...c.lane, lane_commit: laneCommit } } : c);
|
|
13839
|
+
persist();
|
|
13840
|
+
}
|
|
13841
|
+
for (const child of [...record.children]) {
|
|
13842
|
+
if (child.state !== "lane_committed" || !child.lane?.lane_commit)
|
|
13843
|
+
continue;
|
|
13844
|
+
const intentPathOf = (taskId) => input.children.find((c) => c.task_id === taskId)?.intent_path ?? null;
|
|
13845
|
+
try {
|
|
13846
|
+
const { commit } = await lanes.integrate({
|
|
13847
|
+
root: input.root,
|
|
13848
|
+
branch: record.branch ?? "",
|
|
13849
|
+
batch_head: expectedBatchHead(record),
|
|
13850
|
+
lane_base: child.lane.base_head,
|
|
13851
|
+
lane_commit: child.lane.lane_commit,
|
|
13852
|
+
child: { task_id: child.task_id, intent_path: intentPathOf(child.task_id) },
|
|
13853
|
+
siblings: record.children.filter((c) => c.state === "integrated" && c.task_id !== child.task_id).map((c) => ({ task_id: c.task_id, intent_path: intentPathOf(c.task_id), commit: c.commit }))
|
|
13854
|
+
});
|
|
13855
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "integrated", commit, reason: null } : c);
|
|
13856
|
+
record.commits = [...record.commits, commit];
|
|
13857
|
+
persist();
|
|
13858
|
+
} catch (error) {
|
|
13859
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13860
|
+
const reason = error instanceof BatchIntegrationError ? error.reason : "batch_integration_conflict";
|
|
13861
|
+
parkChild(child.task_id, isLineageBreak(message) || message.startsWith(`${reason}:`) ? message : `${reason}: ${message}`);
|
|
13862
|
+
if (isLineageBreak(message)) {
|
|
13863
|
+
record.batch_state = "failed";
|
|
13864
|
+
persist();
|
|
13865
|
+
return finalizeLane(input.root, record, message, "");
|
|
13866
|
+
}
|
|
13867
|
+
}
|
|
13868
|
+
}
|
|
13869
|
+
const coordinatorReal = lanes.resolveRoot(input.root);
|
|
13870
|
+
for (const offer of input.lane_offers ?? []) {
|
|
13871
|
+
const startable = new Set(startableChildren(scheduleView(input, record), limit));
|
|
13872
|
+
const child = record.children.find((c) => c.task_id === offer.task_id);
|
|
13873
|
+
if (!child || child.state !== "pending" || !startable.has(child.task_id)) {
|
|
13874
|
+
refusals.push({ ...offer, reason: "batch_lane_unknown_child" });
|
|
13875
|
+
continue;
|
|
13876
|
+
}
|
|
13877
|
+
const head = expectedBatchHead(record);
|
|
13878
|
+
const facts = lanes.inspectLane(input.root, offer.path);
|
|
13879
|
+
const reason = decideLaneAdmission({
|
|
13880
|
+
facts,
|
|
13881
|
+
coordinator_real_path: coordinatorReal,
|
|
13882
|
+
expected_branch: laneBranchName(record.initiative_slug, child.task_id),
|
|
13883
|
+
batch_head: head,
|
|
13884
|
+
bound_paths: record.children.flatMap((c) => c.lane && c.state !== "released" ? [c.lane.path] : [])
|
|
13885
|
+
});
|
|
13886
|
+
if (reason) {
|
|
13887
|
+
refusals.push({ ...offer, reason });
|
|
13888
|
+
continue;
|
|
13889
|
+
}
|
|
13890
|
+
if (consumedSlots(record) >= record.budget.max_children)
|
|
13891
|
+
break;
|
|
13892
|
+
const lane = { path: facts.real_path, branch: laneBranchName(record.initiative_slug, child.task_id), base_head: head, lane_commit: null };
|
|
13893
|
+
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "lane_admitted", lane } : c);
|
|
13894
|
+
persist();
|
|
13895
|
+
const failed = await enrollInLane(child.task_id);
|
|
13896
|
+
if (failed)
|
|
13897
|
+
return failed;
|
|
13898
|
+
}
|
|
13899
|
+
if (record.children.every((c) => c.state === "integrated" || c.state === "released")) {
|
|
13900
|
+
record.batch_state = "completed";
|
|
13901
|
+
persist();
|
|
13902
|
+
return finalizeLane(input.root, record, "all enrollable children integrated", "", {
|
|
13903
|
+
handoffs: releaseHandoffs(input, record, lanes),
|
|
13904
|
+
refusals
|
|
13905
|
+
});
|
|
13906
|
+
}
|
|
13907
|
+
const handoffs = [];
|
|
13908
|
+
for (const child of record.children) {
|
|
13909
|
+
if (child.state !== "enrolled" || !child.lane)
|
|
13910
|
+
continue;
|
|
13911
|
+
const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
|
|
13912
|
+
if (fresh.error !== null)
|
|
13913
|
+
continue;
|
|
13914
|
+
handoffs.push({
|
|
13915
|
+
role: "executor",
|
|
13916
|
+
task_id: child.task_id,
|
|
13917
|
+
run_id: fresh.projection.run_id ?? null,
|
|
13918
|
+
record_revision: fresh.projection.record_revision,
|
|
13919
|
+
next_obligation: fresh.projection.next_obligation,
|
|
13920
|
+
lane_branch: child.lane.branch,
|
|
13921
|
+
lane_path: child.lane.path
|
|
13922
|
+
});
|
|
13923
|
+
}
|
|
13924
|
+
handoffs.push(...releaseHandoffs(input, record, lanes));
|
|
13925
|
+
const inFlight = record.children.filter((c) => IN_FLIGHT.has(c.state));
|
|
13926
|
+
const startable = startableChildren(scheduleView(input, record), limit);
|
|
13927
|
+
const overBudget = consumedSlots(record) >= record.budget.max_children;
|
|
13928
|
+
if (!inFlight.length && (!startable.length || overBudget)) {
|
|
13929
|
+
const parked = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked");
|
|
13930
|
+
record.batch_state = parked ? "needs_human" : overBudget ? "budget_stopped" : "needs_human";
|
|
13931
|
+
persist();
|
|
13932
|
+
return finalizeLane(input.root, record, parkedMessage ?? (parked ? "a parked child needs a human decision" : overBudget ? `max_children budget exhausted (${record.budget.max_children})` : "no child can start"), "", { refusals });
|
|
13933
|
+
}
|
|
13934
|
+
if (!overBudget) {
|
|
13935
|
+
for (const taskId of startable) {
|
|
13936
|
+
handoffs.push({
|
|
13937
|
+
role: "lane-steward",
|
|
13938
|
+
action: "provision",
|
|
13939
|
+
task_id: taskId,
|
|
13940
|
+
lane_branch: laneBranchName(record.initiative_slug, taskId),
|
|
13941
|
+
base_head: expectedBatchHead(record),
|
|
13942
|
+
executor_hosts: LANE_EXECUTOR_HOSTS
|
|
13943
|
+
});
|
|
13944
|
+
}
|
|
13945
|
+
}
|
|
13946
|
+
if (reviewOpen.length > 0) {
|
|
13947
|
+
return laneReport(record, reviewOpen.length === 1 ? `child ${reviewOpen[0]} holds an open Review reservation` : `children ${reviewOpen.join(", ")} hold open Review reservations`, "Submit each reserved foreground Review verdict in its own Lane, then call start_unattended_batch again to continue.", { handoffs, refusals });
|
|
13948
|
+
}
|
|
13949
|
+
return laneReport(record, null, handoffs.some((h) => h.role === "lane-steward" && h.action === "provision") ? "Provide a Lane for each provision handoff, then call start_unattended_batch again with lane_offers." : "Run each executor handoff in its Lane. When a Lane finishes, call start_unattended_batch again with the same Initiative.", { handoffs, refusals });
|
|
13950
|
+
}
|
|
13951
|
+
|
|
13952
|
+
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
13953
|
+
function batchQaFailureFacts(record) {
|
|
13954
|
+
const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
|
|
13955
|
+
return {
|
|
13956
|
+
qa_failure_count: failed.length,
|
|
13957
|
+
last_qa_failure_at: failed.at(-1)?.at ?? null,
|
|
13958
|
+
recovery_findings: record.findings.filter((f) => f.status === "open").map((f) => ({ id: f.id, kind: f.kind, status: f.status, acceptance_id: f.acceptance_id }))
|
|
13959
|
+
};
|
|
13960
|
+
}
|
|
13961
|
+
function batchGitPortOf(input) {
|
|
13962
|
+
return input.git ?? createDefaultBatchGitPort();
|
|
13963
|
+
}
|
|
13964
|
+
function dependentsOf2(record, taskId) {
|
|
13965
|
+
return record.children.filter((child) => child.blocked_by.includes(taskId));
|
|
13966
|
+
}
|
|
13967
|
+
function skipDependents2(record, taskId, reason) {
|
|
13968
|
+
const skip = new Set([taskId]);
|
|
13969
|
+
const queue = [taskId];
|
|
13970
|
+
while (queue.length > 0) {
|
|
13971
|
+
const current = queue.shift();
|
|
13972
|
+
for (const dependent of dependentsOf2(record, current)) {
|
|
13973
|
+
if (skip.has(dependent.task_id))
|
|
13974
|
+
continue;
|
|
13975
|
+
skip.add(dependent.task_id);
|
|
13976
|
+
queue.push(dependent.task_id);
|
|
13977
|
+
}
|
|
13978
|
+
}
|
|
13979
|
+
record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
|
|
13980
|
+
}
|
|
13981
|
+
function budgetStopReason(record) {
|
|
13982
|
+
const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
|
|
13983
|
+
if (enrolledCount >= record.budget.max_children)
|
|
13984
|
+
return `max_children budget exhausted (${record.budget.max_children})`;
|
|
13985
|
+
return null;
|
|
12555
13986
|
}
|
|
12556
13987
|
function requireFreshProjection(result, taskId) {
|
|
12557
13988
|
if (result.error !== null)
|
|
@@ -12561,11 +13992,12 @@ function requireFreshProjection(result, taskId) {
|
|
|
12561
13992
|
function nextEnrollableChild(record) {
|
|
12562
13993
|
return record.children.find((child) => child.state === "pending" && record.children.every((other) => !child.blocked_by.includes(other.task_id) || other.state === "committed")) ?? null;
|
|
12563
13994
|
}
|
|
12564
|
-
var
|
|
13995
|
+
var TERMINAL_NEXT_ACTIONS2 = {
|
|
12565
13996
|
completed: "The batch settled every enrollable child; review the commits and the tracker.",
|
|
12566
13997
|
budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
|
|
12567
13998
|
failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
|
|
12568
13999
|
rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
|
|
14000
|
+
superseded: "The plan moved past this record and an explicit disposition retired it; it grants no handoff and no child delivery.",
|
|
12569
14001
|
needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
|
|
12570
14002
|
running: "The batch is still running; no terminal report is due yet.",
|
|
12571
14003
|
prepared: "The batch is prepared but not started."
|
|
@@ -12579,7 +14011,7 @@ function reportFor(record, reason, nextAction) {
|
|
|
12579
14011
|
children: record.children,
|
|
12580
14012
|
commits: record.commits,
|
|
12581
14013
|
reason,
|
|
12582
|
-
next_action: nextAction || (
|
|
14014
|
+
next_action: nextAction || (TERMINAL_NEXT_ACTIONS2[record.batch_state] ?? "Inspect the batch run state."),
|
|
12583
14015
|
created_at: record.updated_at
|
|
12584
14016
|
};
|
|
12585
14017
|
}
|
|
@@ -12613,13 +14045,13 @@ function failPersistedLineage(root, existing, message) {
|
|
|
12613
14045
|
const record = { ...existing, batch_state: "failed", children };
|
|
12614
14046
|
for (const child of record.children) {
|
|
12615
14047
|
if (child.state === "needs_human") {
|
|
12616
|
-
|
|
14048
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
12617
14049
|
}
|
|
12618
14050
|
}
|
|
12619
14051
|
return writeBatchRunState(root, record);
|
|
12620
14052
|
}
|
|
12621
14053
|
function reconcileLineage(root, record) {
|
|
12622
|
-
if (!
|
|
14054
|
+
if (!existsSync11(join16(root, ".git")))
|
|
12623
14055
|
return { record, failure: null };
|
|
12624
14056
|
const expected = expectedBatchHead(record);
|
|
12625
14057
|
const lineage = classifyBatchLineage({
|
|
@@ -12642,8 +14074,8 @@ function reconcileLineage(root, record) {
|
|
|
12642
14074
|
};
|
|
12643
14075
|
}
|
|
12644
14076
|
async function validatePersistedRun(input, record) {
|
|
12645
|
-
if (!record.commits.length && record.children[0]?.state === "settled" &&
|
|
12646
|
-
const head =
|
|
14077
|
+
if (!record.commits.length && record.children[0]?.state === "settled" && existsSync11(join16(input.root, ".git"))) {
|
|
14078
|
+
const head = spawnSync11("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
12647
14079
|
const live = head.stdout.trim();
|
|
12648
14080
|
const expected = expectedBatchHead(record);
|
|
12649
14081
|
const adoptable = () => classifyBatchLineage({
|
|
@@ -12672,8 +14104,8 @@ async function validatePersistedRun(input, record) {
|
|
|
12672
14104
|
const git = batchGitPortOf(input);
|
|
12673
14105
|
const evidence = await git.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch);
|
|
12674
14106
|
if (!evidence || evidence.commit !== child.commit) {
|
|
12675
|
-
if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 &&
|
|
12676
|
-
const reach =
|
|
14107
|
+
if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync11(join16(input.root, ".git"))) {
|
|
14108
|
+
const reach = spawnSync11("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
12677
14109
|
if (reach.status !== 0) {
|
|
12678
14110
|
throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
|
|
12679
14111
|
}
|
|
@@ -12758,17 +14190,22 @@ async function startBatch(input) {
|
|
|
12758
14190
|
function rejectionReport(input, reason) {
|
|
12759
14191
|
const persisted = (() => {
|
|
12760
14192
|
try {
|
|
12761
|
-
return
|
|
14193
|
+
return readAnyBatchRunState(input.root, input.batch_id);
|
|
12762
14194
|
} catch {
|
|
12763
14195
|
return null;
|
|
12764
14196
|
}
|
|
12765
14197
|
})();
|
|
14198
|
+
if (persisted !== null && isLaneBatchRecord(persisted))
|
|
14199
|
+
return laneRejectionReport(input, persisted, reason, "settle the reported kernel store condition and retry in the current Host");
|
|
12766
14200
|
return reportFor({
|
|
12767
14201
|
...persisted ?? prepareBatchRunState({ ...input, children: [], now: input.now }),
|
|
12768
14202
|
batch_state: "rejected"
|
|
12769
14203
|
}, reason, "settle the reported kernel store condition and retry in the current Host");
|
|
12770
14204
|
}
|
|
12771
14205
|
async function startBatchLocked(input) {
|
|
14206
|
+
const recorded = readAnyBatchRunState(input.root, input.batch_id);
|
|
14207
|
+
if (input.max_parallel !== undefined || recorded !== null && isLaneBatchRecord(recorded))
|
|
14208
|
+
return await runLaneBatch(input, recorded);
|
|
12772
14209
|
if (!input.children.length) {
|
|
12773
14210
|
const rejected = prepareBatchRunState({ ...input, children: [], now: input.now });
|
|
12774
14211
|
return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
|
|
@@ -12818,7 +14255,7 @@ async function startBatchLocked(input) {
|
|
|
12818
14255
|
const reparked = { ...existing, children: remapped };
|
|
12819
14256
|
for (const parked of remapped) {
|
|
12820
14257
|
if (parked.state === "needs_human")
|
|
12821
|
-
|
|
14258
|
+
skipDependents2(reparked, parked.task_id, `dependency ${parked.task_id} parked`);
|
|
12822
14259
|
}
|
|
12823
14260
|
existing = writeBatchRunState(input.root, {
|
|
12824
14261
|
...reparked,
|
|
@@ -12901,7 +14338,7 @@ async function startBatchLocked(input) {
|
|
|
12901
14338
|
adoptedClaim = true;
|
|
12902
14339
|
} else if (claimState === "foreign") {
|
|
12903
14340
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: "claim held by another batch" } : c);
|
|
12904
|
-
|
|
14341
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
12905
14342
|
record.batch_state = "needs_human";
|
|
12906
14343
|
persist();
|
|
12907
14344
|
return finalize(input.root, record, "claim held by another batch", "needs-human-attention");
|
|
@@ -12947,13 +14384,13 @@ async function startBatchLocked(input) {
|
|
|
12947
14384
|
const message = error instanceof Error ? error.message : String(error);
|
|
12948
14385
|
if (isLineageBreakError(message)) {
|
|
12949
14386
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
12950
|
-
|
|
14387
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
12951
14388
|
record.batch_state = "failed";
|
|
12952
14389
|
persist();
|
|
12953
14390
|
break;
|
|
12954
14391
|
}
|
|
12955
14392
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
12956
|
-
|
|
14393
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
12957
14394
|
record.batch_state = "needs_human";
|
|
12958
14395
|
persist();
|
|
12959
14396
|
break;
|
|
@@ -13011,7 +14448,7 @@ async function resumeBatch(input, projection) {
|
|
|
13011
14448
|
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
|
|
13012
14449
|
const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
|
|
13013
14450
|
if (holdsClaim === false && fresh.claim !== null && fresh.claim.task_id === driven.task_id && !input.kernel.ownsTaskClaim(driven.task_id)) {
|
|
13014
|
-
|
|
14451
|
+
skipDependents2(existing, driven.task_id, `dependency ${driven.task_id} parked`);
|
|
13015
14452
|
existing = writeBatchRunState(input.root, {
|
|
13016
14453
|
...existing,
|
|
13017
14454
|
batch_state: "needs_human",
|
|
@@ -13023,7 +14460,7 @@ async function resumeBatch(input, projection) {
|
|
|
13023
14460
|
const failures = Math.max(existing.consecutive_qa_failures, fresh.qa_failure_count ?? 0);
|
|
13024
14461
|
if (failures >= existing.budget.qa_failure_limit && fresh.last_qa_failure_at && Date.parse(fresh.last_qa_failure_at) > Date.parse(existing.confirmation_time)) {
|
|
13025
14462
|
const reason = "QA failure limit reached; repair and settle the own child through Kernel before resuming the batch";
|
|
13026
|
-
|
|
14463
|
+
skipDependents2(existing, driven.task_id, `dependency ${driven.task_id} parked`);
|
|
13027
14464
|
existing = writeBatchRunState(input.root, {
|
|
13028
14465
|
...existing,
|
|
13029
14466
|
batch_state: "needs_human",
|
|
@@ -13094,7 +14531,7 @@ async function driveInterruptedChild(input, child) {
|
|
|
13094
14531
|
} catch (error) {
|
|
13095
14532
|
const message = error instanceof Error ? error.message : String(error);
|
|
13096
14533
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: `commit lookup failed: ${message}` } : c);
|
|
13097
|
-
|
|
14534
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} failed to commit`);
|
|
13098
14535
|
const isLineageError = message.includes("batch_head_lineage_broken") || message.includes("lineage");
|
|
13099
14536
|
record.batch_state = isLineageError ? "failed" : "needs_human";
|
|
13100
14537
|
persist();
|
|
@@ -13107,12 +14544,12 @@ async function driveInterruptedChild(input, child) {
|
|
|
13107
14544
|
const planChild = input.children.find((c) => c.task_id === child.task_id);
|
|
13108
14545
|
const intentPath = planChild?.intent_path ?? undefined;
|
|
13109
14546
|
const doCommit = async () => batchGitPortOf(input).commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath);
|
|
13110
|
-
if (existing && !record.commits.length &&
|
|
14547
|
+
if (existing && !record.commits.length && existsSync11(join16(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
|
|
13111
14548
|
throw new Error("first unpersisted batch commit provenance is invalid");
|
|
13112
14549
|
const adopted = existing ?? await doCommit().catch((error) => {
|
|
13113
14550
|
const message = error instanceof Error ? error.message : String(error);
|
|
13114
14551
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
13115
|
-
|
|
14552
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} failed to commit`);
|
|
13116
14553
|
record.batch_state = "failed";
|
|
13117
14554
|
persist();
|
|
13118
14555
|
throw new BatchCommitAbortError(message);
|
|
@@ -13142,7 +14579,7 @@ async function driveInterruptedChild(input, child) {
|
|
|
13142
14579
|
const reason = "QA failure limit reached";
|
|
13143
14580
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
13144
14581
|
record.batch_state = "needs_human";
|
|
13145
|
-
|
|
14582
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
13146
14583
|
persist();
|
|
13147
14584
|
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
13148
14585
|
return finalize(input.root, record, reason, "Repair and settle the own child through Kernel, then re-confirm the batch.", { recovery: deriveAssuranceRecovery(child.task_id, fresh, terminal, fresh.recovery_findings) ?? undefined, diagnostics: terminal.diagnostics });
|
|
@@ -13157,7 +14594,7 @@ async function driveInterruptedChild(input, child) {
|
|
|
13157
14594
|
const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
|
|
13158
14595
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
13159
14596
|
record.batch_state = "needs_human";
|
|
13160
|
-
|
|
14597
|
+
skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
13161
14598
|
persist();
|
|
13162
14599
|
return finalize(input.root, record, reason, "");
|
|
13163
14600
|
}
|
|
@@ -13220,22 +14657,126 @@ function createBatchKernelPort(input) {
|
|
|
13220
14657
|
};
|
|
13221
14658
|
}
|
|
13222
14659
|
|
|
13223
|
-
// plugins/immune-brain/runtime/
|
|
13224
|
-
|
|
13225
|
-
|
|
14660
|
+
// plugins/immune-brain/runtime/unattended/batch_disposition.ts
|
|
14661
|
+
var SERIAL_IN_FLIGHT = new Set(["enrolled", "settled"]);
|
|
14662
|
+
var LANE_IN_FLIGHT = new Set(["lane_admitted", "enrolled", "settled", "lane_committed"]);
|
|
14663
|
+
var RETIRE_REASON = "Retired by an explicit literal-user disposition: the batch plan moved past this record, so its authorization could neither reconfirm nor continue. " + "Recorded child commits, child states and the batch branch are preserved as evidence. " + "Children this batch never executed carry no commit, no batch trailer and no approval; nothing infers their delivery from this record.";
|
|
14664
|
+
var RETIRE_NEXT_ACTION = "The Initiative has no active batch. Run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative after checking out its batch branch.";
|
|
14665
|
+
function childEvidenceOf(record) {
|
|
14666
|
+
if (record.contract === "assurance_kernel/batch_run_state/v2")
|
|
14667
|
+
return record.children.map((child) => ({
|
|
14668
|
+
task_id: child.task_id,
|
|
14669
|
+
slice_id: child.slice_id,
|
|
14670
|
+
state: child.state,
|
|
14671
|
+
commit: child.commit,
|
|
14672
|
+
lane_branch: child.lane?.branch ?? null
|
|
14673
|
+
}));
|
|
14674
|
+
return record.children.map((child) => ({
|
|
14675
|
+
task_id: child.task_id,
|
|
14676
|
+
slice_id: child.slice_id,
|
|
14677
|
+
state: child.state,
|
|
14678
|
+
commit: child.commit,
|
|
14679
|
+
lane_branch: null
|
|
14680
|
+
}));
|
|
13226
14681
|
}
|
|
13227
|
-
function
|
|
13228
|
-
|
|
14682
|
+
function inFlightChildOf(record) {
|
|
14683
|
+
const inFlight = record.contract === "assurance_kernel/batch_run_state/v2" ? LANE_IN_FLIGHT : SERIAL_IN_FLIGHT;
|
|
14684
|
+
return record.children.find((child) => inFlight.has(child.state)) ?? null;
|
|
13229
14685
|
}
|
|
13230
|
-
function
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
14686
|
+
function readStaleBatchLookup(root, initiativeSlug) {
|
|
14687
|
+
const found = findExistingActiveBatch(root, initiativeSlug);
|
|
14688
|
+
if (found === null) {
|
|
14689
|
+
const settled = findSettledBatchRecord(root, initiativeSlug);
|
|
14690
|
+
if (settled)
|
|
14691
|
+
return { kind: "terminal", batch_id: settled.batch_id };
|
|
14692
|
+
return { kind: "none" };
|
|
14693
|
+
}
|
|
14694
|
+
if (found.corrupt)
|
|
14695
|
+
return { kind: "corrupt", path: found.path };
|
|
14696
|
+
const record = found.record;
|
|
14697
|
+
if (isTerminalBatchState(record.batch_state))
|
|
14698
|
+
return { kind: "terminal", batch_id: record.batch_id };
|
|
14699
|
+
const inFlight = inFlightChildOf(record);
|
|
14700
|
+
if (inFlight)
|
|
14701
|
+
return { kind: "in_flight", batch_id: record.batch_id, task_id: inFlight.task_id };
|
|
14702
|
+
return {
|
|
14703
|
+
kind: "retirable",
|
|
14704
|
+
record,
|
|
14705
|
+
facts: {
|
|
14706
|
+
batch_id: record.batch_id,
|
|
14707
|
+
initiative_slug: record.initiative_slug,
|
|
14708
|
+
batch_state: record.batch_state,
|
|
14709
|
+
plan_digest: record.plan_digest,
|
|
14710
|
+
branch: record.branch ?? `imm/${record.initiative_slug}`,
|
|
14711
|
+
base_head: record.base_head,
|
|
14712
|
+
confirmation_time: record.confirmation_time,
|
|
14713
|
+
recorded_commits: [...record.commits],
|
|
14714
|
+
children: childEvidenceOf(record)
|
|
14715
|
+
}
|
|
14716
|
+
};
|
|
14717
|
+
}
|
|
14718
|
+
async function retireStaleBatch(options) {
|
|
14719
|
+
const { root, initiative_slug: initiativeSlug, now } = options;
|
|
14720
|
+
const lookup = readStaleBatchLookup(root, initiativeSlug);
|
|
14721
|
+
if (lookup.kind === "none")
|
|
14722
|
+
return { outcome: "rejected", rejection: batchReason("stale_batch_absent", `Initiative ${initiativeSlug} has no active batch`) };
|
|
14723
|
+
if (lookup.kind === "corrupt")
|
|
14724
|
+
return { outcome: "rejected", rejection: batchReason("batch_state_unreadable", lookup.path) };
|
|
14725
|
+
if (lookup.kind === "terminal")
|
|
14726
|
+
return {
|
|
14727
|
+
outcome: "rejected",
|
|
14728
|
+
rejection: batchReason("stale_batch_absent", `batch ${lookup.batch_id} already reached a terminal state`)
|
|
14729
|
+
};
|
|
14730
|
+
if (lookup.kind === "in_flight")
|
|
14731
|
+
return {
|
|
14732
|
+
outcome: "rejected",
|
|
14733
|
+
rejection: batchReason("stale_batch_in_flight", `batch ${lookup.batch_id}, child ${lookup.task_id}`)
|
|
14734
|
+
};
|
|
14735
|
+
const { record, facts } = lookup;
|
|
14736
|
+
const expected = readSecureProjectBytes(root, batchStatePath(record.batch_id));
|
|
14737
|
+
const decision = await options.gate(facts);
|
|
14738
|
+
if (decision.kind === "host_rejection")
|
|
14739
|
+
return { outcome: "host_rejection", value: decision.value };
|
|
14740
|
+
if (!readSecureProjectBytes(root, batchStatePath(record.batch_id)).equals(expected))
|
|
14741
|
+
return { outcome: "rejected", rejection: batchReason("plan_changed", record.batch_id) };
|
|
14742
|
+
const retired = { ...record, batch_state: "superseded" };
|
|
14743
|
+
const stored = replaceAnyBatchRunState(root, expected, retired, () => {
|
|
14744
|
+
if (!readSecureProjectBytes(root, batchStatePath(record.batch_id)).equals(expected))
|
|
14745
|
+
throw new Error("batch state changed before the disposition was applied");
|
|
14746
|
+
});
|
|
14747
|
+
const report = stored.contract === "assurance_kernel/batch_run_state/v2" ? writeBatchRunReport(root, {
|
|
14748
|
+
contract: "assurance_kernel/batch_run_report/v1",
|
|
14749
|
+
batch_id: stored.batch_id,
|
|
14750
|
+
initiative_slug: stored.initiative_slug,
|
|
14751
|
+
batch_state: stored.batch_state,
|
|
14752
|
+
max_parallel: stored.max_parallel,
|
|
14753
|
+
children: stored.children,
|
|
14754
|
+
commits: stored.commits,
|
|
14755
|
+
reason: RETIRE_REASON,
|
|
14756
|
+
handoffs: [],
|
|
14757
|
+
next_action: RETIRE_NEXT_ACTION,
|
|
14758
|
+
created_at: now
|
|
14759
|
+
}) : writeBatchRunReport(root, {
|
|
14760
|
+
contract: "assurance_kernel/batch_run_report/v1",
|
|
14761
|
+
batch_id: stored.batch_id,
|
|
14762
|
+
initiative_slug: stored.initiative_slug,
|
|
14763
|
+
batch_state: stored.batch_state,
|
|
14764
|
+
children: stored.children,
|
|
14765
|
+
commits: stored.commits,
|
|
14766
|
+
reason: RETIRE_REASON,
|
|
14767
|
+
next_action: RETIRE_NEXT_ACTION,
|
|
14768
|
+
created_at: now
|
|
14769
|
+
});
|
|
14770
|
+
return { outcome: "retired", batch_id: stored.batch_id, record: stored, report };
|
|
13234
14771
|
}
|
|
14772
|
+
|
|
14773
|
+
// plugins/immune-brain/runtime/assurance/review_mediation.ts
|
|
14774
|
+
import { createHash as createHash21 } from "node:crypto";
|
|
13235
14775
|
function extractVerdictJson(input) {
|
|
13236
14776
|
if (typeof input === "string") {
|
|
13237
|
-
const
|
|
13238
|
-
|
|
14777
|
+
const first = input.indexOf("{");
|
|
14778
|
+
const last = input.lastIndexOf("}");
|
|
14779
|
+
const cleaned = first >= 0 && last >= first ? input.slice(first, last + 1) : "";
|
|
13239
14780
|
if (!cleaned)
|
|
13240
14781
|
return null;
|
|
13241
14782
|
try {
|
|
@@ -13259,36 +14800,72 @@ function verdictFingerprint(raw) {
|
|
|
13259
14800
|
findings: raw.findings ?? null
|
|
13260
14801
|
});
|
|
13261
14802
|
}
|
|
14803
|
+
function digestOfReviewerBytes(bytes) {
|
|
14804
|
+
return `sha256:${createHash21("sha256").update(bytes).digest("hex")}`;
|
|
14805
|
+
}
|
|
13262
14806
|
var RELEASED_REVIEW_RECOVERY = "Call advance_assurance to obtain a new Review reservation, then dispatch one fresh reviewer with the returned envelope unchanged";
|
|
13263
14807
|
var RETAINED_REVIEW_RECOVERY = "Wait for the dispatched reviewer to finish, then call submit_review again with its verdict; do not dispatch or continue another reviewer";
|
|
13264
|
-
var MISMATCH_REVIEW_RECOVERY = "Resubmit the reviewer's verdict exactly as the reviewer returned it";
|
|
14808
|
+
var MISMATCH_REVIEW_RECOVERY = "Resubmit without a verdict to apply the observed reviewer receipt, or resubmit the reviewer's verdict exactly as the reviewer returned it";
|
|
13265
14809
|
function withReviewRecovery(result, recovery_action) {
|
|
13266
14810
|
if (result.state !== "blocked" || result.code === "verdict_invalid")
|
|
13267
14811
|
return result;
|
|
13268
14812
|
return { ...result, recovery_action };
|
|
13269
14813
|
}
|
|
13270
|
-
async function
|
|
13271
|
-
if (
|
|
13272
|
-
|
|
13273
|
-
|
|
14814
|
+
async function submitMediatedReview(coordinator, ctx, taskId, verdictInput, inspect) {
|
|
14815
|
+
if (coordinator.active(taskId)?.state === "settlement_unknown") {
|
|
14816
|
+
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
14817
|
+
}
|
|
14818
|
+
const observed = inspect();
|
|
13274
14819
|
if (!observed.ok) {
|
|
13275
14820
|
if (observed.release)
|
|
13276
14821
|
return withReviewRecovery(coordinator.abandonReview(taskId, observed.reason), RELEASED_REVIEW_RECOVERY);
|
|
13277
14822
|
return { state: "blocked", reason: observed.reason, recovery_action: RETAINED_REVIEW_RECOVERY };
|
|
13278
14823
|
}
|
|
14824
|
+
const receiptBytes = observed.receipt.result;
|
|
14825
|
+
const options = {
|
|
14826
|
+
receiptRecoveryAction: RETAINED_REVIEW_RECOVERY,
|
|
14827
|
+
reviewer_verdict_sha256: digestOfReviewerBytes(receiptBytes),
|
|
14828
|
+
validateReceipt: () => {
|
|
14829
|
+
const current = inspect();
|
|
14830
|
+
if (!current.ok)
|
|
14831
|
+
return current.reason;
|
|
14832
|
+
return current.receipt.actorId === observed.receipt.actorId && current.receipt.result === receiptBytes ? null : "reviewer receipt changed during submission";
|
|
14833
|
+
}
|
|
14834
|
+
};
|
|
14835
|
+
if (verdictInput === undefined) {
|
|
14836
|
+
if (!coordinator.isReviewVerdictValid(taskId, receiptBytes)) {
|
|
14837
|
+
return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
|
|
14838
|
+
}
|
|
14839
|
+
return coordinator.submitReview(taskId, ctx, receiptBytes, options);
|
|
14840
|
+
}
|
|
13279
14841
|
const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
|
|
13280
14842
|
if (!parentValid)
|
|
13281
14843
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
13282
|
-
|
|
13283
|
-
if (!receiptValid) {
|
|
14844
|
+
if (!coordinator.isReviewVerdictValid(taskId, receiptBytes)) {
|
|
13284
14845
|
return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
|
|
13285
14846
|
}
|
|
13286
14847
|
const parentJson = extractVerdictJson(verdictInput);
|
|
13287
|
-
const receiptJson = extractVerdictJson(
|
|
13288
|
-
if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
14848
|
+
const receiptJson = extractVerdictJson(receiptBytes);
|
|
14849
|
+
if (!parentJson || !receiptJson || verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
13289
14850
|
return { state: "blocked", reason: "parent verdict does not match reviewer receipt", recovery_action: MISMATCH_REVIEW_RECOVERY };
|
|
13290
14851
|
}
|
|
13291
|
-
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
14852
|
+
return coordinator.submitReview(taskId, ctx, verdictInput, options);
|
|
14853
|
+
}
|
|
14854
|
+
|
|
14855
|
+
// plugins/immune-brain/runtime/claude/kernel_ports.ts
|
|
14856
|
+
function diffSnapshotOf(root, record) {
|
|
14857
|
+
return taskDeliveryIdentity(root, record);
|
|
14858
|
+
}
|
|
14859
|
+
function diffHashOf(root, record) {
|
|
14860
|
+
return diffSnapshotOf(root, record).diff_hash;
|
|
14861
|
+
}
|
|
14862
|
+
function readTaskIntentForRecord(root, taskId) {
|
|
14863
|
+
recoverKernelStoreFollowUps(root, taskId);
|
|
14864
|
+
const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref?.path;
|
|
14865
|
+
return readTaskIntent(root, taskId, currentPath);
|
|
14866
|
+
}
|
|
14867
|
+
async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput) {
|
|
14868
|
+
return submitMediatedReview(coordinator, ctx, taskId, verdictInput, () => host.inspectReviewForTask(taskId));
|
|
13292
14869
|
}
|
|
13293
14870
|
function settledKernelResult(result) {
|
|
13294
14871
|
const state = result.state;
|
|
@@ -13496,7 +15073,7 @@ class ClaudeRuntime {
|
|
|
13496
15073
|
if (!slug)
|
|
13497
15074
|
return result;
|
|
13498
15075
|
try {
|
|
13499
|
-
if (
|
|
15076
|
+
if (existsSync12(join17(this.cwd, ".imm", "audit", taskId)))
|
|
13500
15077
|
execFileSync8("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
|
|
13501
15078
|
const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
|
|
13502
15079
|
return { ...result, batch };
|
|
@@ -13593,7 +15170,7 @@ class ClaudeRuntime {
|
|
|
13593
15170
|
throw new Error("approve_breaking_intent_revision requires next_intent");
|
|
13594
15171
|
const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
|
|
13595
15172
|
const nextIntentRef = nextIntent ? { path: `docs/plans/${nextIntent.task_id}.intent.json`, content_hash: nextIntentHash } : undefined;
|
|
13596
|
-
const sidecar = nextIntent ?
|
|
15173
|
+
const sidecar = nextIntent ? join17(this.cwd, priorIntent.intent_ref.path) : undefined;
|
|
13597
15174
|
const stagedSnapshot = sidecar ? captureStagedIntent(this.cwd, priorIntent.intent_ref.path) : undefined;
|
|
13598
15175
|
const restoreStagedIntent2 = () => {
|
|
13599
15176
|
if (!stagedSnapshot)
|
|
@@ -13702,8 +15279,8 @@ class ClaudeRuntime {
|
|
|
13702
15279
|
const { app } = await this.authority();
|
|
13703
15280
|
const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
|
|
13704
15281
|
const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
|
|
13705
|
-
const sidecar =
|
|
13706
|
-
const priorBytes = operation.op === "revise_intent" ?
|
|
15282
|
+
const sidecar = join17(ctx.cwd, priorIntent.intent_ref.path);
|
|
15283
|
+
const priorBytes = operation.op === "revise_intent" ? readFileSync15(sidecar) : null;
|
|
13707
15284
|
const priorStaged = priorBytes !== null ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : null;
|
|
13708
15285
|
try {
|
|
13709
15286
|
if (priorBytes) {
|
|
@@ -13750,7 +15327,7 @@ class ClaudeRuntime {
|
|
|
13750
15327
|
root: this.cwd,
|
|
13751
15328
|
initiative_slug: initiativeSlug,
|
|
13752
15329
|
now,
|
|
13753
|
-
readInitiative: this.readInitiative ??
|
|
15330
|
+
readInitiative: this.readInitiative ?? observeInitiative
|
|
13754
15331
|
});
|
|
13755
15332
|
if (!preflight.ok) {
|
|
13756
15333
|
return {
|
|
@@ -13770,7 +15347,7 @@ class ClaudeRuntime {
|
|
|
13770
15347
|
initiative_slug: initiativeSlug,
|
|
13771
15348
|
now,
|
|
13772
15349
|
projection: preflight.projection,
|
|
13773
|
-
readInitiative: this.readInitiative ??
|
|
15350
|
+
readInitiative: this.readInitiative ?? observeInitiative,
|
|
13774
15351
|
nonce: enrollmentNonce(),
|
|
13775
15352
|
gate: async (facts) => {
|
|
13776
15353
|
if (reuseOnly)
|
|
@@ -13803,6 +15380,13 @@ class ClaudeRuntime {
|
|
|
13803
15380
|
reason: child.reason
|
|
13804
15381
|
})),
|
|
13805
15382
|
budget: facts.budget,
|
|
15383
|
+
...options.max_parallel !== undefined ? {
|
|
15384
|
+
lane_mode: {
|
|
15385
|
+
max_parallel: options.max_parallel,
|
|
15386
|
+
parallel_groups: facts.parallel_groups,
|
|
15387
|
+
serialized: facts.scope_conflicts
|
|
15388
|
+
}
|
|
15389
|
+
} : {},
|
|
13806
15390
|
...facts.reuse_blockers.length > 0 ? {
|
|
13807
15391
|
re_confirmation_required: facts.reuse_blockers,
|
|
13808
15392
|
recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
|
|
@@ -13902,7 +15486,9 @@ class ClaudeRuntime {
|
|
|
13902
15486
|
budget,
|
|
13903
15487
|
now,
|
|
13904
15488
|
kernel: kernelPort,
|
|
13905
|
-
git: this.batchGit
|
|
15489
|
+
git: this.batchGit,
|
|
15490
|
+
...options.max_parallel !== undefined ? { max_parallel: options.max_parallel } : {},
|
|
15491
|
+
...options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}
|
|
13906
15492
|
});
|
|
13907
15493
|
if (report.batch_state === "rejected")
|
|
13908
15494
|
return batchReason("batch_run_rejected", report.reason ?? "");
|
|
@@ -13912,6 +15498,81 @@ class ClaudeRuntime {
|
|
|
13912
15498
|
report
|
|
13913
15499
|
};
|
|
13914
15500
|
}
|
|
15501
|
+
async retireStaleBatch(initiativeSlug, meta) {
|
|
15502
|
+
throwIfCancelled(meta.signal);
|
|
15503
|
+
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
15504
|
+
if (!probe.ok)
|
|
15505
|
+
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
15506
|
+
if (!this.requestConfirmation)
|
|
15507
|
+
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
15508
|
+
const outcome = await retireStaleBatch({
|
|
15509
|
+
root: this.cwd,
|
|
15510
|
+
initiative_slug: initiativeSlug,
|
|
15511
|
+
now: new Date().toISOString(),
|
|
15512
|
+
gate: async (facts) => {
|
|
15513
|
+
let confirmationResult;
|
|
15514
|
+
try {
|
|
15515
|
+
confirmationResult = await this.requestConfirmation({
|
|
15516
|
+
operation: "retire_stale_batch",
|
|
15517
|
+
initiativeSlug,
|
|
15518
|
+
toolCallId: meta.toolCallId,
|
|
15519
|
+
batchDisposition: {
|
|
15520
|
+
batch_id: facts.batch_id,
|
|
15521
|
+
batch_state: facts.batch_state,
|
|
15522
|
+
batch_branch: facts.branch,
|
|
15523
|
+
plan_digest: facts.plan_digest,
|
|
15524
|
+
recorded_commits: facts.recorded_commits.length,
|
|
15525
|
+
children: facts.children.map((child) => ({
|
|
15526
|
+
task_id: child.task_id,
|
|
15527
|
+
slice_id: child.slice_id,
|
|
15528
|
+
state: child.state,
|
|
15529
|
+
commit: child.commit,
|
|
15530
|
+
lane_branch: child.lane_branch
|
|
15531
|
+
}))
|
|
15532
|
+
},
|
|
15533
|
+
signal: meta.signal
|
|
15534
|
+
});
|
|
15535
|
+
} catch (err) {
|
|
15536
|
+
if (meta.signal?.aborted)
|
|
15537
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
15538
|
+
if (err instanceof NativeAuthorityError) {
|
|
15539
|
+
if (err.reasonCode === "unsupported_host")
|
|
15540
|
+
throw err;
|
|
15541
|
+
if (err.reasonCode === "user_cancelled") {
|
|
15542
|
+
return {
|
|
15543
|
+
kind: "host_rejection",
|
|
15544
|
+
value: { state: "cancelled", reason: err.message, recovery_action: err.recoveryAction }
|
|
15545
|
+
};
|
|
15546
|
+
}
|
|
15547
|
+
return {
|
|
15548
|
+
kind: "host_rejection",
|
|
15549
|
+
value: { state: "rejected", reason: err.message, recovery_action: err.recoveryAction }
|
|
15550
|
+
};
|
|
15551
|
+
}
|
|
15552
|
+
return {
|
|
15553
|
+
kind: "host_rejection",
|
|
15554
|
+
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err))
|
|
15555
|
+
};
|
|
15556
|
+
}
|
|
15557
|
+
if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
|
|
15558
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
15559
|
+
if (meta.signal?.aborted)
|
|
15560
|
+
return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
|
|
15561
|
+
if (confirmationResult.decision === "decline")
|
|
15562
|
+
return { kind: "host_rejection", value: batchReason("confirmation_declined") };
|
|
15563
|
+
if (confirmationResult.decision === "cancel")
|
|
15564
|
+
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
15565
|
+
if (confirmationResult.decision !== "accept")
|
|
15566
|
+
return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
|
|
15567
|
+
return { kind: "confirmed", request_id: confirmationResult.requestId };
|
|
15568
|
+
}
|
|
15569
|
+
});
|
|
15570
|
+
if (outcome.outcome === "host_rejection")
|
|
15571
|
+
return outcome.value;
|
|
15572
|
+
if (outcome.outcome === "rejected")
|
|
15573
|
+
return outcome.rejection;
|
|
15574
|
+
return { state: "retired", batch_id: outcome.batch_id, report: outcome.report };
|
|
15575
|
+
}
|
|
13915
15576
|
}
|
|
13916
15577
|
|
|
13917
15578
|
// plugins/immune-brain/runtime/claude/mcp_server.ts
|
|
@@ -13920,12 +15581,13 @@ var TOOLS = [
|
|
|
13920
15581
|
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
13921
15582
|
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
13922
15583
|
{ name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
|
|
13923
|
-
{ name: "submit_review", description: "Submit the
|
|
15584
|
+
{ name: "submit_review", description: "Submit the Review verdict; omit the verdict to apply the correlated reviewer receipt, or relay the reviewer's structured verdict for fingerprint comparison.", privileged: false },
|
|
13924
15585
|
{ name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
|
|
13925
15586
|
{ name: "revise_intent", description: "Apply a compatible TaskIntent revision.", privileged: false },
|
|
13926
15587
|
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
13927
15588
|
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
13928
15589
|
{ name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
|
|
15590
|
+
{ name: "retire_stale_batch", description: "Retire an Initiative's parked or drifted batch record to the terminal superseded state after native confirmation.", privileged: true },
|
|
13929
15591
|
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
|
|
13930
15592
|
{ name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
|
|
13931
15593
|
{ name: "refute_finding", description: "Refute one open finding by binding the fresh passing QA attestation that contradicts it.", privileged: false }
|
|
@@ -13937,7 +15599,21 @@ function listMcpTools() {
|
|
|
13937
15599
|
inputSchema: {
|
|
13938
15600
|
type: "object",
|
|
13939
15601
|
properties: {
|
|
13940
|
-
...tool.name === "start_unattended_batch"
|
|
15602
|
+
...tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch" ? {
|
|
15603
|
+
initiative_slug: { type: "string" },
|
|
15604
|
+
...tool.name === "start_unattended_batch" ? {
|
|
15605
|
+
max_parallel: { type: "integer", minimum: 1 },
|
|
15606
|
+
lane_offers: {
|
|
15607
|
+
type: "array",
|
|
15608
|
+
items: {
|
|
15609
|
+
type: "object",
|
|
15610
|
+
properties: { task_id: { type: "string" }, path: { type: "string" } },
|
|
15611
|
+
required: ["task_id", "path"],
|
|
15612
|
+
additionalProperties: false
|
|
15613
|
+
}
|
|
15614
|
+
}
|
|
15615
|
+
} : {}
|
|
15616
|
+
} : {
|
|
13941
15617
|
task_id: { type: "string" },
|
|
13942
15618
|
...tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {},
|
|
13943
15619
|
...tool.name === "stop" ? { reason: { type: "string" } } : {},
|
|
@@ -13946,7 +15622,7 @@ function listMcpTools() {
|
|
|
13946
15622
|
...tool.name === "refute_finding" ? { finding_id: { type: "string" }, attestation_id: { type: "string" } } : {}
|
|
13947
15623
|
}
|
|
13948
15624
|
},
|
|
13949
|
-
required: tool.name === "start_unattended_batch"
|
|
15625
|
+
required: tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch" ? ["initiative_slug"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : tool.name === "refute_finding" ? ["task_id", "finding_id", "attestation_id"] : tool.name === "revise_intent" ? ["task_id", "next_intent"] : ["task_id"]
|
|
13950
15626
|
},
|
|
13951
15627
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
|
|
13952
15628
|
}));
|
|
@@ -14008,7 +15684,36 @@ function createMcpRuntime(options = {}) {
|
|
|
14008
15684
|
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
14009
15685
|
signal: meta.signal
|
|
14010
15686
|
};
|
|
14011
|
-
|
|
15687
|
+
const laneOffers = parseLaneOffers(args.lane_offers);
|
|
15688
|
+
const maxParallel = resolveLaneParallel(options.cwd ?? process.cwd(), initiativeSlug, parseMaxParallel(args.max_parallel), laneOffers);
|
|
15689
|
+
return runtime.startUnattendedBatch(initiativeSlug, toolMeta, {
|
|
15690
|
+
...maxParallel !== undefined ? { max_parallel: maxParallel } : {},
|
|
15691
|
+
...laneOffers !== undefined ? { lane_offers: laneOffers } : {}
|
|
15692
|
+
});
|
|
15693
|
+
}
|
|
15694
|
+
if (name === "retire_stale_batch") {
|
|
15695
|
+
const initiativeSlug = String(args.initiative_slug ?? "");
|
|
15696
|
+
if (!initiativeSlug)
|
|
15697
|
+
throw new Error("initiative_slug is required");
|
|
15698
|
+
if ("native_decision" in args)
|
|
15699
|
+
throw new Error("native_decision cannot be supplied in tool arguments");
|
|
15700
|
+
if (!negotiatedVersion)
|
|
15701
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
|
|
15702
|
+
if (!negotiatedInteractive) {
|
|
15703
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
15704
|
+
}
|
|
15705
|
+
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
15706
|
+
if (!probe.ok)
|
|
15707
|
+
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
15708
|
+
const toolMeta = {
|
|
15709
|
+
sessionId: meta.sessionId ?? connectionId,
|
|
15710
|
+
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
15711
|
+
taskId: initiativeSlug,
|
|
15712
|
+
initiativeSlug,
|
|
15713
|
+
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
15714
|
+
signal: meta.signal
|
|
15715
|
+
};
|
|
15716
|
+
return runtime.retireStaleBatch(initiativeSlug, toolMeta);
|
|
14012
15717
|
}
|
|
14013
15718
|
const taskId = String(args.task_id ?? "");
|
|
14014
15719
|
if (!taskId)
|
|
@@ -14037,9 +15742,7 @@ function createMcpRuntime(options = {}) {
|
|
|
14037
15742
|
if (name === "advance_assurance")
|
|
14038
15743
|
return runtime.advance(taskId, toolMeta.signal, toolMeta);
|
|
14039
15744
|
if (name === "submit_review") {
|
|
14040
|
-
|
|
14041
|
-
throw new Error("verdict is required");
|
|
14042
|
-
return runtime.submitReview(taskId, args.verdict, toolMeta);
|
|
15745
|
+
return runtime.submitReview(taskId, Object.hasOwn(args, "verdict") ? args.verdict : undefined, toolMeta);
|
|
14043
15746
|
}
|
|
14044
15747
|
if (name === "revise_intent") {
|
|
14045
15748
|
if (!Object.hasOwn(args, "next_intent"))
|
|
@@ -14222,6 +15925,32 @@ async function writeReply(output, reply) {
|
|
|
14222
15925
|
});
|
|
14223
15926
|
}
|
|
14224
15927
|
function elicitationParams(input) {
|
|
15928
|
+
if (input.operation === "retire_stale_batch") {
|
|
15929
|
+
const d = input.batchDisposition;
|
|
15930
|
+
const details = [
|
|
15931
|
+
`Operation: retire_stale_batch`,
|
|
15932
|
+
`Initiative: ${input.initiativeSlug}`,
|
|
15933
|
+
d ? `Batch: ${d.batch_id}` : null,
|
|
15934
|
+
d ? `Batch state: ${d.batch_state}` : null,
|
|
15935
|
+
d ? `Batch branch: ${d.batch_branch}` : null,
|
|
15936
|
+
d ? `Plan digest: ${d.plan_digest}` : null,
|
|
15937
|
+
d ? `Recorded child commits: ${d.recorded_commits}` : null,
|
|
15938
|
+
d ? `Children (${d.children.length}):
|
|
15939
|
+
${d.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`).join(`
|
|
15940
|
+
`)}` : null,
|
|
15941
|
+
"Retiring writes this record to the terminal superseded state. Child states, recorded commits and the batch branch are preserved as evidence; nothing is deleted and no batch trailer or approval is manufactured.",
|
|
15942
|
+
"A child without a commit was never executed by this batch: it carries no delivery evidence, and the retired authorization is never reused for a revised plan.",
|
|
15943
|
+
"After retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative."
|
|
15944
|
+
].filter(Boolean);
|
|
15945
|
+
return {
|
|
15946
|
+
mode: "form",
|
|
15947
|
+
message: `Retire this stale batch record for ${input.initiativeSlug}?
|
|
15948
|
+
|
|
15949
|
+
${details.join(`
|
|
15950
|
+
`)}`,
|
|
15951
|
+
requestedSchema: { type: "object", properties: {} }
|
|
15952
|
+
};
|
|
15953
|
+
}
|
|
14225
15954
|
if (input.operation === "start_unattended_batch") {
|
|
14226
15955
|
const b = input.batchDetails;
|
|
14227
15956
|
const details = [
|
|
@@ -14235,7 +15964,14 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
|
|
|
14235
15964
|
b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
|
|
14236
15965
|
${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
|
|
14237
15966
|
`)}` : null,
|
|
14238
|
-
b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null
|
|
15967
|
+
b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
|
|
15968
|
+
b?.lane_mode ? `Lane mode: max_parallel=${b.lane_mode.max_parallel}` : null,
|
|
15969
|
+
b?.lane_mode ? `Parallel groups (${b.lane_mode.parallel_groups.length}):
|
|
15970
|
+
${b.lane_mode.parallel_groups.map((group) => ` - ${group.join(", ")}`).join(`
|
|
15971
|
+
`)}` : null,
|
|
15972
|
+
b?.lane_mode && b.lane_mode.serialized.length > 0 ? `Serialized by overlapping scope (${b.lane_mode.serialized.length}):
|
|
15973
|
+
${b.lane_mode.serialized.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join(`
|
|
15974
|
+
`)}` : null
|
|
14239
15975
|
].filter(Boolean);
|
|
14240
15976
|
return {
|
|
14241
15977
|
mode: "form",
|