@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.
Files changed (45) hide show
  1. package/README.md +115 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +69 -11
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +380 -981
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/package-delivery.ts +486 -0
  23. package/src/package-verification.ts +509 -0
  24. package/src/route-policy.ts +1 -11
  25. package/src/run-state.ts +4 -16
  26. package/src/run-store.ts +5 -144
  27. package/src/scoped-grep-worker.mjs +23 -0
  28. package/src/scoped-tools.ts +312 -116
  29. package/src/sqlite-schema.ts +171 -0
  30. package/src/storage-schema.ts +352 -0
  31. package/src/subagent-activity.ts +14 -1
  32. package/src/submit-tool.ts +20 -2
  33. package/src/task-ledger.ts +14 -55
  34. package/src/verification-capability.ts +298 -102
  35. package/src/verification-environment.ts +213 -0
  36. package/src/verification-identity.ts +77 -0
  37. package/src/worker-broker.ts +97 -34
  38. package/src/workflow-engine.ts +28 -8285
  39. package/src/workflow-policy.ts +1427 -0
  40. package/src/workflow-state-machine.ts +4478 -0
  41. package/src/workspace-io-entry.mjs +58 -0
  42. package/src/workspace-io-worker.mjs +984 -0
  43. package/src/workspace-io.ts +84 -0
  44. package/src/workspace-store.ts +125 -19
  45. 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, hashCanonicalValue } from "./implement-graph.ts";
59
- import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
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 { observeSafePath } from "./safe-path.ts";
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
- bindCurrentVerificationCapability,
87
- isVerificationCapabilityCurrent,
88
- type VerificationRunnerBinding,
89
- } from "./verification-capability.ts";
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
- "Complete approved WHAT contract; code hashes it and never persists the text.",
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
- function promptMarker(name: EligiblePrompt): string {
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: EligiblePrompt): boolean {
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: EligiblePrompt,
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.localeCompare(right)),
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).localeCompare(canonicalJson(right)),
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: [...phase.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: "paused", code: "needs-task-split" };
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
- executePackageVerification({
1841
- root: input.root,
1842
- dependencyOwner: consumerRoot,
1843
- verification: input.verification,
1844
- signal: input.signal,
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
- if (!result.ok) {
1858
- const adapter = new Set([
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
- const current = [...engines.values()];
2034
- engines.clear();
2035
- const opened = await Promise.allSettled(current);
2036
- await Promise.allSettled(
2037
- opened.flatMap((result) =>
2038
- result.status === "fulfilled"
2039
- ? [Promise.resolve(result.value.close())]
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
- ? "Private stage-bound Abel workflow control. Accepts durable Implement change commands."
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
- canonicalizeControlCommandToolInput(args) as never,
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 !== undefined &&
2291
- (activePrompt === "abel-diagnose" ||
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 = invokedPrompt(event.text);
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();