@abelxiaoxing/cadence 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -10
- package/package.json +5 -3
- package/prompts/abel-design.md +71 -17
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-patch.ts +2 -1
- package/src/canonical.ts +16 -0
- package/src/change-contract.ts +268 -0
- package/src/child-session.ts +4 -3
- package/src/contracts.ts +141 -33
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +148 -31
- package/src/design-control.ts +69 -11
- package/src/design-journal.ts +216 -72
- package/src/durable-workflow.ts +4385 -0
- package/src/implement-graph.ts +11 -16
- package/src/index.ts +380 -981
- package/src/isolation-backend.ts +84 -23
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +14 -1
- package/src/submit-tool.ts +20 -2
- package/src/task-ledger.ts +14 -55
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +97 -34
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1427 -0
- package/src/workflow-state-machine.ts +4478 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +984 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
package/src/index.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
2
|
+
import { packageDeliverySource } from "./package-delivery.ts";
|
|
3
|
+
import {
|
|
4
|
+
changeVerificationResult,
|
|
5
|
+
executePackageVerification,
|
|
6
|
+
phaseVerificationResult,
|
|
7
|
+
} from "./package-verification.ts";
|
|
8
|
+
import { captureVerificationEnvironmentIdentity } from "./verification-environment.ts";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
type PackageDeliverySourceOptions,
|
|
12
|
+
type PackageWorkflowDeliverySource,
|
|
13
|
+
packageDeliverySource,
|
|
14
|
+
} from "./package-delivery.ts";
|
|
15
|
+
export { executePackageVerification } from "./package-verification.ts";
|
|
16
|
+
|
|
1
17
|
// Private workflow orchestration extension. Registers the abel_dispatch tool
|
|
2
18
|
// on load, then keeps it inactive by default by removing only that name from
|
|
3
19
|
// the active set at session start. Eligible-stage activation is wired by the
|
|
@@ -5,14 +21,6 @@
|
|
|
5
21
|
// integration; abel-init and ordinary prompts never activate dispatch.
|
|
6
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
23
|
import { createHash } from "node:crypto";
|
|
8
|
-
import {
|
|
9
|
-
lstatSync,
|
|
10
|
-
mkdirSync,
|
|
11
|
-
mkdtempSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
realpathSync,
|
|
14
|
-
rmSync,
|
|
15
|
-
} from "node:fs";
|
|
16
24
|
import { homedir } from "node:os";
|
|
17
25
|
import path, { dirname, join } from "node:path";
|
|
18
26
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,12 +36,9 @@ import { type Activation, activateTool, deactivateTool } from "./activation.ts";
|
|
|
28
36
|
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
29
37
|
import { runChildSession } from "./child-session.ts";
|
|
30
38
|
import {
|
|
31
|
-
type AtomicVerificationContract,
|
|
32
39
|
type DesignEvidenceResult,
|
|
33
|
-
isValidRelativePath,
|
|
34
40
|
LIMITS,
|
|
35
41
|
type StructuredVerificationContract,
|
|
36
|
-
verificationSteps,
|
|
37
42
|
} from "./contracts.ts";
|
|
38
43
|
import {
|
|
39
44
|
CONTROL_COMMAND_PARAMETERS,
|
|
@@ -41,26 +46,16 @@ import {
|
|
|
41
46
|
validateControlCommand,
|
|
42
47
|
} from "./control-contracts.ts";
|
|
43
48
|
import {
|
|
44
|
-
assessDeliveryTraceability,
|
|
45
|
-
compileImplementPlan,
|
|
46
49
|
DeliveryValidationError,
|
|
47
50
|
DesignPlanValidationError,
|
|
48
|
-
type GateApprovalProof,
|
|
49
|
-
parseGateAReceipt,
|
|
50
|
-
parseImplementPlan,
|
|
51
|
-
parseReadyReceipt,
|
|
52
51
|
} from "./delivery-compiler.ts";
|
|
53
52
|
import {
|
|
54
53
|
DesignController,
|
|
55
54
|
DesignFinalizationError,
|
|
56
55
|
validateDesignControlRequest,
|
|
57
56
|
} from "./design-control.ts";
|
|
58
|
-
import { canonicalJson
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
inspectOpenSpecDelivery,
|
|
62
|
-
type OpenSpecDeliveryInspection,
|
|
63
|
-
} from "./openspec-cli.ts";
|
|
57
|
+
import { canonicalJson } from "./implement-graph.ts";
|
|
58
|
+
import { inspectOpenSpecDelivery } from "./openspec-cli.ts";
|
|
64
59
|
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
65
60
|
import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
66
61
|
import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
@@ -72,7 +67,7 @@ import {
|
|
|
72
67
|
type WorkerRoutePolicy,
|
|
73
68
|
} from "./route-policy.ts";
|
|
74
69
|
import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
|
|
75
|
-
import {
|
|
70
|
+
import { isSafeRegularFile } from "./safe-path.ts";
|
|
76
71
|
import { resolveStateRoot, StateRootError } from "./state-root.ts";
|
|
77
72
|
import {
|
|
78
73
|
ACTIVITY_DETAILS_KEY,
|
|
@@ -81,17 +76,11 @@ import {
|
|
|
81
76
|
renderActivityResult,
|
|
82
77
|
type WorkflowActivityUpdate,
|
|
83
78
|
} from "./subagent-activity.ts";
|
|
84
|
-
import { classifyCandidateContextRequest } from "./submit-tool.ts";
|
|
85
79
|
import {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} from "./
|
|
90
|
-
import {
|
|
91
|
-
openDurableWorkflowEngine,
|
|
92
|
-
type WorkflowAvailableDelivery,
|
|
93
|
-
type WorkflowDeliverySource,
|
|
94
|
-
} from "./workflow-engine.ts";
|
|
80
|
+
classifyCandidateContextRequest,
|
|
81
|
+
permitsContextRead,
|
|
82
|
+
} from "./submit-tool.ts";
|
|
83
|
+
import { openDurableWorkflowEngine } from "./workflow-engine.ts";
|
|
95
84
|
|
|
96
85
|
export {
|
|
97
86
|
inspectOpenSpecDelivery,
|
|
@@ -293,9 +282,9 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
|
|
|
293
282
|
operationId: { type: "string" },
|
|
294
283
|
gate: { type: "string", enum: ["gate-a"] },
|
|
295
284
|
contract: {
|
|
296
|
-
type: "string",
|
|
285
|
+
anyOf: [{ type: "string" }, { type: "object" }],
|
|
297
286
|
description:
|
|
298
|
-
"
|
|
287
|
+
"Structured ChangeContract with goal, acceptance, constraints and policy. Legacy prose remains readable; code hashes authority.",
|
|
299
288
|
},
|
|
300
289
|
},
|
|
301
290
|
required: ["operation", "runId", "operationId", "gate", "contract"],
|
|
@@ -363,11 +352,13 @@ function invokedPrompt(text: string): EligiblePrompt | undefined {
|
|
|
363
352
|
return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
|
|
364
353
|
}
|
|
365
354
|
|
|
366
|
-
|
|
355
|
+
type WorkflowPrompt = EligiblePrompt | "abel-init";
|
|
356
|
+
|
|
357
|
+
function promptMarker(name: WorkflowPrompt): string {
|
|
367
358
|
return `<!-- ABEL:PROMPT:${name} -->`;
|
|
368
359
|
}
|
|
369
360
|
|
|
370
|
-
function hasPackageProvenance(pi: ExtensionAPI, name:
|
|
361
|
+
function hasPackageProvenance(pi: ExtensionAPI, name: WorkflowPrompt): boolean {
|
|
371
362
|
const commands = pi
|
|
372
363
|
.getCommands()
|
|
373
364
|
.filter((candidate) => candidate.name === name);
|
|
@@ -382,7 +373,7 @@ function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
|
|
|
382
373
|
|
|
383
374
|
function hasExpandedPromptMarker(
|
|
384
375
|
prompt: string,
|
|
385
|
-
name:
|
|
376
|
+
name: WorkflowPrompt,
|
|
386
377
|
): boolean {
|
|
387
378
|
const requestEnd = prompt.lastIndexOf("</abel-request>");
|
|
388
379
|
if (requestEnd < 0) return false;
|
|
@@ -650,12 +641,12 @@ function safeDesignFailure(
|
|
|
650
641
|
SAFE_DESIGN_ERROR_CODE.test(entry[1])
|
|
651
642
|
);
|
|
652
643
|
})
|
|
653
|
-
.sort(([left], [right]) => left
|
|
644
|
+
.sort(([left], [right]) => compareCanonicalStrings(left, right)),
|
|
654
645
|
);
|
|
655
646
|
return typeof admitted.code === "string" ? [admitted] : [];
|
|
656
647
|
})
|
|
657
648
|
.sort((left, right) =>
|
|
658
|
-
canonicalJson(left)
|
|
649
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
659
650
|
)
|
|
660
651
|
.filter(
|
|
661
652
|
(candidate, index, values) =>
|
|
@@ -687,6 +678,11 @@ export interface WorkflowControlEngine {
|
|
|
687
678
|
onActivity?: (event: WorkflowActivityUpdate) => void,
|
|
688
679
|
): Promise<Record<string, unknown>>;
|
|
689
680
|
executeDesign?(request: unknown): Promise<Record<string, unknown>>;
|
|
681
|
+
executeAmendment?(
|
|
682
|
+
change: string,
|
|
683
|
+
batchId: string,
|
|
684
|
+
request: unknown,
|
|
685
|
+
): Promise<Record<string, unknown>>;
|
|
690
686
|
assertDesignRun?(runId: string): void;
|
|
691
687
|
recordDesignEvidence?(input: {
|
|
692
688
|
runId: string;
|
|
@@ -700,899 +696,10 @@ export type WorkflowControlEngineFactory = (
|
|
|
700
696
|
parentPayloadBridge: ParentPayloadBridge,
|
|
701
697
|
) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
|
|
702
698
|
|
|
703
|
-
const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
|
|
704
|
-
|
|
705
|
-
export interface PackageDeliverySourceOptions {
|
|
706
|
-
inspectOpenSpec?: (
|
|
707
|
-
consumerRoot: string,
|
|
708
|
-
change: string,
|
|
709
|
-
) => Promise<OpenSpecDeliveryInspection>;
|
|
710
|
-
verifyGateProof?: (input: {
|
|
711
|
-
change: string;
|
|
712
|
-
gate: "gate-a" | "gate-b";
|
|
713
|
-
proof: GateApprovalProof;
|
|
714
|
-
}) => boolean;
|
|
715
|
-
verifyFinalizedDelivery?: (input: {
|
|
716
|
-
change: string;
|
|
717
|
-
deliveryRevision: number;
|
|
718
|
-
receiptHash: string;
|
|
719
|
-
gateA: GateApprovalProof;
|
|
720
|
-
gateB: GateApprovalProof;
|
|
721
|
-
planCanonicalHash: string;
|
|
722
|
-
}) => boolean;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
|
|
726
|
-
discoverLatest(input: {
|
|
727
|
-
stage: "abel-implement";
|
|
728
|
-
change: string;
|
|
729
|
-
}): Promise<WorkflowAvailableDelivery | undefined>;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
699
|
function sha256(value: Uint8Array | string): string {
|
|
733
700
|
return createHash("sha256").update(value).digest("hex");
|
|
734
701
|
}
|
|
735
702
|
|
|
736
|
-
function proofVerified(verify: () => boolean): boolean {
|
|
737
|
-
try {
|
|
738
|
-
return verify() === true;
|
|
739
|
-
} catch {
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function normalizedTrackingArtifact(
|
|
745
|
-
bytes: Uint8Array,
|
|
746
|
-
taskIds: readonly string[],
|
|
747
|
-
): Buffer | undefined {
|
|
748
|
-
let text: string;
|
|
749
|
-
try {
|
|
750
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
751
|
-
} catch {
|
|
752
|
-
return undefined;
|
|
753
|
-
}
|
|
754
|
-
const identities = taskIds.map((taskId) => ({
|
|
755
|
-
taskId,
|
|
756
|
-
pattern: new RegExp(
|
|
757
|
-
`(?:^|[^A-Za-z0-9._:-])${taskId.replace(
|
|
758
|
-
/[.*+?^${}()|[\]\\]/gu,
|
|
759
|
-
"\\$&",
|
|
760
|
-
)}(?![A-Za-z0-9._:-])`,
|
|
761
|
-
"u",
|
|
762
|
-
),
|
|
763
|
-
matches: 0,
|
|
764
|
-
}));
|
|
765
|
-
const normalized = text
|
|
766
|
-
.split(/(?<=\n)/u)
|
|
767
|
-
.map((segment) => {
|
|
768
|
-
const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
|
|
769
|
-
if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
|
|
770
|
-
const matching = identities.filter(({ pattern }) => pattern.test(line));
|
|
771
|
-
if (matching.length !== 1) return segment;
|
|
772
|
-
matching[0].matches += 1;
|
|
773
|
-
return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
|
|
774
|
-
})
|
|
775
|
-
.join("");
|
|
776
|
-
if (identities.some(({ matches }) => matches !== 1)) return undefined;
|
|
777
|
-
return Buffer.from(normalized, "utf8");
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function readPackageDeliveryFile(
|
|
781
|
-
root: string,
|
|
782
|
-
relative: string,
|
|
783
|
-
maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
|
|
784
|
-
): Buffer {
|
|
785
|
-
const observation = observeSafePath(root, relative);
|
|
786
|
-
if (observation.kind !== "file") {
|
|
787
|
-
throw new Error("delivery-file-unavailable");
|
|
788
|
-
}
|
|
789
|
-
const target = path.join(root, ...relative.split("/"));
|
|
790
|
-
const stat = lstatSync(target);
|
|
791
|
-
if (stat.size < 1 || stat.size > maximumBytes) {
|
|
792
|
-
throw new Error("delivery-file-size-invalid");
|
|
793
|
-
}
|
|
794
|
-
return readFileSync(target);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export function packageDeliverySource(
|
|
798
|
-
consumerRoot: string,
|
|
799
|
-
options: PackageDeliverySourceOptions = {},
|
|
800
|
-
): PackageWorkflowDeliverySource {
|
|
801
|
-
const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
|
|
802
|
-
const verifyGateProof = options.verifyGateProof;
|
|
803
|
-
const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
|
|
804
|
-
return {
|
|
805
|
-
async discoverLatest(input) {
|
|
806
|
-
if (
|
|
807
|
-
input.stage !== "abel-implement" ||
|
|
808
|
-
!verifyGateProof ||
|
|
809
|
-
!verifyFinalizedDelivery
|
|
810
|
-
) {
|
|
811
|
-
return undefined;
|
|
812
|
-
}
|
|
813
|
-
try {
|
|
814
|
-
const changeRoot = `openspec/changes/${input.change}`;
|
|
815
|
-
const receiptBytes = readPackageDeliveryFile(
|
|
816
|
-
consumerRoot,
|
|
817
|
-
`${changeRoot}/ready.yaml`,
|
|
818
|
-
4 * 1024 * 1024,
|
|
819
|
-
);
|
|
820
|
-
const receipt = parseReadyReceipt(receiptBytes);
|
|
821
|
-
if (receipt.change !== input.change) return undefined;
|
|
822
|
-
const gateABytes = readPackageDeliveryFile(
|
|
823
|
-
consumerRoot,
|
|
824
|
-
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
825
|
-
4 * 1024 * 1024,
|
|
826
|
-
);
|
|
827
|
-
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
828
|
-
return undefined;
|
|
829
|
-
}
|
|
830
|
-
const gateA = parseGateAReceipt(gateABytes);
|
|
831
|
-
if (
|
|
832
|
-
gateA.change !== receipt.change ||
|
|
833
|
-
gateA.schema !== receipt.schema ||
|
|
834
|
-
!proofVerified(() =>
|
|
835
|
-
verifyGateProof({
|
|
836
|
-
change: input.change,
|
|
837
|
-
gate: "gate-a",
|
|
838
|
-
proof: gateA.approval,
|
|
839
|
-
}),
|
|
840
|
-
) ||
|
|
841
|
-
!proofVerified(() =>
|
|
842
|
-
verifyGateProof({
|
|
843
|
-
change: input.change,
|
|
844
|
-
gate: "gate-b",
|
|
845
|
-
proof: receipt.approvals.gateB,
|
|
846
|
-
}),
|
|
847
|
-
)
|
|
848
|
-
) {
|
|
849
|
-
return undefined;
|
|
850
|
-
}
|
|
851
|
-
const receiptHash = sha256(receiptBytes);
|
|
852
|
-
if (
|
|
853
|
-
!proofVerified(() =>
|
|
854
|
-
verifyFinalizedDelivery({
|
|
855
|
-
change: input.change,
|
|
856
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
857
|
-
receiptHash,
|
|
858
|
-
gateA: gateA.approval,
|
|
859
|
-
gateB: receipt.approvals.gateB,
|
|
860
|
-
planCanonicalHash: receipt.plan.canonicalHash,
|
|
861
|
-
}),
|
|
862
|
-
)
|
|
863
|
-
) {
|
|
864
|
-
return undefined;
|
|
865
|
-
}
|
|
866
|
-
return {
|
|
867
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
868
|
-
receiptHash,
|
|
869
|
-
};
|
|
870
|
-
} catch {
|
|
871
|
-
return undefined;
|
|
872
|
-
}
|
|
873
|
-
},
|
|
874
|
-
async load(input) {
|
|
875
|
-
if (input.stage !== "abel-implement") {
|
|
876
|
-
throw new DeliveryValidationError(["delivery-stage-invalid"]);
|
|
877
|
-
}
|
|
878
|
-
const changeRoot = `openspec/changes/${input.change}`;
|
|
879
|
-
const receiptRelative = `${changeRoot}/ready.yaml`;
|
|
880
|
-
const planRelative = `${changeRoot}/implement-plan.json`;
|
|
881
|
-
let receiptBytes: Buffer;
|
|
882
|
-
try {
|
|
883
|
-
receiptBytes = readPackageDeliveryFile(
|
|
884
|
-
consumerRoot,
|
|
885
|
-
receiptRelative,
|
|
886
|
-
4 * 1024 * 1024,
|
|
887
|
-
);
|
|
888
|
-
} catch {
|
|
889
|
-
throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
|
|
890
|
-
}
|
|
891
|
-
let receipt: ReturnType<typeof parseReadyReceipt>;
|
|
892
|
-
try {
|
|
893
|
-
receipt = parseReadyReceipt(receiptBytes);
|
|
894
|
-
} catch {
|
|
895
|
-
throw new DeliveryValidationError([
|
|
896
|
-
"delivery-receipt-invalid",
|
|
897
|
-
"delivery-recompile-required",
|
|
898
|
-
]);
|
|
899
|
-
}
|
|
900
|
-
const diagnostics = new Set<string>();
|
|
901
|
-
if (receipt.change !== input.change) {
|
|
902
|
-
diagnostics.add("delivery-change-mismatch");
|
|
903
|
-
}
|
|
904
|
-
const receiptHash = sha256(receiptBytes);
|
|
905
|
-
if (
|
|
906
|
-
(input.deliveryRevision !== undefined &&
|
|
907
|
-
input.deliveryRevision !== receipt.deliveryRevision) ||
|
|
908
|
-
(input.receiptHash !== undefined && input.receiptHash !== receiptHash)
|
|
909
|
-
) {
|
|
910
|
-
diagnostics.add("delivery-revision-mismatch");
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
let inspection: OpenSpecDeliveryInspection | undefined;
|
|
914
|
-
try {
|
|
915
|
-
inspection = await inspect(consumerRoot, input.change);
|
|
916
|
-
} catch {
|
|
917
|
-
diagnostics.add("delivery-openspec-unavailable");
|
|
918
|
-
}
|
|
919
|
-
if (inspection) {
|
|
920
|
-
if (!inspection.strictValid) {
|
|
921
|
-
diagnostics.add("delivery-openspec-strict-invalid");
|
|
922
|
-
}
|
|
923
|
-
if (!inspection.planningComplete) {
|
|
924
|
-
diagnostics.add("delivery-openspec-incomplete");
|
|
925
|
-
}
|
|
926
|
-
if (inspection.schema !== receipt.schema) {
|
|
927
|
-
diagnostics.add("delivery-schema-mismatch");
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
let gateABytes: Buffer | undefined;
|
|
932
|
-
let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
|
|
933
|
-
try {
|
|
934
|
-
gateABytes = readPackageDeliveryFile(
|
|
935
|
-
consumerRoot,
|
|
936
|
-
`${changeRoot}/${receipt.approvals.gateA.path}`,
|
|
937
|
-
4 * 1024 * 1024,
|
|
938
|
-
);
|
|
939
|
-
if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
|
|
940
|
-
diagnostics.add("delivery-gate-a-hash-mismatch");
|
|
941
|
-
}
|
|
942
|
-
gateA = parseGateAReceipt(gateABytes);
|
|
943
|
-
if (
|
|
944
|
-
gateA.change !== receipt.change ||
|
|
945
|
-
gateA.schema !== receipt.schema
|
|
946
|
-
) {
|
|
947
|
-
diagnostics.add("delivery-gate-a-binding-mismatch");
|
|
948
|
-
}
|
|
949
|
-
} catch {
|
|
950
|
-
diagnostics.add("delivery-gate-a-invalid");
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const artifactBytes = new Map<string, Buffer>();
|
|
954
|
-
const artifactHashes = new Map(
|
|
955
|
-
receipt.artifacts.map((artifact) => [
|
|
956
|
-
artifact.path,
|
|
957
|
-
artifact.rawSha256,
|
|
958
|
-
]),
|
|
959
|
-
);
|
|
960
|
-
for (const artifact of receipt.artifacts) {
|
|
961
|
-
try {
|
|
962
|
-
const bytes = readPackageDeliveryFile(
|
|
963
|
-
consumerRoot,
|
|
964
|
-
`${changeRoot}/${artifact.path}`,
|
|
965
|
-
);
|
|
966
|
-
artifactBytes.set(artifact.path, bytes);
|
|
967
|
-
if (
|
|
968
|
-
artifact.path !== receipt.traceability.taskPath &&
|
|
969
|
-
sha256(bytes) !== artifact.rawSha256
|
|
970
|
-
) {
|
|
971
|
-
diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
|
|
972
|
-
}
|
|
973
|
-
} catch {
|
|
974
|
-
diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (inspection) {
|
|
978
|
-
const covered = new Set(receipt.artifacts.map((entry) => entry.path));
|
|
979
|
-
const expected = new Set(inspection.artifactPaths);
|
|
980
|
-
for (const relative of expected) {
|
|
981
|
-
if (!isValidRelativePath(relative) || !covered.has(relative)) {
|
|
982
|
-
diagnostics.add(`delivery-artifact-unbound:${relative}`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
for (const relative of covered) {
|
|
986
|
-
if (!expected.has(relative)) {
|
|
987
|
-
diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
if (gateA) {
|
|
992
|
-
if (!verifyGateProof) {
|
|
993
|
-
diagnostics.add("delivery-gate-proof-verifier-unavailable");
|
|
994
|
-
} else if (
|
|
995
|
-
!proofVerified(() =>
|
|
996
|
-
verifyGateProof({
|
|
997
|
-
change: input.change,
|
|
998
|
-
gate: "gate-a",
|
|
999
|
-
proof: gateA.approval,
|
|
1000
|
-
}),
|
|
1001
|
-
)
|
|
1002
|
-
) {
|
|
1003
|
-
diagnostics.add("delivery-gate-a-proof-invalid");
|
|
1004
|
-
}
|
|
1005
|
-
for (const artifact of gateA.artifacts) {
|
|
1006
|
-
if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
|
|
1007
|
-
diagnostics.add(
|
|
1008
|
-
`delivery-gate-a-artifact-mismatch:${artifact.path}`,
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
if (
|
|
1014
|
-
verifyGateProof &&
|
|
1015
|
-
!proofVerified(() =>
|
|
1016
|
-
verifyGateProof({
|
|
1017
|
-
change: input.change,
|
|
1018
|
-
gate: "gate-b",
|
|
1019
|
-
proof: receipt.approvals.gateB,
|
|
1020
|
-
}),
|
|
1021
|
-
)
|
|
1022
|
-
) {
|
|
1023
|
-
diagnostics.add("delivery-gate-b-proof-invalid");
|
|
1024
|
-
}
|
|
1025
|
-
if (gateA) {
|
|
1026
|
-
if (!verifyFinalizedDelivery) {
|
|
1027
|
-
diagnostics.add("delivery-finalization-verifier-unavailable");
|
|
1028
|
-
} else if (
|
|
1029
|
-
!proofVerified(() =>
|
|
1030
|
-
verifyFinalizedDelivery({
|
|
1031
|
-
change: input.change,
|
|
1032
|
-
deliveryRevision: receipt.deliveryRevision,
|
|
1033
|
-
receiptHash,
|
|
1034
|
-
gateA: gateA.approval,
|
|
1035
|
-
gateB: receipt.approvals.gateB,
|
|
1036
|
-
planCanonicalHash: receipt.plan.canonicalHash,
|
|
1037
|
-
}),
|
|
1038
|
-
)
|
|
1039
|
-
) {
|
|
1040
|
-
diagnostics.add("delivery-finalization-proof-invalid");
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
let plan: ReturnType<typeof parseImplementPlan> | undefined;
|
|
1045
|
-
let planBytes: Buffer | undefined;
|
|
1046
|
-
try {
|
|
1047
|
-
planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
|
|
1048
|
-
plan = parseImplementPlan(planBytes);
|
|
1049
|
-
} catch {
|
|
1050
|
-
diagnostics.add("delivery-plan-invalid");
|
|
1051
|
-
}
|
|
1052
|
-
if (plan && planBytes) {
|
|
1053
|
-
if (plan.changeId !== input.change) {
|
|
1054
|
-
diagnostics.add("delivery-plan-change-mismatch");
|
|
1055
|
-
}
|
|
1056
|
-
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
1057
|
-
try {
|
|
1058
|
-
compiled = compileImplementPlan(plan, { consumerRoot });
|
|
1059
|
-
} catch {
|
|
1060
|
-
diagnostics.add("delivery-verification-closure-invalid");
|
|
1061
|
-
}
|
|
1062
|
-
if (
|
|
1063
|
-
receipt.plan.path !== "implement-plan.json" ||
|
|
1064
|
-
receipt.plan.rawSha256 !== sha256(planBytes) ||
|
|
1065
|
-
receipt.plan.canonicalHash !== hashCanonicalValue(plan)
|
|
1066
|
-
) {
|
|
1067
|
-
diagnostics.add("delivery-plan-binding-invalid");
|
|
1068
|
-
}
|
|
1069
|
-
if (
|
|
1070
|
-
compiled &&
|
|
1071
|
-
canonicalJson(compiled.closure) !==
|
|
1072
|
-
canonicalJson(receipt.verificationClosure)
|
|
1073
|
-
) {
|
|
1074
|
-
diagnostics.add("delivery-verification-closure-mismatch");
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
if (plan) {
|
|
1079
|
-
const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
|
|
1080
|
-
const expectedTasksHash = artifactHashes.get(
|
|
1081
|
-
receipt.traceability.taskPath,
|
|
1082
|
-
);
|
|
1083
|
-
if (
|
|
1084
|
-
tasksBytes &&
|
|
1085
|
-
expectedTasksHash &&
|
|
1086
|
-
sha256(tasksBytes) !== expectedTasksHash
|
|
1087
|
-
) {
|
|
1088
|
-
const normalized = normalizedTrackingArtifact(
|
|
1089
|
-
tasksBytes,
|
|
1090
|
-
plan.tracking.taskIds,
|
|
1091
|
-
);
|
|
1092
|
-
if (!normalized || sha256(normalized) !== expectedTasksHash) {
|
|
1093
|
-
diagnostics.add(
|
|
1094
|
-
`delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
const specs = [...artifactBytes]
|
|
1099
|
-
.filter(
|
|
1100
|
-
([relative]) =>
|
|
1101
|
-
relative.startsWith("specs/") && relative.endsWith("/spec.md"),
|
|
1102
|
-
)
|
|
1103
|
-
.flatMap(([relative, bytes]) => {
|
|
1104
|
-
try {
|
|
1105
|
-
return [
|
|
1106
|
-
{
|
|
1107
|
-
path: relative,
|
|
1108
|
-
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
1109
|
-
},
|
|
1110
|
-
];
|
|
1111
|
-
} catch {
|
|
1112
|
-
diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
|
|
1113
|
-
return [];
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
if (!tasksBytes || specs.length === 0) {
|
|
1117
|
-
diagnostics.add("delivery-traceability-input-unavailable");
|
|
1118
|
-
} else {
|
|
1119
|
-
try {
|
|
1120
|
-
const traceability = assessDeliveryTraceability({
|
|
1121
|
-
tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
|
|
1122
|
-
tasksBytes,
|
|
1123
|
-
),
|
|
1124
|
-
specs,
|
|
1125
|
-
plan,
|
|
1126
|
-
});
|
|
1127
|
-
if (!traceability.ok) {
|
|
1128
|
-
for (const diagnostic of traceability.diagnostics) {
|
|
1129
|
-
diagnostics.add(diagnostic);
|
|
1130
|
-
}
|
|
1131
|
-
} else if (
|
|
1132
|
-
canonicalJson(traceability.value) !==
|
|
1133
|
-
canonicalJson(receipt.traceability)
|
|
1134
|
-
) {
|
|
1135
|
-
diagnostics.add("delivery-traceability-binding-mismatch");
|
|
1136
|
-
}
|
|
1137
|
-
} catch {
|
|
1138
|
-
diagnostics.add("delivery-traceability-invalid");
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
if (!plan || !gateA || diagnostics.size > 0) {
|
|
1143
|
-
throw new DeliveryValidationError([...diagnostics]);
|
|
1144
|
-
}
|
|
1145
|
-
return {
|
|
1146
|
-
gate: "gate-b",
|
|
1147
|
-
revision: receipt.deliveryRevision,
|
|
1148
|
-
receiptHash,
|
|
1149
|
-
plan,
|
|
1150
|
-
approvalProofs: {
|
|
1151
|
-
gateA: structuredClone(gateA.approval),
|
|
1152
|
-
gateB: structuredClone(receipt.approvals.gateB),
|
|
1153
|
-
},
|
|
1154
|
-
};
|
|
1155
|
-
},
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
function packageScriptInvocation(
|
|
1160
|
-
packageManager: string,
|
|
1161
|
-
script: string,
|
|
1162
|
-
args: string[],
|
|
1163
|
-
): string[] {
|
|
1164
|
-
return packageManager === "npm" || packageManager === "pnpm"
|
|
1165
|
-
? ["run", script, "--", ...args]
|
|
1166
|
-
: ["run", script, ...args];
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
function bindingFor(
|
|
1170
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1171
|
-
command: string,
|
|
1172
|
-
): VerificationRunnerBinding | undefined {
|
|
1173
|
-
return bindings.find((binding) => binding.command === command);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
function prepareRunnerBindings(
|
|
1177
|
-
root: string,
|
|
1178
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1179
|
-
): {
|
|
1180
|
-
bindings: VerificationRunnerBinding[];
|
|
1181
|
-
mounts: Array<{ source: string; target: string }>;
|
|
1182
|
-
cleanupRoot?: string;
|
|
1183
|
-
} {
|
|
1184
|
-
if (!bindings.some((binding) => binding.mountSource)) {
|
|
1185
|
-
return {
|
|
1186
|
-
bindings: bindings.map((binding) => ({
|
|
1187
|
-
...binding,
|
|
1188
|
-
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1189
|
-
})),
|
|
1190
|
-
mounts: [],
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
|
|
1194
|
-
try {
|
|
1195
|
-
const mounted = new Map<
|
|
1196
|
-
string,
|
|
1197
|
-
{ source: string; target: string; sandboxTarget: string }
|
|
1198
|
-
>();
|
|
1199
|
-
const prepared = bindings.map((binding) => {
|
|
1200
|
-
if (!binding.mountSource) return { ...binding };
|
|
1201
|
-
const source = path.resolve(binding.mountSource);
|
|
1202
|
-
const sourceStat = lstatSync(source);
|
|
1203
|
-
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
1204
|
-
throw new Error("runner-mount-unavailable");
|
|
1205
|
-
}
|
|
1206
|
-
let mount = mounted.get(source);
|
|
1207
|
-
if (!mount) {
|
|
1208
|
-
const target = path.join(cleanupRoot, String(mounted.size));
|
|
1209
|
-
mkdirSync(target, { mode: 0o700 });
|
|
1210
|
-
const sandboxRelative = path
|
|
1211
|
-
.relative(root, target)
|
|
1212
|
-
.split(path.sep)
|
|
1213
|
-
.join("/");
|
|
1214
|
-
mount = {
|
|
1215
|
-
source,
|
|
1216
|
-
target,
|
|
1217
|
-
sandboxTarget: `/workspace/${sandboxRelative}`,
|
|
1218
|
-
};
|
|
1219
|
-
mounted.set(source, mount);
|
|
1220
|
-
}
|
|
1221
|
-
const executable = realpathSync(binding.executablePath);
|
|
1222
|
-
const relative = path.relative(source, executable);
|
|
1223
|
-
if (
|
|
1224
|
-
relative === "" ||
|
|
1225
|
-
relative.startsWith("..") ||
|
|
1226
|
-
path.isAbsolute(relative)
|
|
1227
|
-
) {
|
|
1228
|
-
throw new Error("runner-mount-unavailable");
|
|
1229
|
-
}
|
|
1230
|
-
return {
|
|
1231
|
-
command: binding.command,
|
|
1232
|
-
executablePath: path.posix.join(
|
|
1233
|
-
mount.sandboxTarget,
|
|
1234
|
-
...relative.split(path.sep),
|
|
1235
|
-
),
|
|
1236
|
-
...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
|
|
1237
|
-
};
|
|
1238
|
-
});
|
|
1239
|
-
return {
|
|
1240
|
-
bindings: prepared,
|
|
1241
|
-
mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
|
|
1242
|
-
source,
|
|
1243
|
-
target: sandboxTarget,
|
|
1244
|
-
})),
|
|
1245
|
-
cleanupRoot,
|
|
1246
|
-
};
|
|
1247
|
-
} catch (error) {
|
|
1248
|
-
rmSync(cleanupRoot, { recursive: true, force: true });
|
|
1249
|
-
throw error;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function verificationInvocation(
|
|
1254
|
-
step: AtomicVerificationContract,
|
|
1255
|
-
bindings: readonly VerificationRunnerBinding[],
|
|
1256
|
-
): { executable: string; args: string[] } | undefined {
|
|
1257
|
-
const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
|
|
1258
|
-
if (step.kind === "package-script") {
|
|
1259
|
-
const binding = bindingFor(bindings, step.packageManager);
|
|
1260
|
-
return binding
|
|
1261
|
-
? {
|
|
1262
|
-
executable: binding.executablePath,
|
|
1263
|
-
args: [
|
|
1264
|
-
...(binding.fixedArgs ?? []),
|
|
1265
|
-
...packageScriptInvocation(
|
|
1266
|
-
step.packageManager,
|
|
1267
|
-
step.script,
|
|
1268
|
-
step.args,
|
|
1269
|
-
),
|
|
1270
|
-
],
|
|
1271
|
-
}
|
|
1272
|
-
: undefined;
|
|
1273
|
-
}
|
|
1274
|
-
const runner = step.runner;
|
|
1275
|
-
if (runner.kind === "package-script") {
|
|
1276
|
-
const binding = bindingFor(bindings, runner.packageManager);
|
|
1277
|
-
const args =
|
|
1278
|
-
step.kind === "vitest"
|
|
1279
|
-
? [...step.testFiles, ...step.args, ...reporter]
|
|
1280
|
-
: step.args;
|
|
1281
|
-
return binding
|
|
1282
|
-
? {
|
|
1283
|
-
executable: binding.executablePath,
|
|
1284
|
-
args: [
|
|
1285
|
-
...(binding.fixedArgs ?? []),
|
|
1286
|
-
...packageScriptInvocation(
|
|
1287
|
-
runner.packageManager,
|
|
1288
|
-
runner.script,
|
|
1289
|
-
args,
|
|
1290
|
-
),
|
|
1291
|
-
],
|
|
1292
|
-
}
|
|
1293
|
-
: undefined;
|
|
1294
|
-
}
|
|
1295
|
-
if (runner.kind === "local-binary") {
|
|
1296
|
-
const binding = bindingFor(bindings, runner.executable);
|
|
1297
|
-
return {
|
|
1298
|
-
executable:
|
|
1299
|
-
binding?.executablePath ??
|
|
1300
|
-
`/workspace/node_modules/.bin/${runner.executable}`,
|
|
1301
|
-
args: [
|
|
1302
|
-
...(binding?.fixedArgs ?? []),
|
|
1303
|
-
...(step.kind === "vitest"
|
|
1304
|
-
? [...step.testFiles, ...step.args, ...reporter]
|
|
1305
|
-
: step.args),
|
|
1306
|
-
],
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
const command = runner.kind === "node" ? "node" : "npx";
|
|
1310
|
-
const binding = bindingFor(bindings, command);
|
|
1311
|
-
if (!binding) return undefined;
|
|
1312
|
-
return {
|
|
1313
|
-
executable: binding.executablePath,
|
|
1314
|
-
args:
|
|
1315
|
-
runner.kind === "node"
|
|
1316
|
-
? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
|
|
1317
|
-
: [
|
|
1318
|
-
...(binding.fixedArgs ?? []),
|
|
1319
|
-
"--no-install",
|
|
1320
|
-
runner.executable,
|
|
1321
|
-
...(step.kind === "vitest" ? step.testFiles : []),
|
|
1322
|
-
...step.args,
|
|
1323
|
-
...reporter,
|
|
1324
|
-
],
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function vitestReport(stdout: string):
|
|
1329
|
-
| {
|
|
1330
|
-
total: number;
|
|
1331
|
-
failed: number;
|
|
1332
|
-
success: boolean;
|
|
1333
|
-
failures: string[];
|
|
1334
|
-
failedText: string[];
|
|
1335
|
-
}
|
|
1336
|
-
| undefined {
|
|
1337
|
-
let value: unknown;
|
|
1338
|
-
try {
|
|
1339
|
-
value = JSON.parse(stdout);
|
|
1340
|
-
} catch {
|
|
1341
|
-
return undefined;
|
|
1342
|
-
}
|
|
1343
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1344
|
-
return undefined;
|
|
1345
|
-
}
|
|
1346
|
-
const report = value as Record<string, unknown>;
|
|
1347
|
-
if (
|
|
1348
|
-
!Number.isSafeInteger(report.numTotalTests) ||
|
|
1349
|
-
!Number.isSafeInteger(report.numFailedTests) ||
|
|
1350
|
-
typeof report.success !== "boolean"
|
|
1351
|
-
) {
|
|
1352
|
-
return undefined;
|
|
1353
|
-
}
|
|
1354
|
-
const failedAssertions = (
|
|
1355
|
-
Array.isArray(report.testResults) ? report.testResults : []
|
|
1356
|
-
).flatMap((suite): Array<{ identity: string; text: string[] }> => {
|
|
1357
|
-
if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
|
|
1358
|
-
const record = suite as Record<string, unknown>;
|
|
1359
|
-
const suiteName =
|
|
1360
|
-
typeof record.name === "string"
|
|
1361
|
-
? record.name.replace(/^\/workspace\//u, "")
|
|
1362
|
-
: "unknown-suite";
|
|
1363
|
-
if (!Array.isArray(record.assertionResults)) return [];
|
|
1364
|
-
return record.assertionResults.flatMap(
|
|
1365
|
-
(assertion): Array<{ identity: string; text: string[] }> => {
|
|
1366
|
-
if (
|
|
1367
|
-
!assertion ||
|
|
1368
|
-
typeof assertion !== "object" ||
|
|
1369
|
-
Array.isArray(assertion)
|
|
1370
|
-
) {
|
|
1371
|
-
return [];
|
|
1372
|
-
}
|
|
1373
|
-
const entry = assertion as Record<string, unknown>;
|
|
1374
|
-
if (entry.status !== "failed") return [];
|
|
1375
|
-
const title =
|
|
1376
|
-
typeof entry.fullName === "string"
|
|
1377
|
-
? entry.fullName
|
|
1378
|
-
: typeof entry.title === "string"
|
|
1379
|
-
? entry.title
|
|
1380
|
-
: "unknown-assertion";
|
|
1381
|
-
const diagnostics = Array.isArray(entry.failureMessages)
|
|
1382
|
-
? entry.failureMessages.filter(
|
|
1383
|
-
(message): message is string => typeof message === "string",
|
|
1384
|
-
)
|
|
1385
|
-
: [];
|
|
1386
|
-
return [
|
|
1387
|
-
{
|
|
1388
|
-
identity: `${suiteName}\0${title}`,
|
|
1389
|
-
text: [title, ...diagnostics],
|
|
1390
|
-
},
|
|
1391
|
-
];
|
|
1392
|
-
},
|
|
1393
|
-
);
|
|
1394
|
-
});
|
|
1395
|
-
return {
|
|
1396
|
-
total: report.numTotalTests as number,
|
|
1397
|
-
failed: report.numFailedTests as number,
|
|
1398
|
-
success: report.success,
|
|
1399
|
-
failures: failedAssertions.map((failure) => failure.identity),
|
|
1400
|
-
failedText: failedAssertions.flatMap((failure) => failure.text),
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
function normalizedFailureIdentities(input: {
|
|
1405
|
-
step: AtomicVerificationContract;
|
|
1406
|
-
report?: ReturnType<typeof vitestReport>;
|
|
1407
|
-
output: string;
|
|
1408
|
-
exitCode: number;
|
|
1409
|
-
}): string[] {
|
|
1410
|
-
const vitestFailures = input.report?.failures ?? [];
|
|
1411
|
-
if (vitestFailures.length > 0) {
|
|
1412
|
-
return [...new Set(vitestFailures)]
|
|
1413
|
-
.sort()
|
|
1414
|
-
.map((failure) => sha256(`vitest-failure\0${failure}`));
|
|
1415
|
-
}
|
|
1416
|
-
const normalizedOutput = input.output
|
|
1417
|
-
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
1418
|
-
.replace(/\r\n?/gu, "\n")
|
|
1419
|
-
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
1420
|
-
.slice(0, 64 * 1024);
|
|
1421
|
-
return [
|
|
1422
|
-
sha256(
|
|
1423
|
-
canonicalJson({
|
|
1424
|
-
verificationId: input.step.id,
|
|
1425
|
-
exitCode: input.exitCode,
|
|
1426
|
-
output: normalizedOutput,
|
|
1427
|
-
}),
|
|
1428
|
-
),
|
|
1429
|
-
];
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
export async function executePackageVerification(input: {
|
|
1433
|
-
root: string;
|
|
1434
|
-
dependencyOwner: string;
|
|
1435
|
-
verification: StructuredVerificationContract;
|
|
1436
|
-
signal: AbortSignal;
|
|
1437
|
-
}) {
|
|
1438
|
-
const capability = bindCurrentVerificationCapability(
|
|
1439
|
-
input.root,
|
|
1440
|
-
input.verification,
|
|
1441
|
-
{ dependencyOwner: input.dependencyOwner },
|
|
1442
|
-
);
|
|
1443
|
-
if (!capability.ok) {
|
|
1444
|
-
return {
|
|
1445
|
-
ok: false as const,
|
|
1446
|
-
kind: "paused" as const,
|
|
1447
|
-
code: capability.diagnostic.code,
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
let runners: ReturnType<typeof prepareRunnerBindings>;
|
|
1451
|
-
try {
|
|
1452
|
-
runners = prepareRunnerBindings(
|
|
1453
|
-
input.root,
|
|
1454
|
-
capability.value.runnerBindings,
|
|
1455
|
-
);
|
|
1456
|
-
} catch {
|
|
1457
|
-
return {
|
|
1458
|
-
ok: false as const,
|
|
1459
|
-
kind: "paused" as const,
|
|
1460
|
-
code: "runner-mount-unavailable",
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
try {
|
|
1464
|
-
const nodeModules = path.join(input.dependencyOwner, "node_modules");
|
|
1465
|
-
const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
|
|
1466
|
-
const mounts = [] as Array<{
|
|
1467
|
-
source: string;
|
|
1468
|
-
target: string;
|
|
1469
|
-
writable?: boolean;
|
|
1470
|
-
}>;
|
|
1471
|
-
if (dependency) {
|
|
1472
|
-
if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
|
|
1473
|
-
return {
|
|
1474
|
-
ok: false as const,
|
|
1475
|
-
kind: "paused" as const,
|
|
1476
|
-
code: "dependency-path-unsafe",
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
const target = path.join(input.root, "node_modules");
|
|
1480
|
-
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
1481
|
-
if (
|
|
1482
|
-
targetStat &&
|
|
1483
|
-
(!targetStat.isDirectory() || targetStat.isSymbolicLink())
|
|
1484
|
-
) {
|
|
1485
|
-
return {
|
|
1486
|
-
ok: false as const,
|
|
1487
|
-
kind: "paused" as const,
|
|
1488
|
-
code: "dependency-path-unsafe",
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
1492
|
-
mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
|
|
1493
|
-
}
|
|
1494
|
-
mounts.push(...runners.mounts);
|
|
1495
|
-
const isolation = new BubblewrapIsolationBackend();
|
|
1496
|
-
const steps = verificationSteps(input.verification);
|
|
1497
|
-
let final:
|
|
1498
|
-
| {
|
|
1499
|
-
exitCode: number;
|
|
1500
|
-
classification: AtomicVerificationContract["classification"];
|
|
1501
|
-
diagnostic: { kind: "assertion" | "compiler"; id: string };
|
|
1502
|
-
}
|
|
1503
|
-
| undefined;
|
|
1504
|
-
for (const step of steps) {
|
|
1505
|
-
const invocation = verificationInvocation(step, runners.bindings);
|
|
1506
|
-
if (!invocation) {
|
|
1507
|
-
return {
|
|
1508
|
-
ok: false as const,
|
|
1509
|
-
kind: "paused" as const,
|
|
1510
|
-
code: "runner-missing",
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
const executed = await isolation.run({
|
|
1514
|
-
root: input.root,
|
|
1515
|
-
executable: invocation.executable,
|
|
1516
|
-
args: invocation.args,
|
|
1517
|
-
mounts,
|
|
1518
|
-
environment: { CI: "1" },
|
|
1519
|
-
signal: input.signal,
|
|
1520
|
-
});
|
|
1521
|
-
if (!executed.ok) {
|
|
1522
|
-
if (executed.state === "cancelled") throw input.signal.reason;
|
|
1523
|
-
return {
|
|
1524
|
-
ok: false as const,
|
|
1525
|
-
kind: "paused" as const,
|
|
1526
|
-
code: executed.code,
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
const report =
|
|
1530
|
-
step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
|
|
1531
|
-
const output = `${executed.stdout}\n${executed.stderr}`;
|
|
1532
|
-
const identity =
|
|
1533
|
-
step.classification !== "expected-red" ||
|
|
1534
|
-
(step.kind === "vitest"
|
|
1535
|
-
? report?.failedText.some((text) =>
|
|
1536
|
-
text.includes(step.expectedFailure ?? ""),
|
|
1537
|
-
) === true
|
|
1538
|
-
: output.includes(step.expectedFailure ?? ""));
|
|
1539
|
-
const accepted =
|
|
1540
|
-
step.classification === "expected-red"
|
|
1541
|
-
? executed.exitCode !== 0 &&
|
|
1542
|
-
identity &&
|
|
1543
|
-
(step.kind !== "vitest" ||
|
|
1544
|
-
(report !== undefined &&
|
|
1545
|
-
report.failed > 0 &&
|
|
1546
|
-
report.total >= step.minTests))
|
|
1547
|
-
: executed.exitCode === 0 &&
|
|
1548
|
-
(step.kind !== "vitest" ||
|
|
1549
|
-
(report?.success === true && report.total >= step.minTests));
|
|
1550
|
-
if (!accepted) {
|
|
1551
|
-
return {
|
|
1552
|
-
ok: false as const,
|
|
1553
|
-
kind: "retryable" as const,
|
|
1554
|
-
code:
|
|
1555
|
-
step.classification === "expected-red" && executed.exitCode === 0
|
|
1556
|
-
? "red-not-witnessed"
|
|
1557
|
-
: "verification-rejected",
|
|
1558
|
-
failureIdentities: normalizedFailureIdentities({
|
|
1559
|
-
step,
|
|
1560
|
-
report,
|
|
1561
|
-
output,
|
|
1562
|
-
exitCode: executed.exitCode,
|
|
1563
|
-
}),
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
|
|
1567
|
-
return {
|
|
1568
|
-
ok: false as const,
|
|
1569
|
-
kind: "retryable" as const,
|
|
1570
|
-
code: "verification-input-unavailable",
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
final = {
|
|
1574
|
-
exitCode: executed.exitCode,
|
|
1575
|
-
classification: step.classification,
|
|
1576
|
-
diagnostic: {
|
|
1577
|
-
kind: step.kind === "vitest" ? "assertion" : "compiler",
|
|
1578
|
-
id: step.id,
|
|
1579
|
-
},
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
return final
|
|
1583
|
-
? { ok: true as const, ...final }
|
|
1584
|
-
: {
|
|
1585
|
-
ok: false as const,
|
|
1586
|
-
kind: "paused" as const,
|
|
1587
|
-
code: "verification-contract-unsupported",
|
|
1588
|
-
};
|
|
1589
|
-
} finally {
|
|
1590
|
-
if (runners.cleanupRoot) {
|
|
1591
|
-
rmSync(runners.cleanupRoot, { recursive: true, force: true });
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
703
|
function affectedVerification(
|
|
1597
704
|
task: Parameters<
|
|
1598
705
|
Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
|
|
@@ -1653,7 +760,39 @@ export function openPackageWorkflowControlEngine(
|
|
|
1653
760
|
if (!implementationAgent) {
|
|
1654
761
|
throw new Error("implementation-worker-agent-unavailable");
|
|
1655
762
|
}
|
|
763
|
+
const hostLimit = (name: string, fallback: number): number => {
|
|
764
|
+
const value =
|
|
765
|
+
process.env[name] === undefined ? fallback : Number(process.env[name]);
|
|
766
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
767
|
+
throw new Error("verification-host-limit-invalid");
|
|
768
|
+
return value;
|
|
769
|
+
};
|
|
770
|
+
const maxReportBytes = hostLimit(
|
|
771
|
+
"ABEL_VERIFICATION_MAX_REPORT_BYTES",
|
|
772
|
+
64 * 1024 * 1024,
|
|
773
|
+
);
|
|
774
|
+
const workHardLimit = hostLimit("ABEL_WORK_MAX_UNITS", 512);
|
|
1656
775
|
const engine = openDurableWorkflowEngine({
|
|
776
|
+
workHardLimit,
|
|
777
|
+
verificationPolicy: "report-file-v4",
|
|
778
|
+
verificationEnvironment: (plan, signal) =>
|
|
779
|
+
captureVerificationEnvironmentIdentity(
|
|
780
|
+
consumerRoot,
|
|
781
|
+
[
|
|
782
|
+
...plan.tasks.flatMap((task) => [
|
|
783
|
+
...Object.values(task.phases).map((phase) => phase.verification),
|
|
784
|
+
task.affectedVerification,
|
|
785
|
+
task.repairVerification,
|
|
786
|
+
]),
|
|
787
|
+
plan.verification.baseline.fullSuite,
|
|
788
|
+
plan.verification.change.fullSuite,
|
|
789
|
+
plan.verification.change.postApply,
|
|
790
|
+
...(plan.verification.agentsCheckpoint.verification
|
|
791
|
+
? [plan.verification.agentsCheckpoint.verification]
|
|
792
|
+
: []),
|
|
793
|
+
],
|
|
794
|
+
signal,
|
|
795
|
+
),
|
|
1657
796
|
consumerRoot,
|
|
1658
797
|
stateRoot,
|
|
1659
798
|
deliverySource: packageDeliverySource(consumerRoot, {
|
|
@@ -1706,10 +845,28 @@ export function openPackageWorkflowControlEngine(
|
|
|
1706
845
|
].sort(),
|
|
1707
846
|
}
|
|
1708
847
|
: {
|
|
1709
|
-
read: [
|
|
848
|
+
read: [
|
|
849
|
+
...new Set(
|
|
850
|
+
taskPhases.flatMap((boundary) => [
|
|
851
|
+
...boundary.read,
|
|
852
|
+
...boundary.write,
|
|
853
|
+
...boundary.delete,
|
|
854
|
+
]),
|
|
855
|
+
),
|
|
856
|
+
].sort(),
|
|
1710
857
|
write: [...phase.write],
|
|
1711
858
|
delete: [...phase.delete],
|
|
1712
859
|
};
|
|
860
|
+
executionBoundary.read = [
|
|
861
|
+
...new Set([
|
|
862
|
+
...executionBoundary.read,
|
|
863
|
+
...(input.contextReadPaths ?? []).filter(
|
|
864
|
+
(relative) =>
|
|
865
|
+
permitsContextRead(relative, input.task.roots) &&
|
|
866
|
+
isSafeRegularFile(input.workspaceRoot, relative),
|
|
867
|
+
),
|
|
868
|
+
]),
|
|
869
|
+
].sort();
|
|
1713
870
|
const phaseContract = {
|
|
1714
871
|
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1715
872
|
taskId: input.taskId,
|
|
@@ -1729,6 +886,9 @@ export function openPackageWorkflowControlEngine(
|
|
|
1729
886
|
...(input.contextRequest
|
|
1730
887
|
? { requestedContext: structuredClone(input.contextRequest) }
|
|
1731
888
|
: {}),
|
|
889
|
+
...(input.recoveryFeedback
|
|
890
|
+
? { recoveryFeedback: structuredClone(input.recoveryFeedback) }
|
|
891
|
+
: {}),
|
|
1732
892
|
...(input.repair
|
|
1733
893
|
? {
|
|
1734
894
|
repair: {
|
|
@@ -1750,6 +910,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1750
910
|
input.task.objective,
|
|
1751
911
|
input.task.context.agents,
|
|
1752
912
|
input.task.context.contract,
|
|
913
|
+
"Use recoveryFeedback to change the failing approach. For compact-patch, prefer exact replace operations and omit unchanged bodies; submit one complete atomic patch. Never repeat an unchanged failing submission or weaken verification to obtain a pass.",
|
|
1753
914
|
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1754
915
|
].join("\n\n"),
|
|
1755
916
|
requestId: childRequestId(input.operationId, input.taskId, input.phase),
|
|
@@ -1785,7 +946,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1785
946
|
return { kind: "approval-needed", code: child.failure.code };
|
|
1786
947
|
}
|
|
1787
948
|
if (child.failure.kind === "result-limit") {
|
|
1788
|
-
return { kind: "
|
|
949
|
+
return { kind: "retryable", code: "needs-task-split" };
|
|
1789
950
|
}
|
|
1790
951
|
const attemptDiagnostic = {
|
|
1791
952
|
finalCategory: child.classification.finalCategory,
|
|
@@ -1804,6 +965,7 @@ export function openPackageWorkflowControlEngine(
|
|
|
1804
965
|
if (result.kind === "context-request") {
|
|
1805
966
|
return classifyCandidateContextRequest(result, {
|
|
1806
967
|
phase: input.phase,
|
|
968
|
+
contextReadRoots: input.task.roots,
|
|
1807
969
|
readPaths: phase.read,
|
|
1808
970
|
writePaths: phase.write,
|
|
1809
971
|
deletePaths: phase.delete,
|
|
@@ -1836,44 +998,37 @@ export function openPackageWorkflowControlEngine(
|
|
|
1836
998
|
paths: [...result.paths],
|
|
1837
999
|
};
|
|
1838
1000
|
},
|
|
1839
|
-
verifyPhase: (input) =>
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1001
|
+
verifyPhase: async (input) =>
|
|
1002
|
+
phaseVerificationResult(
|
|
1003
|
+
await executePackageVerification({
|
|
1004
|
+
maxReportBytes,
|
|
1005
|
+
executionWritePaths: input.executionWritePaths,
|
|
1006
|
+
root: input.root,
|
|
1007
|
+
dependencyOwner: consumerRoot,
|
|
1008
|
+
verification: input.verification,
|
|
1009
|
+
signal: input.signal,
|
|
1010
|
+
}),
|
|
1011
|
+
),
|
|
1846
1012
|
verifyChange: async (input) => {
|
|
1847
1013
|
const verifications = input.verification
|
|
1848
1014
|
? [input.verification]
|
|
1849
1015
|
: input.plan.tasks.map((task) => affectedVerification(task));
|
|
1850
1016
|
for (const verification of verifications) {
|
|
1851
1017
|
const result = await executePackageVerification({
|
|
1018
|
+
maxReportBytes,
|
|
1019
|
+
executionWritePaths: input.plan.tasks.flatMap((task) =>
|
|
1020
|
+
Object.values(task.phases).flatMap((phase) => [
|
|
1021
|
+
...phase.write,
|
|
1022
|
+
...phase.delete,
|
|
1023
|
+
]),
|
|
1024
|
+
),
|
|
1852
1025
|
root: input.root,
|
|
1853
1026
|
dependencyOwner: consumerRoot,
|
|
1854
1027
|
verification,
|
|
1855
1028
|
signal: input.signal,
|
|
1856
1029
|
});
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
"runner-missing",
|
|
1860
|
-
"script-missing",
|
|
1861
|
-
"script-command-mismatch",
|
|
1862
|
-
"local-executable-missing",
|
|
1863
|
-
]).has(result.code);
|
|
1864
|
-
return {
|
|
1865
|
-
ok: false,
|
|
1866
|
-
kind: adapter
|
|
1867
|
-
? ("verification-adapter" as const)
|
|
1868
|
-
: result.kind === "paused"
|
|
1869
|
-
? ("environment" as const)
|
|
1870
|
-
: ("verification" as const),
|
|
1871
|
-
code: result.code,
|
|
1872
|
-
...(result.failureIdentities
|
|
1873
|
-
? { failureIdentities: result.failureIdentities }
|
|
1874
|
-
: {}),
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1030
|
+
const observed = changeVerificationResult(result);
|
|
1031
|
+
if (!observed.ok) return observed;
|
|
1877
1032
|
}
|
|
1878
1033
|
return {
|
|
1879
1034
|
ok: true,
|
|
@@ -1921,6 +1076,11 @@ export function openPackageWorkflowControlEngine(
|
|
|
1921
1076
|
executeDesign(request: unknown) {
|
|
1922
1077
|
return design.execute(request);
|
|
1923
1078
|
},
|
|
1079
|
+
executeAmendment(change: string, batchId: string, request: unknown) {
|
|
1080
|
+
return engine.amend(change, batchId, request, (assertAuthority) =>
|
|
1081
|
+
design.executeWithAuthority(request, assertAuthority),
|
|
1082
|
+
);
|
|
1083
|
+
},
|
|
1924
1084
|
assertDesignRun(runId: string) {
|
|
1925
1085
|
design.assertDesignRun(runId);
|
|
1926
1086
|
},
|
|
@@ -1987,8 +1147,11 @@ export function registerWorkflowControl(
|
|
|
1987
1147
|
const parentPayloadBridge = new ParentPayloadBridge();
|
|
1988
1148
|
const activity = new ActivityController();
|
|
1989
1149
|
const engines = new Map<string, Promise<WorkflowControlEngine>>();
|
|
1150
|
+
const designOperations = new Set<Promise<void>>();
|
|
1990
1151
|
const designFailures = new Map<string, SafeDesignFailure>();
|
|
1991
1152
|
let pendingPrompt: EligiblePrompt | undefined;
|
|
1153
|
+
let pendingInit = false;
|
|
1154
|
+
let exitingStage = false;
|
|
1992
1155
|
let activePrompt: EligiblePrompt | undefined;
|
|
1993
1156
|
let designToolSnapshot: string[] | undefined;
|
|
1994
1157
|
const activation = new (class implements Activation {
|
|
@@ -2030,16 +1193,19 @@ export function registerWorkflowControl(
|
|
|
2030
1193
|
return opened;
|
|
2031
1194
|
};
|
|
2032
1195
|
const closeEngines = async () => {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
await Promise.
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
1196
|
+
// Design close() is synchronous and does not own an operation queue.
|
|
1197
|
+
// Keep its journal open through finalization's commit/lease-cleanup path
|
|
1198
|
+
// and the dispatch result handling before closing any engine storage.
|
|
1199
|
+
await Promise.all([...designOperations]);
|
|
1200
|
+
const closed = await Promise.allSettled(
|
|
1201
|
+
[...engines].map(async ([key, opened]) => {
|
|
1202
|
+
const engine = await opened;
|
|
1203
|
+
await engine.close();
|
|
1204
|
+
if (engines.get(key) === opened) engines.delete(key);
|
|
1205
|
+
}),
|
|
2042
1206
|
);
|
|
1207
|
+
const failed = closed.find((result) => result.status === "rejected");
|
|
1208
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
2043
1209
|
};
|
|
2044
1210
|
const restoreDesignTools = () => {
|
|
2045
1211
|
if (!designToolSnapshot) return;
|
|
@@ -2077,6 +1243,16 @@ export function registerWorkflowControl(
|
|
|
2077
1243
|
deactivate();
|
|
2078
1244
|
parentPayloadBridge.clear();
|
|
2079
1245
|
};
|
|
1246
|
+
const exitStage = async () => {
|
|
1247
|
+
if (exitingStage) throw new Error("stage-control-mismatch");
|
|
1248
|
+
exitingStage = true;
|
|
1249
|
+
try {
|
|
1250
|
+
await closeEngines();
|
|
1251
|
+
await deactivateStage();
|
|
1252
|
+
} finally {
|
|
1253
|
+
exitingStage = false;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
2080
1256
|
|
|
2081
1257
|
// Pi validates tool arguments before execute(), so expose only the schema
|
|
2082
1258
|
// legal for the active stage instead of one ambiguous command/packet union.
|
|
@@ -2102,6 +1278,61 @@ export function registerWorkflowControl(
|
|
|
2102
1278
|
} as const;
|
|
2103
1279
|
let registeredParameterKind: "command" | "packet" | undefined;
|
|
2104
1280
|
|
|
1281
|
+
// Stage exit is session control, not a durable Implement command. Keep the
|
|
1282
|
+
// six engine commands unchanged and admit only this exact extra envelope.
|
|
1283
|
+
const FINISH_PARAMETERS = {
|
|
1284
|
+
type: "object",
|
|
1285
|
+
properties: { action: { type: "string", enum: ["finish"] } },
|
|
1286
|
+
required: ["action"],
|
|
1287
|
+
additionalProperties: false,
|
|
1288
|
+
} as const;
|
|
1289
|
+
const AMEND_PARAMETERS = {
|
|
1290
|
+
type: "object",
|
|
1291
|
+
properties: {
|
|
1292
|
+
action: { type: "string", enum: ["amend"] },
|
|
1293
|
+
change: { type: "string" },
|
|
1294
|
+
batchId: { type: "string" },
|
|
1295
|
+
request: DESIGN_CONTROL_REQUEST_SCHEMA,
|
|
1296
|
+
},
|
|
1297
|
+
required: ["action", "change", "batchId", "request"],
|
|
1298
|
+
additionalProperties: false,
|
|
1299
|
+
} as const;
|
|
1300
|
+
const IMPLEMENT_PARAMETERS = {
|
|
1301
|
+
...CONTROL_COMMAND_PARAMETERS,
|
|
1302
|
+
properties: {
|
|
1303
|
+
...CONTROL_COMMAND_PARAMETERS.properties,
|
|
1304
|
+
...AMEND_PARAMETERS.properties,
|
|
1305
|
+
action: { type: "string", enum: ["finish", "amend"] },
|
|
1306
|
+
},
|
|
1307
|
+
required: [],
|
|
1308
|
+
anyOf: [
|
|
1309
|
+
...CONTROL_COMMAND_PARAMETERS.anyOf,
|
|
1310
|
+
FINISH_PARAMETERS,
|
|
1311
|
+
AMEND_PARAMETERS,
|
|
1312
|
+
],
|
|
1313
|
+
} as const;
|
|
1314
|
+
const prepareImplementArguments = (args: unknown): unknown => {
|
|
1315
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
1316
|
+
const record = { ...(args as Record<string, unknown>) };
|
|
1317
|
+
// Strict providers can pad fields belonging to the other union branch.
|
|
1318
|
+
if (record.action !== "amend") {
|
|
1319
|
+
for (const key of ["batchId", "request"]) {
|
|
1320
|
+
if (record[key] === null || record[key] === undefined)
|
|
1321
|
+
delete record[key];
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (record.action === "finish" || record.action === "amend") {
|
|
1325
|
+
for (const key of Object.keys(CONTROL_COMMAND_PARAMETERS.properties)) {
|
|
1326
|
+
if (record[key] === null || record[key] === undefined)
|
|
1327
|
+
delete record[key];
|
|
1328
|
+
}
|
|
1329
|
+
return record;
|
|
1330
|
+
}
|
|
1331
|
+
if (record.action === null || record.action === undefined)
|
|
1332
|
+
delete record.action;
|
|
1333
|
+
return canonicalizeControlCommandToolInput(record);
|
|
1334
|
+
};
|
|
1335
|
+
|
|
2105
1336
|
const registerDispatchTool = (kind: "command" | "packet") => {
|
|
2106
1337
|
if (registeredParameterKind === kind) return;
|
|
2107
1338
|
registeredParameterKind = kind;
|
|
@@ -2110,17 +1341,16 @@ export function registerWorkflowControl(
|
|
|
2110
1341
|
label: "Abel Control",
|
|
2111
1342
|
description:
|
|
2112
1343
|
kind === "command"
|
|
2113
|
-
?
|
|
1344
|
+
? 'Private stage-bound Abel workflow control. Accepts durable Implement commands, batch-bound {"action":"amend"} artifact revisions, or {"action":"finish"} to leave and preserve resumable work.'
|
|
2114
1345
|
: "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
|
|
2115
1346
|
executionMode: "parallel",
|
|
2116
1347
|
...(kind === "command"
|
|
2117
1348
|
? {
|
|
2118
1349
|
prepareArguments: (args: unknown) =>
|
|
2119
|
-
|
|
1350
|
+
prepareImplementArguments(args) as never,
|
|
2120
1351
|
}
|
|
2121
1352
|
: {}),
|
|
2122
|
-
parameters:
|
|
2123
|
-
kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
|
|
1353
|
+
parameters: kind === "command" ? IMPLEMENT_PARAMETERS : PACKET_PARAMETERS,
|
|
2124
1354
|
async execute(
|
|
2125
1355
|
toolCallId: string,
|
|
2126
1356
|
params: unknown,
|
|
@@ -2128,10 +1358,120 @@ export function registerWorkflowControl(
|
|
|
2128
1358
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
2129
1359
|
ctx: ExtensionContext,
|
|
2130
1360
|
) {
|
|
1361
|
+
if (
|
|
1362
|
+
exitingStage ||
|
|
1363
|
+
!activation.isActive() ||
|
|
1364
|
+
activePrompt === undefined
|
|
1365
|
+
) {
|
|
1366
|
+
throw new Error("stage-control-mismatch");
|
|
1367
|
+
}
|
|
1368
|
+
if (kind === "command") params = prepareImplementArguments(params);
|
|
2131
1369
|
const record =
|
|
2132
1370
|
params && typeof params === "object" && !Array.isArray(params)
|
|
2133
1371
|
? (params as Record<string, unknown>)
|
|
2134
1372
|
: undefined;
|
|
1373
|
+
if (record?.action === "amend") {
|
|
1374
|
+
if (
|
|
1375
|
+
activePrompt !== "abel-implement" ||
|
|
1376
|
+
Object.keys(record).some(
|
|
1377
|
+
(key) =>
|
|
1378
|
+
!["action", "change", "batchId", "request"].includes(key),
|
|
1379
|
+
) ||
|
|
1380
|
+
typeof record.change !== "string" ||
|
|
1381
|
+
typeof record.batchId !== "string"
|
|
1382
|
+
) {
|
|
1383
|
+
throw new Error("stage-control-mismatch");
|
|
1384
|
+
}
|
|
1385
|
+
const parsed = validateDesignControlRequest(record.request);
|
|
1386
|
+
if (!parsed.ok) throw new Error(parsed.code);
|
|
1387
|
+
const request = parsed.value;
|
|
1388
|
+
if (
|
|
1389
|
+
request.operation === "bind-change" ||
|
|
1390
|
+
(request.operation === "start" &&
|
|
1391
|
+
(!("change" in request) || request.change !== record.change))
|
|
1392
|
+
) {
|
|
1393
|
+
throw new Error("amendment-change-mismatch");
|
|
1394
|
+
}
|
|
1395
|
+
let settle!: () => void;
|
|
1396
|
+
const pending = new Promise<void>((resolve) => {
|
|
1397
|
+
settle = resolve;
|
|
1398
|
+
});
|
|
1399
|
+
designOperations.add(pending);
|
|
1400
|
+
try {
|
|
1401
|
+
const engine = await engineFor(ctx);
|
|
1402
|
+
if (exitingStage || !activation.isActive() || !engine.executeDesign)
|
|
1403
|
+
throw new Error("stage-control-mismatch");
|
|
1404
|
+
const status = await engine.execute(
|
|
1405
|
+
{
|
|
1406
|
+
command: "status",
|
|
1407
|
+
stage: "abel-implement",
|
|
1408
|
+
change: record.change,
|
|
1409
|
+
},
|
|
1410
|
+
ctx,
|
|
1411
|
+
signal,
|
|
1412
|
+
);
|
|
1413
|
+
const batch = status.decisionBatch as { id?: string } | undefined;
|
|
1414
|
+
if (
|
|
1415
|
+
!["approval-needed", "paused"].includes(String(status.state)) ||
|
|
1416
|
+
batch?.id !== record.batchId
|
|
1417
|
+
)
|
|
1418
|
+
throw new Error("amendment-batch-stale");
|
|
1419
|
+
if (request.operation !== "start") {
|
|
1420
|
+
const designStatus = await engine.executeDesign({
|
|
1421
|
+
operation: "status",
|
|
1422
|
+
runId: request.runId,
|
|
1423
|
+
});
|
|
1424
|
+
if (designStatus.change !== record.change)
|
|
1425
|
+
throw new Error("amendment-change-mismatch");
|
|
1426
|
+
}
|
|
1427
|
+
if (!engine.executeAmendment)
|
|
1428
|
+
throw new Error("amendment-control-unavailable");
|
|
1429
|
+
const revision = await engine.executeAmendment(
|
|
1430
|
+
record.change,
|
|
1431
|
+
record.batchId,
|
|
1432
|
+
request,
|
|
1433
|
+
);
|
|
1434
|
+
const payload =
|
|
1435
|
+
request.operation === "finalize-delivery" &&
|
|
1436
|
+
revision.state === "completed"
|
|
1437
|
+
? {
|
|
1438
|
+
...revision,
|
|
1439
|
+
scope: "amendment",
|
|
1440
|
+
state: "ready",
|
|
1441
|
+
completed: false,
|
|
1442
|
+
runId: status.runId,
|
|
1443
|
+
amendmentRunId: revision.runId,
|
|
1444
|
+
stage: "abel-implement",
|
|
1445
|
+
}
|
|
1446
|
+
: { ...revision, scope: "amendment" };
|
|
1447
|
+
// A compiled amendment stays in Implement; its private Design
|
|
1448
|
+
// records do not activate the Design stage or broaden parent tools.
|
|
1449
|
+
return {
|
|
1450
|
+
content: [
|
|
1451
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1452
|
+
],
|
|
1453
|
+
details: payload,
|
|
1454
|
+
};
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (
|
|
1457
|
+
error instanceof DesignPlanValidationError ||
|
|
1458
|
+
error instanceof DesignFinalizationError ||
|
|
1459
|
+
error instanceof DeliveryValidationError
|
|
1460
|
+
) {
|
|
1461
|
+
const failure = safeDesignFailure(
|
|
1462
|
+
error,
|
|
1463
|
+
request.operation,
|
|
1464
|
+
"execution",
|
|
1465
|
+
);
|
|
1466
|
+
designFailures.set(toolCallId, failure);
|
|
1467
|
+
throw new Error(failure.code);
|
|
1468
|
+
}
|
|
1469
|
+
throw error;
|
|
1470
|
+
} finally {
|
|
1471
|
+
settle();
|
|
1472
|
+
designOperations.delete(pending);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
2135
1475
|
if (record && typeof record.action === "string") {
|
|
2136
1476
|
if (
|
|
2137
1477
|
Object.keys(record).some(
|
|
@@ -2143,6 +1483,18 @@ export function registerWorkflowControl(
|
|
|
2143
1483
|
if (record.action === "finish" && Object.hasOwn(record, "request")) {
|
|
2144
1484
|
throw new Error("control-envelope-ambiguous");
|
|
2145
1485
|
}
|
|
1486
|
+
if (record.action === "finish") {
|
|
1487
|
+
// close() aborts and settles active operations before ordinary tools
|
|
1488
|
+
// return; durable work is retained, never discarded or completed.
|
|
1489
|
+
await exitStage();
|
|
1490
|
+
const payload = { ok: true, state: "inactive" };
|
|
1491
|
+
return {
|
|
1492
|
+
content: [
|
|
1493
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1494
|
+
],
|
|
1495
|
+
details: payload,
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
2146
1498
|
if (
|
|
2147
1499
|
activePrompt !== "abel-design" &&
|
|
2148
1500
|
activePrompt !== "abel-diagnose"
|
|
@@ -2155,9 +1507,18 @@ export function registerWorkflowControl(
|
|
|
2155
1507
|
}
|
|
2156
1508
|
const designRequest = validateDesignControlRequest(record.request);
|
|
2157
1509
|
if (!designRequest.ok) throw new Error(designRequest.code);
|
|
1510
|
+
let settleDesign!: () => void;
|
|
1511
|
+
const settled = new Promise<void>((resolve) => {
|
|
1512
|
+
settleDesign = resolve;
|
|
1513
|
+
});
|
|
1514
|
+
// Register before the first await, including asynchronous engine
|
|
1515
|
+
// opening. Teardown blocks admission before taking its snapshot.
|
|
1516
|
+
designOperations.add(settled);
|
|
2158
1517
|
let boundary: DesignFailureBoundary = "initialization";
|
|
2159
1518
|
try {
|
|
2160
1519
|
const engine = await engineFor(ctx);
|
|
1520
|
+
if (exitingStage || !activation.isActive())
|
|
1521
|
+
throw new Error("stage-control-mismatch");
|
|
2161
1522
|
if (!engine.executeDesign)
|
|
2162
1523
|
throw new Error("design-control-unavailable");
|
|
2163
1524
|
boundary = "execution";
|
|
@@ -2170,7 +1531,9 @@ export function registerWorkflowControl(
|
|
|
2170
1531
|
boundary = "execution";
|
|
2171
1532
|
if (
|
|
2172
1533
|
designRequest.value.operation === "finalize-delivery" &&
|
|
2173
|
-
payload.state === "completed"
|
|
1534
|
+
payload.state === "completed" &&
|
|
1535
|
+
!exitingStage &&
|
|
1536
|
+
activePrompt === "abel-design"
|
|
2174
1537
|
) {
|
|
2175
1538
|
await deactivateStage();
|
|
2176
1539
|
}
|
|
@@ -2188,6 +1551,9 @@ export function registerWorkflowControl(
|
|
|
2188
1551
|
const sanitized = new Error(failure.code);
|
|
2189
1552
|
sanitized.name = "DesignControlError";
|
|
2190
1553
|
throw sanitized;
|
|
1554
|
+
} finally {
|
|
1555
|
+
designOperations.delete(settled);
|
|
1556
|
+
settleDesign();
|
|
2191
1557
|
}
|
|
2192
1558
|
}
|
|
2193
1559
|
let designEngine: WorkflowControlEngine | undefined;
|
|
@@ -2201,6 +1567,8 @@ export function registerWorkflowControl(
|
|
|
2201
1567
|
if (packet.value.stage === "abel-design") {
|
|
2202
1568
|
try {
|
|
2203
1569
|
designEngine = await engineFor(ctx);
|
|
1570
|
+
if (exitingStage || !activation.isActive())
|
|
1571
|
+
throw new Error("stage-control-mismatch");
|
|
2204
1572
|
} catch (error) {
|
|
2205
1573
|
if (error instanceof RunStoreFormatError)
|
|
2206
1574
|
throw runStoreResetError(error);
|
|
@@ -2257,9 +1625,6 @@ export function registerWorkflowControl(
|
|
|
2257
1625
|
});
|
|
2258
1626
|
payload = { ...packetPayload, recordedEvidence };
|
|
2259
1627
|
}
|
|
2260
|
-
if (record.action === "finish" && packetPayload.ok) {
|
|
2261
|
-
await deactivateStage();
|
|
2262
|
-
}
|
|
2263
1628
|
const display = tuiRun
|
|
2264
1629
|
? activity.finalize(toolCallId, payload)
|
|
2265
1630
|
: undefined;
|
|
@@ -2287,15 +1652,16 @@ export function registerWorkflowControl(
|
|
|
2287
1652
|
throw error;
|
|
2288
1653
|
}
|
|
2289
1654
|
if (
|
|
2290
|
-
activePrompt !==
|
|
2291
|
-
|
|
2292
|
-
validation.value.stage !== activePrompt)
|
|
1655
|
+
activePrompt !== "abel-implement" ||
|
|
1656
|
+
validation.value.stage !== activePrompt
|
|
2293
1657
|
) {
|
|
2294
1658
|
throw new Error("stage-control-mismatch");
|
|
2295
1659
|
}
|
|
2296
1660
|
let engine: WorkflowControlEngine;
|
|
2297
1661
|
try {
|
|
2298
1662
|
engine = await engineFor(ctx);
|
|
1663
|
+
if (exitingStage || !activation.isActive())
|
|
1664
|
+
throw new Error("stage-control-mismatch");
|
|
2299
1665
|
} catch (error) {
|
|
2300
1666
|
if (!(error instanceof RunStoreFormatError)) throw error;
|
|
2301
1667
|
const payload = runStoreUnavailableStatus(error, validation.value);
|
|
@@ -2392,7 +1758,10 @@ export function registerWorkflowControl(
|
|
|
2392
1758
|
if (
|
|
2393
1759
|
event.toolName !== DISPATCH_TOOL ||
|
|
2394
1760
|
!event.isError ||
|
|
2395
|
-
event.input.action !== "design"
|
|
1761
|
+
(event.input.action !== "design" &&
|
|
1762
|
+
!(
|
|
1763
|
+
event.input.action === "amend" && designFailures.has(event.toolCallId)
|
|
1764
|
+
))
|
|
2396
1765
|
) {
|
|
2397
1766
|
return;
|
|
2398
1767
|
}
|
|
@@ -2422,12 +1791,35 @@ export function registerWorkflowControl(
|
|
|
2422
1791
|
};
|
|
2423
1792
|
});
|
|
2424
1793
|
pi.on("input", (event) => {
|
|
2425
|
-
pendingPrompt =
|
|
1794
|
+
pendingPrompt = undefined;
|
|
1795
|
+
pendingInit = false;
|
|
1796
|
+
const prompt = invokedPrompt(event.text);
|
|
1797
|
+
const init = /^\/abel-init(?:\s|$)/u.test(event.text);
|
|
1798
|
+
if (event.source !== "interactive" && event.source !== "rpc") {
|
|
1799
|
+
// Stop before Pi expands the template: hiding dispatch alone would still
|
|
1800
|
+
// expose the complete workflow instructions to the parent model.
|
|
1801
|
+
return { action: prompt || init ? "handled" : "continue" };
|
|
1802
|
+
}
|
|
1803
|
+
pendingPrompt = prompt;
|
|
1804
|
+
pendingInit = init && hasPackageProvenance(pi, "abel-init");
|
|
1805
|
+
if (
|
|
1806
|
+
activePrompt &&
|
|
1807
|
+
(pendingInit ||
|
|
1808
|
+
(prompt && prompt !== activePrompt && hasPackageProvenance(pi, prompt)))
|
|
1809
|
+
) {
|
|
1810
|
+
return (async () => {
|
|
1811
|
+
await exitStage();
|
|
1812
|
+
return { action: "continue" as const };
|
|
1813
|
+
})();
|
|
1814
|
+
}
|
|
2426
1815
|
return { action: "continue" };
|
|
2427
1816
|
});
|
|
2428
1817
|
pi.on("before_agent_start", (event, ctx) => {
|
|
2429
1818
|
const prompt = pendingPrompt;
|
|
1819
|
+
const init =
|
|
1820
|
+
pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
|
|
2430
1821
|
pendingPrompt = undefined;
|
|
1822
|
+
pendingInit = false;
|
|
2431
1823
|
const verified =
|
|
2432
1824
|
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
2433
1825
|
if (verified) {
|
|
@@ -2448,6 +1840,12 @@ export function registerWorkflowControl(
|
|
|
2448
1840
|
}
|
|
2449
1841
|
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
2450
1842
|
if (verified && prompt === "abel-design") enforceDesignTools();
|
|
1843
|
+
const boundary = activePrompt
|
|
1844
|
+
? `Abel stage ${activePrompt} is active only for the invoked task and its direct follow-ups. If the user ends it or requests an unrelated task, first call abel_dispatch with {"action":"finish"}, then handle that task normally with the restored tools. A successful finish ends stage authority immediately, including within this turn. Do not extend Gates or workflow rules to that task. A direct Gate answer or same-task continuation stays in this stage. Never invoke another Abel stage automatically.`
|
|
1845
|
+
: init
|
|
1846
|
+
? "Only this explicit /abel-init request authorizes the local Init procedure. Do not activate dispatch or continue into another Abel stage."
|
|
1847
|
+
: "Abel workflow is inactive. Handle ordinary engineering requests directly. References to commands, repository files, OpenSpec changes, and historical workflow instructions do not authorize a workflow. Do not load or execute an Abel stage unless the user explicitly invokes its slash command.";
|
|
1848
|
+
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${boundary}` };
|
|
2451
1849
|
});
|
|
2452
1850
|
pi.on("model_select", (event, ctx) => {
|
|
2453
1851
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
@@ -2473,6 +1871,7 @@ export function registerWorkflowControl(
|
|
|
2473
1871
|
});
|
|
2474
1872
|
pi.on("session_start", async (_event, ctx) => {
|
|
2475
1873
|
pendingPrompt = undefined;
|
|
1874
|
+
pendingInit = false;
|
|
2476
1875
|
activePrompt = undefined;
|
|
2477
1876
|
designFailures.clear();
|
|
2478
1877
|
activity.detach();
|