agentic-flow-kit 0.2.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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/bin/flow-kit.mjs +4 -0
  4. package/modules/gates/dotnet-ca/README.md +24 -0
  5. package/modules/gates/dotnet-ca/check-architecture.sh +128 -0
  6. package/modules/gates/dotnet-ca/local-pre-commit.line +1 -0
  7. package/modules/hosts/omp/agents/implementer.md +12 -0
  8. package/modules/hosts/omp/agents/independent-reviewer.md +11 -0
  9. package/modules/hosts/omp/agents/ui-verifier.md +12 -0
  10. package/modules/hosts/opencode/agents/implementer.md +14 -0
  11. package/modules/hosts/opencode/agents/independent-reviewer.md +10 -0
  12. package/modules/hosts/opencode/agents/ui-verifier.md +10 -0
  13. package/modules/hosts/opencode/commands/build.md +13 -0
  14. package/modules/hosts/opencode/commands/deploy.md +12 -0
  15. package/modules/hosts/opencode/commands/design.md +12 -0
  16. package/modules/hosts/opencode/commands/maintain.md +12 -0
  17. package/modules/hosts/opencode/commands/new-task.md +12 -0
  18. package/modules/hosts/opencode/commands/plan.md +12 -0
  19. package/modules/hosts/opencode/commands/review.md +12 -0
  20. package/modules/hosts/opencode/commands/test.md +14 -0
  21. package/modules/hosts/trae/agents/implementer.md +9 -0
  22. package/modules/hosts/trae/agents/independent-reviewer.md +8 -0
  23. package/modules/hosts/trae/agents/ui-verifier.md +12 -0
  24. package/modules/hosts/trae/commands/wf-build.md +14 -0
  25. package/modules/hosts/trae/commands/wf-deploy.md +13 -0
  26. package/modules/hosts/trae/commands/wf-design.md +13 -0
  27. package/modules/hosts/trae/commands/wf-maintain.md +13 -0
  28. package/modules/hosts/trae/commands/wf-new-task.md +13 -0
  29. package/modules/hosts/trae/commands/wf-plan.md +13 -0
  30. package/modules/hosts/trae/commands/wf-review.md +13 -0
  31. package/modules/hosts/trae/commands/wf-test.md +14 -0
  32. package/modules/hosts/trae/hooks/post-edit-check.cjs +87 -0
  33. package/modules/hosts/trae/hooks/pre-shell-check.cjs +151 -0
  34. package/modules/hosts/trae/hooks/resolve-shell.cjs +21 -0
  35. package/modules/hosts/trae/hooks.json +29 -0
  36. package/modules/hosts/trae/rules/beautify-input-chinese.md +6 -0
  37. package/modules/hosts/trae/rules/git-commit-message.md +10 -0
  38. package/modules/hosts/trae/rules/project-workflow.md +14 -0
  39. package/modules/hosts/zcode/agents/implementer.md +11 -0
  40. package/modules/hosts/zcode/agents/independent-reviewer.md +10 -0
  41. package/modules/hosts/zcode/agents/ui-verifier.md +10 -0
  42. package/package.json +34 -0
  43. package/src/add-gate.mjs +83 -0
  44. package/src/add-host.mjs +63 -0
  45. package/src/cli.mjs +81 -0
  46. package/src/doctor.mjs +150 -0
  47. package/src/init.mjs +190 -0
  48. package/src/profiles.mjs +101 -0
  49. package/src/render.mjs +68 -0
  50. package/src/run-tests.mjs +35 -0
  51. package/src/sync.mjs +141 -0
  52. package/src/sync.test.mjs +207 -0
  53. package/templates/.gitattributes +6 -0
  54. package/templates/AGENTS.md +52 -0
  55. package/templates/_agents/board/index.html +382 -0
  56. package/templates/_agents/board/marked.min.js +6 -0
  57. package/templates/_agents/cache/kb-index.json +1 -0
  58. package/templates/_agents/commands/build.md +74 -0
  59. package/templates/_agents/commands/deploy.md +65 -0
  60. package/templates/_agents/commands/design.md +54 -0
  61. package/templates/_agents/commands/maintain.md +91 -0
  62. package/templates/_agents/commands/new-task.md +73 -0
  63. package/templates/_agents/commands/plan.md +46 -0
  64. package/templates/_agents/commands/review.md +59 -0
  65. package/templates/_agents/commands/test.md +80 -0
  66. package/templates/_agents/hooks/check-pairing-incremental.sh +29 -0
  67. package/templates/_agents/hooks/check-wiki-ledger.sh +18 -0
  68. package/templates/_agents/hooks/commit-check.cjs +140 -0
  69. package/templates/_agents/hooks/local-pre-commit +6 -0
  70. package/templates/_agents/notes/runtime-env.md +18 -0
  71. package/templates/_agents/roles/implementer.md +46 -0
  72. package/templates/_agents/roles/independent-reviewer.md +43 -0
  73. package/templates/_agents/roles/ui-verifier.md +42 -0
  74. package/templates/_agents/rule-budgets.txt +8 -0
  75. package/templates/_agents/scripts/agg-delegations.cjs +175 -0
  76. package/templates/_agents/scripts/check-loop.sh +803 -0
  77. package/templates/_agents/scripts/check-loop.test.sh +1042 -0
  78. package/templates/_agents/scripts/ensure-board.ps1 +48 -0
  79. package/templates/_agents/scripts/gen-wiki-board.mjs +197 -0
  80. package/templates/_agents/scripts/gen-wiki-board.test.mjs +136 -0
  81. package/templates/_agents/scripts/gen-workflow-index.mjs +190 -0
  82. package/templates/_agents/scripts/gen-workflow-index.test.mjs +113 -0
  83. package/templates/_agents/scripts/gen-workflow-metrics.mjs +172 -0
  84. package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +101 -0
  85. package/templates/_agents/scripts/kb-cache-evict.mjs +50 -0
  86. package/templates/_agents/scripts/kb-cache-evict.test.mjs +104 -0
  87. package/templates/_agents/scripts/kb-search.mjs +307 -0
  88. package/templates/_agents/scripts/kb-search.test.mjs +153 -0
  89. package/templates/_agents/scripts/rule-budget.sh +68 -0
  90. package/templates/_agents/scripts/verify-wiki-consistency.mjs +192 -0
  91. package/templates/_agents/scripts/wiki-search.mjs +19 -0
  92. package/templates/_agents/scripts/workflow-board-server.mjs +244 -0
  93. package/templates/_agents/skills/verify-ui/SKILL.md +24 -0
  94. package/templates/_agents/skills/wiki/SKILL.md +104 -0
  95. package/templates/_agents/workflow-modules.txt +11 -0
  96. package/templates/_githooks/commit-msg +15 -0
  97. package/templates/_githooks/post-commit +10 -0
  98. package/templates/_githooks/pre-commit +58 -0
  99. package/templates/_githooks/pre-merge-commit +24 -0
  100. package/templates/_githooks/pre-push +32 -0
  101. package/templates/wiki/INDEX.md +23 -0
  102. package/templates/wiki/drafts-archive/README.md +3 -0
  103. package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +56 -0
  104. package/templates/workflow/README.md +52 -0
  105. package/templates/workflow/delegations.md +28 -0
  106. package/templates/workflow/incidents/_TEMPLATE.md +42 -0
  107. package/templates/workflow/intents/_TEMPLATE.md +40 -0
  108. package/templates/workflow/papercuts.md +7 -0
  109. package/templates/workflow/plans/_TEMPLATE.md +38 -0
  110. package/templates/workflow/regression-checklist.md +22 -0
  111. package/templates/workflow/specs/_TEMPLATE.md +41 -0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // gen-workflow-index.mjs 的 fixture 驱动测试
3
+ // 现场构造临时 workflow 根,以「换 cwd」方式运行被测脚本(脚本按相对路径 'workflow/' 读盘)。
4
+ // 断言:①活跃(draft/approved/open)逐行出表、终态折叠进档案计数(含 fixed incident)、验收勾验统计;
5
+ // ②--check 一致 exit 0 / 漂移 exit 1(失败路径);③缺 `模块:` 容忍显示 —。
6
+ // 用法:node .agents/scripts/gen-workflow-index.test.mjs(在仓库任意目录执行均可)
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
+
13
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const GEN = path.join(SCRIPT_DIR, 'gen-workflow-index.mjs');
15
+ const USAGE = '用法:node .agents/scripts/gen-workflow-index.test.mjs';
16
+
17
+ let pass = 0;
18
+ let fail = 0;
19
+ const check = (desc, cond, detail = '') => {
20
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
21
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
22
+ };
23
+
24
+ const doc = (fm, title, body = '') => `---\n${fm}\n---\n# ${title}\n\n${body}\n`;
25
+
26
+ // mkfix:建临时 workflow 根(4 个子目录齐备——生成器按目录枚举,缺目录会 throw)
27
+ const mkfix = () => {
28
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-workflow-index-test-'));
29
+ for (const d of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(root, 'workflow', d), { recursive: true });
30
+ const w = path.join(root, 'workflow');
31
+ fs.writeFileSync(path.join(w, 'intents', '2026-09-21-alpha.md'), doc(
32
+ '状态: approved\n级别: L1\n日期: 2026-09-21\n模块: pipeline',
33
+ 'INTENT — 在跑的需求甲',
34
+ '## 验收标准(可测试)\n- [x] 已验项(证据:实跑)\n- [ ] 未验项\n',
35
+ ), 'utf8');
36
+ fs.writeFileSync(path.join(w, 'plans', '2026-09-21-alpha.md'), doc(
37
+ '状态: approved\n级别: L1\n模块: pipeline',
38
+ 'PLAN — 在跑的需求甲',
39
+ ), 'utf8');
40
+ fs.writeFileSync(path.join(w, 'plans', '2026-09-19-beta.md'), doc(
41
+ '状态: done\n级别: L1\n模块: material',
42
+ 'PLAN — 已完结的需求乙',
43
+ ), 'utf8');
44
+ fs.writeFileSync(path.join(w, 'incidents', '2026-09-18-gamma.md'), doc(
45
+ '状态: fixed\n级别: L2\n发现: 2026-09-18\n模块: material',
46
+ 'INCIDENT — 已修复未收口的事故丙',
47
+ ), 'utf8');
48
+ fs.writeFileSync(path.join(w, 'intents', '_TEMPLATE.md'), doc('状态: draft\n级别: L1', 'INTENT — 模板不该进表'), 'utf8');
49
+ return root;
50
+ };
51
+
52
+ const run = (root, args = []) => spawnSync(process.execPath, [GEN, ...args], { cwd: root, encoding: 'utf8' });
53
+
54
+ // ---- 场景 1:主路径——活跃逐行 + 终态折叠 + 验收统计 ----
55
+ {
56
+ const root = mkfix();
57
+ const r = run(root);
58
+ const idx = fs.readFileSync(path.join(root, 'workflow', 'INDEX.md'), 'utf8');
59
+ check('场景 1:生成器 exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
60
+ check('场景 1:活跃 2 行(approved intent + approved plan)', idx.includes('## 活跃(2)'), idx.split('\n').slice(0, 14).join('\n'));
61
+ check('场景 1:活跃行含 7 字段(类型/级别/状态/日期/模块/标题/验收)',
62
+ /\| INTENT \| L1 \| approved \| 2026-09-21 \| pipeline \| 在跑的需求甲 \| ☑1\/2 \|/.test(idx),
63
+ idx.split('\n').filter((l) => l.startsWith('| INTENT')).join('\n'));
64
+ check('场景 1:终态折叠为档案 2 篇(done plan + fixed incident)', idx.includes('## 档案计数(2,不进表)'), '');
65
+ check('场景 1:档案按类型×模块计数', idx.includes('| PLAN | material | 1 |') && idx.includes('| INCIDENT | material | 1 |'), '');
66
+ check('场景 1:终态构成行含 done 1 · fixed 1', /终态构成:.*done 1.*fixed 1/.test(idx), idx.split('\n').slice(-2).join('\n'));
67
+ check('场景 1:_TEMPLATE.md 不进表', !idx.includes('模板不该进表'), '');
68
+ fs.rmSync(root, { recursive: true, force: true });
69
+ }
70
+
71
+ // ---- 场景 2:--check 一致 exit 0 / 漂移 exit 1 / 重生成后复原(失败路径)----
72
+ {
73
+ const root = mkfix();
74
+ run(root);
75
+ const ok = run(root, ['--check']);
76
+ check('场景 2:生成后 --check exit 0', ok.status === 0, `exit=${ok.status}\n${ok.stdout}${ok.stderr}`);
77
+ const p = path.join(root, 'workflow', 'intents', '2026-09-21-alpha.md');
78
+ fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replace('状态: approved', '状态: done'), 'utf8');
79
+ const drift = run(root, ['--check']);
80
+ check('场景 2:文档状态变更后 --check exit 1(漂移)', drift.status === 1, `exit=${drift.status}\n${drift.stdout}${drift.stderr}`);
81
+ check('场景 2:漂移提示含重生成命令', /gen-workflow-index\.mjs 重新生成/.test(drift.stderr), drift.stderr);
82
+ run(root);
83
+ const ok2 = run(root, ['--check']);
84
+ check('场景 2:重生成后 --check 复原 exit 0', ok2.status === 0, `exit=${ok2.status}\n${ok2.stdout}${ok2.stderr}`);
85
+ fs.rmSync(root, { recursive: true, force: true });
86
+ }
87
+
88
+ // ---- 场景 3:缺 `模块:` 容忍显示 —(不报错、不阻断)----
89
+ {
90
+ const root = mkfix();
91
+ const p = path.join(root, 'workflow', 'plans', '2026-09-21-alpha.md');
92
+ fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replace('模块: pipeline\n', ''), 'utf8');
93
+ const r = run(root);
94
+ const idx = fs.readFileSync(path.join(root, 'workflow', 'INDEX.md'), 'utf8');
95
+ check('场景 3:缺模块字段仍 exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
96
+ check('场景 3:模块列显示 —', /^\| PLAN \| L1 \| approved \| .* \| — \|/m.test(idx), idx.split('\n').filter((l) => l.startsWith('| PLAN | approved')).join('\n'));
97
+ fs.rmSync(root, { recursive: true, force: true });
98
+ }
99
+
100
+ // ---- 场景 4:非法参数 exit 1 ----
101
+ {
102
+ const root = mkfix();
103
+ const r = run(root, ['--nope']);
104
+ check('场景 4:未知参数 exit 1', r.status === 1 && /未知参数/.test(r.stderr), `exit=${r.status}\n${r.stderr}`);
105
+ fs.rmSync(root, { recursive: true, force: true });
106
+ }
107
+
108
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
109
+ if (fail) {
110
+ console.log(`\n${USAGE}`);
111
+ process.exit(1);
112
+ }
113
+ process.exit(0);
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ // workflow 规模与规则面月度快照(2026-09-21,intent 2026-09-21-workflow-metrics-snapshot)
3
+ // 用法:node .agents/scripts/gen-workflow-metrics.mjs [--month YYYY-MM] [--dry-run] [--help]
4
+ // 目的:把「语料膨胀 + 常驻面体积」变成可观测趋势——每月(或按需)跑一次,往 workflow/metrics.md 追加/更新一行。
5
+ // 口径:文档 = 磁盘(对照 gen-workflow-index / 看板,排除 _TEMPLATE.md);常驻面 = `.agents/rule-budgets.txt`
6
+ // 各条实测(单源,含预算上限),括号内给「预算占用峰值项」。
7
+ // 不做 --check / 不挂门禁:快照是历史记录——任何文档改动都会让「当前值」漂移,拿它做漂移校验只会常红;
8
+ // 规则面的机器约束由 pre-commit 的 rule-budget.sh 负责,本脚本只负责「看见趋势」。
9
+ // 测试:node .agents/scripts/gen-workflow-metrics.test.mjs(fixture 回归,须全绿)
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ const USAGE = `用法:node .agents/scripts/gen-workflow-metrics.mjs [--month YYYY-MM] [--dry-run] [--help]
14
+
15
+ 事实源:workflow/{intents,specs,plans,incidents}/*.md(排除 _TEMPLATE.md)+ workflow/INDEX.md + .agents/rule-budgets.txt
16
+ 产物:workflow/metrics.md —— 每月一行(同月重跑即更新该行,不重复追加);明细打印在 stdout,不落表。
17
+ 选项:
18
+ --month YYYY-MM 指定月份键(默认取当前月;用于补记历史)
19
+ --dry-run 只打印将写入的行与明细,不写盘
20
+ --help 打印本说明`;
21
+
22
+ const args = process.argv.slice(2);
23
+ if (args.includes('--help') || args.includes('-h')) { console.log(USAGE); process.exit(0); }
24
+ const DRY = args.includes('--dry-run');
25
+ let month = '';
26
+ for (let i = 0; i < args.length; i++) if (args[i] === '--month') month = args[++i] ?? '';
27
+ const unknown = [];
28
+ for (let i = 0; i < args.length; i++) {
29
+ const a = args[i];
30
+ if (a === '--dry-run' || a === '--help' || a === '-h') continue;
31
+ if (a === '--month') { i++; continue; }
32
+ unknown.push(a);
33
+ }
34
+ if (unknown.length) { console.error(`❌ 未知参数:${unknown.join(' ')}\n\n${USAGE}`); process.exit(1); }
35
+ if (!month) month = new Date().toISOString().slice(0, 7);
36
+ if (!/^\d{4}-\d{2}$/.test(month)) { console.error(`--month 须为 YYYY-MM(现 ${month || '空'})\n\n${USAGE}`); process.exit(1); }
37
+
38
+ const WORKFLOW = 'workflow';
39
+ const METRICS_P = path.join(WORKFLOW, 'metrics.md');
40
+ const BUDGETS = path.join('.agents', 'rule-budgets.txt');
41
+ const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
42
+ const ACTIVE_STATUS = ['draft', 'approved', 'open'];
43
+ const BEGIN = '<!-- GENERATED:BEGIN — gen-workflow-metrics.mjs 整段重写,手工说明写在本行之前 -->';
44
+ const END = '<!-- GENERATED:END -->';
45
+
46
+ const kb = (b) => (b < 1024 * 1024 ? `${(b / 1024).toFixed(1)} KB` : `${(b / 1024 / 1024).toFixed(2)} MB`);
47
+
48
+ // ---- 语料统计:按类型聚合文件数/字节 + 活跃终态 + 模块已填 ----
49
+ function scanDocs() {
50
+ const perType = {};
51
+ let files = 0;
52
+ let bytes = 0;
53
+ let active = 0;
54
+ let withModule = 0;
55
+ for (const type of DOC_TYPES) {
56
+ let n = 0;
57
+ let b = 0;
58
+ const dir = path.join(WORKFLOW, type);
59
+ for (const f of fs.readdirSync(dir)) {
60
+ if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
61
+ const p = path.join(dir, f);
62
+ const text = fs.readFileSync(p, 'utf8');
63
+ const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
64
+ let status = '';
65
+ let mod = '';
66
+ if (fm) {
67
+ for (const line of fm[1].split(/\r?\n/)) {
68
+ const kv = line.match(/^(\S+):\s*(.*)$/);
69
+ if (!kv) continue;
70
+ if (kv[1] === '状态') status = kv[2].trim();
71
+ if (kv[1] === '模块') mod = kv[2].trim();
72
+ }
73
+ }
74
+ if (ACTIVE_STATUS.includes(status)) active++;
75
+ if (mod) withModule++;
76
+ n++;
77
+ b += fs.statSync(p).size;
78
+ }
79
+ perType[type] = { n, b };
80
+ files += n;
81
+ bytes += b;
82
+ }
83
+ return { perType, files, bytes, active, terminal: files - active, withModule };
84
+ }
85
+
86
+ // ---- 常驻面:按预算表逐条实测(<glob|dir/> <上限>)----
87
+ // 峰值口径:逐文件条取「该文件」,glob 条取「单篇最大」,目录条取「合计」——三者不混(曾把目录合计除以单篇上限得出 548%)
88
+ function scanSurface() {
89
+ if (!fs.existsSync(BUDGETS)) return { entries: [], total: 0, peak: null };
90
+ const entries = [];
91
+ for (const raw of fs.readFileSync(BUDGETS, 'utf8').split(/\r?\n/)) {
92
+ const line = raw.trim();
93
+ if (!line || line.startsWith('#')) continue;
94
+ const [pat, limRaw] = line.split(/\s+/);
95
+ const lim = Number(limRaw);
96
+ if (!pat || !Number.isFinite(lim)) continue;
97
+ let sum = 0;
98
+ let max = 0;
99
+ let label = pat;
100
+ if (pat.endsWith('/')) {
101
+ const dir = pat.slice(0, -1);
102
+ for (const f of fs.readdirSync(dir)) sum += fs.statSync(path.join(dir, f)).size;
103
+ max = sum;
104
+ label = `${pat}(合计)`;
105
+ } else if (pat.includes('*')) {
106
+ const dir = path.dirname(pat);
107
+ const suffix = pat.slice(pat.indexOf('*') + 1);
108
+ for (const f of fs.readdirSync(dir)) {
109
+ if (!f.endsWith(suffix)) continue;
110
+ const s = fs.statSync(path.join(dir, f)).size;
111
+ sum += s;
112
+ if (s > max) max = s;
113
+ }
114
+ label = `${pat}(单篇最大)`;
115
+ } else {
116
+ sum = max = fs.statSync(pat).size;
117
+ }
118
+ entries.push({ pat, label, sum, max, lim, pct: (max / lim) * 100, isDir: pat.endsWith('/') });
119
+ }
120
+ const total = entries.filter((e) => !e.isDir).reduce((n, e) => n + e.sum, 0);
121
+ const peak = entries.slice().sort((a, b) => b.pct - a.pct)[0] || null;
122
+ return { entries, total, peak };
123
+ }
124
+
125
+ const idxText = fs.existsSync(path.join(WORKFLOW, 'INDEX.md')) ? fs.readFileSync(path.join(WORKFLOW, 'INDEX.md'), 'utf8') : '';
126
+ const idxBytes = Buffer.byteLength(idxText, 'utf8');
127
+ const idxActive = Number((idxText.match(/^## 活跃((\d+))/m) || [])[1] ?? NaN);
128
+
129
+ const docs = scanDocs();
130
+ const surf = scanSurface();
131
+ const row = `| ${month} | ${docs.files}(活跃 ${docs.active} / 终态 ${docs.terminal}) | ${kb(docs.bytes)} | ${docs.withModule}/${docs.files} | ${kb(surf.total)}(峰值 ${surf.peak ? `${surf.peak.pct.toFixed(1)}% ${surf.peak.label}` : '—'}) | ${kb(idxBytes)}(${Number.isNaN(idxActive) ? '—' : idxActive} 行) |`;
132
+
133
+ // ---- 明细(stdout,不落表):类型分布 + 逐条预算占用 ----
134
+ console.log(`📅 ${month} 快照`);
135
+ console.log(` 文档:${docs.files} 篇 / ${kb(docs.bytes)}(活跃 ${docs.active} / 终态 ${docs.terminal});模块已填 ${docs.withModule}/${docs.files}`);
136
+ for (const [t, v] of Object.entries(docs.perType)) console.log(` ${t.padEnd(10)} ${String(v.n).padStart(3)} 篇 / ${kb(v.b)}`);
137
+ console.log(` 索引:workflow/INDEX.md ${kb(idxBytes)}(活跃 ${Number.isNaN(idxActive) ? '—' : idxActive} 行)`);
138
+ for (const e of surf.entries) console.log(` ${e.label.padEnd(44)} ${String(e.max).padStart(6)} / ${String(e.lim).padStart(6)} B(${e.pct.toFixed(1)}%)`);
139
+ console.log(` 常驻面合计:${kb(surf.total)}`);
140
+ console.log(` ${DRY ? '(--dry-run,未写盘)' : ''}`);
141
+
142
+ if (DRY) { console.log(`\n将写入行:\n${row}`); process.exit(0); }
143
+
144
+ // ---- 组装:GENERATED 区内的月表按月份键 upsert(同月重跑覆盖,行序按月升序)----
145
+ const HEADER = `# workflow 规模与规则面月度快照
146
+
147
+ > 生成器 \`node .agents/scripts/gen-workflow-metrics.mjs\`(每月或有需要时跑;同月重跑即更新该行)。明细(类型分布 / 逐条预算占用)看脚本 stdout,不落表。
148
+ > 口径:文档 = 磁盘(排除 \`_TEMPLATE.md\`);常驻面 = \`.agents/rule-budgets.txt\` 逐条实测,括号内为「预算占用峰值项」。
149
+ `;
150
+ const TABLE_HEAD = [
151
+ '| 月份 | 文档(活跃/终态) | 文档字节 | 模块已填/总 | 常驻面字节(预算峰值) | INDEX(活跃行) |',
152
+ '|---|---|---|---|---|---|',
153
+ ];
154
+ const existing = fs.existsSync(METRICS_P) ? fs.readFileSync(METRICS_P, 'utf8') : '';
155
+ let rows = [];
156
+ if (existing.includes(BEGIN) && existing.includes(END)) {
157
+ rows = existing.slice(existing.indexOf(BEGIN), existing.indexOf(END))
158
+ .split(/\r?\n/)
159
+ .filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
160
+ }
161
+ rows = rows.filter((l) => !l.startsWith(`| ${month} `));
162
+ rows.push(row);
163
+ rows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
164
+ const inner = `${BEGIN}\n\n${[...TABLE_HEAD, ...rows].join('\n')}\n\n${END}`;
165
+ let out;
166
+ if (existing.includes(BEGIN) && existing.includes(END)) {
167
+ out = existing.slice(0, existing.indexOf(BEGIN)) + inner + existing.slice(existing.indexOf(END) + END.length);
168
+ } else {
169
+ out = (existing ? existing.replace(/\s*$/, '') + '\n\n' : HEADER) + inner + '\n';
170
+ }
171
+ fs.writeFileSync(METRICS_P, out, 'utf8');
172
+ console.log(`✅ 已更新 workflow/metrics.md(${month} 行;共 ${rows.length} 个月)`);
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // gen-workflow-metrics.mjs 的 fixture 驱动测试
3
+ // 现场构造临时语料(换 cwd 运行被测脚本,脚本按相对路径读 workflow/ 与 .agents/rule-budgets.txt)。
4
+ // 断言:①主路径——类型聚合/活跃终态/模块已填/常驻面峰值/INDEX 活跃行;②同月重跑 upsert 不重复、跨月按月升序;
5
+ // ③失败路径——非法 --month / 未知参数 exit 1。
6
+ // 用法:node .agents/scripts/gen-workflow-metrics.test.mjs(在仓库任意目录执行均可)
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
+
13
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const GEN = path.join(SCRIPT_DIR, 'gen-workflow-metrics.mjs');
15
+ const USAGE = '用法:node .agents/scripts/gen-workflow-metrics.test.mjs';
16
+
17
+ let pass = 0;
18
+ let fail = 0;
19
+ const check = (desc, cond, detail = '') => {
20
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
21
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
22
+ };
23
+
24
+ const doc = (fm, title) => `---\n${fm}\n---\n# ${title}\n`;
25
+
26
+ const mkfix = () => {
27
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-workflow-metrics-test-'));
28
+ for (const d of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(root, 'workflow', d), { recursive: true });
29
+ fs.mkdirSync(path.join(root, '.agents', 'commands'), { recursive: true });
30
+ const w = path.join(root, 'workflow');
31
+ fs.writeFileSync(path.join(w, 'intents', '2026-09-21-a.md'), doc('状态: approved\n级别: L1\n日期: 2026-09-21\n模块: pipeline', 'INTENT — 甲'), 'utf8');
32
+ fs.writeFileSync(path.join(w, 'plans', '2026-09-21-a.md'), doc('状态: approved\n级别: L1\n模块: pipeline', 'PLAN — 甲'), 'utf8');
33
+ fs.writeFileSync(path.join(w, 'plans', '2026-09-19-b.md'), doc('状态: done\n级别: L1\n模块: material', 'PLAN — 乙'), 'utf8');
34
+ fs.writeFileSync(path.join(w, 'incidents', '2026-09-18-c.md'), doc('状态: closed\n级别: L2\n发现: 2026-09-18', 'INCIDENT — 丙'), 'utf8');
35
+ fs.writeFileSync(path.join(w, 'intents', '_TEMPLATE.md'), doc('状态: draft\n级别: L1', 'INTENT — 模板'), 'utf8');
36
+ fs.writeFileSync(path.join(w, 'INDEX.md'), '# index fixture\n\n## 活跃(2)\n\n| 类型 |\n|---|\n', 'utf8');
37
+ // 预算表:AGENTS.md 120B / 上限 100 → 峰值 120%;commands 单篇与合计都远低于上限
38
+ fs.writeFileSync(path.join(root, '.agents', 'rule-budgets.txt'), [
39
+ '# fixture 预算表',
40
+ 'AGENTS.md 100',
41
+ '.agents/commands/*.md 50',
42
+ '.agents/commands/ 100',
43
+ '',
44
+ ].join('\n'), 'utf8');
45
+ fs.writeFileSync(path.join(root, 'AGENTS.md'), 'x'.repeat(120), 'utf8');
46
+ fs.writeFileSync(path.join(root, '.agents', 'commands', 'plan.md'), 'y'.repeat(10), 'utf8');
47
+ return root;
48
+ };
49
+
50
+ const run = (root, args) => spawnSync(process.execPath, [GEN, ...args], { cwd: root, encoding: 'utf8' });
51
+ const readMetrics = (root) => fs.readFileSync(path.join(root, 'workflow', 'metrics.md'), 'utf8');
52
+
53
+ // ---- 场景 1:主路径——一行算对 ----
54
+ {
55
+ const root = mkfix();
56
+ const r = run(root, ['--month', '2026-09']);
57
+ const md = fs.existsSync(path.join(root, 'workflow', 'metrics.md')) ? readMetrics(root) : '';
58
+ check('场景 1:生成 exit 0 且落盘 workflow/metrics.md', r.status === 0 && md.length > 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
59
+ check('场景 1:行含 4(活跃 2 / 终态 2)+ 模块 3/4', /\| 2026-09 \| 4(活跃 2 \/ 终态 2) \| .* \| 3\/4 \|/.test(md), md);
60
+ check('场景 1:常驻面峰值取最紧项(120.0% AGENTS.md)', /峰值 120\.0% AGENTS\.md/.test(md), md);
61
+ check('场景 1:常驻面合计 = AGENTS.md + commands 逐文件(130B → 0.1 KB)', /0\.1 KB(峰值/.test(md), md);
62
+ check('场景 1:INDEX 活跃行解析(2 行)', /(2 行) \|/.test(md), md);
63
+ check('场景 1:_TEMPLATE.md 不计入(4 篇而非 5 篇)', !/\| 5(/.test(md), md);
64
+ fs.rmSync(root, { recursive: true, force: true });
65
+ }
66
+
67
+ // ---- 场景 2:同月重跑 upsert 不重复;跨月按月升序 ----
68
+ {
69
+ const root = mkfix();
70
+ run(root, ['--month', '2026-09']);
71
+ run(root, ['--month', '2026-09']);
72
+ let md = readMetrics(root);
73
+ const n09 = (md.match(/^\| 2026-09 \|/gm) || []).length;
74
+ check('场景 2:同月重跑只保留一行', n09 === 1, `${n09} 行\n${md}`);
75
+ run(root, ['--month', '2026-08']);
76
+ md = readMetrics(root);
77
+ const i08 = md.indexOf('| 2026-08 |');
78
+ const i09 = md.indexOf('| 2026-09 |');
79
+ check('场景 2:跨月两行且按月升序', i08 > -1 && i09 > -1 && i08 < i09, md);
80
+ check('场景 2:HEADER 手工说明保留(生成区外内容不丢)', md.includes('# workflow 规模与规则面月度快照'), md);
81
+ fs.rmSync(root, { recursive: true, force: true });
82
+ }
83
+
84
+ // ---- 场景 3:失败路径 + --dry-run 不写盘 ----
85
+ {
86
+ const root = mkfix();
87
+ const bad = run(root, ['--month', '2026-9']);
88
+ check('场景 3:非法 --month exit 1', bad.status === 1 && /须为 YYYY-MM/.test(bad.stderr), `exit=${bad.status}\n${bad.stderr}`);
89
+ const unk = run(root, ['--bogus']);
90
+ check('场景 3:未知参数 exit 1', unk.status === 1 && /未知参数/.test(unk.stderr), `exit=${unk.status}\n${unk.stderr}`);
91
+ const dry = run(root, ['--dry-run']);
92
+ check('场景 3:--dry-run 不写盘且打印将写入行', dry.status === 0 && /将写入行/.test(dry.stdout) && !fs.existsSync(path.join(root, 'workflow', 'metrics.md')), dry.stdout);
93
+ fs.rmSync(root, { recursive: true, force: true });
94
+ }
95
+
96
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
97
+ if (fail) {
98
+ console.log(`\n${USAGE}`);
99
+ process.exit(1);
100
+ }
101
+ process.exit(0);
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ // kb-search 结构缓存推送式失效(2026-09-23 intent kb-cache-post-commit-evict)
3
+ // 用途:.githooks/post-commit 把 `git diff-tree --name-only HEAD` 的清单经 stdin 传入,
4
+ // 摘除本次提交触及的 workflow/、wiki/ 文件在 .agents/cache/kb-index.json 中的全部命名空间条目,
5
+ // 收敛 [mtimeMs,size] 两元组的「同刻同尺寸」失效盲区至提交时点。
6
+ // 语义:非阻断维护——一切坏输入(缺缓存/坏 JSON/stdin 不可读)静默 exit 0;命中才回写且保留原 fp。
7
+ // 测试:node .agents/scripts/kb-cache-evict.test.mjs(--cache 指向临时文件,不触真实缓存)
8
+ // 用法:kb-cache-evict.mjs [--files-from <path>] [--cache <path>];二者缺省时读 stdin 与默认缓存。
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const args = process.argv.slice(2);
15
+ const opt = (name) => {
16
+ const i = args.indexOf(name);
17
+ return i > -1 ? args[i + 1] : '';
18
+ };
19
+
20
+ function readList() {
21
+ const from = opt('--files-from');
22
+ if (from) {
23
+ try { return fs.readFileSync(from, 'utf8'); } catch { return ''; }
24
+ }
25
+ if (process.stdin.isTTY) return '';
26
+ try { return fs.readFileSync(0, 'utf8'); } catch { return ''; }
27
+ }
28
+
29
+ const cacheFile = opt('--cache') || path.join(SCRIPT_DIR, '..', 'cache', 'kb-index.json');
30
+ const files = [...new Set(
31
+ readList()
32
+ .split(/\r?\n/)
33
+ .map((s) => s.trim().replace(/\\/g, '/'))
34
+ .filter((s) => s.startsWith('workflow/') || s.startsWith('wiki/'))
35
+ )];
36
+ if (!files.length) process.exit(0);
37
+
38
+ let j;
39
+ try { j = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); } catch { process.exit(0); }
40
+ if (!j || !j.files || typeof j.files !== 'object') process.exit(0);
41
+
42
+ let removed = 0;
43
+ for (const k of Object.keys(j.files)) {
44
+ const i = k.lastIndexOf('::');
45
+ if (i > -1 && files.includes(k.slice(i + 2))) { delete j.files[k]; removed++; }
46
+ }
47
+ if (removed) {
48
+ try { fs.writeFileSync(cacheFile, JSON.stringify(j)); } catch { /* 只读等写失败:下次查询两元组兜底 */ }
49
+ }
50
+ process.exit(0);
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ // kb-cache-evict.mjs 的 fixture 测试(--cache 指临时文件,不触真实缓存)
3
+ // 断言:①跨命名空间摘除命中 key、他 key 原样保留、fp 不变;②范围外清单不动缓存;
4
+ // ③坏输入(缺文件/坏 JSON/无 files 字段)静默 exit 0;④stdin 管道路径与 --files-from 等价。
5
+ // 用法:node .agents/scripts/kb-cache-evict.test.mjs
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
+ const EVICT = path.join(SCRIPT_DIR, 'kb-cache-evict.mjs');
14
+
15
+ let pass = 0;
16
+ let fail = 0;
17
+ const check = (desc, cond, detail = '') => {
18
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
19
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
20
+ };
21
+
22
+ const mkCache = () => {
23
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-evict-test-'));
24
+ const file = path.join(dir, 'kb-index.json');
25
+ const entry = (t) => ({ st: [111, 222], kind: 'wf', meta: { 状态: t }, title: 'T', lines: [[1, '(标题)', '# x']] });
26
+ fs.writeFileSync(file, JSON.stringify({
27
+ fp: 'FIXFP00000000000',
28
+ files: {
29
+ 'CWD1::workflow/intents/a.md': entry('done'),
30
+ 'CWD2::workflow/intents/a.md': entry('done'),
31
+ 'CWD1::wiki/测试主题/b.md': entry('done'),
32
+ 'CWD1::workflow/plans/keep.md': entry('approved'),
33
+ 'CWD2::workflow/README.md': entry('draft'),
34
+ },
35
+ }), 'utf8');
36
+ return { dir, file };
37
+ };
38
+
39
+ const run = (argv, opts = {}) => spawnSync(process.execPath, [EVICT, ...argv], { encoding: 'utf8', ...opts });
40
+ const load = (f) => JSON.parse(fs.readFileSync(f, 'utf8'));
41
+ const LIST = 'workflow/intents/a.md\nwiki/测试主题/b.md\nbackend/Dtos/X.cs\n';
42
+
43
+ // ---- 场景 1:跨命名空间摘除 + 他 key 与 fp 保留 ----
44
+ {
45
+ const { dir, file } = mkCache();
46
+ const lf = path.join(dir, 'list.txt');
47
+ fs.writeFileSync(lf, LIST, 'utf8');
48
+ const r = run(['--files-from', lf, '--cache', file]);
49
+ const j = load(file);
50
+ check('场景 1:exit 0 且 stderr 空', r.status === 0 && !r.stderr, `exit=${r.status}\n${r.stderr}`);
51
+ check('场景 1:同 rel 跨两命名空间全摘(a.md×2 + b.md×1)',
52
+ !('CWD1::workflow/intents/a.md' in j.files) && !('CWD2::workflow/intents/a.md' in j.files) && !('CWD1::wiki/测试主题/b.md' in j.files), Object.keys(j.files).join('\n'));
53
+ check('场景 1:无关 key 原样保留(keep 与 README 完好,计数 2)',
54
+ Object.keys(j.files).length === 2 && j.files['CWD1::workflow/plans/keep.md']?.meta?.['状态'] === 'approved' && j.files['CWD2::workflow/README.md']?.meta?.['状态'] === 'draft', Object.keys(j.files).join('\n'));
55
+ check('场景 1:fp 保持不变', j.fp === 'FIXFP00000000000', j.fp);
56
+ fs.rmSync(dir, { recursive: true, force: true });
57
+ }
58
+
59
+ // ---- 场景 2:范围外清单 → 缓存文件逐字节不动 ----
60
+ {
61
+ const { dir, file } = mkCache();
62
+ const before = fs.readFileSync(file);
63
+ const lf = path.join(dir, 'list.txt');
64
+ fs.writeFileSync(lf, 'backend/Dtos/X.cs\nwiki\n\n', 'utf8');
65
+ const r = run(['--files-from', lf, '--cache', file]);
66
+ check('场景 2:范围外清单 exit 0 且缓存字节不变', r.status === 0 && fs.readFileSync(file).equals(before), `exit=${r.status}`);
67
+ fs.rmSync(dir, { recursive: true, force: true });
68
+ }
69
+
70
+ // ---- 场景 3:坏输入三态静默 exit 0 ----
71
+ {
72
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-evict-test-'));
73
+ const lf = path.join(dir, 'list.txt');
74
+ fs.writeFileSync(lf, 'workflow/intents/a.md\n', 'utf8');
75
+ const missing = path.join(dir, 'nope.json');
76
+ const garbage = path.join(dir, 'garbage.json');
77
+ fs.writeFileSync(garbage, 'not-json{{{', 'utf8');
78
+ const nofiles = path.join(dir, 'nofiles.json');
79
+ fs.writeFileSync(nofiles, JSON.stringify({ fp: 'x' }), 'utf8');
80
+ const a = run(['--files-from', lf, '--cache', missing]);
81
+ const b = run(['--files-from', lf, '--cache', garbage]);
82
+ const c = run(['--files-from', lf, '--cache', nofiles]);
83
+ check('场景 3:缺缓存/坏 JSON/无 files 字段均 exit 0 且无输出',
84
+ a.status === 0 && b.status === 0 && c.status === 0 && !a.stdout && !b.stdout && !c.stdout && !a.stderr && !b.stderr && !c.stderr,
85
+ `a=${a.status}/${a.stderr} b=${b.status}/${b.stderr} c=${c.status}/${c.stderr}`);
86
+ check('场景 3:清单文件缺失也静默 exit 0', run(['--files-from', path.join(dir, 'nope.txt'), '--cache', garbage]).status === 0);
87
+ fs.rmSync(dir, { recursive: true, force: true });
88
+ }
89
+
90
+ // ---- 场景 4:stdin 管道与 --files-from 等价(钩子实际走的路径) ----
91
+ {
92
+ const { dir, file } = mkCache();
93
+ const r = run(['--cache', file], { input: LIST });
94
+ const j = load(file);
95
+ check('场景 4:stdin 路径摘除同果', r.status === 0 && Object.keys(j.files).length === 2 && !('CWD1::wiki/测试主题/b.md' in j.files), `exit=${r.status} keys=${Object.keys(j.files).join(',')}`);
96
+ fs.rmSync(dir, { recursive: true, force: true });
97
+ }
98
+
99
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
100
+ if (fail) {
101
+ console.log('\n用法:node .agents/scripts/kb-cache-evict.test.mjs');
102
+ process.exit(1);
103
+ }
104
+ process.exit(0);