agentic-flow-kit 0.7.0 → 0.9.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 +36 -12
- package/modules/gates/README.md +24 -0
- package/modules/gates/generated-readonly/README.md +13 -0
- package/modules/gates/generated-readonly/check-generated.mjs +61 -0
- package/modules/gates/generated-readonly/local-pre-commit.line +1 -0
- package/modules/gates/node-layer/README.md +9 -0
- package/modules/gates/node-layer/check-node-layer.mjs +29 -0
- package/modules/gates/node-layer/local-pre-commit.line +1 -0
- package/modules/gates/py-import/README.md +9 -0
- package/modules/gates/py-import/check-py-import.mjs +30 -0
- package/modules/gates/py-import/local-pre-commit.line +1 -0
- package/modules/hosts/claude/agents/implementer.md +52 -0
- package/modules/hosts/claude/agents/independent-reviewer.md +49 -0
- package/modules/hosts/claude/agents/ui-verifier.md +48 -0
- package/modules/hosts/claude/commands/wf-build.md +79 -0
- package/modules/hosts/claude/commands/wf-deploy.md +57 -0
- package/modules/hosts/claude/commands/wf-design.md +42 -0
- package/modules/hosts/claude/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/claude/commands/wf-maintain.md +76 -0
- package/modules/hosts/claude/commands/wf-new-task.md +65 -0
- package/modules/hosts/claude/commands/wf-plan.md +41 -0
- package/modules/hosts/claude/commands/wf-review.md +47 -0
- package/modules/hosts/claude/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/claude/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/claude/commands/wf-test.md +66 -0
- package/modules/hosts/codex/agents/implementer.md +52 -0
- package/modules/hosts/codex/agents/independent-reviewer.md +49 -0
- package/modules/hosts/codex/agents/ui-verifier.md +48 -0
- package/modules/hosts/codex/commands/wf-build.md +79 -0
- package/modules/hosts/codex/commands/wf-deploy.md +57 -0
- package/modules/hosts/codex/commands/wf-design.md +42 -0
- package/modules/hosts/codex/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/codex/commands/wf-maintain.md +76 -0
- package/modules/hosts/codex/commands/wf-new-task.md +65 -0
- package/modules/hosts/codex/commands/wf-plan.md +41 -0
- package/modules/hosts/codex/commands/wf-review.md +47 -0
- package/modules/hosts/codex/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/codex/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/codex/commands/wf-test.md +66 -0
- package/modules/hosts/codex/skills/flow-kit/SKILL.md +14 -0
- package/modules/hosts/cursor/agents/implementer.md +52 -0
- package/modules/hosts/cursor/agents/independent-reviewer.md +49 -0
- package/modules/hosts/cursor/agents/ui-verifier.md +48 -0
- package/modules/hosts/cursor/commands/wf-build.md +79 -0
- package/modules/hosts/cursor/commands/wf-deploy.md +57 -0
- package/modules/hosts/cursor/commands/wf-design.md +42 -0
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/cursor/commands/wf-maintain.md +76 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +65 -0
- package/modules/hosts/cursor/commands/wf-plan.md +41 -0
- package/modules/hosts/cursor/commands/wf-review.md +47 -0
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/cursor/commands/wf-test.md +66 -0
- package/modules/hosts/omp/agents/implementer.md +1 -1
- package/modules/hosts/omp/agents/independent-reviewer.md +1 -1
- package/modules/hosts/omp/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/agents/implementer.md +1 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/commands/{build.md → wf-build.md} +3 -4
- package/modules/hosts/opencode/commands/{design.md → wf-design.md} +2 -2
- package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +1 -1
- package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +3 -3
- package/modules/hosts/opencode/commands/{review.md → wf-review.md} +1 -1
- package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +1 -1
- package/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
- package/modules/hosts/opencode/commands/{test.md → wf-test.md} +3 -1
- package/modules/hosts/trae/agents/implementer.md +1 -1
- package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
- package/modules/hosts/trae/agents/ui-verifier.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -4
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +3 -1
- package/modules/hosts/zcode/agents/implementer.md +1 -1
- package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +16 -4
- package/src/doctor.mjs +58 -5
- package/src/fresh-init.test.mjs +36 -0
- package/src/gates.test.mjs +126 -0
- package/src/init.mjs +8 -1
- package/src/init.test.mjs +7 -1
- package/src/profiles.mjs +10 -4
- package/src/run-tests.mjs +3 -0
- package/src/stack-profile.test.mjs +89 -0
- package/src/sync-hosts.mjs +12 -6
- package/src/sync-hosts.test.mjs +40 -17
- package/templates/AGENTS.md +4 -4
- package/templates/_agents/board/index.html +3 -3
- package/templates/_agents/commands/build.md +4 -5
- package/templates/_agents/commands/design.md +4 -4
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +13 -2
- package/templates/_agents/commands/plan.md +4 -4
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -1
- package/templates/_agents/commands/sync-hosts.md +1 -1
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/commit-check.cjs +45 -24
- package/templates/_agents/lane-surfaces.txt +14 -0
- package/templates/_agents/roles/implementer.md +1 -1
- package/templates/_agents/roles/independent-reviewer.md +1 -1
- package/templates/_agents/roles/ui-verifier.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
- package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
- package/templates/_agents/scripts/check-ledger.mjs +65 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
- package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
- package/templates/_agents/scripts/check-loop.mjs +460 -268
- package/templates/_agents/scripts/check-loop.test.mjs +677 -17
- package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
- package/templates/_agents/scripts/confirm-doc.mjs +159 -15
- package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
- package/templates/_agents/scripts/doctor.test.mjs +105 -4
- package/templates/_agents/scripts/fill-intent.mjs +8 -7
- package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
- package/templates/_agents/scripts/fill-plan.mjs +27 -11
- package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
- package/templates/_agents/scripts/fill-spec.mjs +18 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
- package/templates/_agents/scripts/gate-checklist.mjs +4 -1
- package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
- package/templates/_agents/scripts/policy.mjs +46 -0
- package/templates/_agents/scripts/solidify-task.mjs +101 -0
- package/templates/_agents/scripts/stage-gates.mjs +256 -0
- package/templates/_agents/scripts/trust-mode.mjs +67 -0
- package/templates/_agents/scripts/wf-journal.mjs +71 -2
- package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
- package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
- package/templates/_agents/workflows/pipeline-closing.md +1 -1
- package/templates/_githooks/pre-commit +49 -2
- package/templates/_githooks/pre-push +56 -12
- package/templates/workflow/README.md +22 -3
- package/templates/workflow/incidents/_TEMPLATE.md +4 -0
- package/templates/workflow/intents/_TEMPLATE.md +1 -0
- /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
- /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
- /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -0
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// 判据:① 指纹算法(CRLF 归一 / 剔指纹行防自引用 / 内容敏感) ② 跳转唯一合法性 ③ 落态只动两行
|
|
4
4
|
// ④ 台账追加 schema ⑤ 核心——非 TTY spawn(模拟 AI 调用路径)无 --delegated 必须被拒
|
|
5
5
|
// ⑥ 委托代录(--delegated):免 TTY 落态 + 台账如实记 source/quote;空原话拒跑
|
|
6
|
+
// ⑦ 逐阶段前置门(2026-09-30 stage-gate-machine):specs/plans 的 draft→approved 须入口(plan 另须
|
|
7
|
+
// L2/L3 时同名 spec)已确认;未过 → exit 2 + 未落盘未记账(S19-S22)
|
|
8
|
+
// ⑧ done 前置门(2026-09-30 confirm-gate-approved-history):→done 须 git 历史已留 approved 态
|
|
9
|
+
// (先 done 后提交 / 两跳同批提交 → 拒绝);非 git / legacy / 存量豁免声明 → 跳过(S25-S29)
|
|
6
10
|
// 用法:node templates/_agents/scripts/confirm-doc.test.mjs(npm test 随跑)
|
|
7
11
|
import fs from 'node:fs';
|
|
8
12
|
import os from 'node:os';
|
|
@@ -21,6 +25,14 @@ const check = (desc, cond, detail = '') => {
|
|
|
21
25
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
22
26
|
const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
23
27
|
|
|
28
|
+
// 台账读取 helper(2026-09-30 复核 P2-1):缺失/空行 → 安全返回([])——防「门拒路径下台账未生成」时
|
|
29
|
+
// 未捕获 ENOENT 截断后续用例(注入验证实证:原裸读在门失效时崩溃、无「合计」行)
|
|
30
|
+
const readLedgerOf = (root) => {
|
|
31
|
+
const p = path.join(root, '.agents', 'confirmations.jsonl');
|
|
32
|
+
if (!fs.existsSync(p)) return [];
|
|
33
|
+
return fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
34
|
+
};
|
|
35
|
+
|
|
24
36
|
// ---- S1 指纹:CRLF 归一(同内容不同行尾同指纹)----
|
|
25
37
|
{
|
|
26
38
|
const lf = '---\n状态: draft\n---\n正文';
|
|
@@ -119,12 +131,16 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
119
131
|
const docP = path.join(root, 'workflow', 'plans');
|
|
120
132
|
fs.mkdirSync(docP, { recursive: true });
|
|
121
133
|
fs.writeFileSync(path.join(docP, '2026-09-27-d.md'), '---\n状态: draft\n级别: L2\n---\n# P\n');
|
|
134
|
+
// 逐阶段门夹具(2026-09-30 stage-gate-machine):plan 批准须先有已确认入口——
|
|
135
|
+
// 入口用存量口径(日期早于 confirmDocsEffective)免台账;级别 L1 免 spec 档
|
|
136
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
137
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-d.md'), '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n---\n# I(S11 入口夹具)\n');
|
|
122
138
|
const before = fs.readFileSync(path.join(docP, '2026-09-27-d.md'), 'utf8');
|
|
123
139
|
const fpExpect = computeFingerprint(before);
|
|
124
140
|
// spawnSync 无 TTY(管道 stdin)——模拟 AI 会话内委托代录调用
|
|
125
141
|
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-27-d.md', '--delegated', '2选2'], { cwd: root, encoding: 'utf8' });
|
|
126
142
|
const after = fs.readFileSync(path.join(docP, '2026-09-27-d.md'), 'utf8');
|
|
127
|
-
const led =
|
|
143
|
+
const led = readLedgerOf(root);
|
|
128
144
|
const j = led[led.length - 1];
|
|
129
145
|
check('S11 委托代录:exit 0 + draft→approved + 指纹行 + 台账 source=chat-delegated / quote=原话 / 指纹全量与 frontmatter 前 16 位配对',
|
|
130
146
|
r.status === 0 && after.includes('状态: approved') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
|
|
@@ -145,7 +161,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
145
161
|
const fp2 = computeFingerprint(before);
|
|
146
162
|
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-e.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
147
163
|
const after = fs.readFileSync(path.join(docP, '2026-09-27-e.md'), 'utf8');
|
|
148
|
-
const led =
|
|
164
|
+
const led = readLedgerOf(root);
|
|
149
165
|
check('S12 委托第二跳:approved→done + 指纹行换新值 + 台账 stage=done / source 如实',
|
|
150
166
|
r.status === 0 && after.includes('状态: done') && after.includes(`确认指纹: ${fp2.slice(0, 16)}`) && !after.includes(fp1.slice(0, 16))
|
|
151
167
|
&& led[led.length - 1].stage === 'done' && led[led.length - 1].source === 'chat-delegated',
|
|
@@ -178,7 +194,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
178
194
|
const fpExpect = computeFingerprint(before);
|
|
179
195
|
const r = spawnSync(process.execPath, [CLI, 'workflow/incidents/2026-09-28-i.md', '--delegated', '修好了'], { cwd: root, encoding: 'utf8' });
|
|
180
196
|
const after = fs.readFileSync(path.join(docP, '2026-09-28-i.md'), 'utf8');
|
|
181
|
-
const led =
|
|
197
|
+
const led = readLedgerOf(root);
|
|
182
198
|
const j = led[led.length - 1];
|
|
183
199
|
check('S15 incidents 委托代录:exit 0 + open→fixed + 指纹行 + 台账 stage=fixed / prev=open',
|
|
184
200
|
r.status === 0 && after.includes('状态: fixed') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
|
|
@@ -198,7 +214,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
198
214
|
const fpExpect = computeFingerprint(before);
|
|
199
215
|
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-28-c.md', '--to', 'cancelled', '--delegated', '不做了'], { cwd: root, encoding: 'utf8' });
|
|
200
216
|
const after = fs.readFileSync(path.join(docP, '2026-09-28-c.md'), 'utf8');
|
|
201
|
-
const led =
|
|
217
|
+
const led = readLedgerOf(root);
|
|
202
218
|
const j = led[led.length - 1];
|
|
203
219
|
check('S16 --to cancelled 委托代录:exit 0 + draft→cancelled + 台账 stage=cancelled / prev=draft',
|
|
204
220
|
r.status === 0 && after.includes('状态: cancelled') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
|
|
@@ -263,7 +279,7 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
263
279
|
JSON.stringify({ r1: r1.status, r2: r2.status, stderr: r2.stderr }));
|
|
264
280
|
// 调用事实(2026-09-28 batch-ledger-audit):每次进程调用 **一个** batch;两次独立调用 batch **不同**,
|
|
265
281
|
// 各自 seq=1 / of=1——这是并录审计「逐件 vs 并录」的判别基础(复核 P2-2:plan 要求覆盖此面)
|
|
266
|
-
const led2 =
|
|
282
|
+
const led2 = readLedgerOf(root);
|
|
267
283
|
const [e1, e2] = led2.slice(-2);
|
|
268
284
|
check('S18b 两次独立调用各生成独立 batch(不同批、各自 seq=1/of=1)',
|
|
269
285
|
e1.batch !== e2.batch && e1.seq === 1 && e2.seq === 1 && e1.of === 1 && e2.of === 1,
|
|
@@ -275,7 +291,9 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
275
291
|
// 该路径仅 TTY 可达(delegated 已被入口限为 1 份),故注入两个**仅测试用**逃生门:
|
|
276
292
|
// CONFIRM_DOC_TEST_TTY=1 —— 放行 isTTY 判定(本环境无 TTY)
|
|
277
293
|
// CONFIRM_DOC_TEST_ANSWERS —— 按序注入问答应答(见下「为什么不能用管道 stdin」)
|
|
278
|
-
//
|
|
294
|
+
// NODE_ENV=test —— TTY 逃生门双条件的第二键(2026-09-28 check16-inline-debt:
|
|
295
|
+
// 单变量可被生产会话顺手设置绕过 TTY 门,收紧为双条件)
|
|
296
|
+
// 三者均不改动任何落态/记账语义。
|
|
279
297
|
//
|
|
280
298
|
// **为什么不能用 spawnSync 的 input 管道(2026-09-28 更正)**:此前本用例用 `input:'可以\n可以\n'`
|
|
281
299
|
// 并断言「首份」了事,注释把根因误记为「readline 对第二份提问会 unsettled / 管道输入不被逐次消费」。
|
|
@@ -285,9 +303,9 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
285
303
|
const root = mk2docs();
|
|
286
304
|
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md'], {
|
|
287
305
|
cwd: root, encoding: 'utf8',
|
|
288
|
-
env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以,可以' },
|
|
306
|
+
env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以,可以', NODE_ENV: 'test' },
|
|
289
307
|
});
|
|
290
|
-
const ledN =
|
|
308
|
+
const ledN = readLedgerOf(root);
|
|
291
309
|
const stI = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
|
|
292
310
|
const stP = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
|
|
293
311
|
check('S18c 单次调用落 2 份(TTY 多文档)端到端 → batch 相同 / seq 1,2 / of 均=2 / 两份都落态',
|
|
@@ -306,15 +324,349 @@ const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
|
306
324
|
const root = mk2docs();
|
|
307
325
|
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md'], {
|
|
308
326
|
cwd: root, encoding: 'utf8',
|
|
309
|
-
env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以' }, // 只给一份应答
|
|
327
|
+
env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以', NODE_ENV: 'test' }, // 只给一份应答
|
|
310
328
|
});
|
|
311
|
-
const led1 =
|
|
329
|
+
const led1 = readLedgerOf(root);
|
|
312
330
|
const stP1 = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
|
|
313
331
|
check('S18d 注入应答不足 → 仅首份落态,次份按跳过处理(不落态不记账)',
|
|
314
332
|
led1.length === 1 && led1[0].seq === 1 && led1[0].of === 2 && stP1.includes('状态: draft'),
|
|
315
333
|
JSON.stringify({ status: r.status, ledger: led1 }));
|
|
316
334
|
fs.rmSync(root, { recursive: true, force: true });
|
|
317
335
|
}
|
|
336
|
+
{
|
|
337
|
+
// 【T3 双条件】仅设 CONFIRM_DOC_TEST_TTY=1 而未设 NODE_ENV=test → 逃生门不生效,仍按非 TTY 拒绝
|
|
338
|
+
// (2026-09-28 check16-inline-debt:单变量可被生产会话顺手设置绕过 TTY 门,收紧为双条件——
|
|
339
|
+
// 本用例钉住「缺第二键即不放行」,与 S18c/S18d 的「双键齐 → 放行」构成双向覆盖)
|
|
340
|
+
const root = mk2docs();
|
|
341
|
+
const env1 = { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以' };
|
|
342
|
+
delete env1.NODE_ENV; // 显式剔除——防宿主环境恰好携带 NODE_ENV 时用例失真
|
|
343
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md'], {
|
|
344
|
+
cwd: root, encoding: 'utf8', env: env1,
|
|
345
|
+
});
|
|
346
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
347
|
+
const afterI = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
|
|
348
|
+
check('S18e 仅 CONFIRM_DOC_TEST_TTY=1 无 NODE_ENV=test → 仍非 TTY 拒绝(exit 1 + 零台账 + 文档未动)',
|
|
349
|
+
r.status === 1 && /不可代确认/.test(r.stderr) && noLedger && afterI.includes('状态: draft'),
|
|
350
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
351
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---- S19【逐阶段门】spec 无同名入口 → 拒绝落账(2026-09-30 stage-gate-machine)----
|
|
356
|
+
{
|
|
357
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg1-'));
|
|
358
|
+
const docP = path.join(root, 'workflow', 'specs');
|
|
359
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
360
|
+
fs.writeFileSync(path.join(docP, '2026-09-30-g.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
|
|
361
|
+
const before = fs.readFileSync(path.join(docP, '2026-09-30-g.md'), 'utf8');
|
|
362
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-g.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
363
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-30-g.md'), 'utf8');
|
|
364
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
365
|
+
check('S19 逐阶段门:spec 无同名入口 → exit 2 + 未落盘未记账 + 回退提示',
|
|
366
|
+
r.status === 2 && /逐阶段前置门/.test(r.stderr) && /未找到同名入口/.test(r.stderr) && after === before && noLedger,
|
|
367
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
368
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---- S20【逐阶段门】入口存在但未确认(draft)→ 拒绝 ----
|
|
372
|
+
{
|
|
373
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg2-'));
|
|
374
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
375
|
+
fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
|
|
376
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-h.md'), '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I\n');
|
|
377
|
+
fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-h.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
|
|
378
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-h.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
379
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
380
|
+
check('S20 逐阶段门:入口未确认(draft)→ exit 2 + 提示状态、零落账',
|
|
381
|
+
r.status === 2 && /状态「draft」/.test(r.stderr) && noLedger,
|
|
382
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
383
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---- S21【逐阶段门】L1 入口已确认 → plan 放行(存量口径免台账;级别 L1 免 spec 档)----
|
|
387
|
+
{
|
|
388
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg3-'));
|
|
389
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
390
|
+
fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
|
|
391
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-j.md'), '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n---\n# I\n');
|
|
392
|
+
fs.writeFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-j.md'), '---\n状态: draft\n级别: L1\n---\n# P\n');
|
|
393
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-j.md', '--delegated', '继续'], { cwd: root, encoding: 'utf8' });
|
|
394
|
+
const after = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-j.md'), 'utf8');
|
|
395
|
+
check('S21 逐阶段门:L1 入口 approved(存量口径)→ plan 放行落态',
|
|
396
|
+
r.status === 0 && after.includes('状态: approved'),
|
|
397
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
398
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---- S22【逐阶段门】L2 入口已确认但缺 spec → plan 拒绝 ----
|
|
402
|
+
{
|
|
403
|
+
// 入口须有**台账行**(不复用存量日期短路路径)——夹具写 confirmed 行使前置成立,仅缺 spec
|
|
404
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg4-'));
|
|
405
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
406
|
+
fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
|
|
407
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
408
|
+
const fp4 = '4'.repeat(64);
|
|
409
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-k.md'), `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${fp4.slice(0, 16)}\n---\n# I\n`);
|
|
410
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T04:00:00.000Z', doc: 'workflow/intents/2026-09-30-k.md', stage: 'approved', fingerprint: fp4, prev: 'draft', source: 'chat-delegated', batch: 'k4', seq: 1, of: 1 }) + '\n');
|
|
411
|
+
fs.writeFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-k.md'), '---\n状态: draft\n级别: L2\n---\n# P\n');
|
|
412
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-k.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
413
|
+
const after = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-30-k.md'), 'utf8');
|
|
414
|
+
check('S22 逐阶段门:L2 入口(台账行)缺同名 spec → plan 拒绝(exit 2 + 未落态)',
|
|
415
|
+
r.status === 2 && /缺同名 spec/.test(r.stderr) && after.includes('状态: draft'),
|
|
416
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
417
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---- S23【逐阶段门】入口「台账行」正路放行(非存量口径;复核 P2-4④)----
|
|
421
|
+
{
|
|
422
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-sg5-'));
|
|
423
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
424
|
+
fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
|
|
425
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
426
|
+
const fp = 'b'.repeat(64);
|
|
427
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-m.md'), `---\n状态: approved\n级别: L2\n日期: 2026-09-30\n确认指纹: ${fp.slice(0, 16)}\n---\n# I\n`);
|
|
428
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: '2026-09-30T02:00:00.000Z', doc: 'workflow/intents/2026-09-30-m.md', stage: 'approved', fingerprint: fp, prev: 'draft', source: 'chat-delegated', batch: 't9', seq: 1, of: 1 }) + '\n');
|
|
429
|
+
fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-m.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
|
|
430
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-m.md', '--delegated', '继续'], { cwd: root, encoding: 'utf8' });
|
|
431
|
+
const after = fs.readFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-m.md'), 'utf8');
|
|
432
|
+
check('S23 逐阶段门:入口状态 approved + 台账行(正路)→ spec 放行',
|
|
433
|
+
r.status === 0 && after.includes('状态: approved'),
|
|
434
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
435
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
{
|
|
439
|
+
// N1:存量短路(confirm-doc 侧)——legacy 入口无级别 → spec 放行
|
|
440
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-n1-'));
|
|
441
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
442
|
+
fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
|
|
443
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-n1.md'), '---\n状态: approved\n日期: 2026-01-01\n流程: legacy\n---\n# I(存量、无级别)\n');
|
|
444
|
+
fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-n1.md'), '---\n状态: draft\n级别: L2\n---\n# S\n');
|
|
445
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-n1.md', '--delegated', 'n1 回归'], { cwd: root, encoding: 'utf8' });
|
|
446
|
+
const after = fs.readFileSync(path.join(root, 'workflow', 'specs', '2026-09-30-n1.md'), 'utf8');
|
|
447
|
+
check('S24 N1 存量短路:legacy 入口无级别 → confirm-doc 放行(不锁死确认)',
|
|
448
|
+
r.status === 0 && after.includes('状态: approved'),
|
|
449
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
450
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---- S25–S29【done 前置门】approved 历史留痕(2026-09-30 confirm-gate-approved-history)----
|
|
454
|
+
// git helper:与 check-loop.test 同口径(win32 → git.exe;-c 注入 user 免全局 config 依赖)
|
|
455
|
+
const GITBIN = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
456
|
+
const gitAt = (root, ...args) => spawnSync(GITBIN, args, { cwd: root, encoding: 'utf8' });
|
|
457
|
+
const gitHead = (root) => {
|
|
458
|
+
gitAt(root, 'init', '-q');
|
|
459
|
+
gitAt(root, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init');
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// S25:有 HEAD 但文档从未提交(先 done 后提交形态)→ done 拒绝
|
|
463
|
+
{
|
|
464
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh1-'));
|
|
465
|
+
fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
|
|
466
|
+
const p = path.join(root, 'workflow', 'plans', '2026-09-30-d1.md');
|
|
467
|
+
fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n日期: 2026-09-30\n---\n# P\n');
|
|
468
|
+
const before = fs.readFileSync(p, 'utf8');
|
|
469
|
+
gitHead(root);
|
|
470
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-d1.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
|
|
471
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
472
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
473
|
+
check('S25 done 门:历史无 approved(先 done 后提交形态)→ exit 2 + 零落账 + 零写盘',
|
|
474
|
+
r.status === 2 && /done 前置门/.test(r.stderr) && /从未出现/.test(r.stderr) && after === before && noLedger,
|
|
475
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 260) }));
|
|
476
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// S26:approved 态已提交(留痕)→ done 放行落账
|
|
480
|
+
{
|
|
481
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh2-'));
|
|
482
|
+
fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
|
|
483
|
+
const p = path.join(root, 'workflow', 'plans', '2026-09-30-d2.md');
|
|
484
|
+
fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n日期: 2026-09-30\n---\n# P\n');
|
|
485
|
+
gitHead(root);
|
|
486
|
+
gitAt(root, 'add', '-A');
|
|
487
|
+
gitAt(root, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '-m', 'approved');
|
|
488
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-d2.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
|
|
489
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
490
|
+
const ledger = readLedgerOf(root); // 复核 P2-1:统一 helper(缺失 → [],不崩溃)
|
|
491
|
+
check('S26 done 门:approved 态已提交(留痕)→ 放行落态 + 台账 stage=done',
|
|
492
|
+
r.status === 0 && after.includes('状态: done') && ledger.some((e) => e.stage === 'done' && e.prev === 'approved'),
|
|
493
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
494
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// S27:非 git 环境 → 跳过(不误拦;沿检查 14 口径)
|
|
498
|
+
{
|
|
499
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh3-'));
|
|
500
|
+
fs.mkdirSync(path.join(root, 'workflow', 'plans'), { recursive: true });
|
|
501
|
+
const p = path.join(root, 'workflow', 'plans', '2026-09-30-d3.md');
|
|
502
|
+
fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n---\n# P\n');
|
|
503
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-30-d3.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
|
|
504
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
505
|
+
check('S27 done 门:非 git 环境 → 跳过放行',
|
|
506
|
+
r.status === 0 && after.includes('状态: done'),
|
|
507
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
508
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// S28:legacy 标记 → 跳过(存量口径;intent 文档承载——spec/plan 不产生 legacy 标记)
|
|
512
|
+
{
|
|
513
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh4-'));
|
|
514
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
515
|
+
const p = path.join(root, 'workflow', 'intents', '2026-09-30-d4.md');
|
|
516
|
+
fs.writeFileSync(p, '---\n状态: approved\n级别: L1\n日期: 2026-01-01\n流程: legacy\n---\n# I\n');
|
|
517
|
+
gitHead(root);
|
|
518
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-d4.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
|
|
519
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
520
|
+
check('S28 done 门:legacy 标记 → 跳过放行',
|
|
521
|
+
r.status === 0 && after.includes('状态: done'),
|
|
522
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
523
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// S29:正文「存量确认态豁免(」声明 → 跳过(显式逃生口;与检查 14 同标记)
|
|
527
|
+
{
|
|
528
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-dh5-'));
|
|
529
|
+
fs.mkdirSync(path.join(root, 'workflow', 'specs'), { recursive: true });
|
|
530
|
+
const p = path.join(root, 'workflow', 'specs', '2026-09-30-d5.md');
|
|
531
|
+
fs.writeFileSync(p, '---\n状态: approved\n级别: L2\n---\n# S\n\n存量确认态豁免(S29 构造:声明式逃生口覆盖)\n');
|
|
532
|
+
gitHead(root);
|
|
533
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/specs/2026-09-30-d5.md', '--delegated', '关单'], { cwd: root, encoding: 'utf8' });
|
|
534
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
535
|
+
check('S29 done 门:存量豁免声明 → 跳过放行',
|
|
536
|
+
r.status === 0 && after.includes('状态: done'),
|
|
537
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
538
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ---- S30【协作道批量】--batch:delegated 多份 L0/L1 一次代录 → 全部落态 + 台账 brief:true(2026-09-30 hybrid-governance-risk-lanes)----
|
|
542
|
+
{
|
|
543
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt1-'));
|
|
544
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
545
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt1.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I1\n');
|
|
546
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt2.md'), '---\n状态: draft\n级别: L0\n日期: 2026-09-30\n---\n# I2\n');
|
|
547
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt1.md', 'workflow/intents/2026-09-30-bt2.md', '--delegated', '批量放行', '--batch'], { cwd: root, encoding: 'utf8' });
|
|
548
|
+
const led = readLedgerOf(root);
|
|
549
|
+
const st1 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt1.md'), 'utf8');
|
|
550
|
+
const st2 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt2.md'), 'utf8');
|
|
551
|
+
check('S30 --batch:2 份 L0/L1 一次代录 → 全部 approved + 台账 brief:true / of=2 / seq 递增',
|
|
552
|
+
r.status === 0 && led.length === 2
|
|
553
|
+
&& led.every((e) => e.brief === true && e.source === 'chat-delegated' && e.of === 2 && e.stage === 'approved')
|
|
554
|
+
&& led[0].seq === 1 && led[1].seq === 2
|
|
555
|
+
&& st1.includes('状态: approved') && st2.includes('状态: approved'),
|
|
556
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), led }));
|
|
557
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ---- S31【协作道批量】--batch 混入 L2 → 该份逐份拒绝(exit 2 + 未落盘未记账),L1 份照常落态 ----
|
|
561
|
+
{
|
|
562
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt2-'));
|
|
563
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
564
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt3.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I3\n');
|
|
565
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt4.md'), '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I4\n');
|
|
566
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt3.md', 'workflow/intents/2026-09-30-bt4.md', '--delegated', '批量放行', '--batch'], { cwd: root, encoding: 'utf8' });
|
|
567
|
+
const led = readLedgerOf(root);
|
|
568
|
+
const st4 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt4.md'), 'utf8');
|
|
569
|
+
check('S31 --batch 混入 L2 → L2 拒绝(exit 2 + 未落盘未记账)+ L1 落态',
|
|
570
|
+
r.status === 2 && /不在协作道/.test(r.stderr) && led.length === 1 && led[0].doc.endsWith('bt3.md') && st4.includes('状态: draft'),
|
|
571
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), led }));
|
|
572
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ---- S32 --batch 组合约束:与 --auto / --to 互斥(exit 1,零落账)----
|
|
576
|
+
{
|
|
577
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt3-'));
|
|
578
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
579
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt5.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I5\n');
|
|
580
|
+
const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt5.md', '--delegated', 'x', '--batch', '--auto'], { cwd: root, encoding: 'utf8' });
|
|
581
|
+
const r2 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt5.md', '--delegated', 'x', '--batch', '--to', 'cancelled'], { cwd: root, encoding: 'utf8' });
|
|
582
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
583
|
+
check('S32 --batch 与 --auto / --to 互斥 → 均 exit 1 + 零台账',
|
|
584
|
+
r1.status === 1 && r2.status === 1 && noLedger,
|
|
585
|
+
JSON.stringify({ r1: r1.status, r2: r2.status }));
|
|
586
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---- S33 简洁审计:单份 delegated L0/L1(无 --batch)→ 台账 brief:true + 紧凑输出;L2 份不带 brief ----
|
|
590
|
+
{
|
|
591
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt4-'));
|
|
592
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
593
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-bt6.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I6\n');
|
|
594
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-bt6.md', '--delegated', '单份放行'], { cwd: root, encoding: 'utf8' });
|
|
595
|
+
const led = readLedgerOf(root);
|
|
596
|
+
check('S33 单份 L1 代录 → brief:true + 紧凑输出(协作道/异步审计标记)',
|
|
597
|
+
r.status === 0 && led.length === 1 && led[0].brief === true && /协作道\/异步审计/.test(r.stdout),
|
|
598
|
+
JSON.stringify({ status: r.status, stdout: String(r.stdout).slice(0, 200), led }));
|
|
599
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ---- S34–S37【Trusted 自动泳道】trust-mode.json = Trusted(level 2)→ 非 TTY 免旗标隐式自治(2026-09-30 hybrid-governance-explore-hardening)----
|
|
603
|
+
const writeTrust = (root, cfg) => {
|
|
604
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
605
|
+
fs.writeFileSync(path.join(root, '.agents', 'trust-mode.json'), JSON.stringify(cfg));
|
|
606
|
+
};
|
|
607
|
+
// S34:Trusted + L1 intent 免旗标 → draft→approved 落态 + 台账 source=ai-auto-trust-L2;第二跳 → done(全闭环)
|
|
608
|
+
{
|
|
609
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr1-'));
|
|
610
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
611
|
+
writeTrust(root, { enabled: true, level: 2, name: 'Trusted', enabledAt: '2026-09-30T00:00:00.000Z' });
|
|
612
|
+
const p = path.join(root, 'workflow', 'intents', '2026-09-30-tr1.md');
|
|
613
|
+
fs.writeFileSync(p, '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
|
|
614
|
+
const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr1.md'], { cwd: root, encoding: 'utf8' });
|
|
615
|
+
const led1 = readLedgerOf(root);
|
|
616
|
+
check('S34 Trusted 隐式:非 TTY 无旗标 L1 draft→approved + 台账 source=ai-auto-trust-L2',
|
|
617
|
+
r1.status === 0 && fs.readFileSync(p, 'utf8').includes('状态: approved')
|
|
618
|
+
&& led1.length === 1 && led1[0].source === 'ai-auto-trust-L2' && led1[0].stage === 'approved' && led1[0].prev === 'draft',
|
|
619
|
+
JSON.stringify({ status: r1.status, stderr: String(r1.stderr).slice(0, 300), led: led1 }));
|
|
620
|
+
// 第二跳:非 git 环境 done 门跳过(S27 同口径)→ Level 2 全闭环直达 done
|
|
621
|
+
const r2 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr1.md'], { cwd: root, encoding: 'utf8' });
|
|
622
|
+
const led2 = readLedgerOf(root);
|
|
623
|
+
check('S34b Trusted 隐式第二跳:approved→done(Level 2 全闭环)',
|
|
624
|
+
r2.status === 0 && fs.readFileSync(p, 'utf8').includes('状态: done')
|
|
625
|
+
&& led2.length === 2 && led2[1].stage === 'done' && led2[1].prev === 'approved',
|
|
626
|
+
JSON.stringify({ status: r2.status, stderr: String(r2.stderr).slice(0, 300), led: led2 }));
|
|
627
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
628
|
+
}
|
|
629
|
+
// S35:Trusted 隐式边界——L2 文档与 incidents 均不过自治门(exit 2 未落账)
|
|
630
|
+
{
|
|
631
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr2-'));
|
|
632
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
633
|
+
fs.mkdirSync(path.join(root, 'workflow', 'incidents'), { recursive: true });
|
|
634
|
+
writeTrust(root, { enabled: true, level: 2, name: 'Trusted' });
|
|
635
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-tr2.md'), '---\n状态: draft\n级别: L2\n日期: 2026-09-30\n---\n# I\n');
|
|
636
|
+
fs.writeFileSync(path.join(root, 'workflow', 'incidents', '2026-09-30-tr3.md'), '---\n状态: open\n级别: L1\n---\n# I\n');
|
|
637
|
+
const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr2.md'], { cwd: root, encoding: 'utf8' });
|
|
638
|
+
const r2 = spawnSync(process.execPath, [CLI, 'workflow/incidents/2026-09-30-tr3.md'], { cwd: root, encoding: 'utf8' });
|
|
639
|
+
check('S35 Trusted 隐式边界:L2 文档与 incidents 均拒绝(exit 2 未过自治门)+ 零台账',
|
|
640
|
+
r1.status === 2 && r2.status === 2 && readLedgerOf(root).length === 0,
|
|
641
|
+
JSON.stringify({ r1: r1.status, r2: r2.status, stderr1: String(r1.stderr).slice(0, 200) }));
|
|
642
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
643
|
+
}
|
|
644
|
+
// S36:Standard(level 1)不隐式——非 TTY 无旗标仍拒(隐式泳道仅 Trusted;fail-closed)
|
|
645
|
+
{
|
|
646
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr3-'));
|
|
647
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
648
|
+
writeTrust(root, { enabled: true, level: 1, name: 'Standard' });
|
|
649
|
+
const p = path.join(root, 'workflow', 'intents', '2026-09-30-tr4.md');
|
|
650
|
+
fs.writeFileSync(p, '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
|
|
651
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr4.md'], { cwd: root, encoding: 'utf8' });
|
|
652
|
+
check('S36 Standard 不隐式:非 TTY 无旗标 → 仍 exit 1 拒绝 + 零写盘',
|
|
653
|
+
r.status === 1 && /不可代确认/.test(r.stderr) && fs.readFileSync(p, 'utf8').includes('状态: draft'),
|
|
654
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
655
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
656
|
+
}
|
|
657
|
+
// S37:Trusted 隐式边界——多份 / --to 放弃态 → 不隐式,仍拒(exit 1)
|
|
658
|
+
{
|
|
659
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tr4-'));
|
|
660
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
661
|
+
writeTrust(root, { enabled: true, level: 2, name: 'Trusted' });
|
|
662
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-tr5.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
|
|
663
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-30-tr6.md'), '---\n状态: draft\n级别: L1\n日期: 2026-09-30\n---\n# I\n');
|
|
664
|
+
const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr5.md', 'workflow/intents/2026-09-30-tr6.md'], { cwd: root, encoding: 'utf8' });
|
|
665
|
+
const r2 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-30-tr5.md', '--to', 'cancelled'], { cwd: root, encoding: 'utf8' });
|
|
666
|
+
check('S37 Trusted 隐式边界:多份 / --to 放弃态 → 不隐式仍拒(exit 1 + 零台账)',
|
|
667
|
+
r1.status === 1 && r2.status === 1 && /不可代确认/.test(r1.stderr) && readLedgerOf(root).length === 0,
|
|
668
|
+
JSON.stringify({ r1: r1.status, r2: r2.status, stderr1: String(r1.stderr).slice(0, 160), stderr2: String(r2.stderr).slice(0, 160) }));
|
|
669
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
318
670
|
}
|
|
319
671
|
|
|
320
672
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
@@ -27,7 +27,7 @@ if (!fs.existsSync(SRC)) {
|
|
|
27
27
|
console.error(`doctor.test.mjs:SKIP——未找到 ${SRC}(装户环境无包源 src/;本套件仅在包源仓库/dogfooding 跑 npm test 时有意义)`);
|
|
28
28
|
process.exit(0);
|
|
29
29
|
}
|
|
30
|
-
const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage } = await import(pathToFileURL(SRC).href);
|
|
30
|
+
const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage, checkDelegationSnapshotFreshness } = await import(pathToFileURL(SRC).href);
|
|
31
31
|
|
|
32
32
|
let pass = 0;
|
|
33
33
|
let fail = 0;
|
|
@@ -214,7 +214,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
214
214
|
'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
|
|
215
215
|
},
|
|
216
216
|
adapters: {
|
|
217
|
-
'.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
217
|
+
'.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
218
218
|
'.trae/commands/wf-build.md': `---\nname: wf-build\ndesc: trae\n---\n\n${CMD_BODY}`,
|
|
219
219
|
'.zcode/agents/implementer.md': `---\nname: impl\ndesc: zcode\n---\n\n${ROLE_BODY}`,
|
|
220
220
|
'.opencode/agents/implementer.md': `---\nname: impl\ndesc: oc\n---\n\n${ROLE_BODY}`,
|
|
@@ -249,7 +249,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
249
249
|
const root = mkfixAdapter({
|
|
250
250
|
pkgMarker: true,
|
|
251
251
|
authority: { 'commands/build.md': `---\ndesc: x\n---\n\n${CMD_BODY}` },
|
|
252
|
-
adapters: { '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n漂移` },
|
|
252
|
+
adapters: { '.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n漂移` },
|
|
253
253
|
});
|
|
254
254
|
const r = checkAdapterDrift(root);
|
|
255
255
|
check('场景 11:包源环境(templates/ + modules/ 同时存在)→ skipped=true',
|
|
@@ -291,7 +291,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
291
291
|
'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
|
|
292
292
|
},
|
|
293
293
|
adapters: {
|
|
294
|
-
'.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
294
|
+
'.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
295
295
|
},
|
|
296
296
|
});
|
|
297
297
|
const r = checkAdapterDrift(root);
|
|
@@ -372,5 +372,106 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
372
372
|
fs.rmSync(pkg, { recursive: true, force: true });
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
// ---- 场景 19:快照最新(snapshotMonth >= ledgerMonth)→ stale=false ----
|
|
376
|
+
{
|
|
377
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
|
|
378
|
+
fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
|
|
379
|
+
fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
|
|
380
|
+
'## 委派结果',
|
|
381
|
+
'',
|
|
382
|
+
'| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |',
|
|
383
|
+
'|------|----------|------------|------|------|',
|
|
384
|
+
'| 2026-09-20 | x | y | 一次通过 | z |',
|
|
385
|
+
'| 2026-09-26 | x | y | 一次通过 | z |',
|
|
386
|
+
'',
|
|
387
|
+
'## 月度聚合快照',
|
|
388
|
+
'',
|
|
389
|
+
'| 月份 | 有效任务 | 一次通过率 | 返工次数 |',
|
|
390
|
+
'|------|----------|------------|----------|',
|
|
391
|
+
'| 2026-09 | 2 | 100% | 0 |',
|
|
392
|
+
'',
|
|
393
|
+
].join('\n'));
|
|
394
|
+
const r = checkDelegationSnapshotFreshness(root);
|
|
395
|
+
check('场景 19:快照最新 → stale=false skipped=false',
|
|
396
|
+
r.stale === false && r.skipped === false && r.ledgerMonth === '2026-09' && r.snapshotMonth === '2026-09',
|
|
397
|
+
JSON.stringify(r));
|
|
398
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---- 场景 20:快照陈旧(snapshotMonth < ledgerMonth)→ stale=true ----
|
|
402
|
+
{
|
|
403
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
|
|
404
|
+
fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
|
|
405
|
+
fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
|
|
406
|
+
'## 委派结果',
|
|
407
|
+
'',
|
|
408
|
+
'| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |',
|
|
409
|
+
'|------|----------|------------|------|------|',
|
|
410
|
+
'| 2026-09-10 | x | y | 一次通过 | z |',
|
|
411
|
+
'',
|
|
412
|
+
'## 月度聚合快照',
|
|
413
|
+
'',
|
|
414
|
+
'| 月份 | 有效任务 | 一次通过率 | 返工次数 |',
|
|
415
|
+
'|------|----------|------------|----------|',
|
|
416
|
+
'| 2026-08 | 1 | 100% | 0 |',
|
|
417
|
+
'',
|
|
418
|
+
].join('\n'));
|
|
419
|
+
const r = checkDelegationSnapshotFreshness(root);
|
|
420
|
+
check('场景 20:快照陈旧(08 < 09)→ stale=true',
|
|
421
|
+
r.stale === true && r.skipped === false && r.ledgerMonth === '2026-09' && r.snapshotMonth === '2026-08',
|
|
422
|
+
JSON.stringify(r));
|
|
423
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---- 场景 21:有数据行但无快照行 → stale=true snapshotMonth=null ----
|
|
427
|
+
{
|
|
428
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
|
|
429
|
+
fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
|
|
430
|
+
fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
|
|
431
|
+
'## 委派结果',
|
|
432
|
+
'',
|
|
433
|
+
'| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |',
|
|
434
|
+
'|------|----------|------------|------|------|',
|
|
435
|
+
'| 2026-09-25 | x | y | 一次通过 | z |',
|
|
436
|
+
'',
|
|
437
|
+
].join('\n'));
|
|
438
|
+
const r = checkDelegationSnapshotFreshness(root);
|
|
439
|
+
check('场景 21:无快照行 → stale=true snapshotMonth=null',
|
|
440
|
+
r.stale === true && r.skipped === false && r.snapshotMonth === null,
|
|
441
|
+
JSON.stringify(r));
|
|
442
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---- 场景 22:无台账数据行 → skipped=true ----
|
|
446
|
+
{
|
|
447
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
|
|
448
|
+
fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
|
|
449
|
+
fs.writeFileSync(path.join(root, 'workflow', 'delegations.md'), [
|
|
450
|
+
'# delegations',
|
|
451
|
+
'',
|
|
452
|
+
'## 月度聚合快照',
|
|
453
|
+
'',
|
|
454
|
+
'| 月份 | 有效任务 |',
|
|
455
|
+
'|------|----------|',
|
|
456
|
+
'| 2026-09 | 0 |',
|
|
457
|
+
'',
|
|
458
|
+
].join('\n'));
|
|
459
|
+
const r = checkDelegationSnapshotFreshness(root);
|
|
460
|
+
check('场景 22:无数据行 → skipped=true',
|
|
461
|
+
r.skipped === true && r.stale === false,
|
|
462
|
+
JSON.stringify(r));
|
|
463
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---- 场景 23:delegations.md 不存在 → skipped=true ----
|
|
467
|
+
{
|
|
468
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-snap-'));
|
|
469
|
+
const r = checkDelegationSnapshotFreshness(root);
|
|
470
|
+
check('场景 23:文件不存在 → skipped=true',
|
|
471
|
+
r.skipped === true && r.stale === false,
|
|
472
|
+
JSON.stringify(r));
|
|
473
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
474
|
+
}
|
|
475
|
+
|
|
375
476
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
376
477
|
process.exit(fail ? 1 : 0);
|