@abelxiaoxing/cadence 1.2.4 → 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 (46) hide show
  1. package/README.md +155 -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 +83 -15
  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 +399 -1079
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/openspec-cli.ts +494 -0
  23. package/src/package-delivery.ts +486 -0
  24. package/src/package-verification.ts +509 -0
  25. package/src/route-policy.ts +1 -11
  26. package/src/run-state.ts +4 -16
  27. package/src/run-store.ts +5 -144
  28. package/src/scoped-grep-worker.mjs +23 -0
  29. package/src/scoped-tools.ts +312 -116
  30. package/src/sqlite-schema.ts +171 -0
  31. package/src/storage-schema.ts +352 -0
  32. package/src/subagent-activity.ts +14 -1
  33. package/src/submit-tool.ts +20 -2
  34. package/src/task-ledger.ts +14 -55
  35. package/src/verification-capability.ts +298 -102
  36. package/src/verification-environment.ts +213 -0
  37. package/src/verification-identity.ts +77 -0
  38. package/src/worker-broker.ts +97 -34
  39. package/src/workflow-engine.ts +28 -8285
  40. package/src/workflow-policy.ts +1427 -0
  41. package/src/workflow-state-machine.ts +4478 -0
  42. package/src/workspace-io-entry.mjs +58 -0
  43. package/src/workspace-io-worker.mjs +984 -0
  44. package/src/workspace-io.ts +84 -0
  45. package/src/workspace-store.ts +125 -19
  46. package/skills/abel-workflow/SKILL.md +0 -205
package/src/index.ts CHANGED
@@ -1,23 +1,29 @@
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
4
20
  // workflow routing (abel-design/implement/diagnose provenance) in the prompts
5
21
  // integration; abel-init and ordinary prompts never activate dispatch.
6
22
  import { AsyncLocalStorage } from "node:async_hooks";
7
- import { execFile } from "node:child_process";
8
23
  import { createHash } from "node:crypto";
9
- import {
10
- lstatSync,
11
- mkdirSync,
12
- mkdtempSync,
13
- readFileSync,
14
- realpathSync,
15
- rmSync,
16
- } from "node:fs";
17
24
  import { homedir } from "node:os";
18
25
  import path, { dirname, join } from "node:path";
19
26
  import { fileURLToPath } from "node:url";
20
- import { promisify } from "node:util";
21
27
  import type {
22
28
  AgentToolResult,
23
29
  AgentToolUpdateCallback,
@@ -30,12 +36,9 @@ import { type Activation, activateTool, deactivateTool } from "./activation.ts";
30
36
  import { loadAgentDefinitions } from "./agent-registry.ts";
31
37
  import { runChildSession } from "./child-session.ts";
32
38
  import {
33
- type AtomicVerificationContract,
34
39
  type DesignEvidenceResult,
35
- isValidRelativePath,
36
40
  LIMITS,
37
41
  type StructuredVerificationContract,
38
- verificationSteps,
39
42
  } from "./contracts.ts";
40
43
  import {
41
44
  CONTROL_COMMAND_PARAMETERS,
@@ -43,22 +46,16 @@ import {
43
46
  validateControlCommand,
44
47
  } from "./control-contracts.ts";
45
48
  import {
46
- assessDeliveryTraceability,
47
- compileImplementPlan,
48
49
  DeliveryValidationError,
49
50
  DesignPlanValidationError,
50
- type GateApprovalProof,
51
- parseGateAReceipt,
52
- parseImplementPlan,
53
- parseReadyReceipt,
54
51
  } from "./delivery-compiler.ts";
55
52
  import {
56
53
  DesignController,
57
54
  DesignFinalizationError,
58
55
  validateDesignControlRequest,
59
56
  } from "./design-control.ts";
60
- import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
61
- import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
57
+ import { canonicalJson } from "./implement-graph.ts";
58
+ import { inspectOpenSpecDelivery } from "./openspec-cli.ts";
62
59
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
63
60
  import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
64
61
  import { runtimeForWorkerRoute } from "./parent-provider.ts";
@@ -70,7 +67,7 @@ import {
70
67
  type WorkerRoutePolicy,
71
68
  } from "./route-policy.ts";
72
69
  import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
73
- import { observeSafePath } from "./safe-path.ts";
70
+ import { isSafeRegularFile } from "./safe-path.ts";
74
71
  import { resolveStateRoot, StateRootError } from "./state-root.ts";
75
72
  import {
76
73
  ACTIVITY_DETAILS_KEY,
@@ -79,17 +76,16 @@ import {
79
76
  renderActivityResult,
80
77
  type WorkflowActivityUpdate,
81
78
  } from "./subagent-activity.ts";
82
- import { classifyCandidateContextRequest } from "./submit-tool.ts";
83
- import {
84
- bindCurrentVerificationCapability,
85
- isVerificationCapabilityCurrent,
86
- type VerificationRunnerBinding,
87
- } from "./verification-capability.ts";
88
79
  import {
89
- openDurableWorkflowEngine,
90
- type WorkflowAvailableDelivery,
91
- type WorkflowDeliverySource,
92
- } from "./workflow-engine.ts";
80
+ classifyCandidateContextRequest,
81
+ permitsContextRead,
82
+ } from "./submit-tool.ts";
83
+ import { openDurableWorkflowEngine } from "./workflow-engine.ts";
84
+
85
+ export {
86
+ inspectOpenSpecDelivery,
87
+ type OpenSpecDeliveryInspection,
88
+ } from "./openspec-cli.ts";
93
89
 
94
90
  export const DISPATCH_TOOL = "abel_dispatch";
95
91
  const DESIGN_PARENT_READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
@@ -286,9 +282,9 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
286
282
  operationId: { type: "string" },
287
283
  gate: { type: "string", enum: ["gate-a"] },
288
284
  contract: {
289
- type: "string",
285
+ anyOf: [{ type: "string" }, { type: "object" }],
290
286
  description:
291
- "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.",
292
288
  },
293
289
  },
294
290
  required: ["operation", "runId", "operationId", "gate", "contract"],
@@ -356,11 +352,13 @@ function invokedPrompt(text: string): EligiblePrompt | undefined {
356
352
  return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
357
353
  }
358
354
 
359
- function promptMarker(name: EligiblePrompt): string {
355
+ type WorkflowPrompt = EligiblePrompt | "abel-init";
356
+
357
+ function promptMarker(name: WorkflowPrompt): string {
360
358
  return `<!-- ABEL:PROMPT:${name} -->`;
361
359
  }
362
360
 
363
- function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
361
+ function hasPackageProvenance(pi: ExtensionAPI, name: WorkflowPrompt): boolean {
364
362
  const commands = pi
365
363
  .getCommands()
366
364
  .filter((candidate) => candidate.name === name);
@@ -375,7 +373,7 @@ function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
375
373
 
376
374
  function hasExpandedPromptMarker(
377
375
  prompt: string,
378
- name: EligiblePrompt,
376
+ name: WorkflowPrompt,
379
377
  ): boolean {
380
378
  const requestEnd = prompt.lastIndexOf("</abel-request>");
381
379
  if (requestEnd < 0) return false;
@@ -589,7 +587,16 @@ function safeDesignFailure(
589
587
  error instanceof DesignFinalizationError ||
590
588
  error instanceof DesignPlanValidationError ||
591
589
  error instanceof DeliveryValidationError
592
- ? error.diagnostics
590
+ ? error instanceof DesignFinalizationError
591
+ ? [
592
+ ...error.diagnostics.filter(
593
+ (code) =>
594
+ !error.openSpecDiagnostic ||
595
+ code !== "design-openspec-unavailable",
596
+ ),
597
+ ...(error.openSpecDiagnostic ? [error.openSpecDiagnostic] : []),
598
+ ]
599
+ : error.diagnostics
593
600
  : [];
594
601
  const diagnostics = rawDiagnostics
595
602
  .flatMap((candidate) => {
@@ -625,17 +632,21 @@ function safeDesignFailure(
625
632
  "dependencyTaskId",
626
633
  "producerTaskId",
627
634
  "producerPhase",
635
+ "command",
636
+ "reason",
637
+ "systemCode",
638
+ "exitCode",
628
639
  ].includes(entry[0]) &&
629
640
  typeof entry[1] === "string" &&
630
641
  SAFE_DESIGN_ERROR_CODE.test(entry[1])
631
642
  );
632
643
  })
633
- .sort(([left], [right]) => left.localeCompare(right)),
644
+ .sort(([left], [right]) => compareCanonicalStrings(left, right)),
634
645
  );
635
646
  return typeof admitted.code === "string" ? [admitted] : [];
636
647
  })
637
648
  .sort((left, right) =>
638
- canonicalJson(left).localeCompare(canonicalJson(right)),
649
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
639
650
  )
640
651
  .filter(
641
652
  (candidate, index, values) =>
@@ -667,6 +678,11 @@ export interface WorkflowControlEngine {
667
678
  onActivity?: (event: WorkflowActivityUpdate) => void,
668
679
  ): Promise<Record<string, unknown>>;
669
680
  executeDesign?(request: unknown): Promise<Record<string, unknown>>;
681
+ executeAmendment?(
682
+ change: string,
683
+ batchId: string,
684
+ request: unknown,
685
+ ): Promise<Record<string, unknown>>;
670
686
  assertDesignRun?(runId: string): void;
671
687
  recordDesignEvidence?(input: {
672
688
  runId: string;
@@ -680,998 +696,10 @@ export type WorkflowControlEngineFactory = (
680
696
  parentPayloadBridge: ParentPayloadBridge,
681
697
  ) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
682
698
 
683
- const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
684
- const execFileAsync = promisify(execFile);
685
-
686
- export interface OpenSpecDeliveryInspection {
687
- change: string;
688
- schema: string;
689
- planningComplete: boolean;
690
- strictValid: boolean;
691
- artifactPaths: string[];
692
- }
693
-
694
- export interface PackageDeliverySourceOptions {
695
- inspectOpenSpec?: (
696
- consumerRoot: string,
697
- change: string,
698
- ) => Promise<OpenSpecDeliveryInspection>;
699
- verifyGateProof?: (input: {
700
- change: string;
701
- gate: "gate-a" | "gate-b";
702
- proof: GateApprovalProof;
703
- }) => boolean;
704
- verifyFinalizedDelivery?: (input: {
705
- change: string;
706
- deliveryRevision: number;
707
- receiptHash: string;
708
- gateA: GateApprovalProof;
709
- gateB: GateApprovalProof;
710
- planCanonicalHash: string;
711
- }) => boolean;
712
- }
713
-
714
- export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
715
- discoverLatest(input: {
716
- stage: "abel-implement";
717
- change: string;
718
- }): Promise<WorkflowAvailableDelivery | undefined>;
719
- }
720
-
721
699
  function sha256(value: Uint8Array | string): string {
722
700
  return createHash("sha256").update(value).digest("hex");
723
701
  }
724
702
 
725
- function proofVerified(verify: () => boolean): boolean {
726
- try {
727
- return verify() === true;
728
- } catch {
729
- return false;
730
- }
731
- }
732
-
733
- function normalizedTrackingArtifact(
734
- bytes: Uint8Array,
735
- taskIds: readonly string[],
736
- ): Buffer | undefined {
737
- let text: string;
738
- try {
739
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
740
- } catch {
741
- return undefined;
742
- }
743
- const identities = taskIds.map((taskId) => ({
744
- taskId,
745
- pattern: new RegExp(
746
- `(?:^|[^A-Za-z0-9._:-])${taskId.replace(
747
- /[.*+?^${}()|[\]\\]/gu,
748
- "\\$&",
749
- )}(?![A-Za-z0-9._:-])`,
750
- "u",
751
- ),
752
- matches: 0,
753
- }));
754
- const normalized = text
755
- .split(/(?<=\n)/u)
756
- .map((segment) => {
757
- const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
758
- if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
759
- const matching = identities.filter(({ pattern }) => pattern.test(line));
760
- if (matching.length !== 1) return segment;
761
- matching[0].matches += 1;
762
- return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
763
- })
764
- .join("");
765
- if (identities.some(({ matches }) => matches !== 1)) return undefined;
766
- return Buffer.from(normalized, "utf8");
767
- }
768
-
769
- function readPackageDeliveryFile(
770
- root: string,
771
- relative: string,
772
- maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
773
- ): Buffer {
774
- const observation = observeSafePath(root, relative);
775
- if (observation.kind !== "file") {
776
- throw new Error("delivery-file-unavailable");
777
- }
778
- const target = path.join(root, ...relative.split("/"));
779
- const stat = lstatSync(target);
780
- if (stat.size < 1 || stat.size > maximumBytes) {
781
- throw new Error("delivery-file-size-invalid");
782
- }
783
- return readFileSync(target);
784
- }
785
-
786
- function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
787
- return value && typeof value === "object" && !Array.isArray(value)
788
- ? (value as Record<string, unknown>)
789
- : undefined;
790
- }
791
-
792
- function parseCommandJson(
793
- value: string,
794
- code: string,
795
- ): Record<string, unknown> {
796
- let parsed: unknown;
797
- try {
798
- parsed = JSON.parse(value);
799
- } catch {
800
- throw new Error(code);
801
- }
802
- const record = deliveryRecord(parsed);
803
- if (!record) throw new Error(code);
804
- return record;
805
- }
806
-
807
- export async function inspectOpenSpecDelivery(
808
- consumerRoot: string,
809
- change: string,
810
- ): Promise<OpenSpecDeliveryInspection> {
811
- const [statusExecution, validationExecution] = await Promise.all([
812
- execFileAsync("openspec", ["status", "--change", change, "--json"], {
813
- cwd: consumerRoot,
814
- encoding: "utf8",
815
- maxBuffer: 4 * 1024 * 1024,
816
- }),
817
- execFileAsync(
818
- "openspec",
819
- ["validate", change, "--strict", "--json", "--no-interactive"],
820
- {
821
- cwd: consumerRoot,
822
- encoding: "utf8",
823
- maxBuffer: 4 * 1024 * 1024,
824
- },
825
- ),
826
- ]);
827
- const status = parseCommandJson(
828
- statusExecution.stdout,
829
- "delivery-openspec-status-invalid",
830
- );
831
- const validation = parseCommandJson(
832
- validationExecution.stdout,
833
- "delivery-openspec-validation-invalid",
834
- );
835
- const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
836
- const artifactPathsRecord = deliveryRecord(status.artifactPaths);
837
- const artifactPaths = artifactPathsRecord
838
- ? Object.values(artifactPathsRecord).flatMap((entry) => {
839
- const record = deliveryRecord(entry);
840
- if (!record || !Array.isArray(record.existingOutputPaths)) return [];
841
- return record.existingOutputPaths.flatMap((candidate) => {
842
- if (typeof candidate !== "string") return [];
843
- const resolved = path.resolve(candidate);
844
- const relative = path.relative(changeRoot, resolved);
845
- return relative &&
846
- !relative.startsWith(`..${path.sep}`) &&
847
- relative !== ".." &&
848
- !path.isAbsolute(relative)
849
- ? [relative.split(path.sep).join("/")]
850
- : [];
851
- });
852
- })
853
- : [];
854
- const items = Array.isArray(validation.items) ? validation.items : [];
855
- const strictValid =
856
- items.length === 1 &&
857
- deliveryRecord(items[0])?.id === change &&
858
- deliveryRecord(items[0])?.valid === true;
859
- if (
860
- status.changeName !== change ||
861
- typeof status.schemaName !== "string" ||
862
- artifactPaths.length === 0
863
- ) {
864
- throw new Error("delivery-openspec-status-invalid");
865
- }
866
- return {
867
- change,
868
- schema: status.schemaName,
869
- planningComplete:
870
- status.isPlanningComplete === true && status.isComplete === true,
871
- strictValid,
872
- artifactPaths: [...new Set(artifactPaths)].sort(),
873
- };
874
- }
875
-
876
- export function packageDeliverySource(
877
- consumerRoot: string,
878
- options: PackageDeliverySourceOptions = {},
879
- ): PackageWorkflowDeliverySource {
880
- const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
881
- const verifyGateProof = options.verifyGateProof;
882
- const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
883
- return {
884
- async discoverLatest(input) {
885
- if (
886
- input.stage !== "abel-implement" ||
887
- !verifyGateProof ||
888
- !verifyFinalizedDelivery
889
- ) {
890
- return undefined;
891
- }
892
- try {
893
- const changeRoot = `openspec/changes/${input.change}`;
894
- const receiptBytes = readPackageDeliveryFile(
895
- consumerRoot,
896
- `${changeRoot}/ready.yaml`,
897
- 4 * 1024 * 1024,
898
- );
899
- const receipt = parseReadyReceipt(receiptBytes);
900
- if (receipt.change !== input.change) return undefined;
901
- const gateABytes = readPackageDeliveryFile(
902
- consumerRoot,
903
- `${changeRoot}/${receipt.approvals.gateA.path}`,
904
- 4 * 1024 * 1024,
905
- );
906
- if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
907
- return undefined;
908
- }
909
- const gateA = parseGateAReceipt(gateABytes);
910
- if (
911
- gateA.change !== receipt.change ||
912
- gateA.schema !== receipt.schema ||
913
- !proofVerified(() =>
914
- verifyGateProof({
915
- change: input.change,
916
- gate: "gate-a",
917
- proof: gateA.approval,
918
- }),
919
- ) ||
920
- !proofVerified(() =>
921
- verifyGateProof({
922
- change: input.change,
923
- gate: "gate-b",
924
- proof: receipt.approvals.gateB,
925
- }),
926
- )
927
- ) {
928
- return undefined;
929
- }
930
- const receiptHash = sha256(receiptBytes);
931
- if (
932
- !proofVerified(() =>
933
- verifyFinalizedDelivery({
934
- change: input.change,
935
- deliveryRevision: receipt.deliveryRevision,
936
- receiptHash,
937
- gateA: gateA.approval,
938
- gateB: receipt.approvals.gateB,
939
- planCanonicalHash: receipt.plan.canonicalHash,
940
- }),
941
- )
942
- ) {
943
- return undefined;
944
- }
945
- return {
946
- deliveryRevision: receipt.deliveryRevision,
947
- receiptHash,
948
- };
949
- } catch {
950
- return undefined;
951
- }
952
- },
953
- async load(input) {
954
- if (input.stage !== "abel-implement") {
955
- throw new DeliveryValidationError(["delivery-stage-invalid"]);
956
- }
957
- const changeRoot = `openspec/changes/${input.change}`;
958
- const receiptRelative = `${changeRoot}/ready.yaml`;
959
- const planRelative = `${changeRoot}/implement-plan.json`;
960
- let receiptBytes: Buffer;
961
- try {
962
- receiptBytes = readPackageDeliveryFile(
963
- consumerRoot,
964
- receiptRelative,
965
- 4 * 1024 * 1024,
966
- );
967
- } catch {
968
- throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
969
- }
970
- let receipt: ReturnType<typeof parseReadyReceipt>;
971
- try {
972
- receipt = parseReadyReceipt(receiptBytes);
973
- } catch {
974
- throw new DeliveryValidationError([
975
- "delivery-receipt-invalid",
976
- "delivery-recompile-required",
977
- ]);
978
- }
979
- const diagnostics = new Set<string>();
980
- if (receipt.change !== input.change) {
981
- diagnostics.add("delivery-change-mismatch");
982
- }
983
- const receiptHash = sha256(receiptBytes);
984
- if (
985
- (input.deliveryRevision !== undefined &&
986
- input.deliveryRevision !== receipt.deliveryRevision) ||
987
- (input.receiptHash !== undefined && input.receiptHash !== receiptHash)
988
- ) {
989
- diagnostics.add("delivery-revision-mismatch");
990
- }
991
-
992
- let inspection: OpenSpecDeliveryInspection | undefined;
993
- try {
994
- inspection = await inspect(consumerRoot, input.change);
995
- } catch {
996
- diagnostics.add("delivery-openspec-unavailable");
997
- }
998
- if (inspection) {
999
- if (!inspection.strictValid) {
1000
- diagnostics.add("delivery-openspec-strict-invalid");
1001
- }
1002
- if (!inspection.planningComplete) {
1003
- diagnostics.add("delivery-openspec-incomplete");
1004
- }
1005
- if (inspection.schema !== receipt.schema) {
1006
- diagnostics.add("delivery-schema-mismatch");
1007
- }
1008
- }
1009
-
1010
- let gateABytes: Buffer | undefined;
1011
- let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
1012
- try {
1013
- gateABytes = readPackageDeliveryFile(
1014
- consumerRoot,
1015
- `${changeRoot}/${receipt.approvals.gateA.path}`,
1016
- 4 * 1024 * 1024,
1017
- );
1018
- if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
1019
- diagnostics.add("delivery-gate-a-hash-mismatch");
1020
- }
1021
- gateA = parseGateAReceipt(gateABytes);
1022
- if (
1023
- gateA.change !== receipt.change ||
1024
- gateA.schema !== receipt.schema
1025
- ) {
1026
- diagnostics.add("delivery-gate-a-binding-mismatch");
1027
- }
1028
- } catch {
1029
- diagnostics.add("delivery-gate-a-invalid");
1030
- }
1031
-
1032
- const artifactBytes = new Map<string, Buffer>();
1033
- const artifactHashes = new Map(
1034
- receipt.artifacts.map((artifact) => [
1035
- artifact.path,
1036
- artifact.rawSha256,
1037
- ]),
1038
- );
1039
- for (const artifact of receipt.artifacts) {
1040
- try {
1041
- const bytes = readPackageDeliveryFile(
1042
- consumerRoot,
1043
- `${changeRoot}/${artifact.path}`,
1044
- );
1045
- artifactBytes.set(artifact.path, bytes);
1046
- if (
1047
- artifact.path !== receipt.traceability.taskPath &&
1048
- sha256(bytes) !== artifact.rawSha256
1049
- ) {
1050
- diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
1051
- }
1052
- } catch {
1053
- diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
1054
- }
1055
- }
1056
- if (inspection) {
1057
- const covered = new Set(receipt.artifacts.map((entry) => entry.path));
1058
- const expected = new Set(inspection.artifactPaths);
1059
- for (const relative of expected) {
1060
- if (!isValidRelativePath(relative) || !covered.has(relative)) {
1061
- diagnostics.add(`delivery-artifact-unbound:${relative}`);
1062
- }
1063
- }
1064
- for (const relative of covered) {
1065
- if (!expected.has(relative)) {
1066
- diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
1067
- }
1068
- }
1069
- }
1070
- if (gateA) {
1071
- if (!verifyGateProof) {
1072
- diagnostics.add("delivery-gate-proof-verifier-unavailable");
1073
- } else if (
1074
- !proofVerified(() =>
1075
- verifyGateProof({
1076
- change: input.change,
1077
- gate: "gate-a",
1078
- proof: gateA.approval,
1079
- }),
1080
- )
1081
- ) {
1082
- diagnostics.add("delivery-gate-a-proof-invalid");
1083
- }
1084
- for (const artifact of gateA.artifacts) {
1085
- if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
1086
- diagnostics.add(
1087
- `delivery-gate-a-artifact-mismatch:${artifact.path}`,
1088
- );
1089
- }
1090
- }
1091
- }
1092
- if (
1093
- verifyGateProof &&
1094
- !proofVerified(() =>
1095
- verifyGateProof({
1096
- change: input.change,
1097
- gate: "gate-b",
1098
- proof: receipt.approvals.gateB,
1099
- }),
1100
- )
1101
- ) {
1102
- diagnostics.add("delivery-gate-b-proof-invalid");
1103
- }
1104
- if (gateA) {
1105
- if (!verifyFinalizedDelivery) {
1106
- diagnostics.add("delivery-finalization-verifier-unavailable");
1107
- } else if (
1108
- !proofVerified(() =>
1109
- verifyFinalizedDelivery({
1110
- change: input.change,
1111
- deliveryRevision: receipt.deliveryRevision,
1112
- receiptHash,
1113
- gateA: gateA.approval,
1114
- gateB: receipt.approvals.gateB,
1115
- planCanonicalHash: receipt.plan.canonicalHash,
1116
- }),
1117
- )
1118
- ) {
1119
- diagnostics.add("delivery-finalization-proof-invalid");
1120
- }
1121
- }
1122
-
1123
- let plan: ReturnType<typeof parseImplementPlan> | undefined;
1124
- let planBytes: Buffer | undefined;
1125
- try {
1126
- planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
1127
- plan = parseImplementPlan(planBytes);
1128
- } catch {
1129
- diagnostics.add("delivery-plan-invalid");
1130
- }
1131
- if (plan && planBytes) {
1132
- if (plan.changeId !== input.change) {
1133
- diagnostics.add("delivery-plan-change-mismatch");
1134
- }
1135
- let compiled: ReturnType<typeof compileImplementPlan> | undefined;
1136
- try {
1137
- compiled = compileImplementPlan(plan, { consumerRoot });
1138
- } catch {
1139
- diagnostics.add("delivery-verification-closure-invalid");
1140
- }
1141
- if (
1142
- receipt.plan.path !== "implement-plan.json" ||
1143
- receipt.plan.rawSha256 !== sha256(planBytes) ||
1144
- receipt.plan.canonicalHash !== hashCanonicalValue(plan)
1145
- ) {
1146
- diagnostics.add("delivery-plan-binding-invalid");
1147
- }
1148
- if (
1149
- compiled &&
1150
- canonicalJson(compiled.closure) !==
1151
- canonicalJson(receipt.verificationClosure)
1152
- ) {
1153
- diagnostics.add("delivery-verification-closure-mismatch");
1154
- }
1155
- }
1156
-
1157
- if (plan) {
1158
- const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
1159
- const expectedTasksHash = artifactHashes.get(
1160
- receipt.traceability.taskPath,
1161
- );
1162
- if (
1163
- tasksBytes &&
1164
- expectedTasksHash &&
1165
- sha256(tasksBytes) !== expectedTasksHash
1166
- ) {
1167
- const normalized = normalizedTrackingArtifact(
1168
- tasksBytes,
1169
- plan.tracking.taskIds,
1170
- );
1171
- if (!normalized || sha256(normalized) !== expectedTasksHash) {
1172
- diagnostics.add(
1173
- `delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
1174
- );
1175
- }
1176
- }
1177
- const specs = [...artifactBytes]
1178
- .filter(
1179
- ([relative]) =>
1180
- relative.startsWith("specs/") && relative.endsWith("/spec.md"),
1181
- )
1182
- .flatMap(([relative, bytes]) => {
1183
- try {
1184
- return [
1185
- {
1186
- path: relative,
1187
- text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
1188
- },
1189
- ];
1190
- } catch {
1191
- diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
1192
- return [];
1193
- }
1194
- });
1195
- if (!tasksBytes || specs.length === 0) {
1196
- diagnostics.add("delivery-traceability-input-unavailable");
1197
- } else {
1198
- try {
1199
- const traceability = assessDeliveryTraceability({
1200
- tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
1201
- tasksBytes,
1202
- ),
1203
- specs,
1204
- plan,
1205
- });
1206
- if (!traceability.ok) {
1207
- for (const diagnostic of traceability.diagnostics) {
1208
- diagnostics.add(diagnostic);
1209
- }
1210
- } else if (
1211
- canonicalJson(traceability.value) !==
1212
- canonicalJson(receipt.traceability)
1213
- ) {
1214
- diagnostics.add("delivery-traceability-binding-mismatch");
1215
- }
1216
- } catch {
1217
- diagnostics.add("delivery-traceability-invalid");
1218
- }
1219
- }
1220
- }
1221
- if (!plan || !gateA || diagnostics.size > 0) {
1222
- throw new DeliveryValidationError([...diagnostics]);
1223
- }
1224
- return {
1225
- gate: "gate-b",
1226
- revision: receipt.deliveryRevision,
1227
- receiptHash,
1228
- plan,
1229
- approvalProofs: {
1230
- gateA: structuredClone(gateA.approval),
1231
- gateB: structuredClone(receipt.approvals.gateB),
1232
- },
1233
- };
1234
- },
1235
- };
1236
- }
1237
-
1238
- function packageScriptInvocation(
1239
- packageManager: string,
1240
- script: string,
1241
- args: string[],
1242
- ): string[] {
1243
- return packageManager === "npm" || packageManager === "pnpm"
1244
- ? ["run", script, "--", ...args]
1245
- : ["run", script, ...args];
1246
- }
1247
-
1248
- function bindingFor(
1249
- bindings: readonly VerificationRunnerBinding[],
1250
- command: string,
1251
- ): VerificationRunnerBinding | undefined {
1252
- return bindings.find((binding) => binding.command === command);
1253
- }
1254
-
1255
- function prepareRunnerBindings(
1256
- root: string,
1257
- bindings: readonly VerificationRunnerBinding[],
1258
- ): {
1259
- bindings: VerificationRunnerBinding[];
1260
- mounts: Array<{ source: string; target: string }>;
1261
- cleanupRoot?: string;
1262
- } {
1263
- if (!bindings.some((binding) => binding.mountSource)) {
1264
- return {
1265
- bindings: bindings.map((binding) => ({
1266
- ...binding,
1267
- ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
1268
- })),
1269
- mounts: [],
1270
- };
1271
- }
1272
- const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
1273
- try {
1274
- const mounted = new Map<
1275
- string,
1276
- { source: string; target: string; sandboxTarget: string }
1277
- >();
1278
- const prepared = bindings.map((binding) => {
1279
- if (!binding.mountSource) return { ...binding };
1280
- const source = path.resolve(binding.mountSource);
1281
- const sourceStat = lstatSync(source);
1282
- if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
1283
- throw new Error("runner-mount-unavailable");
1284
- }
1285
- let mount = mounted.get(source);
1286
- if (!mount) {
1287
- const target = path.join(cleanupRoot, String(mounted.size));
1288
- mkdirSync(target, { mode: 0o700 });
1289
- const sandboxRelative = path
1290
- .relative(root, target)
1291
- .split(path.sep)
1292
- .join("/");
1293
- mount = {
1294
- source,
1295
- target,
1296
- sandboxTarget: `/workspace/${sandboxRelative}`,
1297
- };
1298
- mounted.set(source, mount);
1299
- }
1300
- const executable = realpathSync(binding.executablePath);
1301
- const relative = path.relative(source, executable);
1302
- if (
1303
- relative === "" ||
1304
- relative.startsWith("..") ||
1305
- path.isAbsolute(relative)
1306
- ) {
1307
- throw new Error("runner-mount-unavailable");
1308
- }
1309
- return {
1310
- command: binding.command,
1311
- executablePath: path.posix.join(
1312
- mount.sandboxTarget,
1313
- ...relative.split(path.sep),
1314
- ),
1315
- ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
1316
- };
1317
- });
1318
- return {
1319
- bindings: prepared,
1320
- mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
1321
- source,
1322
- target: sandboxTarget,
1323
- })),
1324
- cleanupRoot,
1325
- };
1326
- } catch (error) {
1327
- rmSync(cleanupRoot, { recursive: true, force: true });
1328
- throw error;
1329
- }
1330
- }
1331
-
1332
- function verificationInvocation(
1333
- step: AtomicVerificationContract,
1334
- bindings: readonly VerificationRunnerBinding[],
1335
- ): { executable: string; args: string[] } | undefined {
1336
- const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
1337
- if (step.kind === "package-script") {
1338
- const binding = bindingFor(bindings, step.packageManager);
1339
- return binding
1340
- ? {
1341
- executable: binding.executablePath,
1342
- args: [
1343
- ...(binding.fixedArgs ?? []),
1344
- ...packageScriptInvocation(
1345
- step.packageManager,
1346
- step.script,
1347
- step.args,
1348
- ),
1349
- ],
1350
- }
1351
- : undefined;
1352
- }
1353
- const runner = step.runner;
1354
- if (runner.kind === "package-script") {
1355
- const binding = bindingFor(bindings, runner.packageManager);
1356
- const args =
1357
- step.kind === "vitest"
1358
- ? [...step.testFiles, ...step.args, ...reporter]
1359
- : step.args;
1360
- return binding
1361
- ? {
1362
- executable: binding.executablePath,
1363
- args: [
1364
- ...(binding.fixedArgs ?? []),
1365
- ...packageScriptInvocation(
1366
- runner.packageManager,
1367
- runner.script,
1368
- args,
1369
- ),
1370
- ],
1371
- }
1372
- : undefined;
1373
- }
1374
- if (runner.kind === "local-binary") {
1375
- const binding = bindingFor(bindings, runner.executable);
1376
- return {
1377
- executable:
1378
- binding?.executablePath ??
1379
- `/workspace/node_modules/.bin/${runner.executable}`,
1380
- args: [
1381
- ...(binding?.fixedArgs ?? []),
1382
- ...(step.kind === "vitest"
1383
- ? [...step.testFiles, ...step.args, ...reporter]
1384
- : step.args),
1385
- ],
1386
- };
1387
- }
1388
- const command = runner.kind === "node" ? "node" : "npx";
1389
- const binding = bindingFor(bindings, command);
1390
- if (!binding) return undefined;
1391
- return {
1392
- executable: binding.executablePath,
1393
- args:
1394
- runner.kind === "node"
1395
- ? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
1396
- : [
1397
- ...(binding.fixedArgs ?? []),
1398
- "--no-install",
1399
- runner.executable,
1400
- ...(step.kind === "vitest" ? step.testFiles : []),
1401
- ...step.args,
1402
- ...reporter,
1403
- ],
1404
- };
1405
- }
1406
-
1407
- function vitestReport(stdout: string):
1408
- | {
1409
- total: number;
1410
- failed: number;
1411
- success: boolean;
1412
- failures: string[];
1413
- failedText: string[];
1414
- }
1415
- | undefined {
1416
- let value: unknown;
1417
- try {
1418
- value = JSON.parse(stdout);
1419
- } catch {
1420
- return undefined;
1421
- }
1422
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1423
- return undefined;
1424
- }
1425
- const report = value as Record<string, unknown>;
1426
- if (
1427
- !Number.isSafeInteger(report.numTotalTests) ||
1428
- !Number.isSafeInteger(report.numFailedTests) ||
1429
- typeof report.success !== "boolean"
1430
- ) {
1431
- return undefined;
1432
- }
1433
- const failedAssertions = (
1434
- Array.isArray(report.testResults) ? report.testResults : []
1435
- ).flatMap((suite): Array<{ identity: string; text: string[] }> => {
1436
- if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
1437
- const record = suite as Record<string, unknown>;
1438
- const suiteName =
1439
- typeof record.name === "string"
1440
- ? record.name.replace(/^\/workspace\//u, "")
1441
- : "unknown-suite";
1442
- if (!Array.isArray(record.assertionResults)) return [];
1443
- return record.assertionResults.flatMap(
1444
- (assertion): Array<{ identity: string; text: string[] }> => {
1445
- if (
1446
- !assertion ||
1447
- typeof assertion !== "object" ||
1448
- Array.isArray(assertion)
1449
- ) {
1450
- return [];
1451
- }
1452
- const entry = assertion as Record<string, unknown>;
1453
- if (entry.status !== "failed") return [];
1454
- const title =
1455
- typeof entry.fullName === "string"
1456
- ? entry.fullName
1457
- : typeof entry.title === "string"
1458
- ? entry.title
1459
- : "unknown-assertion";
1460
- const diagnostics = Array.isArray(entry.failureMessages)
1461
- ? entry.failureMessages.filter(
1462
- (message): message is string => typeof message === "string",
1463
- )
1464
- : [];
1465
- return [
1466
- {
1467
- identity: `${suiteName}\0${title}`,
1468
- text: [title, ...diagnostics],
1469
- },
1470
- ];
1471
- },
1472
- );
1473
- });
1474
- return {
1475
- total: report.numTotalTests as number,
1476
- failed: report.numFailedTests as number,
1477
- success: report.success,
1478
- failures: failedAssertions.map((failure) => failure.identity),
1479
- failedText: failedAssertions.flatMap((failure) => failure.text),
1480
- };
1481
- }
1482
-
1483
- function normalizedFailureIdentities(input: {
1484
- step: AtomicVerificationContract;
1485
- report?: ReturnType<typeof vitestReport>;
1486
- output: string;
1487
- exitCode: number;
1488
- }): string[] {
1489
- const vitestFailures = input.report?.failures ?? [];
1490
- if (vitestFailures.length > 0) {
1491
- return [...new Set(vitestFailures)]
1492
- .sort()
1493
- .map((failure) => sha256(`vitest-failure\0${failure}`));
1494
- }
1495
- const normalizedOutput = input.output
1496
- .replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
1497
- .replace(/\r\n?/gu, "\n")
1498
- .replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
1499
- .slice(0, 64 * 1024);
1500
- return [
1501
- sha256(
1502
- canonicalJson({
1503
- verificationId: input.step.id,
1504
- exitCode: input.exitCode,
1505
- output: normalizedOutput,
1506
- }),
1507
- ),
1508
- ];
1509
- }
1510
-
1511
- export async function executePackageVerification(input: {
1512
- root: string;
1513
- dependencyOwner: string;
1514
- verification: StructuredVerificationContract;
1515
- signal: AbortSignal;
1516
- }) {
1517
- const capability = bindCurrentVerificationCapability(
1518
- input.root,
1519
- input.verification,
1520
- { dependencyOwner: input.dependencyOwner },
1521
- );
1522
- if (!capability.ok) {
1523
- return {
1524
- ok: false as const,
1525
- kind: "paused" as const,
1526
- code: capability.diagnostic.code,
1527
- };
1528
- }
1529
- let runners: ReturnType<typeof prepareRunnerBindings>;
1530
- try {
1531
- runners = prepareRunnerBindings(
1532
- input.root,
1533
- capability.value.runnerBindings,
1534
- );
1535
- } catch {
1536
- return {
1537
- ok: false as const,
1538
- kind: "paused" as const,
1539
- code: "runner-mount-unavailable",
1540
- };
1541
- }
1542
- try {
1543
- const nodeModules = path.join(input.dependencyOwner, "node_modules");
1544
- const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
1545
- const mounts = [] as Array<{
1546
- source: string;
1547
- target: string;
1548
- writable?: boolean;
1549
- }>;
1550
- if (dependency) {
1551
- if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
1552
- return {
1553
- ok: false as const,
1554
- kind: "paused" as const,
1555
- code: "dependency-path-unsafe",
1556
- };
1557
- }
1558
- const target = path.join(input.root, "node_modules");
1559
- const targetStat = lstatSync(target, { throwIfNoEntry: false });
1560
- if (
1561
- targetStat &&
1562
- (!targetStat.isDirectory() || targetStat.isSymbolicLink())
1563
- ) {
1564
- return {
1565
- ok: false as const,
1566
- kind: "paused" as const,
1567
- code: "dependency-path-unsafe",
1568
- };
1569
- }
1570
- if (!targetStat) mkdirSync(target, { mode: 0o700 });
1571
- mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
1572
- }
1573
- mounts.push(...runners.mounts);
1574
- const isolation = new BubblewrapIsolationBackend();
1575
- const steps = verificationSteps(input.verification);
1576
- let final:
1577
- | {
1578
- exitCode: number;
1579
- classification: AtomicVerificationContract["classification"];
1580
- diagnostic: { kind: "assertion" | "compiler"; id: string };
1581
- }
1582
- | undefined;
1583
- for (const step of steps) {
1584
- const invocation = verificationInvocation(step, runners.bindings);
1585
- if (!invocation) {
1586
- return {
1587
- ok: false as const,
1588
- kind: "paused" as const,
1589
- code: "runner-missing",
1590
- };
1591
- }
1592
- const executed = await isolation.run({
1593
- root: input.root,
1594
- executable: invocation.executable,
1595
- args: invocation.args,
1596
- mounts,
1597
- environment: { CI: "1" },
1598
- signal: input.signal,
1599
- });
1600
- if (!executed.ok) {
1601
- if (executed.state === "cancelled") throw input.signal.reason;
1602
- return {
1603
- ok: false as const,
1604
- kind: "paused" as const,
1605
- code: executed.code,
1606
- };
1607
- }
1608
- const report =
1609
- step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
1610
- const output = `${executed.stdout}\n${executed.stderr}`;
1611
- const identity =
1612
- step.classification !== "expected-red" ||
1613
- (step.kind === "vitest"
1614
- ? report?.failedText.some((text) =>
1615
- text.includes(step.expectedFailure ?? ""),
1616
- ) === true
1617
- : output.includes(step.expectedFailure ?? ""));
1618
- const accepted =
1619
- step.classification === "expected-red"
1620
- ? executed.exitCode !== 0 &&
1621
- identity &&
1622
- (step.kind !== "vitest" ||
1623
- (report !== undefined &&
1624
- report.failed > 0 &&
1625
- report.total >= step.minTests))
1626
- : executed.exitCode === 0 &&
1627
- (step.kind !== "vitest" ||
1628
- (report?.success === true && report.total >= step.minTests));
1629
- if (!accepted) {
1630
- return {
1631
- ok: false as const,
1632
- kind: "retryable" as const,
1633
- code:
1634
- step.classification === "expected-red" && executed.exitCode === 0
1635
- ? "red-not-witnessed"
1636
- : "verification-rejected",
1637
- failureIdentities: normalizedFailureIdentities({
1638
- step,
1639
- report,
1640
- output,
1641
- exitCode: executed.exitCode,
1642
- }),
1643
- };
1644
- }
1645
- if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
1646
- return {
1647
- ok: false as const,
1648
- kind: "retryable" as const,
1649
- code: "verification-input-unavailable",
1650
- };
1651
- }
1652
- final = {
1653
- exitCode: executed.exitCode,
1654
- classification: step.classification,
1655
- diagnostic: {
1656
- kind: step.kind === "vitest" ? "assertion" : "compiler",
1657
- id: step.id,
1658
- },
1659
- };
1660
- }
1661
- return final
1662
- ? { ok: true as const, ...final }
1663
- : {
1664
- ok: false as const,
1665
- kind: "paused" as const,
1666
- code: "verification-contract-unsupported",
1667
- };
1668
- } finally {
1669
- if (runners.cleanupRoot) {
1670
- rmSync(runners.cleanupRoot, { recursive: true, force: true });
1671
- }
1672
- }
1673
- }
1674
-
1675
703
  function affectedVerification(
1676
704
  task: Parameters<
1677
705
  Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
@@ -1732,7 +760,39 @@ export function openPackageWorkflowControlEngine(
1732
760
  if (!implementationAgent) {
1733
761
  throw new Error("implementation-worker-agent-unavailable");
1734
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);
1735
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
+ ),
1736
796
  consumerRoot,
1737
797
  stateRoot,
1738
798
  deliverySource: packageDeliverySource(consumerRoot, {
@@ -1785,10 +845,28 @@ export function openPackageWorkflowControlEngine(
1785
845
  ].sort(),
1786
846
  }
1787
847
  : {
1788
- 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(),
1789
857
  write: [...phase.write],
1790
858
  delete: [...phase.delete],
1791
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();
1792
870
  const phaseContract = {
1793
871
  candidateId: input.candidateArtifact.identity.candidateId,
1794
872
  taskId: input.taskId,
@@ -1808,6 +886,9 @@ export function openPackageWorkflowControlEngine(
1808
886
  ...(input.contextRequest
1809
887
  ? { requestedContext: structuredClone(input.contextRequest) }
1810
888
  : {}),
889
+ ...(input.recoveryFeedback
890
+ ? { recoveryFeedback: structuredClone(input.recoveryFeedback) }
891
+ : {}),
1811
892
  ...(input.repair
1812
893
  ? {
1813
894
  repair: {
@@ -1829,6 +910,7 @@ export function openPackageWorkflowControlEngine(
1829
910
  input.task.objective,
1830
911
  input.task.context.agents,
1831
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.",
1832
914
  `<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
1833
915
  ].join("\n\n"),
1834
916
  requestId: childRequestId(input.operationId, input.taskId, input.phase),
@@ -1864,7 +946,7 @@ export function openPackageWorkflowControlEngine(
1864
946
  return { kind: "approval-needed", code: child.failure.code };
1865
947
  }
1866
948
  if (child.failure.kind === "result-limit") {
1867
- return { kind: "paused", code: "needs-task-split" };
949
+ return { kind: "retryable", code: "needs-task-split" };
1868
950
  }
1869
951
  const attemptDiagnostic = {
1870
952
  finalCategory: child.classification.finalCategory,
@@ -1883,6 +965,7 @@ export function openPackageWorkflowControlEngine(
1883
965
  if (result.kind === "context-request") {
1884
966
  return classifyCandidateContextRequest(result, {
1885
967
  phase: input.phase,
968
+ contextReadRoots: input.task.roots,
1886
969
  readPaths: phase.read,
1887
970
  writePaths: phase.write,
1888
971
  deletePaths: phase.delete,
@@ -1915,44 +998,37 @@ export function openPackageWorkflowControlEngine(
1915
998
  paths: [...result.paths],
1916
999
  };
1917
1000
  },
1918
- verifyPhase: (input) =>
1919
- executePackageVerification({
1920
- root: input.root,
1921
- dependencyOwner: consumerRoot,
1922
- verification: input.verification,
1923
- signal: input.signal,
1924
- }),
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
+ ),
1925
1012
  verifyChange: async (input) => {
1926
1013
  const verifications = input.verification
1927
1014
  ? [input.verification]
1928
1015
  : input.plan.tasks.map((task) => affectedVerification(task));
1929
1016
  for (const verification of verifications) {
1930
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
+ ),
1931
1025
  root: input.root,
1932
1026
  dependencyOwner: consumerRoot,
1933
1027
  verification,
1934
1028
  signal: input.signal,
1935
1029
  });
1936
- if (!result.ok) {
1937
- const adapter = new Set([
1938
- "runner-missing",
1939
- "script-missing",
1940
- "script-command-mismatch",
1941
- "local-executable-missing",
1942
- ]).has(result.code);
1943
- return {
1944
- ok: false,
1945
- kind: adapter
1946
- ? ("verification-adapter" as const)
1947
- : result.kind === "paused"
1948
- ? ("environment" as const)
1949
- : ("verification" as const),
1950
- code: result.code,
1951
- ...(result.failureIdentities
1952
- ? { failureIdentities: result.failureIdentities }
1953
- : {}),
1954
- };
1955
- }
1030
+ const observed = changeVerificationResult(result);
1031
+ if (!observed.ok) return observed;
1956
1032
  }
1957
1033
  return {
1958
1034
  ok: true,
@@ -2000,6 +1076,11 @@ export function openPackageWorkflowControlEngine(
2000
1076
  executeDesign(request: unknown) {
2001
1077
  return design.execute(request);
2002
1078
  },
1079
+ executeAmendment(change: string, batchId: string, request: unknown) {
1080
+ return engine.amend(change, batchId, request, (assertAuthority) =>
1081
+ design.executeWithAuthority(request, assertAuthority),
1082
+ );
1083
+ },
2003
1084
  assertDesignRun(runId: string) {
2004
1085
  design.assertDesignRun(runId);
2005
1086
  },
@@ -2066,8 +1147,11 @@ export function registerWorkflowControl(
2066
1147
  const parentPayloadBridge = new ParentPayloadBridge();
2067
1148
  const activity = new ActivityController();
2068
1149
  const engines = new Map<string, Promise<WorkflowControlEngine>>();
1150
+ const designOperations = new Set<Promise<void>>();
2069
1151
  const designFailures = new Map<string, SafeDesignFailure>();
2070
1152
  let pendingPrompt: EligiblePrompt | undefined;
1153
+ let pendingInit = false;
1154
+ let exitingStage = false;
2071
1155
  let activePrompt: EligiblePrompt | undefined;
2072
1156
  let designToolSnapshot: string[] | undefined;
2073
1157
  const activation = new (class implements Activation {
@@ -2109,16 +1193,19 @@ export function registerWorkflowControl(
2109
1193
  return opened;
2110
1194
  };
2111
1195
  const closeEngines = async () => {
2112
- const current = [...engines.values()];
2113
- engines.clear();
2114
- const opened = await Promise.allSettled(current);
2115
- await Promise.allSettled(
2116
- opened.flatMap((result) =>
2117
- result.status === "fulfilled"
2118
- ? [Promise.resolve(result.value.close())]
2119
- : [],
2120
- ),
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
+ }),
2121
1206
  );
1207
+ const failed = closed.find((result) => result.status === "rejected");
1208
+ if (failed?.status === "rejected") throw failed.reason;
2122
1209
  };
2123
1210
  const restoreDesignTools = () => {
2124
1211
  if (!designToolSnapshot) return;
@@ -2156,6 +1243,16 @@ export function registerWorkflowControl(
2156
1243
  deactivate();
2157
1244
  parentPayloadBridge.clear();
2158
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
+ };
2159
1256
 
2160
1257
  // Pi validates tool arguments before execute(), so expose only the schema
2161
1258
  // legal for the active stage instead of one ambiguous command/packet union.
@@ -2181,6 +1278,61 @@ export function registerWorkflowControl(
2181
1278
  } as const;
2182
1279
  let registeredParameterKind: "command" | "packet" | undefined;
2183
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
+
2184
1336
  const registerDispatchTool = (kind: "command" | "packet") => {
2185
1337
  if (registeredParameterKind === kind) return;
2186
1338
  registeredParameterKind = kind;
@@ -2189,17 +1341,16 @@ export function registerWorkflowControl(
2189
1341
  label: "Abel Control",
2190
1342
  description:
2191
1343
  kind === "command"
2192
- ? "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.'
2193
1345
  : "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
2194
1346
  executionMode: "parallel",
2195
1347
  ...(kind === "command"
2196
1348
  ? {
2197
1349
  prepareArguments: (args: unknown) =>
2198
- canonicalizeControlCommandToolInput(args) as never,
1350
+ prepareImplementArguments(args) as never,
2199
1351
  }
2200
1352
  : {}),
2201
- parameters:
2202
- kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
1353
+ parameters: kind === "command" ? IMPLEMENT_PARAMETERS : PACKET_PARAMETERS,
2203
1354
  async execute(
2204
1355
  toolCallId: string,
2205
1356
  params: unknown,
@@ -2207,10 +1358,120 @@ export function registerWorkflowControl(
2207
1358
  onUpdate: AgentToolUpdateCallback<unknown> | undefined,
2208
1359
  ctx: ExtensionContext,
2209
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);
2210
1369
  const record =
2211
1370
  params && typeof params === "object" && !Array.isArray(params)
2212
1371
  ? (params as Record<string, unknown>)
2213
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
+ }
2214
1475
  if (record && typeof record.action === "string") {
2215
1476
  if (
2216
1477
  Object.keys(record).some(
@@ -2222,6 +1483,18 @@ export function registerWorkflowControl(
2222
1483
  if (record.action === "finish" && Object.hasOwn(record, "request")) {
2223
1484
  throw new Error("control-envelope-ambiguous");
2224
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
+ }
2225
1498
  if (
2226
1499
  activePrompt !== "abel-design" &&
2227
1500
  activePrompt !== "abel-diagnose"
@@ -2234,9 +1507,18 @@ export function registerWorkflowControl(
2234
1507
  }
2235
1508
  const designRequest = validateDesignControlRequest(record.request);
2236
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);
2237
1517
  let boundary: DesignFailureBoundary = "initialization";
2238
1518
  try {
2239
1519
  const engine = await engineFor(ctx);
1520
+ if (exitingStage || !activation.isActive())
1521
+ throw new Error("stage-control-mismatch");
2240
1522
  if (!engine.executeDesign)
2241
1523
  throw new Error("design-control-unavailable");
2242
1524
  boundary = "execution";
@@ -2249,7 +1531,9 @@ export function registerWorkflowControl(
2249
1531
  boundary = "execution";
2250
1532
  if (
2251
1533
  designRequest.value.operation === "finalize-delivery" &&
2252
- payload.state === "completed"
1534
+ payload.state === "completed" &&
1535
+ !exitingStage &&
1536
+ activePrompt === "abel-design"
2253
1537
  ) {
2254
1538
  await deactivateStage();
2255
1539
  }
@@ -2267,6 +1551,9 @@ export function registerWorkflowControl(
2267
1551
  const sanitized = new Error(failure.code);
2268
1552
  sanitized.name = "DesignControlError";
2269
1553
  throw sanitized;
1554
+ } finally {
1555
+ designOperations.delete(settled);
1556
+ settleDesign();
2270
1557
  }
2271
1558
  }
2272
1559
  let designEngine: WorkflowControlEngine | undefined;
@@ -2280,6 +1567,8 @@ export function registerWorkflowControl(
2280
1567
  if (packet.value.stage === "abel-design") {
2281
1568
  try {
2282
1569
  designEngine = await engineFor(ctx);
1570
+ if (exitingStage || !activation.isActive())
1571
+ throw new Error("stage-control-mismatch");
2283
1572
  } catch (error) {
2284
1573
  if (error instanceof RunStoreFormatError)
2285
1574
  throw runStoreResetError(error);
@@ -2336,9 +1625,6 @@ export function registerWorkflowControl(
2336
1625
  });
2337
1626
  payload = { ...packetPayload, recordedEvidence };
2338
1627
  }
2339
- if (record.action === "finish" && packetPayload.ok) {
2340
- await deactivateStage();
2341
- }
2342
1628
  const display = tuiRun
2343
1629
  ? activity.finalize(toolCallId, payload)
2344
1630
  : undefined;
@@ -2366,15 +1652,16 @@ export function registerWorkflowControl(
2366
1652
  throw error;
2367
1653
  }
2368
1654
  if (
2369
- activePrompt !== undefined &&
2370
- (activePrompt === "abel-diagnose" ||
2371
- validation.value.stage !== activePrompt)
1655
+ activePrompt !== "abel-implement" ||
1656
+ validation.value.stage !== activePrompt
2372
1657
  ) {
2373
1658
  throw new Error("stage-control-mismatch");
2374
1659
  }
2375
1660
  let engine: WorkflowControlEngine;
2376
1661
  try {
2377
1662
  engine = await engineFor(ctx);
1663
+ if (exitingStage || !activation.isActive())
1664
+ throw new Error("stage-control-mismatch");
2378
1665
  } catch (error) {
2379
1666
  if (!(error instanceof RunStoreFormatError)) throw error;
2380
1667
  const payload = runStoreUnavailableStatus(error, validation.value);
@@ -2471,7 +1758,10 @@ export function registerWorkflowControl(
2471
1758
  if (
2472
1759
  event.toolName !== DISPATCH_TOOL ||
2473
1760
  !event.isError ||
2474
- event.input.action !== "design"
1761
+ (event.input.action !== "design" &&
1762
+ !(
1763
+ event.input.action === "amend" && designFailures.has(event.toolCallId)
1764
+ ))
2475
1765
  ) {
2476
1766
  return;
2477
1767
  }
@@ -2501,12 +1791,35 @@ export function registerWorkflowControl(
2501
1791
  };
2502
1792
  });
2503
1793
  pi.on("input", (event) => {
2504
- 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
+ }
2505
1815
  return { action: "continue" };
2506
1816
  });
2507
1817
  pi.on("before_agent_start", (event, ctx) => {
2508
1818
  const prompt = pendingPrompt;
1819
+ const init =
1820
+ pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
2509
1821
  pendingPrompt = undefined;
1822
+ pendingInit = false;
2510
1823
  const verified =
2511
1824
  prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
2512
1825
  if (verified) {
@@ -2527,6 +1840,12 @@ export function registerWorkflowControl(
2527
1840
  }
2528
1841
  if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
2529
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}` };
2530
1849
  });
2531
1850
  pi.on("model_select", (event, ctx) => {
2532
1851
  const sessionId = ctx.sessionManager?.getSessionId?.();
@@ -2552,6 +1871,7 @@ export function registerWorkflowControl(
2552
1871
  });
2553
1872
  pi.on("session_start", async (_event, ctx) => {
2554
1873
  pendingPrompt = undefined;
1874
+ pendingInit = false;
2555
1875
  activePrompt = undefined;
2556
1876
  designFailures.clear();
2557
1877
  activity.detach();