@abelxiaoxing/cadence 1.2.3 → 1.3.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/README.md CHANGED
@@ -71,6 +71,46 @@ npm install -g @abelxiaoxing/cadence
71
71
 
72
72
  本包要求 Node.js `>=22.13.0`,以使用稳定可用的内置 `node:sqlite`;不从参考仓库源安装,也不维护 Pi 主机版本兼容矩阵。
73
73
 
74
+ ## OpenSpec 启动与平台范围
75
+
76
+ Design finalization 和 Implement delivery 加载共用无 shell 的 OpenSpec 适配层:读取已安装 `@fission-ai/openspec` 的 `package.json` 中 `bin` 声明,再以 Node 执行 JS 入口;不直接执行 Windows npm 的 `.cmd`,也不调用 `npx`、自动下载或切换 CLI 版本。
77
+
78
+ 默认按宿主 `PATH` 定位 OpenSpec,支持 npm 全局默认/自定义 prefix,以及 Unix npm/Bun 的入口符号链接。
79
+ 不搜索当前目录,不自动信任 consumer repository 内的 CLI;第一个已发现安装损坏时明确失败,不偷偷选用后续版本。
80
+ 版本管理器的任意包装脚本、原生 exe 和 PowerShell 脚本不作为可执行回退。
81
+
82
+ 特殊安装可在**启动宿主之前**配置以下环境变量(不属于 Worker 请求参数):
83
+
84
+ | 变量 | 含义 |
85
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
86
+ | `ABEL_OPENSPEC_PACKAGE_ROOT` | 已安装 `@fission-ai/openspec` 的绝对包目录,包含 `package.json`;不是 `.cmd` 或 JS 文件路径。显式配置代表操作者信任该安装。 |
87
+ | `ABEL_OPENSPEC_NODE` | Node 原生可执行文件的绝对路径,例如 `C:\Program Files\nodejs\node.exe`;默认复用 Node 宿主。非 Node 宿主必须显式配置。 |
88
+
89
+ 配置无效不会退回 PATH。
90
+ Windows 环境变量名按不区分大小写读取。
91
+ 更换全局安装或 PATH 后应重启宿主,确保它继承新环境。
92
+
93
+ 两个 CLI 子进程各有 30 秒超时、每个输出流 4 MiB 上限,关闭 stdin、禁用 OpenSpec telemetry,并在返回检查结果前等待两者结束。
94
+ 校验报告的 exit 1 与 `valid: false` 被识别为 strict-invalid;无法启动、协议异常、超时、取消、输出超限分别给出安全诊断。
95
+ 诊断只暴露 command、phase、reason、允许列表中的 systemCode 和 exitCode,不返回原始 stderr、绝对路径或环境变量。
96
+ inspection 不可用时不再级联误报 traceability 输入缺失,且绝不生成成功的 ready 收据。
97
+
98
+ 平台契约 CI 使用 Linux、Windows、macOS × Node 22.13.0/24.13.0,显式安装 OpenSpec 1.5.0,验证真实 npm 全局入口、严格校验、Design finalization、失败重试和 proof-bound delivery 加载。
99
+ 状态协议要求布尔字段 `isComplete`,用于判断 1.5.0 的规划产物是否齐全;若另有 `isPlanningComplete`,该字段也必须为布尔值,且两者均为 `true` 才视为规划完成。
100
+ CLI 的其他版本必须满足相同 JSON 协议;新增支持版本应加入契约测试。
101
+
102
+ **此矩阵不代表原生 Implement 隔离已全平台可用。**
103
+ 当前隔离后端仍为 Linux Bubblewrap;Windows/macOS 的原生隔离、ACL 与完整文件应用语义需要单独实现和验收,缺少隔离能力时保持暂停,不降级为主工作区直接执行。
104
+
105
+ 本地可用 `CADENCE_REAL_OPENSPEC=1` 启用真实 CLI 测试(Windows 可用 PowerShell 设置 `$env:CADENCE_REAL_OPENSPEC = "1"`),然后运行:
106
+
107
+ ```sh
108
+ bun run test:target test/openspec-cli.test.ts test/design-delivery.integration.test.ts
109
+ ```
110
+
111
+ 未启用时,真实 CLI 用例明确跳过,其他适配层回归测试照常执行。
112
+ `bun run check` 的 JS 语法扫描使用 Node 遍历,不依赖 Unix `find`/`xargs`。
113
+
74
114
  ## Worker 路由(Worker routes)
75
115
 
76
116
  路由配置是可选的:没有项目级或用户级文件时,三个包内 Agent 默认继承当前父模型,因此首次 Design、Implement 或 Diagnose 不需要预先配置 endpoint。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "pack:check": "node scripts/pack-check.mjs",
41
41
  "traceability:check": "node scripts/traceability-check.mjs",
42
42
  "verify": "bun run check && bun run lint && bun run test && bun run pack:check",
43
- "check": "find skills test scripts -name '*.mjs' -type f -print0 | xargs -0 -r -n1 node --check && tsc --noEmit",
43
+ "check": "node scripts/check-syntax.mjs && tsc --noEmit",
44
44
  "lint": "biome check . && rumdl check .",
45
45
  "test": "vitest run",
46
46
  "test:target": "vitest run",
@@ -27,6 +27,7 @@ import {
27
27
  DesignJournal,
28
28
  type DesignStatusProjection,
29
29
  } from "./design-journal.ts";
30
+ import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
30
31
  import { RunStore } from "./run-store.ts";
31
32
  import { observeSafePath } from "./safe-path.ts";
32
33
  import type { ResolvedStateRoot } from "./state-root.ts";
@@ -131,11 +132,17 @@ export interface DesignControllerOptions {
131
132
  export class DesignFinalizationError extends Error {
132
133
  readonly diagnostics: readonly string[];
133
134
 
134
- constructor(diagnostics: readonly string[]) {
135
+ readonly openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>;
136
+
137
+ constructor(
138
+ diagnostics: readonly string[],
139
+ openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>,
140
+ ) {
135
141
  const normalized = [...new Set(diagnostics)].sort();
136
142
  super("design-finalization-invalid");
137
143
  this.name = "DesignFinalizationError";
138
144
  this.diagnostics = Object.freeze(normalized);
145
+ this.openSpecDiagnostic = openSpecDiagnostic;
139
146
  }
140
147
  }
141
148
 
@@ -456,12 +463,21 @@ export class DesignController {
456
463
  private constructor(options: DesignControllerOptions) {
457
464
  this.consumerRoot = path.resolve(options.consumerRoot);
458
465
  this.#runs = RunStore.open(options.stateRoot);
459
- this.#journal = DesignJournal.open(options.stateRoot, {
460
- ...(options.now ? { now: options.now } : {}),
461
- ...(options.finalizationLeaseMs !== undefined
462
- ? { finalizationLeaseMs: options.finalizationLeaseMs }
463
- : {}),
464
- });
466
+ try {
467
+ this.#journal = DesignJournal.open(options.stateRoot, {
468
+ ...(options.now ? { now: options.now } : {}),
469
+ ...(options.finalizationLeaseMs !== undefined
470
+ ? { finalizationLeaseMs: options.finalizationLeaseMs }
471
+ : {}),
472
+ });
473
+ } catch (error) {
474
+ try {
475
+ this.#runs.close();
476
+ } catch {
477
+ // The Design journal initialization failure remains authoritative.
478
+ }
479
+ throw error;
480
+ }
465
481
  this.#inspectOpenSpec = options.inspectOpenSpec;
466
482
  }
467
483
 
@@ -588,18 +604,32 @@ export class DesignController {
588
604
 
589
605
  #start(request: Extract<DesignControlRequest, { operation: "start" }>) {
590
606
  const fromRequirement = "requirement" in request;
591
- const run = this.#runs.startRun({
592
- stage: "abel-design",
593
- ...(fromRequirement
594
- ? {
595
- provisionalKey: transientHash(
596
- "abel-design-requirement",
597
- request.requirement,
598
- ),
599
- }
600
- : { change: request.change }),
601
- operationId: request.operationId,
602
- });
607
+ let run: ReturnType<RunStore["startRun"]>;
608
+ try {
609
+ run = this.#runs.startRun({
610
+ stage: "abel-design",
611
+ ...(fromRequirement
612
+ ? {
613
+ provisionalKey: transientHash(
614
+ "abel-design-requirement",
615
+ request.requirement,
616
+ ),
617
+ provisionalAliases: [
618
+ transientHash(
619
+ "abel-design-requirement-v1",
620
+ request.requirement,
621
+ ),
622
+ ],
623
+ }
624
+ : { change: request.change }),
625
+ operationId: request.operationId,
626
+ });
627
+ } catch (error) {
628
+ if (error instanceof Error && error.message === "operation-id-conflict") {
629
+ throw new Error("design-operation-conflict", { cause: error });
630
+ }
631
+ throw error;
632
+ }
603
633
  if (run.state === "created") {
604
634
  this.#runs.transition({
605
635
  runId: run.runId,
@@ -911,9 +941,12 @@ export class DesignController {
911
941
  removeSafeFile(this.consumerRoot, readyPath);
912
942
  const diagnostics = new Set<string>();
913
943
  let inspection: DesignOpenSpecInspection | undefined;
944
+ let openSpecDiagnostic: Readonly<OpenSpecDiagnostic> | undefined;
914
945
  try {
915
946
  inspection = await this.#inspectOpenSpec(this.consumerRoot, change);
916
- } catch {
947
+ } catch (error) {
948
+ if (error instanceof OpenSpecCliError)
949
+ openSpecDiagnostic = error.diagnostic;
917
950
  diagnostics.add("design-openspec-unavailable");
918
951
  }
919
952
  if (inspection) {
@@ -981,7 +1014,7 @@ export class DesignController {
981
1014
  let traceability:
982
1015
  | ReturnType<typeof assessDeliveryTraceability>
983
1016
  | undefined;
984
- if (compiled) {
1017
+ if (compiled && inspection) {
985
1018
  const tasks = artifactBytes.get("tasks.md");
986
1019
  const specs = [...artifactBytes]
987
1020
  .filter(
@@ -1012,7 +1045,7 @@ export class DesignController {
1012
1045
  !traceability?.ok ||
1013
1046
  diagnostics.size > 0
1014
1047
  ) {
1015
- throw new DesignFinalizationError([...diagnostics]);
1048
+ throw new DesignFinalizationError([...diagnostics], openSpecDiagnostic);
1016
1049
  }
1017
1050
  const gateAReceipt = compileGateAReceipt({
1018
1051
  change,
@@ -317,12 +317,21 @@ export class DesignJournal {
317
317
  }
318
318
  const stateRoot = prepareStateRoot(unresolved);
319
319
  const database = new DatabaseSync(stateRoot.databasePath);
320
- database.exec("PRAGMA foreign_keys = ON");
321
- database.exec("PRAGMA journal_mode = WAL");
322
- database.exec("PRAGMA synchronous = FULL");
323
- database.exec("PRAGMA busy_timeout = 5000");
324
- database.exec(SCHEMA);
325
- return new DesignJournal(stateRoot, database, options);
320
+ try {
321
+ database.exec("PRAGMA foreign_keys = ON");
322
+ database.exec("PRAGMA journal_mode = WAL");
323
+ database.exec("PRAGMA synchronous = FULL");
324
+ database.exec("PRAGMA busy_timeout = 5000");
325
+ database.exec(SCHEMA);
326
+ return new DesignJournal(stateRoot, database, options);
327
+ } catch (error) {
328
+ try {
329
+ database.close();
330
+ } catch {
331
+ // The Design journal initialization failure remains authoritative.
332
+ }
333
+ throw error;
334
+ }
326
335
  }
327
336
 
328
337
  #assertOpen(): void {
@@ -337,7 +346,11 @@ export class DesignJournal {
337
346
  this.#database.exec("COMMIT");
338
347
  return result;
339
348
  } catch (error) {
340
- this.#database.exec("ROLLBACK");
349
+ try {
350
+ this.#database.exec("ROLLBACK");
351
+ } catch {
352
+ // The original operation or commit failure remains authoritative.
353
+ }
341
354
  throw error;
342
355
  }
343
356
  }
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  // workflow routing (abel-design/implement/diagnose provenance) in the prompts
5
5
  // integration; abel-init and ordinary prompts never activate dispatch.
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
- import { execFile } from "node:child_process";
8
7
  import { createHash } from "node:crypto";
9
8
  import {
10
9
  lstatSync,
@@ -17,7 +16,6 @@ import {
17
16
  import { homedir } from "node:os";
18
17
  import path, { dirname, join } from "node:path";
19
18
  import { fileURLToPath } from "node:url";
20
- import { promisify } from "node:util";
21
19
  import type {
22
20
  AgentToolResult,
23
21
  AgentToolUpdateCallback,
@@ -59,6 +57,10 @@ import {
59
57
  } from "./design-control.ts";
60
58
  import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
61
59
  import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
60
+ import {
61
+ inspectOpenSpecDelivery,
62
+ type OpenSpecDeliveryInspection,
63
+ } from "./openspec-cli.ts";
62
64
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
63
65
  import { ParentPayloadBridge } from "./parent-payload-bridge.ts";
64
66
  import { runtimeForWorkerRoute } from "./parent-provider.ts";
@@ -69,9 +71,9 @@ import {
69
71
  unavailableRoutePolicy,
70
72
  type WorkerRoutePolicy,
71
73
  } from "./route-policy.ts";
72
- import { RunStoreFormatError } from "./run-store.ts";
74
+ import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
73
75
  import { observeSafePath } from "./safe-path.ts";
74
- import { resolveStateRoot } from "./state-root.ts";
76
+ import { resolveStateRoot, StateRootError } from "./state-root.ts";
75
77
  import {
76
78
  ACTIVITY_DETAILS_KEY,
77
79
  ActivityController,
@@ -91,6 +93,11 @@ import {
91
93
  type WorkflowDeliverySource,
92
94
  } from "./workflow-engine.ts";
93
95
 
96
+ export {
97
+ inspectOpenSpecDelivery,
98
+ type OpenSpecDeliveryInspection,
99
+ } from "./openspec-cli.ts";
100
+
94
101
  export const DISPATCH_TOOL = "abel_dispatch";
95
102
  const DESIGN_PARENT_READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
96
103
 
@@ -431,25 +438,174 @@ type SafeDesignFailure = {
431
438
  kind: "design-control-failure";
432
439
  operation: string;
433
440
  code: string;
434
- diagnostics: ReadonlyArray<Record<string, string>>;
441
+ diagnostics: ReadonlyArray<Record<string, string | boolean>>;
435
442
  };
436
443
 
444
+ type DesignFailureBoundary =
445
+ | "dispatch"
446
+ | "initialization"
447
+ | "execution"
448
+ | "serialization";
449
+
450
+ function sqliteLocked(error: unknown): boolean {
451
+ return (
452
+ typeof error === "object" &&
453
+ error !== null &&
454
+ "errcode" in error &&
455
+ ((error as { errcode?: unknown }).errcode === 5 ||
456
+ (error as { errcode?: unknown }).errcode === 6)
457
+ );
458
+ }
459
+
460
+ function sqliteUnavailable(error: unknown): boolean {
461
+ if (typeof error !== "object" || error === null) return false;
462
+ const errcode = Number((error as { errcode?: unknown }).errcode);
463
+ const code = (error as { code?: unknown }).code;
464
+ return (
465
+ [8, 10, 13, 14, 23, 24].includes(errcode) ||
466
+ (typeof code === "string" &&
467
+ [
468
+ "EACCES",
469
+ "EBUSY",
470
+ "EMFILE",
471
+ "ENFILE",
472
+ "ENOSPC",
473
+ "EPERM",
474
+ "EROFS",
475
+ ].includes(code))
476
+ );
477
+ }
478
+
479
+ function classifyDesignFailure(
480
+ error: unknown,
481
+ operation: string,
482
+ boundary: DesignFailureBoundary,
483
+ ): {
484
+ code: string;
485
+ category: string;
486
+ retryable: boolean;
487
+ field?: string;
488
+ } {
489
+ const message = error instanceof Error ? error.message : "";
490
+ if (error instanceof DesignPlanValidationError) {
491
+ return {
492
+ code: "design-plan-validation-invalid",
493
+ category: "validation",
494
+ retryable: false,
495
+ };
496
+ }
497
+ if (
498
+ error instanceof RunStoreMigrationError ||
499
+ error instanceof RunStoreFormatError
500
+ ) {
501
+ return {
502
+ code: "design-store-migration-failed",
503
+ category: "storage",
504
+ retryable: false,
505
+ field: "schemaVersion",
506
+ };
507
+ }
508
+ if (sqliteLocked(error)) {
509
+ return {
510
+ code: "design-store-locked",
511
+ category: "storage",
512
+ retryable: true,
513
+ };
514
+ }
515
+ if (sqliteUnavailable(error)) {
516
+ return {
517
+ code: "design-store-unavailable",
518
+ category: "storage",
519
+ retryable: false,
520
+ };
521
+ }
522
+ if (error instanceof StateRootError) {
523
+ return {
524
+ code: "design-store-unavailable",
525
+ category: "storage",
526
+ retryable: false,
527
+ };
528
+ }
529
+ if (
530
+ message === "design-operation-conflict" ||
531
+ message === "operation-id-conflict"
532
+ ) {
533
+ return {
534
+ code: "design-operation-conflict",
535
+ category: "control",
536
+ retryable: false,
537
+ field: "operationId",
538
+ };
539
+ }
540
+ if (message === "stage-control-mismatch") {
541
+ return {
542
+ code: "design-stage-inactive",
543
+ category: "stage",
544
+ retryable: false,
545
+ };
546
+ }
547
+ if (message === "invalid-design-control-request") {
548
+ return {
549
+ code: "invalid-design-control-request",
550
+ category: "validation",
551
+ retryable: false,
552
+ };
553
+ }
554
+ if (boundary === "serialization") {
555
+ return {
556
+ code: "design-receipt-serialization-failed",
557
+ category: "control",
558
+ retryable: false,
559
+ };
560
+ }
561
+ if (boundary === "initialization") {
562
+ return {
563
+ code: "design-store-unavailable",
564
+ category: "storage",
565
+ retryable: false,
566
+ };
567
+ }
568
+ if (
569
+ error instanceof DesignFinalizationError ||
570
+ error instanceof DeliveryValidationError ||
571
+ (message.startsWith("design-") && SAFE_DESIGN_ERROR_CODE.test(message))
572
+ ) {
573
+ return { code: message, category: "control", retryable: false };
574
+ }
575
+ if (operation === "start") {
576
+ return {
577
+ code: "design-run-create-failed",
578
+ category: "storage",
579
+ retryable: false,
580
+ };
581
+ }
582
+ return {
583
+ code: "design-control-internal",
584
+ category: "control",
585
+ retryable: false,
586
+ };
587
+ }
588
+
437
589
  function safeDesignFailure(
438
590
  error: unknown,
439
591
  operation: string,
592
+ boundary: DesignFailureBoundary = "dispatch",
440
593
  ): SafeDesignFailure {
441
- const message = error instanceof Error ? error.message : "";
442
- const code =
443
- error instanceof DesignPlanValidationError
444
- ? "design-plan-validation-invalid"
445
- : SAFE_DESIGN_ERROR_CODE.test(message)
446
- ? message
447
- : "design-control-failed";
594
+ const classified = classifyDesignFailure(error, operation, boundary);
448
595
  const rawDiagnostics: readonly unknown[] =
449
596
  error instanceof DesignFinalizationError ||
450
597
  error instanceof DesignPlanValidationError ||
451
598
  error instanceof DeliveryValidationError
452
- ? error.diagnostics
599
+ ? error instanceof DesignFinalizationError
600
+ ? [
601
+ ...error.diagnostics.filter(
602
+ (code) =>
603
+ !error.openSpecDiagnostic ||
604
+ code !== "design-openspec-unavailable",
605
+ ),
606
+ ...(error.openSpecDiagnostic ? [error.openSpecDiagnostic] : []),
607
+ ]
608
+ : error.diagnostics
453
609
  : [];
454
610
  const diagnostics = rawDiagnostics
455
611
  .flatMap((candidate) => {
@@ -468,8 +624,11 @@ function safeDesignFailure(
468
624
  }
469
625
  const admitted = Object.fromEntries(
470
626
  Object.entries(candidate as Record<string, unknown>)
471
- .filter(
472
- (entry): entry is [string, string] =>
627
+ .filter((entry): entry is [string, string | boolean] => {
628
+ if (entry[0] === "retryable") {
629
+ return typeof entry[1] === "boolean";
630
+ }
631
+ return (
473
632
  [
474
633
  "code",
475
634
  "taskId",
@@ -482,10 +641,15 @@ function safeDesignFailure(
482
641
  "dependencyTaskId",
483
642
  "producerTaskId",
484
643
  "producerPhase",
644
+ "command",
645
+ "reason",
646
+ "systemCode",
647
+ "exitCode",
485
648
  ].includes(entry[0]) &&
486
649
  typeof entry[1] === "string" &&
487
- SAFE_DESIGN_ERROR_CODE.test(entry[1]),
488
- )
650
+ SAFE_DESIGN_ERROR_CODE.test(entry[1])
651
+ );
652
+ })
489
653
  .sort(([left], [right]) => left.localeCompare(right)),
490
654
  );
491
655
  return typeof admitted.code === "string" ? [admitted] : [];
@@ -499,10 +663,18 @@ function safeDesignFailure(
499
663
  canonicalJson(candidate) !== canonicalJson(values[index - 1]),
500
664
  )
501
665
  .slice(0, MAX_DESIGN_ERROR_DIAGNOSTICS);
666
+ if (diagnostics.length === 0) {
667
+ diagnostics.push({
668
+ code: classified.code,
669
+ category: classified.category,
670
+ retryable: classified.retryable,
671
+ ...(classified.field ? { field: classified.field } : {}),
672
+ });
673
+ }
502
674
  return {
503
675
  kind: "design-control-failure",
504
676
  operation: SAFE_DESIGN_ERROR_CODE.test(operation) ? operation : "unknown",
505
- code,
677
+ code: classified.code,
506
678
  diagnostics,
507
679
  };
508
680
  }
@@ -529,15 +701,6 @@ export type WorkflowControlEngineFactory = (
529
701
  ) => WorkflowControlEngine | Promise<WorkflowControlEngine>;
530
702
 
531
703
  const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;
532
- const execFileAsync = promisify(execFile);
533
-
534
- export interface OpenSpecDeliveryInspection {
535
- change: string;
536
- schema: string;
537
- planningComplete: boolean;
538
- strictValid: boolean;
539
- artifactPaths: string[];
540
- }
541
704
 
542
705
  export interface PackageDeliverySourceOptions {
543
706
  inspectOpenSpec?: (
@@ -631,96 +794,6 @@ function readPackageDeliveryFile(
631
794
  return readFileSync(target);
632
795
  }
633
796
 
634
- function deliveryRecord(value: unknown): Record<string, unknown> | undefined {
635
- return value && typeof value === "object" && !Array.isArray(value)
636
- ? (value as Record<string, unknown>)
637
- : undefined;
638
- }
639
-
640
- function parseCommandJson(
641
- value: string,
642
- code: string,
643
- ): Record<string, unknown> {
644
- let parsed: unknown;
645
- try {
646
- parsed = JSON.parse(value);
647
- } catch {
648
- throw new Error(code);
649
- }
650
- const record = deliveryRecord(parsed);
651
- if (!record) throw new Error(code);
652
- return record;
653
- }
654
-
655
- export async function inspectOpenSpecDelivery(
656
- consumerRoot: string,
657
- change: string,
658
- ): Promise<OpenSpecDeliveryInspection> {
659
- const [statusExecution, validationExecution] = await Promise.all([
660
- execFileAsync("openspec", ["status", "--change", change, "--json"], {
661
- cwd: consumerRoot,
662
- encoding: "utf8",
663
- maxBuffer: 4 * 1024 * 1024,
664
- }),
665
- execFileAsync(
666
- "openspec",
667
- ["validate", change, "--strict", "--json", "--no-interactive"],
668
- {
669
- cwd: consumerRoot,
670
- encoding: "utf8",
671
- maxBuffer: 4 * 1024 * 1024,
672
- },
673
- ),
674
- ]);
675
- const status = parseCommandJson(
676
- statusExecution.stdout,
677
- "delivery-openspec-status-invalid",
678
- );
679
- const validation = parseCommandJson(
680
- validationExecution.stdout,
681
- "delivery-openspec-validation-invalid",
682
- );
683
- const changeRoot = path.resolve(consumerRoot, "openspec", "changes", change);
684
- const artifactPathsRecord = deliveryRecord(status.artifactPaths);
685
- const artifactPaths = artifactPathsRecord
686
- ? Object.values(artifactPathsRecord).flatMap((entry) => {
687
- const record = deliveryRecord(entry);
688
- if (!record || !Array.isArray(record.existingOutputPaths)) return [];
689
- return record.existingOutputPaths.flatMap((candidate) => {
690
- if (typeof candidate !== "string") return [];
691
- const resolved = path.resolve(candidate);
692
- const relative = path.relative(changeRoot, resolved);
693
- return relative &&
694
- !relative.startsWith(`..${path.sep}`) &&
695
- relative !== ".." &&
696
- !path.isAbsolute(relative)
697
- ? [relative.split(path.sep).join("/")]
698
- : [];
699
- });
700
- })
701
- : [];
702
- const items = Array.isArray(validation.items) ? validation.items : [];
703
- const strictValid =
704
- items.length === 1 &&
705
- deliveryRecord(items[0])?.id === change &&
706
- deliveryRecord(items[0])?.valid === true;
707
- if (
708
- status.changeName !== change ||
709
- typeof status.schemaName !== "string" ||
710
- artifactPaths.length === 0
711
- ) {
712
- throw new Error("delivery-openspec-status-invalid");
713
- }
714
- return {
715
- change,
716
- schema: status.schemaName,
717
- planningComplete:
718
- status.isPlanningComplete === true && status.isComplete === true,
719
- strictValid,
720
- artifactPaths: [...new Set(artifactPaths)].sort(),
721
- };
722
- }
723
-
724
797
  export function packageDeliverySource(
725
798
  consumerRoot: string,
726
799
  options: PackageDeliverySourceOptions = {},
@@ -2082,18 +2155,19 @@ export function registerWorkflowControl(
2082
2155
  }
2083
2156
  const designRequest = validateDesignControlRequest(record.request);
2084
2157
  if (!designRequest.ok) throw new Error(designRequest.code);
2085
- let engine: WorkflowControlEngine;
2086
- try {
2087
- engine = await engineFor(ctx);
2088
- } catch (error) {
2089
- if (error instanceof RunStoreFormatError)
2090
- throw runStoreResetError(error);
2091
- throw error;
2092
- }
2093
- if (!engine.executeDesign)
2094
- throw new Error("design-control-unavailable");
2158
+ let boundary: DesignFailureBoundary = "initialization";
2095
2159
  try {
2160
+ const engine = await engineFor(ctx);
2161
+ if (!engine.executeDesign)
2162
+ throw new Error("design-control-unavailable");
2163
+ boundary = "execution";
2096
2164
  const payload = await engine.executeDesign(designRequest.value);
2165
+ boundary = "serialization";
2166
+ const serialized = JSON.stringify(payload);
2167
+ if (serialized === undefined) {
2168
+ throw new Error("design-receipt-serialization-failed");
2169
+ }
2170
+ boundary = "execution";
2097
2171
  if (
2098
2172
  designRequest.value.operation === "finalize-delivery" &&
2099
2173
  payload.state === "completed"
@@ -2101,15 +2175,14 @@ export function registerWorkflowControl(
2101
2175
  await deactivateStage();
2102
2176
  }
2103
2177
  return {
2104
- content: [
2105
- { type: "text" as const, text: JSON.stringify(payload) },
2106
- ],
2178
+ content: [{ type: "text" as const, text: serialized }],
2107
2179
  details: payload,
2108
2180
  };
2109
2181
  } catch (error) {
2110
2182
  const failure = safeDesignFailure(
2111
2183
  error,
2112
2184
  designRequest.value.operation,
2185
+ boundary,
2113
2186
  );
2114
2187
  designFailures.set(toolCallId, failure);
2115
2188
  const sanitized = new Error(failure.code);