@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 +40 -0
- package/package.json +2 -2
- package/src/design-control.ts +55 -22
- package/src/design-journal.ts +20 -7
- package/src/index.ts +203 -130
- package/src/openspec-cli.ts +494 -0
- package/src/run-store.ts +221 -25
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.
|
|
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": "
|
|
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",
|
package/src/design-control.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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,
|
package/src/design-journal.ts
CHANGED
|
@@ -317,12 +317,21 @@ export class DesignJournal {
|
|
|
317
317
|
}
|
|
318
318
|
const stateRoot = prepareStateRoot(unresolved);
|
|
319
319
|
const database = new DatabaseSync(stateRoot.databasePath);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|