@abelxiaoxing/cadence 1.1.0 → 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.
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import { loadAgentDefinitions } from "./agent-registry.ts";
31
31
  import { runChildSession } from "./child-session.ts";
32
32
  import {
33
33
  type AtomicVerificationContract,
34
+ type DesignEvidenceResult,
34
35
  isValidRelativePath,
35
36
  LIMITS,
36
37
  type StructuredVerificationContract,
@@ -41,11 +42,16 @@ import {
41
42
  assessDeliveryTraceability,
42
43
  compileImplementPlan,
43
44
  DeliveryValidationError,
45
+ type GateApprovalProof,
44
46
  IMPLEMENT_PLAN_SCHEMA_VERSION,
45
47
  parseGateAReceipt,
46
48
  parseImplementPlan,
47
49
  parseReadyReceipt,
48
50
  } from "./delivery-compiler.ts";
51
+ import {
52
+ DesignController,
53
+ validateDesignControlRequest,
54
+ } from "./design-control.ts";
49
55
  import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
50
56
  import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
51
57
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
@@ -75,10 +81,12 @@ import {
75
81
  } from "./verification-capability.ts";
76
82
  import {
77
83
  openDurableWorkflowEngine,
84
+ type WorkflowAvailableDelivery,
78
85
  type WorkflowDeliverySource,
79
86
  } from "./workflow-engine.ts";
80
87
 
81
88
  export const DISPATCH_TOOL = "abel_dispatch";
89
+ const DESIGN_PARENT_READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
82
90
 
83
91
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
84
92
  const ELIGIBLE_PROMPTS = [
@@ -109,6 +117,10 @@ const DESIGN_REQUEST_SCHEMA = {
109
117
  enum: ["design-explorer"],
110
118
  description: "Exact package-owned read-only Agent role.",
111
119
  },
120
+ runId: {
121
+ type: "string",
122
+ description: "Durable Design run identity returned by Design start.",
123
+ },
112
124
  id: {
113
125
  type: "string",
114
126
  description:
@@ -182,6 +194,7 @@ const DESIGN_REQUEST_SCHEMA = {
182
194
  required: [
183
195
  "stage",
184
196
  "role",
197
+ "runId",
185
198
  "id",
186
199
  "phase",
187
200
  "objective",
@@ -193,6 +206,90 @@ const DESIGN_REQUEST_SCHEMA = {
193
206
  additionalProperties: false,
194
207
  } as const;
195
208
 
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;
292
+
196
293
  function invokedPrompt(text: string): EligiblePrompt | undefined {
197
294
  const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
198
295
  return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
@@ -273,6 +370,12 @@ export interface WorkflowControlEngine {
273
370
  signal?: AbortSignal,
274
371
  onActivity?: (event: WorkflowActivityUpdate) => void,
275
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;
276
379
  close(): void | Promise<void>;
277
380
  }
278
381
 
@@ -297,12 +400,40 @@ export interface PackageDeliverySourceOptions {
297
400
  consumerRoot: string,
298
401
  change: string,
299
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>;
300
423
  }
301
424
 
302
425
  function sha256(value: Uint8Array | string): string {
303
426
  return createHash("sha256").update(value).digest("hex");
304
427
  }
305
428
 
429
+ function proofVerified(verify: () => boolean): boolean {
430
+ try {
431
+ return verify() === true;
432
+ } catch {
433
+ return false;
434
+ }
435
+ }
436
+
306
437
  function normalizedTrackingArtifact(
307
438
  bytes: Uint8Array,
308
439
  taskIds: readonly string[],
@@ -449,9 +580,80 @@ export async function inspectOpenSpecDelivery(
449
580
  export function packageDeliverySource(
450
581
  consumerRoot: string,
451
582
  options: PackageDeliverySourceOptions = {},
452
- ): WorkflowDeliverySource {
583
+ ): PackageWorkflowDeliverySource {
453
584
  const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
585
+ const verifyGateProof = options.verifyGateProof;
586
+ const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
454
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
+ },
455
657
  async load(input) {
456
658
  if (input.stage !== "abel-implement") {
457
659
  throw new DeliveryValidationError(["delivery-stage-invalid"]);
@@ -567,6 +769,19 @@ export function packageDeliverySource(
567
769
  }
568
770
  }
569
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
+ }
570
785
  for (const artifact of gateA.artifacts) {
571
786
  if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
572
787
  diagnostics.add(
@@ -575,6 +790,36 @@ export function packageDeliverySource(
575
790
  }
576
791
  }
577
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
+ }
578
823
 
579
824
  let plan: ReturnType<typeof parseImplementPlan> | undefined;
580
825
  let planBytes: Buffer | undefined;
@@ -675,7 +920,7 @@ export function packageDeliverySource(
675
920
  }
676
921
  }
677
922
  }
678
- if (!plan || diagnostics.size > 0) {
923
+ if (!plan || !gateA || diagnostics.size > 0) {
679
924
  throw new DeliveryValidationError([...diagnostics]);
680
925
  }
681
926
  return {
@@ -684,6 +929,10 @@ export function packageDeliverySource(
684
929
  revision: receipt.deliveryRevision,
685
930
  receiptHash,
686
931
  plan,
932
+ approvalProofs: {
933
+ gateA: structuredClone(gateA.approval),
934
+ gateB: structuredClone(receipt.approvals.gateB),
935
+ },
687
936
  };
688
937
  },
689
938
  };
@@ -1175,6 +1424,11 @@ export function openPackageWorkflowControlEngine(
1175
1424
  xdgStateHome: process.env.XDG_STATE_HOME,
1176
1425
  });
1177
1426
  const contexts = new AsyncLocalStorage<ExtensionContext>();
1427
+ const design = DesignController.open({
1428
+ consumerRoot,
1429
+ stateRoot,
1430
+ inspectOpenSpec: inspectOpenSpecDelivery,
1431
+ });
1178
1432
  const implementationAgent = loadAgentDefinitions().find(
1179
1433
  (agent) => agent.role === "implementation-worker",
1180
1434
  );
@@ -1184,7 +1438,10 @@ export function openPackageWorkflowControlEngine(
1184
1438
  const engine = openDurableWorkflowEngine({
1185
1439
  consumerRoot,
1186
1440
  stateRoot,
1187
- deliverySource: packageDeliverySource(consumerRoot),
1441
+ deliverySource: packageDeliverySource(consumerRoot, {
1442
+ verifyGateProof: (input) => design.verifyGateProof(input),
1443
+ verifyFinalizedDelivery: (input) => design.verifyFinalizedDelivery(input),
1444
+ }),
1188
1445
  routePolicy: routeResolution.ok
1189
1446
  ? routeResolution.policy
1190
1447
  : unavailableRoutePolicy(),
@@ -1379,12 +1636,27 @@ export function openPackageWorkflowControlEngine(
1379
1636
  operationRouteResolution,
1380
1637
  engine.routePolicyStatus(),
1381
1638
  );
1382
- return { ...outcome, routePolicy };
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 };
1383
1645
  });
1384
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
+ },
1385
1656
  async close() {
1386
1657
  contexts.disable();
1387
1658
  await engine.close();
1659
+ design.close();
1388
1660
  },
1389
1661
  };
1390
1662
  }
@@ -1410,6 +1682,7 @@ export function registerWorkflowControl(
1410
1682
  const engines = new Map<string, Promise<WorkflowControlEngine>>();
1411
1683
  let pendingPrompt: EligiblePrompt | undefined;
1412
1684
  let activePrompt: EligiblePrompt | undefined;
1685
+ let designToolSnapshot: string[] | undefined;
1413
1686
  const activation = new (class implements Activation {
1414
1687
  state: Activation["state"] = "inactive";
1415
1688
 
@@ -1460,222 +1733,327 @@ export function registerWorkflowControl(
1460
1733
  ),
1461
1734
  );
1462
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
+ };
1463
1755
  const deactivate = () => {
1464
1756
  activation.drain();
1757
+ if (designToolSnapshot) {
1758
+ restoreDesignTools();
1759
+ return;
1760
+ }
1465
1761
  const active = pi.getActiveTools();
1466
1762
  if (active.includes(DISPATCH_TOOL)) {
1467
1763
  pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
1468
1764
  }
1469
1765
  };
1766
+ const deactivateStage = async () => {
1767
+ activePrompt = undefined;
1768
+ await packetRuntime.drain();
1769
+ deactivate();
1770
+ parentPayloadBridge.clear();
1771
+ };
1470
1772
 
1471
- pi.registerTool({
1472
- name: DISPATCH_TOOL,
1473
- label: "Abel Control",
1474
- description:
1475
- "Private stage-bound Abel workflow control. Implement accepts durable change commands; Design and Diagnose accept bounded packet commands.",
1476
- executionMode: "parallel",
1477
- parameters: {
1478
- type: "object",
1479
- properties: {
1480
- version: { type: "integer", enum: [2] },
1481
- command: {
1482
- type: "string",
1483
- enum: ["start", "status", "resume", "rebind", "cancel", "discard"],
1484
- },
1485
- stage: {
1486
- type: "string",
1487
- enum: ["abel-design", "abel-implement"],
1488
- },
1489
- change: { type: "string" },
1490
- provisionalKey: { type: "string" },
1491
- operationId: { type: "string" },
1492
- deliveryRevision: { type: "integer", minimum: 1 },
1493
- receiptHash: { type: "string" },
1494
- routeId: { type: "string" },
1495
- action: { type: "string", enum: [...PACKET_ACTIONS] },
1496
- request: {
1497
- anyOf: [DESIGN_REQUEST_SCHEMA, GENERIC_REQUEST_SCHEMA],
1498
- },
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"],
1499
1781
  },
1500
- additionalProperties: false,
1501
- oneOf: [
1502
- {
1503
- required: ["version", "command", "stage"],
1504
- not: {
1505
- anyOf: [{ required: ["action"] }, { required: ["request"] }],
1506
- },
1507
- },
1508
- {
1509
- required: ["action"],
1510
- not: {
1511
- anyOf: [
1512
- { required: ["version"] },
1513
- { required: ["command"] },
1514
- { required: ["stage"] },
1515
- { required: ["change"] },
1516
- { required: ["provisionalKey"] },
1517
- { required: ["operationId"] },
1518
- { required: ["deliveryRevision"] },
1519
- { required: ["receiptHash"] },
1520
- { required: ["routeId"] },
1521
- ],
1522
- },
1523
- },
1524
- ],
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" },
1525
1792
  },
1526
- async execute(
1527
- toolCallId: string,
1528
- params: unknown,
1529
- signal: AbortSignal | undefined,
1530
- onUpdate: AgentToolUpdateCallback<unknown> | undefined,
1531
- ctx: ExtensionContext,
1532
- ) {
1533
- const record =
1534
- params && typeof params === "object" && !Array.isArray(params)
1535
- ? (params as Record<string, unknown>)
1536
- : undefined;
1537
- if (record && typeof record.action === "string") {
1538
- if (
1539
- Object.keys(record).some(
1540
- (key) => key !== "action" && key !== "request",
1541
- )
1542
- ) {
1543
- throw new Error("control-envelope-ambiguous");
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;
1816
+ pi.registerTool({
1817
+ name: DISPATCH_TOOL,
1818
+ label: "Abel Control",
1819
+ description:
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.",
1823
+ executionMode: "parallel",
1824
+ parameters:
1825
+ kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
1826
+ async execute(
1827
+ toolCallId: string,
1828
+ params: unknown,
1829
+ signal: AbortSignal | undefined,
1830
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
1831
+ ctx: ExtensionContext,
1832
+ ) {
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",
1841
+ )
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");
1928
+ }
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;
1544
1961
  }
1545
1962
  if (
1546
- activePrompt !== "abel-design" &&
1547
- activePrompt !== "abel-diagnose"
1963
+ activePrompt !== undefined &&
1964
+ (activePrompt === "abel-diagnose" ||
1965
+ validation.value.stage !== activePrompt)
1548
1966
  ) {
1549
1967
  throw new Error("stage-control-mismatch");
1550
1968
  }
1551
- if (record.action === "run") {
1552
- const packet = packetRuntime.validateRequest(record.request);
1553
- if (!packet.ok || packet.value.stage !== activePrompt) {
1554
- throw new Error(
1555
- packet.ok ? "stage-control-mismatch" : packet.reason,
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,
1556
1977
  );
1978
+ } catch {
1979
+ // Presentation state must never alter workflow execution.
1557
1980
  }
1558
1981
  }
1559
- const operation = {
1560
- request: record.request,
1561
- };
1562
- const tuiRun = ctx.mode === "tui" && record.action === "run";
1563
- const payload = tuiRun
1564
- ? await packetRuntime.execute(
1565
- record.action,
1566
- operation,
1567
- ctx,
1568
- signal,
1569
- activity.observe(
1570
- toolCallId,
1571
- onUpdate as ((result: unknown) => void) | undefined,
1572
- ),
1573
- )
1574
- : await packetRuntime.execute(record.action, operation, ctx, signal);
1575
- const display = tuiRun
1576
- ? activity.finalize(toolCallId, payload)
1577
- : undefined;
1578
- const { payload: publicPayload, usage } = splitUsage(payload);
1579
- const details = display
1580
- ? {
1581
- ...(publicPayload as Record<string, unknown>),
1582
- [ACTIVITY_DETAILS_KEY]: display,
1583
- }
1584
- : publicPayload;
1585
- return {
1586
- content: [
1587
- { type: "text" as const, text: JSON.stringify(publicPayload) },
1588
- ],
1589
- details,
1590
- ...(usage === undefined ? {} : { usage }),
1591
- };
1592
- }
1593
- const validation = validateControlCommand(params);
1594
- if (!validation.ok) {
1595
- const error = new Error(validation.code);
1596
- error.name = "ControlCommandError";
1597
- throw error;
1598
- }
1599
- if (
1600
- activePrompt !== undefined &&
1601
- (activePrompt === "abel-diagnose" ||
1602
- validation.value.stage !== activePrompt)
1603
- ) {
1604
- throw new Error("stage-control-mismatch");
1605
- }
1606
- const engine = await engineFor(ctx);
1607
- const tuiControl = ctx.mode === "tui";
1608
- if (tuiControl) {
1982
+ let payload: Record<string, unknown>;
1609
1983
  try {
1610
- activity.beginWorkflow(
1611
- toolCallId,
1984
+ payload = await engine.execute(
1612
1985
  validation.value,
1613
- onUpdate as ((result: unknown) => void) | undefined,
1614
- );
1615
- } catch {
1616
- // Presentation state must never alter workflow execution.
1617
- }
1618
- }
1619
- let payload: Record<string, unknown>;
1620
- try {
1621
- payload = await engine.execute(
1622
- validation.value,
1623
- ctx,
1624
- signal,
1625
- tuiControl
1626
- ? (event) => {
1627
- try {
1628
- activity.updateWorkflow(toolCallId, validation.value, event);
1629
- } catch {
1630
- // Presentation state must never alter workflow execution.
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
+ }
1631
1999
  }
1632
- }
1633
- : undefined,
1634
- );
1635
- } catch (error) {
1636
- if (tuiControl) activity.failWorkflow(toolCallId);
1637
- throw error;
1638
- }
1639
- let display: ReturnType<typeof activity.finalizeWorkflow>;
1640
- if (tuiControl) {
1641
- try {
1642
- display = activity.finalizeWorkflow(
1643
- toolCallId,
1644
- validation.value,
1645
- payload,
2000
+ : undefined,
1646
2001
  );
1647
- } catch {
1648
- activity.failWorkflow(toolCallId);
2002
+ } catch (error) {
2003
+ if (tuiControl) activity.failWorkflow(toolCallId);
2004
+ throw error;
1649
2005
  }
1650
- }
1651
- return {
1652
- content: [{ type: "text" as const, text: JSON.stringify(payload) }],
1653
- details: display
1654
- ? { ...payload, [ACTIVITY_DETAILS_KEY]: display }
1655
- : payload,
1656
- };
1657
- },
1658
- renderCall(args: unknown, theme: Theme, context: unknown) {
1659
- return renderActivityCall(
1660
- args,
1661
- theme,
1662
- context as Parameters<typeof renderActivityCall>[2],
1663
- );
1664
- },
1665
- renderResult(
1666
- result: AgentToolResult<unknown>,
1667
- options: ToolRenderResultOptions,
1668
- theme: Theme,
1669
- context: unknown,
1670
- ) {
1671
- return renderActivityResult(
1672
- result,
1673
- options,
1674
- theme,
1675
- context as Parameters<typeof renderActivityResult>[3],
1676
- );
1677
- },
1678
- } as never);
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
+ }
2027
+ return {
2028
+ content: [{ type: "text" as const, text: JSON.stringify(payload) }],
2029
+ details: display
2030
+ ? { ...payload, [ACTIVITY_DETAILS_KEY]: display }
2031
+ : payload,
2032
+ };
2033
+ },
2034
+ renderCall(args: unknown, theme: Theme, context: unknown) {
2035
+ return renderActivityCall(
2036
+ args,
2037
+ theme,
2038
+ context as Parameters<typeof renderActivityCall>[2],
2039
+ );
2040
+ },
2041
+ renderResult(
2042
+ result: AgentToolResult<unknown>,
2043
+ options: ToolRenderResultOptions,
2044
+ theme: Theme,
2045
+ context: unknown,
2046
+ ) {
2047
+ return renderActivityResult(
2048
+ result,
2049
+ options,
2050
+ theme,
2051
+ context as Parameters<typeof renderActivityResult>[3],
2052
+ );
2053
+ },
2054
+ } as never);
2055
+ };
2056
+ registerDispatchTool("packet");
1679
2057
 
1680
2058
  pi.on("input", (event) => {
1681
2059
  pendingPrompt = invokedPrompt(event.text);
@@ -1687,6 +2065,10 @@ export function registerWorkflowControl(
1687
2065
  const verified =
1688
2066
  prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
1689
2067
  if (verified) {
2068
+ if (activePrompt === "abel-design" && prompt !== "abel-design") {
2069
+ restoreDesignTools();
2070
+ }
2071
+ registerDispatchTool(prompt === "abel-diagnose" ? "packet" : "command");
1690
2072
  activePrompt = prompt;
1691
2073
  const sessionId = ctx.sessionManager?.getSessionId?.();
1692
2074
  if (typeof sessionId === "string") {
@@ -1699,6 +2081,7 @@ export function registerWorkflowControl(
1699
2081
  parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
1700
2082
  }
1701
2083
  if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
2084
+ if (verified && prompt === "abel-design") enforceDesignTools();
1702
2085
  });
1703
2086
  pi.on("model_select", (event, ctx) => {
1704
2087
  const sessionId = ctx.sessionManager?.getSessionId?.();