@abelxiaoxing/cadence 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +172 -13
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/package.json +9 -5
  8. package/prompts/abel-design.md +141 -38
  9. package/prompts/abel-diagnose.md +10 -1
  10. package/prompts/abel-implement.md +118 -20
  11. package/prompts/abel-init.md +9 -2
  12. package/src/activity-contracts.ts +82 -0
  13. package/src/apply-transaction.ts +16 -14
  14. package/src/artifact-store.ts +17 -0
  15. package/src/candidate-artifact.ts +143 -0
  16. package/src/candidate-patch.ts +33 -12
  17. package/src/candidate-workspace.ts +74 -0
  18. package/src/canonical.ts +16 -0
  19. package/src/change-contract-schema.ts +191 -0
  20. package/src/change-contract.ts +273 -0
  21. package/src/change-verification.ts +896 -0
  22. package/src/child-model.ts +129 -0
  23. package/src/child-session.ts +296 -328
  24. package/src/contracts.ts +325 -109
  25. package/src/control-contracts.ts +51 -1
  26. package/src/delivery-compiler.ts +241 -141
  27. package/src/delivery-revision.ts +42 -0
  28. package/src/design-control.ts +182 -15
  29. package/src/design-diagnostics.ts +150 -0
  30. package/src/design-journal.ts +216 -72
  31. package/src/durable-contracts.ts +210 -0
  32. package/src/durable-workflow.ts +963 -0
  33. package/src/evidence-draft.ts +122 -0
  34. package/src/implement-graph.ts +14 -16
  35. package/src/implement-plan.ts +70 -0
  36. package/src/index.ts +341 -1405
  37. package/src/isolation-backend.ts +84 -23
  38. package/src/model-source.ts +22 -0
  39. package/src/package-candidate.ts +224 -0
  40. package/src/package-delivery.ts +486 -0
  41. package/src/package-verification.ts +509 -0
  42. package/src/package-workflow.ts +253 -0
  43. package/src/packet-runtime.ts +27 -48
  44. package/src/parent-provider.ts +86 -198
  45. package/src/phase-execution.ts +2248 -0
  46. package/src/pi-adapter.ts +19 -0
  47. package/src/plan-draft-summary.ts +146 -0
  48. package/src/plan-draft.ts +493 -0
  49. package/src/route-policy.ts +1 -11
  50. package/src/run-state.ts +4 -16
  51. package/src/run-store.ts +5 -144
  52. package/src/scoped-grep-worker.mjs +23 -0
  53. package/src/scoped-tools.ts +312 -116
  54. package/src/sqlite-schema.ts +171 -0
  55. package/src/storage-schema.ts +352 -0
  56. package/src/subagent-activity.ts +46 -67
  57. package/src/submit-tool.ts +40 -61
  58. package/src/task-ledger.ts +14 -55
  59. package/src/transport-budget.ts +32 -0
  60. package/src/verification-capability.ts +298 -102
  61. package/src/verification-environment.ts +213 -0
  62. package/src/verification-identity.ts +77 -0
  63. package/src/worker-broker.ts +134 -91
  64. package/src/workflow-engine.ts +28 -8285
  65. package/src/workflow-policy.ts +1388 -0
  66. package/src/workflow-recovery-policy.ts +95 -0
  67. package/src/workflow-scheduling.ts +105 -0
  68. package/src/workflow-state-machine.ts +4086 -0
  69. package/src/workflow-status.ts +384 -0
  70. package/src/workspace-io-entry.mjs +58 -0
  71. package/src/workspace-io-worker.mjs +991 -0
  72. package/src/workspace-io.ts +84 -0
  73. package/src/workspace-store.ts +125 -19
  74. package/skills/abel-workflow/SKILL.md +0 -205
  75. package/src/empty-resource-loader.ts +0 -38
  76. package/src/parent-payload-bridge.ts +0 -523
package/src/index.ts CHANGED
@@ -1,20 +1,26 @@
1
+ import { compareCanonicalStrings } from "./canonical.ts";
2
+ import { CHANGE_CONTRACT_SCHEMA } from "./change-contract-schema.ts";
3
+ import {
4
+ projectDesignDiagnostic,
5
+ type SafeDesignDiagnostic,
6
+ } from "./design-diagnostics.ts";
7
+ import { openPackageWorkflowService } from "./package-workflow.ts";
8
+ import { packageContext } from "./pi-adapter.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
- import { AsyncLocalStorage } from "node:async_hooks";
7
- import { createHash } from "node:crypto";
8
- import {
9
- lstatSync,
10
- mkdirSync,
11
- mkdtempSync,
12
- readFileSync,
13
- realpathSync,
14
- rmSync,
15
- } from "node:fs";
16
- import { homedir } from "node:os";
17
- import path, { dirname, join } from "node:path";
22
+
23
+ import { dirname, join } from "node:path";
18
24
  import { fileURLToPath } from "node:url";
19
25
  import type {
20
26
  AgentToolResult,
@@ -25,55 +31,28 @@ import type {
25
31
  ToolRenderResultOptions,
26
32
  } from "@earendil-works/pi-coding-agent";
27
33
  import { type Activation, activateTool, deactivateTool } from "./activation.ts";
28
- import { loadAgentDefinitions } from "./agent-registry.ts";
29
- import { runChildSession } from "./child-session.ts";
30
- import {
31
- type AtomicVerificationContract,
32
- type DesignEvidenceResult,
33
- isValidRelativePath,
34
- LIMITS,
35
- type StructuredVerificationContract,
36
- verificationSteps,
37
- } from "./contracts.ts";
34
+
35
+ import type { DesignEvidenceResult } from "./contracts.ts";
38
36
  import {
39
37
  CONTROL_COMMAND_PARAMETERS,
40
38
  canonicalizeControlCommandToolInput,
41
39
  validateControlCommand,
42
40
  } from "./control-contracts.ts";
43
41
  import {
44
- assessDeliveryTraceability,
45
- compileImplementPlan,
46
42
  DeliveryValidationError,
47
43
  DesignPlanValidationError,
48
- type GateApprovalProof,
49
- parseGateAReceipt,
50
- parseImplementPlan,
51
- parseReadyReceipt,
52
44
  } from "./delivery-compiler.ts";
53
45
  import {
54
- DesignController,
46
+ DesignControlValidationError,
55
47
  DesignFinalizationError,
56
48
  validateDesignControlRequest,
57
49
  } 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";
50
+ import { canonicalJson } from "./implement-graph.ts";
64
51
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
65
- import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
66
- import { runtimeForWorkerRoute } from "./parent-provider.ts";
67
- import {
68
- inspectRoutePolicy,
69
- loadRoutePolicy,
70
- type RoutePolicyResolution,
71
- unavailableRoutePolicy,
72
- type WorkerRoutePolicy,
73
- } from "./route-policy.ts";
52
+
74
53
  import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
75
- import { observeSafePath } from "./safe-path.ts";
76
- import { resolveStateRoot, StateRootError } from "./state-root.ts";
54
+
55
+ import { StateRootError } from "./state-root.ts";
77
56
  import {
78
57
  ACTIVITY_DETAILS_KEY,
79
58
  ActivityController,
@@ -81,17 +60,6 @@ import {
81
60
  renderActivityResult,
82
61
  type WorkflowActivityUpdate,
83
62
  } from "./subagent-activity.ts";
84
- import { classifyCandidateContextRequest } from "./submit-tool.ts";
85
- 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";
95
63
 
96
64
  export {
97
65
  inspectOpenSpecDelivery,
@@ -293,9 +261,9 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
293
261
  operationId: { type: "string" },
294
262
  gate: { type: "string", enum: ["gate-a"] },
295
263
  contract: {
296
- type: "string",
264
+ anyOf: [{ type: "string" }, CHANGE_CONTRACT_SCHEMA],
297
265
  description:
298
- "Complete approved WHAT contract; code hashes it and never persists the text.",
266
+ "ChangeContract: {goal, acceptance:[{id,statement,verification}], constraints:[{id,statement}], policy:{writeRoots,dependencies,verificationModes}}. Pass the example's changeContract object, not its whole plan. Legacy prose remains readable; code hashes authority.",
299
267
  },
300
268
  },
301
269
  required: ["operation", "runId", "operationId", "gate", "contract"],
@@ -325,7 +293,7 @@ const DESIGN_CONTROL_REQUEST_SCHEMA = {
325
293
  content: {
326
294
  type: "string",
327
295
  maxLength: 16 * 1024 * 1024,
328
- description: "Exact bounded UTF-8 artifact content.",
296
+ description: `Exact bounded UTF-8 artifact content. For plan-draft.json, omit tracking, phase verificationInputs and relatedTests disposition to derive them. Read the complete example at ${join(PACKAGE_ROOT, "config/plan-draft.example.json")}.`,
329
297
  },
330
298
  },
331
299
  required: ["operation", "runId", "operationId", "path", "content"],
@@ -363,11 +331,13 @@ function invokedPrompt(text: string): EligiblePrompt | undefined {
363
331
  return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
364
332
  }
365
333
 
366
- function promptMarker(name: EligiblePrompt): string {
334
+ type WorkflowPrompt = EligiblePrompt | "abel-init";
335
+
336
+ function promptMarker(name: WorkflowPrompt): string {
367
337
  return `<!-- ABEL:PROMPT:${name} -->`;
368
338
  }
369
339
 
370
- function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
340
+ function hasPackageProvenance(pi: ExtensionAPI, name: WorkflowPrompt): boolean {
371
341
  const commands = pi
372
342
  .getCommands()
373
343
  .filter((candidate) => candidate.name === name);
@@ -382,7 +352,7 @@ function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
382
352
 
383
353
  function hasExpandedPromptMarker(
384
354
  prompt: string,
385
- name: EligiblePrompt,
355
+ name: WorkflowPrompt,
386
356
  ): boolean {
387
357
  const requestEnd = prompt.lastIndexOf("</abel-request>");
388
358
  if (requestEnd < 0) return false;
@@ -438,7 +408,7 @@ type SafeDesignFailure = {
438
408
  kind: "design-control-failure";
439
409
  operation: string;
440
410
  code: string;
441
- diagnostics: ReadonlyArray<Record<string, string | boolean>>;
411
+ diagnostics: readonly SafeDesignDiagnostic[];
442
412
  };
443
413
 
444
414
  type DesignFailureBoundary =
@@ -487,6 +457,13 @@ function classifyDesignFailure(
487
457
  field?: string;
488
458
  } {
489
459
  const message = error instanceof Error ? error.message : "";
460
+ if (error instanceof DesignControlValidationError) {
461
+ return {
462
+ code: "invalid-design-control-request",
463
+ category: "validation",
464
+ retryable: false,
465
+ };
466
+ }
490
467
  if (error instanceof DesignPlanValidationError) {
491
468
  return {
492
469
  code: "design-plan-validation-invalid",
@@ -609,53 +586,11 @@ function safeDesignFailure(
609
586
  : [];
610
587
  const diagnostics = rawDiagnostics
611
588
  .flatMap((candidate) => {
612
- if (typeof candidate === "string") {
613
- const diagnosticCode = candidate.split(":", 1)[0] ?? "";
614
- return SAFE_DESIGN_ERROR_CODE.test(diagnosticCode)
615
- ? [{ code: diagnosticCode }]
616
- : [];
617
- }
618
- if (
619
- candidate === null ||
620
- typeof candidate !== "object" ||
621
- Array.isArray(candidate)
622
- ) {
623
- return [];
624
- }
625
- const admitted = Object.fromEntries(
626
- Object.entries(candidate as Record<string, unknown>)
627
- .filter((entry): entry is [string, string | boolean] => {
628
- if (entry[0] === "retryable") {
629
- return typeof entry[1] === "boolean";
630
- }
631
- return (
632
- [
633
- "code",
634
- "taskId",
635
- "phase",
636
- "field",
637
- "category",
638
- "owner",
639
- "verificationId",
640
- "outputId",
641
- "dependencyTaskId",
642
- "producerTaskId",
643
- "producerPhase",
644
- "command",
645
- "reason",
646
- "systemCode",
647
- "exitCode",
648
- ].includes(entry[0]) &&
649
- typeof entry[1] === "string" &&
650
- SAFE_DESIGN_ERROR_CODE.test(entry[1])
651
- );
652
- })
653
- .sort(([left], [right]) => left.localeCompare(right)),
654
- );
655
- return typeof admitted.code === "string" ? [admitted] : [];
589
+ const diagnostic = projectDesignDiagnostic(candidate);
590
+ return diagnostic ? [diagnostic] : [];
656
591
  })
657
592
  .sort((left, right) =>
658
- canonicalJson(left).localeCompare(canonicalJson(right)),
593
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
659
594
  )
660
595
  .filter(
661
596
  (candidate, index, values) =>
@@ -687,6 +622,11 @@ export interface WorkflowControlEngine {
687
622
  onActivity?: (event: WorkflowActivityUpdate) => void,
688
623
  ): Promise<Record<string, unknown>>;
689
624
  executeDesign?(request: unknown): Promise<Record<string, unknown>>;
625
+ executeAmendment?(
626
+ change: string,
627
+ batchId: string,
628
+ request: unknown,
629
+ ): Promise<Record<string, unknown>>;
690
630
  assertDesignRun?(runId: string): void;
691
631
  recordDesignEvidence?(input: {
692
632
  runId: string;
@@ -697,1240 +637,22 @@ export interface WorkflowControlEngine {
697
637
 
698
638
  export type WorkflowControlEngineFactory = (
699
639
  ctx: ExtensionContext,
700
- parentPayloadBridge: ParentPayloadBridge,
701
640
  ) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
702
641
 
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
- function sha256(value: Uint8Array | string): string {
733
- return createHash("sha256").update(value).digest("hex");
734
- }
735
-
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
- function affectedVerification(
1597
- task: Parameters<
1598
- Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
1599
- >[0]["task"],
1600
- ): StructuredVerificationContract {
1601
- if (task.affectedVerification) {
1602
- return structuredClone(task.affectedVerification);
1603
- }
1604
- const finalPhase = task.phases.refactor ?? task.phases.green;
1605
- const verification = structuredClone(finalPhase.verification);
1606
- const affected = [...new Set(task.impactClosure.affectedSuite)].sort();
1607
- if (affected.length === 0) return verification;
1608
- if (verification.kind === "vitest") {
1609
- return { ...verification, testFiles: affected };
1610
- }
1611
- if (verification.kind === "steps") {
1612
- return {
1613
- ...verification,
1614
- steps: verification.steps.map((step) =>
1615
- step.kind === "vitest" ? { ...step, testFiles: affected } : step,
1616
- ),
1617
- };
1618
- }
1619
- return verification;
1620
- }
1621
-
1622
- function childRequestId(
1623
- operationId: string,
1624
- taskId: string,
1625
- phase: string,
1626
- ): string {
1627
- return `child-${sha256(`${operationId}\0${taskId}\0${phase}`).slice(0, 40)}`;
1628
- }
1629
-
642
+ /** Pi adapter: project host capabilities before entering the package services. */
1630
643
  export function openPackageWorkflowControlEngine(
1631
644
  initialContext: ExtensionContext,
1632
- parentPayloadBridge: ParentPayloadBridge,
1633
645
  ): WorkflowControlEngine {
1634
- const consumerRoot = path.resolve(initialContext.cwd);
1635
- const routeResolution = loadRoutePolicy({
1636
- cwd: consumerRoot,
1637
- home: homedir(),
1638
- ...(initialContext.model ? { parentModel: initialContext.model } : {}),
1639
- });
1640
- const stateRoot = resolveStateRoot({
1641
- consumerRoot,
1642
- xdgStateHome: process.env.XDG_STATE_HOME,
1643
- });
1644
- const contexts = new AsyncLocalStorage<ExtensionContext>();
1645
- const design = DesignController.open({
1646
- consumerRoot,
1647
- stateRoot,
1648
- inspectOpenSpec: inspectOpenSpecDelivery,
1649
- });
1650
- const implementationAgent = loadAgentDefinitions().find(
1651
- (agent) => agent.role === "implementation-worker",
1652
- );
1653
- if (!implementationAgent) {
1654
- throw new Error("implementation-worker-agent-unavailable");
1655
- }
1656
- const engine = openDurableWorkflowEngine({
1657
- consumerRoot,
1658
- stateRoot,
1659
- deliverySource: packageDeliverySource(consumerRoot, {
1660
- verifyGateProof: (input) => design.verifyGateProof(input),
1661
- verifyFinalizedDelivery: (input) => design.verifyFinalizedDelivery(input),
1662
- }),
1663
- routePolicy: routeResolution.ok
1664
- ? routeResolution.policy
1665
- : unavailableRoutePolicy(),
1666
- proposeCandidate: async (input) => {
1667
- const context = contexts.getStore();
1668
- if (!context) {
1669
- return { kind: "paused", code: "parent-context-unavailable" };
1670
- }
1671
- const phaseRuntime = await runtimeForWorkerRoute(
1672
- input.route as WorkerRoutePolicy,
1673
- context,
1674
- parentPayloadBridge,
1675
- input.signal,
1676
- process.env,
1677
- { onResponse: input.onHeaders },
1678
- );
1679
- if (!phaseRuntime.ok) {
1680
- if (phaseRuntime.failure.kind === "cancelled") {
1681
- return { kind: "operation-cancelled", code: "cancelled" };
1682
- }
1683
- throw new Error(phaseRuntime.failure.code);
1684
- }
1685
- const phase = input.task.phases[input.phase];
1686
- if (!phase) {
1687
- return { kind: "paused", code: "task-phase-unavailable" };
1688
- }
1689
- const taskPhases = Object.values(input.task.phases);
1690
- const executionBoundary = input.artifactCorrection
1691
- ? {
1692
- read: [
1693
- ...new Set(
1694
- taskPhases.flatMap((boundary) => [
1695
- ...boundary.read,
1696
- ...boundary.write,
1697
- ...boundary.delete,
1698
- ]),
1699
- ),
1700
- ].sort(),
1701
- write: [
1702
- ...new Set(taskPhases.flatMap((boundary) => boundary.write)),
1703
- ].sort(),
1704
- delete: [
1705
- ...new Set(taskPhases.flatMap((boundary) => boundary.delete)),
1706
- ].sort(),
1707
- }
1708
- : {
1709
- read: [...phase.read],
1710
- write: [...phase.write],
1711
- delete: [...phase.delete],
1712
- };
1713
- const phaseContract = {
1714
- candidateId: input.candidateArtifact.identity.candidateId,
1715
- taskId: input.taskId,
1716
- phase: input.phase,
1717
- readSet: executionBoundary.read,
1718
- writeSet: executionBoundary.write,
1719
- deleteSet: executionBoundary.delete,
1720
- verification: structuredClone(phase.verification),
1721
- agentsImpact: input.task.agents.impact,
1722
- agentsTarget: input.task.agents.target ?? null,
1723
- agentsManagedOnly: true,
1724
- agentsWriteAllowed: false,
1725
- impactClosure: structuredClone(input.task.impactClosure),
1726
- ...(input.artifactCorrection
1727
- ? { artifactCorrection: structuredClone(input.artifactCorrection) }
1728
- : {}),
1729
- ...(input.contextRequest
1730
- ? { requestedContext: structuredClone(input.contextRequest) }
1731
- : {}),
1732
- ...(input.repair
1733
- ? {
1734
- repair: {
1735
- attempt: input.repair.attempt,
1736
- attribution: input.repair.attribution,
1737
- failureIdentities: [...input.repair.failureIdentities],
1738
- verification: structuredClone(input.task.repairVerification),
1739
- inBoundaryOnly: true,
1740
- },
1741
- }
1742
- : {}),
1743
- };
1744
- const child = await runChildSession({
1745
- cwd: input.workspaceRoot,
1746
- modelRuntime: phaseRuntime.modelRuntime,
1747
- model: phaseRuntime.model,
1748
- systemPrompt: [
1749
- implementationAgent.content,
1750
- input.task.objective,
1751
- input.task.context.agents,
1752
- input.task.context.contract,
1753
- `<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
1754
- ].join("\n\n"),
1755
- requestId: childRequestId(input.operationId, input.taskId, input.phase),
1756
- taskId: input.taskId,
1757
- role: "implementation-worker",
1758
- phase: input.phase,
1759
- output: "diff",
1760
- roots: input.task.roots.map((root) =>
1761
- path.resolve(input.workspaceRoot, root),
1762
- ),
1763
- allowedPaths: [
1764
- ...new Set([
1765
- ...executionBoundary.read,
1766
- ...executionBoundary.write,
1767
- ...executionBoundary.delete,
1768
- ]),
1769
- ],
1770
- timeoutMs: LIMITS.phaseTimeoutMs,
1771
- signal: input.signal,
1772
- failureOverride: phaseRuntime.failureOverride,
1773
- ledgerProjection: input.ledgerProjection,
1774
- candidateArtifact: input.candidateArtifact,
1775
- onStreamProgress: input.onProgress,
1776
- });
1777
- if (!child.ok) {
1778
- if (child.failure.kind === "transport") {
1779
- throw new Error(child.failure.code);
1780
- }
1781
- if (child.failure.kind === "cancelled") {
1782
- return { kind: "operation-cancelled", code: "cancelled" };
1783
- }
1784
- if (child.failure.kind === "approval-boundary") {
1785
- return { kind: "approval-needed", code: child.failure.code };
1786
- }
1787
- if (child.failure.kind === "result-limit") {
1788
- return { kind: "paused", code: "needs-task-split" };
1789
- }
1790
- const attemptDiagnostic = {
1791
- finalCategory: child.classification.finalCategory,
1792
- submitAttempts: child.classification.attempts,
1793
- schema: child.classification.schema,
1794
- identityMismatch: Object.entries(child.classification.identity)
1795
- .filter(([, matches]) => !matches)
1796
- .map(([dimension]) => dimension),
1797
- };
1798
- return child.failure.kind === "environment" ||
1799
- child.failure.kind === "verification-adapter"
1800
- ? { kind: "paused", code: child.failure.code, attemptDiagnostic }
1801
- : { kind: "retryable", code: child.failure.code, attemptDiagnostic };
1802
- }
1803
- const result = child.result;
1804
- if (result.kind === "context-request") {
1805
- return classifyCandidateContextRequest(result, {
1806
- phase: input.phase,
1807
- readPaths: phase.read,
1808
- writePaths: phase.write,
1809
- deletePaths: phase.delete,
1810
- taskPaths: [
1811
- ...new Set(
1812
- Object.values(input.task.phases).flatMap((boundary) => [
1813
- ...boundary.read,
1814
- ...boundary.write,
1815
- ...boundary.delete,
1816
- ]),
1817
- ),
1818
- ],
1819
- redWritePaths: input.task.phases.red.write,
1820
- agents: {
1821
- impact: input.task.agents.impact,
1822
- ...(input.task.agents.target
1823
- ? { target: input.task.agents.target }
1824
- : {}),
1825
- },
1826
- });
1827
- }
1828
- if (result.kind !== "sealed-candidate") {
1829
- return { kind: "retryable", code: "candidate-diff-invalid" };
1830
- }
1831
- return {
1832
- kind: "sealed-candidate",
1833
- candidateId: result.candidateId,
1834
- artifactHash: result.artifactHash,
1835
- bytes: result.bytes,
1836
- paths: [...result.paths],
1837
- };
1838
- },
1839
- verifyPhase: (input) =>
1840
- executePackageVerification({
1841
- root: input.root,
1842
- dependencyOwner: consumerRoot,
1843
- verification: input.verification,
1844
- signal: input.signal,
1845
- }),
1846
- verifyChange: async (input) => {
1847
- const verifications = input.verification
1848
- ? [input.verification]
1849
- : input.plan.tasks.map((task) => affectedVerification(task));
1850
- for (const verification of verifications) {
1851
- const result = await executePackageVerification({
1852
- root: input.root,
1853
- dependencyOwner: consumerRoot,
1854
- verification,
1855
- signal: input.signal,
1856
- });
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
- }
1877
- }
1878
- return {
1879
- ok: true,
1880
- exitCode: 0,
1881
- classification: "expected-green",
1882
- };
1883
- },
1884
- });
646
+ const service = openPackageWorkflowService(packageContext(initialContext));
1885
647
  return {
1886
- async execute(
1887
- command: unknown,
1888
- context = initialContext,
1889
- signal,
1890
- onActivity,
1891
- ) {
1892
- const operationRouteResolution = loadRoutePolicy({
1893
- cwd: consumerRoot,
1894
- home: homedir(),
1895
- ...(context.model ? { parentModel: context.model } : {}),
1896
- });
1897
- engine.updateRoutePolicy(
1898
- operationRouteResolution.ok
1899
- ? operationRouteResolution.policy
1900
- : unavailableRoutePolicy(),
648
+ ...service,
649
+ execute(command, context = initialContext, signal, onActivity) {
650
+ return service.execute(
651
+ command,
652
+ packageContext(context),
653
+ signal,
654
+ onActivity,
1901
655
  );
1902
- const validation = validateControlCommand(command);
1903
- if (!validation.ok) {
1904
- const error = new Error(validation.code);
1905
- error.name = "ControlCommandError";
1906
- throw error;
1907
- }
1908
- return contexts.run(context, async () => {
1909
- const outcome = await engine.execute(
1910
- validation.value,
1911
- signal,
1912
- onActivity,
1913
- );
1914
- const routePolicy = visibleRoutePolicyStatus(
1915
- operationRouteResolution,
1916
- engine.routePolicyStatus(),
1917
- );
1918
- return { ...outcome, routePolicy };
1919
- });
1920
- },
1921
- executeDesign(request: unknown) {
1922
- return design.execute(request);
1923
- },
1924
- assertDesignRun(runId: string) {
1925
- design.assertDesignRun(runId);
1926
- },
1927
- recordDesignEvidence(input) {
1928
- return design.recordEvidence(input);
1929
- },
1930
- async close() {
1931
- contexts.disable();
1932
- await engine.close();
1933
- design.close();
1934
656
  },
1935
657
  };
1936
658
  }
@@ -1968,27 +690,17 @@ function runStoreResetError(error: RunStoreFormatError): Error {
1968
690
  return reset;
1969
691
  }
1970
692
 
1971
- function visibleRoutePolicyStatus(
1972
- resolution: RoutePolicyResolution,
1973
- brokerStatus: Record<string, unknown> | undefined,
1974
- ): Record<string, unknown> {
1975
- if (!resolution.ok) return inspectRoutePolicy(resolution);
1976
- const inspected = brokerStatus ?? inspectRoutePolicy(resolution);
1977
- return {
1978
- ...inspected,
1979
- source: { kind: resolution.source.kind },
1980
- };
1981
- }
1982
-
1983
693
  export function registerWorkflowControl(
1984
694
  pi: ExtensionAPI,
1985
695
  engineFactory: WorkflowControlEngineFactory = openPackageWorkflowControlEngine,
1986
696
  ): void {
1987
- const parentPayloadBridge = new ParentPayloadBridge();
1988
697
  const activity = new ActivityController();
1989
698
  const engines = new Map<string, Promise<WorkflowControlEngine>>();
699
+ const designOperations = new Set<Promise<void>>();
1990
700
  const designFailures = new Map<string, SafeDesignFailure>();
1991
701
  let pendingPrompt: EligiblePrompt | undefined;
702
+ let pendingInit = false;
703
+ let exitingStage = false;
1992
704
  let activePrompt: EligiblePrompt | undefined;
1993
705
  let designToolSnapshot: string[] | undefined;
1994
706
  const activation = new (class implements Activation {
@@ -2016,13 +728,13 @@ export function registerWorkflowControl(
2016
728
  return true;
2017
729
  }
2018
730
  })();
2019
- const packetRuntime = new PacketRuntime({ activation, parentPayloadBridge });
731
+ const packetRuntime = new PacketRuntime({ activation });
2020
732
 
2021
733
  const engineFor = (ctx: ExtensionContext) => {
2022
734
  const key = ctx.cwd;
2023
735
  const existing = engines.get(key);
2024
736
  if (existing) return existing;
2025
- const opened = Promise.resolve(engineFactory(ctx, parentPayloadBridge));
737
+ const opened = Promise.resolve(engineFactory(ctx));
2026
738
  engines.set(key, opened);
2027
739
  void opened.catch(() => {
2028
740
  if (engines.get(key) === opened) engines.delete(key);
@@ -2030,16 +742,19 @@ export function registerWorkflowControl(
2030
742
  return opened;
2031
743
  };
2032
744
  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
- ),
745
+ // Design close() is synchronous and does not own an operation queue.
746
+ // Keep its journal open through finalization's commit/lease-cleanup path
747
+ // and the dispatch result handling before closing any engine storage.
748
+ await Promise.all([...designOperations]);
749
+ const closed = await Promise.allSettled(
750
+ [...engines].map(async ([key, opened]) => {
751
+ const engine = await opened;
752
+ await engine.close();
753
+ if (engines.get(key) === opened) engines.delete(key);
754
+ }),
2042
755
  );
756
+ const failed = closed.find((result) => result.status === "rejected");
757
+ if (failed?.status === "rejected") throw failed.reason;
2043
758
  };
2044
759
  const restoreDesignTools = () => {
2045
760
  if (!designToolSnapshot) return;
@@ -2075,7 +790,16 @@ export function registerWorkflowControl(
2075
790
  activePrompt = undefined;
2076
791
  await packetRuntime.drain();
2077
792
  deactivate();
2078
- parentPayloadBridge.clear();
793
+ };
794
+ const exitStage = async () => {
795
+ if (exitingStage) throw new Error("stage-control-mismatch");
796
+ exitingStage = true;
797
+ try {
798
+ await closeEngines();
799
+ await deactivateStage();
800
+ } finally {
801
+ exitingStage = false;
802
+ }
2079
803
  };
2080
804
 
2081
805
  // Pi validates tool arguments before execute(), so expose only the schema
@@ -2102,6 +826,61 @@ export function registerWorkflowControl(
2102
826
  } as const;
2103
827
  let registeredParameterKind: "command" | "packet" | undefined;
2104
828
 
829
+ // Stage exit is session control, not a durable Implement command. Keep the
830
+ // six engine commands unchanged and admit only this exact extra envelope.
831
+ const FINISH_PARAMETERS = {
832
+ type: "object",
833
+ properties: { action: { type: "string", enum: ["finish"] } },
834
+ required: ["action"],
835
+ additionalProperties: false,
836
+ } as const;
837
+ const AMEND_PARAMETERS = {
838
+ type: "object",
839
+ properties: {
840
+ action: { type: "string", enum: ["amend"] },
841
+ change: { type: "string" },
842
+ batchId: { type: "string" },
843
+ request: DESIGN_CONTROL_REQUEST_SCHEMA,
844
+ },
845
+ required: ["action", "change", "batchId", "request"],
846
+ additionalProperties: false,
847
+ } as const;
848
+ const IMPLEMENT_PARAMETERS = {
849
+ ...CONTROL_COMMAND_PARAMETERS,
850
+ properties: {
851
+ ...CONTROL_COMMAND_PARAMETERS.properties,
852
+ ...AMEND_PARAMETERS.properties,
853
+ action: { type: "string", enum: ["finish", "amend"] },
854
+ },
855
+ required: [],
856
+ anyOf: [
857
+ ...CONTROL_COMMAND_PARAMETERS.anyOf,
858
+ FINISH_PARAMETERS,
859
+ AMEND_PARAMETERS,
860
+ ],
861
+ } as const;
862
+ const prepareImplementArguments = (args: unknown): unknown => {
863
+ if (!args || typeof args !== "object" || Array.isArray(args)) return args;
864
+ const record = { ...(args as Record<string, unknown>) };
865
+ // Strict providers can pad fields belonging to the other union branch.
866
+ if (record.action !== "amend") {
867
+ for (const key of ["batchId", "request"]) {
868
+ if (record[key] === null || record[key] === undefined)
869
+ delete record[key];
870
+ }
871
+ }
872
+ if (record.action === "finish" || record.action === "amend") {
873
+ for (const key of Object.keys(CONTROL_COMMAND_PARAMETERS.properties)) {
874
+ if (record[key] === null || record[key] === undefined)
875
+ delete record[key];
876
+ }
877
+ return record;
878
+ }
879
+ if (record.action === null || record.action === undefined)
880
+ delete record.action;
881
+ return canonicalizeControlCommandToolInput(record);
882
+ };
883
+
2105
884
  const registerDispatchTool = (kind: "command" | "packet") => {
2106
885
  if (registeredParameterKind === kind) return;
2107
886
  registeredParameterKind = kind;
@@ -2110,17 +889,16 @@ export function registerWorkflowControl(
2110
889
  label: "Abel Control",
2111
890
  description:
2112
891
  kind === "command"
2113
- ? "Private stage-bound Abel workflow control. Accepts durable Implement change commands."
892
+ ? '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
893
  : "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
2115
894
  executionMode: "parallel",
2116
895
  ...(kind === "command"
2117
896
  ? {
2118
897
  prepareArguments: (args: unknown) =>
2119
- canonicalizeControlCommandToolInput(args) as never,
898
+ prepareImplementArguments(args) as never,
2120
899
  }
2121
900
  : {}),
2122
- parameters:
2123
- kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
901
+ parameters: kind === "command" ? IMPLEMENT_PARAMETERS : PACKET_PARAMETERS,
2124
902
  async execute(
2125
903
  toolCallId: string,
2126
904
  params: unknown,
@@ -2128,10 +906,127 @@ export function registerWorkflowControl(
2128
906
  onUpdate: AgentToolUpdateCallback<unknown> | undefined,
2129
907
  ctx: ExtensionContext,
2130
908
  ) {
909
+ if (
910
+ exitingStage ||
911
+ !activation.isActive() ||
912
+ activePrompt === undefined
913
+ ) {
914
+ throw new Error("stage-control-mismatch");
915
+ }
916
+ if (kind === "command") params = prepareImplementArguments(params);
2131
917
  const record =
2132
918
  params && typeof params === "object" && !Array.isArray(params)
2133
919
  ? (params as Record<string, unknown>)
2134
920
  : undefined;
921
+ if (record?.action === "amend") {
922
+ if (
923
+ activePrompt !== "abel-implement" ||
924
+ Object.keys(record).some(
925
+ (key) =>
926
+ !["action", "change", "batchId", "request"].includes(key),
927
+ ) ||
928
+ typeof record.change !== "string" ||
929
+ typeof record.batchId !== "string"
930
+ ) {
931
+ throw new Error("stage-control-mismatch");
932
+ }
933
+ const parsed = validateDesignControlRequest(record.request);
934
+ if (!parsed.ok) {
935
+ const error = new DesignControlValidationError(parsed.diagnostics);
936
+ designFailures.set(
937
+ toolCallId,
938
+ safeDesignFailure(error, "validation"),
939
+ );
940
+ throw error;
941
+ }
942
+ const request = parsed.value;
943
+ if (
944
+ request.operation === "bind-change" ||
945
+ (request.operation === "start" &&
946
+ (!("change" in request) || request.change !== record.change))
947
+ ) {
948
+ throw new Error("amendment-change-mismatch");
949
+ }
950
+ let settle!: () => void;
951
+ const pending = new Promise<void>((resolve) => {
952
+ settle = resolve;
953
+ });
954
+ designOperations.add(pending);
955
+ try {
956
+ const engine = await engineFor(ctx);
957
+ if (exitingStage || !activation.isActive() || !engine.executeDesign)
958
+ throw new Error("stage-control-mismatch");
959
+ const status = await engine.execute(
960
+ {
961
+ command: "status",
962
+ stage: "abel-implement",
963
+ change: record.change,
964
+ },
965
+ ctx,
966
+ signal,
967
+ );
968
+ const batch = status.decisionBatch as { id?: string } | undefined;
969
+ if (
970
+ !["approval-needed", "paused"].includes(String(status.state)) ||
971
+ batch?.id !== record.batchId
972
+ )
973
+ throw new Error("amendment-batch-stale");
974
+ if (request.operation !== "start") {
975
+ const designStatus = await engine.executeDesign({
976
+ operation: "status",
977
+ runId: request.runId,
978
+ });
979
+ if (designStatus.change !== record.change)
980
+ throw new Error("amendment-change-mismatch");
981
+ }
982
+ if (!engine.executeAmendment)
983
+ throw new Error("amendment-control-unavailable");
984
+ const revision = await engine.executeAmendment(
985
+ record.change,
986
+ record.batchId,
987
+ request,
988
+ );
989
+ const payload =
990
+ request.operation === "finalize-delivery" &&
991
+ revision.state === "completed"
992
+ ? {
993
+ ...revision,
994
+ scope: "amendment",
995
+ state: "ready",
996
+ completed: false,
997
+ runId: status.runId,
998
+ amendmentRunId: revision.runId,
999
+ stage: "abel-implement",
1000
+ }
1001
+ : { ...revision, scope: "amendment" };
1002
+ // A compiled amendment stays in Implement; its private Design
1003
+ // records do not activate the Design stage or broaden parent tools.
1004
+ return {
1005
+ content: [
1006
+ { type: "text" as const, text: JSON.stringify(payload) },
1007
+ ],
1008
+ details: payload,
1009
+ };
1010
+ } catch (error) {
1011
+ if (
1012
+ error instanceof DesignPlanValidationError ||
1013
+ error instanceof DesignFinalizationError ||
1014
+ error instanceof DeliveryValidationError
1015
+ ) {
1016
+ const failure = safeDesignFailure(
1017
+ error,
1018
+ request.operation,
1019
+ "execution",
1020
+ );
1021
+ designFailures.set(toolCallId, failure);
1022
+ throw new Error(failure.code);
1023
+ }
1024
+ throw error;
1025
+ } finally {
1026
+ settle();
1027
+ designOperations.delete(pending);
1028
+ }
1029
+ }
2135
1030
  if (record && typeof record.action === "string") {
2136
1031
  if (
2137
1032
  Object.keys(record).some(
@@ -2143,6 +1038,18 @@ export function registerWorkflowControl(
2143
1038
  if (record.action === "finish" && Object.hasOwn(record, "request")) {
2144
1039
  throw new Error("control-envelope-ambiguous");
2145
1040
  }
1041
+ if (record.action === "finish") {
1042
+ // close() aborts and settles active operations before ordinary tools
1043
+ // return; durable work is retained, never discarded or completed.
1044
+ await exitStage();
1045
+ const payload = { ok: true, state: "inactive" };
1046
+ return {
1047
+ content: [
1048
+ { type: "text" as const, text: JSON.stringify(payload) },
1049
+ ],
1050
+ details: payload,
1051
+ };
1052
+ }
2146
1053
  if (
2147
1054
  activePrompt !== "abel-design" &&
2148
1055
  activePrompt !== "abel-diagnose"
@@ -2154,10 +1061,28 @@ export function registerWorkflowControl(
2154
1061
  throw new Error("stage-control-mismatch");
2155
1062
  }
2156
1063
  const designRequest = validateDesignControlRequest(record.request);
2157
- if (!designRequest.ok) throw new Error(designRequest.code);
1064
+ if (!designRequest.ok) {
1065
+ const error = new DesignControlValidationError(
1066
+ designRequest.diagnostics,
1067
+ );
1068
+ designFailures.set(
1069
+ toolCallId,
1070
+ safeDesignFailure(error, "validation"),
1071
+ );
1072
+ throw error;
1073
+ }
1074
+ let settleDesign!: () => void;
1075
+ const settled = new Promise<void>((resolve) => {
1076
+ settleDesign = resolve;
1077
+ });
1078
+ // Register before the first await, including asynchronous engine
1079
+ // opening. Teardown blocks admission before taking its snapshot.
1080
+ designOperations.add(settled);
2158
1081
  let boundary: DesignFailureBoundary = "initialization";
2159
1082
  try {
2160
1083
  const engine = await engineFor(ctx);
1084
+ if (exitingStage || !activation.isActive())
1085
+ throw new Error("stage-control-mismatch");
2161
1086
  if (!engine.executeDesign)
2162
1087
  throw new Error("design-control-unavailable");
2163
1088
  boundary = "execution";
@@ -2170,7 +1095,9 @@ export function registerWorkflowControl(
2170
1095
  boundary = "execution";
2171
1096
  if (
2172
1097
  designRequest.value.operation === "finalize-delivery" &&
2173
- payload.state === "completed"
1098
+ payload.state === "completed" &&
1099
+ !exitingStage &&
1100
+ activePrompt === "abel-design"
2174
1101
  ) {
2175
1102
  await deactivateStage();
2176
1103
  }
@@ -2188,6 +1115,9 @@ export function registerWorkflowControl(
2188
1115
  const sanitized = new Error(failure.code);
2189
1116
  sanitized.name = "DesignControlError";
2190
1117
  throw sanitized;
1118
+ } finally {
1119
+ designOperations.delete(settled);
1120
+ settleDesign();
2191
1121
  }
2192
1122
  }
2193
1123
  let designEngine: WorkflowControlEngine | undefined;
@@ -2201,6 +1131,8 @@ export function registerWorkflowControl(
2201
1131
  if (packet.value.stage === "abel-design") {
2202
1132
  try {
2203
1133
  designEngine = await engineFor(ctx);
1134
+ if (exitingStage || !activation.isActive())
1135
+ throw new Error("stage-control-mismatch");
2204
1136
  } catch (error) {
2205
1137
  if (error instanceof RunStoreFormatError)
2206
1138
  throw runStoreResetError(error);
@@ -2220,7 +1152,7 @@ export function registerWorkflowControl(
2220
1152
  ? await packetRuntime.execute(
2221
1153
  record.action,
2222
1154
  operation,
2223
- ctx,
1155
+ packageContext(ctx),
2224
1156
  signal,
2225
1157
  activity.observe(
2226
1158
  toolCallId,
@@ -2230,7 +1162,7 @@ export function registerWorkflowControl(
2230
1162
  : await packetRuntime.execute(
2231
1163
  record.action,
2232
1164
  operation,
2233
- ctx,
1165
+ packageContext(ctx),
2234
1166
  signal,
2235
1167
  );
2236
1168
  let payload:
@@ -2257,9 +1189,6 @@ export function registerWorkflowControl(
2257
1189
  });
2258
1190
  payload = { ...packetPayload, recordedEvidence };
2259
1191
  }
2260
- if (record.action === "finish" && packetPayload.ok) {
2261
- await deactivateStage();
2262
- }
2263
1192
  const display = tuiRun
2264
1193
  ? activity.finalize(toolCallId, payload)
2265
1194
  : undefined;
@@ -2287,15 +1216,16 @@ export function registerWorkflowControl(
2287
1216
  throw error;
2288
1217
  }
2289
1218
  if (
2290
- activePrompt !== undefined &&
2291
- (activePrompt === "abel-diagnose" ||
2292
- validation.value.stage !== activePrompt)
1219
+ activePrompt !== "abel-implement" ||
1220
+ validation.value.stage !== activePrompt
2293
1221
  ) {
2294
1222
  throw new Error("stage-control-mismatch");
2295
1223
  }
2296
1224
  let engine: WorkflowControlEngine;
2297
1225
  try {
2298
1226
  engine = await engineFor(ctx);
1227
+ if (exitingStage || !activation.isActive())
1228
+ throw new Error("stage-control-mismatch");
2299
1229
  } catch (error) {
2300
1230
  if (!(error instanceof RunStoreFormatError)) throw error;
2301
1231
  const payload = runStoreUnavailableStatus(error, validation.value);
@@ -2392,7 +1322,10 @@ export function registerWorkflowControl(
2392
1322
  if (
2393
1323
  event.toolName !== DISPATCH_TOOL ||
2394
1324
  !event.isError ||
2395
- event.input.action !== "design"
1325
+ (event.input.action !== "design" &&
1326
+ !(
1327
+ event.input.action === "amend" && designFailures.has(event.toolCallId)
1328
+ ))
2396
1329
  ) {
2397
1330
  return;
2398
1331
  }
@@ -2422,12 +1355,35 @@ export function registerWorkflowControl(
2422
1355
  };
2423
1356
  });
2424
1357
  pi.on("input", (event) => {
2425
- pendingPrompt = invokedPrompt(event.text);
1358
+ pendingPrompt = undefined;
1359
+ pendingInit = false;
1360
+ const prompt = invokedPrompt(event.text);
1361
+ const init = /^\/abel-init(?:\s|$)/u.test(event.text);
1362
+ if (event.source !== "interactive" && event.source !== "rpc") {
1363
+ // Stop before Pi expands the template: hiding dispatch alone would still
1364
+ // expose the complete workflow instructions to the parent model.
1365
+ return { action: prompt || init ? "handled" : "continue" };
1366
+ }
1367
+ pendingPrompt = prompt;
1368
+ pendingInit = init && hasPackageProvenance(pi, "abel-init");
1369
+ if (
1370
+ activePrompt &&
1371
+ (pendingInit ||
1372
+ (prompt && prompt !== activePrompt && hasPackageProvenance(pi, prompt)))
1373
+ ) {
1374
+ return (async () => {
1375
+ await exitStage();
1376
+ return { action: "continue" as const };
1377
+ })();
1378
+ }
2426
1379
  return { action: "continue" };
2427
1380
  });
2428
- pi.on("before_agent_start", (event, ctx) => {
1381
+ pi.on("before_agent_start", (event) => {
2429
1382
  const prompt = pendingPrompt;
1383
+ const init =
1384
+ pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
2430
1385
  pendingPrompt = undefined;
1386
+ pendingInit = false;
2431
1387
  const verified =
2432
1388
  prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
2433
1389
  if (verified) {
@@ -2436,28 +1392,15 @@ export function registerWorkflowControl(
2436
1392
  }
2437
1393
  registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
2438
1394
  activePrompt = prompt;
2439
- const sessionId = ctx.sessionManager?.getSessionId?.();
2440
- if (typeof sessionId === "string") {
2441
- parentPayloadBridge.beginSession(sessionId);
2442
- } else {
2443
- parentPayloadBridge.clear();
2444
- }
2445
- }
2446
- if (ctx.model) {
2447
- parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
2448
1395
  }
2449
1396
  if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
2450
1397
  if (verified && prompt === "abel-design") enforceDesignTools();
2451
- });
2452
- pi.on("model_select", (event, ctx) => {
2453
- const sessionId = ctx.sessionManager?.getSessionId?.();
2454
- if (typeof sessionId !== "string") {
2455
- parentPayloadBridge.clear();
2456
- return;
2457
- }
2458
- parentPayloadBridge.beginSession(sessionId);
2459
- const model = event.model ?? ctx.model;
2460
- if (model) parentPayloadBridge.install(model, ctx.modelRegistry);
1398
+ const boundary = activePrompt
1399
+ ? `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.`
1400
+ : init
1401
+ ? "Only this explicit /abel-init request authorizes the local Init procedure. Do not activate dispatch or continue into another Abel stage."
1402
+ : "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.";
1403
+ return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${boundary}` };
2461
1404
  });
2462
1405
  pi.on("before_provider_request", (event, ctx) => {
2463
1406
  if (
@@ -2473,19 +1416,13 @@ export function registerWorkflowControl(
2473
1416
  });
2474
1417
  pi.on("session_start", async (_event, ctx) => {
2475
1418
  pendingPrompt = undefined;
1419
+ pendingInit = false;
2476
1420
  activePrompt = undefined;
2477
1421
  designFailures.clear();
2478
1422
  activity.detach();
2479
1423
  await packetRuntime.drain();
2480
1424
  await closeEngines();
2481
1425
  deactivate();
2482
- const sessionId = ctx.sessionManager?.getSessionId?.();
2483
- if (typeof sessionId === "string") {
2484
- parentPayloadBridge.beginSession(sessionId);
2485
- if (ctx.model) parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
2486
- } else {
2487
- parentPayloadBridge.clear();
2488
- }
2489
1426
  if (ctx.mode === "tui") activity.attach(ctx.ui);
2490
1427
  });
2491
1428
  pi.on("session_shutdown", async () => {
@@ -2496,7 +1433,6 @@ export function registerWorkflowControl(
2496
1433
  await closeEngines();
2497
1434
  activity.clear();
2498
1435
  deactivate();
2499
- parentPayloadBridge.clear();
2500
1436
  });
2501
1437
  }
2502
1438