agentic-flow-kit 1.8.0 → 1.8.2

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.
Files changed (49) hide show
  1. package/modules/hosts/claude/commands/wf-new-task.md +1 -0
  2. package/modules/hosts/claude/commands/wf-review.md +1 -1
  3. package/modules/hosts/codex/commands/wf-new-task.md +1 -0
  4. package/modules/hosts/codex/commands/wf-review.md +1 -1
  5. package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
  6. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  7. package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
  8. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  9. package/modules/hosts/trae/commands/wf-new-task.md +1 -0
  10. package/modules/hosts/trae/commands/wf-review.md +1 -1
  11. package/package.json +1 -1
  12. package/templates/_agents/commands/new-task.md +1 -0
  13. package/templates/_agents/commands/review.md +1 -1
  14. package/templates/_agents/engine-lock.json +28 -20
  15. package/templates/_agents/hooks/commit-check.cjs +57 -30
  16. package/templates/_agents/notes/engine-internals.md +17 -0
  17. package/templates/_agents/rule-budgets.txt +7 -1
  18. package/templates/_agents/scripts/agg-judgments.mjs +77 -1
  19. package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
  20. package/templates/_agents/scripts/archive-sweep.mjs +3 -1
  21. package/templates/_agents/scripts/check-convergence.mjs +8 -3
  22. package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
  23. package/templates/_agents/scripts/check-lane-surface.test.mjs +16 -2
  24. package/templates/_agents/scripts/check-ledger-invariant.mjs +30 -4
  25. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +28 -0
  26. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
  27. package/templates/_agents/scripts/check-loop.mjs +9 -0
  28. package/templates/_agents/scripts/check-loop.test.mjs +17 -5
  29. package/templates/_agents/scripts/check-prejudgment.mjs +8 -3
  30. package/templates/_agents/scripts/closeout-l1.mjs +217 -0
  31. package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
  32. package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
  33. package/templates/_agents/scripts/confirm-doc.mjs +14 -2
  34. package/templates/_agents/scripts/confirm-doc.test.mjs +43 -0
  35. package/templates/_agents/scripts/gen-workflow-metrics.mjs +17 -0
  36. package/templates/_agents/scripts/ledger-shards.mjs +42 -5
  37. package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
  38. package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
  39. package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
  40. package/templates/_agents/scripts/pre-push.test.mjs +26 -10
  41. package/templates/_agents/scripts/rule-budget.mjs +14 -1
  42. package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
  43. package/templates/_agents/scripts/verify.mjs +32 -8
  44. package/templates/_agents/scripts/verify.test.mjs +29 -0
  45. package/templates/_agents/scripts-test-exempt.txt +2 -1
  46. package/templates/_agents/skills/review-method/SKILL.md +55 -0
  47. package/templates/_githooks/pre-commit +9 -4
  48. package/templates/_githooks/pre-merge-commit +4 -3
  49. package/templates/_githooks/pre-push +129 -75
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ // closeout-l1 — L1 关单一把编排(2026-10-15 减重手段④ 自源仓回灌:记录性动作一把生成,判断留给人)
3
+ // 把 test.md §6 的 L1 关单链(预检 → confirm-doc 代录 → INDEX 重生成 → 提交清单)收进一条命令。
4
+ // 用法:node .agents/scripts/closeout-l1.mjs <workflow/plans/xxx.md> # 预检模式:只查差距,不落任何状态
5
+ // node .agents/scripts/closeout-l1.mjs <plan> --delegated "<用户对话原话>" # 执行模式:预检过 → confirm-doc 代录 → 重生成 INDEX
6
+ // (可加 --root <仓库根>;缺省 git toplevel,无 git 用 cwd)
7
+ // 边界(判据单源,本工具不复制强判断):
8
+ // - 证据格式 / approved 历史 / 测试绿硬门 = confirm-doc(stage-gates/policy 单源),本工具预检只是
9
+ // 提前暴露差距的 advisory 快筛;执行模式原样转发 confirm-doc,其拒绝即本命令拒绝。
10
+ // - --delegated 调用纪律:AI 须先取得用户对话内明确放行(AGENTS.md 确认门条款),原话入账供对质。
11
+ // - L2/L3 不受理(spec 收敛对账 + 独立复核是防御道本体,不做捷径);incidents 不受理(两跳制)。
12
+ // kit 适配(源仓→kit 逐条):① 源仓引用 test.md §5/§5.5,kit 关单链在 test.md §6;② verify 预检
13
+ // 镜像 kit done 前置口径(policy v6+ 按 verifyDocSince 绑定本单绿行,不带 --doc 的绿行不给背书);
14
+ // ③ 分片路径修复:源仓 main() 拼 `<基名.md>.jsonl`,而 confirm-doc/ledger-loader 写入口径是
15
+ // `<基名>.jsonl`(basename 去 .md)——源仓提交清单会把分片误报成主台账(其测试只盖纯函数未暴露),
16
+ // kit 抽 shardOf 纯函数修正并补断言。
17
+ // 测试:node .agents/scripts/closeout-l1.test.mjs(parseArgs / fmOf / acceptanceOf / preflight / shardOf 纯函数进程内断言)
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { spawnSync } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { hasFreshVerifyLine, loadKitPolicy } from './policy.mjs';
23
+
24
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ // parseArgs(argv):{ plan, delegated, root } | { error }
27
+ export function parseArgs(argv = []) {
28
+ const out = { plan: null, delegated: null, root: null, error: null };
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i];
31
+ if (a === '--delegated') {
32
+ out.delegated = argv[++i];
33
+ if (!out.delegated) out.error = '--delegated 须带「<用户对话原话>」参数';
34
+ } else if (a.startsWith('--delegated=')) {
35
+ out.delegated = a.slice('--delegated='.length);
36
+ } else if (a === '--root') {
37
+ out.root = argv[++i];
38
+ if (!out.root) out.error = '--root 须带仓库根路径';
39
+ } else if (!a.startsWith('--')) {
40
+ if (out.plan) out.error = `只受理一份 plan(现已有 ${out.plan})`;
41
+ else out.plan = a;
42
+ } else {
43
+ out.error = `未知参数 ${a}`;
44
+ }
45
+ }
46
+ if (!out.error && !out.plan) out.error = '用法:node .agents/scripts/closeout-l1.mjs <workflow/plans/xxx.md> [--delegated "<用户对话原话>"] [--root <仓库根>]';
47
+ return out;
48
+ }
49
+
50
+ // frontmatter 平铺解析(key: value;本工具只消费 状态/级别 两个键)
51
+ export function fmOf(text) {
52
+ const lines = String(text).replace(/\r\n/g, '\n').split('\n');
53
+ if (!/^---\s*$/.test(lines[0] || '')) return {};
54
+ const fm = {};
55
+ for (let i = 1; i < lines.length; i++) {
56
+ if (/^---\s*$/.test(lines[i])) break;
57
+ const m = /^([^\s:]+):\s*(.*)$/.exec(lines[i]);
58
+ if (m) fm[m[1]] = m[2].trim();
59
+ }
60
+ return fm;
61
+ }
62
+
63
+ // acceptanceOf(text):定位「## …验收标准…」节,统计勾选框条目。
64
+ // 返回 { found, total, unchecked: [条目文本] };节内无任何勾选框条目 → found=true, total=0(判为异常)。
65
+ export function acceptanceOf(text) {
66
+ const lines = String(text).replace(/\r\n/g, '\n').split('\n');
67
+ let start = -1;
68
+ for (let i = 0; i < lines.length; i++) {
69
+ if (/^##\s/.test(lines[i]) && lines[i].includes('验收标准')) { start = i; break; }
70
+ if (i > 0 && /^##\s/.test(lines[i]) && start >= 0) break; // 上一节结束仍无(不可达,防御)
71
+ }
72
+ if (start < 0) return { found: false, total: 0, unchecked: [] };
73
+ const unchecked = [];
74
+ let total = 0;
75
+ for (let i = start + 1; i < lines.length; i++) {
76
+ if (/^##\s/.test(lines[i])) break; // 节结束
77
+ const m = /^\s*-\s*\[([ xX])\]\s*(.*)$/.exec(lines[i]);
78
+ if (!m) continue;
79
+ total++;
80
+ if (m[1] === ' ') unchecked.push(m[2].trim());
81
+ }
82
+ return { found: true, total, unchecked };
83
+ }
84
+
85
+ // preflight({ rel, text, freshVerify }):L1 关单前置快筛(advisory;强门在 confirm-doc)。
86
+ // 返回 { ok, problems: [], level, status, total, checked }。
87
+ export function preflight({ rel, text, freshVerify }) {
88
+ const problems = [];
89
+ if (!/^workflow\/plans\/[^/]+\.md$/.test(rel) || rel.includes('_TEMPLATE')) {
90
+ problems.push(`路径须为 workflow/plans/ 直下文档(现「${rel}」)——L2/L3 intent/spec 与 incidents 不在本工具受理面`);
91
+ }
92
+ const fm = fmOf(text);
93
+ const level = fm['级别'] || '';
94
+ const status = fm['状态'] || '';
95
+ if (level !== 'L1') {
96
+ problems.push(`级别「${level || '缺失'}」≠ L1——L2/L3 关单走 .agents/commands/test.md 全流程(验收判据对账 + 独立复核),本工具只服务协作道 L1`);
97
+ }
98
+ if (status === 'done' || status === 'superseded' || status === 'cancelled') {
99
+ problems.push(`状态已为终态「${status}」——本 plan 已收口,无需重复关单`);
100
+ } else if (status !== 'approved') {
101
+ problems.push(`状态「${status || '缺失'}」≠ approved——draft 须先过起草确认(confirm-doc approved),关单只受理 approved → done`);
102
+ }
103
+ const acc = acceptanceOf(text);
104
+ if (!acc.found) {
105
+ problems.push('未找到「## 验收标准」节——L1 plan 须有可勾验的验收标准(模板口径)');
106
+ } else if (acc.total === 0) {
107
+ problems.push('「验收标准」节内无勾选框条目(- [ ] / - [x])——验收标准须逐条可勾验');
108
+ } else if (acc.unchecked.length) {
109
+ problems.push(`验收标准 ${acc.unchecked.length}/${acc.total} 条未勾验(勾验与补证据是判断动作,留在关单前完成):`);
110
+ for (const u of acc.unchecked) problems.push(` - [ ] ${u}`);
111
+ }
112
+ if (!freshVerify) {
113
+ problems.push('24h 内无本单绑定的 verify 绿行(.agents/verifications.jsonl)——先跑 node .agents/scripts/verify.mjs --doc <本单路径>(全绿自动落绿凭证与 verify-evidence 摘要;v6+ 只认带本单 --doc 的绿行)');
114
+ }
115
+ return { ok: problems.length === 0, problems, level, status, total: acc.total, checked: acc.total - acc.unchecked.length };
116
+ }
117
+
118
+ // shardOf(rel):该 plan 的确认台账分片路径(posix,仓库根相对)。写入口径单源 = ledger-loader
119
+ // writeConfirmation(basename 去 .md)——源仓首版此处拼了 `<基名.md>.jsonl`,提交清单会漏列分片。
120
+ export function shardOf(rel) {
121
+ return path.join('.agents', 'confirmations.d', `${path.basename(rel, '.md')}.jsonl`);
122
+ }
123
+
124
+ function gitRoot() {
125
+ const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });
126
+ return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : process.cwd();
127
+ }
128
+
129
+ // freshVerifyOf(root, rel):镜像 kit confirm-doc done 前置的绿行口径(判定单源 policy.hasFreshVerifyLine):
130
+ // policy v5+(verifySince)才有 24h 绿行门;v6+(verifyDocSince)按本 doc 精确绑定。预检与强门同口径,
131
+ // 差距快筛才不失真。只读,台账缺失/不可读一律 false(advisory 层 fail-open,同 confirm-doc)。
132
+ function freshVerifyOf(root, rel) {
133
+ try {
134
+ const p = path.join(root, '.agents', 'verifications.jsonl');
135
+ if (!fs.existsSync(p)) return false;
136
+ const pol = loadKitPolicy(root);
137
+ if (typeof pol.verifySince !== 'string') return true; // v1-v4 无绿行门 → 预检不产差距
138
+ const bound = typeof pol.verifyDocSince === 'string' ? rel : null;
139
+ return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/), Date.now(), bound);
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ function findArchived(root, rel) {
146
+ const base = path.basename(rel);
147
+ for (const monthDir of fs.existsSync(path.join(root, 'workflow', 'archive'))
148
+ ? fs.readdirSync(path.join(root, 'workflow', 'archive')) : []) {
149
+ const cand = path.join('workflow', 'archive', monthDir, 'plans', base);
150
+ if (fs.existsSync(path.join(root, cand))) return cand;
151
+ }
152
+ return null;
153
+ }
154
+
155
+ function main() {
156
+ const args = parseArgs(process.argv.slice(2));
157
+ if (args.error) {
158
+ console.error(`closeout-l1: ${args.error}`);
159
+ process.exit(2);
160
+ }
161
+ const root = path.resolve(args.root || gitRoot());
162
+ const rel = path.relative(root, path.resolve(args.plan)).replace(/\\/g, '/');
163
+ const abs = path.join(root, rel);
164
+ if (!fs.existsSync(abs)) {
165
+ console.error(`closeout-l1: 文件不存在 ${rel}(相对仓库根 ${root})`);
166
+ process.exit(2);
167
+ }
168
+ const text = fs.readFileSync(abs, 'utf8');
169
+ const pf = preflight({ rel, text, freshVerify: freshVerifyOf(root, rel) });
170
+
171
+ console.log(`L1 关单预检|${rel}`);
172
+ console.log(` 级别 ${pf.level || '—'}|状态 ${pf.status || '—'}|验收 ${pf.checked}/${pf.total || 0} 勾验|verify 绿行 ${pf.problems.some((p) => p.includes('verify')) ? '✗' : '✓(24h)'}`);
173
+ if (!pf.ok) {
174
+ console.error(`\n预检未过(${pf.problems.length} 项差距,逐条处理后再来):`);
175
+ for (const p of pf.problems) console.error(` ✗ ${p}`);
176
+ process.exit(2);
177
+ }
178
+ console.log(' 预检通过。');
179
+
180
+ if (!args.delegated) {
181
+ console.log('\n下一步(执行关单,须先取得用户对话内明确放行):');
182
+ console.log(` node .agents/scripts/closeout-l1.mjs ${rel} --delegated "<用户确认原话>"`);
183
+ return;
184
+ }
185
+
186
+ // 执行模式:confirm-doc 原样转发(全部强门在彼处),随后重生成 INDEX。
187
+ const cd = spawnSync(process.execPath, [
188
+ path.join(SCRIPT_DIR, 'confirm-doc.mjs'), rel, '--delegated', args.delegated, '--root', root,
189
+ ], { stdio: 'inherit' });
190
+ if (cd.status !== 0) {
191
+ console.error(`closeout-l1: confirm-doc 拒绝(exit ${cd.status})——按上方提示修复后重试;本命令不做任何绕过。`);
192
+ process.exit(cd.status === null ? 2 : cd.status);
193
+ }
194
+ const gi = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-workflow-index.mjs')], { cwd: root, encoding: 'utf8' });
195
+ if (gi.status !== 0) {
196
+ console.error('closeout-l1: ⚠️ INDEX 重生成失败(确认已落账不可回滚;提交前须手动跑 node .agents/scripts/gen-workflow-index.mjs 修齐,否则索引漂移门会拦提交)');
197
+ }
198
+
199
+ // 汇总:关单后的落盘面与提交清单(归档 mv 时给出新路径)。
200
+ const base = path.basename(rel);
201
+ const archivedTo = fs.existsSync(abs) ? null : findArchived(root, rel);
202
+ const shard = shardOf(rel);
203
+ const adds = [
204
+ archivedTo || rel,
205
+ fs.existsSync(path.join(root, shard)) ? shard : '.agents/confirmations.jsonl',
206
+ 'workflow/INDEX.md',
207
+ ];
208
+ console.log(`\n关单完成:approved → done(台账已记 chat-delegated,原话入账供对质)`);
209
+ if (archivedTo) console.log(`📦 已随确认滚动归档:${rel} → ${archivedTo}(家族原子,git mv 已暂存)`);
210
+ console.log(`提交清单(git add):
211
+ ${adds.join('\n ')}`);
212
+ console.log(`建议提交说明:docs(workflow): 关单 ${base.replace(/\.md$/, '')}(L1 done)——用户对话内确认代录${archivedTo ? `,归档至 ${path.dirname(archivedTo)}/` : ''}`);
213
+ console.log('提醒:本会话自做任务另记 workflow/delegations.md 并跑 agg-delegations.mjs(test.md §6);verify-evidence.md 有改动时随同提交。');
214
+ }
215
+
216
+ const isMain = process.argv[1] && process.argv[1].endsWith('closeout-l1.mjs');
217
+ if (isMain) main();
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // closeout-l1.test.mjs — L1 关单编排纯函数套件(2026-10-15 减重手段④ 自源仓回灌随附)
3
+ // parseArgs / fmOf / acceptanceOf / preflight / shardOf 进程内断言:快筛差距逐项暴露、
4
+ // L2/L3 与终态件不受理、勾验缺一即拦、verify 凭证缺失即拦、分片路径与写入口径同形。
5
+ // 运行:node templates/_agents/scripts/closeout-l1.test.mjs
6
+ import { parseArgs, fmOf, acceptanceOf, preflight, shardOf } from './closeout-l1.mjs';
7
+
8
+ let pass = 0;
9
+ let fail = 0;
10
+ const check = (desc, cond, detail = '') => {
11
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
12
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
13
+ };
14
+
15
+ const PLAN_OK = `---
16
+ 状态: approved
17
+ 级别: L1
18
+ 模块: purchase
19
+ ---
20
+ # PLAN — demo
21
+
22
+ ## 改动方案
23
+
24
+ - 改 A
25
+
26
+ ## 验收标准(可测试)
27
+
28
+ - [x] 判据一(证据:UI 实测 A1 PASS)
29
+ - [x] 判据二(证据:UT 用例名 demo_case)
30
+ `;
31
+
32
+ // ---- A1 parseArgs ----
33
+ {
34
+ const a = parseArgs(['workflow/plans/x.md', '--delegated', '可以关单', '--root', 'E:/r']);
35
+ check('A1 三参齐解析', a.plan === 'workflow/plans/x.md' && a.delegated === '可以关单' && a.root === 'E:/r' && !a.error, JSON.stringify(a));
36
+ check('A1 缺 plan → error', !!parseArgs(['--delegated', 'q']).error);
37
+ check('A1 --delegated 缺值 → error', !!parseArgs(['workflow/plans/x.md', '--delegated']).error);
38
+ check('A1 未知参数 → error', !!parseArgs(['workflow/plans/x.md', '--wow']).error);
39
+ check('A1 两份 plan → error', !!parseArgs(['a.md', 'b.md']).error);
40
+ check('A1 --delegated= 等号形态', parseArgs(['x.md', '--delegated=原话']).delegated === '原话');
41
+ }
42
+
43
+ // ---- A2 fmOf / acceptanceOf ----
44
+ {
45
+ const fm = fmOf(PLAN_OK);
46
+ check('A2 frontmatter 状态/级别', fm['状态'] === 'approved' && fm['级别'] === 'L1' && fm['模块'] === 'purchase', JSON.stringify(fm));
47
+ const acc = acceptanceOf(PLAN_OK);
48
+ check('A2 验收节 2 条全勾', acc.found === true && acc.total === 2 && acc.unchecked.length === 0, JSON.stringify(acc));
49
+ check('A2 无验收节 → found=false', acceptanceOf('# 无节文档\n\n- [ ] 孤儿勾选不算').found === false);
50
+ const mixed = acceptanceOf('## 验收标准\n\n- [x] 甲\n- [ ] 乙待办\n- [X] 丙大写勾\n\n## 下一节\n\n- [ ] 节外不算\n');
51
+ check('A2 未勾只数节内(乙),大写 X 认勾、节外不计', mixed.total === 3 && mixed.unchecked.length === 1 && mixed.unchecked[0] === '乙待办', JSON.stringify(mixed));
52
+ check('A2 节在无勾选框条目 → total=0', acceptanceOf('## 验收标准\n\n纯文字段\n').total === 0);
53
+ }
54
+
55
+ // ---- A3 preflight 快乐路径 ----
56
+ {
57
+ const pf = preflight({ rel: 'workflow/plans/2026-10-15-demo.md', text: PLAN_OK, freshVerify: true });
58
+ check('A3 L1+approved+全勾+有凭证 → ok', pf.ok === true && pf.problems.length === 0 && pf.checked === 2, JSON.stringify(pf));
59
+ }
60
+
61
+ // ---- A4 级别/状态不受理 ----
62
+ {
63
+ const l2 = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('级别: L1', '级别: L2'), freshVerify: true });
64
+ check('A4 L2 → 拒并指回 test.md', !l2.ok && l2.problems.some((p) => p.includes('L2/L3') && p.includes('test.md')), JSON.stringify(l2.problems));
65
+ const draft = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('状态: approved', '状态: draft'), freshVerify: true });
66
+ check('A4 draft → 拒(须先过起草确认)', !draft.ok && draft.problems.some((p) => p.includes('draft')), JSON.stringify(draft.problems));
67
+ const done = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('状态: approved', '状态: done'), freshVerify: true });
68
+ check('A4 done → 拒(已收口)', !done.ok && done.problems.some((p) => p.includes('终态')), JSON.stringify(done.problems));
69
+ check('A4 级别缺失也拒(fail-closed)', !preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('级别: L1\n', ''), freshVerify: true }).ok);
70
+ }
71
+
72
+ // ---- A5 路径受理面 ----
73
+ {
74
+ check('A5 intents 路径不受理', !preflight({ rel: 'workflow/intents/x.md', text: PLAN_OK, freshVerify: true }).ok);
75
+ check('A5 incidents 路径不受理', !preflight({ rel: 'workflow/incidents/x.md', text: PLAN_OK, freshVerify: true }).ok);
76
+ check('A5 模板不受理', !preflight({ rel: 'workflow/plans/_TEMPLATE.md', text: PLAN_OK, freshVerify: true }).ok);
77
+ check('A5 子目录路径不受理', !preflight({ rel: 'workflow/plans/sub/x.md', text: PLAN_OK, freshVerify: true }).ok);
78
+ }
79
+
80
+ // ---- A6 勾验与凭证差距 ----
81
+ {
82
+ const unchecked = preflight({
83
+ rel: 'workflow/plans/x.md',
84
+ text: PLAN_OK.replace('- [x] 判据二(证据:UT 用例名 demo_case)', '- [ ] 判据二'),
85
+ freshVerify: true,
86
+ });
87
+ check('A6 未勾验逐条列出(计数行 + 条目行分项)', !unchecked.ok
88
+ && unchecked.problems.some((p) => p.includes('1/2 条未勾验'))
89
+ && unchecked.problems.some((p) => p.includes('判据二')), JSON.stringify(unchecked.problems));
90
+ const noVerify = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK, freshVerify: false });
91
+ check('A6 verify 凭证缺失 → 拦并指 verify.mjs --doc', !noVerify.ok && noVerify.problems.some((p) => p.includes('verify.mjs') && p.includes('--doc')), JSON.stringify(noVerify.problems));
92
+ const noAcc = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.split('## 验收标准')[0], freshVerify: true });
93
+ check('A6 无验收节 → 拦', !noAcc.ok && noAcc.problems.some((p) => p.includes('验收标准')));
94
+ }
95
+
96
+ // ---- A7 shardOf:与 ledger-loader writeConfirmation 写入口径同形(<基名去.md>.jsonl)----
97
+ // 源仓首版 main() 拼 `<基名.md>.jsonl` 与写入口径错位——提交清单会把分片误报成主台账。本断言钉死修正。
98
+ {
99
+ const s = shardOf('workflow/plans/2026-10-15-demo.md');
100
+ check('A7 分片路径 = confirmations.d/<基名>.jsonl(去 .md)', s.replace(/\\/g, '/') === '.agents/confirmations.d/2026-10-15-demo.jsonl', s);
101
+ check('A7 分片路径不含 .md.jsonl 双后缀', !s.replace(/\\/g, '/').includes('.md.jsonl'), s);
102
+ }
103
+
104
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
105
+ process.exit(fail ? 1 : 0);
@@ -149,6 +149,55 @@ for (const [label, writePkg] of unsatisfied) {
149
149
  r.status === 0 && !ran(f.marks, 'build') && r.out.includes('pkg:scripts.build') && !r.out.includes('无匹配的源码变更'), r.out);
150
150
  }
151
151
 
152
+ // 场景 10:--no-build 跳过全部构建(2026-10-16 构建门后移 pre-push)——ext 命中也不跑
153
+ f = mkfix();
154
+ stage(f.root, 'src/a.xyz', 'x');
155
+ r = runHook(f.root, ['--no-build']);
156
+ check('S10 --no-build 命中 ext 也不构建(后移 pre-push)',
157
+ r.status === 0 && !ran(f.marks, 'extbuild') && r.out.includes('--no-build'), r.out);
158
+
159
+ // 场景 11:--range 推送范围模式——范围内 .xyz 触发 extbuild;敏感扫描跳过有提示
160
+ f = mkfix();
161
+ fs.writeFileSync(path.join(f.root, 'old.txt'), 'old\n');
162
+ git(f.root, ['add', 'old.txt']);
163
+ git(f.root, ['commit', '-q', '-m', 'old']);
164
+ stage(f.root, 'src/ranged.xyz', 'x');
165
+ const shaOld = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
166
+ git(f.root, ['commit', '-q', '-m', 'ranged']);
167
+ const shaNew = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
168
+ r = runHook(f.root, ['--range', shaOld, shaNew]);
169
+ check('S11 --range 范围内 .xyz 触发 extbuild 且提示跳过敏感扫描',
170
+ r.status === 0 && ran(f.marks, 'extbuild') && r.out.includes('SKIP: --range 模式跳过敏感扫描'), r.out);
171
+
172
+ // 场景 11b:--range 范围只含非触发文件 → 零构建
173
+ f = mkfix();
174
+ fs.writeFileSync(path.join(f.root, 'a.txt'), 'a\n');
175
+ git(f.root, ['add', 'a.txt']);
176
+ git(f.root, ['commit', '-q', '-m', 'a']);
177
+ const s1 = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
178
+ fs.writeFileSync(path.join(f.root, 'b.txt'), 'b\n');
179
+ git(f.root, ['add', 'b.txt']);
180
+ git(f.root, ['commit', '-q', '-m', 'b']);
181
+ const s2 = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
182
+ r = runHook(f.root, ['--range', s1, s2]);
183
+ check('S11b --range 范围无触发文件 → 零构建',
184
+ r.status === 0 && !ran(f.marks, 'extbuild') && !ran(f.marks, 'prefixbuild'), r.out);
185
+
186
+ // 场景 12:--range 全零 old sha(新分支首推)→ 全量构建语义(等价 --full 构建面)
187
+ f = mkfix();
188
+ fs.writeFileSync(path.join(f.root, 'seed.txt'), 'seed\n');
189
+ git(f.root, ['add', 'seed.txt']);
190
+ git(f.root, ['commit', '-q', '-m', 'seed']);
191
+ const shaHead = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
192
+ r = runHook(f.root, ['--range', '0'.repeat(40), shaHead]);
193
+ check('S12 --range 全零 old = 全量构建语义',
194
+ r.status === 0 && ran(f.marks, 'extbuild') && ran(f.marks, 'prefixbuild'), r.out);
195
+
196
+ // 场景 13:--range sha 非法 → BLOCK(信任边界:sha 来自 git 管道仍校验)
197
+ f = mkfix();
198
+ r = runHook(f.root, ['--range', 'not-a-sha', 'HEAD']);
199
+ check('S13 --range 非 sha 参数 → BLOCK exit 1', r.status === 1 && r.out.includes('BLOCK'), r.out);
200
+
152
201
  for (const d of fs.readdirSync(os.tmpdir()).filter((x) => x.startsWith('cc-trigger-test-'))) {
153
202
  fs.rmSync(path.join(os.tmpdir(), d), { recursive: true, force: true });
154
203
  }
@@ -541,9 +541,21 @@ if (isMain) {
541
541
  // 内容绑定约束,与既有归档批次同口径)。失败一律 fail-open:归档是卫生动作,不得回滚已完成的
542
542
  // 确认(**归档失败不阻断关单**——与源仓语义逐字一致)。启用锚(照搬源仓,无 policy 锚):
543
543
  // ① 仓库已建立归档约定(workflow/archive/ 存在)——真实装户仓有,临时 fixture 无,避免测试夹具
544
- // 被静默搬走(也避免在未采纳归档的仓库里凭空造目录);② env 逃生门 CONFIRM_DOC_NO_ARCHIVE。
544
+ // 被静默搬走(也避免在未采纳归档的仓库里凭空造目录);② env 逃生门 CONFIRM_DOC_NO_ARCHIVE;
545
+ // ③ 特性分支默认不归档(2026-10-16 自源仓 13f67ef7 回灌):分支上先关单再合并时,diff 只剩
546
+ // archive/ 路径,泳道/配对增量门只认 workflow 活跃直路径,合法合并被拦。main/master/develop 与
547
+ // 游离 HEAD 维持关单即归档。CONFIRM_DOC_NO_ARCHIVE=1 仍强制关;CONFIRM_DOC_ARCHIVE=1 可在特性分支强制归档。
545
548
  const archiveRoot = path.join(root, 'workflow', 'archive');
546
- if (confirmed > 0 && !process.env.CONFIRM_DOC_NO_ARCHIVE && fs.existsSync(archiveRoot)) {
549
+ let featureHold = false;
550
+ if (confirmed > 0 && !process.env.CONFIRM_DOC_NO_ARCHIVE && process.env.CONFIRM_DOC_ARCHIVE !== '1') {
551
+ let branch = '';
552
+ try { branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); } catch { branch = ''; }
553
+ featureHold = !!branch && branch !== 'HEAD' && !['main', 'master', 'develop'].includes(branch);
554
+ if (featureHold && fs.existsSync(archiveRoot)) {
555
+ console.log(`📦 特性分支 ${branch} 不归档(合并前保持 workflow 活跃路径;main/develop 关单才归档)。CONFIRM_DOC_ARCHIVE=1 可强制`);
556
+ }
557
+ }
558
+ if (confirmed > 0 && !process.env.CONFIRM_DOC_NO_ARCHIVE && !featureHold && fs.existsSync(archiveRoot)) {
547
559
  try {
548
560
  const { analyze, buildDocList, loadLastTouch, parseStatus, TERMINAL, ACTIVE } = await import('./archive-sweep.mjs');
549
561
  // 台账终态锚:kit 读侧单源 = readLedgerRows(主台账+分片透明合并;源仓走
@@ -1056,5 +1056,48 @@ const writeTrust = (root, cfg) => {
1056
1056
  fs.rmSync(root2, { recursive: true, force: true });
1057
1057
  }
1058
1058
 
1059
+ // ---- S45c/S45d 特性分支默认不归档(2026-10-16 自源仓 13f67ef7 回灌)----
1060
+ // 分支上先关单再合并时 diff 只剩 archive/ 路径,泳道/配对增量门只认 workflow 活跃直路径;
1061
+ // main/master/develop 与游离 HEAD 维持关单即归档。CONFIRM_DOC_ARCHIVE=1 可在特性分支强制归档。
1062
+ {
1063
+ const mkRepoBranch = (branchName) => {
1064
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-branch-'));
1065
+ const git = (...a) => spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', a, { cwd: root, encoding: 'utf8' });
1066
+ git('init', '-q');
1067
+ git('config', 'user.email', 't@t');
1068
+ git('config', 'user.name', 't');
1069
+ git('checkout', '-q', '-b', branchName);
1070
+ const ip = path.join(root, 'workflow', 'intents');
1071
+ fs.mkdirSync(ip, { recursive: true });
1072
+ fs.mkdirSync(path.join(root, 'workflow', 'archive'), { recursive: true });
1073
+ const fp = branchName === 'main' ? 'eeee5555eeee5555' : 'ffff6666ffff6666';
1074
+ fs.writeFileSync(path.join(ip, '2026-09-01-branch.md'), `---\n状态: approved\n级别: L1\n确认指纹: ${fp}\n---\n# I\n\n## 验收标准(可测试)\n- [x] 通过(证据:测试全绿)\n`, 'utf8');
1075
+ git('add', '-A');
1076
+ spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', ['commit', '-q', '-m', 'c1'], { cwd: root, env: { ...process.env, GIT_AUTHOR_DATE: '2026-09-01T10:00:00+08:00', GIT_COMMITTER_DATE: '2026-09-01T10:00:00+08:00' } });
1077
+ return { root, ip };
1078
+ };
1079
+ // S45c:特性分支关单 → 不归档(保持 workflow 活跃直路径),但状态照常 done
1080
+ {
1081
+ const { root, ip } = mkRepoBranch('feature/x');
1082
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-01-branch.md', '--delegated', '可以'],
1083
+ { cwd: root, encoding: 'utf8', env: { ...process.env, CONFIRM_DOC_TEST_NOW: '2026-10-13' } });
1084
+ check('S45c 特性分支关单默认不归档(状态置 done、文档留活跃路径)',
1085
+ r.status === 0 && /状态: done/.test(fs.readFileSync(path.join(ip, '2026-09-01-branch.md'), 'utf8'))
1086
+ && !fs.existsSync(path.join(root, 'workflow', 'archive', 'intents')) && /不归档/.test(r.stdout),
1087
+ JSON.stringify({ status: r.status, stdout: String(r.stdout).slice(-260) }));
1088
+ fs.rmSync(root, { recursive: true, force: true });
1089
+ }
1090
+ // S45d:特性分支 + CONFIRM_DOC_ARCHIVE=1 → 强制归档
1091
+ {
1092
+ const { root } = mkRepoBranch('feature/y');
1093
+ const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-01-branch.md', '--delegated', '可以'],
1094
+ { cwd: root, encoding: 'utf8', env: { ...process.env, CONFIRM_DOC_TEST_NOW: '2026-10-13', CONFIRM_DOC_ARCHIVE: '1' } });
1095
+ check('S45d 特性分支 CONFIRM_DOC_ARCHIVE=1 强制归档',
1096
+ r.status === 0 && /📦 归档/.test(r.stdout),
1097
+ JSON.stringify({ status: r.status, stdout: String(r.stdout).slice(-260) }));
1098
+ fs.rmSync(root, { recursive: true, force: true });
1099
+ }
1100
+ }
1101
+
1059
1102
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
1060
1103
  process.exit(fail ? 1 : 0);
@@ -14,6 +14,7 @@ import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { loadEnums } from './workflow-enums.mjs';
16
16
  import { readLedgerRows as loadLedgerRows } from './ledger-loader.mjs';
17
+ import { analyze, buildDocList, loadLastTouch, loadLedgerTs, parseStatus, ACTIVE } from './archive-sweep.mjs';
17
18
 
18
19
  const USAGE = `用法:node .agents/scripts/gen-workflow-metrics.mjs [--month YYYY-MM] [--dry-run] [--help]
19
20
 
@@ -217,6 +218,22 @@ console.log(` 常驻面合计:${kb(surf.total)}`);
217
218
  console.log(` 闭环漏斗:收口 ${funnel.closed}(完整链 ${funnel.full} = 一次通过 ${funnel.once} + 返工 ${funnel.rework};协议前 ${funnel.legacy})|中位周期 ${medianDays === null ? '—' : `${medianDays.toFixed(1)} 天`}|台账行 ${funnel.ledgerRows}`);
218
219
  for (const it of funnel.items) console.log(` ${String(it.doc).padEnd(58)} ${it.kind.padEnd(7)} ${it.days} 天`);
219
220
  if (funnel.items.length) console.log(' ℹ️ 机器口径可与 workflow/delegations.md 自做表的人工自报交叉对照(差异即信号,不对账)');
221
+ // 归档积压提醒(2026-10-15 减重手段⑥ 自源仓回灌,回路别等人喊):滚动归档已随确认门自动跑
222
+ // (confirm-doc archive-on-close),历史积压走 archive-sweep 手工批次——月度 stdout 报候选数使积压可见。
223
+ // 判定单源 = archive-sweep.analyze(21 天窗,禁第二套口径);analyze/loadLastTouch 抛错(如
224
+ // 浅克隆无 git 历史)fail-open 静默——观测提醒不挡快照。
225
+ try {
226
+ const allDocs = buildDocList(process.cwd());
227
+ const arch = analyze({
228
+ docs: allDocs,
229
+ ledgerTs: loadLedgerTs(process.cwd()),
230
+ lastTouch: loadLastTouch(process.cwd()),
231
+ activeTexts: allDocs.filter((d) => ACTIVE.has(parseStatus(d.text))).map((d) => d.text),
232
+ windowDays: 21,
233
+ });
234
+ const fams = new Set(arch.candidates.map((x) => x.file));
235
+ console.log(` 归档积压:候选 ${arch.candidates.length} 篇 / ${fams.size} 族(21 天窗)${arch.candidates.length ? '——node .agents/scripts/archive-sweep.mjs 查看,--execute 执行' : ''}`);
236
+ } catch { /* fail-open */ }
220
237
  console.log(` ${DRY ? '(--dry-run,未写盘)' : ''}`);
221
238
 
222
239
  if (DRY) { console.log(`\n将写入行:\n${row}\n${funnelRow}`); process.exit(0); }
@@ -226,8 +226,30 @@ export function renderShard(name, rows, eol = '\n') {
226
226
  return out.length ? `${out.join(eol)}${eol}` : '';
227
227
  }
228
228
 
229
+ // insertIntoSection(text, section, lines):把行插进已有分片的对应 ## 节(下一节标题之前)。
230
+ // 节不存在则整节追加到文末。无 section 的行不走这里(保持追加到文末 = 末表)。
231
+ function insertIntoSection(text, section, lines) {
232
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
233
+ const parts = text.split(/\r?\n/);
234
+ if (parts.length && parts[parts.length - 1] === '') parts.pop();
235
+ const start = parts.findIndex((l) => l.startsWith('## ') && l.includes(section));
236
+ if (start < 0) {
237
+ return `${parts.join(eol)}${eol}${eol}## ${section}${eol}${eol}${lines.join(eol)}${eol}`;
238
+ }
239
+ let end = parts.length;
240
+ for (let i = start + 1; i < parts.length; i++) {
241
+ if (parts[i].startsWith('## ')) { end = i; break; }
242
+ }
243
+ let at = end;
244
+ while (at > start && parts[at - 1].trim() === '') at--;
245
+ parts.splice(at, 0, ...lines);
246
+ return `${parts.join(eol)}${eol}`;
247
+ }
248
+
229
249
  // appendRows(root, name, rows, { month, section }):把数据行追加进指定月分片(缺省取行内日期月)。
230
250
  // rows 可为字符串数组或 { line, section } 数组(多表台账须带 section 才能归对表)。
251
+ // 分片已存在时:带 section 的行插入该节(不落到文末自做表);不带 section 的行仍追加到文末
252
+ // (2026-10-16 自源仓 13f67ef7 回灌——修「合并丢共有行」时新增行误落末表)。
231
253
  // 返回实际写入的分片路径列表。写失败 fail-loud(不静默吞)。
232
254
  export function appendRows(root, name, rows, { month, section = '' } = {}) {
233
255
  const written = [];
@@ -247,9 +269,21 @@ export function appendRows(root, name, rows, { month, section = '' } = {}) {
247
269
  const existingRows = new Set(existing.split(/\r?\n/).filter((l) => l.trim()));
248
270
  const fresh = rs.filter((r) => !existingRows.has(r.line));
249
271
  if (!fresh.length) { written.push(p); continue; } // 已在分片 → 幂等跳过
250
- const next = existing
251
- ? `${existing.replace(/\s*$/, '')}\n${fresh.map((r) => r.line).join('\n')}\n`
252
- : renderShard(name, fresh, '\n');
272
+ let next;
273
+ if (!existing) next = renderShard(name, fresh, '\n');
274
+ else {
275
+ const placed = fresh.filter((r) => r.section);
276
+ const tail = fresh.filter((r) => !r.section);
277
+ let text = existing;
278
+ const bySec = new Map();
279
+ for (const r of placed) {
280
+ if (!bySec.has(r.section)) bySec.set(r.section, []);
281
+ bySec.get(r.section).push(r.line);
282
+ }
283
+ for (const [sec, lines] of bySec) text = insertIntoSection(text, sec, lines);
284
+ if (tail.length) text = `${text.replace(/\s*$/, '')}\n${tail.map((r) => r.line).join('\n')}\n`;
285
+ next = text.endsWith('\n') ? text : `${text}\n`;
286
+ }
253
287
  fs.writeFileSync(p, next, 'utf8');
254
288
  written.push(p);
255
289
  }
@@ -296,11 +330,14 @@ function main() {
296
330
  if (!row) { console.error('--append 须带 --row "<数据行>"'); process.exit(2); }
297
331
  const mIdx = argv.indexOf('--month');
298
332
  const month = mIdx >= 0 ? argv[mIdx + 1] : undefined;
299
- const written = appendRows(root, name, [row], { month });
333
+ const sIdx = argv.indexOf('--section');
334
+ const section = sIdx >= 0 ? argv[sIdx + 1] : '';
335
+ if (sIdx >= 0 && !section) { console.error('--section 须带节标题(如 委派结果)'); process.exit(2); }
336
+ const written = appendRows(root, name, [row], { month, section });
300
337
  console.log(`✓ 追加 1 行 → ${written.map((p) => path.relative(root, p).split(path.sep).join('/')).join(', ')}`);
301
338
  return;
302
339
  }
303
- console.error('用法:node .agents/scripts/ledger-shards.mjs --check | --split <ledger> [--dry-run] | --append <ledger> --row "<行>" [--month YYYY-MM]');
340
+ console.error('用法:node .agents/scripts/ledger-shards.mjs --check | --split <ledger> [--dry-run] | --append <ledger> --row "<行>" [--month YYYY-MM] [--section <节标题>]');
304
341
  process.exit(2);
305
342
  }
306
343
 
@@ -247,6 +247,23 @@ const wf = (root, ...p) => path.join(root, 'workflow', ...p);
247
247
  fs.rmSync(root, { recursive: true, force: true });
248
248
  }
249
249
 
250
+ // ---- S20 appendRows --section(2026-10-16 自源仓 13f67ef7 回灌):已有双表时委派行插入委派节,
251
+ // 不带 section 仍落文末(修「合并丢共有行」时新增行误落末表)----
252
+ {
253
+ const root = mkroot();
254
+ appendRows(root, 'delegations', ['| 2026-10-15 | model | 委派甲 | 一次通过 | 备注 | |'], { section: '委派结果' });
255
+ appendRows(root, 'delegations', ['| 2026-10-15 | 自做甲 | 一次通过 | 备注 | |'], { section: '自做任务结果' });
256
+ appendRows(root, 'delegations', ['| 2026-10-15 | model | 委派乙 | 一次通过 | 备注 | |'], { section: '委派结果' });
257
+ appendRows(root, 'delegations', ['| 2026-10-15 | 自做乙 | 一次通过 | 备注 | |']);
258
+ const shard = fs.readFileSync(shardPathFor(root, 'delegations', '2026-10'), 'utf8');
259
+ const iSelf = shard.indexOf('## 自做任务结果');
260
+ const iDelegRow = shard.indexOf('委派乙');
261
+ const iSelfRow = shard.indexOf('自做乙');
262
+ check('S20 后追加的委派行仍在自做节之前', iDelegRow >= 0 && iDelegRow < iSelf, shard);
263
+ check('S20b 不带 section 的行落在文末', iSelf >= 0 && iSelf < iSelfRow, shard);
264
+ fs.rmSync(root, { recursive: true, force: true });
265
+ }
266
+
250
267
  // ---- S18 appendRows 幂等:同行走两次只落一次 ----
251
268
  {
252
269
  const root = mkroot();