agentic-flow-kit 0.2.0 → 0.2.1

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 (56) hide show
  1. package/README.md +3 -2
  2. package/modules/gates/dotnet-ca/check-architecture.sh +14 -10
  3. package/modules/hosts/trae/commands/wf-build.md +14 -14
  4. package/modules/hosts/trae/commands/wf-deploy.md +13 -13
  5. package/modules/hosts/trae/commands/wf-design.md +13 -13
  6. package/modules/hosts/trae/commands/wf-maintain.md +13 -13
  7. package/modules/hosts/trae/commands/wf-new-task.md +13 -13
  8. package/modules/hosts/trae/commands/wf-plan.md +13 -13
  9. package/modules/hosts/trae/commands/wf-review.md +13 -13
  10. package/modules/hosts/trae/commands/wf-test.md +14 -14
  11. package/modules/hosts/trae/hooks/post-edit-check.cjs +10 -0
  12. package/modules/hosts/trae/hooks/pre-shell-check.cjs +14 -4
  13. package/modules/hosts/trae/hooks/resolve-shell.cjs +30 -21
  14. package/modules/hosts/trae/rules/beautify-input-chinese.md +6 -6
  15. package/modules/hosts/trae/rules/git-commit-message.md +10 -10
  16. package/package.json +10 -2
  17. package/src/add-gate.mjs +8 -1
  18. package/src/add-host.mjs +4 -0
  19. package/src/cli.mjs +4 -3
  20. package/src/doctor.mjs +9 -2
  21. package/src/render.mjs +2 -0
  22. package/src/sync.mjs +20 -4
  23. package/src/sync.test.mjs +40 -1
  24. package/templates/AGENTS.md +2 -2
  25. package/templates/_agents/board/index.html +382 -382
  26. package/templates/_agents/board/marked.min.js +6 -6
  27. package/templates/_agents/cache/kb-index.json +1 -1
  28. package/templates/_agents/commands/build.md +1 -1
  29. package/templates/_agents/commands/design.md +1 -1
  30. package/templates/_agents/commands/maintain.md +1 -1
  31. package/templates/_agents/commands/new-task.md +73 -73
  32. package/templates/_agents/commands/plan.md +2 -2
  33. package/templates/_agents/commands/test.md +1 -1
  34. package/templates/_agents/hooks/check-pairing-incremental.sh +30 -29
  35. package/templates/_agents/hooks/commit-check.cjs +6 -1
  36. package/templates/_agents/scripts/agg-delegations.cjs +35 -18
  37. package/templates/_agents/scripts/agg-delegations.test.mjs +95 -0
  38. package/templates/_agents/scripts/check-loop.sh +6 -0
  39. package/templates/_agents/scripts/check-loop.test.sh +1042 -1042
  40. package/templates/_agents/scripts/commit-check-trigger.test.mjs +94 -0
  41. package/templates/_agents/scripts/ensure-board.ps1 +1 -1
  42. package/templates/_agents/scripts/gen-wiki-board.mjs +13 -3
  43. package/templates/_agents/scripts/gen-wiki-board.test.mjs +22 -0
  44. package/templates/_agents/scripts/gen-workflow-index.mjs +7 -2
  45. package/templates/_agents/scripts/gen-workflow-index.test.mjs +27 -0
  46. package/templates/_agents/scripts/gen-workflow-metrics.mjs +17 -3
  47. package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +21 -0
  48. package/templates/_agents/scripts/kb-search.mjs +2 -1
  49. package/templates/_agents/scripts/rule-budget.sh +8 -2
  50. package/templates/_agents/scripts/verify-wiki-consistency.mjs +6 -1
  51. package/templates/_agents/scripts/workflow-board-server.mjs +245 -244
  52. package/templates/_githooks/commit-msg +4 -2
  53. package/templates/_githooks/pre-push +32 -32
  54. package/templates/workflow/README.md +2 -2
  55. package/templates/workflow/intents/_TEMPLATE.md +40 -40
  56. package/templates/workflow/specs/_TEMPLATE.md +41 -41
@@ -0,0 +1,94 @@
1
+ // commit-check-trigger.test.mjs — builds/checks 触发判定测试(ext 全局 / prefix 前缀 / 双不命中 / 引擎自改豁免 / --full)。
2
+ // 方法:临时 git 仓库 + 被测 hook + 假构建命令(node -e 打标)——不跑真实构建,秒级完成。
3
+ // 命名避开装户项目的 commit-check.test.mjs(敏感判据测试,项目自持)——本文件测触发器,随包走。
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+ const HOOK = path.join(SCRIPT_DIR, '..', 'hooks', 'commit-check.cjs');
12
+ const MARK = 'TRIGGER_TEST_BUILD_RAN';
13
+
14
+ let pass = 0, fail = 0;
15
+ const check = (desc, cond, detail = '') => {
16
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
17
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n ${detail}` : ''}`); }
18
+ };
19
+ const git = (root, args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' });
20
+
21
+ // mkfix:临时 git 仓 + hook + 触发器假配置(构建命令 = 打标文件,跑没跑看标记存在性)
22
+ const mkfix = () => {
23
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-trigger-test-'));
24
+ git(root, ['init', '-q']);
25
+ git(root, ['config', 'user.email', 't@t']);
26
+ git(root, ['config', 'user.name', 't']);
27
+ fs.mkdirSync(path.join(root, '.agents', 'hooks'), { recursive: true });
28
+ fs.copyFileSync(HOOK, path.join(root, '.agents', 'hooks', 'commit-check.cjs'));
29
+ const marks = path.join(root, 'marks');
30
+ const cmd = (name) => `node -e "require('fs').writeFileSync('${marks.replaceAll('\\', '/').replaceAll('\'', '\\\'')}/${name}','')"`;
31
+ fs.mkdirSync(marks);
32
+ fs.writeFileSync(path.join(root, '.agents', 'hooks', 'commit-check.config.json'), JSON.stringify({
33
+ knownPatterns: [],
34
+ builds: [
35
+ { name: 'extbuild', command: cmd('extbuild'), ext: ['.xyz'] },
36
+ { name: 'prefixbuild', command: cmd('prefixbuild'), prefix: 'docs/' },
37
+ ],
38
+ checks: [],
39
+ }));
40
+ return { root, marks };
41
+ };
42
+
43
+ // stage:写文件并暂存,跑 hook(extraArgs 透传如 --full),返回 { status, out, ran }
44
+ const runHook = (root, extraArgs = []) => {
45
+ const r = spawnSync('node', ['.agents/hooks/commit-check.cjs', ...extraArgs], { cwd: root, encoding: 'utf8' });
46
+ return { status: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
47
+ };
48
+ const stage = (root, rel, content) => {
49
+ const abs = path.join(root, rel);
50
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
51
+ fs.writeFileSync(abs, content, 'utf8');
52
+ git(root, ['add', '--', rel]);
53
+ };
54
+ const ran = (marks, name) => fs.existsSync(path.join(marks, name));
55
+
56
+ let f = mkfix();
57
+
58
+ // 场景 1:ext 全局触发——任意目录下 .xyz 命中 extbuild
59
+ stage(f.root, 'src/a.xyz', 'x');
60
+ let r = runHook(f.root);
61
+ check('S1 ext 全局触发(.xyz → extbuild 跑)', ran(f.marks, 'extbuild') && r.status === 0, r.out);
62
+
63
+ // 场景 2:prefix 前缀触发——docs/ 下任意文件命中 prefixbuild;其他位置同名扩展不命中
64
+ f = mkfix();
65
+ stage(f.root, 'wiki/看板.html', '<html></html>');
66
+ r = runHook(f.root);
67
+ check('S2 prefix 外的 .html 不触发 prefixbuild(wiki html 场景)', !ran(f.marks, 'prefixbuild') && r.status === 0, r.out);
68
+ stage(f.root, 'docs/readme.md', 'doc');
69
+ r = runHook(f.root);
70
+ check('S2b docs/ 前缀任意文件触发 prefixbuild', ran(f.marks, 'prefixbuild') && r.status === 0, r.out);
71
+
72
+ // 场景 3:双不命中 → SKIP 提示且零构建
73
+ f = mkfix();
74
+ stage(f.root, 'notes/other.txt', 'plain');
75
+ r = runHook(f.root);
76
+ check('S3 双不命中 → SKIP 提示、无构建', r.out.includes('SKIP') && !ran(f.marks, 'extbuild') && !ran(f.marks, 'prefixbuild') && r.status === 0, r.out);
77
+
78
+ // 场景 4:引擎自改豁免——.agents/ 下即使放 .xyz 也不触发构建
79
+ f = mkfix();
80
+ stage(f.root, '.agents/scripts/engine.xyz', 'engine');
81
+ r = runHook(f.root);
82
+ check('S4 .agents/ 自身变更不触发构建', !ran(f.marks, 'extbuild') && r.status === 0, r.out);
83
+
84
+ // 场景 5:--full 全跑(合并提交口径)
85
+ f = mkfix();
86
+ stage(f.root, 'unrelated.txt', 'x');
87
+ r = runHook(f.root, ['--full']);
88
+ check('S5 --full 无条件全跑全部 builds', ran(f.marks, 'extbuild') && ran(f.marks, 'prefixbuild') && r.status === 0, r.out);
89
+
90
+ for (const d of fs.readdirSync(os.tmpdir()).filter((x) => x.startsWith('cc-trigger-test-'))) {
91
+ fs.rmSync(path.join(os.tmpdir(), d), { recursive: true, force: true });
92
+ }
93
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
94
+ process.exit(fail ? 1 : 0);
@@ -1,4 +1,4 @@
1
- # ensure-board.ps1 — 任务入口幂等拉起 workflow 看板(只起不关:Windows 重启即天然回收,无需 stop/status)
1
+ # ensure-board.ps1 — workflow 看板按需手动拉起(默认不自动开启;只起不关:Windows 重启即天然回收,无需 stop/status)
2
2
  # 行为:探活(端口占用者须响应 /api/board 才认定为看板,否则不动手、报占用者并 exit 1)
3
3
  # → 前端或服务端任一代码早于进程 = 旧代码自动 kill 重启 → 全新启动成功后弹浏览器(重启/已运行不弹,SSE 自动重连)
4
4
  # 用法:powershell -NoProfile -File .agents/scripts/ensure-board.ps1 [-Port 8933] [-NoOpen]
@@ -92,7 +92,8 @@ const archiveCount = (() => {
92
92
  else if (!gitIgnoredArchive.has(r)) n++;
93
93
  }
94
94
  };
95
- walk(path.join(WIKI, 'drafts-archive'));
95
+ if (fs.existsSync(path.join(WIKI, 'drafts-archive'))) walk(path.join(WIKI, 'drafts-archive'));
96
+ else console.error('⚠️ wiki/drafts-archive/ 不存在,归档计数按 0 处理(wiki 协议目录,建议恢复)');
96
97
  return n;
97
98
  })();
98
99
 
@@ -135,8 +136,13 @@ let out = indexText;
135
136
  const fixedRows = [...overviewRows, '| drafts-archive | — | 原文档备份(按 日期-主题 分目录) |'];
136
137
  out = [...lines.slice(0, headIdx), '| 主题 | 文件数 | 用途 |', '|---|---|---|', ...fixedRows, ...lines.slice(lastData + 1)].join(eol);
137
138
  }
138
- // 合计行数字
139
- out = out.replace(/合计:\*\*\d+ 份\*\*知识文档,\*\*\d+ 个主题\*\*/, `合计:**${totalFiles} 份**知识文档,**${diskTopics.size} 个主题**`);
139
+ // 合计行数字(锚点缺失即 fail-loud——静默 no-op 会让合计行悄悄陈旧,2026-09-24 审查修复)
140
+ const totalRe = /合计:\*\*\d+ 份\*\*知识文档,\*\*\d+ 个主题\*\*/;
141
+ if (!totalRe.test(out)) {
142
+ console.error('❌ INDEX「合计」行锚点未找到(须形如「合计:**N 份**知识文档,**M 个主题**」),未写盘');
143
+ process.exit(1);
144
+ }
145
+ out = out.replace(totalRe, `合计:**${totalFiles} 份**知识文档,**${diskTopics.size} 个主题**`);
140
146
  // 映射表区(节标题之间整段重写)
141
147
  {
142
148
  const lines = out.split(/\r?\n/);
@@ -144,6 +150,10 @@ out = out.replace(/合计:\*\*\d+ 份\*\*知识文档,\*\*\d+ 个主题\*\*/
144
150
  const e = lines.findIndex((l) => l.startsWith('## 命名规则'));
145
151
  if (s >= 0 && e > s) {
146
152
  out = [...lines.slice(0, s + 1), '', mappingSection, '', '---', '', ...lines.slice(e)].join(eol);
153
+ } else {
154
+ // 锚点缺失即 fail-loud(对齐速览表/合计行口径)——曾静默跳过致映射表悄悄陈旧仍报 ✅(2026-09-24 审查修复)
155
+ console.error('❌ INDEX 映射表锚点未找到(须有「## 文件 → 主题 → 归属目录 映射表」与其后的「## 命名规则」),未写盘');
156
+ process.exit(1);
147
157
  }
148
158
  }
149
159
 
@@ -128,6 +128,28 @@ const runGen = (root) => spawnSync(process.execPath, [GEN], { cwd: root, encodin
128
128
  fs.rmSync(root, { recursive: true, force: true });
129
129
  }
130
130
 
131
+ // ---- 场景 4:映射表锚点缺失 → fail-loud(2026-09-24 审查修复:原静默跳过致映射表悄悄陈旧仍报成功)----
132
+ {
133
+ const root = mkfix('\n');
134
+ const idx = path.join(root, 'wiki', 'INDEX.md');
135
+ fs.writeFileSync(idx, fs.readFileSync(idx, 'utf8').replace(/^## 文件 → 主题 → 归属目录 映射表$/m, '## 映射(锚点被改)'), 'utf8');
136
+ const r = runGen(root);
137
+ check('场景 4:映射表锚点缺失 → exit 1', r.status === 1, `exit=${r.status}\n${r.stdout}${r.stderr}`);
138
+ check('场景 4:报错点名映射表锚点', (r.stderr || '').includes('映射表锚点'), r.stderr);
139
+ check('场景 4:未写盘(INDEX 原样保留)', fs.readFileSync(idx, 'utf8').includes('## 映射(锚点被改)'), '');
140
+ fs.rmSync(root, { recursive: true, force: true });
141
+ }
142
+
143
+ // ---- 场景 5:drafts-archive 缺失 → 不崩栈,警告 + 归档按 0(2026-09-24 审查修复)----
144
+ {
145
+ const root = mkfix('\n');
146
+ fs.rmSync(path.join(root, 'wiki', 'drafts-archive'), { recursive: true });
147
+ const r = runGen(root);
148
+ check('场景 5:drafts-archive 缺失 → exit 0(不崩栈)', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
149
+ check('场景 5:stderr 有缺失警告', (r.stderr || '').includes('drafts-archive'), r.stderr);
150
+ fs.rmSync(root, { recursive: true, force: true });
151
+ }
152
+
131
153
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
132
154
  if (fail) {
133
155
  console.log(`\n${USAGE}`);
@@ -55,7 +55,8 @@ function parseDoc(text) {
55
55
  }
56
56
  const h1 = text.match(/^# (.+)$/m);
57
57
  let title = h1 ? h1[1] : '';
58
- const dash = title.match(/\S+\s*[—–-]+\s*(.+)$/);
58
+ // 前缀分隔只认 em/en dash(任意间距)或带空格的 ASCII 连字符——裸连字符是 kebab-case 的一部分,不截断(2026-09-24)
59
+ const dash = title.match(/\S+\s*(?:[—–]+|\s-\s)\s*(.+)$/);
59
60
  if (dash) title = dash[1];
60
61
  return { meta, title };
61
62
  }
@@ -142,8 +143,12 @@ const docs = scan();
142
143
  const existing = fs.existsSync(INDEX_P) ? fs.readFileSync(INDEX_P, 'utf8') : '';
143
144
  const inner = `${BEGIN}\n\n${render(docs)}\n\n${END}`;
144
145
  let out;
145
- if (existing.includes(BEGIN) && existing.includes(END)) {
146
+ if (existing.includes(BEGIN) && existing.includes(END) && existing.indexOf(BEGIN) < existing.indexOf(END)) {
146
147
  out = existing.slice(0, existing.indexOf(BEGIN)) + inner + existing.slice(existing.indexOf(END) + END.length);
148
+ } else if (existing.includes(BEGIN) || existing.includes(END)) {
149
+ // 锚点残缺(END 被手工截断 / 顺序颠倒):fail-loud 不写盘——曾走 append 分支产生双 GENERATED 区,旧区永不清理(2026-09-24)
150
+ console.error('❌ workflow/INDEX.md 生成锚点不完整(GENERATED:BEGIN/END 须成对且 BEGIN 在前)——手工修复锚点后重跑,未写盘');
151
+ process.exit(1);
147
152
  } else {
148
153
  out = (existing ? existing.replace(/\s*$/, '') + '\n\n' : HEADER) + inner + '\n';
149
154
  }
@@ -105,6 +105,33 @@ const run = (root, args = []) => spawnSync(process.execPath, [GEN, ...args], { c
105
105
  fs.rmSync(root, { recursive: true, force: true });
106
106
  }
107
107
 
108
+ // ---- 场景 5:kebab-case H1 不被连字符截断(2026-09-24 前缀正则收窄回归)----
109
+ {
110
+ const root = mkfix();
111
+ fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-09-22-kebab.md'), doc(
112
+ '状态: draft\n级别: L1\n日期: 2026-09-22\n模块: pipeline',
113
+ 'workflow-doc-retrieval',
114
+ ), 'utf8');
115
+ const r = run(root);
116
+ const idx = fs.readFileSync(path.join(root, 'workflow', 'INDEX.md'), 'utf8');
117
+ check('场景 5:kebab-case H1 完整保留(未被截成末段)', idx.includes('| workflow-doc-retrieval |') && !idx.includes('| retrieval |'), idx.split('\n').filter((l) => l.includes('retrieval')).join('\n'));
118
+ check('场景 5:em-dash 前缀仍正常剥离', idx.includes('| 在跑的需求甲 |'), '');
119
+ fs.rmSync(root, { recursive: true, force: true });
120
+ }
121
+
122
+ // ---- 场景 6:END 锚点被截断 → fail-loud 不写盘(2026-09-24:原 append 分支产生双生成区)----
123
+ {
124
+ const root = mkfix();
125
+ run(root);
126
+ const p = path.join(root, 'workflow', 'INDEX.md');
127
+ fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replace('<!-- GENERATED:END -->', ''), 'utf8');
128
+ const r = run(root);
129
+ check('场景 6:END 锚点缺失 → exit 1', r.status === 1, `exit=${r.status}\n${r.stdout}${r.stderr}`);
130
+ check('场景 6:报错点名锚点不完整', (r.stderr || '').includes('锚点不完整'), r.stderr);
131
+ check('场景 6:未写盘(无双重生成区)', (fs.readFileSync(p, 'utf8').match(/GENERATED:BEGIN/g) || []).length === 1, '');
132
+ fs.rmSync(root, { recursive: true, force: true });
133
+ }
134
+
108
135
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
109
136
  if (fail) {
110
137
  console.log(`\n${USAGE}`);
@@ -99,20 +99,34 @@ function scanSurface() {
99
99
  let label = pat;
100
100
  if (pat.endsWith('/')) {
101
101
  const dir = pat.slice(0, -1);
102
+ if (!fs.existsSync(dir)) { console.error(`⚠️ 预算目录不存在,跳过该条:${pat}`); continue; }
102
103
  for (const f of fs.readdirSync(dir)) sum += fs.statSync(path.join(dir, f)).size;
103
104
  max = sum;
104
105
  label = `${pat}(合计)`;
105
106
  } else if (pat.includes('*')) {
106
- const dir = path.dirname(pat);
107
- const suffix = pat.slice(pat.indexOf('*') + 1);
107
+ // 词表约定 glob = 基名内单个 `*`(<前缀>*<后缀>),目录段不支持通配——与 rule-budget.sh 的 shell glob 同口径。
108
+ // 多 `*` / 目录段含 `*` 出词表:警告跳过,不做「只按后缀 endsWith」的放宽式匹配(2026-09-24 收口:
109
+ // 曾按首个 * 取后缀,`pl*.md` 会误吃目录内全部 .md、`a*b` 会误吃一切 b 结尾文件)
110
+ const stars = pat.split('*').length - 1;
111
+ const starIdx = pat.indexOf('*');
112
+ const lastSlash = pat.lastIndexOf('/');
113
+ if (stars !== 1 || starIdx < lastSlash) {
114
+ console.error(`⚠️ 预算表 glob 仅支持基名内单个 *(<前缀>*<后缀>),跳过该条:${pat}`);
115
+ continue;
116
+ }
117
+ const dir = lastSlash >= 0 ? pat.slice(0, lastSlash + 1) : '.';
118
+ const basePrefix = pat.slice(lastSlash + 1, starIdx);
119
+ const baseSuffix = pat.slice(starIdx + 1);
120
+ if (!fs.existsSync(dir)) { console.error(`⚠️ 预算目录不存在,跳过该条:${pat}`); continue; }
108
121
  for (const f of fs.readdirSync(dir)) {
109
- if (!f.endsWith(suffix)) continue;
122
+ if (!f.startsWith(basePrefix) || !f.endsWith(baseSuffix)) continue;
110
123
  const s = fs.statSync(path.join(dir, f)).size;
111
124
  sum += s;
112
125
  if (s > max) max = s;
113
126
  }
114
127
  label = `${pat}(单篇最大)`;
115
128
  } else {
129
+ if (!fs.existsSync(pat)) { console.error(`⚠️ 预算文件不存在,跳过该条:${pat}`); continue; }
116
130
  sum = max = fs.statSync(pat).size;
117
131
  }
118
132
  entries.push({ pat, label, sum, max, lim, pct: (max / lim) * 100, isDir: pat.endsWith('/') });
@@ -93,6 +93,27 @@ const readMetrics = (root) => fs.readFileSync(path.join(root, 'workflow', 'metri
93
93
  fs.rmSync(root, { recursive: true, force: true });
94
94
  }
95
95
 
96
+ // ---- 场景 4:预算表 glob 词表收口——单 * 前缀+后缀匹配;多 * / 目录段 * 警告跳过(2026-09-24)----
97
+ {
98
+ const root = mkfix();
99
+ fs.writeFileSync(path.join(root, '.agents', 'commands', 'zeta.md'), 'z'.repeat(500), 'utf8'); // 非 pl 前缀:不得被 pl*.md 吃进
100
+ fs.writeFileSync(path.join(root, '.agents', 'rule-budgets.txt'), [
101
+ '# fixture 预算表(glob 词表收口)',
102
+ 'AGENTS.md 100',
103
+ '.agents/commands/pl*.md 9999',
104
+ '.agents/commands/**.md 9999',
105
+ '.agents/*/cmd.md 9999',
106
+ '',
107
+ ].join('\n'), 'utf8');
108
+ const r = run(root, ['--month', '2026-09']);
109
+ const md = fs.existsSync(path.join(root, 'workflow', 'metrics.md')) ? readMetrics(root) : '';
110
+ check('场景 4:exit 0(词表外条目跳过不阻断)', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
111
+ check('场景 4:单 * 按前缀+后缀匹配(pl*.md 只吃 plan.md 10B,zeta.md 500B 不计入)', /\.agents\/commands\/pl\*\.md(单篇最大)\s+10\s+\/\s+9999/.test(r.stdout), r.stdout);
112
+ check('场景 4:常驻面合计随之正确(120+10=130B → 0.1 KB)', /0\.1 KB(峰值/.test(md) && /常驻面合计:0\.1 KB/.test(r.stdout), `${md}\n${r.stdout}`);
113
+ check('场景 4:多 * 与目录段 * 各警告跳过', (r.stderr || '').includes('**.md') && (r.stderr || '').includes('.agents/*/cmd.md'), r.stderr);
114
+ fs.rmSync(root, { recursive: true, force: true });
115
+ }
116
+
96
117
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
97
118
  if (fail) {
98
119
  console.log(`\n${USAGE}`);
@@ -95,7 +95,8 @@ function parseDoc(text) {
95
95
  }
96
96
  const h1 = text.match(/^# (.+)$/m);
97
97
  let title = h1 ? h1[1] : '';
98
- const dash = title.match(/\S+\s*[—–-]+\s*(.+)$/);
98
+ // 前缀分隔只认 em/en dash(任意间距)或带空格的 ASCII 连字符——裸连字符是 kebab-case 的一部分,不截断(2026-09-24)
99
+ const dash = title.match(/\S+\s*(?:[—–]+|\s-\s)\s*(.+)$/);
99
100
  if (dash) title = dash[1];
100
101
  return { meta, title };
101
102
  }
@@ -34,9 +34,15 @@ while IFS= read -r line; do
34
34
  case "$pat" in ''|'#'*) continue ;; esac
35
35
  case "$lim" in ''|*[!0-9]*) continue ;; esac
36
36
  case "$pat" in
37
- */) # 目录合计:--staged 下仅当暂存触及该目录内文件才查
37
+ */) # 目录合计:--staged 下仅当暂存触及该目录内文件才查(case 字面前缀匹配——pat 含 . 时不能当正则用,2026-09-24)
38
38
  if [ "$MODE" = "--staged" ]; then
39
- printf '%s\n' "$staged_list" | grep -q "^${pat}" || continue
39
+ found=0
40
+ while IFS= read -r sf; do
41
+ case "$sf" in "$pat"*) found=1 ;; esac
42
+ done <<EOF
43
+ $staged_list
44
+ EOF
45
+ [ "$found" = "1" ] || continue
40
46
  fi
41
47
  total=0
42
48
  for f in "$pat"*; do
@@ -73,7 +73,11 @@ const gitIgnoredArchive = (() => {
73
73
  return new Set();
74
74
  }
75
75
  })();
76
- const archiveCount = walkTree(path.join(WIKI, 'drafts-archive')).files.filter((r) => !gitIgnoredArchive.has(r)).length;
76
+ // drafts-archive 缺失:ENOENT 崩栈改明示失败(本脚本是门禁,协议目录被删应 exit 1 而非崩栈,2026-09-24)
77
+ const ARCHIVE_DIR = path.join(WIKI, 'drafts-archive');
78
+ let archiveCount = 0;
79
+ if (fs.existsSync(ARCHIVE_DIR)) archiveCount = walkTree(ARCHIVE_DIR).files.filter((r) => !gitIgnoredArchive.has(r)).length;
80
+ else problems.push('wiki/drafts-archive/ 目录缺失(wiki 协议结构,模板自带)——归档一致性无从校验');
77
81
 
78
82
  // ---- 看板 DATA 解析 ----
79
83
  const html = fs.readFileSync(path.join(WIKI, '知识沉淀总览.html'), 'utf8');
@@ -175,6 +179,7 @@ for (const t of DATA.topics) {
175
179
  if (!fs.existsSync(p) || !fs.statSync(p).isDirectory()) deadLinks.push(`${t.name}/${f.file} → ${f.dir}`);
176
180
  }
177
181
  }
182
+ // 静态 href 扫描守的是「手工编辑看板」时加的链接(生成链接全在 JS 运行时拼,不落静态属性)——看板是 owned 内容,人为改动须可达
178
183
  for (const m2 of html.matchAll(/href="([^"]+)"/g)) {
179
184
  const href = m2[1];
180
185
  if (/^(https?:|mailto:|#)/i.test(href) || href.includes('${')) continue; // 外链 / 锚点 / 模板占位