@abelxiaoxing/cadence 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,145 @@ 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
+ ...(["requirement", "change"] as const).map((field) => ({
214
+ type: "object" as const,
215
+ properties: {
216
+ operation: { type: "string" as const, enum: ["start"] },
217
+ operationId: { type: "string" as const },
218
+ [field]: {
219
+ type: "string" as const,
220
+ description:
221
+ field === "requirement"
222
+ ? "Raw Design requirement; code derives and stores only its stable hash."
223
+ : "Existing canonical change name to resume or revise.",
224
+ },
225
+ },
226
+ required: ["operation", "operationId", field],
227
+ additionalProperties: false,
228
+ })),
229
+ {
230
+ type: "object",
231
+ properties: {
232
+ operation: { type: "string", enum: ["status"] },
233
+ runId: { type: "string" },
234
+ },
235
+ required: ["operation", "runId"],
236
+ additionalProperties: false,
237
+ },
238
+ {
239
+ type: "object",
240
+ properties: {
241
+ operation: { type: "string", enum: ["bind-change"] },
242
+ runId: { type: "string" },
243
+ operationId: { type: "string" },
244
+ change: { type: "string" },
245
+ },
246
+ required: ["operation", "runId", "operationId", "change"],
247
+ additionalProperties: false,
248
+ },
249
+ {
250
+ type: "object",
251
+ properties: {
252
+ operation: { type: "string", enum: ["record-decision"] },
253
+ runId: { type: "string" },
254
+ operationId: { type: "string" },
255
+ decisionId: { type: "string" },
256
+ category: { type: "string", enum: ["behavior", "technical"] },
257
+ contract: {
258
+ type: "string",
259
+ description:
260
+ "Normalized decision contract; code hashes it and never persists the text.",
261
+ },
262
+ refs: { type: "array", items: { type: "string" } },
263
+ },
264
+ required: [
265
+ "operation",
266
+ "runId",
267
+ "operationId",
268
+ "decisionId",
269
+ "category",
270
+ "contract",
271
+ "refs",
272
+ ],
273
+ additionalProperties: false,
274
+ },
275
+ {
276
+ type: "object",
277
+ properties: {
278
+ operation: { type: "string", enum: ["approve-gate"] },
279
+ runId: { type: "string" },
280
+ operationId: { type: "string" },
281
+ gate: { type: "string", enum: ["gate-a"] },
282
+ contract: {
283
+ type: "string",
284
+ description:
285
+ "Complete approved WHAT contract; code hashes it and never persists the text.",
286
+ },
287
+ },
288
+ required: ["operation", "runId", "operationId", "gate", "contract"],
289
+ additionalProperties: false,
290
+ },
291
+ {
292
+ type: "object",
293
+ properties: {
294
+ operation: { type: "string", enum: ["approve-gate"] },
295
+ runId: { type: "string" },
296
+ operationId: { type: "string" },
297
+ gate: { type: "string", enum: ["gate-b"] },
298
+ },
299
+ required: ["operation", "runId", "operationId", "gate"],
300
+ additionalProperties: false,
301
+ },
302
+ {
303
+ type: "object",
304
+ properties: {
305
+ operation: { type: "string", enum: ["write-artifact"] },
306
+ runId: { type: "string" },
307
+ operationId: { type: "string" },
308
+ path: {
309
+ type: "string",
310
+ description: "Allowed path relative to this Design change root.",
311
+ },
312
+ content: {
313
+ type: "string",
314
+ maxLength: 16 * 1024 * 1024,
315
+ description: "Exact bounded UTF-8 artifact content.",
316
+ },
317
+ },
318
+ required: ["operation", "runId", "operationId", "path", "content"],
319
+ additionalProperties: false,
320
+ },
321
+ {
322
+ type: "object",
323
+ properties: {
324
+ operation: { type: "string", enum: ["delete-artifact"] },
325
+ runId: { type: "string" },
326
+ operationId: { type: "string" },
327
+ path: {
328
+ type: "string",
329
+ description: "Allowed path relative to this Design change root.",
330
+ },
331
+ },
332
+ required: ["operation", "runId", "operationId", "path"],
333
+ additionalProperties: false,
334
+ },
335
+ ...(["compile-plan", "finalize-delivery"] as const).map((operation) => ({
336
+ type: "object" as const,
337
+ properties: {
338
+ operation: { type: "string" as const, enum: [operation] },
339
+ runId: { type: "string" as const },
340
+ operationId: { type: "string" as const },
341
+ },
342
+ required: ["operation", "runId", "operationId"],
343
+ additionalProperties: false,
344
+ })),
345
+ ],
346
+ } as const;
347
+
196
348
  function invokedPrompt(text: string): EligiblePrompt | undefined {
197
349
  const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
198
350
  return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
@@ -273,6 +425,12 @@ export interface WorkflowControlEngine {
273
425
  signal?: AbortSignal,
274
426
  onActivity?: (event: WorkflowActivityUpdate) => void,
275
427
  ): Promise<Record<string, unknown>>;
428
+ executeDesign?(request: unknown): Promise<Record<string, unknown>>;
429
+ assertDesignRun?(runId: string): void;
430
+ recordDesignEvidence?(input: {
431
+ runId: string;
432
+ evidence: DesignEvidenceResult;
433
+ }): unknown;
276
434
  close(): void | Promise<void>;
277
435
  }
278
436
 
@@ -297,12 +455,40 @@ export interface PackageDeliverySourceOptions {
297
455
  consumerRoot: string,
298
456
  change: string,
299
457
  ) => Promise<OpenSpecDeliveryInspection>;
458
+ verifyGateProof?: (input: {
459
+ change: string;
460
+ gate: "gate-a" | "gate-b";
461
+ proof: GateApprovalProof;
462
+ }) => boolean;
463
+ verifyFinalizedDelivery?: (input: {
464
+ change: string;
465
+ deliveryRevision: number;
466
+ receiptHash: string;
467
+ gateA: GateApprovalProof;
468
+ gateB: GateApprovalProof;
469
+ planCanonicalHash: string;
470
+ }) => boolean;
471
+ }
472
+
473
+ export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
474
+ discoverLatest(input: {
475
+ stage: "abel-implement";
476
+ change: string;
477
+ }): Promise<WorkflowAvailableDelivery | undefined>;
300
478
  }
301
479
 
302
480
  function sha256(value: Uint8Array | string): string {
303
481
  return createHash("sha256").update(value).digest("hex");
304
482
  }
305
483
 
484
+ function proofVerified(verify: () => boolean): boolean {
485
+ try {
486
+ return verify() === true;
487
+ } catch {
488
+ return false;
489
+ }
490
+ }
491
+
306
492
  function normalizedTrackingArtifact(
307
493
  bytes: Uint8Array,
308
494
  taskIds: readonly string[],
@@ -449,9 +635,80 @@ export async function inspectOpenSpecDelivery(
449
635
  export function packageDeliverySource(
450
636
  consumerRoot: string,
451
637
  options: PackageDeliverySourceOptions = {},
452
- ): WorkflowDeliverySource {
638
+ ): PackageWorkflowDeliverySource {
453
639
  const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
640
+ const verifyGateProof = options.verifyGateProof;
641
+ const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
454
642
  return {
643
+ async discoverLatest(input) {
644
+ if (
645
+ input.stage !== "abel-implement" ||
646
+ !verifyGateProof ||
647
+ !verifyFinalizedDelivery
648
+ ) {
649
+ return undefined;
650
+ }
651
+ try {
652
+ const changeRoot = `openspec/changes/${input.change}`;
653
+ const receiptBytes = readPackageDeliveryFile(
654
+ consumerRoot,
655
+ `${changeRoot}/ready.yaml`,
656
+ 4 * 1024 * 1024,
657
+ );
658
+ const receipt = parseReadyReceipt(receiptBytes);
659
+ if (receipt.change !== input.change) return undefined;
660
+ const gateABytes = readPackageDeliveryFile(
661
+ consumerRoot,
662
+ `${changeRoot}/${receipt.approvals.gateA.path}`,
663
+ 4 * 1024 * 1024,
664
+ );
665
+ if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
666
+ return undefined;
667
+ }
668
+ const gateA = parseGateAReceipt(gateABytes);
669
+ if (
670
+ gateA.change !== receipt.change ||
671
+ gateA.schema !== receipt.schema ||
672
+ !proofVerified(() =>
673
+ verifyGateProof({
674
+ change: input.change,
675
+ gate: "gate-a",
676
+ proof: gateA.approval,
677
+ }),
678
+ ) ||
679
+ !proofVerified(() =>
680
+ verifyGateProof({
681
+ change: input.change,
682
+ gate: "gate-b",
683
+ proof: receipt.approvals.gateB,
684
+ }),
685
+ )
686
+ ) {
687
+ return undefined;
688
+ }
689
+ const receiptHash = sha256(receiptBytes);
690
+ if (
691
+ !proofVerified(() =>
692
+ verifyFinalizedDelivery({
693
+ change: input.change,
694
+ deliveryRevision: receipt.deliveryRevision,
695
+ receiptHash,
696
+ gateA: gateA.approval,
697
+ gateB: receipt.approvals.gateB,
698
+ planCanonicalHash: receipt.plan.canonicalHash,
699
+ }),
700
+ )
701
+ ) {
702
+ return undefined;
703
+ }
704
+ return {
705
+ deliveryRevision: receipt.deliveryRevision,
706
+ receiptHash,
707
+ };
708
+ } catch {
709
+ return undefined;
710
+ }
711
+ },
455
712
  async load(input) {
456
713
  if (input.stage !== "abel-implement") {
457
714
  throw new DeliveryValidationError(["delivery-stage-invalid"]);
@@ -567,6 +824,19 @@ export function packageDeliverySource(
567
824
  }
568
825
  }
569
826
  if (gateA) {
827
+ if (!verifyGateProof) {
828
+ diagnostics.add("delivery-gate-proof-verifier-unavailable");
829
+ } else if (
830
+ !proofVerified(() =>
831
+ verifyGateProof({
832
+ change: input.change,
833
+ gate: "gate-a",
834
+ proof: gateA.approval,
835
+ }),
836
+ )
837
+ ) {
838
+ diagnostics.add("delivery-gate-a-proof-invalid");
839
+ }
570
840
  for (const artifact of gateA.artifacts) {
571
841
  if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
572
842
  diagnostics.add(
@@ -575,6 +845,36 @@ export function packageDeliverySource(
575
845
  }
576
846
  }
577
847
  }
848
+ if (
849
+ verifyGateProof &&
850
+ !proofVerified(() =>
851
+ verifyGateProof({
852
+ change: input.change,
853
+ gate: "gate-b",
854
+ proof: receipt.approvals.gateB,
855
+ }),
856
+ )
857
+ ) {
858
+ diagnostics.add("delivery-gate-b-proof-invalid");
859
+ }
860
+ if (gateA) {
861
+ if (!verifyFinalizedDelivery) {
862
+ diagnostics.add("delivery-finalization-verifier-unavailable");
863
+ } else if (
864
+ !proofVerified(() =>
865
+ verifyFinalizedDelivery({
866
+ change: input.change,
867
+ deliveryRevision: receipt.deliveryRevision,
868
+ receiptHash,
869
+ gateA: gateA.approval,
870
+ gateB: receipt.approvals.gateB,
871
+ planCanonicalHash: receipt.plan.canonicalHash,
872
+ }),
873
+ )
874
+ ) {
875
+ diagnostics.add("delivery-finalization-proof-invalid");
876
+ }
877
+ }
578
878
 
579
879
  let plan: ReturnType<typeof parseImplementPlan> | undefined;
580
880
  let planBytes: Buffer | undefined;
@@ -675,7 +975,7 @@ export function packageDeliverySource(
675
975
  }
676
976
  }
677
977
  }
678
- if (!plan || diagnostics.size > 0) {
978
+ if (!plan || !gateA || diagnostics.size > 0) {
679
979
  throw new DeliveryValidationError([...diagnostics]);
680
980
  }
681
981
  return {
@@ -684,6 +984,10 @@ export function packageDeliverySource(
684
984
  revision: receipt.deliveryRevision,
685
985
  receiptHash,
686
986
  plan,
987
+ approvalProofs: {
988
+ gateA: structuredClone(gateA.approval),
989
+ gateB: structuredClone(receipt.approvals.gateB),
990
+ },
687
991
  };
688
992
  },
689
993
  };
@@ -1169,12 +1473,18 @@ export function openPackageWorkflowControlEngine(
1169
1473
  const routeResolution = loadRoutePolicy({
1170
1474
  cwd: consumerRoot,
1171
1475
  home: homedir(),
1476
+ ...(initialContext.model ? { parentModel: initialContext.model } : {}),
1172
1477
  });
1173
1478
  const stateRoot = resolveStateRoot({
1174
1479
  consumerRoot,
1175
1480
  xdgStateHome: process.env.XDG_STATE_HOME,
1176
1481
  });
1177
1482
  const contexts = new AsyncLocalStorage<ExtensionContext>();
1483
+ const design = DesignController.open({
1484
+ consumerRoot,
1485
+ stateRoot,
1486
+ inspectOpenSpec: inspectOpenSpecDelivery,
1487
+ });
1178
1488
  const implementationAgent = loadAgentDefinitions().find(
1179
1489
  (agent) => agent.role === "implementation-worker",
1180
1490
  );
@@ -1184,7 +1494,10 @@ export function openPackageWorkflowControlEngine(
1184
1494
  const engine = openDurableWorkflowEngine({
1185
1495
  consumerRoot,
1186
1496
  stateRoot,
1187
- deliverySource: packageDeliverySource(consumerRoot),
1497
+ deliverySource: packageDeliverySource(consumerRoot, {
1498
+ verifyGateProof: (input) => design.verifyGateProof(input),
1499
+ verifyFinalizedDelivery: (input) => design.verifyFinalizedDelivery(input),
1500
+ }),
1188
1501
  routePolicy: routeResolution.ok
1189
1502
  ? routeResolution.policy
1190
1503
  : unavailableRoutePolicy(),
@@ -1357,6 +1670,7 @@ export function openPackageWorkflowControlEngine(
1357
1670
  const operationRouteResolution = loadRoutePolicy({
1358
1671
  cwd: consumerRoot,
1359
1672
  home: homedir(),
1673
+ ...(context.model ? { parentModel: context.model } : {}),
1360
1674
  });
1361
1675
  engine.updateRoutePolicy(
1362
1676
  operationRouteResolution.ok
@@ -1382,9 +1696,19 @@ export function openPackageWorkflowControlEngine(
1382
1696
  return { ...outcome, routePolicy };
1383
1697
  });
1384
1698
  },
1699
+ executeDesign(request: unknown) {
1700
+ return design.execute(request);
1701
+ },
1702
+ assertDesignRun(runId: string) {
1703
+ design.assertDesignRun(runId);
1704
+ },
1705
+ recordDesignEvidence(input) {
1706
+ return design.recordEvidence(input);
1707
+ },
1385
1708
  async close() {
1386
1709
  contexts.disable();
1387
1710
  await engine.close();
1711
+ design.close();
1388
1712
  },
1389
1713
  };
1390
1714
  }
@@ -1410,6 +1734,7 @@ export function registerWorkflowControl(
1410
1734
  const engines = new Map<string, Promise<WorkflowControlEngine>>();
1411
1735
  let pendingPrompt: EligiblePrompt | undefined;
1412
1736
  let activePrompt: EligiblePrompt | undefined;
1737
+ let designToolSnapshot: string[] | undefined;
1413
1738
  const activation = new (class implements Activation {
1414
1739
  state: Activation["state"] = "inactive";
1415
1740
 
@@ -1460,222 +1785,322 @@ export function registerWorkflowControl(
1460
1785
  ),
1461
1786
  );
1462
1787
  };
1788
+ const restoreDesignTools = () => {
1789
+ if (!designToolSnapshot) return;
1790
+ const snapshot = designToolSnapshot;
1791
+ designToolSnapshot = undefined;
1792
+ pi.setActiveTools([...snapshot]);
1793
+ };
1794
+ const enforceDesignTools = () => {
1795
+ if (!designToolSnapshot) {
1796
+ designToolSnapshot = pi
1797
+ .getActiveTools()
1798
+ .filter((name) => name !== DISPATCH_TOOL);
1799
+ }
1800
+ pi.setActiveTools(
1801
+ activateTool(
1802
+ designToolSnapshot.filter((name) => DESIGN_PARENT_READ_TOOLS.has(name)),
1803
+ DISPATCH_TOOL,
1804
+ ),
1805
+ );
1806
+ };
1463
1807
  const deactivate = () => {
1464
1808
  activation.drain();
1809
+ if (designToolSnapshot) {
1810
+ restoreDesignTools();
1811
+ return;
1812
+ }
1465
1813
  const active = pi.getActiveTools();
1466
1814
  if (active.includes(DISPATCH_TOOL)) {
1467
1815
  pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
1468
1816
  }
1469
1817
  };
1818
+ const deactivateStage = async () => {
1819
+ activePrompt = undefined;
1820
+ await packetRuntime.drain();
1821
+ deactivate();
1822
+ parentPayloadBridge.clear();
1823
+ };
1470
1824
 
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
- },
1825
+ // Pi validates tool arguments before execute(), so expose only the schema
1826
+ // legal for the active stage instead of one ambiguous command/packet union.
1827
+ const CONTROL_COMMAND_PARAMETERS = {
1828
+ type: "object",
1829
+ properties: {
1830
+ command: {
1831
+ type: "string",
1832
+ enum: ["start", "status", "resume", "rebind", "cancel", "discard"],
1499
1833
  },
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
- ],
1834
+ stage: {
1835
+ type: "string",
1836
+ enum: ["abel-implement"],
1837
+ },
1838
+ change: { type: "string" },
1839
+ operationId: { type: "string" },
1840
+ deliveryRevision: { type: "integer", minimum: 1 },
1841
+ receiptHash: { type: "string" },
1842
+ routeId: { type: "string" },
1525
1843
  },
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");
1844
+ required: ["command", "stage"],
1845
+ additionalProperties: false,
1846
+ } as const;
1847
+ const PACKET_PARAMETERS = {
1848
+ type: "object",
1849
+ properties: {
1850
+ action: { type: "string", enum: [...PACKET_ACTIONS, "design"] },
1851
+ request: {
1852
+ anyOf: [
1853
+ DESIGN_REQUEST_SCHEMA,
1854
+ DESIGN_CONTROL_REQUEST_SCHEMA,
1855
+ GENERIC_REQUEST_SCHEMA,
1856
+ ],
1857
+ },
1858
+ },
1859
+ required: ["action"],
1860
+ additionalProperties: false,
1861
+ } as const;
1862
+ let registeredParameterKind: "command" | "packet" | undefined;
1863
+
1864
+ const registerDispatchTool = (kind: "command" | "packet") => {
1865
+ if (registeredParameterKind === kind) return;
1866
+ registeredParameterKind = kind;
1867
+ pi.registerTool({
1868
+ name: DISPATCH_TOOL,
1869
+ label: "Abel Control",
1870
+ description:
1871
+ kind === "command"
1872
+ ? "Private stage-bound Abel workflow control. Accepts durable Implement change commands."
1873
+ : "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
1874
+ executionMode: "parallel",
1875
+ parameters:
1876
+ kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
1877
+ async execute(
1878
+ toolCallId: string,
1879
+ params: unknown,
1880
+ signal: AbortSignal | undefined,
1881
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
1882
+ ctx: ExtensionContext,
1883
+ ) {
1884
+ const record =
1885
+ params && typeof params === "object" && !Array.isArray(params)
1886
+ ? (params as Record<string, unknown>)
1887
+ : undefined;
1888
+ if (record && typeof record.action === "string") {
1889
+ if (
1890
+ Object.keys(record).some(
1891
+ (key) => key !== "action" && key !== "request",
1892
+ )
1893
+ ) {
1894
+ throw new Error("control-envelope-ambiguous");
1895
+ }
1896
+ if (
1897
+ activePrompt !== "abel-design" &&
1898
+ activePrompt !== "abel-diagnose"
1899
+ ) {
1900
+ throw new Error("stage-control-mismatch");
1901
+ }
1902
+ if (record.action === "design") {
1903
+ if (activePrompt !== "abel-design") {
1904
+ throw new Error("stage-control-mismatch");
1905
+ }
1906
+ const designRequest = validateDesignControlRequest(record.request);
1907
+ if (!designRequest.ok) throw new Error(designRequest.code);
1908
+ const engine = await engineFor(ctx);
1909
+ if (!engine.executeDesign)
1910
+ throw new Error("design-control-unavailable");
1911
+ const payload = await engine.executeDesign(designRequest.value);
1912
+ if (
1913
+ designRequest.value.operation === "finalize-delivery" &&
1914
+ payload.state === "completed"
1915
+ ) {
1916
+ await deactivateStage();
1917
+ }
1918
+ return {
1919
+ content: [
1920
+ { type: "text" as const, text: JSON.stringify(payload) },
1921
+ ],
1922
+ details: payload,
1923
+ };
1924
+ }
1925
+ let designEngine: WorkflowControlEngine | undefined;
1926
+ if (record.action === "run") {
1927
+ const packet = packetRuntime.validateRequest(record.request);
1928
+ if (!packet.ok || packet.value.stage !== activePrompt) {
1929
+ throw new Error(
1930
+ packet.ok ? "stage-control-mismatch" : packet.reason,
1931
+ );
1932
+ }
1933
+ if (packet.value.stage === "abel-design") {
1934
+ designEngine = await engineFor(ctx);
1935
+ if (!designEngine.assertDesignRun || !packet.value.runId) {
1936
+ throw new Error("design-control-unavailable");
1937
+ }
1938
+ designEngine.assertDesignRun(packet.value.runId);
1939
+ }
1940
+ }
1941
+ const operation = {
1942
+ request: record.request,
1943
+ };
1944
+ const tuiRun = ctx.mode === "tui" && record.action === "run";
1945
+ const packetPayload = tuiRun
1946
+ ? await packetRuntime.execute(
1947
+ record.action,
1948
+ operation,
1949
+ ctx,
1950
+ signal,
1951
+ activity.observe(
1952
+ toolCallId,
1953
+ onUpdate as ((result: unknown) => void) | undefined,
1954
+ ),
1955
+ )
1956
+ : await packetRuntime.execute(
1957
+ record.action,
1958
+ operation,
1959
+ ctx,
1960
+ signal,
1961
+ );
1962
+ let payload:
1963
+ | typeof packetPayload
1964
+ | (typeof packetPayload & {
1965
+ recordedEvidence: unknown;
1966
+ }) = packetPayload;
1967
+ if (
1968
+ packetPayload.ok &&
1969
+ record.action === "run" &&
1970
+ activePrompt === "abel-design"
1971
+ ) {
1972
+ const packet = packetRuntime.validateRequest(record.request);
1973
+ if (
1974
+ !packet.ok ||
1975
+ !packet.value.runId ||
1976
+ !designEngine?.recordDesignEvidence
1977
+ ) {
1978
+ throw new Error("design-control-unavailable");
1979
+ }
1980
+ const recordedEvidence = designEngine.recordDesignEvidence({
1981
+ runId: packet.value.runId,
1982
+ evidence: packetPayload.result as DesignEvidenceResult,
1983
+ });
1984
+ payload = { ...packetPayload, recordedEvidence };
1985
+ }
1986
+ if (record.action === "finish" && packetPayload.ok) {
1987
+ await deactivateStage();
1988
+ }
1989
+ const display = tuiRun
1990
+ ? activity.finalize(toolCallId, payload)
1991
+ : undefined;
1992
+ const { payload: publicPayload, usage } = splitUsage(payload);
1993
+ const details = display
1994
+ ? {
1995
+ ...(publicPayload as Record<string, unknown>),
1996
+ [ACTIVITY_DETAILS_KEY]: display,
1997
+ }
1998
+ : publicPayload;
1999
+ return {
2000
+ content: [
2001
+ { type: "text" as const, text: JSON.stringify(publicPayload) },
2002
+ ],
2003
+ details,
2004
+ ...(usage === undefined ? {} : { usage }),
2005
+ };
2006
+ }
2007
+ const validation = validateControlCommand(params);
2008
+ if (!validation.ok) {
2009
+ const error = new Error(validation.code);
2010
+ error.name = "ControlCommandError";
2011
+ throw error;
1544
2012
  }
1545
2013
  if (
1546
- activePrompt !== "abel-design" &&
1547
- activePrompt !== "abel-diagnose"
2014
+ activePrompt !== undefined &&
2015
+ (activePrompt === "abel-diagnose" ||
2016
+ validation.value.stage !== activePrompt)
1548
2017
  ) {
1549
2018
  throw new Error("stage-control-mismatch");
1550
2019
  }
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,
2020
+ const engine = await engineFor(ctx);
2021
+ const tuiControl = ctx.mode === "tui";
2022
+ if (tuiControl) {
2023
+ try {
2024
+ activity.beginWorkflow(
2025
+ toolCallId,
2026
+ validation.value,
2027
+ onUpdate as ((result: unknown) => void) | undefined,
1556
2028
  );
2029
+ } catch {
2030
+ // Presentation state must never alter workflow execution.
1557
2031
  }
1558
2032
  }
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) {
2033
+ let payload: Record<string, unknown>;
1609
2034
  try {
1610
- activity.beginWorkflow(
1611
- toolCallId,
2035
+ payload = await engine.execute(
1612
2036
  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.
2037
+ ctx,
2038
+ signal,
2039
+ tuiControl
2040
+ ? (event) => {
2041
+ try {
2042
+ activity.updateWorkflow(
2043
+ toolCallId,
2044
+ validation.value,
2045
+ event,
2046
+ );
2047
+ } catch {
2048
+ // Presentation state must never alter workflow execution.
2049
+ }
1631
2050
  }
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,
2051
+ : undefined,
1646
2052
  );
1647
- } catch {
1648
- activity.failWorkflow(toolCallId);
2053
+ } catch (error) {
2054
+ if (tuiControl) activity.failWorkflow(toolCallId);
2055
+ throw error;
1649
2056
  }
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);
2057
+ if (
2058
+ ["completed", "discarded", "rejected"].includes(String(payload.state))
2059
+ ) {
2060
+ await deactivateStage();
2061
+ }
2062
+ let display: ReturnType<typeof activity.finalizeWorkflow>;
2063
+ if (tuiControl) {
2064
+ try {
2065
+ display = activity.finalizeWorkflow(
2066
+ toolCallId,
2067
+ validation.value,
2068
+ payload,
2069
+ );
2070
+ } catch {
2071
+ activity.failWorkflow(toolCallId);
2072
+ }
2073
+ }
2074
+ return {
2075
+ content: [{ type: "text" as const, text: JSON.stringify(payload) }],
2076
+ details: display
2077
+ ? { ...payload, [ACTIVITY_DETAILS_KEY]: display }
2078
+ : payload,
2079
+ };
2080
+ },
2081
+ renderCall(args: unknown, theme: Theme, context: unknown) {
2082
+ return renderActivityCall(
2083
+ args,
2084
+ theme,
2085
+ context as Parameters<typeof renderActivityCall>[2],
2086
+ );
2087
+ },
2088
+ renderResult(
2089
+ result: AgentToolResult<unknown>,
2090
+ options: ToolRenderResultOptions,
2091
+ theme: Theme,
2092
+ context: unknown,
2093
+ ) {
2094
+ return renderActivityResult(
2095
+ result,
2096
+ options,
2097
+ theme,
2098
+ context as Parameters<typeof renderActivityResult>[3],
2099
+ );
2100
+ },
2101
+ } as never);
2102
+ };
2103
+ registerDispatchTool("packet");
1679
2104
 
1680
2105
  pi.on("input", (event) => {
1681
2106
  pendingPrompt = invokedPrompt(event.text);
@@ -1687,6 +2112,10 @@ export function registerWorkflowControl(
1687
2112
  const verified =
1688
2113
  prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
1689
2114
  if (verified) {
2115
+ if (activePrompt === "abel-design" && prompt !== "abel-design") {
2116
+ restoreDesignTools();
2117
+ }
2118
+ registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
1690
2119
  activePrompt = prompt;
1691
2120
  const sessionId = ctx.sessionManager?.getSessionId?.();
1692
2121
  if (typeof sessionId === "string") {
@@ -1699,6 +2128,7 @@ export function registerWorkflowControl(
1699
2128
  parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
1700
2129
  }
1701
2130
  if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
2131
+ if (verified && prompt === "abel-design") enforceDesignTools();
1702
2132
  });
1703
2133
  pi.on("model_select", (event, ctx) => {
1704
2134
  const sessionId = ctx.sessionManager?.getSessionId?.();