@abelxiaoxing/cadence 1.0.13 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +29 -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 +150 -228
  10. package/prompts/abel-diagnose.md +37 -18
  11. package/prompts/abel-implement.md +111 -178
  12. package/prompts/abel-init.md +36 -33
  13. package/skills/abel-workflow/SKILL.md +156 -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 +105 -696
  18. package/src/control-contracts.ts +259 -0
  19. package/src/delivery-compiler.ts +1088 -0
  20. package/src/file-snapshot.ts +5 -0
  21. package/src/implement-graph.ts +11 -4
  22. package/src/index.ts +1465 -133
  23. package/src/isolation-backend.ts +269 -0
  24. package/src/packet-runtime.ts +533 -0
  25. package/src/parent-provider.ts +23 -4
  26. package/src/route-policy.ts +483 -0
  27. package/src/run-state.ts +393 -0
  28. package/src/run-store.ts +918 -0
  29. package/src/safe-path.ts +11 -0
  30. package/src/scoped-tools.ts +36 -0
  31. package/src/state-root.ts +215 -0
  32. package/src/subagent-activity.ts +526 -27
  33. package/src/submit-tool.ts +274 -0
  34. package/src/task-ledger.ts +1380 -0
  35. package/src/verification-capability.ts +529 -42
  36. package/src/worker-broker.ts +703 -0
  37. package/src/workflow-engine.ts +7391 -0
  38. package/src/workspace-store.ts +544 -0
  39. package/src/candidate-preflight.ts +0 -1618
  40. package/src/drain.ts +0 -20
  41. package/src/patch.ts +0 -505
  42. package/src/result-store.ts +0 -312
  43. package/src/runtime.ts +0 -2436
  44. package/src/scheduler.ts +0 -384
  45. package/src/subagent-endpoint.ts +0 -158
  46. 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,15 +27,56 @@ 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
+ isValidRelativePath,
35
+ LIMITS,
36
+ type StructuredVerificationContract,
37
+ verificationSteps,
38
+ } from "./contracts.ts";
39
+ import { validateControlCommand } from "./control-contracts.ts";
40
+ import {
41
+ assessDeliveryTraceability,
42
+ compileImplementPlan,
43
+ DeliveryValidationError,
44
+ IMPLEMENT_PLAN_SCHEMA_VERSION,
45
+ parseGateAReceipt,
46
+ parseImplementPlan,
47
+ parseReadyReceipt,
48
+ } from "./delivery-compiler.ts";
49
+ import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
50
+ import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
51
+ import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
18
52
  import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
19
- import { Runtime } from "./runtime.ts";
53
+ import { runtimeForWorkerRoute } from "./parent-provider.ts";
54
+ import {
55
+ inspectRoutePolicy,
56
+ loadRoutePolicy,
57
+ type RoutePolicyResolution,
58
+ unavailableRoutePolicy,
59
+ type WorkerRoutePolicy,
60
+ } from "./route-policy.ts";
61
+ import { observeSafePath } from "./safe-path.ts";
62
+ import { resolveStateRoot } from "./state-root.ts";
20
63
  import {
21
64
  ACTIVITY_DETAILS_KEY,
22
65
  ActivityController,
23
66
  renderActivityCall,
24
67
  renderActivityResult,
68
+ type WorkflowActivityUpdate,
25
69
  } from "./subagent-activity.ts";
70
+ import { classifyCandidateContextRequest } from "./submit-tool.ts";
71
+ import {
72
+ bindCurrentVerificationCapability,
73
+ isVerificationCapabilityCurrent,
74
+ type VerificationRunnerBinding,
75
+ } from "./verification-capability.ts";
76
+ import {
77
+ openDurableWorkflowEngine,
78
+ type WorkflowDeliverySource,
79
+ } from "./workflow-engine.ts";
26
80
 
27
81
  export const DISPATCH_TOOL = "abel_dispatch";
28
82
 
@@ -139,12 +193,6 @@ const DESIGN_REQUEST_SCHEMA = {
139
193
  additionalProperties: false,
140
194
  } as const;
141
195
 
142
- function requestSchemaForStage(name?: EligiblePrompt): object {
143
- return name === "abel-design"
144
- ? DESIGN_REQUEST_SCHEMA
145
- : GENERIC_REQUEST_SCHEMA;
146
- }
147
-
148
196
  function invokedPrompt(text: string): EligiblePrompt | undefined {
149
197
  const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
150
198
  return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
@@ -218,68 +266,1303 @@ function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
218
266
  return { payload, usage };
219
267
  }
220
268
 
221
- export default function register(pi: ExtensionAPI): void {
269
+ export interface WorkflowControlEngine {
270
+ execute(
271
+ command: unknown,
272
+ context?: ExtensionContext,
273
+ signal?: AbortSignal,
274
+ onActivity?: (event: WorkflowActivityUpdate) => void,
275
+ ): Promise<Record<string, unknown>>;
276
+ close(): void | Promise<void>;
277
+ }
278
+
279
+ export type WorkflowControlEngineFactory = (
280
+ ctx: ExtensionContext,
281
+ parentPayloadBridge: ParentPayloadBridge,
282
+ ) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
283
+
284
+ const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
285
+ const execFileAsync = promisify(execFile);
286
+
287
+ export interface OpenSpecDeliveryInspection {
288
+ change: string;
289
+ schema: string;
290
+ planningComplete: boolean;
291
+ strictValid: boolean;
292
+ artifactPaths: string[];
293
+ }
294
+
295
+ export interface PackageDeliverySourceOptions {
296
+ inspectOpenSpec?: (
297
+ consumerRoot: string,
298
+ change: string,
299
+ ) => Promise<OpenSpecDeliveryInspection>;
300
+ }
301
+
302
+ function sha256(value: Uint8Array | string): string {
303
+ return createHash("sha256").update(value).digest("hex");
304
+ }
305
+
306
+ function normalizedTrackingArtifact(
307
+ bytes: Uint8Array,
308
+ taskIds: readonly string[],
309
+ ): Buffer | undefined {
310
+ let text: string;
311
+ try {
312
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
313
+ } catch {
314
+ return undefined;
315
+ }
316
+ const identities = taskIds.map((taskId) => ({
317
+ taskId,
318
+ pattern: new RegExp(
319
+ `(?:^|[^A-Za-z0-9._:-])${taskId.replace(
320
+ /[.*+?^${}()|[\]\\]/gu,
321
+ "\\$&",
322
+ )}(?![A-Za-z0-9._:-])`,
323
+ "u",
324
+ ),
325
+ matches: 0,
326
+ }));
327
+ const normalized = text
328
+ .split(/(?<=\n)/u)
329
+ .map((segment) => {
330
+ const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
331
+ if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
332
+ const matching = identities.filter(({ pattern }) => pattern.test(line));
333
+ if (matching.length !== 1) return segment;
334
+ matching[0].matches += 1;
335
+ return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
336
+ })
337
+ .join("");
338
+ if (identities.some(({ matches }) => matches !== 1)) return undefined;
339
+ return Buffer.from(normalized, "utf8");
340
+ }
341
+
342
+ function readPackageDeliveryFile(
343
+ root: string,
344
+ relative: string,
345
+ maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
346
+ ): Buffer {
347
+ const observation = observeSafePath(root, relative);
348
+ if (observation.kind !== "file") {
349
+ throw new Error("delivery-file-unavailable");
350
+ }
351
+ const target = path.join(root, ...relative.split("/"));
352
+ const stat = lstatSync(target);
353
+ if (stat.size < 1 || stat.size > maximumBytes) {
354
+ throw new Error("delivery-file-size-invalid");
355
+ }
356
+ return readFileSync(target);
357
+ }
358
+
359
+ function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
360
+ return value && typeof value === "object" && !Array.isArray(value)
361
+ ? (value as Record<string, unknown>)
362
+ : undefined;
363
+ }
364
+
365
+ function parseCommandJson(
366
+ value: string,
367
+ code: string,
368
+ ): Record<string, unknown> {
369
+ let parsed: unknown;
370
+ try {
371
+ parsed = JSON.parse(value);
372
+ } catch {
373
+ throw new Error(code);
374
+ }
375
+ const record = deliveryRecord(parsed);
376
+ if (!record) throw new Error(code);
377
+ return record;
378
+ }
379
+
380
+ export async function inspectOpenSpecDelivery(
381
+ consumerRoot: string,
382
+ change: string,
383
+ ): Promise<OpenSpecDeliveryInspection> {
384
+ const [statusExecution, validationExecution] = await Promise.all([
385
+ execFileAsync("openspec", ["status", "--change", change, "--json"], {
386
+ cwd: consumerRoot,
387
+ encoding: "utf8",
388
+ maxBuffer: 4 * 1024 * 1024,
389
+ }),
390
+ execFileAsync(
391
+ "openspec",
392
+ ["validate", change, "--strict", "--json", "--no-interactive"],
393
+ {
394
+ cwd: consumerRoot,
395
+ encoding: "utf8",
396
+ maxBuffer: 4 * 1024 * 1024,
397
+ },
398
+ ),
399
+ ]);
400
+ const status = parseCommandJson(
401
+ statusExecution.stdout,
402
+ "delivery-openspec-status-invalid",
403
+ );
404
+ const validation = parseCommandJson(
405
+ validationExecution.stdout,
406
+ "delivery-openspec-validation-invalid",
407
+ );
408
+ const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
409
+ const artifactPathsRecord = deliveryRecord(status.artifactPaths);
410
+ const artifactPaths = artifactPathsRecord
411
+ ? Object.values(artifactPathsRecord).flatMap((entry) => {
412
+ const record = deliveryRecord(entry);
413
+ if (!record || !Array.isArray(record.existingOutputPaths)) return [];
414
+ return record.existingOutputPaths.flatMap((candidate) => {
415
+ if (typeof candidate !== "string") return [];
416
+ const resolved = path.resolve(candidate);
417
+ const relative = path.relative(changeRoot, resolved);
418
+ return relative &&
419
+ !relative.startsWith(`..${path.sep}`) &&
420
+ relative !== ".." &&
421
+ !path.isAbsolute(relative)
422
+ ? [relative.split(path.sep).join("/")]
423
+ : [];
424
+ });
425
+ })
426
+ : [];
427
+ const items = Array.isArray(validation.items) ? validation.items : [];
428
+ const strictValid =
429
+ items.length === 1 &&
430
+ deliveryRecord(items[0])?.id === change &&
431
+ deliveryRecord(items[0])?.valid === true;
432
+ if (
433
+ status.changeName !== change ||
434
+ typeof status.schemaName !== "string" ||
435
+ artifactPaths.length === 0
436
+ ) {
437
+ throw new Error("delivery-openspec-status-invalid");
438
+ }
439
+ return {
440
+ change,
441
+ schema: status.schemaName,
442
+ planningComplete:
443
+ status.isPlanningComplete === true && status.isComplete === true,
444
+ strictValid,
445
+ artifactPaths: [...new Set(artifactPaths)].sort(),
446
+ };
447
+ }
448
+
449
+ export function packageDeliverySource(
450
+ consumerRoot: string,
451
+ options: PackageDeliverySourceOptions = {},
452
+ ): WorkflowDeliverySource {
453
+ const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
454
+ return {
455
+ async load(input) {
456
+ if (input.stage !== "abel-implement") {
457
+ throw new DeliveryValidationError(["delivery-stage-invalid"]);
458
+ }
459
+ const changeRoot = `openspec/changes/${input.change}`;
460
+ const receiptRelative = `${changeRoot}/ready.yaml`;
461
+ const planRelative = `${changeRoot}/implement-plan.json`;
462
+ let receiptBytes: Buffer;
463
+ try {
464
+ receiptBytes = readPackageDeliveryFile(
465
+ consumerRoot,
466
+ receiptRelative,
467
+ 4 * 1024 * 1024,
468
+ );
469
+ } catch {
470
+ throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
471
+ }
472
+ let receipt: ReturnType<typeof parseReadyReceipt>;
473
+ try {
474
+ receipt = parseReadyReceipt(receiptBytes);
475
+ } catch {
476
+ throw new DeliveryValidationError(["delivery-receipt-invalid"]);
477
+ }
478
+ const diagnostics = new Set<string>();
479
+ if (receipt.change !== input.change) {
480
+ diagnostics.add("delivery-change-mismatch");
481
+ }
482
+ const receiptHash = sha256(receiptBytes);
483
+ if (
484
+ (input.deliveryRevision !== undefined &&
485
+ input.deliveryRevision !== receipt.deliveryRevision) ||
486
+ (input.receiptHash !== undefined && input.receiptHash !== receiptHash)
487
+ ) {
488
+ diagnostics.add("delivery-revision-mismatch");
489
+ }
490
+
491
+ let inspection: OpenSpecDeliveryInspection | undefined;
492
+ try {
493
+ inspection = await inspect(consumerRoot, input.change);
494
+ } catch {
495
+ diagnostics.add("delivery-openspec-unavailable");
496
+ }
497
+ if (inspection) {
498
+ if (!inspection.strictValid) {
499
+ diagnostics.add("delivery-openspec-strict-invalid");
500
+ }
501
+ if (!inspection.planningComplete) {
502
+ diagnostics.add("delivery-openspec-incomplete");
503
+ }
504
+ if (inspection.schema !== receipt.schema) {
505
+ diagnostics.add("delivery-schema-mismatch");
506
+ }
507
+ }
508
+
509
+ let gateABytes: Buffer | undefined;
510
+ let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
511
+ try {
512
+ gateABytes = readPackageDeliveryFile(
513
+ consumerRoot,
514
+ `${changeRoot}/${receipt.approvals.gateA.path}`,
515
+ 4 * 1024 * 1024,
516
+ );
517
+ if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
518
+ diagnostics.add("delivery-gate-a-hash-mismatch");
519
+ }
520
+ gateA = parseGateAReceipt(gateABytes);
521
+ if (
522
+ gateA.change !== receipt.change ||
523
+ gateA.schema !== receipt.schema
524
+ ) {
525
+ diagnostics.add("delivery-gate-a-binding-mismatch");
526
+ }
527
+ } catch {
528
+ diagnostics.add("delivery-gate-a-invalid");
529
+ }
530
+
531
+ const artifactBytes = new Map<string, Buffer>();
532
+ const artifactHashes = new Map(
533
+ receipt.artifacts.map((artifact) => [
534
+ artifact.path,
535
+ artifact.rawSha256,
536
+ ]),
537
+ );
538
+ for (const artifact of receipt.artifacts) {
539
+ try {
540
+ const bytes = readPackageDeliveryFile(
541
+ consumerRoot,
542
+ `${changeRoot}/${artifact.path}`,
543
+ );
544
+ artifactBytes.set(artifact.path, bytes);
545
+ if (
546
+ artifact.path !== receipt.traceability.taskPath &&
547
+ sha256(bytes) !== artifact.rawSha256
548
+ ) {
549
+ diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
550
+ }
551
+ } catch {
552
+ diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
553
+ }
554
+ }
555
+ if (inspection) {
556
+ const covered = new Set(receipt.artifacts.map((entry) => entry.path));
557
+ const expected = new Set(inspection.artifactPaths);
558
+ for (const relative of expected) {
559
+ if (!isValidRelativePath(relative) || !covered.has(relative)) {
560
+ diagnostics.add(`delivery-artifact-unbound:${relative}`);
561
+ }
562
+ }
563
+ for (const relative of covered) {
564
+ if (!expected.has(relative)) {
565
+ diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
566
+ }
567
+ }
568
+ }
569
+ if (gateA) {
570
+ for (const artifact of gateA.artifacts) {
571
+ if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
572
+ diagnostics.add(
573
+ `delivery-gate-a-artifact-mismatch:${artifact.path}`,
574
+ );
575
+ }
576
+ }
577
+ }
578
+
579
+ let plan: ReturnType<typeof parseImplementPlan> | undefined;
580
+ let planBytes: Buffer | undefined;
581
+ try {
582
+ planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
583
+ plan = parseImplementPlan(planBytes);
584
+ } catch {
585
+ diagnostics.add("delivery-plan-invalid");
586
+ }
587
+ if (plan && planBytes) {
588
+ if (plan.changeId !== input.change) {
589
+ diagnostics.add("delivery-plan-change-mismatch");
590
+ }
591
+ let compiled: ReturnType<typeof compileImplementPlan> | undefined;
592
+ try {
593
+ compiled = compileImplementPlan(plan, { consumerRoot });
594
+ } catch {
595
+ diagnostics.add("delivery-verification-closure-invalid");
596
+ }
597
+ if (
598
+ receipt.plan.path !== "implement-plan.json" ||
599
+ receipt.plan.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
600
+ receipt.plan.rawSha256 !== sha256(planBytes) ||
601
+ receipt.plan.canonicalHash !== hashCanonicalValue(plan)
602
+ ) {
603
+ diagnostics.add("delivery-plan-binding-invalid");
604
+ }
605
+ if (
606
+ compiled &&
607
+ canonicalJson(compiled.closure) !==
608
+ canonicalJson(receipt.verificationClosure)
609
+ ) {
610
+ diagnostics.add("delivery-verification-closure-mismatch");
611
+ }
612
+ }
613
+
614
+ if (plan) {
615
+ const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
616
+ const expectedTasksHash = artifactHashes.get(
617
+ receipt.traceability.taskPath,
618
+ );
619
+ if (
620
+ tasksBytes &&
621
+ expectedTasksHash &&
622
+ sha256(tasksBytes) !== expectedTasksHash
623
+ ) {
624
+ const normalized = normalizedTrackingArtifact(
625
+ tasksBytes,
626
+ plan.tracking.taskIds,
627
+ );
628
+ if (!normalized || sha256(normalized) !== expectedTasksHash) {
629
+ diagnostics.add(
630
+ `delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
631
+ );
632
+ }
633
+ }
634
+ const specs = [...artifactBytes]
635
+ .filter(
636
+ ([relative]) =>
637
+ relative.startsWith("specs/") && relative.endsWith("/spec.md"),
638
+ )
639
+ .flatMap(([relative, bytes]) => {
640
+ try {
641
+ return [
642
+ {
643
+ path: relative,
644
+ text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
645
+ },
646
+ ];
647
+ } catch {
648
+ diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
649
+ return [];
650
+ }
651
+ });
652
+ if (!tasksBytes || specs.length === 0) {
653
+ diagnostics.add("delivery-traceability-input-unavailable");
654
+ } else {
655
+ try {
656
+ const traceability = assessDeliveryTraceability({
657
+ tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
658
+ tasksBytes,
659
+ ),
660
+ specs,
661
+ plan,
662
+ });
663
+ if (!traceability.ok) {
664
+ for (const diagnostic of traceability.diagnostics) {
665
+ diagnostics.add(diagnostic);
666
+ }
667
+ } else if (
668
+ canonicalJson(traceability.value) !==
669
+ canonicalJson(receipt.traceability)
670
+ ) {
671
+ diagnostics.add("delivery-traceability-binding-mismatch");
672
+ }
673
+ } catch {
674
+ diagnostics.add("delivery-traceability-invalid");
675
+ }
676
+ }
677
+ }
678
+ if (!plan || diagnostics.size > 0) {
679
+ throw new DeliveryValidationError([...diagnostics]);
680
+ }
681
+ return {
682
+ version: 2,
683
+ gate: "gate-b",
684
+ revision: receipt.deliveryRevision,
685
+ receiptHash,
686
+ plan,
687
+ };
688
+ },
689
+ };
690
+ }
691
+
692
+ function packageScriptInvocation(
693
+ packageManager: string,
694
+ script: string,
695
+ args: string[],
696
+ ): string[] {
697
+ return packageManager === "npm" || packageManager === "pnpm"
698
+ ? ["run", script, "--", ...args]
699
+ : ["run", script, ...args];
700
+ }
701
+
702
+ function bindingFor(
703
+ bindings: readonly VerificationRunnerBinding[],
704
+ command: string,
705
+ ): VerificationRunnerBinding | undefined {
706
+ return bindings.find((binding) => binding.command === command);
707
+ }
708
+
709
+ function prepareRunnerBindings(
710
+ root: string,
711
+ bindings: readonly VerificationRunnerBinding[],
712
+ ): {
713
+ bindings: VerificationRunnerBinding[];
714
+ mounts: Array<{ source: string; target: string }>;
715
+ cleanupRoot?: string;
716
+ } {
717
+ if (!bindings.some((binding) => binding.mountSource)) {
718
+ return {
719
+ bindings: bindings.map((binding) => ({
720
+ ...binding,
721
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
722
+ })),
723
+ mounts: [],
724
+ };
725
+ }
726
+ const cleanupRoot = mkdtempSync(path.join(root, ".cadence-runners-"));
727
+ try {
728
+ const mounted = new Map<
729
+ string,
730
+ { source: string; target: string; sandboxTarget: string }
731
+ >();
732
+ const prepared = bindings.map((binding) => {
733
+ if (!binding.mountSource) return { ...binding };
734
+ const source = path.resolve(binding.mountSource);
735
+ const sourceStat = lstatSync(source);
736
+ if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
737
+ throw new Error("runner-mount-unavailable");
738
+ }
739
+ let mount = mounted.get(source);
740
+ if (!mount) {
741
+ const target = path.join(cleanupRoot, String(mounted.size));
742
+ mkdirSync(target, { mode: 0o700 });
743
+ const sandboxRelative = path
744
+ .relative(root, target)
745
+ .split(path.sep)
746
+ .join("/");
747
+ mount = {
748
+ source,
749
+ target,
750
+ sandboxTarget: `/workspace/${sandboxRelative}`,
751
+ };
752
+ mounted.set(source, mount);
753
+ }
754
+ const executable = realpathSync(binding.executablePath);
755
+ const relative = path.relative(source, executable);
756
+ if (
757
+ relative === "" ||
758
+ relative.startsWith("..") ||
759
+ path.isAbsolute(relative)
760
+ ) {
761
+ throw new Error("runner-mount-unavailable");
762
+ }
763
+ return {
764
+ command: binding.command,
765
+ executablePath: path.posix.join(
766
+ mount.sandboxTarget,
767
+ ...relative.split(path.sep),
768
+ ),
769
+ ...(binding.fixedArgs ? { fixedArgs: [...binding.fixedArgs] } : {}),
770
+ };
771
+ });
772
+ return {
773
+ bindings: prepared,
774
+ mounts: [...mounted.values()].map(({ source, sandboxTarget }) => ({
775
+ source,
776
+ target: sandboxTarget,
777
+ })),
778
+ cleanupRoot,
779
+ };
780
+ } catch (error) {
781
+ rmSync(cleanupRoot, { recursive: true, force: true });
782
+ throw error;
783
+ }
784
+ }
785
+
786
+ function verificationInvocation(
787
+ step: AtomicVerificationContract,
788
+ bindings: readonly VerificationRunnerBinding[],
789
+ ): { executable: string; args: string[] } | undefined {
790
+ const reporter = step.kind === "vitest" ? ["--reporter=json"] : [];
791
+ if (step.kind === "package-script") {
792
+ const binding = bindingFor(bindings, step.packageManager);
793
+ return binding
794
+ ? {
795
+ executable: binding.executablePath,
796
+ args: [
797
+ ...(binding.fixedArgs ?? []),
798
+ ...packageScriptInvocation(
799
+ step.packageManager,
800
+ step.script,
801
+ step.args,
802
+ ),
803
+ ],
804
+ }
805
+ : undefined;
806
+ }
807
+ const runner = step.runner;
808
+ if (runner.kind === "package-script") {
809
+ const binding = bindingFor(bindings, runner.packageManager);
810
+ const args =
811
+ step.kind === "vitest"
812
+ ? [...step.testFiles, ...step.args, ...reporter]
813
+ : step.args;
814
+ return binding
815
+ ? {
816
+ executable: binding.executablePath,
817
+ args: [
818
+ ...(binding.fixedArgs ?? []),
819
+ ...packageScriptInvocation(
820
+ runner.packageManager,
821
+ runner.script,
822
+ args,
823
+ ),
824
+ ],
825
+ }
826
+ : undefined;
827
+ }
828
+ if (runner.kind === "local-binary") {
829
+ const binding = bindingFor(bindings, runner.executable);
830
+ return {
831
+ executable:
832
+ binding?.executablePath ??
833
+ `/workspace/node_modules/.bin/${runner.executable}`,
834
+ args: [
835
+ ...(binding?.fixedArgs ?? []),
836
+ ...(step.kind === "vitest"
837
+ ? [...step.testFiles, ...step.args, ...reporter]
838
+ : step.args),
839
+ ],
840
+ };
841
+ }
842
+ const command = runner.kind === "node" ? "node" : "npx";
843
+ const binding = bindingFor(bindings, command);
844
+ if (!binding) return undefined;
845
+ return {
846
+ executable: binding.executablePath,
847
+ args:
848
+ runner.kind === "node"
849
+ ? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
850
+ : [
851
+ ...(binding.fixedArgs ?? []),
852
+ "--no-install",
853
+ runner.executable,
854
+ ...(step.kind === "vitest" ? step.testFiles : []),
855
+ ...step.args,
856
+ ...reporter,
857
+ ],
858
+ };
859
+ }
860
+
861
+ function vitestReport(stdout: string):
862
+ | {
863
+ total: number;
864
+ failed: number;
865
+ success: boolean;
866
+ failures: string[];
867
+ failedText: string[];
868
+ }
869
+ | undefined {
870
+ let value: unknown;
871
+ try {
872
+ value = JSON.parse(stdout);
873
+ } catch {
874
+ return undefined;
875
+ }
876
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
877
+ return undefined;
878
+ }
879
+ const report = value as Record<string, unknown>;
880
+ if (
881
+ !Number.isSafeInteger(report.numTotalTests) ||
882
+ !Number.isSafeInteger(report.numFailedTests) ||
883
+ typeof report.success !== "boolean"
884
+ ) {
885
+ return undefined;
886
+ }
887
+ const failedAssertions = (
888
+ Array.isArray(report.testResults) ? report.testResults : []
889
+ ).flatMap((suite): Array<{ identity: string; text: string[] }> => {
890
+ if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
891
+ const record = suite as Record<string, unknown>;
892
+ const suiteName =
893
+ typeof record.name === "string"
894
+ ? record.name.replace(/^\/workspace\//u, "")
895
+ : "unknown-suite";
896
+ if (!Array.isArray(record.assertionResults)) return [];
897
+ return record.assertionResults.flatMap(
898
+ (assertion): Array<{ identity: string; text: string[] }> => {
899
+ if (
900
+ !assertion ||
901
+ typeof assertion !== "object" ||
902
+ Array.isArray(assertion)
903
+ ) {
904
+ return [];
905
+ }
906
+ const entry = assertion as Record<string, unknown>;
907
+ if (entry.status !== "failed") return [];
908
+ const title =
909
+ typeof entry.fullName === "string"
910
+ ? entry.fullName
911
+ : typeof entry.title === "string"
912
+ ? entry.title
913
+ : "unknown-assertion";
914
+ const diagnostics = Array.isArray(entry.failureMessages)
915
+ ? entry.failureMessages.filter(
916
+ (message): message is string => typeof message === "string",
917
+ )
918
+ : [];
919
+ return [
920
+ {
921
+ identity: `${suiteName}\0${title}`,
922
+ text: [title, ...diagnostics],
923
+ },
924
+ ];
925
+ },
926
+ );
927
+ });
928
+ return {
929
+ total: report.numTotalTests as number,
930
+ failed: report.numFailedTests as number,
931
+ success: report.success,
932
+ failures: failedAssertions.map((failure) => failure.identity),
933
+ failedText: failedAssertions.flatMap((failure) => failure.text),
934
+ };
935
+ }
936
+
937
+ function normalizedFailureIdentities(input: {
938
+ step: AtomicVerificationContract;
939
+ report?: ReturnType<typeof vitestReport>;
940
+ output: string;
941
+ exitCode: number;
942
+ }): string[] {
943
+ const vitestFailures = input.report?.failures ?? [];
944
+ if (vitestFailures.length > 0) {
945
+ return [...new Set(vitestFailures)]
946
+ .sort()
947
+ .map((failure) => sha256(`vitest-failure-v1\0${failure}`));
948
+ }
949
+ const normalizedOutput = input.output
950
+ .replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
951
+ .replace(/\r\n?/gu, "\n")
952
+ .replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
953
+ .slice(0, 64 * 1024);
954
+ return [
955
+ sha256(
956
+ canonicalJson({
957
+ version: 1,
958
+ verificationId: input.step.id,
959
+ exitCode: input.exitCode,
960
+ output: normalizedOutput,
961
+ }),
962
+ ),
963
+ ];
964
+ }
965
+
966
+ export async function executePackageVerification(input: {
967
+ root: string;
968
+ dependencyOwner: string;
969
+ verification: StructuredVerificationContract;
970
+ signal: AbortSignal;
971
+ }) {
972
+ const capability = bindCurrentVerificationCapability(
973
+ input.root,
974
+ input.verification,
975
+ { dependencyOwner: input.dependencyOwner },
976
+ );
977
+ if (!capability.ok) {
978
+ return {
979
+ ok: false as const,
980
+ kind: "paused" as const,
981
+ code: capability.diagnostic.code,
982
+ };
983
+ }
984
+ let runners: ReturnType<typeof prepareRunnerBindings>;
985
+ try {
986
+ runners = prepareRunnerBindings(
987
+ input.root,
988
+ capability.value.runnerBindings,
989
+ );
990
+ } catch {
991
+ return {
992
+ ok: false as const,
993
+ kind: "paused" as const,
994
+ code: "runner-mount-unavailable",
995
+ };
996
+ }
997
+ try {
998
+ const nodeModules = path.join(input.dependencyOwner, "node_modules");
999
+ const dependency = lstatSync(nodeModules, { throwIfNoEntry: false });
1000
+ const mounts = [] as Array<{
1001
+ source: string;
1002
+ target: string;
1003
+ writable?: boolean;
1004
+ }>;
1005
+ if (dependency) {
1006
+ if (!dependency.isDirectory() || dependency.isSymbolicLink()) {
1007
+ return {
1008
+ ok: false as const,
1009
+ kind: "paused" as const,
1010
+ code: "dependency-path-unsafe",
1011
+ };
1012
+ }
1013
+ const target = path.join(input.root, "node_modules");
1014
+ const targetStat = lstatSync(target, { throwIfNoEntry: false });
1015
+ if (
1016
+ targetStat &&
1017
+ (!targetStat.isDirectory() || targetStat.isSymbolicLink())
1018
+ ) {
1019
+ return {
1020
+ ok: false as const,
1021
+ kind: "paused" as const,
1022
+ code: "dependency-path-unsafe",
1023
+ };
1024
+ }
1025
+ if (!targetStat) mkdirSync(target, { mode: 0o700 });
1026
+ mounts.push({ source: nodeModules, target: "/workspace/node_modules" });
1027
+ }
1028
+ mounts.push(...runners.mounts);
1029
+ const isolation = new BubblewrapIsolationBackend();
1030
+ const steps = verificationSteps(input.verification);
1031
+ let final:
1032
+ | {
1033
+ exitCode: number;
1034
+ classification: AtomicVerificationContract["classification"];
1035
+ diagnostic: { kind: "assertion" | "compiler"; id: string };
1036
+ }
1037
+ | undefined;
1038
+ for (const step of steps) {
1039
+ const invocation = verificationInvocation(step, runners.bindings);
1040
+ if (!invocation) {
1041
+ return {
1042
+ ok: false as const,
1043
+ kind: "paused" as const,
1044
+ code: "runner-missing",
1045
+ };
1046
+ }
1047
+ const executed = await isolation.run({
1048
+ root: input.root,
1049
+ executable: invocation.executable,
1050
+ args: invocation.args,
1051
+ mounts,
1052
+ environment: { CI: "1" },
1053
+ signal: input.signal,
1054
+ });
1055
+ if (!executed.ok) {
1056
+ if (executed.state === "cancelled") throw input.signal.reason;
1057
+ return {
1058
+ ok: false as const,
1059
+ kind: "paused" as const,
1060
+ code: executed.code,
1061
+ };
1062
+ }
1063
+ const report =
1064
+ step.kind === "vitest" ? vitestReport(executed.stdout) : undefined;
1065
+ const output = `${executed.stdout}\n${executed.stderr}`;
1066
+ const identity =
1067
+ step.classification !== "expected-red" ||
1068
+ (step.kind === "vitest"
1069
+ ? report?.failedText.some((text) =>
1070
+ text.includes(step.expectedFailure ?? ""),
1071
+ ) === true
1072
+ : output.includes(step.expectedFailure ?? ""));
1073
+ const accepted =
1074
+ step.classification === "expected-red"
1075
+ ? executed.exitCode !== 0 &&
1076
+ identity &&
1077
+ (step.kind !== "vitest" ||
1078
+ (report !== undefined &&
1079
+ report.failed > 0 &&
1080
+ report.total >= step.minTests))
1081
+ : executed.exitCode === 0 &&
1082
+ (step.kind !== "vitest" ||
1083
+ (report?.success === true && report.total >= step.minTests));
1084
+ if (!accepted) {
1085
+ return {
1086
+ ok: false as const,
1087
+ kind: "retryable" as const,
1088
+ code:
1089
+ step.classification === "expected-red" && executed.exitCode === 0
1090
+ ? "red-not-witnessed"
1091
+ : "verification-rejected",
1092
+ failureIdentities: normalizedFailureIdentities({
1093
+ step,
1094
+ report,
1095
+ output,
1096
+ exitCode: executed.exitCode,
1097
+ }),
1098
+ };
1099
+ }
1100
+ if (!isVerificationCapabilityCurrent(input.root, capability.value)) {
1101
+ return {
1102
+ ok: false as const,
1103
+ kind: "retryable" as const,
1104
+ code: "verification-input-unavailable",
1105
+ };
1106
+ }
1107
+ final = {
1108
+ exitCode: executed.exitCode,
1109
+ classification: step.classification,
1110
+ diagnostic: {
1111
+ kind: step.kind === "vitest" ? "assertion" : "compiler",
1112
+ id: step.id,
1113
+ },
1114
+ };
1115
+ }
1116
+ return final
1117
+ ? { ok: true as const, ...final }
1118
+ : {
1119
+ ok: false as const,
1120
+ kind: "paused" as const,
1121
+ code: "verification-contract-unsupported",
1122
+ };
1123
+ } finally {
1124
+ if (runners.cleanupRoot) {
1125
+ rmSync(runners.cleanupRoot, { recursive: true, force: true });
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ function affectedVerification(
1131
+ task: Parameters<
1132
+ Parameters<typeof openDurableWorkflowEngine>[0]["proposeCandidate"]
1133
+ >[0]["task"],
1134
+ ): StructuredVerificationContract {
1135
+ if (task.affectedVerification) {
1136
+ return structuredClone(task.affectedVerification);
1137
+ }
1138
+ const finalPhase = task.phases.refactor ?? task.phases.green;
1139
+ const verification = structuredClone(finalPhase.verification);
1140
+ const affected = [...new Set(task.impactClosure.affectedSuite)].sort();
1141
+ if (affected.length === 0) return verification;
1142
+ if (verification.kind === "vitest") {
1143
+ return { ...verification, testFiles: affected };
1144
+ }
1145
+ if (verification.kind === "steps") {
1146
+ return {
1147
+ ...verification,
1148
+ steps: verification.steps.map((step) =>
1149
+ step.kind === "vitest" ? { ...step, testFiles: affected } : step,
1150
+ ),
1151
+ };
1152
+ }
1153
+ return verification;
1154
+ }
1155
+
1156
+ function childRequestId(
1157
+ operationId: string,
1158
+ taskId: string,
1159
+ phase: string,
1160
+ ): string {
1161
+ return `child-${sha256(`${operationId}\0${taskId}\0${phase}`).slice(0, 40)}`;
1162
+ }
1163
+
1164
+ export function openPackageWorkflowControlEngine(
1165
+ initialContext: ExtensionContext,
1166
+ parentPayloadBridge: ParentPayloadBridge,
1167
+ ): WorkflowControlEngine {
1168
+ const consumerRoot = path.resolve(initialContext.cwd);
1169
+ const routeResolution = loadRoutePolicy({
1170
+ cwd: consumerRoot,
1171
+ home: homedir(),
1172
+ });
1173
+ const stateRoot = resolveStateRoot({
1174
+ consumerRoot,
1175
+ xdgStateHome: process.env.XDG_STATE_HOME,
1176
+ });
1177
+ const contexts = new AsyncLocalStorage<ExtensionContext>();
1178
+ const implementationAgent = loadAgentDefinitions().find(
1179
+ (agent) => agent.role === "implementation-worker",
1180
+ );
1181
+ if (!implementationAgent) {
1182
+ throw new Error("implementation-worker-agent-unavailable");
1183
+ }
1184
+ const engine = openDurableWorkflowEngine({
1185
+ consumerRoot,
1186
+ stateRoot,
1187
+ deliverySource: packageDeliverySource(consumerRoot),
1188
+ routePolicy: routeResolution.ok
1189
+ ? routeResolution.policy
1190
+ : unavailableRoutePolicy(),
1191
+ proposeCandidate: async (input) => {
1192
+ const context = contexts.getStore();
1193
+ if (!context) {
1194
+ return { kind: "paused", code: "parent-context-unavailable" };
1195
+ }
1196
+ const phaseRuntime = await runtimeForWorkerRoute(
1197
+ input.route as WorkerRoutePolicy,
1198
+ context,
1199
+ parentPayloadBridge,
1200
+ input.signal,
1201
+ );
1202
+ if (!phaseRuntime.ok) {
1203
+ if (phaseRuntime.failure.kind === "cancelled") {
1204
+ return { kind: "operation-cancelled", code: "cancelled" };
1205
+ }
1206
+ throw new Error(phaseRuntime.failure.code);
1207
+ }
1208
+ const phase = input.task.phases[input.phase];
1209
+ if (!phase) {
1210
+ return { kind: "paused", code: "task-phase-unavailable" };
1211
+ }
1212
+ const phaseContract = {
1213
+ candidateId: input.candidateArtifact.identity.candidateId,
1214
+ taskId: input.taskId,
1215
+ phase: input.phase,
1216
+ readSet: [...phase.read],
1217
+ writeSet: [...phase.write],
1218
+ deleteSet: [...phase.delete],
1219
+ verification: structuredClone(phase.verification),
1220
+ agentsImpact: input.task.agents.impact,
1221
+ agentsTarget: input.task.agents.target ?? null,
1222
+ agentsManagedOnly: true,
1223
+ agentsWriteAllowed: false,
1224
+ impactClosure: structuredClone(input.task.impactClosure),
1225
+ ...(input.repair
1226
+ ? {
1227
+ repair: {
1228
+ attempt: input.repair.attempt,
1229
+ attribution: input.repair.attribution,
1230
+ failureIdentities: [...input.repair.failureIdentities],
1231
+ verification: structuredClone(input.task.repairVerification),
1232
+ inBoundaryOnly: true,
1233
+ },
1234
+ }
1235
+ : {}),
1236
+ };
1237
+ const child = await runChildSession({
1238
+ cwd: input.workspaceRoot,
1239
+ modelRuntime: phaseRuntime.modelRuntime,
1240
+ model: phaseRuntime.model,
1241
+ systemPrompt: [
1242
+ implementationAgent.content,
1243
+ input.task.objective,
1244
+ input.task.context.agents,
1245
+ input.task.context.contract,
1246
+ `<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
1247
+ ].join("\n\n"),
1248
+ requestId: childRequestId(input.operationId, input.taskId, input.phase),
1249
+ taskId: input.taskId,
1250
+ role: "implementation-worker",
1251
+ phase: input.phase,
1252
+ output: "diff",
1253
+ roots: input.task.roots.map((root) =>
1254
+ path.resolve(input.workspaceRoot, root),
1255
+ ),
1256
+ allowedPaths: [
1257
+ ...new Set([...phase.read, ...phase.write, ...phase.delete]),
1258
+ ],
1259
+ timeoutMs: LIMITS.phaseTimeoutMs,
1260
+ signal: input.signal,
1261
+ failureOverride: phaseRuntime.failureOverride,
1262
+ ledgerProjection: input.ledgerProjection,
1263
+ candidateArtifact: input.candidateArtifact,
1264
+ onStreamStart: input.onHeaders,
1265
+ onStreamProgress: input.onProgress,
1266
+ });
1267
+ if (!child.ok) {
1268
+ if (child.failure.kind === "transport") {
1269
+ throw new Error(child.failure.code);
1270
+ }
1271
+ if (child.failure.kind === "cancelled") {
1272
+ return { kind: "operation-cancelled", code: "cancelled" };
1273
+ }
1274
+ if (child.failure.kind === "approval-boundary") {
1275
+ return { kind: "approval-needed", code: child.failure.code };
1276
+ }
1277
+ if (child.failure.kind === "result-limit") {
1278
+ return { kind: "paused", code: "needs-task-split" };
1279
+ }
1280
+ return child.failure.kind === "environment" ||
1281
+ child.failure.kind === "verification-adapter"
1282
+ ? { kind: "paused", code: child.failure.code }
1283
+ : { kind: "retryable", code: child.failure.code };
1284
+ }
1285
+ const result = child.result;
1286
+ if (result.kind === "context-request") {
1287
+ return classifyCandidateContextRequest(result, [
1288
+ ...phase.read,
1289
+ ...phase.write,
1290
+ ...phase.delete,
1291
+ ]);
1292
+ }
1293
+ if (result.kind !== "sealed-candidate") {
1294
+ return { kind: "retryable", code: "candidate-diff-invalid" };
1295
+ }
1296
+ return {
1297
+ kind: "sealed-candidate",
1298
+ candidateId: result.candidateId,
1299
+ artifactHash: result.artifactHash,
1300
+ bytes: result.bytes,
1301
+ paths: [...result.paths],
1302
+ };
1303
+ },
1304
+ verifyPhase: (input) =>
1305
+ executePackageVerification({
1306
+ root: input.root,
1307
+ dependencyOwner: consumerRoot,
1308
+ verification: input.verification,
1309
+ signal: input.signal,
1310
+ }),
1311
+ verifyChange: async (input) => {
1312
+ const verifications = input.verification
1313
+ ? [input.verification]
1314
+ : input.plan.tasks.map((task) => affectedVerification(task));
1315
+ for (const verification of verifications) {
1316
+ const result = await executePackageVerification({
1317
+ root: input.root,
1318
+ dependencyOwner: consumerRoot,
1319
+ verification,
1320
+ signal: input.signal,
1321
+ });
1322
+ if (!result.ok) {
1323
+ const adapter = new Set([
1324
+ "runner-missing",
1325
+ "script-missing",
1326
+ "script-command-mismatch",
1327
+ "local-executable-missing",
1328
+ ]).has(result.code);
1329
+ return {
1330
+ ok: false,
1331
+ kind: adapter
1332
+ ? ("verification-adapter" as const)
1333
+ : result.kind === "paused"
1334
+ ? ("environment" as const)
1335
+ : ("verification" as const),
1336
+ code: result.code,
1337
+ ...(result.failureIdentities
1338
+ ? { failureIdentities: result.failureIdentities }
1339
+ : {}),
1340
+ };
1341
+ }
1342
+ }
1343
+ return {
1344
+ ok: true,
1345
+ exitCode: 0,
1346
+ classification: "expected-green",
1347
+ };
1348
+ },
1349
+ });
1350
+ return {
1351
+ async execute(
1352
+ command: unknown,
1353
+ context = initialContext,
1354
+ signal,
1355
+ onActivity,
1356
+ ) {
1357
+ const operationRouteResolution = loadRoutePolicy({
1358
+ cwd: consumerRoot,
1359
+ home: homedir(),
1360
+ });
1361
+ engine.updateRoutePolicy(
1362
+ operationRouteResolution.ok
1363
+ ? operationRouteResolution.policy
1364
+ : unavailableRoutePolicy(),
1365
+ );
1366
+ const validation = validateControlCommand(command);
1367
+ if (!validation.ok) {
1368
+ const error = new Error(validation.code);
1369
+ error.name = "ControlCommandError";
1370
+ throw error;
1371
+ }
1372
+ return contexts.run(context, async () => {
1373
+ const outcome = await engine.execute(
1374
+ validation.value,
1375
+ signal,
1376
+ onActivity,
1377
+ );
1378
+ const routePolicy = visibleRoutePolicyStatus(
1379
+ operationRouteResolution,
1380
+ engine.routePolicyStatus(),
1381
+ );
1382
+ return { ...outcome, routePolicy };
1383
+ });
1384
+ },
1385
+ async close() {
1386
+ contexts.disable();
1387
+ await engine.close();
1388
+ },
1389
+ };
1390
+ }
1391
+
1392
+ function visibleRoutePolicyStatus(
1393
+ resolution: RoutePolicyResolution,
1394
+ brokerStatus: Record<string, unknown> | undefined,
1395
+ ): Record<string, unknown> {
1396
+ if (!resolution.ok) return inspectRoutePolicy(resolution);
1397
+ const inspected = brokerStatus ?? inspectRoutePolicy(resolution);
1398
+ return {
1399
+ ...inspected,
1400
+ source: { kind: resolution.source.kind },
1401
+ };
1402
+ }
1403
+
1404
+ export function registerWorkflowControl(
1405
+ pi: ExtensionAPI,
1406
+ engineFactory: WorkflowControlEngineFactory = openPackageWorkflowControlEngine,
1407
+ ): void {
222
1408
  const parentPayloadBridge = new ParentPayloadBridge();
223
- const runtime = new Runtime({ parentPayloadBridge });
224
1409
  const activity = new ActivityController();
1410
+ const engines = new Map<string, Promise<WorkflowControlEngine>>();
1411
+ let pendingPrompt: EligiblePrompt | undefined;
1412
+ let activePrompt: EligiblePrompt | undefined;
1413
+ const activation = new (class implements Activation {
1414
+ state: Activation["state"] = "inactive";
225
1415
 
226
- const registerDispatchTool = (stage?: EligiblePrompt) => {
227
- pi.registerTool({
228
- name: DISPATCH_TOOL,
229
- label: "Abel Dispatch",
230
- 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.",
234
- 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
- },
1416
+ isActive(): boolean {
1417
+ return this.state === "active";
1418
+ }
1419
+
1420
+ request(): boolean {
1421
+ if (this.state !== "inactive") return false;
1422
+ this.state = "pending";
1423
+ return true;
1424
+ }
1425
+
1426
+ activate(): boolean {
1427
+ if (this.state !== "pending") return false;
1428
+ this.state = "active";
1429
+ return true;
1430
+ }
1431
+
1432
+ drain(): boolean {
1433
+ if (this.state === "inactive" || this.state === "pending") return false;
1434
+ this.state = "inactive";
1435
+ return true;
1436
+ }
1437
+ })();
1438
+ const packetRuntime = new PacketRuntime({ activation, parentPayloadBridge });
1439
+
1440
+ const engineFor = (ctx: ExtensionContext) => {
1441
+ const key = ctx.cwd;
1442
+ const existing = engines.get(key);
1443
+ if (existing) return existing;
1444
+ const opened = Promise.resolve(engineFactory(ctx, parentPayloadBridge));
1445
+ engines.set(key, opened);
1446
+ void opened.catch(() => {
1447
+ if (engines.get(key) === opened) engines.delete(key);
1448
+ });
1449
+ return opened;
1450
+ };
1451
+ const closeEngines = async () => {
1452
+ const current = [...engines.values()];
1453
+ engines.clear();
1454
+ const opened = await Promise.allSettled(current);
1455
+ await Promise.allSettled(
1456
+ opened.flatMap((result) =>
1457
+ result.status === "fulfilled"
1458
+ ? [Promise.resolve(result.value.close())]
1459
+ : [],
1460
+ ),
1461
+ );
1462
+ };
1463
+ const deactivate = () => {
1464
+ activation.drain();
1465
+ const active = pi.getActiveTools();
1466
+ if (active.includes(DISPATCH_TOOL)) {
1467
+ pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
1468
+ }
1469
+ };
1470
+
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],
258
1498
  },
259
- required: ["action"],
260
1499
  },
261
- async execute(
262
- toolCallId: string,
263
- params: {
264
- action?: string;
265
- request?: unknown;
266
- resultId?: string;
267
- requestId?: string;
268
- rejection?: unknown;
269
- agentsCheckpoint?: unknown;
1500
+ additionalProperties: false,
1501
+ oneOf: [
1502
+ {
1503
+ required: ["version", "command", "stage"],
1504
+ not: {
1505
+ anyOf: [{ required: ["action"] }, { required: ["request"] }],
1506
+ },
270
1507
  },
271
- signal: AbortSignal | undefined,
272
- onUpdate: AgentToolUpdateCallback<unknown> | undefined,
273
- ctx: ExtensionContext,
274
- ) {
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,
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
+ ],
1525
+ },
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");
1544
+ }
1545
+ if (
1546
+ activePrompt !== "abel-design" &&
1547
+ activePrompt !== "abel-diagnose"
1548
+ ) {
1549
+ throw new Error("stage-control-mismatch");
1550
+ }
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,
1556
+ );
1557
+ }
1558
+ }
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,
283
1566
  operation,
284
1567
  ctx,
285
1568
  signal,
@@ -288,65 +1571,123 @@ export default function register(pi: ExtensionAPI): void {
288
1571
  onUpdate as ((result: unknown) => void) | undefined,
289
1572
  ),
290
1573
  )
291
- : await runtime.execute(action, operation, ctx, signal);
1574
+ : await packetRuntime.execute(record.action, operation, ctx, signal);
292
1575
  const display = tuiRun
293
- ? activity.finalize(toolCallId, result)
1576
+ ? activity.finalize(toolCallId, payload)
294
1577
  : undefined;
295
- const { payload, usage } = splitUsage(result);
1578
+ const { payload: publicPayload, usage } = splitUsage(payload);
296
1579
  const details = display
297
1580
  ? {
298
- ...(payload as Record<string, unknown>),
1581
+ ...(publicPayload as Record<string, unknown>),
299
1582
  [ACTIVITY_DETAILS_KEY]: display,
300
1583
  }
301
- : payload;
1584
+ : publicPayload;
302
1585
  return {
303
- content: [{ type: "text", text: JSON.stringify(payload) }],
1586
+ content: [
1587
+ { type: "text" as const, text: JSON.stringify(publicPayload) },
1588
+ ],
304
1589
  details,
305
1590
  ...(usage === undefined ? {} : { usage }),
306
1591
  };
307
- },
308
- renderCall(args: unknown, theme: Theme, context: unknown) {
309
- return renderActivityCall(
310
- args,
311
- theme,
312
- context as Parameters<typeof renderActivityCall>[2],
313
- );
314
- },
315
- renderResult(
316
- result: AgentToolResult<unknown>,
317
- options: ToolRenderResultOptions,
318
- theme: Theme,
319
- context: unknown,
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)
320
1603
  ) {
321
- return renderActivityResult(
322
- result,
323
- options,
324
- theme,
325
- context as Parameters<typeof renderActivityResult>[3],
1604
+ throw new Error("stage-control-mismatch");
1605
+ }
1606
+ const engine = await engineFor(ctx);
1607
+ const tuiControl = ctx.mode === "tui";
1608
+ if (tuiControl) {
1609
+ try {
1610
+ activity.beginWorkflow(
1611
+ toolCallId,
1612
+ 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.
1631
+ }
1632
+ }
1633
+ : undefined,
326
1634
  );
327
- },
328
- } as never);
329
- };
330
-
331
- registerDispatchTool();
332
-
333
- let pendingPrompt: EligiblePrompt | undefined;
334
- let activePrompt: EligiblePrompt | undefined;
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,
1646
+ );
1647
+ } catch {
1648
+ activity.failWorkflow(toolCallId);
1649
+ }
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);
335
1679
 
336
1680
  pi.on("input", (event) => {
337
1681
  pendingPrompt = invokedPrompt(event.text);
338
1682
  return { action: "continue" };
339
1683
  });
340
-
341
1684
  pi.on("before_agent_start", (event, ctx) => {
342
1685
  const prompt = pendingPrompt;
343
1686
  pendingPrompt = undefined;
344
1687
  const verified =
345
- prompt &&
346
- isVerifiedStageInvocation(pi, runtime.activation, prompt, event.prompt);
1688
+ prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
347
1689
  if (verified) {
348
1690
  activePrompt = prompt;
349
- registerDispatchTool(prompt);
350
1691
  const sessionId = ctx.sessionManager?.getSessionId?.();
351
1692
  if (typeof sessionId === "string") {
352
1693
  parentPayloadBridge.beginSession(sessionId);
@@ -357,10 +1698,18 @@ export default function register(pi: ExtensionAPI): void {
357
1698
  if (ctx.model) {
358
1699
  parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
359
1700
  }
360
- if (prompt)
361
- activateDispatcher(pi, runtime.activation, prompt, event.prompt);
1701
+ if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
1702
+ });
1703
+ pi.on("model_select", (event, ctx) => {
1704
+ const sessionId = ctx.sessionManager?.getSessionId?.();
1705
+ if (typeof sessionId !== "string") {
1706
+ parentPayloadBridge.clear();
1707
+ return;
1708
+ }
1709
+ parentPayloadBridge.beginSession(sessionId);
1710
+ const model = event.model ?? ctx.model;
1711
+ if (model) parentPayloadBridge.install(model, ctx.modelRegistry);
362
1712
  });
363
-
364
1713
  pi.on("before_provider_request", (event, ctx) => {
365
1714
  if (
366
1715
  activePrompt !== "abel-design" ||
@@ -373,50 +1722,33 @@ export default function register(pi: ExtensionAPI): void {
373
1722
  }
374
1723
  return { ...event.payload, parallel_tool_calls: true };
375
1724
  });
376
-
377
1725
  pi.on("session_start", async (_event, ctx) => {
378
1726
  pendingPrompt = undefined;
379
1727
  activePrompt = undefined;
380
1728
  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();
1729
+ await packetRuntime.drain();
1730
+ await closeEngines();
1731
+ deactivate();
387
1732
  const sessionId = ctx.sessionManager?.getSessionId?.();
388
1733
  if (typeof sessionId === "string") {
389
1734
  parentPayloadBridge.beginSession(sessionId);
390
- if (ctx.model) {
391
- parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
392
- }
1735
+ if (ctx.model) parentPayloadBridge.install(ctx.model, ctx.modelRegistry);
393
1736
  } else {
394
1737
  parentPayloadBridge.clear();
395
1738
  }
396
1739
  if (ctx.mode === "tui") activity.attach(ctx.ui);
397
1740
  });
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
1741
  pi.on("session_shutdown", async () => {
413
1742
  activePrompt = undefined;
414
1743
  activity.detach();
415
- await runtime.drain();
1744
+ await packetRuntime.drain();
1745
+ await closeEngines();
416
1746
  activity.clear();
417
- const active = pi.getActiveTools();
418
- if (active.includes(DISPATCH_TOOL)) {
419
- pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
420
- }
1747
+ deactivate();
1748
+ parentPayloadBridge.clear();
421
1749
  });
422
1750
  }
1751
+
1752
+ export default function register(pi: ExtensionAPI): void {
1753
+ registerWorkflowControl(pi);
1754
+ }