immune-brain 5.2.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +17 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +63 -19
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +22 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +17 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -28,6 +28,7 @@ import { Type } from "typebox";
|
|
|
28
28
|
import { PLUGIN_VERSION } from "../runtime/plugin_version";
|
|
29
29
|
import { findResumableBatchSlugForTask } from "../runtime/unattended/batch_preflight";
|
|
30
30
|
import { executePiUnattendedBatch } from "./imm-unattended-batch";
|
|
31
|
+
import { registerPiRunContinuation } from "./pi-run-continuation";
|
|
31
32
|
import {
|
|
32
33
|
} from "./pi-canary-verification";
|
|
33
34
|
import {
|
|
@@ -102,10 +103,13 @@ import {
|
|
|
102
103
|
import { inspectStorageLayout } from "../runtime/kernel/storage_paths";
|
|
103
104
|
import { migrateLegacyLayout } from "../runtime/kernel/storage_layout_migration";
|
|
104
105
|
import {
|
|
105
|
-
|
|
106
|
-
parseTaskIntentV1,
|
|
106
|
+
assertScopeAdditionsAreFiles,
|
|
107
107
|
canonicalIntentHash,
|
|
108
|
+
parseProjectTaskIntent,
|
|
109
|
+
parseTaskIntentV1,
|
|
110
|
+
readTaskIntent,
|
|
108
111
|
type ReadTaskIntentResult,
|
|
112
|
+
withNextScope,
|
|
109
113
|
} from "../runtime/kernel/intent";
|
|
110
114
|
import { readBackendClaim, readTaskTombstone } from "../runtime/kernel/backend_claim";
|
|
111
115
|
import {
|
|
@@ -121,8 +125,11 @@ import {
|
|
|
121
125
|
} from "../runtime/kernel/canary_application";
|
|
122
126
|
import { findingsDigestV2 } from "../runtime/kernel/reducer";
|
|
123
127
|
import {
|
|
128
|
+
parseReworkChoice,
|
|
124
129
|
projectAssurance,
|
|
130
|
+
reworkExitLines,
|
|
125
131
|
type AssuranceProjectionResult,
|
|
132
|
+
type ReworkChoice,
|
|
126
133
|
} from "../runtime/kernel/assurance_projection";
|
|
127
134
|
import type { TaskRecord } from "../runtime/kernel/types";
|
|
128
135
|
import type { RoleDelegationContext } from "../runtime/role_prompt_bridge";
|
|
@@ -131,7 +138,7 @@ import {
|
|
|
131
138
|
runGithubTrackerOperation,
|
|
132
139
|
type GithubTrackerResult,
|
|
133
140
|
} from "../runtime/github_issue_tracker";
|
|
134
|
-
import { reviewReworkFindings } from "../runtime/assurance/coordinator";
|
|
141
|
+
import { projectTaskStatus, reviewReworkFindings, type StatusProjectionResult } from "../runtime/assurance/coordinator";
|
|
135
142
|
import { laneWriteRefusal } from "../runtime/unattended/lane_workspace";
|
|
136
143
|
import { activeTaskNotice } from "../runtime/assurance/active_task_notice";
|
|
137
144
|
import {
|
|
@@ -355,7 +362,7 @@ export function piRunEntryRefusal(
|
|
|
355
362
|
): Extract<PhaseAdmission, { ok: false }> | null {
|
|
356
363
|
const sessionId = piSessionId(ctx);
|
|
357
364
|
const state = sessionId ? piPhaseEntries().get(sessionId) ?? NO_PHASE_ENTRY : null;
|
|
358
|
-
const admission = admitPhaseOperation({ kind: "tool", name: operation }, state
|
|
365
|
+
const admission = admitPhaseOperation({ kind: "tool", name: operation }, state);
|
|
359
366
|
return admission.ok ? null : admission;
|
|
360
367
|
}
|
|
361
368
|
|
|
@@ -421,6 +428,10 @@ export default function (
|
|
|
421
428
|
return { action: "continue" } as const;
|
|
422
429
|
});
|
|
423
430
|
|
|
431
|
+
// Validate dispatch before the receipt observer records an Agent call.
|
|
432
|
+
const continuation = registerPiRunContinuation(pi, {
|
|
433
|
+
entry: (text, root) => recognizePhaseEntry(text, piAliasReader(root)),
|
|
434
|
+
});
|
|
424
435
|
pi.registerCommand("imm-tasks", {
|
|
425
436
|
handler: async (_args: string, ctx?: ExtensionContext) => {
|
|
426
437
|
if (!ctx || ctx.mode !== "tui") return;
|
|
@@ -658,12 +669,37 @@ export default function (
|
|
|
658
669
|
ctx,
|
|
659
670
|
taskId,
|
|
660
671
|
await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>),
|
|
672
|
+
(slug) => continuation.selectedParent(ctx.cwd, ctx.sessionManager.getSessionId(), slug),
|
|
661
673
|
);
|
|
662
674
|
presentTaskRailResult(ctx, taskId, enriched);
|
|
663
675
|
throwIfCanaryToolFailure(taskId, action.op, enriched);
|
|
664
676
|
return toolResult(JSON.stringify(enriched, null, 2), enriched);
|
|
665
677
|
}
|
|
666
|
-
const
|
|
678
|
+
const requested = requestedScope(action as { op: string; next_intent?: unknown });
|
|
679
|
+
if (requested) {
|
|
680
|
+
// The directory rule is the compatible route's own refusal; it runs
|
|
681
|
+
// before the projection so a directory's files are not reported as
|
|
682
|
+
// unstaged scope changes instead (ADR 0021, D4).
|
|
683
|
+
let prior: ReadTaskIntentResult | null = null;
|
|
684
|
+
try {
|
|
685
|
+
prior = readTaskIntentForRecord(ctx.cwd, taskId);
|
|
686
|
+
} catch {
|
|
687
|
+
// An unreadable intent is the projection's diagnostic to report.
|
|
688
|
+
}
|
|
689
|
+
if (prior) {
|
|
690
|
+
try {
|
|
691
|
+
assertScopeAdditionsAreFiles(ctx.cwd, prior.intent.scope_hint, requested);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
694
|
+
return failCanaryTool(taskId, action.op, "blocked", "scope_addition_is_directory", message, "name concrete files, or widen through approve_breaking_intent_revision");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Status is a read: unstaged in-scope paths come back as a warning. Every
|
|
699
|
+
// other operation projects through the refusing delivery identity.
|
|
700
|
+
const projection: StatusProjectionResult = action.op === "status"
|
|
701
|
+
? await projectTaskStatus(ctx.cwd, taskId)
|
|
702
|
+
: await projectAssuranceForTask(ctx.cwd, taskId, requested);
|
|
667
703
|
if (projection.error) {
|
|
668
704
|
const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
|
|
669
705
|
const details = {
|
|
@@ -682,7 +718,8 @@ export default function (
|
|
|
682
718
|
const blockers = state.blocking_finding_ids.length
|
|
683
719
|
+ state.unresolved_user_decision_ids.length
|
|
684
720
|
+ state.replan_required_ids.length;
|
|
685
|
-
const
|
|
721
|
+
const warnings = projection.warnings ?? [];
|
|
722
|
+
const status = { plugin_version: PLUGIN_VERSION, ...state, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
686
723
|
const details = {
|
|
687
724
|
state: "status",
|
|
688
725
|
operation: "status",
|
|
@@ -690,7 +727,8 @@ export default function (
|
|
|
690
727
|
lifecycle: state.lifecycle,
|
|
691
728
|
artifact_state: state.artifact_state,
|
|
692
729
|
task_state: state,
|
|
693
|
-
|
|
730
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
731
|
+
result: `${fresh}/${total} acceptance items fresh; ${blockers} blocker${blockers === 1 ? "" : "s"}${warnings.map((warning) => `; ${warning.message}`).join("")}`,
|
|
694
732
|
next_action: state.next_obligation,
|
|
695
733
|
};
|
|
696
734
|
presentTaskRailResult(ctx, taskId, details);
|
|
@@ -812,6 +850,7 @@ export default function (
|
|
|
812
850
|
type AuthorizationOutcome =
|
|
813
851
|
| { state: "applied"; operation: AuthorizeOperation; lifecycle?: string; delivery_error?: string }
|
|
814
852
|
| { state: "cancelled"; operation: AuthorizeOperation; reason: string }
|
|
853
|
+
| { state: "replan_selected"; operation: AuthorizeOperation; reason: string }
|
|
815
854
|
| { state: "blocked"; reason: string };
|
|
816
855
|
|
|
817
856
|
async function authorizeExactOperation(
|
|
@@ -826,7 +865,7 @@ export default function (
|
|
|
826
865
|
let nextIntentRef: { path: string; content_hash: string } | undefined;
|
|
827
866
|
if (operation === "approve-breaking-intent-revision") {
|
|
828
867
|
try {
|
|
829
|
-
nextIntent =
|
|
868
|
+
nextIntent = parseProjectTaskIntent(ctx.cwd, nextIntentInput);
|
|
830
869
|
if (nextIntent.task_id !== taskId)
|
|
831
870
|
throw new Error("next intent task_id must match the enrolled task");
|
|
832
871
|
nextIntentHash = await canonicalIntentHash(nextIntent);
|
|
@@ -849,7 +888,7 @@ export default function (
|
|
|
849
888
|
return { state: "blocked", reason };
|
|
850
889
|
}
|
|
851
890
|
try {
|
|
852
|
-
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
891
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId, nextIntent?.scope_hint);
|
|
853
892
|
if (projection.error || !projection.claim) {
|
|
854
893
|
const reason = projection.error ?? "no active backend claim";
|
|
855
894
|
notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
|
|
@@ -880,7 +919,11 @@ export default function (
|
|
|
880
919
|
const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
881
920
|
if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
|
|
882
921
|
throw new Error("TaskRecord changed while preparing the breaking revision");
|
|
883
|
-
|
|
922
|
+
// The revision is bound against the scope it asks for (ADR 0021, D6).
|
|
923
|
+
stagedNextDiffHash = taskDeliveryIdentity(
|
|
924
|
+
ctx.cwd,
|
|
925
|
+
withNextScope(stagedRecord.record, nextIntent.scope_hint),
|
|
926
|
+
).diff_hash;
|
|
884
927
|
} catch (error) {
|
|
885
928
|
let restoreError: unknown;
|
|
886
929
|
try { restoreStagedIntent(); } catch (err) { restoreError = err; }
|
|
@@ -907,6 +950,9 @@ export default function (
|
|
|
907
950
|
return { state: "blocked", reason };
|
|
908
951
|
}
|
|
909
952
|
}
|
|
953
|
+
// Only a replan boundary offers exits; both Hosts render the same ones.
|
|
954
|
+
const reworkExits = operation === "authorize-rework" ? projection.projection.authorization.exits : undefined;
|
|
955
|
+
let reworkChoice: ReworkChoice | undefined;
|
|
910
956
|
const dialogSummary = [
|
|
911
957
|
`Task: ${taskId}`,
|
|
912
958
|
`Decision: ${operation}`,
|
|
@@ -932,7 +978,10 @@ export default function (
|
|
|
932
978
|
]
|
|
933
979
|
: []),
|
|
934
980
|
...(operation === "authorize-rework"
|
|
935
|
-
? [
|
|
981
|
+
? [
|
|
982
|
+
`Replan boundaries: ${projection.projection.replan_required_ids.join(", ")}`,
|
|
983
|
+
...(reworkExits ? reworkExitLines(reworkExits) : []),
|
|
984
|
+
]
|
|
936
985
|
: []),
|
|
937
986
|
...(nextIntent
|
|
938
987
|
? [
|
|
@@ -971,12 +1020,46 @@ export default function (
|
|
|
971
1020
|
summary: dialogSummary,
|
|
972
1021
|
details: dialogDetails,
|
|
973
1022
|
signal: ctx.signal,
|
|
974
|
-
actions:
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1023
|
+
actions: reworkExits
|
|
1024
|
+
? [
|
|
1025
|
+
{ value: "grant_rounds", label: uxText(UX_LANG, "Grant more Review rounds", "追加 Review 轮次"), description: uxText(UX_LANG, `Grant ${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max} further rounds`, `追加 ${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max} 轮`) },
|
|
1026
|
+
...(reworkExits.accept_residual_risk
|
|
1027
|
+
? [{ value: "accept_residual_risk", label: uxText(UX_LANG, "Accept residual risk", "接受残余风险"), description: uxText(UX_LANG, "Accept the open Review findings with a reason", "附理由接受未决 Review 发现") }]
|
|
1028
|
+
: []),
|
|
1029
|
+
{ value: "replan", label: uxText(UX_LANG, "Replan", "重新规划"), description: uxText(UX_LANG, "Revise the Intent instead", "改为修订 Intent") },
|
|
1030
|
+
{ value: "cancel", label: uxText(UX_LANG, "Cancel", "取消"), description: uxText(UX_LANG, "Leave managed authority unchanged", "保持托管权限状态不变") },
|
|
1031
|
+
]
|
|
1032
|
+
: [
|
|
1033
|
+
{ value: "authorize", label: uxText(UX_LANG, "Authorize", "批准"), description: uxText(UX_LANG, `Apply ${operation} after re-checking state`, `重新校验状态后应用 ${operation}`) },
|
|
1034
|
+
{ value: "cancel", label: uxText(UX_LANG, "Cancel", "取消"), description: uxText(UX_LANG, "Leave managed authority unchanged", "保持托管权限状态不变") },
|
|
1035
|
+
],
|
|
978
1036
|
});
|
|
979
|
-
|
|
1037
|
+
if (reworkExits) {
|
|
1038
|
+
if (selected === "grant_rounds" || selected === "accept_residual_risk" || selected === "replan") {
|
|
1039
|
+
const raw: Record<string, unknown> = { exit: selected };
|
|
1040
|
+
if (selected === "grant_rounds") {
|
|
1041
|
+
const text = await ctx.ui.input(
|
|
1042
|
+
uxText(UX_LANG, `Further Review rounds (${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max})`, `追加 Review 轮次(${reworkExits.grant_rounds.min}-${reworkExits.grant_rounds.max})`),
|
|
1043
|
+
String(reworkExits.grant_rounds.min),
|
|
1044
|
+
{ signal: ctx.signal },
|
|
1045
|
+
);
|
|
1046
|
+
if (text !== undefined && text.trim()) raw.rounds = Number(text.trim());
|
|
1047
|
+
if (text === undefined) raw.exit = "cancelled";
|
|
1048
|
+
} else if (selected === "accept_residual_risk") {
|
|
1049
|
+
const text = await ctx.ui.input(uxText(UX_LANG, "Reason for accepting residual risk", "接受残余风险的理由"), "", { signal: ctx.signal });
|
|
1050
|
+
if (text === undefined) raw.exit = "cancelled";
|
|
1051
|
+
else raw.reason = text;
|
|
1052
|
+
}
|
|
1053
|
+
// The same parser the Claude Host uses, so the accepted shapes cannot drift.
|
|
1054
|
+
reworkChoice = raw.exit === "cancelled" ? undefined : (parseReworkChoice(reworkExits, raw) ?? undefined);
|
|
1055
|
+
confirmed = reworkChoice !== undefined;
|
|
1056
|
+
if (raw.exit !== "cancelled" && !reworkChoice) {
|
|
1057
|
+
notifyOnce(ctx, `authorization-exit:${taskId}`, `authorize ${operation}: the chosen exit was not valid; nothing applied`, "warning");
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
} else {
|
|
1061
|
+
confirmed = selected === "authorize";
|
|
1062
|
+
}
|
|
980
1063
|
} catch (error) {
|
|
981
1064
|
if (nextIntent) {
|
|
982
1065
|
restoreStagedIntent();
|
|
@@ -1006,6 +1089,12 @@ export default function (
|
|
|
1006
1089
|
progression.closeInvocation(invocation);
|
|
1007
1090
|
return { state: "cancelled", operation, reason: "cancelled" };
|
|
1008
1091
|
}
|
|
1092
|
+
// A replan is not a mutation of this operation: the Intent revision has
|
|
1093
|
+
// its own gate, so nothing is written here.
|
|
1094
|
+
if (reworkChoice?.exit === "replan") {
|
|
1095
|
+
progression.closeInvocation(invocation);
|
|
1096
|
+
return { state: "replan_selected", operation, reason: "revise the Intent, then authorize approve-breaking-intent-revision" };
|
|
1097
|
+
}
|
|
1009
1098
|
if (!progression.sessionActiveValue() || progression.sessionGenerationValue() !== authorizationGeneration || progression.invocationState(invocation) !== "open") {
|
|
1010
1099
|
if (nextIntent) {
|
|
1011
1100
|
restoreStagedIntent();
|
|
@@ -1043,7 +1132,12 @@ export default function (
|
|
|
1043
1132
|
next_intent_ref: nextIntentRef!,
|
|
1044
1133
|
}
|
|
1045
1134
|
: operation === "authorize-rework"
|
|
1046
|
-
?
|
|
1135
|
+
? reworkChoice?.exit === "accept_residual_risk"
|
|
1136
|
+
? { op: "accept_residual_risk" as const, reason: reworkChoice.reason }
|
|
1137
|
+
: {
|
|
1138
|
+
op: "authorize_rework" as const,
|
|
1139
|
+
...(reworkChoice?.exit === "grant_rounds" ? { rounds: reworkChoice.rounds } : {}),
|
|
1140
|
+
}
|
|
1047
1141
|
: userDecisionOperation!;
|
|
1048
1142
|
// The exact host-built operation is shared by capability digest and
|
|
1049
1143
|
// application payload; command arguments cannot inject authority fields.
|
|
@@ -1054,9 +1148,11 @@ export default function (
|
|
|
1054
1148
|
const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
|
|
1055
1149
|
if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
|
|
1056
1150
|
throw new Error("TaskRecord changed before the breaking revision digest");
|
|
1057
|
-
const operationDiffHash = liveRecord?.record
|
|
1058
|
-
? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
|
|
1059
|
-
:
|
|
1151
|
+
const operationDiffHash = liveRecord?.record && nextIntent
|
|
1152
|
+
? taskDeliveryIdentity(ctx.cwd, withNextScope(liveRecord.record, nextIntent.scope_hint)).diff_hash
|
|
1153
|
+
: liveRecord?.record
|
|
1154
|
+
? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
|
|
1155
|
+
: projection.projection.diff_hash;
|
|
1060
1156
|
if (nextIntent && operationDiffHash !== stagedNextDiffHash)
|
|
1061
1157
|
throw new Error("staged next-state diff changed after native confirmation");
|
|
1062
1158
|
const capability = await mintCapability(registry, {
|
|
@@ -1075,7 +1171,8 @@ export default function (
|
|
|
1075
1171
|
...(exactOperation.op === "resolve_user_decision"
|
|
1076
1172
|
? { finding_id: exactOperation.finding_id, resolution: exactOperation.resolution }
|
|
1077
1173
|
: {}),
|
|
1078
|
-
...(exactOperation.op === "stop" ? { reason: exactOperation.reason } : {}),
|
|
1174
|
+
...(exactOperation.op === "stop" || exactOperation.op === "accept_residual_risk" ? { reason: exactOperation.reason } : {}),
|
|
1175
|
+
...(exactOperation.op === "authorize_rework" && "rounds" in exactOperation ? { rounds: exactOperation.rounds } : {}),
|
|
1079
1176
|
now,
|
|
1080
1177
|
});
|
|
1081
1178
|
const result = (await app.execute({
|
|
@@ -1083,13 +1180,15 @@ export default function (
|
|
|
1083
1180
|
task_id: taskId,
|
|
1084
1181
|
operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
|
|
1085
1182
|
prior_intent_token: priorIntent.token,
|
|
1086
|
-
diffProvider: (root: string, record: TaskRecord) =>
|
|
1183
|
+
diffProvider: (root: string, record: TaskRecord) =>
|
|
1184
|
+
diffSnapshotOf(root, nextIntent ? withNextScope(record, nextIntent.scope_hint) : record),
|
|
1087
1185
|
now,
|
|
1088
1186
|
})) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
|
|
1089
1187
|
if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
|
|
1090
1188
|
if (
|
|
1091
1189
|
exactOperation.op === "stop" ||
|
|
1092
1190
|
exactOperation.op === "authorize_rework" ||
|
|
1191
|
+
exactOperation.op === "accept_residual_risk" ||
|
|
1093
1192
|
exactOperation.op === "approve_breaking_intent_revision"
|
|
1094
1193
|
) stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
1095
1194
|
return { state: "applied", operation, lifecycle: result.record.lifecycle };
|
|
@@ -1241,8 +1340,8 @@ export function buildUserDecisionOperation(record: {
|
|
|
1241
1340
|
* as the Claude adapter's `readTaskIntentForRecord`.
|
|
1242
1341
|
*/
|
|
1243
1342
|
export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskIntentResult {
|
|
1244
|
-
const
|
|
1245
|
-
return readTaskIntent(root, taskId,
|
|
1343
|
+
const record = readTaskRecordRaw(root, taskId).record;
|
|
1344
|
+
return readTaskIntent(root, taskId, record?.intent_ref.path, record?.intent_snapshot);
|
|
1246
1345
|
}
|
|
1247
1346
|
|
|
1248
1347
|
/**
|
|
@@ -1343,8 +1442,34 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
|
|
|
1343
1442
|
return "Working";
|
|
1344
1443
|
}
|
|
1345
1444
|
|
|
1346
|
-
export async function projectAssuranceForTask(
|
|
1347
|
-
|
|
1445
|
+
export async function projectAssuranceForTask(
|
|
1446
|
+
root: string,
|
|
1447
|
+
taskId: string,
|
|
1448
|
+
nextScope?: readonly string[],
|
|
1449
|
+
): Promise<AssuranceProjectionResult> {
|
|
1450
|
+
// A scope revision is projected against the scope it asks for (ADR 0021, D6).
|
|
1451
|
+
return projectAssurance(
|
|
1452
|
+
root,
|
|
1453
|
+
taskId,
|
|
1454
|
+
nextScope
|
|
1455
|
+
? (rootDir: string, record: TaskRecord) => diffSnapshotOf(rootDir, withNextScope(record, nextScope))
|
|
1456
|
+
: undefined,
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* The canonical scope a `revise_intent` payload asks for. It goes through the
|
|
1462
|
+
* Intent parser so entries it would fold away (covered by a directory) match the
|
|
1463
|
+
* scope the Kernel later records; a payload the parser rejects is reported by
|
|
1464
|
+
* the revision path itself, not here.
|
|
1465
|
+
*/
|
|
1466
|
+
function requestedScope(action: { op: string; next_intent?: unknown }): string[] | undefined {
|
|
1467
|
+
if (action.op !== "revise_intent") return undefined;
|
|
1468
|
+
try {
|
|
1469
|
+
return parseTaskIntentV1(action.next_intent).scope_hint;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return undefined;
|
|
1472
|
+
}
|
|
1348
1473
|
}
|
|
1349
1474
|
|
|
1350
1475
|
/**
|
|
@@ -1418,6 +1543,7 @@ async function mintCapability(
|
|
|
1418
1543
|
next_intent?: unknown;
|
|
1419
1544
|
next_intent_ref?: unknown;
|
|
1420
1545
|
reason?: string;
|
|
1546
|
+
rounds?: number;
|
|
1421
1547
|
finding_id?: string;
|
|
1422
1548
|
resolution?: string;
|
|
1423
1549
|
now: string;
|
|
@@ -1433,6 +1559,7 @@ async function mintCapability(
|
|
|
1433
1559
|
at: now,
|
|
1434
1560
|
actor_id: input.actor_id,
|
|
1435
1561
|
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
1562
|
+
...(input.rounds !== undefined ? { rounds: input.rounds } : {}),
|
|
1436
1563
|
...(input.findings !== undefined ? { findings: input.findings } : {}),
|
|
1437
1564
|
...(input.approval !== undefined ? { approval: input.approval } : {}),
|
|
1438
1565
|
...(input.next_intent !== undefined ? { next_intent: input.next_intent } : {}),
|
|
@@ -1480,9 +1607,13 @@ async function executeOrdinaryOperation(
|
|
|
1480
1607
|
): Promise<unknown> {
|
|
1481
1608
|
const { app } = await authorityPair();
|
|
1482
1609
|
const operation = input.operation.op === "revise_intent"
|
|
1483
|
-
? { ...input.operation, next_intent:
|
|
1610
|
+
? { ...input.operation, next_intent: parseProjectTaskIntent(ctx.cwd, input.operation.next_intent) }
|
|
1484
1611
|
: input.operation;
|
|
1485
1612
|
const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
|
|
1613
|
+
const nextScope = operation.op === "revise_intent"
|
|
1614
|
+
? (operation.next_intent as { scope_hint: string[] }).scope_hint
|
|
1615
|
+
: undefined;
|
|
1616
|
+
if (nextScope) assertScopeAdditionsAreFiles(ctx.cwd, priorIntent.intent.scope_hint, nextScope);
|
|
1486
1617
|
const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
|
|
1487
1618
|
const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
|
|
1488
1619
|
// A content-changing revision writes the sidecar before the kernel's drift
|
|
@@ -1503,7 +1634,8 @@ async function executeOrdinaryOperation(
|
|
|
1503
1634
|
task_id: input.taskId,
|
|
1504
1635
|
operation: operation as never,
|
|
1505
1636
|
prior_intent_token: priorIntent.token,
|
|
1506
|
-
diffProvider: (root: string, record: TaskRecord) =>
|
|
1637
|
+
diffProvider: (root: string, record: TaskRecord) =>
|
|
1638
|
+
diffSnapshotOf(root, nextScope ? withNextScope(record, nextScope) : record),
|
|
1507
1639
|
now: new Date().toISOString(),
|
|
1508
1640
|
});
|
|
1509
1641
|
if (operation.op === "freeze_artifacts" || operation.op === "stop")
|
|
@@ -1548,6 +1680,7 @@ async function closeOutBatchChild(
|
|
|
1548
1680
|
ctx: ExtensionContext,
|
|
1549
1681
|
taskId: string,
|
|
1550
1682
|
enriched: Record<string, unknown>,
|
|
1683
|
+
selectedParent: (slug: string) => boolean,
|
|
1551
1684
|
): Promise<Record<string, unknown>> {
|
|
1552
1685
|
const taskState = enriched.task_state as { lifecycle?: unknown } | undefined;
|
|
1553
1686
|
if (taskState?.lifecycle !== "done") return enriched;
|
|
@@ -1559,6 +1692,7 @@ async function closeOutBatchChild(
|
|
|
1559
1692
|
}
|
|
1560
1693
|
if (!slug) return enriched;
|
|
1561
1694
|
try {
|
|
1695
|
+
if (!selectedParent(slug)) return enriched;
|
|
1562
1696
|
// The batch commit takes exactly the scope envelope plus this task's own
|
|
1563
1697
|
// audit evidence and refuses unstaged changes: stage only that evidence.
|
|
1564
1698
|
if (existsSync(join(ctx.cwd, ".imm", "audit", taskId)))
|
|
@@ -1735,6 +1869,7 @@ function throwIfCanaryToolFailure(
|
|
|
1735
1869
|
// Preserve only the coordinator's bounded observation metadata.
|
|
1736
1870
|
{
|
|
1737
1871
|
...(details.diagnostics ? { diagnostics: details.diagnostics } : {}),
|
|
1872
|
+
...(details.prepare_observation ? { prepare_observation: details.prepare_observation } : {}),
|
|
1738
1873
|
...(details.environment_failure ? { environment_failure: true } : {}),
|
|
1739
1874
|
...(details.recovery ? { recovery: details.recovery } : {}),
|
|
1740
1875
|
...(details.recovery_error ? { recovery_error: details.recovery_error } : {}),
|
|
@@ -12,12 +12,16 @@ import {
|
|
|
12
12
|
} from "../runtime/kernel/batch_authority";
|
|
13
13
|
import { createEnrollmentAuthorityRegistry } from "../runtime/kernel/enrollment_authority";
|
|
14
14
|
import {
|
|
15
|
-
startBatch,
|
|
15
|
+
startBatch, completedParentReport,
|
|
16
16
|
type BatchRunnerKernelPort,
|
|
17
17
|
type BatchRunReport,
|
|
18
18
|
type BatchTrackerPort,
|
|
19
19
|
} from "../runtime/unattended/batch_runner";
|
|
20
20
|
import { createBatchKernelPort } from "../runtime/unattended/batch_kernel_port";
|
|
21
|
+
import { observeInitiative } from "../runtime/unattended/batch_plan";
|
|
22
|
+
import { integrateSettledCriticalSlices, advanceNativeParent } from "../runtime/unattended/batch_git";
|
|
23
|
+
import { parseDeliveryConsolidation, type DeliveryConsolidation } from "../runtime/unattended/parent_delivery_consolidation";
|
|
24
|
+
|
|
21
25
|
import type { BatchRunnerGitPort } from "../runtime/unattended/batch_git";
|
|
22
26
|
import type { InitiativeObservationReader } from "../runtime/unattended/types";
|
|
23
27
|
import { advancePiTask, piRunEntryRefusal } from "./imm-canary-work";
|
|
@@ -27,7 +31,7 @@ import { parseLaneRevision, REVISION_DELEGATION_TEXT, type LaneRevisionRequest }
|
|
|
27
31
|
import { createBatchTrackerPort, laneRuntimeContractRefusal, parseLaneInstruction, type LaneInstructionRequest, parseLaneOffers, parseMaxParallel, resolveLaneParallel, type LaneOffer } from "../runtime/unattended/batch_lanes";
|
|
28
32
|
import {
|
|
29
33
|
authorizeBatch,
|
|
30
|
-
|
|
34
|
+
prepareBatchPreflight,
|
|
31
35
|
} from "../runtime/unattended/batch_preflight";
|
|
32
36
|
import { retireStaleBatch, type StaleBatchFacts } from "../runtime/unattended/batch_disposition";
|
|
33
37
|
import type { BatchLaneRunReport } from "../runtime/unattended/batch_state";
|
|
@@ -113,6 +117,7 @@ export interface PiBatchExecutionOptions {
|
|
|
113
117
|
lane_offers?: LaneOffer[];
|
|
114
118
|
/** Lane mode: the project's full verification commands, run once every child is integrated. */
|
|
115
119
|
final_verification?: string[];
|
|
120
|
+
delivery_consolidation?: DeliveryConsolidation;
|
|
116
121
|
/** Lane mode: one instruction for an idle Lane session, recorded before it is sent. */
|
|
117
122
|
lane_instruction?: LaneInstructionRequest;
|
|
118
123
|
/** Lane mode, ADR 0018: one breaking Intent revision the coordinator approves under the batch's grant. */
|
|
@@ -134,6 +139,7 @@ export interface PiBatchExecutionOptions {
|
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
export type PiBatchExecutionResult =
|
|
142
|
+
| NonNullable<Awaited<ReturnType<typeof advanceNativeParent>>>
|
|
137
143
|
| { state: "started"; batch_id: string; report: BatchRunReport }
|
|
138
144
|
| { state: "rejected"; reason: string; recovery_action: string }
|
|
139
145
|
| { state: "cancelled"; reason: string; recovery_action: string }
|
|
@@ -154,13 +160,56 @@ function piConfirmationRef(input: {
|
|
|
154
160
|
|
|
155
161
|
export async function executePiUnattendedBatch(
|
|
156
162
|
options: PiBatchExecutionOptions,
|
|
163
|
+
): Promise<PiBatchExecutionResult> {
|
|
164
|
+
let commit: string | undefined;
|
|
165
|
+
try {
|
|
166
|
+
const result = await executePreparedPiBatch(options, (value) => { commit = value; });
|
|
167
|
+
if (commit && "reason" in result) result.reason += `; batch precondition commit ${commit} remains`;
|
|
168
|
+
return result;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (commit && error instanceof Error) error.message += `; batch precondition commit ${commit} remains`;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function executePreparedPiBatch(
|
|
176
|
+
options: PiBatchExecutionOptions, onCommit: (commit: string | undefined) => void,
|
|
157
177
|
): Promise<PiBatchExecutionResult> {
|
|
158
178
|
const { root, initiativeSlug, signal } = options;
|
|
159
179
|
const reuseOnly = options.reuseOnly === true;
|
|
160
180
|
const interactive = options.interactive ?? true;
|
|
181
|
+
const consolidation = parseDeliveryConsolidation(options.delivery_consolidation);
|
|
161
182
|
|
|
162
183
|
if (!reuseOnly && !interactive) return { state: "rejected", ...nonInteractiveRefusal() };
|
|
163
|
-
|
|
184
|
+
if (!reuseOnly && !options.confirmBatch) return batchReason("confirmation_port_unavailable");
|
|
185
|
+
|
|
186
|
+
await integrateSettledCriticalSlices(root, initiativeSlug, options.readInitiative);
|
|
187
|
+
const completed = await completedParentReport(root, initiativeSlug, options.readInitiative, options.batchTracker);
|
|
188
|
+
if (completed) return { state: "started", batch_id: completed.batch_id, report: completed };
|
|
189
|
+
const native = await advanceNativeParent(root, initiativeSlug, options.final_verification, options.readInitiative, options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
|
|
190
|
+
async (effects) => {
|
|
191
|
+
// A continuation never opens a gate; unconfirmed commands wait for a fresh explicit entry.
|
|
192
|
+
if (reuseOnly || !options.confirmBatch) return batchReason("confirmation_port_unavailable");
|
|
193
|
+
let decision: "accept" | "accept_delegate" | "decline" | "cancel";
|
|
194
|
+
try {
|
|
195
|
+
decision = await options.confirmBatch({
|
|
196
|
+
title: `Authorize Parent Final Verification: ${effects.initiative_slug}`,
|
|
197
|
+
summary: `Initiative: ${effects.initiative_slug}\nPlan digest: ${effects.plan_digest}\nNo batch run is started.\nFinal verification commands run in this workspace outside Host permission:\n${effects.final_commands.map((command) => ` - ${command}`).join("\n")}`,
|
|
198
|
+
details: `Parent Tasks (${effects.children.length}):\n${effects.children.map((child) => ` - ${child.task_id} (${child.slice_id})`).join("\n")}${effects.integrate_task_ids.length > 0 ? `\n\nSettled deliveries committed first (${effects.integrate_task_ids.length}):\n${effects.integrate_task_ids.map((id) => ` - ${id}`).join("\n")}` : ""}`,
|
|
199
|
+
planDigest: effects.plan_digest,
|
|
200
|
+
...(effects.delivery_consolidation ? { details: JSON.stringify(effects, null, 2) } : {}),
|
|
201
|
+
signal,
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (signal?.aborted) return batchReason("confirmation_cancelled");
|
|
205
|
+
return batchReason("confirmation_failed", err instanceof Error ? err.message : String(err));
|
|
206
|
+
}
|
|
207
|
+
if (decision === "cancel" || signal?.aborted) return batchReason("confirmation_cancelled");
|
|
208
|
+
if (decision === "decline") return batchReason("confirmation_declined");
|
|
209
|
+
if (decision !== "accept") return batchReason("confirmation_no_decision");
|
|
210
|
+
return null;
|
|
211
|
+
}, consolidation);
|
|
212
|
+
if (native) return native;
|
|
164
213
|
// A lane_offers-only call resumes the recorded lane batch; refused before any gate otherwise.
|
|
165
214
|
const resolvedParallel = resolveLaneParallel(root, initiativeSlug, options.max_parallel, options.lane_offers, options.lane_instruction ?? options.lane_revision);
|
|
166
215
|
if (resolvedParallel !== undefined) options = { ...options, max_parallel: resolvedParallel };
|
|
@@ -170,12 +219,14 @@ export async function executePiUnattendedBatch(
|
|
|
170
219
|
// plan digest, and base HEAD are one shared projection, so neither Host
|
|
171
220
|
// re-implements a batch decision.
|
|
172
221
|
let now = new Date().toISOString();
|
|
173
|
-
const preflight = await
|
|
222
|
+
const preflight = await prepareBatchPreflight({
|
|
174
223
|
root,
|
|
175
224
|
initiative_slug: initiativeSlug,
|
|
176
225
|
now,
|
|
177
226
|
readInitiative: options.readInitiative,
|
|
227
|
+
signal, reuseOnly,
|
|
178
228
|
});
|
|
229
|
+
onCommit(preflight.precondition_commit);
|
|
179
230
|
if (!preflight.ok) {
|
|
180
231
|
return { state: preflight.state, reason: preflight.reason, recovery_action: preflight.recovery_action };
|
|
181
232
|
}
|
|
@@ -218,7 +269,7 @@ export async function executePiUnattendedBatch(
|
|
|
218
269
|
}
|
|
219
270
|
const confirmDetails = {
|
|
220
271
|
title: `Authorize Unattended Batch: ${facts.initiative_slug}`,
|
|
221
|
-
summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.
|
|
272
|
+
summary: `Initiative: ${facts.initiative_slug}\nBatch branch: ${facts.batch_branch}\nPlan digest: ${facts.plan_digest}\nBudget: max_children=${facts.budget.max_children}, qa_failure_limit=${facts.budget.qa_failure_limit}${options.max_parallel !== undefined ? `\nLane mode: max_parallel=${options.max_parallel}` : ""}${options.final_verification ? `\nFinal verification: ${options.final_verification.join("; ")}` : ""}`,
|
|
222
273
|
details: `Ordered children (${facts.children.length}):\n${facts.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [risk: ${child.risk}] [status: ${child.status === "already_settled" ? "completed" : "pending execution"}]`).join("\n")}${facts.excluded.length > 0 ? `\n\nExcluded children:\n${facts.excluded.map((child) => ` - ${child.task_id} (${child.slice_id}): ${child.reason}`).join("\n")}` : ""}${options.max_parallel !== undefined ? `\n\nParallel groups (${facts.parallel_groups.length}):\n${facts.parallel_groups.map((group) => ` - ${group.join(", ")}`).join("\n")}${facts.scope_conflicts.length > 0 ? `\n\nSerialized by overlapping scope:\n${facts.scope_conflicts.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join("\n")}` : ""}` : ""}${facts.reuse_blockers.length > 0 ? `\n\nRe-confirmation required: ${facts.reuse_blockers.join(", ")}.\nRecovery: confirm to issue a fresh authorization bound to the current plan and HEAD.` : ""}`,
|
|
223
274
|
planDigest: facts.plan_digest,
|
|
224
275
|
signal,
|
|
@@ -294,6 +345,7 @@ export async function executePiUnattendedBatch(
|
|
|
294
345
|
// this Host supplies only its own advanceTask progression seam (D5).
|
|
295
346
|
const kernelPort = createBatchKernelPort({
|
|
296
347
|
root,
|
|
348
|
+
parent: await (options.readInitiative ?? observeInitiative)(root, initiativeSlug),
|
|
297
349
|
enrollmentRegistry,
|
|
298
350
|
registry: batchRegistry,
|
|
299
351
|
capability,
|
|
@@ -303,7 +355,7 @@ export async function executePiUnattendedBatch(
|
|
|
303
355
|
overrides: otherOverrides,
|
|
304
356
|
});
|
|
305
357
|
|
|
306
|
-
|
|
358
|
+
let report = await startBatch({
|
|
307
359
|
root,
|
|
308
360
|
batch_id: batchId,
|
|
309
361
|
initiative_slug: initiativeSlug,
|
|
@@ -317,11 +369,11 @@ export async function executePiUnattendedBatch(
|
|
|
317
369
|
now,
|
|
318
370
|
kernel: kernelPort,
|
|
319
371
|
git: options.batchGit,
|
|
372
|
+
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
320
373
|
...(options.max_parallel !== undefined
|
|
321
374
|
? {
|
|
322
375
|
max_parallel: options.max_parallel,
|
|
323
376
|
tracker: options.batchTracker ?? createBatchTrackerPort(root, initiativeSlug),
|
|
324
|
-
...(options.final_verification ? { final_verification: options.final_verification } : {}),
|
|
325
377
|
...(options.lane_instruction ? { lane_instruction: options.lane_instruction } : {}),
|
|
326
378
|
...(options.lane_revision ? { lane_revision: options.lane_revision } : {}),
|
|
327
379
|
...(authorization.revision_delegation !== undefined ? { revision_delegation: authorization.revision_delegation } : {}),
|
|
@@ -334,6 +386,8 @@ export async function executePiUnattendedBatch(
|
|
|
334
386
|
return batchReason("batch_run_rejected", report.reason ?? "");
|
|
335
387
|
}
|
|
336
388
|
|
|
389
|
+
if (report.batch_state === "completed" && report.final_verification?.passed)
|
|
390
|
+
report = await completedParentReport(root, initiativeSlug, options.readInitiative, options.batchTracker) ?? report;
|
|
337
391
|
return {
|
|
338
392
|
state: "started",
|
|
339
393
|
batch_id: batchId,
|
|
@@ -362,7 +416,7 @@ function retireConfirmationDetails(facts: StaleBatchFacts): {
|
|
|
362
416
|
details: `Children (${facts.children.length}):\n${facts.children
|
|
363
417
|
.map(
|
|
364
418
|
(child) =>
|
|
365
|
-
` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`,
|
|
419
|
+
` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}${child.kernel_lifecycle ? ` [Kernel run: ${child.kernel_lifecycle}]` : ""}`,
|
|
366
420
|
)
|
|
367
421
|
.join("\n")}\n\nRetiring 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.\n\nA 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.\n\nAfter retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative.`,
|
|
368
422
|
};
|
|
@@ -437,6 +491,11 @@ export default function (
|
|
|
437
491
|
lane_offers: Type.Optional(Type.Array(
|
|
438
492
|
Type.Object({ task_id: Type.String(), path: Type.String() }, { additionalProperties: false }),
|
|
439
493
|
)),
|
|
494
|
+
delivery_consolidation: Type.Optional(Type.Object({
|
|
495
|
+
task_id: Type.String(), run_id: Type.String(), source_runs: Type.Array(Type.Object({
|
|
496
|
+
task_id: Type.String(), run_id: Type.String(),
|
|
497
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 16 }),
|
|
498
|
+
}, { additionalProperties: false })),
|
|
440
499
|
final_verification: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 8 })),
|
|
441
500
|
lane_instruction: Type.Optional(Type.Object({
|
|
442
501
|
task_id: Type.String(),
|
|
@@ -451,7 +510,7 @@ export default function (
|
|
|
451
510
|
}, { additionalProperties: false }),
|
|
452
511
|
execute: async (
|
|
453
512
|
_toolCallId: string,
|
|
454
|
-
params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; lane_instruction?: unknown; lane_revision?: unknown },
|
|
513
|
+
params: { initiative_slug: string; max_parallel?: number; lane_offers?: Array<{ task_id: string; path: string }>; final_verification?: string[]; delivery_consolidation?: unknown; lane_instruction?: unknown; lane_revision?: unknown },
|
|
455
514
|
signal: AbortSignal | undefined,
|
|
456
515
|
_onUpdate: unknown,
|
|
457
516
|
ctx: ExtensionContext,
|
|
@@ -486,6 +545,7 @@ export default function (
|
|
|
486
545
|
const maxParallel = parseMaxParallel(params.max_parallel);
|
|
487
546
|
const laneOffers = parseLaneOffers(params.lane_offers);
|
|
488
547
|
const finalVerification = parseFinalVerification(params.final_verification);
|
|
548
|
+
const deliveryConsolidation = parseDeliveryConsolidation(params.delivery_consolidation);
|
|
489
549
|
const laneInstruction = parseLaneInstruction(params.lane_instruction);
|
|
490
550
|
const laneRevision = parseLaneRevision(params.lane_revision);
|
|
491
551
|
|
|
@@ -495,6 +555,7 @@ export default function (
|
|
|
495
555
|
...(maxParallel !== undefined ? { max_parallel: maxParallel } : {}),
|
|
496
556
|
...(laneOffers !== undefined ? { lane_offers: laneOffers } : {}),
|
|
497
557
|
...(finalVerification !== undefined ? { final_verification: finalVerification } : {}),
|
|
558
|
+
...(deliveryConsolidation !== undefined ? { delivery_consolidation: deliveryConsolidation } : {}),
|
|
498
559
|
...(laneInstruction !== undefined ? { lane_instruction: laneInstruction } : {}),
|
|
499
560
|
...(laneRevision !== undefined ? { lane_revision: laneRevision } : {}),
|
|
500
561
|
interactive: ctx.mode === "tui",
|
|
@@ -537,7 +598,7 @@ export default function (
|
|
|
537
598
|
},
|
|
538
599
|
});
|
|
539
600
|
|
|
540
|
-
if (result.state !== "started") {
|
|
601
|
+
if (result.state !== "started" && result.state !== "native_parent") {
|
|
541
602
|
const failureState = result.state === "blocked" ? "blocked" : "failed";
|
|
542
603
|
throwToolFailure({
|
|
543
604
|
tool: "imm_canary_enrollment",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssuranceAdvanceResult } from "../runtime/assurance/coordinator";
|
|
2
2
|
|
|
3
3
|
export type AssuranceFailureDetails = Pick<AssuranceAdvanceResult,
|
|
4
|
-
"diagnostics" | "environment_failure" | "recovery" | "recovery_error">;
|
|
4
|
+
"diagnostics" | "prepare_observation" | "environment_failure" | "recovery" | "recovery_error">;
|
|
5
5
|
|
|
6
6
|
export interface ToolFailureV1 extends AssuranceFailureDetails {
|
|
7
7
|
contract: "immune_brain/tool_failure/v1";
|