agentic-flow-kit 0.5.0 → 0.6.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 +2 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
- package/modules/hosts/omp/agents/implementer.md +2 -0
- package/modules/hosts/omp/agents/independent-reviewer.md +2 -0
- package/modules/hosts/omp/agents/ui-verifier.md +2 -0
- package/modules/hosts/opencode/agents/implementer.md +2 -0
- package/modules/hosts/opencode/agents/independent-reviewer.md +2 -0
- package/modules/hosts/opencode/agents/ui-verifier.md +2 -0
- package/modules/hosts/opencode/commands/build.md +20 -1
- package/modules/hosts/opencode/commands/design.md +2 -0
- package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
- package/modules/hosts/opencode/commands/maintain.md +2 -3
- package/modules/hosts/opencode/commands/plan.md +4 -1
- package/modules/hosts/opencode/commands/review.md +1 -0
- package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
- package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
- package/modules/hosts/opencode/commands/test.md +4 -2
- package/modules/hosts/trae/agents/implementer.md +2 -0
- package/modules/hosts/trae/agents/independent-reviewer.md +2 -0
- package/modules/hosts/trae/agents/ui-verifier.md +2 -0
- package/modules/hosts/trae/commands/wf-build.md +20 -1
- package/modules/hosts/trae/commands/wf-design.md +2 -0
- package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
- package/modules/hosts/trae/commands/wf-maintain.md +2 -3
- package/modules/hosts/trae/commands/wf-plan.md +4 -1
- package/modules/hosts/trae/commands/wf-review.md +1 -0
- package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
- package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
- package/modules/hosts/trae/commands/wf-test.md +4 -2
- package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
- package/modules/hosts/trae/hooks.json +1 -1
- package/modules/hosts/zcode/agents/implementer.md +2 -0
- package/modules/hosts/zcode/agents/independent-reviewer.md +2 -0
- package/modules/hosts/zcode/agents/ui-verifier.md +2 -0
- package/package.json +2 -2
- package/src/add-gate.mjs +5 -3
- package/src/doctor.mjs +121 -18
- package/src/gate-dotnet-ca.test.mjs +124 -0
- package/src/init.mjs +79 -17
- package/src/init.test.mjs +60 -1
- package/src/render.mjs +64 -4
- package/src/run-tests.mjs +3 -0
- package/src/sync-hosts.mjs +3 -2
- package/src/sync.mjs +64 -20
- package/src/sync.test.mjs +42 -0
- package/src/trae-hooks.test.mjs +115 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/board/index.html +32 -3
- package/templates/_agents/commands/build.md +5 -4
- package/templates/_agents/commands/design.md +1 -0
- package/templates/_agents/commands/maintain.md +1 -2
- package/templates/_agents/commands/plan.md +2 -0
- package/templates/_agents/commands/test.md +2 -1
- package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
- package/templates/_agents/roles/implementer.md +2 -0
- package/templates/_agents/roles/independent-reviewer.md +2 -0
- package/templates/_agents/roles/ui-verifier.md +2 -0
- package/templates/_agents/scripts/agg-delegations.cjs +39 -30
- package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
- package/templates/_agents/scripts/check-loop.mjs +262 -16
- package/templates/_agents/scripts/check-loop.test.mjs +621 -0
- package/templates/_agents/scripts/confirm-doc.mjs +228 -0
- package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
- package/templates/_agents/scripts/doctor.test.mjs +74 -1
- package/templates/_agents/scripts/ensure-board.mjs +29 -2
- package/templates/_agents/scripts/gate-checklist.mjs +2 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +3 -3
- package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -3
- package/templates/_agents/scripts/kb-search.mjs +22 -8
- package/templates/_agents/scripts/kb-search.test.mjs +38 -8
- package/templates/_agents/scripts/source-sync-check.mjs +17 -3
- package/templates/_agents/scripts/source-sync-check.test.mjs +96 -8
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
- package/templates/_agents/scripts/workflow-board-server.mjs +64 -9
- package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
- package/templates/_agents/scripts/workflow-enums.mjs +4 -2
- package/templates/_agents/workflow-enums.txt +3 -3
- package/templates/_agents/workflows/_TEMPLATE.md +1 -0
- package/templates/_githooks/pre-commit +16 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// confirm-doc — 用户确认门(2026-09-26 confirm-gate-machine;2026-09-27 confirm-gate-delegated 加委托代录形态;同日 gate-coverage 收 incidents;同日 closing-coverage 加放弃态 --to)
|
|
3
|
+
// 唯一确认入口:workflow/{intents,specs,plans} 文档(draft→approved 起草确认 / approved→done 关单确认)
|
|
4
|
+
// 与 workflow/incidents 文档(open→fixed 修复落地确认 / fixed→closed 关单确认——不支持 open→closed 单跳,
|
|
5
|
+
// 强制两跳留痕;2026-09-28 起 check-loop 15 对账)。放弃态走显式 `--to`(2026-09-27 closing-coverage):
|
|
6
|
+
// --to cancelled 自 draft/approved/open/fixed(未确认过的东西谈不上被取代);--to superseded 自
|
|
7
|
+
// approved/done/fixed/closed(已确认/闭环的结论被新档取代)。check-loop 检查 15 按「确认指纹 + 台账配对」
|
|
8
|
+
// 对账(四终态另做内容绑定),无记录即 hard-block。
|
|
9
|
+
// 两形态:
|
|
10
|
+
// ① TTY 模式(默认):用户终端亲手运行、逐份过目全文、逐份键入「可以」——非交互环境直接拒绝
|
|
11
|
+
// (AI 会话的 spawnSync 无 TTY,本形态在 AI 手里跑不起来)。
|
|
12
|
+
// ② 委托代录模式(--delegated "<用户对话原话>",2026-09-27):用户已在对话中明确放行后,AI 代为
|
|
13
|
+
// 落态记账——免 TTY、免逐份问答,但台账行如实记 source:"chat-delegated" + quote:<原话>,
|
|
14
|
+
// 永不伪装 TTY 行。调用纪律(AI 须先取得用户对话内明确确认)由 AGENTS.md 确认门条款承载,
|
|
15
|
+
// quote 原话入账供事后对质。
|
|
16
|
+
// 台账:.agents/confirmations.jsonl(入 git、追加式)——每行 {ts, doc, stage, fingerprint(64位), prev,
|
|
17
|
+
// source:"tty"|"chat-delegated", quote?(仅委托行), batch, seq, of}。
|
|
18
|
+
// batch/seq/of(2026-09-28 batch-ledger-audit 新增,纯增字段、向后兼容、历史行不回填):
|
|
19
|
+
// 记「本次进程调用落了几份态」这一**写入时确定已知的事实**——batch 为本次调用生成的短随机串
|
|
20
|
+
// (无时间语义、无全局唯一要求,仅需单台账内不碰撞),seq 为该次调用内件序(从 1),of 为本次
|
|
21
|
+
// 调用总份数。check-loop 检查 15 的并录审计据此直读事实判定,不再从 quote 相等 + ts 接近反推
|
|
22
|
+
// (旧判据可被「换 quote」平凡规避、且合规逐件复用同句必然误报——见同名 incident)。
|
|
23
|
+
// 指纹:内容 CRLF 归一 → 剔除「确认指纹:」行(防自引用)→ sha256;frontmatter 存前 16 位,台账存全量。
|
|
24
|
+
// 用法:node .agents/scripts/confirm-doc.mjs <workflow/intents|x.md> [<doc2>...] [--root <仓库根>]
|
|
25
|
+
// node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<用户对话原话>"(委托代录)
|
|
26
|
+
// node .agents/scripts/confirm-doc.mjs <doc...> --to superseded|cancelled(放弃态:取代/取消,留指纹与台账)
|
|
27
|
+
// 多文档一次传入:仅 TTY 模式(用户亲手逐份过目键入——天然逐件);--delegated 一次仅一份
|
|
28
|
+
// (2026-09-27 confirm-gate-one-per-call:多份并录曾系统性塌掉 build.md「逐件确认」三道门)。
|
|
29
|
+
// 测试:node templates/_agents/scripts/confirm-doc.test.mjs(纯函数逐项 + 非 TTY spawn 拒绝断言 + 委托场景)
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import readline from 'node:readline';
|
|
33
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
37
|
+
export function computeFingerprint(text) {
|
|
38
|
+
const norm = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
39
|
+
.filter((l) => !/^确认指纹:/.test(l))
|
|
40
|
+
.join('\n');
|
|
41
|
+
return createHash('sha256').update(norm, 'utf8').digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// nextStage(status):唯一合法前向跳转(docs 两跳 + incidents 两跳);其余(done/closed/superseded/cancelled/缺失)返回 null
|
|
45
|
+
export function nextStage(status) {
|
|
46
|
+
if (status === 'draft') return 'approved';
|
|
47
|
+
if (status === 'approved') return 'done';
|
|
48
|
+
if (status === 'open') return 'fixed'; // incidents:修复落地确认(三件套全落地)
|
|
49
|
+
if (status === 'fixed') return 'closed'; // incidents:关单确认(防复发验证已落地)
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 放弃态跳转表(2026-09-27 closing-coverage):cancelled 自未确认/进行态,superseded 自已确认态——
|
|
54
|
+
// draft/open 未被确认过,谈不上「被取代」,只能 cancelled;终态间仅允许 done/closed→superseded(结论被新档取代),其余互跳拒绝
|
|
55
|
+
const CANCELLABLE = new Set(['draft', 'approved', 'open', 'fixed']);
|
|
56
|
+
const SUPERSEDABLE = new Set(['approved', 'done', 'fixed', 'closed']);
|
|
57
|
+
export const ABANDON_TARGETS = ['superseded', 'cancelled'];
|
|
58
|
+
|
|
59
|
+
// resolveTransition(status, to):带 --to 目标的跳转裁决(pure)。to 为空 = 默认前向(nextStage);
|
|
60
|
+
// to ∈ {superseded, cancelled} 按上表判合法性;其余值或非法组合返回 null
|
|
61
|
+
export function resolveTransition(status, to) {
|
|
62
|
+
if (!to) return nextStage(status);
|
|
63
|
+
if (to === 'cancelled') return CANCELLABLE.has(status) ? 'cancelled' : null;
|
|
64
|
+
if (to === 'superseded') return SUPERSEDABLE.has(status) ? 'superseded' : null;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// applyTransition(text, targetStage, fingerprint16):frontmatter 内只改「状态:」行值 + 增/换「确认指纹:」行,
|
|
69
|
+
// 其余字节原样保留;无 frontmatter(首行非 ---)或未闭合 → 返回 null(调用方跳过不写)
|
|
70
|
+
export function applyTransition(text, targetStage, fingerprint16) {
|
|
71
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n');
|
|
72
|
+
if (!/^---\s*$/.test(lines[0] || '')) return null;
|
|
73
|
+
let end = -1;
|
|
74
|
+
for (let i = 1; i < lines.length; i++) {
|
|
75
|
+
if (/^---\s*$/.test(lines[i])) { end = i; break; }
|
|
76
|
+
}
|
|
77
|
+
if (end === -1) return null;
|
|
78
|
+
let hasStatus = false;
|
|
79
|
+
let hasFp = false;
|
|
80
|
+
for (let i = 1; i < end; i++) {
|
|
81
|
+
if (!hasStatus && /^状态:/.test(lines[i])) { lines[i] = `状态: ${targetStage}`; hasStatus = true; }
|
|
82
|
+
else if (!hasFp && /^确认指纹:/.test(lines[i])) { lines[i] = `确认指纹: ${fingerprint16}`; hasFp = true; }
|
|
83
|
+
}
|
|
84
|
+
const fm = lines.slice(1, end);
|
|
85
|
+
if (!hasStatus) fm.unshift(`状态: ${targetStage}`); // 防御:无状态键本应被 nextStage 拦,不至此
|
|
86
|
+
if (!hasFp) fm.push(`确认指纹: ${fingerprint16}`);
|
|
87
|
+
return [lines[0], ...fm, ...lines.slice(end)].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// appendLedger(root, entry):追加一行 JSON(目录不存在自动建;只 append 不重写)
|
|
91
|
+
export function appendLedger(root, entry) {
|
|
92
|
+
const p = path.join(root, '.agents', 'confirmations.jsonl');
|
|
93
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
94
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
95
|
+
return p;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DOC_RE = /^workflow\/(intents|specs|plans|incidents)\/[^/]+\.md$/;
|
|
99
|
+
|
|
100
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('confirm-doc.mjs');
|
|
101
|
+
if (isMain) {
|
|
102
|
+
const argv = process.argv.slice(2);
|
|
103
|
+
let root = process.cwd();
|
|
104
|
+
let delegatedQuote = null; // null = TTY 模式;字符串 = 委托代录的用户对话原话
|
|
105
|
+
let toTarget = null; // null = 默认前向跳转;superseded|cancelled = 放弃态显式目标
|
|
106
|
+
const docs = [];
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
if (argv[i] === '--root') root = path.resolve(argv[++i]);
|
|
109
|
+
else if (argv[i] === '--delegated') delegatedQuote = argv[++i] ?? '';
|
|
110
|
+
else if (argv[i] === '--to') toTarget = argv[++i] ?? '';
|
|
111
|
+
else docs.push(argv[i].replace(/\\/g, '/'));
|
|
112
|
+
}
|
|
113
|
+
if (toTarget !== null && !ABANDON_TARGETS.includes(toTarget)) {
|
|
114
|
+
console.error(`用法:--to 仅接受 superseded|cancelled(现「${toTarget}」)——cancelled 自 draft/approved/open/fixed;superseded 自 approved/done/fixed/closed(未确认态只能 cancelled;终态间仅允许 done/closed→superseded)`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const delegated = delegatedQuote !== null;
|
|
118
|
+
if (delegated && !String(delegatedQuote).trim()) {
|
|
119
|
+
console.error('用法:--delegated 须带用户对话原话(node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<用户原话>")——原话入台账供事后对质,不可缺省');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
// TTY 门(仅 TTY 模式;先于文档参数校验):AI 会话内非委托调用一律拒绝
|
|
123
|
+
// CONFIRM_DOC_TEST_TTY:**仅测试用**逃生门——本测试环境无 TTY,而 TTY 多文档分支
|
|
124
|
+
// (batch 相同 / seq 递增 / of=N)是并录审计的判别基础,必须可测(复核 P2-2)。
|
|
125
|
+
// 该变量只影响 isTTY 判定,不改动任何落态/记账语义;生产路径不设置该变量。
|
|
126
|
+
const ttyForced = process.env.CONFIRM_DOC_TEST_TTY === '1';
|
|
127
|
+
if (!delegated && !ttyForced && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
128
|
+
console.error('确认门须由用户在终端亲手运行(node .agents/scripts/confirm-doc.mjs <workflow/文档>...)——AI 会话内不可代确认;用户对话内明确确认后可用 --delegated "<用户原话>" 委托代录(台账如实记来源)');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
if (!docs.length) {
|
|
132
|
+
console.error('用法:node .agents/scripts/confirm-doc.mjs <workflow/intents|specs|plans/x.md> [...] [--root <仓库根>] [--delegated "<用户原话>"]');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (delegated) {
|
|
136
|
+
console.error('⚠️ 委托代录模式:确认语义 = 用户已在对话中明确放行;台账行将如实记 source=chat-delegated 与原话,不伪装 TTY 确认');
|
|
137
|
+
console.error(` 用户原话:「${delegatedQuote}」`);
|
|
138
|
+
// 单文档强制(2026-09-27 confirm-gate-one-per-call):delegated 没有TTY「逐份过目」的天然机制——
|
|
139
|
+
// 多文档并录曾系统性塌掉三道阶段门(build.md「逐件确认不得并作一次」)。逐件调用:一次一份、
|
|
140
|
+
// 每次带当次用户原话。TTY 形态不受限(用户亲手逐份过目键入,天然逐件)。
|
|
141
|
+
if (docs.length > 1) {
|
|
142
|
+
console.error(`❌ --delegated 一次仅接受一份文档(现 ${docs.length} 份:${docs.join(' ')})——逐件确认口径(build.md):每份一次调用、每次带当次用户原话`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// 调用事实(2026-09-28 batch-ledger-audit):本次进程调用的批次标识与件序——
|
|
147
|
+
// 供 check-loop 检查 15 的并录审计直读(替代「同 quote + ts 接近」的反推)。
|
|
148
|
+
const batch = randomBytes(3).toString('hex');
|
|
149
|
+
let seq = 1;
|
|
150
|
+
// 应答来源(**仅测试用**注入,见 CONFIRM_DOC_TEST_ANSWERS):TTY 多文档路径的 batch/seq/of 是并录
|
|
151
|
+
// 审计的判别基础,必须可端到端验证;而 spawnSync 的管道 stdin 是「写完即关」,readline 会在第二个
|
|
152
|
+
// question 注册前吞掉后续行、且 stdin 已 EOF → 第 2 份必然挂住(**不是无 TTY 的限制**,是管道 EOF;
|
|
153
|
+
// 2026-09-28 复核 P2-2 更正:此前测试注释把根因误记为「管道输入不被逐次消费」)。
|
|
154
|
+
// 故提供问答注入:按序取预置应答,绕开 stdin/EOF 时序,不改动任何落态与记账语义。
|
|
155
|
+
const injected = process.env.CONFIRM_DOC_TEST_ANSWERS !== undefined
|
|
156
|
+
? String(process.env.CONFIRM_DOC_TEST_ANSWERS).split(',').map((s) => s.trim())
|
|
157
|
+
: null;
|
|
158
|
+
const rl = delegated || injected ? null : readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
159
|
+
const ask = (q) => new Promise((res) => {
|
|
160
|
+
if (injected) {
|
|
161
|
+
process.stdout.write(q);
|
|
162
|
+
return res(injected.shift() ?? '');
|
|
163
|
+
}
|
|
164
|
+
return rl.question(q, res);
|
|
165
|
+
});
|
|
166
|
+
let confirmed = 0;
|
|
167
|
+
for (const doc of docs) {
|
|
168
|
+
if (!DOC_RE.test(doc)) {
|
|
169
|
+
console.error(`跳过 ${doc}:路径须匹配 workflow/{intents,specs,plans,incidents}/<文件>.md`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const abs = path.join(root, doc);
|
|
173
|
+
let text;
|
|
174
|
+
try { text = fs.readFileSync(abs, 'utf8'); }
|
|
175
|
+
catch { console.error(`跳过 ${doc}:文件不可读`); continue; }
|
|
176
|
+
// 状态键只在 frontmatter 区(首 --- 至闭合 ---)取——防正文误命中
|
|
177
|
+
const nl = text.replace(/\r\n/g, '\n').split('\n');
|
|
178
|
+
let st = '';
|
|
179
|
+
if (/^---\s*$/.test(nl[0] || '')) {
|
|
180
|
+
for (let i = 1; i < nl.length && !/^---\s*$/.test(nl[i]); i++) {
|
|
181
|
+
const m = nl[i].match(/^状态:\s*(.*)$/);
|
|
182
|
+
if (m) { st = m[1].trim(); break; }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const target = resolveTransition(st, toTarget);
|
|
186
|
+
if (!target) {
|
|
187
|
+
console.error(`跳过 ${doc}:当前状态「${st || '缺失'}」无合法跳转(前向:docs draft→approved / approved→done;incidents open→fixed / fixed→closed;放弃 --to:cancelled 自 draft/approved/open/fixed,superseded 自 approved/done/fixed/closed)`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const fp = computeFingerprint(text);
|
|
191
|
+
if (delegated) {
|
|
192
|
+
fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
|
|
193
|
+
appendLedger(root, {
|
|
194
|
+
ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
|
|
195
|
+
source: 'chat-delegated', quote: String(delegatedQuote),
|
|
196
|
+
// 调用事实(batch-ledger-audit):seq=本次调用内件序(从 1),of=本次调用总份数
|
|
197
|
+
batch, seq, of: docs.length,
|
|
198
|
+
});
|
|
199
|
+
console.log(`✓ ${doc} ${st} → ${target}(指纹 ${fp.slice(0, 16)},代录已记账:source=chat-delegated)`);
|
|
200
|
+
confirmed++;
|
|
201
|
+
seq++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
console.log('\n'.repeat(3) + '='.repeat(72));
|
|
205
|
+
console.log(`文档:${doc} 状态:${st} → ${target} 指纹:${fp.slice(0, 16)}`);
|
|
206
|
+
console.log('='.repeat(72));
|
|
207
|
+
console.log(text);
|
|
208
|
+
console.log('='.repeat(72));
|
|
209
|
+
const ans = (await ask(`键入"可以"确认本份(${st} → ${target}),其他任意输入跳过:`)).trim();
|
|
210
|
+
if (ans !== '可以') {
|
|
211
|
+
console.log(`× 已跳过 ${doc}(未落状态)`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
|
|
215
|
+
appendLedger(root, {
|
|
216
|
+
ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
|
|
217
|
+
source: 'tty',
|
|
218
|
+
batch, seq, of: docs.length, // 调用事实(形态无关:TTY 多文档天然逐件过目,of>1 不构成违规)
|
|
219
|
+
});
|
|
220
|
+
console.log(`✓ ${doc} → ${target}(指纹 ${fp.slice(0, 16)},台账已记)`);
|
|
221
|
+
confirmed++;
|
|
222
|
+
seq++;
|
|
223
|
+
}
|
|
224
|
+
if (rl) rl.close();
|
|
225
|
+
console.log(`\n完成:确认 ${confirmed} 份 / 跳过 ${docs.length - confirmed} 份${delegated ? '(委托代录)' : ''}`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
export default { computeFingerprint, nextStage, applyTransition, appendLedger };
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// confirm-doc.test.mjs — 用户确认门测试(2026-09-26 confirm-gate-machine;2026-09-27 confirm-gate-delegated 补委托场景)
|
|
3
|
+
// 判据:① 指纹算法(CRLF 归一 / 剔指纹行防自引用 / 内容敏感) ② 跳转唯一合法性 ③ 落态只动两行
|
|
4
|
+
// ④ 台账追加 schema ⑤ 核心——非 TTY spawn(模拟 AI 调用路径)无 --delegated 必须被拒
|
|
5
|
+
// ⑥ 委托代录(--delegated):免 TTY 落态 + 台账如实记 source/quote;空原话拒跑
|
|
6
|
+
// 用法:node templates/_agents/scripts/confirm-doc.test.mjs(npm test 随跑)
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { computeFingerprint, nextStage, applyTransition, appendLedger, resolveTransition } from './confirm-doc.mjs';
|
|
13
|
+
|
|
14
|
+
let pass = 0;
|
|
15
|
+
let fail = 0;
|
|
16
|
+
const check = (desc, cond, detail = '') => {
|
|
17
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
18
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const CLI = path.join(SCRIPT_DIR, 'confirm-doc.mjs');
|
|
23
|
+
|
|
24
|
+
// ---- S1 指纹:CRLF 归一(同内容不同行尾同指纹)----
|
|
25
|
+
{
|
|
26
|
+
const lf = '---\n状态: draft\n---\n正文';
|
|
27
|
+
const crlf = lf.replace(/\n/g, '\r\n');
|
|
28
|
+
check('S1 CRLF 归一——同内容不同行尾指纹相同', computeFingerprint(lf) === computeFingerprint(crlf));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---- S2 指纹:剔「确认指纹:」行(防自引用——加指纹行不改指纹)----
|
|
32
|
+
{
|
|
33
|
+
const a = '---\n状态: draft\n---\n正文';
|
|
34
|
+
const b = '---\n状态: draft\n确认指纹: abc123\n---\n正文';
|
|
35
|
+
check('S2 指纹行不参与指纹计算(防自引用)', computeFingerprint(a) === computeFingerprint(b));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- S3 指纹:内容敏感(一字之差指纹不同;64 位 hex)----
|
|
39
|
+
{
|
|
40
|
+
const fp1 = computeFingerprint('---\n状态: draft\n---\n正文A');
|
|
41
|
+
const fp2 = computeFingerprint('---\n状态: draft\n---\n正文B');
|
|
42
|
+
check('S3 内容敏感且为 64 位 hex', fp1 !== fp2 && /^[0-9a-f]{64}$/.test(fp1));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- S4 nextStage:合法前向跳转(docs 两跳 + incidents 两跳,2026-09-27 gate-coverage)----
|
|
46
|
+
{
|
|
47
|
+
check('S4 跳转表:draft→approved、approved→done、done/空/自造值→null',
|
|
48
|
+
nextStage('draft') === 'approved' && nextStage('approved') === 'done'
|
|
49
|
+
&& nextStage('done') === null && nextStage('') === null && nextStage('进行中') === null);
|
|
50
|
+
check('S4 incidents 跳转表:open→fixed、fixed→closed、closed→null(无 open→closed 单跳)',
|
|
51
|
+
nextStage('open') === 'fixed' && nextStage('fixed') === 'closed'
|
|
52
|
+
&& nextStage('closed') === null && nextStage('open') !== 'closed');
|
|
53
|
+
// --to 放弃态跳转表(2026-09-27 closing-coverage):cancelled 自未确认/进行态;superseded 自已确认态
|
|
54
|
+
const cancelOk = ['draft', 'approved', 'open', 'fixed'].every((s) => resolveTransition(s, 'cancelled') === 'cancelled');
|
|
55
|
+
const supersedeOk = ['approved', 'done', 'fixed', 'closed'].every((s) => resolveTransition(s, 'superseded') === 'superseded');
|
|
56
|
+
check('S4 --to 合法表:cancelled×{draft,approved,open,fixed} / superseded×{approved,done,fixed,closed} 全通',
|
|
57
|
+
cancelOk && supersedeOk);
|
|
58
|
+
check('S4 --to 非法表:draft→superseded / open→superseded / done→cancelled / cancelled→cancelled / 非法目标值 全拒',
|
|
59
|
+
resolveTransition('draft', 'superseded') === null && resolveTransition('open', 'superseded') === null
|
|
60
|
+
&& resolveTransition('done', 'cancelled') === null && resolveTransition('cancelled', 'cancelled') === null
|
|
61
|
+
&& resolveTransition('approved', 'deleted') === null && resolveTransition('approved', null) === 'done');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- S5 applyTransition:只动状态行 + 增指纹行,正文逐字节原样 ----
|
|
65
|
+
{
|
|
66
|
+
const src = '---\n状态: draft\n级别: L2\n---\n\n# PLAN — x\n\n正文含 状态: 假行 不受影响';
|
|
67
|
+
const out = applyTransition(src, 'approved', 'a3f9000000000000');
|
|
68
|
+
check('S5 落态:状态行改值、指纹行追加、级别行与正文原样',
|
|
69
|
+
out.startsWith('---\n状态: approved\n级别: L2\n确认指纹: a3f9000000000000\n---\n\n# PLAN — x\n\n正文含 状态: 假行 不受影响'),
|
|
70
|
+
JSON.stringify(out));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- S6 applyTransition:已有指纹行则覆盖(done 跳换新指纹)----
|
|
74
|
+
{
|
|
75
|
+
const src = '---\n状态: approved\n确认指纹: old\n---\nX';
|
|
76
|
+
const out = applyTransition(src, 'done', 'new1234567890abcd');
|
|
77
|
+
check('S6 已有指纹行覆盖为新值', out.includes('状态: done') && out.includes('确认指纹: new1234567890abcd') && !out.includes('old'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- S7 applyTransition:无 frontmatter → null 不写 ----
|
|
81
|
+
{
|
|
82
|
+
check('S7 无 frontmatter 返回 null', applyTransition('# 无 fm\n状态: draft', 'approved', 'x') === null);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- S8 appendLedger:追加行 schema(ts/doc/stage/fingerprint/prev)----
|
|
86
|
+
{
|
|
87
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-led-'));
|
|
88
|
+
appendLedger(root, { ts: 'T', doc: 'workflow/plans/a.md', stage: 'approved', fingerprint: 'f'.repeat(64), prev: 'draft' });
|
|
89
|
+
const lines = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n');
|
|
90
|
+
const j = JSON.parse(lines[0]);
|
|
91
|
+
check('S8 台账追加且 schema 齐', lines.length === 1 && j.doc === 'workflow/plans/a.md' && j.stage === 'approved' && j.fingerprint.length === 64 && j.prev === 'draft');
|
|
92
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- S9【核心】非 TTY spawn(AI 调用路径)必须被拒 ----
|
|
96
|
+
{
|
|
97
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tty-'));
|
|
98
|
+
const docP = path.join(root, 'workflow', 'plans');
|
|
99
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
100
|
+
fs.writeFileSync(path.join(docP, '2026-09-27-x.md'), '---\n状态: draft\n---\n# P');
|
|
101
|
+
// spawnSync 无 TTY(管道 stdin)——模拟 AI 会话内调用;即使参数完全合法也必须拒绝
|
|
102
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-27-x.md'], { cwd: root, encoding: 'utf8', input: '可以\n' });
|
|
103
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-27-x.md'), 'utf8');
|
|
104
|
+
check('S9 非 TTY 调用被拒:exit 1 + 提示 + 文档未被改动(AI 无法代确认)',
|
|
105
|
+
r.status === 1 && /不可代确认/.test(r.stderr) && after === '---\n状态: draft\n---\n# P',
|
|
106
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, after }));
|
|
107
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- S10 非 TTY 拒绝先于参数校验(无参数同拒)----
|
|
111
|
+
{
|
|
112
|
+
const r = spawnSync(process.execPath, [CLI], { encoding: 'utf8' });
|
|
113
|
+
check('S10 TTY 门最先(无参数也是拒绝而非用法提示)', r.status === 1 && /不可代确认/.test(r.stderr));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- S11 委托代录:非 TTY spawn + --delegated → 免 TTY 落态 + 台账如实记 source/quote ----
|
|
117
|
+
{
|
|
118
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-del-'));
|
|
119
|
+
const docP = path.join(root, 'workflow', 'plans');
|
|
120
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
121
|
+
fs.writeFileSync(path.join(docP, '2026-09-27-d.md'), '---\n状态: draft\n级别: L2\n---\n# P\n');
|
|
122
|
+
const before = fs.readFileSync(path.join(docP, '2026-09-27-d.md'), 'utf8');
|
|
123
|
+
const fpExpect = computeFingerprint(before);
|
|
124
|
+
// spawnSync 无 TTY(管道 stdin)——模拟 AI 会话内委托代录调用
|
|
125
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-27-d.md', '--delegated', '2选2'], { cwd: root, encoding: 'utf8' });
|
|
126
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-27-d.md'), 'utf8');
|
|
127
|
+
const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
128
|
+
const j = led[led.length - 1];
|
|
129
|
+
check('S11 委托代录:exit 0 + draft→approved + 指纹行 + 台账 source=chat-delegated / quote=原话 / 指纹全量与 frontmatter 前 16 位配对',
|
|
130
|
+
r.status === 0 && after.includes('状态: approved') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
|
|
131
|
+
&& j.source === 'chat-delegated' && j.quote === '2选2' && j.stage === 'approved' && j.prev === 'draft'
|
|
132
|
+
&& j.fingerprint === fpExpect,
|
|
133
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, ledger: led }));
|
|
134
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- S12 委托代录第二跳:approved→done 换新指纹(台账行仍如实记来源)----
|
|
138
|
+
{
|
|
139
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-del2-'));
|
|
140
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
141
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
142
|
+
const fp1 = computeFingerprint('---\n状态: draft\n---\n# I\n');
|
|
143
|
+
fs.writeFileSync(path.join(docP, '2026-09-27-e.md'), `---\n状态: approved\n确认指纹: ${fp1.slice(0, 16)}\n---\n# I\n`);
|
|
144
|
+
const before = fs.readFileSync(path.join(docP, '2026-09-27-e.md'), 'utf8');
|
|
145
|
+
const fp2 = computeFingerprint(before);
|
|
146
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-e.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
147
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-27-e.md'), 'utf8');
|
|
148
|
+
const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
149
|
+
check('S12 委托第二跳:approved→done + 指纹行换新值 + 台账 stage=done / source 如实',
|
|
150
|
+
r.status === 0 && after.includes('状态: done') && after.includes(`确认指纹: ${fp2.slice(0, 16)}`) && !after.includes(fp1.slice(0, 16))
|
|
151
|
+
&& led[led.length - 1].stage === 'done' && led[led.length - 1].source === 'chat-delegated',
|
|
152
|
+
JSON.stringify({ status: r.status, after }));
|
|
153
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- S13 委托代录:--delegated 空原话 → exit 1 用法提示,零文件改动 ----
|
|
157
|
+
{
|
|
158
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-del3-'));
|
|
159
|
+
const docP = path.join(root, 'workflow', 'plans');
|
|
160
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
161
|
+
fs.writeFileSync(path.join(docP, '2026-09-27-f.md'), '---\n状态: draft\n---\n# P\n');
|
|
162
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-27-f.md', '--delegated', ' '], { cwd: root, encoding: 'utf8' });
|
|
163
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-27-f.md'), 'utf8');
|
|
164
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
165
|
+
check('S13 空原话拒跑:exit 1 + 用法提示 + 文档未动 + 无台账',
|
|
166
|
+
r.status === 1 && /--delegated 须带用户对话原话/.test(r.stderr) && after === '---\n状态: draft\n---\n# P\n' && noLedger,
|
|
167
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
168
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- S15 incidents 委托代录(2026-09-27 gate-coverage):open→fixed 落态 + 台账 stage=fixed ----
|
|
172
|
+
{
|
|
173
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-inc-'));
|
|
174
|
+
const docP = path.join(root, 'workflow', 'incidents');
|
|
175
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
176
|
+
fs.writeFileSync(path.join(docP, '2026-09-28-i.md'), '---\n状态: open\n级别: L1\n---\n# I\n');
|
|
177
|
+
const before = fs.readFileSync(path.join(docP, '2026-09-28-i.md'), 'utf8');
|
|
178
|
+
const fpExpect = computeFingerprint(before);
|
|
179
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/incidents/2026-09-28-i.md', '--delegated', '修好了'], { cwd: root, encoding: 'utf8' });
|
|
180
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-28-i.md'), 'utf8');
|
|
181
|
+
const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
182
|
+
const j = led[led.length - 1];
|
|
183
|
+
check('S15 incidents 委托代录:exit 0 + open→fixed + 指纹行 + 台账 stage=fixed / prev=open',
|
|
184
|
+
r.status === 0 && after.includes('状态: fixed') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
|
|
185
|
+
&& j.source === 'chat-delegated' && j.quote === '修好了' && j.stage === 'fixed' && j.prev === 'open'
|
|
186
|
+
&& j.fingerprint === fpExpect,
|
|
187
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, ledger: led }));
|
|
188
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- S16 放弃态 --to(2026-09-27 closing-coverage):draft --to cancelled 委托代录落态 + 台账 ----
|
|
192
|
+
{
|
|
193
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-to-'));
|
|
194
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
195
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
196
|
+
fs.writeFileSync(path.join(docP, '2026-09-28-c.md'), '---\n状态: draft\n级别: L1\n---\n# I\n');
|
|
197
|
+
const before = fs.readFileSync(path.join(docP, '2026-09-28-c.md'), 'utf8');
|
|
198
|
+
const fpExpect = computeFingerprint(before);
|
|
199
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-28-c.md', '--to', 'cancelled', '--delegated', '不做了'], { cwd: root, encoding: 'utf8' });
|
|
200
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-28-c.md'), 'utf8');
|
|
201
|
+
const led = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
202
|
+
const j = led[led.length - 1];
|
|
203
|
+
check('S16 --to cancelled 委托代录:exit 0 + draft→cancelled + 台账 stage=cancelled / prev=draft',
|
|
204
|
+
r.status === 0 && after.includes('状态: cancelled') && after.includes(`确认指纹: ${fpExpect.slice(0, 16)}`)
|
|
205
|
+
&& j.stage === 'cancelled' && j.prev === 'draft' && j.quote === '不做了' && j.fingerprint === fpExpect,
|
|
206
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, ledger: led }));
|
|
207
|
+
// 调用事实(2026-09-28 batch-ledger-audit):委托单份 → of=1、seq=1、batch 为非空字符串
|
|
208
|
+
// (check-loop 检查 15 的并录审计据此判「是否一次调用落多份」——不再从 quote/ts 反推)
|
|
209
|
+
check('S16b 台账行含调用事实 batch/seq/of 且单份 = of=1 / seq=1',
|
|
210
|
+
typeof j.batch === 'string' && j.batch.length > 0 && j.seq === 1 && j.of === 1,
|
|
211
|
+
JSON.stringify({ batch: j.batch, seq: j.seq, of: j.of }));
|
|
212
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---- S17 --to 非法组合拒跑:done --to cancelled → 跳过且不落台账 ----
|
|
216
|
+
{
|
|
217
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-to2-'));
|
|
218
|
+
const docP = path.join(root, 'workflow', 'plans');
|
|
219
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
220
|
+
fs.writeFileSync(path.join(docP, '2026-09-28-d.md'), '---\n状态: done\n级别: L1\n---\n# P\n');
|
|
221
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-28-d.md', '--to', 'cancelled', '--delegated', '试试'], { cwd: root, encoding: 'utf8' });
|
|
222
|
+
const after = fs.readFileSync(path.join(docP, '2026-09-28-d.md'), 'utf8');
|
|
223
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
224
|
+
check('S17 --to 非法组合(done→cancelled):跳过不落态 + 无台账 + 文档未动',
|
|
225
|
+
/无合法跳转/.test(r.stderr || '') && after.includes('状态: done') && noLedger,
|
|
226
|
+
JSON.stringify({ stderr: r.stderr }));
|
|
227
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---- S18 delegated 单文档强制(2026-09-27 confirm-gate-one-per-call)----
|
|
231
|
+
// 多文档并录曾系统性塌掉三道阶段门(build.md「逐件确认不得并作一次」)——机器层收口
|
|
232
|
+
{
|
|
233
|
+
const mk2docs = () => {
|
|
234
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-single-'));
|
|
235
|
+
for (const sub of ['intents', 'plans']) {
|
|
236
|
+
fs.mkdirSync(path.join(root, 'workflow', sub), { recursive: true });
|
|
237
|
+
fs.writeFileSync(path.join(root, 'workflow', sub, '2026-09-27-s.md'), `---\n状态: draft\n级别: L1\n---\n# ${sub === 'intents' ? 'I' : 'P'}\n`);
|
|
238
|
+
}
|
|
239
|
+
return root;
|
|
240
|
+
};
|
|
241
|
+
{
|
|
242
|
+
const root = mk2docs();
|
|
243
|
+
const before1 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
|
|
244
|
+
const before2 = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
|
|
245
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md', '--delegated', '两份一起'], { cwd: root, encoding: 'utf8' });
|
|
246
|
+
const noLedger = !fs.existsSync(path.join(root, '.agents', 'confirmations.jsonl'));
|
|
247
|
+
const after1 = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
|
|
248
|
+
const after2 = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
|
|
249
|
+
check('S18 delegated 双文档 → exit 1 拒绝 + 逐件口径提示 + 零台账 + 文档零改动',
|
|
250
|
+
r.status === 1 && /一次仅接受一份文档/.test(r.stderr) && /逐件/.test(r.stderr)
|
|
251
|
+
&& noLedger && after1 === before1 && after2 === before2,
|
|
252
|
+
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
253
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
{
|
|
256
|
+
const root = mk2docs();
|
|
257
|
+
const r1 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', '--delegated', '第一份可以'], { cwd: root, encoding: 'utf8' });
|
|
258
|
+
const r2 = spawnSync(process.execPath, [CLI, 'workflow/plans/2026-09-27-s.md', '--delegated', '第一份可以'], { cwd: root, encoding: 'utf8' });
|
|
259
|
+
check('S18 delegated 单文档逐次调用照常(两次各落态各记账)',
|
|
260
|
+
r1.status === 0 && r2.status === 0
|
|
261
|
+
&& fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8').includes('状态: approved')
|
|
262
|
+
&& fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8').includes('状态: approved'),
|
|
263
|
+
JSON.stringify({ r1: r1.status, r2: r2.status, stderr: r2.stderr }));
|
|
264
|
+
// 调用事实(2026-09-28 batch-ledger-audit):每次进程调用 **一个** batch;两次独立调用 batch **不同**,
|
|
265
|
+
// 各自 seq=1 / of=1——这是并录审计「逐件 vs 并录」的判别基础(复核 P2-2:plan 要求覆盖此面)
|
|
266
|
+
const led2 = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
267
|
+
const [e1, e2] = led2.slice(-2);
|
|
268
|
+
check('S18b 两次独立调用各生成独立 batch(不同批、各自 seq=1/of=1)',
|
|
269
|
+
e1.batch !== e2.batch && e1.seq === 1 && e2.seq === 1 && e1.of === 1 && e2.of === 1,
|
|
270
|
+
JSON.stringify(led2.slice(-2)));
|
|
271
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
{
|
|
274
|
+
// 【复核 P2-2】单次调用落 N 份 → batch 相同 / seq 递增 1,2 / of=2 —— **端到端**验证 TTY 多文档路径。
|
|
275
|
+
// 该路径仅 TTY 可达(delegated 已被入口限为 1 份),故注入两个**仅测试用**逃生门:
|
|
276
|
+
// CONFIRM_DOC_TEST_TTY=1 —— 放行 isTTY 判定(本环境无 TTY)
|
|
277
|
+
// CONFIRM_DOC_TEST_ANSWERS —— 按序注入问答应答(见下「为什么不能用管道 stdin」)
|
|
278
|
+
// 两者均不改动任何落态/记账语义。
|
|
279
|
+
//
|
|
280
|
+
// **为什么不能用 spawnSync 的 input 管道(2026-09-28 更正)**:此前本用例用 `input:'可以\n可以\n'`
|
|
281
|
+
// 并断言「首份」了事,注释把根因误记为「readline 对第二份提问会 unsettled / 管道输入不被逐次消费」。
|
|
282
|
+
// 经复现,真实机制是:spawnSync 的管道**一次性写完即关闭写端**,readline 在第一个 question 注册前
|
|
283
|
+
// 就已把第二行读入并丢弃,且 stdin 随即 EOF —— 第二个 question 永远等不到输入。**与「有无 TTY」无关**,
|
|
284
|
+
// 是「管道 EOF + 跨 await 边界的 question 注册时序」所致(嵌套回调版同样两问两答可用,加 await 即挂)。
|
|
285
|
+
const root = mk2docs();
|
|
286
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md'], {
|
|
287
|
+
cwd: root, encoding: 'utf8',
|
|
288
|
+
env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以,可以' },
|
|
289
|
+
});
|
|
290
|
+
const ledN = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
291
|
+
const stI = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-09-27-s.md'), 'utf8');
|
|
292
|
+
const stP = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
|
|
293
|
+
check('S18c 单次调用落 2 份(TTY 多文档)端到端 → batch 相同 / seq 1,2 / of 均=2 / 两份都落态',
|
|
294
|
+
r.status === 0 && ledN.length === 2
|
|
295
|
+
&& ledN[0].batch === ledN[1].batch && typeof ledN[0].batch === 'string' && ledN[0].batch.length > 0
|
|
296
|
+
&& ledN[0].seq === 1 && ledN[1].seq === 2
|
|
297
|
+
&& ledN[0].of === 2 && ledN[1].of === 2
|
|
298
|
+
&& ledN[0].source === 'tty' && ledN[1].source === 'tty'
|
|
299
|
+
&& stI.includes('状态: approved') && stP.includes('状态: approved'),
|
|
300
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), ledger: ledN }));
|
|
301
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
{
|
|
304
|
+
// 【复核 P2-2】注入应答不足时,剩余文档按「未确认」跳过(不落态、不记账)——钉住注入语义的边界,
|
|
305
|
+
// 避免测试逃生门被误用成「无条件放行」。
|
|
306
|
+
const root = mk2docs();
|
|
307
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-27-s.md', 'workflow/plans/2026-09-27-s.md'], {
|
|
308
|
+
cwd: root, encoding: 'utf8',
|
|
309
|
+
env: { ...process.env, CONFIRM_DOC_TEST_TTY: '1', CONFIRM_DOC_TEST_ANSWERS: '可以' }, // 只给一份应答
|
|
310
|
+
});
|
|
311
|
+
const led1 = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
312
|
+
const stP1 = fs.readFileSync(path.join(root, 'workflow', 'plans', '2026-09-27-s.md'), 'utf8');
|
|
313
|
+
check('S18d 注入应答不足 → 仅首份落态,次份按跳过处理(不落态不记账)',
|
|
314
|
+
led1.length === 1 && led1[0].seq === 1 && led1[0].of === 2 && stP1.includes('状态: draft'),
|
|
315
|
+
JSON.stringify({ status: r.status, ledger: led1 }));
|
|
316
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
321
|
+
process.exit(fail ? 1 : 0);
|