@abelxiaoxing/cadence 1.0.14 → 1.1.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 (48) hide show
  1. package/README.md +39 -13
  2. package/agents/contract-reviewer.md +2 -2
  3. package/agents/design-explorer.md +1 -1
  4. package/agents/diagnosis-worker.md +6 -3
  5. package/agents/implementation-worker.md +14 -9
  6. package/config/.env.example +5 -17
  7. package/config/routes.example.json +42 -0
  8. package/package.json +3 -3
  9. package/prompts/abel-design.md +206 -228
  10. package/prompts/abel-diagnose.md +39 -18
  11. package/prompts/abel-implement.md +122 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +190 -168
  14. package/src/apply-transaction.ts +1427 -0
  15. package/src/artifact-store.ts +204 -0
  16. package/src/child-session.ts +136 -17
  17. package/src/contracts.ts +121 -696
  18. package/src/control-contracts.ts +246 -0
  19. package/src/delivery-compiler.ts +1118 -0
  20. package/src/design-control.ts +846 -0
  21. package/src/design-journal.ts +1569 -0
  22. package/src/file-snapshot.ts +5 -0
  23. package/src/implement-graph.ts +11 -4
  24. package/src/index.ts +1833 -118
  25. package/src/isolation-backend.ts +269 -0
  26. package/src/packet-runtime.ts +533 -0
  27. package/src/parent-provider.ts +23 -4
  28. package/src/route-policy.ts +483 -0
  29. package/src/run-state.ts +430 -0
  30. package/src/run-store.ts +990 -0
  31. package/src/safe-path.ts +11 -0
  32. package/src/scoped-tools.ts +36 -0
  33. package/src/state-root.ts +215 -0
  34. package/src/subagent-activity.ts +526 -27
  35. package/src/submit-tool.ts +274 -0
  36. package/src/task-ledger.ts +1380 -0
  37. package/src/verification-capability.ts +560 -45
  38. package/src/worker-broker.ts +703 -0
  39. package/src/workflow-engine.ts +7706 -0
  40. package/src/workspace-store.ts +544 -0
  41. package/src/candidate-preflight.ts +0 -1618
  42. package/src/drain.ts +0 -20
  43. package/src/patch.ts +0 -505
  44. package/src/result-store.ts +0 -312
  45. package/src/runtime.ts +0 -2436
  46. package/src/scheduler.ts +0 -384
  47. package/src/subagent-endpoint.ts +0 -158
  48. package/src/worker.ts +0 -204
package/src/index.ts CHANGED
@@ -3,8 +3,21 @@
3
3
  // the active set at session start. Eligible-stage activation is wired by the
4
4
  // workflow routing (abel-design/implement/diagnose provenance) in the prompts
5
5
  // integration; abel-init and ordinary prompts never activate dispatch.
6
- import { dirname, join } from "node:path";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { execFile } from "node:child_process";
8
+ import { createHash } from "node:crypto";
9
+ import {
10
+ lstatSync,
11
+ mkdirSync,
12
+ mkdtempSync,
13
+ readFileSync,
14
+ realpathSync,
15
+ rmSync,
16
+ } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import path, { dirname, join } from "node:path";
7
19
  import { fileURLToPath } from "node:url";
20
+ import { promisify } from "node:util";
8
21
  import type {
9
22
  AgentToolResult,
10
23
  AgentToolUpdateCallback,
@@ -14,17 +27,66 @@ import type {
14
27
  ToolRenderResultOptions,
15
28
  } from "@earendil-works/pi-coding-agent";
16
29
  import { type Activation, activateTool, deactivateTool } from "./activation.ts";
17
- import { ACTIONS } from "./contracts.ts";
30
+ import { loadAgentDefinitions } from "./agent-registry.ts";
31
+ import { runChildSession } from "./child-session.ts";
32
+ import {
33
+ type AtomicVerificationContract,
34
+ type DesignEvidenceResult,
35
+ isValidRelativePath,
36
+ LIMITS,
37
+ type StructuredVerificationContract,
38
+ verificationSteps,
39
+ } from "./contracts.ts";
40
+ import { validateControlCommand } from "./control-contracts.ts";
41
+ import {
42
+ assessDeliveryTraceability,
43
+ compileImplementPlan,
44
+ DeliveryValidationError,
45
+ type GateApprovalProof,
46
+ IMPLEMENT_PLAN_SCHEMA_VERSION,
47
+ parseGateAReceipt,
48
+ parseImplementPlan,
49
+ parseReadyReceipt,
50
+ } from "./delivery-compiler.ts";
51
+ import {
52
+ DesignController,
53
+ validateDesignControlRequest,
54
+ } from "./design-control.ts";
55
+ import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
56
+ import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
57
+ import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
18
58
  import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
19
- import { Runtime } from "./runtime.ts";
59
+ import { runtimeForWorkerRoute } from "./parent-provider.ts";
60
+ import {
61
+ inspectRoutePolicy,
62
+ loadRoutePolicy,
63
+ type RoutePolicyResolution,
64
+ unavailableRoutePolicy,
65
+ type WorkerRoutePolicy,
66
+ } from "./route-policy.ts";
67
+ import { observeSafePath } from "./safe-path.ts";
68
+ import { resolveStateRoot } from "./state-root.ts";
20
69
  import {
21
70
  ACTIVITY_DETAILS_KEY,
22
71
  ActivityController,
23
72
  renderActivityCall,
24
73
  renderActivityResult,
74
+ type WorkflowActivityUpdate,
25
75
  } from "./subagent-activity.ts";
76
+ import { classifyCandidateContextRequest } from "./submit-tool.ts";
77
+ import {
78
+ bindCurrentVerificationCapability,
79
+ isVerificationCapabilityCurrent,
80
+ type VerificationRunnerBinding,
81
+ } from "./verification-capability.ts";
82
+ import {
83
+ openDurableWorkflowEngine,
84
+ type WorkflowAvailableDelivery,
85
+ type WorkflowDeliverySource,
86
+ } from "./workflow-engine.ts";
26
87
 
27
88
  export const DISPATCH_TOOL = "abel_dispatch";
89
+ const DESIGN_PARENT_READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
28
90
 
29
91
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
30
92
  const ELIGIBLE_PROMPTS = [
@@ -55,6 +117,10 @@ const DESIGN_REQUEST_SCHEMA = {
55
117
  enum: ["design-explorer"],
56
118
  description: "Exact package-owned read-only Agent role.",
57
119
  },
120
+ runId: {
121
+ type: "string",
122
+ description: "Durable Design run identity returned by Design start.",
123
+ },
58
124
  id: {
59
125
  type: "string",
60
126
  description:
@@ -128,6 +194,7 @@ const DESIGN_REQUEST_SCHEMA = {
128
194
  required: [
129
195
  "stage",
130
196
  "role",
197
+ "runId",
131
198
  "id",
132
199
  "phase",
133
200
  "objective",
@@ -139,11 +206,89 @@ const DESIGN_REQUEST_SCHEMA = {
139
206
  additionalProperties: false,
140
207
  } as const;
141
208
 
142
- function requestSchemaForStage(name?: EligiblePrompt): object {
143
- return name === "abel-design"
144
- ? DESIGN_REQUEST_SCHEMA
145
- : GENERIC_REQUEST_SCHEMA;
146
- }
209
+ const DESIGN_CONTROL_REQUEST_SCHEMA = {
210
+ description:
211
+ "One closed durable Design control operation. The operation body is strict and idempotent by operationId.",
212
+ oneOf: [
213
+ {
214
+ type: "object",
215
+ properties: {
216
+ operation: { type: "string", enum: ["record-decision"] },
217
+ runId: { type: "string" },
218
+ operationId: { type: "string" },
219
+ decisionId: { type: "string" },
220
+ category: { type: "string", enum: ["behavior", "technical"] },
221
+ contractHash: { type: "string" },
222
+ refs: { type: "array", items: { type: "string" } },
223
+ },
224
+ required: [
225
+ "operation",
226
+ "runId",
227
+ "operationId",
228
+ "decisionId",
229
+ "category",
230
+ "contractHash",
231
+ "refs",
232
+ ],
233
+ additionalProperties: false,
234
+ },
235
+ {
236
+ type: "object",
237
+ properties: {
238
+ operation: { type: "string", enum: ["approve-gate"] },
239
+ runId: { type: "string" },
240
+ operationId: { type: "string" },
241
+ gate: { type: "string", enum: ["gate-a", "gate-b"] },
242
+ contractHash: { type: "string" },
243
+ },
244
+ required: ["operation", "runId", "operationId", "gate", "contractHash"],
245
+ additionalProperties: false,
246
+ },
247
+ {
248
+ type: "object",
249
+ properties: {
250
+ operation: { type: "string", enum: ["write-artifact"] },
251
+ runId: { type: "string" },
252
+ operationId: { type: "string" },
253
+ path: {
254
+ type: "string",
255
+ description: "Allowed path relative to this Design change root.",
256
+ },
257
+ content: {
258
+ type: "string",
259
+ maxLength: 16 * 1024 * 1024,
260
+ description: "Exact bounded UTF-8 artifact content.",
261
+ },
262
+ },
263
+ required: ["operation", "runId", "operationId", "path", "content"],
264
+ additionalProperties: false,
265
+ },
266
+ {
267
+ type: "object",
268
+ properties: {
269
+ operation: { type: "string", enum: ["delete-artifact"] },
270
+ runId: { type: "string" },
271
+ operationId: { type: "string" },
272
+ path: {
273
+ type: "string",
274
+ description: "Allowed path relative to this Design change root.",
275
+ },
276
+ },
277
+ required: ["operation", "runId", "operationId", "path"],
278
+ additionalProperties: false,
279
+ },
280
+ ...(["compile-plan", "finalize-delivery"] as const).map((operation) => ({
281
+ type: "object" as const,
282
+ properties: {
283
+ operation: { type: "string" as const, enum: [operation] },
284
+ runId: { type: "string" as const },
285
+ operationId: { type: "string" as const },
286
+ },
287
+ required: ["operation", "runId", "operationId"],
288
+ additionalProperties: false,
289
+ })),
290
+ ],
291
+ } as const;
147
292
 
148
293
  function invokedPrompt(text: string): EligiblePrompt | undefined {
149
294
  const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
@@ -218,91 +363,1672 @@ function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
218
363
  return { payload, usage };
219
364
  }
220
365
 
221
- export default function register(pi: ExtensionAPI): void {
366
+ export interface WorkflowControlEngine {
367
+ execute(
368
+ command: unknown,
369
+ context?: ExtensionContext,
370
+ signal?: AbortSignal,
371
+ onActivity?: (event: WorkflowActivityUpdate) => void,
372
+ ): Promise<Record<string, unknown>>;
373
+ executeDesign?(request: unknown): Promise<Record<string, unknown>>;
374
+ assertDesignRun?(runId: string): void;
375
+ recordDesignEvidence?(input: {
376
+ runId: string;
377
+ evidence: DesignEvidenceResult;
378
+ }): unknown;
379
+ close(): void | Promise<void>;
380
+ }
381
+
382
+ export type WorkflowControlEngineFactory = (
383
+ ctx: ExtensionContext,
384
+ parentPayloadBridge: ParentPayloadBridge,
385
+ ) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
386
+
387
+ const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
388
+ const execFileAsync = promisify(execFile);
389
+
390
+ export interface OpenSpecDeliveryInspection {
391
+ change: string;
392
+ schema: string;
393
+ planningComplete: boolean;
394
+ strictValid: boolean;
395
+ artifactPaths: string[];
396
+ }
397
+
398
+ export interface PackageDeliverySourceOptions {
399
+ inspectOpenSpec?: (
400
+ consumerRoot: string,
401
+ change: string,
402
+ ) => Promise<OpenSpecDeliveryInspection>;
403
+ verifyGateProof?: (input: {
404
+ change: string;
405
+ gate: "gate-a" | "gate-b";
406
+ proof: GateApprovalProof;
407
+ }) => boolean;
408
+ verifyFinalizedDelivery?: (input: {
409
+ change: string;
410
+ deliveryRevision: number;
411
+ receiptHash: string;
412
+ gateA: GateApprovalProof;
413
+ gateB: GateApprovalProof;
414
+ planCanonicalHash: string;
415
+ }) => boolean;
416
+ }
417
+
418
+ export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
419
+ discoverLatest(input: {
420
+ stage: "abel-design" | "abel-implement";
421
+ change: string;
422
+ }): Promise<WorkflowAvailableDelivery | undefined>;
423
+ }
424
+
425
+ function sha256(value: Uint8Array | string): string {
426
+ return createHash("sha256").update(value).digest("hex");
427
+ }
428
+
429
+ function proofVerified(verify: () => boolean): boolean {
430
+ try {
431
+ return verify() === true;
432
+ } catch {
433
+ return false;
434
+ }
435
+ }
436
+
437
+ function normalizedTrackingArtifact(
438
+ bytes: Uint8Array,
439
+ taskIds: readonly string[],
440
+ ): Buffer | undefined {
441
+ let text: string;
442
+ try {
443
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
444
+ } catch {
445
+ return undefined;
446
+ }
447
+ const identities = taskIds.map((taskId) => ({
448
+ taskId,
449
+ pattern: new RegExp(
450
+ `(?:^|[^A-Za-z0-9._:-])${taskId.replace(
451
+ /[.*+?^${}()|[\]\\]/gu,
452
+ "\\$&",
453
+ )}(?![A-Za-z0-9._:-])`,
454
+ "u",
455
+ ),
456
+ matches: 0,
457
+ }));
458
+ const normalized = text
459
+ .split(/(?<=\n)/u)
460
+ .map((segment) => {
461
+ const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
462
+ if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
463
+ const matching = identities.filter(({ pattern }) => pattern.test(line));
464
+ if (matching.length !== 1) return segment;
465
+ matching[0].matches += 1;
466
+ return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
467
+ })
468
+ .join("");
469
+ if (identities.some(({ matches }) => matches !== 1)) return undefined;
470
+ return Buffer.from(normalized, "utf8");
471
+ }
472
+
473
+ function readPackageDeliveryFile(
474
+ root: string,
475
+ relative: string,
476
+ maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
477
+ ): Buffer {
478
+ const observation = observeSafePath(root, relative);
479
+ if (observation.kind !== "file") {
480
+ throw new Error("delivery-file-unavailable");
481
+ }
482
+ const target = path.join(root, ...relative.split("/"));
483
+ const stat = lstatSync(target);
484
+ if (stat.size < 1 || stat.size > maximumBytes) {
485
+ throw new Error("delivery-file-size-invalid");
486
+ }
487
+ return readFileSync(target);
488
+ }
489
+
490
+ function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
491
+ return value && typeof value === "object" && !Array.isArray(value)
492
+ ? (value as Record<string, unknown>)
493
+ : undefined;
494
+ }
495
+
496
+ function parseCommandJson(
497
+ value: string,
498
+ code: string,
499
+ ): Record<string, unknown> {
500
+ let parsed: unknown;
501
+ try {
502
+ parsed = JSON.parse(value);
503
+ } catch {
504
+ throw new Error(code);
505
+ }
506
+ const record = deliveryRecord(parsed);
507
+ if (!record) throw new Error(code);
508
+ return record;
509
+ }
510
+
511
+ export async function inspectOpenSpecDelivery(
512
+ consumerRoot: string,
513
+ change: string,
514
+ ): Promise<OpenSpecDeliveryInspection> {
515
+ const [statusExecution, validationExecution] = await Promise.all([
516
+ execFileAsync("openspec", ["status", "--change", change, "--json"], {
517
+ cwd: consumerRoot,
518
+ encoding: "utf8",
519
+ maxBuffer: 4 * 1024 * 1024,
520
+ }),
521
+ execFileAsync(
522
+ "openspec",
523
+ ["validate", change, "--strict", "--json", "--no-interactive"],
524
+ {
525
+ cwd: consumerRoot,
526
+ encoding: "utf8",
527
+ maxBuffer: 4 * 1024 * 1024,
528
+ },
529
+ ),
530
+ ]);
531
+ const status = parseCommandJson(
532
+ statusExecution.stdout,
533
+ "delivery-openspec-status-invalid",
534
+ );
535
+ const validation = parseCommandJson(
536
+ validationExecution.stdout,
537
+ "delivery-openspec-validation-invalid",
538
+ );
539
+ const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
540
+ const artifactPathsRecord = deliveryRecord(status.artifactPaths);
541
+ const artifactPaths = artifactPathsRecord
542
+ ? Object.values(artifactPathsRecord).flatMap((entry) => {
543
+ const record = deliveryRecord(entry);
544
+ if (!record || !Array.isArray(record.existingOutputPaths)) return [];
545
+ return record.existingOutputPaths.flatMap((candidate) => {
546
+ if (typeof candidate !== "string") return [];
547
+ const resolved = path.resolve(candidate);
548
+ const relative = path.relative(changeRoot, resolved);
549
+ return relative &&
550
+ !relative.startsWith(`..${path.sep}`) &&
551
+ relative !== ".." &&
552
+ !path.isAbsolute(relative)
553
+ ? [relative.split(path.sep).join("/")]
554
+ : [];
555
+ });
556
+ })
557
+ : [];
558
+ const items = Array.isArray(validation.items) ? validation.items : [];
559
+ const strictValid =
560
+ items.length === 1 &&
561
+ deliveryRecord(items[0])?.id === change &&
562
+ deliveryRecord(items[0])?.valid === true;
563
+ if (
564
+ status.changeName !== change ||
565
+ typeof status.schemaName !== "string" ||
566
+ artifactPaths.length === 0
567
+ ) {
568
+ throw new Error("delivery-openspec-status-invalid");
569
+ }
570
+ return {
571
+ change,
572
+ schema: status.schemaName,
573
+ planningComplete:
574
+ status.isPlanningComplete === true && status.isComplete === true,
575
+ strictValid,
576
+ artifactPaths: [...new Set(artifactPaths)].sort(),
577
+ };
578
+ }
579
+
580
+ export function packageDeliverySource(
581
+ consumerRoot: string,
582
+ options: PackageDeliverySourceOptions = {},
583
+ ): PackageWorkflowDeliverySource {
584
+ const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
585
+ const verifyGateProof = options.verifyGateProof;
586
+ const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
587
+ return {
588
+ async discoverLatest(input) {
589
+ if (
590
+ input.stage !== "abel-implement" ||
591
+ !verifyGateProof ||
592
+ !verifyFinalizedDelivery
593
+ ) {
594
+ return undefined;
595
+ }
596
+ try {
597
+ const changeRoot = `openspec/changes/${input.change}`;
598
+ const receiptBytes = readPackageDeliveryFile(
599
+ consumerRoot,
600
+ `${changeRoot}/ready.yaml`,
601
+ 4 * 1024 * 1024,
602
+ );
603
+ const receipt = parseReadyReceipt(receiptBytes);
604
+ if (receipt.change !== input.change) return undefined;
605
+ const gateABytes = readPackageDeliveryFile(
606
+ consumerRoot,
607
+ `${changeRoot}/${receipt.approvals.gateA.path}`,
608
+ 4 * 1024 * 1024,
609
+ );
610
+ if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
611
+ return undefined;
612
+ }
613
+ const gateA = parseGateAReceipt(gateABytes);
614
+ if (
615
+ gateA.change !== receipt.change ||
616
+ gateA.schema !== receipt.schema ||
617
+ !proofVerified(() =>
618
+ verifyGateProof({
619
+ change: input.change,
620
+ gate: "gate-a",
621
+ proof: gateA.approval,
622
+ }),
623
+ ) ||
624
+ !proofVerified(() =>
625
+ verifyGateProof({
626
+ change: input.change,
627
+ gate: "gate-b",
628
+ proof: receipt.approvals.gateB,
629
+ }),
630
+ )
631
+ ) {
632
+ return undefined;
633
+ }
634
+ const receiptHash = sha256(receiptBytes);
635
+ if (
636
+ !proofVerified(() =>
637
+ verifyFinalizedDelivery({
638
+ change: input.change,
639
+ deliveryRevision: receipt.deliveryRevision,
640
+ receiptHash,
641
+ gateA: gateA.approval,
642
+ gateB: receipt.approvals.gateB,
643
+ planCanonicalHash: receipt.plan.canonicalHash,
644
+ }),
645
+ )
646
+ ) {
647
+ return undefined;
648
+ }
649
+ return {
650
+ deliveryRevision: receipt.deliveryRevision,
651
+ receiptHash,
652
+ };
653
+ } catch {
654
+ return undefined;
655
+ }
656
+ },
657
+ async load(input) {
658
+ if (input.stage !== "abel-implement") {
659
+ throw new DeliveryValidationError(["delivery-stage-invalid"]);
660
+ }
661
+ const changeRoot = `openspec/changes/${input.change}`;
662
+ const receiptRelative = `${changeRoot}/ready.yaml`;
663
+ const planRelative = `${changeRoot}/implement-plan.json`;
664
+ let receiptBytes: Buffer;
665
+ try {
666
+ receiptBytes = readPackageDeliveryFile(
667
+ consumerRoot,
668
+ receiptRelative,
669
+ 4 * 1024 * 1024,
670
+ );
671
+ } catch {
672
+ throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
673
+ }
674
+ let receipt: ReturnType<typeof parseReadyReceipt>;
675
+ try {
676
+ receipt = parseReadyReceipt(receiptBytes);
677
+ } catch {
678
+ throw new DeliveryValidationError(["delivery-receipt-invalid"]);
679
+ }
680
+ const diagnostics = new Set<string>();
681
+ if (receipt.change !== input.change) {
682
+ diagnostics.add("delivery-change-mismatch");
683
+ }
684
+ const receiptHash = sha256(receiptBytes);
685
+ if (
686
+ (input.deliveryRevision !== undefined &&
687
+ input.deliveryRevision !== receipt.deliveryRevision) ||
688
+ (input.receiptHash !== undefined && input.receiptHash !== receiptHash)
689
+ ) {
690
+ diagnostics.add("delivery-revision-mismatch");
691
+ }
692
+
693
+ let inspection: OpenSpecDeliveryInspection | undefined;
694
+ try {
695
+ inspection = await inspect(consumerRoot, input.change);
696
+ } catch {
697
+ diagnostics.add("delivery-openspec-unavailable");
698
+ }
699
+ if (inspection) {
700
+ if (!inspection.strictValid) {
701
+ diagnostics.add("delivery-openspec-strict-invalid");
702
+ }
703
+ if (!inspection.planningComplete) {
704
+ diagnostics.add("delivery-openspec-incomplete");
705
+ }
706
+ if (inspection.schema !== receipt.schema) {
707
+ diagnostics.add("delivery-schema-mismatch");
708
+ }
709
+ }
710
+
711
+ let gateABytes: Buffer | undefined;
712
+ let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
713
+ try {
714
+ gateABytes = readPackageDeliveryFile(
715
+ consumerRoot,
716
+ `${changeRoot}/${receipt.approvals.gateA.path}`,
717
+ 4 * 1024 * 1024,
718
+ );
719
+ if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
720
+ diagnostics.add("delivery-gate-a-hash-mismatch");
721
+ }
722
+ gateA = parseGateAReceipt(gateABytes);
723
+ if (
724
+ gateA.change !== receipt.change ||
725
+ gateA.schema !== receipt.schema
726
+ ) {
727
+ diagnostics.add("delivery-gate-a-binding-mismatch");
728
+ }
729
+ } catch {
730
+ diagnostics.add("delivery-gate-a-invalid");
731
+ }
732
+
733
+ const artifactBytes = new Map<string, Buffer>();
734
+ const artifactHashes = new Map(
735
+ receipt.artifacts.map((artifact) => [
736
+ artifact.path,
737
+ artifact.rawSha256,
738
+ ]),
739
+ );
740
+ for (const artifact of receipt.artifacts) {
741
+ try {
742
+ const bytes = readPackageDeliveryFile(
743
+ consumerRoot,
744
+ `${changeRoot}/${artifact.path}`,
745
+ );
746
+ artifactBytes.set(artifact.path, bytes);
747
+ if (
748
+ artifact.path !== receipt.traceability.taskPath &&
749
+ sha256(bytes) !== artifact.rawSha256
750
+ ) {
751
+ diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
752
+ }
753
+ } catch {
754
+ diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
755
+ }
756
+ }
757
+ if (inspection) {
758
+ const covered = new Set(receipt.artifacts.map((entry) => entry.path));
759
+ const expected = new Set(inspection.artifactPaths);
760
+ for (const relative of expected) {
761
+ if (!isValidRelativePath(relative) || !covered.has(relative)) {
762
+ diagnostics.add(`delivery-artifact-unbound:${relative}`);
763
+ }
764
+ }
765
+ for (const relative of covered) {
766
+ if (!expected.has(relative)) {
767
+ diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
768
+ }
769
+ }
770
+ }
771
+ if (gateA) {
772
+ if (!verifyGateProof) {
773
+ diagnostics.add("delivery-gate-proof-verifier-unavailable");
774
+ } else if (
775
+ !proofVerified(() =>
776
+ verifyGateProof({
777
+ change: input.change,
778
+ gate: "gate-a",
779
+ proof: gateA.approval,
780
+ }),
781
+ )
782
+ ) {
783
+ diagnostics.add("delivery-gate-a-proof-invalid");
784
+ }
785
+ for (const artifact of gateA.artifacts) {
786
+ if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
787
+ diagnostics.add(
788
+ `delivery-gate-a-artifact-mismatch:${artifact.path}`,
789
+ );
790
+ }
791
+ }
792
+ }
793
+ if (
794
+ verifyGateProof &&
795
+ !proofVerified(() =>
796
+ verifyGateProof({
797
+ change: input.change,
798
+ gate: "gate-b",
799
+ proof: receipt.approvals.gateB,
800
+ }),
801
+ )
802
+ ) {
803
+ diagnostics.add("delivery-gate-b-proof-invalid");
804
+ }
805
+ if (gateA) {
806
+ if (!verifyFinalizedDelivery) {
807
+ diagnostics.add("delivery-finalization-verifier-unavailable");
808
+ } else if (
809
+ !proofVerified(() =>
810
+ verifyFinalizedDelivery({
811
+ change: input.change,
812
+ deliveryRevision: receipt.deliveryRevision,
813
+ receiptHash,
814
+ gateA: gateA.approval,
815
+ gateB: receipt.approvals.gateB,
816
+ planCanonicalHash: receipt.plan.canonicalHash,
817
+ }),
818
+ )
819
+ ) {
820
+ diagnostics.add("delivery-finalization-proof-invalid");
821
+ }
822
+ }
823
+
824
+ let plan: ReturnType<typeof parseImplementPlan> | undefined;
825
+ let planBytes: Buffer | undefined;
826
+ try {
827
+ planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
828
+ plan = parseImplementPlan(planBytes);
829
+ } catch {
830
+ diagnostics.add("delivery-plan-invalid");
831
+ }
832
+ if (plan && planBytes) {
833
+ if (plan.changeId !== input.change) {
834
+ diagnostics.add("delivery-plan-change-mismatch");
835
+ }
836
+ let compiled: ReturnType<typeof compileImplementPlan> | undefined;
837
+ try {
838
+ compiled = compileImplementPlan(plan, { consumerRoot });
839
+ } catch {
840
+ diagnostics.add("delivery-verification-closure-invalid");
841
+ }
842
+ if (
843
+ receipt.plan.path !== "implement-plan.json" ||
844
+ receipt.plan.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
845
+ receipt.plan.rawSha256 !== sha256(planBytes) ||
846
+ receipt.plan.canonicalHash !== hashCanonicalValue(plan)
847
+ ) {
848
+ diagnostics.add("delivery-plan-binding-invalid");
849
+ }
850
+ if (
851
+ compiled &&
852
+ canonicalJson(compiled.closure) !==
853
+ canonicalJson(receipt.verificationClosure)
854
+ ) {
855
+ diagnostics.add("delivery-verification-closure-mismatch");
856
+ }
857
+ }
858
+
859
+ if (plan) {
860
+ const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
861
+ const expectedTasksHash = artifactHashes.get(
862
+ receipt.traceability.taskPath,
863
+ );
864
+ if (
865
+ tasksBytes &&
866
+ expectedTasksHash &&
867
+ sha256(tasksBytes) !== expectedTasksHash
868
+ ) {
869
+ const normalized = normalizedTrackingArtifact(
870
+ tasksBytes,
871
+ plan.tracking.taskIds,
872
+ );
873
+ if (!normalized || sha256(normalized) !== expectedTasksHash) {
874
+ diagnostics.add(
875
+ `delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
876
+ );
877
+ }
878
+ }
879
+ const specs = [...artifactBytes]
880
+ .filter(
881
+ ([relative]) =>
882
+ relative.startsWith("specs/") && relative.endsWith("/spec.md"),
883
+ )
884
+ .flatMap(([relative, bytes]) => {
885
+ try {
886
+ return [
887
+ {
888
+ path: relative,
889
+ text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
890
+ },
891
+ ];
892
+ } catch {
893
+ diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
894
+ return [];
895
+ }
896
+ });
897
+ if (!tasksBytes || specs.length === 0) {
898
+ diagnostics.add("delivery-traceability-input-unavailable");
899
+ } else {
900
+ try {
901
+ const traceability = assessDeliveryTraceability({
902
+ tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
903
+ tasksBytes,
904
+ ),
905
+ specs,
906
+ plan,
907
+ });
908
+ if (!traceability.ok) {
909
+ for (const diagnostic of traceability.diagnostics) {
910
+ diagnostics.add(diagnostic);
911
+ }
912
+ } else if (
913
+ canonicalJson(traceability.value) !==
914
+ canonicalJson(receipt.traceability)
915
+ ) {
916
+ diagnostics.add("delivery-traceability-binding-mismatch");
917
+ }
918
+ } catch {
919
+ diagnostics.add("delivery-traceability-invalid");
920
+ }
921
+ }
922
+ }
923
+ if (!plan || !gateA || diagnostics.size > 0) {
924
+ throw new DeliveryValidationError([...diagnostics]);
925
+ }
926
+ return {
927
+ version: 2,
928
+ gate: "gate-b",
929
+ revision: receipt.deliveryRevision,
930
+ receiptHash,
931
+ plan,
932
+ approvalProofs: {
933
+ gateA: structuredClone(gateA.approval),
934
+ gateB: structuredClone(receipt.approvals.gateB),
935
+ },
936
+ };
937
+ },
938
+ };
939
+ }
940
+
941
+ function packageScriptInvocation(
942
+ packageManager: string,
943
+ script: string,
944
+ args: string[],
945
+ ): string[] {
946
+ return packageManager === "npm" || packageManager === "pnpm"
947
+ ? ["run", script, "--", ...args]
948
+ : ["run", script, ...args];
949
+ }
950
+
951
+ function bindingFor(
952
+ bindings: readonly VerificationRunnerBinding[],
953
+ command: string,
954
+ ): VerificationRunnerBinding | undefined {
955
+ return bindings.find((binding) => binding.command === command);
956
+ }
957
+
958
+ function prepareRunnerBindings(
959
+ root: string,
960
+ bindings: readonly VerificationRunnerBinding[],
961
+ ): {
962
+ bindings: VerificationRunnerBinding[];
963
+ mounts: Array<{ source: string; target: string }>;
964
+ cleanupRoot?: string;
965
+ } {
966
+ if (!bindings.some((binding) => binding.mountSource)) {
967
+ return {
968
+ bindings: bindings.map((binding) => ({
969
+ ...binding,
970
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
971
+ })),
972
+ mounts: [],
973
+ };
974
+ }
975
+ const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
976
+ try {
977
+ const mounted = new Map<
978
+ string,
979
+ { source: string; target: string; sandboxTarget: string }
980
+ >();
981
+ const prepared = bindings.map((binding) => {
982
+ if (!binding.mountSource) return { ...binding };
983
+ const source = path.resolve(binding.mountSource);
984
+ const sourceStat = lstatSync(source);
985
+ if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
986
+ throw new Error("runner-mount-unavailable");
987
+ }
988
+ let mount = mounted.get(source);
989
+ if (!mount) {
990
+ const target = path.join(cleanupRoot, String(mounted.size));
991
+ mkdirSync(target, { mode: 0o700 });
992
+ const sandboxRelative = path
993
+ .relative(root, target)
994
+ .split(path.sep)
995
+ .join("/");
996
+ mount = {
997
+ source,
998
+ target,
999
+ sandboxTarget: `/workspace/${sandboxRelative}`,
1000
+ };
1001
+ mounted.set(source, mount);
1002
+ }
1003
+ const executable = realpathSync(binding.executablePath);
1004
+ const relative = path.relative(source, executable);
1005
+ if (
1006
+ relative === "" ||
1007
+ relative.startsWith("..") ||
1008
+ path.isAbsolute(relative)
1009
+ ) {
1010
+ throw new Error("runner-mount-unavailable");
1011
+ }
1012
+ return {
1013
+ command: binding.command,
1014
+ executablePath: path.posix.join(
1015
+ mount.sandboxTarget,
1016
+ ...relative.split(path.sep),
1017
+ ),
1018
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
1019
+ };
1020
+ });
1021
+ return {
1022
+ bindings: prepared,
1023
+ mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
1024
+ source,
1025
+ target: sandboxTarget,
1026
+ })),
1027
+ cleanupRoot,
1028
+ };
1029
+ } catch (error) {
1030
+ rmSync(cleanupRoot, { recursive: true, force: true });
1031
+ throw error;
1032
+ }
1033
+ }
1034
+
1035
+ function verificationInvocation(
1036
+ step: AtomicVerificationContract,
1037
+ bindings: readonly VerificationRunnerBinding[],
1038
+ ): { executable: string; args: string[] } | undefined {
1039
+ const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
1040
+ if (step.kind === "package-script") {
1041
+ const binding = bindingFor(bindings, step.packageManager);
1042
+ return binding
1043
+ ? {
1044
+ executable: binding.executablePath,
1045
+ args: [
1046
+ ...(binding.fixedArgs ?? []),
1047
+ ...packageScriptInvocation(
1048
+ step.packageManager,
1049
+ step.script,
1050
+ step.args,
1051
+ ),
1052
+ ],
1053
+ }
1054
+ : undefined;
1055
+ }
1056
+ const runner = step.runner;
1057
+ if (runner.kind === "package-script") {
1058
+ const binding = bindingFor(bindings, runner.packageManager);
1059
+ const args =
1060
+ step.kind === "vitest"
1061
+ ? [...step.testFiles, ...step.args, ...reporter]
1062
+ : step.args;
1063
+ return binding
1064
+ ? {
1065
+ executable: binding.executablePath,
1066
+ args: [
1067
+ ...(binding.fixedArgs ?? []),
1068
+ ...packageScriptInvocation(
1069
+ runner.packageManager,
1070
+ runner.script,
1071
+ args,
1072
+ ),
1073
+ ],
1074
+ }
1075
+ : undefined;
1076
+ }
1077
+ if (runner.kind === "local-binary") {
1078
+ const binding = bindingFor(bindings, runner.executable);
1079
+ return {
1080
+ executable:
1081
+ binding?.executablePath ??
1082
+ `/workspace/node_modules/.bin/${runner.executable}`,
1083
+ args: [
1084
+ ...(binding?.fixedArgs ?? []),
1085
+ ...(step.kind === "vitest"
1086
+ ? [...step.testFiles, ...step.args, ...reporter]
1087
+ : step.args),
1088
+ ],
1089
+ };
1090
+ }
1091
+ const command = runner.kind === "node" ? "node" : "npx";
1092
+ const binding = bindingFor(bindings, command);
1093
+ if (!binding) return undefined;
1094
+ return {
1095
+ executable: binding.executablePath,
1096
+ args:
1097
+ runner.kind === "node"
1098
+ ? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
1099
+ : [
1100
+ ...(binding.fixedArgs ?? []),
1101
+ "--no-install",
1102
+ runner.executable,
1103
+ ...(step.kind === "vitest" ? step.testFiles : []),
1104
+ ...step.args,
1105
+ ...reporter,
1106
+ ],
1107
+ };
1108
+ }
1109
+
1110
+ function vitestReport(stdout: string):
1111
+ | {
1112
+ total: number;
1113
+ failed: number;
1114
+ success: boolean;
1115
+ failures: string[];
1116
+ failedText: string[];
1117
+ }
1118
+ | undefined {
1119
+ let value: unknown;
1120
+ try {
1121
+ value = JSON.parse(stdout);
1122
+ } catch {
1123
+ return undefined;
1124
+ }
1125
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1126
+ return undefined;
1127
+ }
1128
+ const report = value as Record<string, unknown>;
1129
+ if (
1130
+ !Number.isSafeInteger(report.numTotalTests) ||
1131
+ !Number.isSafeInteger(report.numFailedTests) ||
1132
+ typeof report.success !== "boolean"
1133
+ ) {
1134
+ return undefined;
1135
+ }
1136
+ const failedAssertions = (
1137
+ Array.isArray(report.testResults) ? report.testResults : []
1138
+ ).flatMap((suite): Array<{ identity: string; text: string[] }> => {
1139
+ if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
1140
+ const record = suite as Record<string, unknown>;
1141
+ const suiteName =
1142
+ typeof record.name === "string"
1143
+ ? record.name.replace(/^\/workspace\//u, "")
1144
+ : "unknown-suite";
1145
+ if (!Array.isArray(record.assertionResults)) return [];
1146
+ return record.assertionResults.flatMap(
1147
+ (assertion): Array<{ identity: string; text: string[] }> => {
1148
+ if (
1149
+ !assertion ||
1150
+ typeof assertion !== "object" ||
1151
+ Array.isArray(assertion)
1152
+ ) {
1153
+ return [];
1154
+ }
1155
+ const entry = assertion as Record<string, unknown>;
1156
+ if (entry.status !== "failed") return [];
1157
+ const title =
1158
+ typeof entry.fullName === "string"
1159
+ ? entry.fullName
1160
+ : typeof entry.title === "string"
1161
+ ? entry.title
1162
+ : "unknown-assertion";
1163
+ const diagnostics = Array.isArray(entry.failureMessages)
1164
+ ? entry.failureMessages.filter(
1165
+ (message): message is string => typeof message === "string",
1166
+ )
1167
+ : [];
1168
+ return [
1169
+ {
1170
+ identity: `${suiteName}\0${title}`,
1171
+ text: [title, ...diagnostics],
1172
+ },
1173
+ ];
1174
+ },
1175
+ );
1176
+ });
1177
+ return {
1178
+ total: report.numTotalTests as number,
1179
+ failed: report.numFailedTests as number,
1180
+ success: report.success,
1181
+ failures: failedAssertions.map((failure) => failure.identity),
1182
+ failedText: failedAssertions.flatMap((failure) => failure.text),
1183
+ };
1184
+ }
1185
+
1186
+ function normalizedFailureIdentities(input: {
1187
+ step: AtomicVerificationContract;
1188
+ report?: ReturnType<typeof vitestReport>;
1189
+ output: string;
1190
+ exitCode: number;
1191
+ }): string[] {
1192
+ const vitestFailures = input.report?.failures ?? [];
1193
+ if (vitestFailures.length > 0) {
1194
+ return [...new Set(vitestFailures)]
1195
+ .sort()
1196
+ .map((failure) => sha256(`vitest-failure-v1\0${failure}`));
1197
+ }
1198
+ const normalizedOutput = input.output
1199
+ .replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
1200
+ .replace(/\r\n?/gu, "\n")
1201
+ .replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
1202
+ .slice(0, 64 * 1024);
1203
+ return [
1204
+ sha256(
1205
+ canonicalJson({
1206
+ version: 1,
1207
+ verificationId: input.step.id,
1208
+ exitCode: input.exitCode,
1209
+ output: normalizedOutput,
1210
+ }),
1211
+ ),
1212
+ ];
1213
+ }
1214
+
1215
+ export async function executePackageVerification(input: {
1216
+ root: string;
1217
+ dependencyOwner: string;
1218
+ verification: StructuredVerificationContract;
1219
+ signal: AbortSignal;
1220
+ }) {
1221
+ const capability = bindCurrentVerificationCapability(
1222
+ input.root,
1223
+ input.verification,
1224
+ { dependencyOwner: input.dependencyOwner },
1225
+ );
1226
+ if (!capability.ok) {
1227
+ return {
1228
+ ok: false as const,
1229
+ kind: "paused" as const,
1230
+ code: capability.diagnostic.code,
1231
+ };
1232
+ }
1233
+ let runners: ReturnType<typeof prepareRunnerBindings>;
1234
+ try {
1235
+ runners = prepareRunnerBindings(
1236
+ input.root,
1237
+ capability.value.runnerBindings,
1238
+ );
1239
+ } catch {
1240
+ return {
1241
+ ok: false as const,
1242
+ kind: "paused" as const,
1243
+ code: "runner-mount-unavailable",
1244
+ };
1245
+ }
1246
+ try {
1247
+ const nodeModules = path.join(input.dependencyOwner, "node_modules");
1248
+ const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
1249
+ const mounts = [] as Array<{
1250
+ source: string;
1251
+ target: string;
1252
+ writable?: boolean;
1253
+ }>;
1254
+ if (dependency) {
1255
+ if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
1256
+ return {
1257
+ ok: false as const,
1258
+ kind: "paused" as const,
1259
+ code: "dependency-path-unsafe",
1260
+ };
1261
+ }
1262
+ const target = path.join(input.root, "node_modules");
1263
+ const targetStat = lstatSync(target, { throwIfNoEntry: false });
1264
+ if (
1265
+ targetStat &&
1266
+ (!targetStat.isDirectory() || targetStat.isSymbolicLink())
1267
+ ) {
1268
+ return {
1269
+ ok: false as const,
1270
+ kind: "paused" as const,
1271
+ code: "dependency-path-unsafe",
1272
+ };
1273
+ }
1274
+ if (!targetStat) mkdirSync(target, { mode: 0o700 });
1275
+ mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
1276
+ }
1277
+ mounts.push(...runners.mounts);
1278
+ const isolation = new BubblewrapIsolationBackend();
1279
+ const steps = verificationSteps(input.verification);
1280
+ let final:
1281
+ | {
1282
+ exitCode: number;
1283
+ classification: AtomicVerificationContract["classification"];
1284
+ diagnostic: { kind: "assertion" | "compiler"; id: string };
1285
+ }
1286
+ | undefined;
1287
+ for (const step of steps) {
1288
+ const invocation = verificationInvocation(step, runners.bindings);
1289
+ if (!invocation) {
1290
+ return {
1291
+ ok: false as const,
1292
+ kind: "paused" as const,
1293
+ code: "runner-missing",
1294
+ };
1295
+ }
1296
+ const executed = await isolation.run({
1297
+ root: input.root,
1298
+ executable: invocation.executable,
1299
+ args: invocation.args,
1300
+ mounts,
1301
+ environment: { CI: "1" },
1302
+ signal: input.signal,
1303
+ });
1304
+ if (!executed.ok) {
1305
+ if (executed.state === "cancelled") throw input.signal.reason;
1306
+ return {
1307
+ ok: false as const,
1308
+ kind: "paused" as const,
1309
+ code: executed.code,
1310
+ };
1311
+ }
1312
+ const report =
1313
+ step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
1314
+ const output = `${executed.stdout}\n${executed.stderr}`;
1315
+ const identity =
1316
+ step.classification !== "expected-red" ||
1317
+ (step.kind === "vitest"
1318
+ ? report?.failedText.some((text) =>
1319
+ text.includes(step.expectedFailure ?? ""),
1320
+ ) === true
1321
+ : output.includes(step.expectedFailure ?? ""));
1322
+ const accepted =
1323
+ step.classification === "expected-red"
1324
+ ? executed.exitCode !== 0 &&
1325
+ identity &&
1326
+ (step.kind !== "vitest" ||
1327
+ (report !== undefined &&
1328
+ report.failed > 0 &&
1329
+ report.total >= step.minTests))
1330
+ : executed.exitCode === 0 &&
1331
+ (step.kind !== "vitest" ||
1332
+ (report?.success === true && report.total >= step.minTests));
1333
+ if (!accepted) {
1334
+ return {
1335
+ ok: false as const,
1336
+ kind: "retryable" as const,
1337
+ code:
1338
+ step.classification === "expected-red" && executed.exitCode === 0
1339
+ ? "red-not-witnessed"
1340
+ : "verification-rejected",
1341
+ failureIdentities: normalizedFailureIdentities({
1342
+ step,
1343
+ report,
1344
+ output,
1345
+ exitCode: executed.exitCode,
1346
+ }),
1347
+ };
1348
+ }
1349
+ if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
1350
+ return {
1351
+ ok: false as const,
1352
+ kind: "retryable" as const,
1353
+ code: "verification-input-unavailable",
1354
+ };
1355
+ }
1356
+ final = {
1357
+ exitCode: executed.exitCode,
1358
+ classification: step.classification,
1359
+ diagnostic: {
1360
+ kind: step.kind === "vitest" ? "assertion" : "compiler",
1361
+ id: step.id,
1362
+ },
1363
+ };
1364
+ }
1365
+ return final
1366
+ ? { ok: true as const, ...final }
1367
+ : {
1368
+ ok: false as const,
1369
+ kind: "paused" as const,
1370
+ code: "verification-contract-unsupported",
1371
+ };
1372
+ } finally {
1373
+ if (runners.cleanupRoot) {
1374
+ rmSync(runners.cleanupRoot, { recursive: true, force: true });
1375
+ }
1376
+ }
1377
+ }
1378
+
1379
+ function affectedVerification(
1380
+ task: Parameters<
1381
+ Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
1382
+ >[0]["task"],
1383
+ ): StructuredVerificationContract {
1384
+ if (task.affectedVerification) {
1385
+ return structuredClone(task.affectedVerification);
1386
+ }
1387
+ const finalPhase = task.phases.refactor ?? task.phases.green;
1388
+ const verification = structuredClone(finalPhase.verification);
1389
+ const affected = [...new Set(task.impactClosure.affectedSuite)].sort();
1390
+ if (affected.length === 0) return verification;
1391
+ if (verification.kind === "vitest") {
1392
+ return { ...verification, testFiles: affected };
1393
+ }
1394
+ if (verification.kind === "steps") {
1395
+ return {
1396
+ ...verification,
1397
+ steps: verification.steps.map((step) =>
1398
+ step.kind === "vitest" ? { ...step, testFiles: affected } : step,
1399
+ ),
1400
+ };
1401
+ }
1402
+ return verification;
1403
+ }
1404
+
1405
+ function childRequestId(
1406
+ operationId: string,
1407
+ taskId: string,
1408
+ phase: string,
1409
+ ): string {
1410
+ return `child-${sha256(`${operationId}\0${taskId}\0${phase}`).slice(0, 40)}`;
1411
+ }
1412
+
1413
+ export function openPackageWorkflowControlEngine(
1414
+ initialContext: ExtensionContext,
1415
+ parentPayloadBridge: ParentPayloadBridge,
1416
+ ): WorkflowControlEngine {
1417
+ const consumerRoot = path.resolve(initialContext.cwd);
1418
+ const routeResolution = loadRoutePolicy({
1419
+ cwd: consumerRoot,
1420
+ home: homedir(),
1421
+ });
1422
+ const stateRoot = resolveStateRoot({
1423
+ consumerRoot,
1424
+ xdgStateHome: process.env.XDG_STATE_HOME,
1425
+ });
1426
+ const contexts = new AsyncLocalStorage<ExtensionContext>();
1427
+ const design = DesignController.open({
1428
+ consumerRoot,
1429
+ stateRoot,
1430
+ inspectOpenSpec: inspectOpenSpecDelivery,
1431
+ });
1432
+ const implementationAgent = loadAgentDefinitions().find(
1433
+ (agent) => agent.role === "implementation-worker",
1434
+ );
1435
+ if (!implementationAgent) {
1436
+ throw new Error("implementation-worker-agent-unavailable");
1437
+ }
1438
+ const engine = openDurableWorkflowEngine({
1439
+ consumerRoot,
1440
+ stateRoot,
1441
+ deliverySource: packageDeliverySource(consumerRoot, {
1442
+ verifyGateProof: (input) => design.verifyGateProof(input),
1443
+ verifyFinalizedDelivery: (input) => design.verifyFinalizedDelivery(input),
1444
+ }),
1445
+ routePolicy: routeResolution.ok
1446
+ ? routeResolution.policy
1447
+ : unavailableRoutePolicy(),
1448
+ proposeCandidate: async (input) => {
1449
+ const context = contexts.getStore();
1450
+ if (!context) {
1451
+ return { kind: "paused", code: "parent-context-unavailable" };
1452
+ }
1453
+ const phaseRuntime = await runtimeForWorkerRoute(
1454
+ input.route as WorkerRoutePolicy,
1455
+ context,
1456
+ parentPayloadBridge,
1457
+ input.signal,
1458
+ );
1459
+ if (!phaseRuntime.ok) {
1460
+ if (phaseRuntime.failure.kind === "cancelled") {
1461
+ return { kind: "operation-cancelled", code: "cancelled" };
1462
+ }
1463
+ throw new Error(phaseRuntime.failure.code);
1464
+ }
1465
+ const phase = input.task.phases[input.phase];
1466
+ if (!phase) {
1467
+ return { kind: "paused", code: "task-phase-unavailable" };
1468
+ }
1469
+ const phaseContract = {
1470
+ candidateId: input.candidateArtifact.identity.candidateId,
1471
+ taskId: input.taskId,
1472
+ phase: input.phase,
1473
+ readSet: [...phase.read],
1474
+ writeSet: [...phase.write],
1475
+ deleteSet: [...phase.delete],
1476
+ verification: structuredClone(phase.verification),
1477
+ agentsImpact: input.task.agents.impact,
1478
+ agentsTarget: input.task.agents.target ?? null,
1479
+ agentsManagedOnly: true,
1480
+ agentsWriteAllowed: false,
1481
+ impactClosure: structuredClone(input.task.impactClosure),
1482
+ ...(input.repair
1483
+ ? {
1484
+ repair: {
1485
+ attempt: input.repair.attempt,
1486
+ attribution: input.repair.attribution,
1487
+ failureIdentities: [...input.repair.failureIdentities],
1488
+ verification: structuredClone(input.task.repairVerification),
1489
+ inBoundaryOnly: true,
1490
+ },
1491
+ }
1492
+ : {}),
1493
+ };
1494
+ const child = await runChildSession({
1495
+ cwd: input.workspaceRoot,
1496
+ modelRuntime: phaseRuntime.modelRuntime,
1497
+ model: phaseRuntime.model,
1498
+ systemPrompt: [
1499
+ implementationAgent.content,
1500
+ input.task.objective,
1501
+ input.task.context.agents,
1502
+ input.task.context.contract,
1503
+ `<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
1504
+ ].join("\n\n"),
1505
+ requestId: childRequestId(input.operationId, input.taskId, input.phase),
1506
+ taskId: input.taskId,
1507
+ role: "implementation-worker",
1508
+ phase: input.phase,
1509
+ output: "diff",
1510
+ roots: input.task.roots.map((root) =>
1511
+ path.resolve(input.workspaceRoot, root),
1512
+ ),
1513
+ allowedPaths: [
1514
+ ...new Set([...phase.read, ...phase.write, ...phase.delete]),
1515
+ ],
1516
+ timeoutMs: LIMITS.phaseTimeoutMs,
1517
+ signal: input.signal,
1518
+ failureOverride: phaseRuntime.failureOverride,
1519
+ ledgerProjection: input.ledgerProjection,
1520
+ candidateArtifact: input.candidateArtifact,
1521
+ onStreamStart: input.onHeaders,
1522
+ onStreamProgress: input.onProgress,
1523
+ });
1524
+ if (!child.ok) {
1525
+ if (child.failure.kind === "transport") {
1526
+ throw new Error(child.failure.code);
1527
+ }
1528
+ if (child.failure.kind === "cancelled") {
1529
+ return { kind: "operation-cancelled", code: "cancelled" };
1530
+ }
1531
+ if (child.failure.kind === "approval-boundary") {
1532
+ return { kind: "approval-needed", code: child.failure.code };
1533
+ }
1534
+ if (child.failure.kind === "result-limit") {
1535
+ return { kind: "paused", code: "needs-task-split" };
1536
+ }
1537
+ return child.failure.kind === "environment" ||
1538
+ child.failure.kind === "verification-adapter"
1539
+ ? { kind: "paused", code: child.failure.code }
1540
+ : { kind: "retryable", code: child.failure.code };
1541
+ }
1542
+ const result = child.result;
1543
+ if (result.kind === "context-request") {
1544
+ return classifyCandidateContextRequest(result, [
1545
+ ...phase.read,
1546
+ ...phase.write,
1547
+ ...phase.delete,
1548
+ ]);
1549
+ }
1550
+ if (result.kind !== "sealed-candidate") {
1551
+ return { kind: "retryable", code: "candidate-diff-invalid" };
1552
+ }
1553
+ return {
1554
+ kind: "sealed-candidate",
1555
+ candidateId: result.candidateId,
1556
+ artifactHash: result.artifactHash,
1557
+ bytes: result.bytes,
1558
+ paths: [...result.paths],
1559
+ };
1560
+ },
1561
+ verifyPhase: (input) =>
1562
+ executePackageVerification({
1563
+ root: input.root,
1564
+ dependencyOwner: consumerRoot,
1565
+ verification: input.verification,
1566
+ signal: input.signal,
1567
+ }),
1568
+ verifyChange: async (input) => {
1569
+ const verifications = input.verification
1570
+ ? [input.verification]
1571
+ : input.plan.tasks.map((task) => affectedVerification(task));
1572
+ for (const verification of verifications) {
1573
+ const result = await executePackageVerification({
1574
+ root: input.root,
1575
+ dependencyOwner: consumerRoot,
1576
+ verification,
1577
+ signal: input.signal,
1578
+ });
1579
+ if (!result.ok) {
1580
+ const adapter = new Set([
1581
+ "runner-missing",
1582
+ "script-missing",
1583
+ "script-command-mismatch",
1584
+ "local-executable-missing",
1585
+ ]).has(result.code);
1586
+ return {
1587
+ ok: false,
1588
+ kind: adapter
1589
+ ? ("verification-adapter" as const)
1590
+ : result.kind === "paused"
1591
+ ? ("environment" as const)
1592
+ : ("verification" as const),
1593
+ code: result.code,
1594
+ ...(result.failureIdentities
1595
+ ? { failureIdentities: result.failureIdentities }
1596
+ : {}),
1597
+ };
1598
+ }
1599
+ }
1600
+ return {
1601
+ ok: true,
1602
+ exitCode: 0,
1603
+ classification: "expected-green",
1604
+ };
1605
+ },
1606
+ });
1607
+ return {
1608
+ async execute(
1609
+ command: unknown,
1610
+ context = initialContext,
1611
+ signal,
1612
+ onActivity,
1613
+ ) {
1614
+ const operationRouteResolution = loadRoutePolicy({
1615
+ cwd: consumerRoot,
1616
+ home: homedir(),
1617
+ });
1618
+ engine.updateRoutePolicy(
1619
+ operationRouteResolution.ok
1620
+ ? operationRouteResolution.policy
1621
+ : unavailableRoutePolicy(),
1622
+ );
1623
+ const validation = validateControlCommand(command);
1624
+ if (!validation.ok) {
1625
+ const error = new Error(validation.code);
1626
+ error.name = "ControlCommandError";
1627
+ throw error;
1628
+ }
1629
+ return contexts.run(context, async () => {
1630
+ const outcome = await engine.execute(
1631
+ validation.value,
1632
+ signal,
1633
+ onActivity,
1634
+ );
1635
+ const routePolicy = visibleRoutePolicyStatus(
1636
+ operationRouteResolution,
1637
+ engine.routePolicyStatus(),
1638
+ );
1639
+ const designStatus =
1640
+ validation.value.stage === "abel-design" &&
1641
+ typeof outcome.runId === "string"
1642
+ ? { design: design.status(outcome.runId) }
1643
+ : {};
1644
+ return { ...outcome, ...designStatus, routePolicy };
1645
+ });
1646
+ },
1647
+ executeDesign(request: unknown) {
1648
+ return design.execute(request);
1649
+ },
1650
+ assertDesignRun(runId: string) {
1651
+ design.assertDesignRun(runId);
1652
+ },
1653
+ recordDesignEvidence(input) {
1654
+ return design.recordEvidence(input);
1655
+ },
1656
+ async close() {
1657
+ contexts.disable();
1658
+ await engine.close();
1659
+ design.close();
1660
+ },
1661
+ };
1662
+ }
1663
+
1664
+ function visibleRoutePolicyStatus(
1665
+ resolution: RoutePolicyResolution,
1666
+ brokerStatus: Record<string, unknown> | undefined,
1667
+ ): Record<string, unknown> {
1668
+ if (!resolution.ok) return inspectRoutePolicy(resolution);
1669
+ const inspected = brokerStatus ?? inspectRoutePolicy(resolution);
1670
+ return {
1671
+ ...inspected,
1672
+ source: { kind: resolution.source.kind },
1673
+ };
1674
+ }
1675
+
1676
+ export function registerWorkflowControl(
1677
+ pi: ExtensionAPI,
1678
+ engineFactory: WorkflowControlEngineFactory = openPackageWorkflowControlEngine,
1679
+ ): void {
222
1680
  const parentPayloadBridge = new ParentPayloadBridge();
223
- const runtime = new Runtime({ parentPayloadBridge });
224
1681
  const activity = new ActivityController();
1682
+ const engines = new Map<string, Promise<WorkflowControlEngine>>();
1683
+ let pendingPrompt: EligiblePrompt | undefined;
1684
+ let activePrompt: EligiblePrompt | undefined;
1685
+ let designToolSnapshot: string[] | undefined;
1686
+ const activation = new (class implements Activation {
1687
+ state: Activation["state"] = "inactive";
225
1688
 
226
- const registerDispatchTool = (stage?: EligiblePrompt) => {
1689
+ isActive(): boolean {
1690
+ return this.state === "active";
1691
+ }
1692
+
1693
+ request(): boolean {
1694
+ if (this.state !== "inactive") return false;
1695
+ this.state = "pending";
1696
+ return true;
1697
+ }
1698
+
1699
+ activate(): boolean {
1700
+ if (this.state !== "pending") return false;
1701
+ this.state = "active";
1702
+ return true;
1703
+ }
1704
+
1705
+ drain(): boolean {
1706
+ if (this.state === "inactive" || this.state === "pending") return false;
1707
+ this.state = "inactive";
1708
+ return true;
1709
+ }
1710
+ })();
1711
+ const packetRuntime = new PacketRuntime({ activation, parentPayloadBridge });
1712
+
1713
+ const engineFor = (ctx: ExtensionContext) => {
1714
+ const key = ctx.cwd;
1715
+ const existing = engines.get(key);
1716
+ if (existing) return existing;
1717
+ const opened = Promise.resolve(engineFactory(ctx, parentPayloadBridge));
1718
+ engines.set(key, opened);
1719
+ void opened.catch(() => {
1720
+ if (engines.get(key) === opened) engines.delete(key);
1721
+ });
1722
+ return opened;
1723
+ };
1724
+ const closeEngines = async () => {
1725
+ const current = [...engines.values()];
1726
+ engines.clear();
1727
+ const opened = await Promise.allSettled(current);
1728
+ await Promise.allSettled(
1729
+ opened.flatMap((result) =>
1730
+ result.status === "fulfilled"
1731
+ ? [Promise.resolve(result.value.close())]
1732
+ : [],
1733
+ ),
1734
+ );
1735
+ };
1736
+ const restoreDesignTools = () => {
1737
+ if (!designToolSnapshot) return;
1738
+ const snapshot = designToolSnapshot;
1739
+ designToolSnapshot = undefined;
1740
+ pi.setActiveTools([...snapshot]);
1741
+ };
1742
+ const enforceDesignTools = () => {
1743
+ if (!designToolSnapshot) {
1744
+ designToolSnapshot = pi
1745
+ .getActiveTools()
1746
+ .filter((name) => name !== DISPATCH_TOOL);
1747
+ }
1748
+ pi.setActiveTools(
1749
+ activateTool(
1750
+ designToolSnapshot.filter((name) => DESIGN_PARENT_READ_TOOLS.has(name)),
1751
+ DISPATCH_TOOL,
1752
+ ),
1753
+ );
1754
+ };
1755
+ const deactivate = () => {
1756
+ activation.drain();
1757
+ if (designToolSnapshot) {
1758
+ restoreDesignTools();
1759
+ return;
1760
+ }
1761
+ const active = pi.getActiveTools();
1762
+ if (active.includes(DISPATCH_TOOL)) {
1763
+ pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
1764
+ }
1765
+ };
1766
+ const deactivateStage = async () => {
1767
+ activePrompt = undefined;
1768
+ await packetRuntime.drain();
1769
+ deactivate();
1770
+ parentPayloadBridge.clear();
1771
+ };
1772
+
1773
+ // Pi validates tool arguments before execute(), so expose only the schema
1774
+ // legal for the active stage instead of one ambiguous command/packet union.
1775
+ const CONTROL_COMMAND_PARAMETERS = {
1776
+ type: "object",
1777
+ properties: {
1778
+ command: {
1779
+ type: "string",
1780
+ enum: ["start", "status", "resume", "rebind", "cancel", "discard"],
1781
+ },
1782
+ stage: {
1783
+ type: "string",
1784
+ enum: ["abel-design", "abel-implement"],
1785
+ },
1786
+ change: { type: "string" },
1787
+ provisionalKey: { type: "string" },
1788
+ operationId: { type: "string" },
1789
+ deliveryRevision: { type: "integer", minimum: 1 },
1790
+ receiptHash: { type: "string" },
1791
+ routeId: { type: "string" },
1792
+ },
1793
+ required: ["command", "stage"],
1794
+ additionalProperties: false,
1795
+ } as const;
1796
+ const PACKET_PARAMETERS = {
1797
+ type: "object",
1798
+ properties: {
1799
+ action: { type: "string", enum: [...PACKET_ACTIONS, "design"] },
1800
+ request: {
1801
+ anyOf: [
1802
+ DESIGN_REQUEST_SCHEMA,
1803
+ DESIGN_CONTROL_REQUEST_SCHEMA,
1804
+ GENERIC_REQUEST_SCHEMA,
1805
+ ],
1806
+ },
1807
+ },
1808
+ required: ["action"],
1809
+ additionalProperties: false,
1810
+ } as const;
1811
+ let registeredParameterKind: "command" | "packet" | undefined;
1812
+
1813
+ const registerDispatchTool = (kind: "command" | "packet") => {
1814
+ if (registeredParameterKind === kind) return;
1815
+ registeredParameterKind = kind;
227
1816
  pi.registerTool({
228
1817
  name: DISPATCH_TOOL,
229
- label: "Abel Dispatch",
1818
+ label: "Abel Control",
230
1819
  description:
231
- stage === "abel-design"
232
- ? "Run exactly one bounded read-only design-explorer packet. When parallel packets are required, emit every sibling abel_dispatch call together in one assistant response before waiting for any result."
233
- : "Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, apply a parent-only stable AGENTS checkpoint, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
1820
+ kind === "command"
1821
+ ? "Private stage-bound Abel workflow control. Accepts durable Design and Implement change commands."
1822
+ : "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
234
1823
  executionMode: "parallel",
235
- renderShell: "self",
236
- parameters: {
237
- type: "object",
238
- properties: {
239
- action: { type: "string", enum: [...ACTIONS] },
240
- request: requestSchemaForStage(stage),
241
- resultId: {
242
- type: "string",
243
- description: "Retained result id for apply/discard",
244
- },
245
- requestId: {
246
- type: "string",
247
- description: "Current Implement apply/discard operation identity",
248
- },
249
- rejection: {
250
- type: "object",
251
- description: "Typed parent rejection for Implement discard",
252
- },
253
- agentsCheckpoint: {
254
- type: "object",
255
- description:
256
- "Parent-owned approved managed-block checkpoint for action=apply (mutually exclusive with resultId)",
257
- },
258
- },
259
- required: ["action"],
260
- },
1824
+ parameters:
1825
+ kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
261
1826
  async execute(
262
1827
  toolCallId: string,
263
- params: {
264
- action?: string;
265
- request?: unknown;
266
- resultId?: string;
267
- requestId?: string;
268
- rejection?: unknown;
269
- agentsCheckpoint?: unknown;
270
- },
1828
+ params: unknown,
271
1829
  signal: AbortSignal | undefined,
272
1830
  onUpdate: AgentToolUpdateCallback<unknown> | undefined,
273
1831
  ctx: ExtensionContext,
274
1832
  ) {
275
- const action = typeof params?.action === "string" ? params.action : "";
276
- const validRun =
277
- action === "run" && runtime.validateRequest(params.request).ok;
278
- const tuiRun = ctx.mode === "tui" && validRun;
279
- const { action: _action, ...operation } = params;
280
- const result = tuiRun
281
- ? await runtime.execute(
282
- action,
283
- operation,
284
- ctx,
285
- signal,
286
- activity.observe(
287
- toolCallId,
288
- onUpdate as ((result: unknown) => void) | undefined,
289
- ),
1833
+ const record =
1834
+ params && typeof params === "object" && !Array.isArray(params)
1835
+ ? (params as Record<string, unknown>)
1836
+ : undefined;
1837
+ if (record && typeof record.action === "string") {
1838
+ if (
1839
+ Object.keys(record).some(
1840
+ (key) => key !== "action" && key !== "request",
290
1841
  )
291
- : await runtime.execute(action, operation, ctx, signal);
292
- const display = tuiRun
293
- ? activity.finalize(toolCallId, result)
294
- : undefined;
295
- const { payload, usage } = splitUsage(result);
296
- const details = display
297
- ? {
298
- ...(payload as Record<string, unknown>),
299
- [ACTIVITY_DETAILS_KEY]: display,
1842
+ ) {
1843
+ throw new Error("control-envelope-ambiguous");
1844
+ }
1845
+ if (
1846
+ activePrompt !== "abel-design" &&
1847
+ activePrompt !== "abel-diagnose"
1848
+ ) {
1849
+ throw new Error("stage-control-mismatch");
1850
+ }
1851
+ if (record.action === "design") {
1852
+ if (activePrompt !== "abel-design") {
1853
+ throw new Error("stage-control-mismatch");
1854
+ }
1855
+ const designRequest = validateDesignControlRequest(record.request);
1856
+ if (!designRequest.ok) throw new Error(designRequest.code);
1857
+ const engine = await engineFor(ctx);
1858
+ if (!engine.executeDesign)
1859
+ throw new Error("design-control-unavailable");
1860
+ const payload = await engine.executeDesign(designRequest.value);
1861
+ if (
1862
+ designRequest.value.operation === "finalize-delivery" &&
1863
+ payload.state === "completed"
1864
+ ) {
1865
+ await deactivateStage();
1866
+ }
1867
+ return {
1868
+ content: [
1869
+ { type: "text" as const, text: JSON.stringify(payload) },
1870
+ ],
1871
+ details: payload,
1872
+ };
1873
+ }
1874
+ let designEngine: WorkflowControlEngine | undefined;
1875
+ if (record.action === "run") {
1876
+ const packet = packetRuntime.validateRequest(record.request);
1877
+ if (!packet.ok || packet.value.stage !== activePrompt) {
1878
+ throw new Error(
1879
+ packet.ok ? "stage-control-mismatch" : packet.reason,
1880
+ );
1881
+ }
1882
+ if (packet.value.stage === "abel-design") {
1883
+ designEngine = await engineFor(ctx);
1884
+ if (!designEngine.assertDesignRun || !packet.value.runId) {
1885
+ throw new Error("design-control-unavailable");
1886
+ }
1887
+ designEngine.assertDesignRun(packet.value.runId);
1888
+ }
1889
+ }
1890
+ const operation = {
1891
+ request: record.request,
1892
+ };
1893
+ const tuiRun = ctx.mode === "tui" && record.action === "run";
1894
+ const packetPayload = tuiRun
1895
+ ? await packetRuntime.execute(
1896
+ record.action,
1897
+ operation,
1898
+ ctx,
1899
+ signal,
1900
+ activity.observe(
1901
+ toolCallId,
1902
+ onUpdate as ((result: unknown) => void) | undefined,
1903
+ ),
1904
+ )
1905
+ : await packetRuntime.execute(
1906
+ record.action,
1907
+ operation,
1908
+ ctx,
1909
+ signal,
1910
+ );
1911
+ let payload:
1912
+ | typeof packetPayload
1913
+ | (typeof packetPayload & {
1914
+ recordedEvidence: unknown;
1915
+ }) = packetPayload;
1916
+ if (
1917
+ packetPayload.ok &&
1918
+ record.action === "run" &&
1919
+ activePrompt === "abel-design"
1920
+ ) {
1921
+ const packet = packetRuntime.validateRequest(record.request);
1922
+ if (
1923
+ !packet.ok ||
1924
+ !packet.value.runId ||
1925
+ !designEngine?.recordDesignEvidence
1926
+ ) {
1927
+ throw new Error("design-control-unavailable");
300
1928
  }
301
- : payload;
1929
+ const recordedEvidence = designEngine.recordDesignEvidence({
1930
+ runId: packet.value.runId,
1931
+ evidence: packetPayload.result as DesignEvidenceResult,
1932
+ });
1933
+ payload = { ...packetPayload, recordedEvidence };
1934
+ }
1935
+ if (record.action === "finish" && packetPayload.ok) {
1936
+ await deactivateStage();
1937
+ }
1938
+ const display = tuiRun
1939
+ ? activity.finalize(toolCallId, payload)
1940
+ : undefined;
1941
+ const { payload: publicPayload, usage } = splitUsage(payload);
1942
+ const details = display
1943
+ ? {
1944
+ ...(publicPayload as Record<string, unknown>),
1945
+ [ACTIVITY_DETAILS_KEY]: display,
1946
+ }
1947
+ : publicPayload;
1948
+ return {
1949
+ content: [
1950
+ { type: "text" as const, text: JSON.stringify(publicPayload) },
1951
+ ],
1952
+ details,
1953
+ ...(usage === undefined ? {} : { usage }),
1954
+ };
1955
+ }
1956
+ const validation = validateControlCommand(params);
1957
+ if (!validation.ok) {
1958
+ const error = new Error(validation.code);
1959
+ error.name = "ControlCommandError";
1960
+ throw error;
1961
+ }
1962
+ if (
1963
+ activePrompt !== undefined &&
1964
+ (activePrompt === "abel-diagnose" ||
1965
+ validation.value.stage !== activePrompt)
1966
+ ) {
1967
+ throw new Error("stage-control-mismatch");
1968
+ }
1969
+ const engine = await engineFor(ctx);
1970
+ const tuiControl = ctx.mode === "tui";
1971
+ if (tuiControl) {
1972
+ try {
1973
+ activity.beginWorkflow(
1974
+ toolCallId,
1975
+ validation.value,
1976
+ onUpdate as ((result: unknown) => void) | undefined,
1977
+ );
1978
+ } catch {
1979
+ // Presentation state must never alter workflow execution.
1980
+ }
1981
+ }
1982
+ let payload: Record<string, unknown>;
1983
+ try {
1984
+ payload = await engine.execute(
1985
+ validation.value,
1986
+ ctx,
1987
+ signal,
1988
+ tuiControl
1989
+ ? (event) => {
1990
+ try {
1991
+ activity.updateWorkflow(
1992
+ toolCallId,
1993
+ validation.value,
1994
+ event,
1995
+ );
1996
+ } catch {
1997
+ // Presentation state must never alter workflow execution.
1998
+ }
1999
+ }
2000
+ : undefined,
2001
+ );
2002
+ } catch (error) {
2003
+ if (tuiControl) activity.failWorkflow(toolCallId);
2004
+ throw error;
2005
+ }
2006
+ if (
2007
+ ["completed", "discarded", "rejected"].includes(String(payload.state))
2008
+ ) {
2009
+ await deactivateStage();
2010
+ } else if (validation.value.stage === "abel-design") {
2011
+ // Design starts/statuses through the command surface, then switches to
2012
+ // its bounded evidence and artifact packet surface for the next turn.
2013
+ registerDispatchTool("packet");
2014
+ }
2015
+ let display: ReturnType<typeof activity.finalizeWorkflow>;
2016
+ if (tuiControl) {
2017
+ try {
2018
+ display = activity.finalizeWorkflow(
2019
+ toolCallId,
2020
+ validation.value,
2021
+ payload,
2022
+ );
2023
+ } catch {
2024
+ activity.failWorkflow(toolCallId);
2025
+ }
2026
+ }
302
2027
  return {
303
- content: [{ type: "text", text: JSON.stringify(payload) }],
304
- details,
305
- ...(usage === undefined ? {} : { usage }),
2028
+ content: [{ type: "text" as const, text: JSON.stringify(payload) }],
2029
+ details: display
2030
+ ? { ...payload, [ACTIVITY_DETAILS_KEY]: display }
2031
+ : payload,
306
2032
  };
307
2033
  },
308
2034
  renderCall(args: unknown, theme: Theme, context: unknown) {
@@ -327,26 +2053,23 @@ export default function register(pi: ExtensionAPI): void {
327
2053
  },
328
2054
  } as never);
329
2055
  };
330
-
331
- registerDispatchTool();
332
-
333
- let pendingPrompt: EligiblePrompt | undefined;
334
- let activePrompt: EligiblePrompt | undefined;
2056
+ registerDispatchTool("packet");
335
2057
 
336
2058
  pi.on("input", (event) => {
337
2059
  pendingPrompt = invokedPrompt(event.text);
338
2060
  return { action: "continue" };
339
2061
  });
340
-
341
2062
  pi.on("before_agent_start", (event, ctx) => {
342
2063
  const prompt = pendingPrompt;
343
2064
  pendingPrompt = undefined;
344
2065
  const verified =
345
- prompt &&
346
- isVerifiedStageInvocation(pi, runtime.activation, prompt, event.prompt);
2066
+ prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
347
2067
  if (verified) {
2068
+ if (activePrompt === "abel-design" && prompt !== "abel-design") {
2069
+ restoreDesignTools();
2070
+ }
2071
+ registerDispatchTool(prompt === "abel-diagnose" ? "packet" : "command");
348
2072
  activePrompt = prompt;
349
- registerDispatchTool(prompt);
350
2073
  const sessionId = ctx.sessionManager?.getSessionId?.();
351
2074
  if (typeof sessionId === "string") {
352
2075
  parentPayloadBridge.beginSession(sessionId);
@@ -357,10 +2080,19 @@ export default function register(pi: ExtensionAPI): void {
357
2080
  if (ctx.model) {
358
2081
  parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
359
2082
  }
360
- if (prompt)
361
- activateDispatcher(pi, runtime.activation, prompt, event.prompt);
2083
+ if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
2084
+ if (verified && prompt === "abel-design") enforceDesignTools();
2085
+ });
2086
+ pi.on("model_select", (event, ctx) => {
2087
+ const sessionId = ctx.sessionManager?.getSessionId?.();
2088
+ if (typeof sessionId !== "string") {
2089
+ parentPayloadBridge.clear();
2090
+ return;
2091
+ }
2092
+ parentPayloadBridge.beginSession(sessionId);
2093
+ const model = event.model ?? ctx.model;
2094
+ if (model) parentPayloadBridge.install(model, ctx.modelRegistry);
362
2095
  });
363
-
364
2096
  pi.on("before_provider_request", (event, ctx) => {
365
2097
  if (
366
2098
  activePrompt !== "abel-design" ||
@@ -373,50 +2105,33 @@ export default function register(pi: ExtensionAPI): void {
373
2105
  }
374
2106
  return { ...event.payload, parallel_tool_calls: true };
375
2107
  });
376
-
377
2108
  pi.on("session_start", async (_event, ctx) => {
378
2109
  pendingPrompt = undefined;
379
2110
  activePrompt = undefined;
380
2111
  activity.detach();
381
- const active = pi.getActiveTools();
382
- if (active.includes(DISPATCH_TOOL)) {
383
- pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
384
- }
385
- await runtime.drain();
386
- activity.clear();
2112
+ await packetRuntime.drain();
2113
+ await closeEngines();
2114
+ deactivate();
387
2115
  const sessionId = ctx.sessionManager?.getSessionId?.();
388
2116
  if (typeof sessionId === "string") {
389
2117
  parentPayloadBridge.beginSession(sessionId);
390
- if (ctx.model) {
391
- parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
392
- }
2118
+ if (ctx.model) parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
393
2119
  } else {
394
2120
  parentPayloadBridge.clear();
395
2121
  }
396
2122
  if (ctx.mode === "tui") activity.attach(ctx.ui);
397
2123
  });
398
-
399
- pi.on("model_select", (event, ctx) => {
400
- const sessionId = ctx.sessionManager?.getSessionId?.();
401
- if (typeof sessionId !== "string") {
402
- parentPayloadBridge.clear();
403
- return;
404
- }
405
- parentPayloadBridge.beginSession(sessionId);
406
- const model = event.model ?? ctx.model;
407
- if (model) {
408
- parentPayloadBridge.install(model, ctx.modelRegistry);
409
- }
410
- });
411
-
412
2124
  pi.on("session_shutdown", async () => {
413
2125
  activePrompt = undefined;
414
2126
  activity.detach();
415
- await runtime.drain();
2127
+ await packetRuntime.drain();
2128
+ await closeEngines();
416
2129
  activity.clear();
417
- const active = pi.getActiveTools();
418
- if (active.includes(DISPATCH_TOOL)) {
419
- pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
420
- }
2130
+ deactivate();
2131
+ parentPayloadBridge.clear();
421
2132
  });
422
2133
  }
2134
+
2135
+ export default function register(pi: ExtensionAPI): void {
2136
+ registerWorkflowControl(pi);
2137
+ }