agentic-flow-kit 1.8.1 → 1.8.7

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 (74) hide show
  1. package/README.md +1 -1
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-design.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +1 -0
  5. package/modules/hosts/claude/commands/wf-review.md +1 -1
  6. package/modules/hosts/codex/commands/wf-build.md +1 -1
  7. package/modules/hosts/codex/commands/wf-design.md +1 -1
  8. package/modules/hosts/codex/commands/wf-new-task.md +1 -0
  9. package/modules/hosts/codex/commands/wf-review.md +1 -1
  10. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  11. package/modules/hosts/cursor/commands/wf-design.md +1 -1
  12. package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
  13. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  14. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  15. package/modules/hosts/opencode/commands/wf-design.md +1 -1
  16. package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
  17. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  18. package/modules/hosts/trae/commands/wf-build.md +1 -1
  19. package/modules/hosts/trae/commands/wf-design.md +1 -1
  20. package/modules/hosts/trae/commands/wf-new-task.md +1 -0
  21. package/modules/hosts/trae/commands/wf-review.md +1 -1
  22. package/package.json +1 -1
  23. package/src/jscpd-drift.test.mjs +3 -2
  24. package/templates/AGENTS.md +1 -0
  25. package/templates/_agents/commands/build.md +1 -1
  26. package/templates/_agents/commands/design.md +1 -1
  27. package/templates/_agents/commands/new-task.md +1 -0
  28. package/templates/_agents/commands/review.md +1 -1
  29. package/templates/_agents/engine-lock.json +32 -24
  30. package/templates/_agents/hooks/commit-check.cjs +57 -30
  31. package/templates/_agents/mechanism-registry.md +8 -0
  32. package/templates/_agents/notes/engine-internals.md +17 -0
  33. package/templates/_agents/rule-budgets.txt +10 -2
  34. package/templates/_agents/scripts/agg-judgments.mjs +77 -1
  35. package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
  36. package/templates/_agents/scripts/archive-sweep.mjs +3 -1
  37. package/templates/_agents/scripts/check-convergence.mjs +8 -3
  38. package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
  39. package/templates/_agents/scripts/check-lane-surface.test.mjs +17 -3
  40. package/templates/_agents/scripts/check-ledger-invariant.mjs +109 -24
  41. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +29 -1
  42. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
  43. package/templates/_agents/scripts/check-loop.mjs +41 -3
  44. package/templates/_agents/scripts/check-loop.test.mjs +37 -0
  45. package/templates/_agents/scripts/check-scope.test.mjs +4 -1
  46. package/templates/_agents/scripts/check-scripts-syntax.mjs +33 -10
  47. package/templates/_agents/scripts/closeout-l1.mjs +217 -0
  48. package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
  49. package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
  50. package/templates/_agents/scripts/confirm-doc.mjs +27 -8
  51. package/templates/_agents/scripts/confirm-doc.test.mjs +68 -9
  52. package/templates/_agents/scripts/gen-workflow-metrics.mjs +158 -9
  53. package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +119 -0
  54. package/templates/_agents/scripts/lane-surface-audit.test.mjs +1 -1
  55. package/templates/_agents/scripts/ledger-shards.mjs +42 -5
  56. package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
  57. package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
  58. package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
  59. package/templates/_agents/scripts/pre-push.test.mjs +27 -11
  60. package/templates/_agents/scripts/rule-budget.mjs +14 -1
  61. package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
  62. package/templates/_agents/scripts/solidify-task.mjs +3 -1
  63. package/templates/_agents/scripts/solidify-task.test.mjs +10 -10
  64. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  65. package/templates/_agents/scripts/verify.mjs +237 -78
  66. package/templates/_agents/scripts/verify.test.mjs +44 -0
  67. package/templates/_agents/scripts-test-exempt.txt +2 -1
  68. package/templates/_agents/skills/review-method/SKILL.md +3 -0
  69. package/templates/_githooks/pre-commit +9 -4
  70. package/templates/_githooks/pre-merge-commit +4 -3
  71. package/templates/_githooks/pre-push +129 -75
  72. package/templates/workflow/README.md +2 -1
  73. package/templates/workflow/intents/_TEMPLATE.md +5 -0
  74. package/templates/workflow/specs/_TEMPLATE.md +1 -1
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // mechanism-retire-watch.test.mjs — 1b 机制退役催办纯函数套件(2026-10-14 L5 自举接线 1b)
3
+ // parseRegistry / ruleNamesOf / zeroMonthsOf / evaluate 进程内断言。
4
+ import { parseRegistry, ruleNamesOf, zeroMonthsOf, evaluate } from './mechanism-retire-watch.mjs';
5
+
6
+ let pass = 0;
7
+ let fail = 0;
8
+ const check = (desc, cond, detail = '') => {
9
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
10
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
11
+ };
12
+
13
+ // ---- S1 parseRegistry ----
14
+ {
15
+ const text = [
16
+ '| 机制 | 门禁位置 | 触发统计口径 | 退役阈值 | 最近触发 | 状态 | 处置期限 |',
17
+ '|---|---|---|---|---|---|---|',
18
+ '| 甲门 | check-loop §1 | WARN「甲规则」计数(月) | 连续 6 月 = 0 → 评估退役 | 2026-10 在用 | 现役 | — |',
19
+ '| 乙门 | check-loop §2 | hard 计数 | 不退役(主干) | 2026-10 | 现役 | — |',
20
+ '| 丙门 | check-loop §3 | WARN 计数 | 连续 6 月 = 0 | 2026-10 | 待退役 | 2026-01-01 |',
21
+ '| 坏行 | 少列 |',
22
+ ].join('\n');
23
+ const rows = parseRegistry(text);
24
+ check('S1 只取 7 列数据行(表头/分隔/坏行剔除)', rows.length === 3, String(rows.length));
25
+ check('S1 列序正确', rows[0].name === '甲门' && rows[0].status === '现役' && rows[0].deadline === '—', JSON.stringify(rows[0]));
26
+ check('S1 待退役行期限解析', rows[2].status === '待退役' && rows[2].deadline === '2026-01-01');
27
+ }
28
+
29
+ // ---- S2 ruleNamesOf ----
30
+ check('S2 单规则名', ruleNamesOf('WARN「模板未填」计数(月)').join(',') === '模板未填');
31
+ check('S2 斜杠分隔多规则', ruleNamesOf('WARN「预判类型不符/预判级别不符/预判字段不可解析」计数').join(',') === '预判类型不符,预判级别不符,预判字段不可解析');
32
+ check('S2 无「」→ 空', ruleNamesOf('hard/WARN 计数(月)').length === 0);
33
+
34
+ // ---- S3 zeroMonthsOf ----
35
+ check('S3 连续 6 月 = 0', zeroMonthsOf('连续 6 月 = 0 → 评估退役') === 6);
36
+ check('S3 带前缀仍解析', zeroMonthsOf('WARN 命中连续 6 月 = 0 且新增预判字段停滞 → 评估退役') === 6);
37
+ check('S3 非零阈值形态 → null', zeroMonthsOf('不退役(防伪主干)') === null);
38
+ check('S3 使用率型 → null', zeroMonthsOf('<10% 且无新增 → 退役双协议') === null);
39
+
40
+ // ---- S4 evaluate:阈值触达 ----
41
+ {
42
+ const reg = [{ name: '甲门', criterion: 'WARN「甲规则」计数', threshold: '连续 2 月 = 0 → 评估退役', status: '现役', deadline: '—' }];
43
+ const j = [
44
+ { ts: '2026-08-01T00:00:00Z', rules: [{ rule: '甲规则', level: 'WARN', count: 0 }] },
45
+ { ts: '2026-09-01T00:00:00Z', rules: [] },
46
+ ];
47
+ const r = evaluate(reg, j, new Date('2026-10-14'));
48
+ check('S4 连续 2 月零触发 → 动作项', r.actions.length === 1 && r.actions[0].kind === 'threshold', JSON.stringify(r.actions));
49
+ check('S4 动作项含窗口与规则名', /2026-08 \/ 2026-09/.test(r.actions[0].detail) && /甲规则/.test(r.actions[0].detail), r.actions[0].detail);
50
+ }
51
+
52
+ // ---- S5 evaluate:窗口内有触发 → 不产动作项 ----
53
+ {
54
+ const reg = [{ name: '甲门', criterion: 'WARN「甲规则」计数', threshold: '连续 2 月 = 0', status: '现役', deadline: '—' }];
55
+ const j = [
56
+ { ts: '2026-08-01T00:00:00Z', rules: [{ rule: '甲规则', level: 'WARN', count: 1 }] },
57
+ { ts: '2026-09-01T00:00:00Z', rules: [] },
58
+ ];
59
+ const r = evaluate(reg, j, new Date('2026-10-14'));
60
+ check('S5 窗口内命中 → 无动作项', r.actions.length === 0, JSON.stringify(r.actions));
61
+ }
62
+
63
+ // ---- S6 evaluate:数据不足 / 无规则名 → notAuto ----
64
+ {
65
+ const reg = [
66
+ { name: '甲门', criterion: 'WARN「甲规则」计数', threshold: '连续 6 月 = 0', status: '现役', deadline: '—' },
67
+ { name: '乙门', criterion: 'hard 计数', threshold: '连续 6 月 = 0', status: '现役', deadline: '—' },
68
+ { name: '丙门', criterion: 'WARN「丙规则」计数', threshold: '不退役(主干)', status: '现役', deadline: '—' },
69
+ ];
70
+ const j = [{ ts: '2026-09-01T00:00:00Z', rules: [] }];
71
+ const r = evaluate(reg, j, new Date('2026-10-14'));
72
+ check('S6 数据不足 → notAuto', r.notAuto.some((x) => x.name === '甲门' && /数据不足/.test(x.reason)), JSON.stringify(r.notAuto));
73
+ check('S6 无规则名 → notAuto', r.notAuto.some((x) => x.name === '乙门' && /未列规则名/.test(x.reason)));
74
+ check('S6 非零阈值形态 → notAuto', r.notAuto.some((x) => x.name === '丙门' && /非「连续/.test(x.reason)));
75
+ check('S6 无动作项', r.actions.length === 0);
76
+ }
77
+
78
+ // ---- S7 evaluate:待退役逾期 / 缺期限 ----
79
+ {
80
+ const reg = [
81
+ { name: '甲门', criterion: '', threshold: '', status: '待退役', deadline: '2026-01-01' },
82
+ { name: '乙门', criterion: '', threshold: '', status: '待退役', deadline: '下批次' },
83
+ { name: '丙门', criterion: '', threshold: '', status: '已退役', deadline: '—' },
84
+ ];
85
+ const r = evaluate(reg, [], new Date('2026-10-14'));
86
+ check('S7 逾期 → overdue', r.actions.some((a) => a.kind === 'overdue' && a.name === '甲门'), JSON.stringify(r.actions));
87
+ check('S7 非日期期限 → no-deadline', r.actions.some((a) => a.kind === 'no-deadline' && a.name === '乙门'));
88
+ check('S7 已退役行跳过', !r.actions.some((a) => a.name === '丙门'));
89
+ }
90
+ // ---- S7b:期限当天不逾期(本地日,不当天 08:00 后提前催)----
91
+ {
92
+ const reg = [{ name: '甲门', criterion: '', threshold: '', status: '待退役', deadline: '2026-10-14' }];
93
+ const now = new Date(2026, 9, 14, 9, 0, 0);
94
+ const r = evaluate(reg, [], now);
95
+ check('S7b 期限当天本地 09:00 不逾期', r.actions.length === 0, JSON.stringify(r.actions));
96
+ }
97
+ // ---- S4b:有数据月不连续 → 缺月不算零命中 ----
98
+ {
99
+ const reg = [{ name: '甲门', criterion: 'WARN「甲规则」计数', threshold: '连续 2 月 = 0 → 评估退役', status: '现役', deadline: '—' }];
100
+ const j = [
101
+ { ts: '2026-08-01T00:00:00Z', rules: [] },
102
+ { ts: '2026-10-01T00:00:00Z', rules: [] },
103
+ ];
104
+ const r = evaluate(reg, j, new Date('2026-10-14T00:00:00Z'));
105
+ check('S4b 8 月与 10 月之间缺 9 月 → 不产退役动作', r.actions.length === 0, JSON.stringify(r.actions));
106
+ check('S4b 缺月记入 notAuto', r.notAuto.some((x) => x.name === '甲门' && /缺月/.test(x.reason) && /2026-09/.test(x.reason)), JSON.stringify(r.notAuto));
107
+ }
108
+
109
+ // ---- S8 空入参 ----
110
+ check('S8 空台账 → 无动作项', evaluate([], [], new Date()).actions.length === 0);
111
+ check('S8 null judgments 不 throw', evaluate([], null, new Date()).actions.length === 0);
112
+
113
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
114
+ process.exit(fail ? 1 : 0);
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // pre-push.test.mjs — 加固门目标分支判定 fixture 套件(2026-10-01 v09-review-defects)
3
- // 真仓端到端:临时仓库挂载真实 pre-push,.agents/scripts/check-loop.sh 以桩脚本记录 argv。
2
+ // pre-push.test.mjs — 加固门目标分支判定 + 构建门 fixture 套件
3
+ // (2026-10-01 v09-review-defects 建;2026-10-16 随 pre-push node 化更新:桩改 .agents/scripts/check-loop.mjs
4
+ // 直调,补 commit-check 构建门桩与 --range 断言——自源仓 2ff869a9 回灌面)
5
+ // 真仓端到端:临时仓库挂载真实 pre-push,check-loop.mjs 与 commit-check.cjs 以桩记录 argv。
4
6
  // 断言三态 refspec:
5
- // main→main → argv 含 --hardening(加固门)
6
- // experiment/x→main → argv 含 --hardening(修复回归:直推漏口)
7
- // experiment/y→experiment/y → argv 不含 --hardening(advisory)
7
+ // main→main → argv 含 --hardening(加固门);构建门 --range 新分支首推=全量零 sha
8
+ // experiment/x→main → argv 含 --hardening(修复回归:直推漏口);构建门 old 为远端实 sha
9
+ // experiment/y→experiment/y → argv 不含 --hardening(advisory);构建门仍按范围跑(advisory 不阻断)
8
10
  import fs from 'node:fs';
9
11
  import os from 'node:os';
10
12
  import path from 'node:path';
@@ -34,11 +36,15 @@ const setup = () => {
34
36
  const remote = path.join(root, 'remote.git');
35
37
  fs.mkdirSync(path.join(root, '.githooks'), { recursive: true });
36
38
  fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
37
- // 挂载真实 pre-push(装户同款),stub 替换 check-loop 记录 argv
39
+ fs.mkdirSync(path.join(root, '.agents', 'hooks'), { recursive: true });
40
+ // 挂载真实 pre-push(装户同款),stub 替换 check-loop 与 commit-check 记录 argv
38
41
  fs.copyFileSync(PRE_PUSH, path.join(root, '.githooks', 'pre-push'));
39
- // Linux:git 静默跳过不可执行钩子(推送照常成功、桩无记录)——挂载后必须设执行位;桩经 sh 调用无须 +x
42
+ // Linux:git 静默跳过不可执行钩子(推送照常成功、桩无记录)——挂载后必须设执行位;桩经 node 调用无须 +x
40
43
  fs.chmodSync(path.join(root, '.githooks', 'pre-push'), 0o755);
41
- fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), '#!/bin/sh\necho "$@" >> .stub-args\nexit 0\n');
44
+ fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.mjs'),
45
+ 'import fs from "node:fs";\nfs.appendFileSync(".stub-args", process.argv.slice(2).join(" ") + "\\n");\n');
46
+ fs.writeFileSync(path.join(root, '.agents', 'hooks', 'commit-check.cjs'),
47
+ 'const fs = require("node:fs");\nfs.appendFileSync(".stub-args-cc", process.argv.slice(2).join(" ") + "\\n");\n');
42
48
  fs.writeFileSync(path.join(root, 'a.txt'), 'a\n');
43
49
  git(root, ['init', '-q']);
44
50
  git(root, ['config', 'user.email', 't@t']);
@@ -54,15 +60,21 @@ const stubArgs = (root) => {
54
60
  const p = path.join(root, '.stub-args');
55
61
  return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
56
62
  };
63
+ const ccArgs = (root) => {
64
+ const p = path.join(root, '.stub-args-cc');
65
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
66
+ };
57
67
 
58
68
  const root = setup();
59
69
  const w = (f, c) => fs.writeFileSync(path.join(root, f), c);
60
70
 
61
- // 1) main → main:应含 --hardening
71
+ // 1) main → main:应含 --hardening;构建门首推 = 全量零 sha
62
72
  git(root, ['checkout', '-q', '-b', 'main']);
63
73
  let r = run(root, ['push', 'origin', 'HEAD:main']);
64
74
  check('main→main 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
65
75
  check('main→main argv 含 --hardening', (stubArgs(root)[0] || '').includes('--hardening'), stubArgs(root).join('|'));
76
+ check('main→main 构建门 --range 新分支首推=全量零 sha(2026-10-16 构建门后移)',
77
+ (ccArgs(root)[0] || '').startsWith('--range 0000000000000000000000000000000000000000 '), ccArgs(root).join('|'));
66
78
 
67
79
  // 2) experiment/x → main(回归漏口):本地分支在 experiment,目标 main → 必须含 --hardening
68
80
  git(root, ['checkout', '-q', '-b', 'experiment/x']);
@@ -72,8 +84,10 @@ git(root, ['commit', '-q', '-m', 'x']);
72
84
  r = run(root, ['push', 'origin', 'HEAD:main']);
73
85
  check('experiment/x→main 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
74
86
  check('experiment/x→main argv 含 --hardening(直推漏口修复)', (stubArgs(root)[1] || '').includes('--hardening'), stubArgs(root).join('|'));
87
+ check('experiment/x→main 构建门 --range old 为远端实 sha(非零)',
88
+ /^--range [0-9a-f]{40} /.test(ccArgs(root)[1] || ''), ccArgs(root).join('|'));
75
89
 
76
- // 3) experiment/y → experiment/y:目标在探索泳道 → 不含 --hardening
90
+ // 3) experiment/y → experiment/y:目标在探索泳道 → 不含 --hardening;构建门仍按范围跑(advisory)
77
91
  git(root, ['fetch', '-q', 'origin']);
78
92
  git(root, ['branch', 'experiment/y', 'origin/main']);
79
93
  git(root, ['checkout', '-q', 'experiment/y']);
@@ -84,6 +98,8 @@ r = run(root, ['push', 'origin', 'HEAD:experiment/y']);
84
98
  check('experiment→experiment 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
85
99
  const line3 = stubArgs(root)[2] || '';
86
100
  check('experiment→experiment argv 不含 --hardening', !line3.includes('--hardening'), stubArgs(root).join('|'));
101
+ check('experiment→experiment 构建门仍按范围跑(advisory 不阻断)',
102
+ /^--range [0-9a-f]{40} /.test(ccArgs(root)[2] || ''), ccArgs(root).join('|'));
87
103
 
88
- console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
104
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
89
105
  process.exit(fail ? 1 : 0);
@@ -8,6 +8,10 @@
8
8
  // 「提交后树」= 索引(index)内容:staged 版优先于 HEAD;无 git(fixture)回退工作区文件。
9
9
  // 字节口径 = 索引 / HEAD 内容(git cat-file,LF 归一)。禁用工作区 wc -c 口径(CRLF 膨胀系统性偏大,
10
10
  // 2026-09-21 实测差 ~500B 一度误判超限)——本脚本所有字节数都走 cat-file,仅无 git 时才落工作区。
11
+ // 条目数预算(2026-10-15 减重手段② 自源仓回灌):上限字段形如 `<N>#count` 时按「匹配文件条数」执法
12
+ // 而非字节——规则面一进一出(新增一条须先退役一条,退役判定 agg-judgments --retire-candidates)。
13
+ // 旧格式行(纯数字)不受影响;`#count` 行对旧读者(未升级的脚本副本)因上限非纯数字被跳过——
14
+ // fail-open,不误拦。
11
15
  import { spawnSync } from 'node:child_process';
12
16
  import fs from 'node:fs';
13
17
  import path from 'node:path';
@@ -60,13 +64,22 @@ const expand = (pat) => {
60
64
 
61
65
  let viol = 0;
62
66
  const over = (what, bytes, lim) => { console.log(`常驻面超限:${what} ${bytes}B > 上限 ${lim}B`); viol = 1; };
67
+ const overCount = (pat, n, lim) => { console.log(`常驻面超限(条目数):${pat} ${n} 项 > 上限 ${lim} 项——一进一出:新增须先退役一条(退役判定 agg-judgments --retire-candidates)`); viol = 1; };
63
68
 
64
69
  for (const line of fs.readFileSync(BUDGETS, 'utf8').split(/\r?\n/)) {
65
70
  const sp = line.lastIndexOf(' ');
66
71
  if (sp < 0) continue;
67
72
  const pat = line.slice(0, sp), lim = line.slice(sp + 1);
68
73
  if (pat === '' || pat.startsWith('#')) continue;
69
- if (!/^\d+$/.test(lim)) continue;
74
+ // 条目数预算(2026-10-15 减重手段②):`<N>#count` = 匹配文件数上限(一进一出的执法面)
75
+ const cnt = /^(\d+)#count$/.exec(lim);
76
+ if (!cnt && !/^\d+$/.test(lim)) continue;
77
+ if (cnt) {
78
+ if (mode === '--staged' && !staged.some((sf) => sf.startsWith(pat))) continue;
79
+ const n = expand(pat).length;
80
+ if (n > Number(cnt[1])) overCount(pat, n, cnt[1]);
81
+ continue;
82
+ }
70
83
  if (pat.endsWith('/')) {
71
84
  // 目录合计:--staged 下仅当暂存触及该目录内文件才查(字面前缀匹配,同 sh 版 2026-09-24 口径)
72
85
  if (mode === '--staged' && !staged.some((sf) => sf.startsWith(pat))) continue;
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // rule-budget.test.mjs — 常驻面预算黑盒套件(2026-10-15 减重手段② 自源仓回灌随附:#count 条目数指令)
3
+ // 夹具:os.tmpdir 下临时目录(非 git 环境——脚本 git rev-parse 失败留在 fixture 目录,
4
+ // 字节口径回退工作区文件;与真实仓「索引/HEAD」口径的差异不影响指令语义判定)。
5
+ // 覆盖:字节超限/未超、#count 超限/压线、旧格式行不受 #count 影响、缺预算表 exit 0、
6
+ // --staged 无 git(staged 空)时 #count 门跳过。
7
+ // 运行:node templates/_agents/scripts/rule-budget.test.mjs
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'rule-budget.mjs');
15
+
16
+ let pass = 0;
17
+ let fail = 0;
18
+ const check = (desc, cond, detail = '') => {
19
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
20
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
21
+ };
22
+
23
+ function makeFixture({ budgets, dirs = {}, files = {} }) {
24
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rule-budget-'));
25
+ if (budgets !== null) {
26
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
27
+ fs.writeFileSync(path.join(root, '.agents', 'rule-budgets.txt'), budgets, 'utf8');
28
+ }
29
+ for (const [dir, names] of Object.entries(dirs)) {
30
+ fs.mkdirSync(path.join(root, dir), { recursive: true });
31
+ for (const n of names) fs.writeFileSync(path.join(root, dir, n), 'x\n', 'utf8');
32
+ }
33
+ for (const [f, content] of Object.entries(files)) {
34
+ fs.mkdirSync(path.dirname(path.join(root, f)), { recursive: true });
35
+ fs.writeFileSync(path.join(root, f), content, 'utf8');
36
+ }
37
+ return root;
38
+ }
39
+
40
+ const run = (cwd, mode = '--all') => {
41
+ const r = spawnSync(process.execPath, [SCRIPT, mode], { cwd, encoding: 'utf8' });
42
+ return { code: r.status, out: String(r.stdout || '') };
43
+ };
44
+
45
+ let root;
46
+
47
+ // ---- T1 字节超限(旧格式语义不回归)----
48
+ root = makeFixture({
49
+ budgets: 'AGENTS.md 10\n',
50
+ files: { 'AGENTS.md': 'x'.repeat(50) },
51
+ });
52
+ {
53
+ const r = run(root);
54
+ check('T1 字节超限 exit 1 + 违例行', r.code === 1 && r.out.includes('常驻面超限:AGENTS.md') && r.out.includes('> 上限 10B'), JSON.stringify(r));
55
+ }
56
+
57
+ // ---- T2 字节未超 → 0 ----
58
+ root = makeFixture({
59
+ budgets: 'AGENTS.md 100\n',
60
+ files: { 'AGENTS.md': 'x'.repeat(50) },
61
+ });
62
+ check('T2 字节未超 exit 0', run(root).code === 0);
63
+
64
+ // ---- T3 #count 超限 ----
65
+ root = makeFixture({
66
+ budgets: '.agents/rules/ 2#count\n',
67
+ dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
68
+ });
69
+ {
70
+ const r = run(root);
71
+ check('T3 #count 超限 exit 1 + 条目数违例行 + 一进一出提示', r.code === 1 && r.out.includes('条目数') && r.out.includes('3 项 > 上限 2 项') && r.out.includes('一进一出'), JSON.stringify(r));
72
+ }
73
+
74
+ // ---- T4 #count 压线(= 上限)→ 0 ----
75
+ root = makeFixture({
76
+ budgets: '.agents/rules/ 3#count\n',
77
+ dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
78
+ });
79
+ check('T4 #count 压线 exit 0', run(root).code === 0);
80
+
81
+ // ---- T5 混合表:#count 行存在但未触其它字节条目 → 0(互不干扰)----
82
+ root = makeFixture({
83
+ budgets: 'AGENTS.md 100\n.agents/rules/ 3#count\n',
84
+ files: { 'AGENTS.md': 'x'.repeat(50) },
85
+ dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
86
+ });
87
+ check('T5 混合表双通过 exit 0', run(root).code === 0);
88
+
89
+ // ---- T6 缺预算表 → exit 0(fixture 沙箱不拦,既有语义)----
90
+ root = makeFixture({ budgets: null });
91
+ check('T6 缺预算表 exit 0', run(root).code === 0);
92
+
93
+ // ---- T7 --staged 无 git(staged 空):#count 门跳过不拦 ----
94
+ root = makeFixture({
95
+ budgets: '.agents/rules/ 2#count\n',
96
+ dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
97
+ });
98
+ {
99
+ const r = run(root, '--staged');
100
+ check('T7 --staged 无 git 环境跳过 #count 门 exit 0', r.code === 0, JSON.stringify(r));
101
+ }
102
+
103
+ // ---- T8 glob 形态 #count(* 后缀)也按条数执法 ----
104
+ root = makeFixture({
105
+ budgets: '.agents/commands/*.md 2#count\n',
106
+ dirs: { '.agents/commands': ['a.md', 'b.md', 'c.md', 'd.txt'] },
107
+ });
108
+ {
109
+ const r = run(root);
110
+ check('T8 glob #count 只数 *.md(3 项 > 2)exit 1', r.code === 1 && r.out.includes('3 项'), JSON.stringify(r));
111
+ }
112
+
113
+ for (const d of fs.readdirSync(os.tmpdir()).filter((f) => f.startsWith('rule-budget-'))) {
114
+ try { fs.rmSync(path.join(os.tmpdir(), d), { recursive: true, force: true }); } catch { /* Windows 文件锁容忍 */ }
115
+ }
116
+
117
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
118
+ process.exit(fail ? 1 : 0);
@@ -45,7 +45,9 @@ const classify = (content) => {
45
45
 
46
46
  function run() {
47
47
  const { topic, root, delegated, auto } = parseArgs();
48
- const draftsDir = path.join(root, '.zcode', 'drafts');
48
+ // 草稿暂存区 = .zcode/workflow-drafts(宿主保存工作流草稿的实际目录;
49
+ // 2026-10-16 自源仓 8deb032b 回灌纠正:原 .zcode/drafts 自引入起从未存在,脚本一跑即 fail)
50
+ const draftsDir = path.join(root, '.zcode', 'workflow-drafts');
49
51
  if (!fs.existsSync(draftsDir)) fail(`草稿目录 ${draftsDir} 不存在`);
50
52
  const files = fs.readdirSync(draftsDir).filter((f) => f.includes(topic) && f.endsWith('.md'));
51
53
  if (!files.length) fail(`在 ${draftsDir} 中未找到主题为 ${topic} 的草稿文件`);
@@ -35,7 +35,7 @@ const mkfix = () => {
35
35
  fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
36
36
  }
37
37
  fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
38
- fs.mkdirSync(path.join(d, '.zcode', 'drafts'), { recursive: true });
38
+ fs.mkdirSync(path.join(d, '.zcode', 'workflow-drafts'), { recursive: true });
39
39
  fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), ENUMS_FIXTURE);
40
40
  fs.writeFileSync(path.join(d, '.agents', 'workflow-modules.txt'), '# modules\npipeline\nui\n');
41
41
  return d;
@@ -57,7 +57,7 @@ const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-
57
57
  // T1:默认只迁移不落账
58
58
  {
59
59
  const d = mkfix();
60
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
60
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-task.md'), DRAFT('demo'));
61
61
  const r = run(d, ['--topic', 'demo', '--root', d]);
62
62
  check('T1 默认迁移成功 exit 0', r.code === 0, r.out);
63
63
  check('T1 草稿已迁移到 intents 且日期前缀正确',
@@ -68,7 +68,7 @@ const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-
68
68
  // T2:--delegated 原话原样转发
69
69
  {
70
70
  const d = mkfix();
71
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
71
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-task.md'), DRAFT('demo'));
72
72
  const r = run(d, ['--topic', 'demo', '--root', d, '--delegated', '固化了可以']);
73
73
  check('T2 --delegated 流程 exit 0', r.code === 0, r.out);
74
74
  const lines = ledgerLines(d);
@@ -81,7 +81,7 @@ const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-
81
81
  // T3:--auto 在 Strict(缺 trust-mode.json)下被拒 → 失败计数并 exit 1
82
82
  {
83
83
  const d = mkfix();
84
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
84
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-task.md'), DRAFT('demo'));
85
85
  const r = run(d, ['--topic', 'demo', '--root', d, '--auto']);
86
86
  check('T3 --auto 在缺省 Strict 下 exit 1(不吞失败)', r.code === 1, r.out);
87
87
  check('T3 输出含失败汇总', r.out.includes('确认汇总') && r.out.includes('失败'), r.out);
@@ -90,28 +90,28 @@ const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-
90
90
  // T4:缺草稿 → exit 1
91
91
  {
92
92
  const d = mkfix();
93
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'other.md'), DRAFT('other'));
93
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'other.md'), DRAFT('other'));
94
94
  const r = run(d, ['--topic', 'nope', '--root', d]);
95
95
  check('T4 缺主题草稿 exit 1 且带指引', r.code === 1 && r.out.includes('未找到主题'), r.out);
96
96
  }
97
97
  // T5:草稿非可识别文档 → exit 1
98
98
  {
99
99
  const d = mkfix();
100
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-note.md'), '# 随手笔记\nnot a workflow doc\n');
100
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-note.md'), '# 随手笔记\nnot a workflow doc\n');
101
101
  const r = run(d, ['--topic', 'demo', '--root', d]);
102
102
  check('T5 无可识别文档 exit 1', r.code === 1 && r.out.includes('未找到可识别'), r.out);
103
103
  }
104
104
  // T6:--delegated 与 --auto 互斥 → exit 1
105
105
  {
106
106
  const d = mkfix();
107
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
107
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-task.md'), DRAFT('demo'));
108
108
  const r = run(d, ['--topic', 'demo', '--root', d, '--delegated', 'x', '--auto']);
109
109
  check('T6 互斥 exit 1', r.code === 1 && r.out.includes('互斥'), r.out);
110
110
  }
111
111
  // T7:失败路径退出码传播(确认失败后 exit 1,不吞)
112
112
  {
113
113
  const d = mkfix();
114
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
114
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-task.md'), DRAFT('demo'));
115
115
  // 让确认失败:用 --auto 触发 Strict 拒绝(缺 trust-mode.json 即 Strict)。
116
116
  const r = run(d, ['--topic', 'demo', '--root', d, '--auto']);
117
117
  check('T7 失败后 exit 1 且索引仍生成', r.code === 1 && fs.existsSync(path.join(d, 'workflow', 'INDEX.md')), r.out);
@@ -119,11 +119,11 @@ const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-
119
119
  // T8:索引更新失败传播退出码(独立于确认失败路径——删除 workflow/plans 目录使 gen-workflow-index 失败)
120
120
  {
121
121
  const d = mkfix();
122
- fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
122
+ fs.writeFileSync(path.join(d, '.zcode', 'workflow-drafts', 'demo-task.md'), DRAFT('demo'));
123
123
  fs.rmSync(path.join(d, 'workflow', 'plans'), { recursive: true });
124
124
  const r = run(d, ['--topic', 'demo', '--root', d]);
125
125
  check('T8 索引更新失败 exit 1 且输出指明', r.code === 1 && r.out.includes('索引更新失败'), r.out);
126
126
  }
127
127
 
128
- console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
128
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
129
129
  process.exit(fail ? 1 : 0);
@@ -106,5 +106,5 @@ const DRAFT = (level = 'L1') => `---\n状态: draft\n级别: ${level}\nrisk_leve
106
106
  `${r.stdout}${r.stderr}\n${lines.join('|')}`);
107
107
  }
108
108
 
109
- console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
109
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
110
110
  process.exit(fail ? 1 : 0);