@abelxiaoxing/cadence 1.5.1 → 1.5.3
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 +62 -0
- package/package.json +1 -1
- package/src/change-contract.ts +71 -20
- package/src/contracts.ts +112 -5
- package/src/delivery-compiler.ts +27 -0
- package/src/design-control.ts +1 -0
- package/src/design-diagnostics.ts +29 -0
- package/src/index.ts +4 -1
- package/src/operator-cli.mjs +34 -9
- package/src/operator-tools.ts +75 -21
package/README.md
CHANGED
|
@@ -31,6 +31,9 @@ node /absolute/path/to/cadence/src/operator-cli.mjs runs /path/to/consumer
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
`doctor` 检查 Git、Node、OpenSpec 入口、实际 Bubblewrap 启动、请求预算和常用脚本静态能力;不调用模型、不执行产品测试、不保证所有集成服务均可用。
|
|
34
|
+
包管理器优先采用 `package.json.packageManager`,其次推断唯一锁文件所属的管理器;没有声明和锁文件时明确标为 npm 默认值。
|
|
35
|
+
输出包含 `selectedPackageManager`、`selectionSource`、`lockfiles` 和 `ambiguousLockfiles`;多个管理器锁文件且无明确声明时阻塞脚本探测,不静默选择 Bun。
|
|
36
|
+
Doctor 不读取私有计划,计划中的显式 runner 仍由正式预检检查;请让项目声明与计划保持一致。
|
|
34
37
|
`runs` 只读列出该项目最多 1,000 个 run 和有界存储占用;不会创建、迁移或删除数据库,不自动清理暂停任务。
|
|
35
38
|
|
|
36
39
|
### 可信本地项目:显式复用宿主环境
|
|
@@ -87,6 +90,7 @@ Gate、交付校验和执行状态转换由控制面代码保证。
|
|
|
87
90
|
当前任务的 Gate 回答与续轮保持阶段有效;用户结束工作流或转向无关任务时,父模型先调用 `{"action":"finish"}`,扩展等待活动操作停止并恢复原工具,保留可恢复进度。
|
|
88
91
|
此退出同样适用于 Implement,不表示完成或丢弃。
|
|
89
92
|
显式调用 Init 也会先退出已有阶段。
|
|
93
|
+
Design 同任务续轮重新收紧允许的工具集合,预检失败或暂停不自动退出;会话重载/恢复不继承阶段激活权限,需重新显式调用对应命令,已保存的 run 不会因此完成或丢弃。
|
|
90
94
|
|
|
91
95
|
Implement 只暴露 `start`、`status`、`resume`、`rebind`、`cancel` 和 `discard` 控制命令。
|
|
92
96
|
`status` 完全本地可用;相同 operation id 幂等重放,进程、会话或 Worker 更换后仍从 durable checkpoint 继续。
|
|
@@ -99,6 +103,12 @@ Design 从第一步起统一使用 `action: "design"`:新需求通过 `start(r
|
|
|
99
103
|
内联原子验证也可省略 `id` / `classification`;任务公共 `read` 与阶段读取合并,固定 baseline/repair 保护字段由编译器补齐,恢复次数和 AGENTS 影响仍需作者明确选择。
|
|
100
104
|
[多任务示例](config/plan-draft.multiple-tasks.example.json) 保留显式依赖和产物 producer;ordered steps 仍使用完整内联合同。
|
|
101
105
|
`validate-plan-draft` 返回有界的最终权限、验证用途、恢复次数和实际派生来源摘要;摘要不充当批准或封存证明。
|
|
106
|
+
编译结果的 `checks` 分开报告 `structure`、`verificationCapability`、`contractCoverage` 和 `sealing`;静态 `closure.executable: true` 不代表产品测试已运行或 `READY_TO_IMPLEMENT`。
|
|
107
|
+
独立 `compileImplementPlan` 未传合同则 `contractCoverage: "not-checked"`;传入合同也不证明该合同已获批准。
|
|
108
|
+
正式预检从私有 journal 注入当前 Gate A 合同并报告 `gateA: "current"`,但仍是 `sealing: "not-performed"`;只有正式控制面可以批准和封存交付。
|
|
109
|
+
验收缺失诊断列出 acceptance ID;仅当验证 ID 唯一对应一个不同义务时返回字段差异,不猜测最接近的验证器,也不回显命令或参数。
|
|
110
|
+
`changedSurfaces` 仅接受 `none`、`route-authorization`、`page-state`、`api-response`、`public-html`;公共影响不得改标为 `none` 来绕过闭包。
|
|
111
|
+
`verification.change.affected` 是固定标记 `"task-affected-contracts"`,不能使用 `{ "use": "..." }`;`affectedSuite` 中公共影响测试必须在 `relatedTests` 声明证据和实际 owner。
|
|
102
112
|
推导有歧义时拒绝编译,显式错误仍会报错;代码不会据此扩大路径、依赖或产物权限,封存计划保持完整、严格的结构。
|
|
103
113
|
预检集中返回不同任务的独立结构错误,并提供字段位置、期望/实际路径和修正提示;工具反馈与 TUI 共用有界脱敏投影。
|
|
104
114
|
Design status 将可调用的 `legalOperations` 与顶层 `packetActions` 分开;显式退出只能发送 `{"action":"finish"}`,不能伪装成 `operation: "finish"`。
|
|
@@ -140,6 +150,58 @@ Gate 等待和可恢复暂停保持激活以接收直接后续操作。
|
|
|
140
150
|
暂停会保留最小结构化恢复事实;完成或显式 discard 后安全清理。
|
|
141
151
|
不会持久化凭据、环境值、原始 prompt、隐藏推理、完整子会话或原始模型输出。
|
|
142
152
|
|
|
153
|
+
### 公共 UI 的 impactClosure 作者格式
|
|
154
|
+
|
|
155
|
+
`page-state` 与 `public-html` 可以同时声明。
|
|
156
|
+
以下是 **PlanDraft 任务片段**,合并到现有 `tasks[i]`,不是完整计划;其余 Gate A、阶段、产物和验证合同仍按单任务示例填写。
|
|
157
|
+
路径和证据仅用于说明,必须替换为仓库真实调查结果。
|
|
158
|
+
假设 Red 修改 `test/page-state.mjs`,已有 `test/public-html.mjs` 只读保留:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"read": ["test/page-state.mjs", "test/public-html.mjs"],
|
|
163
|
+
"impactClosure": {
|
|
164
|
+
"changedSurfaces": ["page-state", "public-html"],
|
|
165
|
+
"searchEvidence": [
|
|
166
|
+
"检索页面状态的调用点及 test/、tests/ 中的覆盖:test/page-state.mjs 需更新状态切换断言,test/public-html.mjs 覆盖现有公开 HTML。"
|
|
167
|
+
],
|
|
168
|
+
"relatedTests": [
|
|
169
|
+
{
|
|
170
|
+
"path": "test/page-state.mjs",
|
|
171
|
+
"evidence": "本任务 Red 更新页面状态切换回归;由阶段 write 推导为 current-task。"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"path": "test/public-html.mjs",
|
|
175
|
+
"evidence": "保留已有 HTML 断言并执行受影响验证;无任务写入,推导为 unaffected。"
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"affectedSuite": ["test/page-state.mjs", "test/public-html.mjs"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
完整规则:
|
|
184
|
+
|
|
185
|
+
- `impactClosure` 恰好包含 `changedSurfaces`、`searchEvidence`、`relatedTests`、`affectedSuite` 四个字段,不接受任意扩展字段。
|
|
186
|
+
- `changedSurfaces` 是非空、无重复的合法枚举数组;`none` 不能与其他值混用。
|
|
187
|
+
不得为了通过校验将公共影响改为 `none`。
|
|
188
|
+
- 存在公共影响时,后三个数组均不得为空;`searchEvidence` 每项为非空字符串,记录实际检索及结论,不是路径对象,也不是可省略的推导字段。
|
|
189
|
+
- `relatedTests` 每项包含 `path` 和非空字符串 `evidence`;路径无重复,必须是 `test/` 或 `tests/` 下的仓库相对文件路径,并位于本任务各阶段合并后的 read/write 范围。
|
|
190
|
+
`src/foo.test.ts` 不满足当前测试路径规则。
|
|
191
|
+
- PlanDraft 可省略 `disposition`:本任务写入则推导为 `current-task`,只有另一任务写入则为 `regression-task` 并推导 `regressionTaskId`,没有任务写入则为 `unaffected`;多个其他写入者时必须消除歧义。
|
|
192
|
+
`unaffected` 表示不修改该测试,不表示不运行该测试。
|
|
193
|
+
- 显式作者格式为 `{ "path": "test/page-state.mjs", "disposition": "current-task", "evidence": "实际证据" }`;只读保留用 `unaffected`。
|
|
194
|
+
跨任务格式为 `{ "path": "test/page-state.mjs", "disposition": "regression-task", "evidence": "实际证据", "regressionTaskId": "ui-regression" }`。
|
|
195
|
+
只有 `regression-task` 携带 `regressionTaskId`;依赖、产物和阶段权限仍需按实际计划声明,不会由闭包新增。
|
|
196
|
+
- `affectedSuite` 是无重复的测试路径数组,每项必须有对应的 `relatedTests` 条目;具备快照的运行时校验还要求其中至少一个是已有普通测试文件,不能全靠未来新增测试充当已有证据。
|
|
197
|
+
- `affectedSuite` 是影响清单,不是验证器。
|
|
198
|
+
显式 `affectedVerification` 应实际运行这些测试,例如 Vitest 的 `testFiles` 列出二者,或使用仓库真实的测试脚本。
|
|
199
|
+
仍需声明验证输入并遵守 Gate A,不能只补清单就宣称测试已运行。
|
|
200
|
+
|
|
201
|
+
`validate-plan-draft` 对三个空数组分别返回字段级 category:`search-evidence-required`、`related-tests-required`、`affected-suite-required`,并提供代码维护的 `hint`。
|
|
202
|
+
测试漏列仍返回 `impactClosure.affectedSuite.<index>` / `related-test-missing` 和具体 `path`。
|
|
203
|
+
空数组不足以确定应选哪些测试,因此不会虚构 `expectedPaths`;按实际证据补齐后,在同一个 Design run 重试预检。
|
|
204
|
+
|
|
143
205
|
## 内部边界与运行验证
|
|
144
206
|
|
|
145
207
|
扩展入口负责宿主生命周期与服务装配,交付加载和验证适配器分别由独立模块负责。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
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",
|
package/src/change-contract.ts
CHANGED
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
validateVerificationContract,
|
|
7
7
|
verificationInputPaths,
|
|
8
8
|
} from "./contracts.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type DesignPlanDiagnostic,
|
|
11
|
+
DesignPlanValidationError,
|
|
12
|
+
} from "./design-diagnostics.ts";
|
|
10
13
|
import { canonicalJson } from "./run-state.ts";
|
|
11
14
|
|
|
12
15
|
export interface ChangeContract {
|
|
@@ -151,25 +154,73 @@ export function assertPlanWithinChangeContract(
|
|
|
151
154
|
})[],
|
|
152
155
|
verifications: readonly StructuredVerificationContract[],
|
|
153
156
|
): void {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
for (const accepted of contract.acceptance)
|
|
171
|
-
if (
|
|
172
|
-
|
|
157
|
+
const candidates = [
|
|
158
|
+
...verifications,
|
|
159
|
+
...tasks.flatMap((task) => [
|
|
160
|
+
task.affectedVerification,
|
|
161
|
+
...Object.entries(task.phases)
|
|
162
|
+
.filter(
|
|
163
|
+
([phase]) =>
|
|
164
|
+
phase !== "red" ||
|
|
165
|
+
!task.verificationMode ||
|
|
166
|
+
task.verificationMode === "behavior",
|
|
167
|
+
)
|
|
168
|
+
.map(([, phase]) => phase.verification),
|
|
169
|
+
]),
|
|
170
|
+
];
|
|
171
|
+
const available = new Set(candidates.map(verificationObligation));
|
|
172
|
+
const diagnostics: DesignPlanDiagnostic[] = [];
|
|
173
|
+
for (const [index, accepted] of contract.acceptance.entries()) {
|
|
174
|
+
if (available.has(verificationObligation(accepted.verification))) continue;
|
|
175
|
+
// Only attribute a mismatch when the display ID selects one distinct
|
|
176
|
+
// obligation. Never guess a "closest" verifier or echo command/argument bytes.
|
|
177
|
+
const matching = [
|
|
178
|
+
...new Set(
|
|
179
|
+
candidates
|
|
180
|
+
.filter((candidate) => candidate.id === accepted.verification.id)
|
|
181
|
+
.map(verificationObligation),
|
|
182
|
+
),
|
|
183
|
+
];
|
|
184
|
+
const expected = JSON.parse(verificationObligation(accepted.verification));
|
|
185
|
+
const actual =
|
|
186
|
+
matching.length === 1 && matching[0]
|
|
187
|
+
? JSON.parse(matching[0])
|
|
188
|
+
: undefined;
|
|
189
|
+
diagnostics.push({
|
|
190
|
+
code: "change-contract-acceptance-missing",
|
|
191
|
+
acceptanceId: accepted.id,
|
|
192
|
+
verificationId: accepted.verification.id,
|
|
193
|
+
field: `changeContract.acceptance.${index}.verification`,
|
|
194
|
+
...(actual
|
|
195
|
+
? {
|
|
196
|
+
mismatchedFields: [
|
|
197
|
+
"kind",
|
|
198
|
+
"runner",
|
|
199
|
+
"packageManager",
|
|
200
|
+
"script",
|
|
201
|
+
"command",
|
|
202
|
+
"args",
|
|
203
|
+
"testFiles",
|
|
204
|
+
"minTests",
|
|
205
|
+
"classification",
|
|
206
|
+
"expectedFailure",
|
|
207
|
+
"steps",
|
|
208
|
+
]
|
|
209
|
+
.filter(
|
|
210
|
+
(key) =>
|
|
211
|
+
canonicalJson(expected[key] ?? null) !==
|
|
212
|
+
canonicalJson(actual[key] ?? null),
|
|
213
|
+
)
|
|
214
|
+
.sort(),
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (diagnostics.length)
|
|
220
|
+
throw new DesignPlanValidationError(
|
|
221
|
+
"change-contract-acceptance-missing",
|
|
222
|
+
diagnostics,
|
|
223
|
+
);
|
|
173
224
|
for (const task of tasks) {
|
|
174
225
|
if (
|
|
175
226
|
!contract.policy.verificationModes.includes(
|
package/src/contracts.ts
CHANGED
|
@@ -1233,6 +1233,7 @@ export type ImplementGraphContractDiagnostic =
|
|
|
1233
1233
|
path?: string;
|
|
1234
1234
|
expectedPaths?: string[];
|
|
1235
1235
|
actualPaths?: string[];
|
|
1236
|
+
allowedValues?: string[];
|
|
1236
1237
|
}
|
|
1237
1238
|
| {
|
|
1238
1239
|
code: "invalid-output-path";
|
|
@@ -1518,6 +1519,99 @@ function taskBoundaryContractDiagnostic(
|
|
|
1518
1519
|
: diagnostic("task", "contract");
|
|
1519
1520
|
}
|
|
1520
1521
|
|
|
1522
|
+
/** Refine rejected author input; this never admits a partial or repaired task. */
|
|
1523
|
+
function taskFieldDiagnostics(
|
|
1524
|
+
task: Record<string, unknown>,
|
|
1525
|
+
): ImplementGraphContractDiagnostic[] {
|
|
1526
|
+
const diagnostics: ImplementGraphContractDiagnostic[] = [];
|
|
1527
|
+
const add = (
|
|
1528
|
+
detail: Omit<
|
|
1529
|
+
Extract<
|
|
1530
|
+
ImplementGraphContractDiagnostic,
|
|
1531
|
+
{ code: "invalid-implement-graph" }
|
|
1532
|
+
>,
|
|
1533
|
+
"code"
|
|
1534
|
+
>,
|
|
1535
|
+
) => {
|
|
1536
|
+
if (diagnostics.length < 64)
|
|
1537
|
+
diagnostics.push({
|
|
1538
|
+
code: "invalid-implement-graph",
|
|
1539
|
+
...(validIdentifier(task.taskId) ? { taskId: task.taskId } : {}),
|
|
1540
|
+
...detail,
|
|
1541
|
+
});
|
|
1542
|
+
};
|
|
1543
|
+
if (task.phases && typeof task.phases === "object") {
|
|
1544
|
+
const phases = task.phases as Record<string, unknown>;
|
|
1545
|
+
for (const phase of IMPLEMENTATION_PHASES) {
|
|
1546
|
+
const boundary = phases[phase];
|
|
1547
|
+
if (!boundary || typeof boundary !== "object") continue;
|
|
1548
|
+
for (const field of ["read", "write", "delete"] as const) {
|
|
1549
|
+
const paths = (boundary as Record<string, unknown>)[field];
|
|
1550
|
+
if (!Array.isArray(paths)) continue;
|
|
1551
|
+
const seen = new Set<string>();
|
|
1552
|
+
for (const [index, file] of paths.entries()) {
|
|
1553
|
+
if (!isValidRelativePath(file)) continue;
|
|
1554
|
+
if (seen.has(file))
|
|
1555
|
+
add({
|
|
1556
|
+
phase,
|
|
1557
|
+
field: `phases.${phase}.${field}.${index}`,
|
|
1558
|
+
category: "duplicate-path",
|
|
1559
|
+
path: file,
|
|
1560
|
+
});
|
|
1561
|
+
seen.add(file);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (task.impactClosure && typeof task.impactClosure === "object") {
|
|
1567
|
+
const closure = task.impactClosure as Record<string, unknown>;
|
|
1568
|
+
if (Array.isArray(closure.changedSurfaces)) {
|
|
1569
|
+
for (const [index, surface] of closure.changedSurfaces.entries()) {
|
|
1570
|
+
if (!(IMPACT_SURFACES as readonly unknown[]).includes(surface))
|
|
1571
|
+
add({
|
|
1572
|
+
field: `impactClosure.changedSurfaces.${index}`,
|
|
1573
|
+
category: "enum",
|
|
1574
|
+
allowedValues: [...IMPACT_SURFACES],
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
const publicImpact = closure.changedSurfaces.some(
|
|
1578
|
+
(surface) =>
|
|
1579
|
+
surface !== "none" &&
|
|
1580
|
+
(IMPACT_SURFACES as readonly unknown[]).includes(surface),
|
|
1581
|
+
);
|
|
1582
|
+
if (publicImpact) {
|
|
1583
|
+
for (const [field, category] of [
|
|
1584
|
+
["searchEvidence", "search-evidence-required"],
|
|
1585
|
+
["relatedTests", "related-tests-required"],
|
|
1586
|
+
["affectedSuite", "affected-suite-required"],
|
|
1587
|
+
] as const) {
|
|
1588
|
+
if (Array.isArray(closure[field]) && closure[field].length === 0)
|
|
1589
|
+
add({ field: `impactClosure.${field}`, category });
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (
|
|
1593
|
+
publicImpact &&
|
|
1594
|
+
Array.isArray(closure.affectedSuite) &&
|
|
1595
|
+
Array.isArray(closure.relatedTests)
|
|
1596
|
+
) {
|
|
1597
|
+
const related = new Set(
|
|
1598
|
+
closure.relatedTests.flatMap((item) =>
|
|
1599
|
+
item && typeof item === "object" ? [item.path] : [],
|
|
1600
|
+
),
|
|
1601
|
+
);
|
|
1602
|
+
for (const [index, file] of closure.affectedSuite.entries())
|
|
1603
|
+
if (isValidRelativePath(file) && !related.has(file))
|
|
1604
|
+
add({
|
|
1605
|
+
field: `impactClosure.affectedSuite.${index}`,
|
|
1606
|
+
category: "related-test-missing",
|
|
1607
|
+
path: file,
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return diagnostics;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1521
1615
|
export function validateImplementGraphBoundary(value: unknown):
|
|
1522
1616
|
| { ok: true; value: ImplementGraphBoundary }
|
|
1523
1617
|
| {
|
|
@@ -1573,11 +1667,24 @@ export function validateImplementGraphBoundary(value: unknown):
|
|
|
1573
1667
|
{ changeId: value.changeId, ...task },
|
|
1574
1668
|
undefined,
|
|
1575
1669
|
);
|
|
1576
|
-
if (diagnostic)
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1670
|
+
if (diagnostic) {
|
|
1671
|
+
const fields = taskFieldDiagnostics(task);
|
|
1672
|
+
// Replace a broad diagnostic only when these details refine that field;
|
|
1673
|
+
// retain unrelated shape/authority failures in the same batch.
|
|
1674
|
+
if (
|
|
1675
|
+
!fields.some(
|
|
1676
|
+
(detail) =>
|
|
1677
|
+
diagnostic.field &&
|
|
1678
|
+
detail.field?.startsWith(`${diagnostic.field}.`),
|
|
1679
|
+
)
|
|
1680
|
+
)
|
|
1681
|
+
fields.push({ code: "invalid-implement-graph", ...diagnostic });
|
|
1682
|
+
for (const detail of fields)
|
|
1683
|
+
failures.push({
|
|
1684
|
+
reason: "invalid Implement task boundary",
|
|
1685
|
+
diagnostic: detail,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1581
1688
|
}
|
|
1582
1689
|
for (const candidate of value.outputs) {
|
|
1583
1690
|
if (
|
package/src/delivery-compiler.ts
CHANGED
|
@@ -264,6 +264,13 @@ export type {
|
|
|
264
264
|
export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
|
|
265
265
|
|
|
266
266
|
export interface CompiledDelivery {
|
|
267
|
+
/** Static checks only: neither compilation nor preflight seals a delivery. */
|
|
268
|
+
checks: {
|
|
269
|
+
structure: "passed";
|
|
270
|
+
verificationCapability: "passed";
|
|
271
|
+
contractCoverage: "passed" | "not-checked";
|
|
272
|
+
sealing: "not-performed";
|
|
273
|
+
};
|
|
267
274
|
plan: ImplementPlan;
|
|
268
275
|
bytes: Uint8Array;
|
|
269
276
|
rawSha256: string;
|
|
@@ -945,6 +952,20 @@ function normalizeAgentsCheckpointOperations(
|
|
|
945
952
|
}
|
|
946
953
|
|
|
947
954
|
function normalizeVerificationPlan(value: unknown): PlanVerification {
|
|
955
|
+
if (
|
|
956
|
+
isRecord(value) &&
|
|
957
|
+
isRecord(value.change) &&
|
|
958
|
+
value.change.affected !== "task-affected-contracts"
|
|
959
|
+
) {
|
|
960
|
+
throw new DesignPlanValidationError("delivery-verification-plan-invalid", [
|
|
961
|
+
{
|
|
962
|
+
code: "delivery-verification-plan-invalid",
|
|
963
|
+
field: "verification.change.affected",
|
|
964
|
+
category: "enum",
|
|
965
|
+
allowedValues: ["task-affected-contracts"],
|
|
966
|
+
},
|
|
967
|
+
]);
|
|
968
|
+
}
|
|
948
969
|
if (
|
|
949
970
|
!isRecord(value) ||
|
|
950
971
|
!exactKeys(value, [
|
|
@@ -1289,6 +1310,12 @@ export function compileImplementPlan(
|
|
|
1289
1310
|
const planHash = hashCanonicalValue(plan);
|
|
1290
1311
|
const closure = structuredClone(readiness.closure);
|
|
1291
1312
|
return {
|
|
1313
|
+
checks: {
|
|
1314
|
+
structure: "passed",
|
|
1315
|
+
verificationCapability: "passed",
|
|
1316
|
+
contractCoverage: plan.changeContract ? "passed" : "not-checked",
|
|
1317
|
+
sealing: "not-performed",
|
|
1318
|
+
},
|
|
1292
1319
|
plan,
|
|
1293
1320
|
bytes,
|
|
1294
1321
|
rawSha256,
|
package/src/design-control.ts
CHANGED
|
@@ -978,6 +978,7 @@ export class DesignController {
|
|
|
978
978
|
operation: request.operation,
|
|
979
979
|
runId: request.runId,
|
|
980
980
|
valid: true,
|
|
981
|
+
checks: { ...compiled.checks, gateA: "current" },
|
|
981
982
|
summary: planAuthoringSummary(compiled.plan, authoredDraft),
|
|
982
983
|
plan: {
|
|
983
984
|
taskCount: compiled.plan.tasks.length,
|
|
@@ -11,6 +11,7 @@ const IDENTITIES = new Set([
|
|
|
11
11
|
"category",
|
|
12
12
|
"owner",
|
|
13
13
|
"verificationId",
|
|
14
|
+
"acceptanceId",
|
|
14
15
|
"outputId",
|
|
15
16
|
"dependencyTaskId",
|
|
16
17
|
"producerTaskId",
|
|
@@ -22,6 +23,19 @@ const IDENTITIES = new Set([
|
|
|
22
23
|
]);
|
|
23
24
|
const HINTS = new Map<string, string>(
|
|
24
25
|
Object.entries({
|
|
26
|
+
"duplicate-path":
|
|
27
|
+
"Remove the repeated path at the indicated index; keep the existing declaration and do not widen phase permissions.",
|
|
28
|
+
enum: "Use one of allowedValues at the indicated field. For public impact, choose the actual changed surface; do not substitute none to bypass closure checks.",
|
|
29
|
+
"public-impact-incomplete":
|
|
30
|
+
"Public impact requires non-empty searchEvidence, relatedTests and affectedSuite; every affectedSuite path must have a relatedTests entry. Keep the actual changedSurfaces; do not substitute none.",
|
|
31
|
+
"search-evidence-required":
|
|
32
|
+
"Add non-empty searchEvidence strings describing actual searches of changed public surfaces, callers and existing tests. Do not invent evidence or substitute none for changedSurfaces.",
|
|
33
|
+
"related-tests-required":
|
|
34
|
+
"Add relatedTests entries with path and non-empty evidence for actual tests under test/ or tests/, within the task's declared read/write scope. In PlanDraft omit disposition to derive ownership from task writes; do not widen writes or substitute none.",
|
|
35
|
+
"affected-suite-required":
|
|
36
|
+
"Add actual affected test paths under test/ or tests/ to affectedSuite and declare each in relatedTests. Bind their execution in affectedVerification; retained snapshot validation requires at least one existing test. Do not invent paths or substitute none.",
|
|
37
|
+
"related-test-missing":
|
|
38
|
+
"Add this affected-suite test to relatedTests with evidence and its actual owner. Do not remove public impact or widen writes to satisfy ownership.",
|
|
25
39
|
"change-contract-acceptance-missing":
|
|
26
40
|
"Read the current approved ChangeContract from Design status and include each accepted verification obligation in the plan's phase, affected, full-suite or post-apply verification. Command, arguments, inputs and classification must match; copying only its ID is insufficient. Keep accepted IDs, statements and scope unchanged.",
|
|
27
41
|
"script-command-mismatch":
|
|
@@ -96,6 +110,18 @@ export function projectDesignDiagnostic(
|
|
|
96
110
|
: entry;
|
|
97
111
|
else if (key === "path" && safePath(entry)) result.path = entry;
|
|
98
112
|
else if (
|
|
113
|
+
(key === "allowedValues" || key === "mismatchedFields") &&
|
|
114
|
+
Array.isArray(entry)
|
|
115
|
+
) {
|
|
116
|
+
result[key] = [
|
|
117
|
+
...new Set(
|
|
118
|
+
entry.filter(
|
|
119
|
+
(item): item is string =>
|
|
120
|
+
typeof item === "string" && CODE.test(item),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
].slice(0, 32);
|
|
124
|
+
} else if (
|
|
99
125
|
(key === "expectedPaths" || key === "actualPaths") &&
|
|
100
126
|
Array.isArray(entry)
|
|
101
127
|
) {
|
|
@@ -120,6 +146,9 @@ export interface DesignPlanDiagnostic {
|
|
|
120
146
|
category?: string;
|
|
121
147
|
owner?: string;
|
|
122
148
|
verificationId?: string;
|
|
149
|
+
acceptanceId?: string;
|
|
150
|
+
allowedValues?: string[];
|
|
151
|
+
mismatchedFields?: string[];
|
|
123
152
|
outputId?: string;
|
|
124
153
|
dependencyTaskId?: string;
|
|
125
154
|
producerTaskId?: string;
|
package/src/index.ts
CHANGED
|
@@ -887,6 +887,8 @@ export function registerWorkflowControl(
|
|
|
887
887
|
pi.registerTool({
|
|
888
888
|
name: DISPATCH_TOOL,
|
|
889
889
|
label: "Abel Control",
|
|
890
|
+
promptSnippet:
|
|
891
|
+
"Control the explicitly activated Abel stage; retain paused work across same-task follow-ups.",
|
|
890
892
|
description:
|
|
891
893
|
kind === "command"
|
|
892
894
|
? 'Private stage-bound Abel workflow control. Accepts durable Implement commands, batch-bound {"action":"amend"} artifact revisions, or {"action":"finish"} to leave and preserve resumable work.'
|
|
@@ -1394,7 +1396,8 @@ export function registerWorkflowControl(
|
|
|
1394
1396
|
activePrompt = prompt;
|
|
1395
1397
|
}
|
|
1396
1398
|
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
1397
|
-
if (
|
|
1399
|
+
if (activation.isActive() && activePrompt === "abel-design")
|
|
1400
|
+
enforceDesignTools();
|
|
1398
1401
|
const boundary = activePrompt
|
|
1399
1402
|
? `Abel stage ${activePrompt} is active only for the invoked task and its direct follow-ups. If the user ends it or requests an unrelated task, first call abel_dispatch with {"action":"finish"}, then handle that task normally with the restored tools. A successful finish ends stage authority immediately, including within this turn. Do not extend Gates or workflow rules to that task. A direct Gate answer or same-task continuation stays in this stage. Never invoke another Abel stage automatically.`
|
|
1400
1403
|
: init
|
package/src/operator-cli.mjs
CHANGED
|
@@ -1239,6 +1239,26 @@ function validateAcceptedVerificationCapability(root, verification, dependencyOw
|
|
|
1239
1239
|
}
|
|
1240
1240
|
|
|
1241
1241
|
// src/operator-tools.ts
|
|
1242
|
+
var PACKAGE_LOCKS = {
|
|
1243
|
+
bun: ["bun.lock", "bun.lockb"],
|
|
1244
|
+
npm: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
1245
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
1246
|
+
yarn: ["yarn.lock"]
|
|
1247
|
+
};
|
|
1248
|
+
function packageManagerSelection(root, declaration) {
|
|
1249
|
+
const managers = Object.keys(PACKAGE_LOCKS);
|
|
1250
|
+
const lockfiles = managers.flatMap((manager) => PACKAGE_LOCKS[manager].filter((file) => lstatSync6(path5.join(root, file), { throwIfNoEntry: false })?.isFile()));
|
|
1251
|
+
const inferred = managers.filter((manager) => PACKAGE_LOCKS[manager].some((file) => lockfiles.includes(file)));
|
|
1252
|
+
const declared = typeof declaration === "string" ? managers.find((manager) => new RegExp(`^${manager}@[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][a-z0-9._+-]+)?$`, "iu").test(declaration)) : undefined;
|
|
1253
|
+
const selectedPackageManager = declaration !== undefined ? declared ?? null : inferred.length === 1 ? inferred[0] ?? null : inferred.length > 1 ? null : "npm";
|
|
1254
|
+
const selectionSource = declaration !== undefined ? declared ? "package.json" : "invalid-declaration" : inferred.length === 1 ? "lockfile" : inferred.length > 1 ? "ambiguous" : "default";
|
|
1255
|
+
return {
|
|
1256
|
+
selectedPackageManager,
|
|
1257
|
+
selectionSource,
|
|
1258
|
+
lockfiles,
|
|
1259
|
+
ambiguousLockfiles: inferred.length > 1
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1242
1262
|
function inspectConsumer(root) {
|
|
1243
1263
|
root = realpathSync4(root);
|
|
1244
1264
|
const checks = [];
|
|
@@ -1317,6 +1337,7 @@ function inspectConsumer(root) {
|
|
|
1317
1337
|
return `${links} repository workspace link(s); their targets must be included in the candidate baseline`;
|
|
1318
1338
|
}, "Repair dangling/external workspace links and ensure local package sources are tracked or admitted in the candidate.");
|
|
1319
1339
|
const manifest = path5.join(root, "package.json");
|
|
1340
|
+
let selection = packageManagerSelection(root, undefined);
|
|
1320
1341
|
if (lstatSync6(manifest, { throwIfNoEntry: false })?.isFile()) {
|
|
1321
1342
|
check("manifest", () => {
|
|
1322
1343
|
const parsed2 = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
@@ -1326,19 +1347,23 @@ function inspectConsumer(root) {
|
|
|
1326
1347
|
}, "Repair package.json.");
|
|
1327
1348
|
const parsed = (() => {
|
|
1328
1349
|
try {
|
|
1329
|
-
|
|
1350
|
+
const value = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
1351
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1330
1352
|
} catch {
|
|
1331
1353
|
return {};
|
|
1332
1354
|
}
|
|
1333
1355
|
})();
|
|
1356
|
+
selection = packageManagerSelection(root, parsed.packageManager);
|
|
1357
|
+
check("package-manager", () => {
|
|
1358
|
+
if (!selection.selectedPackageManager)
|
|
1359
|
+
throw new Error(`package-manager-${selection.selectionSource}`);
|
|
1360
|
+
return `${selection.selectedPackageManager} (${selection.selectionSource})`;
|
|
1361
|
+
}, "Declare the intended packageManager in package.json; doctor does not read private plans, install runners or remove lockfiles.");
|
|
1334
1362
|
for (const script of ["test", "check", "build"].filter((name) => typeof parsed.scripts?.[name] === "string")) {
|
|
1335
1363
|
check(`script:${script}`, () => {
|
|
1336
|
-
const manager =
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
yarn: "yarn.lock",
|
|
1340
|
-
npm: "package-lock.json"
|
|
1341
|
-
}[name]), { throwIfNoEntry: false })) ?? "npm";
|
|
1364
|
+
const manager = selection.selectedPackageManager;
|
|
1365
|
+
if (!manager)
|
|
1366
|
+
throw new Error(`package-manager-${selection.selectionSource}`);
|
|
1342
1367
|
const result = validateVerificationAdapterCapability(root, {
|
|
1343
1368
|
kind: "package-script",
|
|
1344
1369
|
id: `doctor-${script}`,
|
|
@@ -1349,12 +1374,12 @@ function inspectConsumer(root) {
|
|
|
1349
1374
|
classification: "expected-green"
|
|
1350
1375
|
});
|
|
1351
1376
|
if (!result.ok)
|
|
1352
|
-
throw new Error(result.diagnostic.code);
|
|
1377
|
+
throw new Error(`${manager}:${result.diagnostic.code}`);
|
|
1353
1378
|
return "statically admitted; tests not executed";
|
|
1354
1379
|
}, "Check the runner, installed dependencies and execution configuration; this probe does not execute your tests.");
|
|
1355
1380
|
}
|
|
1356
1381
|
}
|
|
1357
|
-
return { root, ok: checks.every((entry) => entry.ok), checks };
|
|
1382
|
+
return { root, ...selection, ok: checks.every((entry) => entry.ok), checks };
|
|
1358
1383
|
}
|
|
1359
1384
|
function storageUsage(root, maximum = 1e5) {
|
|
1360
1385
|
let entries = 0;
|
package/src/operator-tools.ts
CHANGED
|
@@ -14,6 +14,59 @@ export interface DoctorCheck {
|
|
|
14
14
|
detail: string;
|
|
15
15
|
nextStep?: string;
|
|
16
16
|
}
|
|
17
|
+
const PACKAGE_LOCKS = {
|
|
18
|
+
bun: ["bun.lock", "bun.lockb"],
|
|
19
|
+
npm: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
20
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
21
|
+
yarn: ["yarn.lock"],
|
|
22
|
+
} as const;
|
|
23
|
+
type PackageManager = keyof typeof PACKAGE_LOCKS;
|
|
24
|
+
|
|
25
|
+
function packageManagerSelection(root: string, declaration: unknown) {
|
|
26
|
+
const managers = Object.keys(PACKAGE_LOCKS) as PackageManager[];
|
|
27
|
+
const lockfiles = managers.flatMap((manager) =>
|
|
28
|
+
PACKAGE_LOCKS[manager].filter((file) =>
|
|
29
|
+
lstatSync(path.join(root, file), { throwIfNoEntry: false })?.isFile(),
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
const inferred = managers.filter((manager) =>
|
|
33
|
+
PACKAGE_LOCKS[manager].some((file) => lockfiles.includes(file)),
|
|
34
|
+
);
|
|
35
|
+
const declared =
|
|
36
|
+
typeof declaration === "string"
|
|
37
|
+
? managers.find((manager) =>
|
|
38
|
+
new RegExp(
|
|
39
|
+
`^${manager}@[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][a-z0-9._+-]+)?$`,
|
|
40
|
+
"iu",
|
|
41
|
+
).test(declaration),
|
|
42
|
+
)
|
|
43
|
+
: undefined;
|
|
44
|
+
const selectedPackageManager =
|
|
45
|
+
declaration !== undefined
|
|
46
|
+
? (declared ?? null)
|
|
47
|
+
: inferred.length === 1
|
|
48
|
+
? (inferred[0] ?? null)
|
|
49
|
+
: inferred.length > 1
|
|
50
|
+
? null
|
|
51
|
+
: "npm";
|
|
52
|
+
const selectionSource =
|
|
53
|
+
declaration !== undefined
|
|
54
|
+
? declared
|
|
55
|
+
? "package.json"
|
|
56
|
+
: "invalid-declaration"
|
|
57
|
+
: inferred.length === 1
|
|
58
|
+
? "lockfile"
|
|
59
|
+
: inferred.length > 1
|
|
60
|
+
? "ambiguous"
|
|
61
|
+
: "default";
|
|
62
|
+
return {
|
|
63
|
+
selectedPackageManager,
|
|
64
|
+
selectionSource,
|
|
65
|
+
lockfiles,
|
|
66
|
+
ambiguousLockfiles: inferred.length > 1,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
17
70
|
export function inspectConsumer(root: string) {
|
|
18
71
|
root = realpathSync(root);
|
|
19
72
|
const checks: DoctorCheck[] = [];
|
|
@@ -136,6 +189,7 @@ export function inspectConsumer(root: string) {
|
|
|
136
189
|
"Repair dangling/external workspace links and ensure local package sources are tracked or admitted in the candidate.",
|
|
137
190
|
);
|
|
138
191
|
const manifest = path.join(root, "package.json");
|
|
192
|
+
let selection = packageManagerSelection(root, undefined);
|
|
139
193
|
if (lstatSync(manifest, { throwIfNoEntry: false })?.isFile()) {
|
|
140
194
|
check(
|
|
141
195
|
"manifest",
|
|
@@ -151,51 +205,51 @@ export function inspectConsumer(root: string) {
|
|
|
151
205
|
);
|
|
152
206
|
const parsed = (() => {
|
|
153
207
|
try {
|
|
154
|
-
|
|
208
|
+
const value = JSON.parse(readFileSync(manifest, "utf8"));
|
|
209
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
210
|
+
? value
|
|
211
|
+
: {};
|
|
155
212
|
} catch {
|
|
156
213
|
return {};
|
|
157
214
|
}
|
|
158
215
|
})();
|
|
216
|
+
selection = packageManagerSelection(root, parsed.packageManager);
|
|
217
|
+
check(
|
|
218
|
+
"package-manager",
|
|
219
|
+
() => {
|
|
220
|
+
if (!selection.selectedPackageManager)
|
|
221
|
+
throw new Error(`package-manager-${selection.selectionSource}`);
|
|
222
|
+
return `${selection.selectedPackageManager} (${selection.selectionSource})`;
|
|
223
|
+
},
|
|
224
|
+
"Declare the intended packageManager in package.json; doctor does not read private plans, install runners or remove lockfiles.",
|
|
225
|
+
);
|
|
159
226
|
for (const script of ["test", "check", "build"].filter(
|
|
160
227
|
(name) => typeof parsed.scripts?.[name] === "string",
|
|
161
228
|
)) {
|
|
162
229
|
check(
|
|
163
230
|
`script:${script}`,
|
|
164
231
|
() => {
|
|
165
|
-
const manager =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
path.join(
|
|
169
|
-
root,
|
|
170
|
-
(
|
|
171
|
-
{
|
|
172
|
-
bun: "bun.lock",
|
|
173
|
-
pnpm: "pnpm-lock.yaml",
|
|
174
|
-
yarn: "yarn.lock",
|
|
175
|
-
npm: "package-lock.json",
|
|
176
|
-
} as Record<string, string>
|
|
177
|
-
)[name],
|
|
178
|
-
),
|
|
179
|
-
{ throwIfNoEntry: false },
|
|
180
|
-
),
|
|
181
|
-
) ?? "npm";
|
|
232
|
+
const manager = selection.selectedPackageManager;
|
|
233
|
+
if (!manager)
|
|
234
|
+
throw new Error(`package-manager-${selection.selectionSource}`);
|
|
182
235
|
const result = validateVerificationAdapterCapability(root, {
|
|
183
236
|
kind: "package-script",
|
|
184
237
|
id: `doctor-${script}`,
|
|
185
|
-
packageManager: manager
|
|
238
|
+
packageManager: manager,
|
|
186
239
|
script,
|
|
187
240
|
command: parsed.scripts[script],
|
|
188
241
|
args: [],
|
|
189
242
|
classification: "expected-green",
|
|
190
243
|
});
|
|
191
|
-
if (!result.ok)
|
|
244
|
+
if (!result.ok)
|
|
245
|
+
throw new Error(`${manager}:${result.diagnostic.code}`);
|
|
192
246
|
return "statically admitted; tests not executed";
|
|
193
247
|
},
|
|
194
248
|
"Check the runner, installed dependencies and execution configuration; this probe does not execute your tests.",
|
|
195
249
|
);
|
|
196
250
|
}
|
|
197
251
|
}
|
|
198
|
-
return { root, ok: checks.every((entry) => entry.ok), checks };
|
|
252
|
+
return { root, ...selection, ok: checks.every((entry) => entry.ok), checks };
|
|
199
253
|
}
|
|
200
254
|
|
|
201
255
|
/** Bounded metadata traversal, no symlink following and no automatic deletion. */
|