agentic-flow-kit 0.3.1 → 0.5.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 (87) hide show
  1. package/README.md +2 -0
  2. package/modules/hosts/omp/agents/implementer.md +45 -6
  3. package/modules/hosts/omp/agents/independent-reviewer.md +42 -5
  4. package/modules/hosts/omp/agents/ui-verifier.md +41 -6
  5. package/modules/hosts/opencode/agents/implementer.md +45 -1
  6. package/modules/hosts/opencode/agents/independent-reviewer.md +42 -1
  7. package/modules/hosts/opencode/agents/ui-verifier.md +41 -1
  8. package/modules/hosts/opencode/commands/build.md +55 -7
  9. package/modules/hosts/opencode/commands/deploy.md +51 -6
  10. package/modules/hosts/opencode/commands/design.md +34 -6
  11. package/modules/hosts/opencode/commands/maintain.md +71 -6
  12. package/modules/hosts/opencode/commands/new-task.md +59 -6
  13. package/modules/hosts/opencode/commands/plan.md +32 -6
  14. package/modules/hosts/opencode/commands/review.md +40 -6
  15. package/modules/hosts/opencode/commands/test.md +56 -8
  16. package/modules/hosts/trae/agents/implementer.md +44 -2
  17. package/modules/hosts/trae/agents/independent-reviewer.md +42 -1
  18. package/modules/hosts/trae/agents/ui-verifier.md +39 -3
  19. package/modules/hosts/trae/commands/wf-build.md +55 -7
  20. package/modules/hosts/trae/commands/wf-deploy.md +51 -6
  21. package/modules/hosts/trae/commands/wf-design.md +34 -6
  22. package/modules/hosts/trae/commands/wf-maintain.md +71 -6
  23. package/modules/hosts/trae/commands/wf-new-task.md +59 -6
  24. package/modules/hosts/trae/commands/wf-plan.md +32 -6
  25. package/modules/hosts/trae/commands/wf-review.md +40 -6
  26. package/modules/hosts/trae/commands/wf-test.md +56 -7
  27. package/modules/hosts/zcode/agents/implementer.md +45 -6
  28. package/modules/hosts/zcode/agents/independent-reviewer.md +42 -5
  29. package/modules/hosts/zcode/agents/ui-verifier.md +41 -5
  30. package/package.json +6 -3
  31. package/src/cli.mjs +15 -2
  32. package/src/doctor.mjs +140 -13
  33. package/src/init.test.mjs +25 -2
  34. package/src/pack.test.mjs +24 -0
  35. package/src/run-tests.mjs +4 -11
  36. package/src/sync-hosts.mjs +193 -0
  37. package/src/sync-hosts.test.mjs +170 -0
  38. package/src/sync.mjs +1 -0
  39. package/src/sync.test.mjs +10 -0
  40. package/templates/AGENTS.md +2 -2
  41. package/templates/_agents/commands/build.md +20 -0
  42. package/templates/_agents/commands/design.md +1 -0
  43. package/templates/_agents/commands/gate-checklist.md +91 -0
  44. package/templates/_agents/commands/maintain.md +1 -1
  45. package/templates/_agents/commands/plan.md +2 -1
  46. package/templates/_agents/commands/review.md +1 -0
  47. package/templates/_agents/commands/source-sync-check.md +103 -0
  48. package/templates/_agents/commands/sync-hosts.md +79 -0
  49. package/templates/_agents/commands/test.md +2 -1
  50. package/templates/_agents/notes/runtime-env.md +1 -1
  51. package/templates/_agents/rule-budgets.txt +1 -1
  52. package/templates/_agents/scripts/check-loop.mjs +517 -0
  53. package/templates/_agents/scripts/check-loop.sh +5 -808
  54. package/templates/_agents/scripts/check-loop.test.mjs +481 -0
  55. package/templates/_agents/scripts/doctor.test.mjs +298 -0
  56. package/templates/_agents/scripts/ensure-board.mjs +158 -0
  57. package/templates/_agents/scripts/fill-intent.mjs +59 -0
  58. package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
  59. package/templates/_agents/scripts/fill-plan.mjs +60 -0
  60. package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
  61. package/templates/_agents/scripts/fill-spec.mjs +52 -0
  62. package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
  63. package/templates/_agents/scripts/gate-checklist.mjs +192 -0
  64. package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
  65. package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
  66. package/templates/_agents/scripts/kb-search.mjs +3 -1
  67. package/templates/_agents/scripts/source-sync-check.mjs +131 -0
  68. package/templates/_agents/scripts/source-sync-check.test.mjs +156 -0
  69. package/templates/_agents/scripts/verify.mjs +8 -10
  70. package/templates/_agents/scripts/verify.test.mjs +13 -1
  71. package/templates/_agents/scripts/wf-journal.mjs +158 -0
  72. package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
  73. package/templates/_agents/scripts/workflow-board-server.mjs +18 -10
  74. package/templates/_agents/scripts/workflow-enums.mjs +68 -0
  75. package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
  76. package/templates/_agents/scripts/workflows-check.mjs +260 -0
  77. package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
  78. package/templates/_agents/workflow-enums.txt +19 -0
  79. package/templates/_agents/workflows/_TEMPLATE.md +74 -0
  80. package/templates/_agents/workflows/pipeline-closing.md +29 -0
  81. package/templates/_agents/workflows/source-sync-repair.md +30 -0
  82. package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
  83. package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
  84. package/templates/workflow/README.md +2 -2
  85. package/templates/_agents/cache/kb-index.json +0 -1
  86. package/templates/_agents/scripts/check-loop.test.sh +0 -1042
  87. package/templates/_agents/scripts/ensure-board.ps1 +0 -48
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env node
2
+ // check-loop: AI-Native 闭环骨架断档扫描(agentic-flow-kit 通用版;2026-09-26 check-loop-node 自 POSIX sh 全量迁移 node)
3
+ // 扫描 workflow/ 目录,输出 intent/spec/plan 配对、模板字段占位符、incidents 复盘三件套、新 intent 回路等断档。
4
+ // 迁移背景:sh 版为规避 MSYS fork 开销引入 awk 预扫临时文件 + fd 3/4 与 glob 序逐行对齐的隐式契约
5
+ // (重排循环/加 continue 即静默错位)、四处临时文件无 trap 清理——node 为 kit 硬依赖,双语言实现面纯负担。
6
+ // 判定语义/消息文案/exit 语义与 sh 版逐条对齐(check-loop.test.mjs 37+ 场景为对齐安全网);
7
+ // 输出 banner 保留「check-loop.sh」字样为稳定输出契约(doctor 按 WARN 行计数、pre-push 按 exit 码判定)。
8
+ // 旧入口 check-loop.sh 为 4 行兼容 shim(exec node 本文件)——.githooks/pre-push 与文档引用零改动继续有效。
9
+ //
10
+ // 严重性分级(沿用):
11
+ // - hard-block(阻断 push / CI):配对断裂 + 回路断档(影响三件套可追溯性与闭环电路)
12
+ // - warning(打印提示,exit 0):其余卫生项(状态未确认 / 模板占位残留 / 引用漂移 / Adapter 一致性等)
13
+ // 违例输出到 stderr 并 exit 1;只剩警告时 exit 0。
14
+ //
15
+ // 检查项清单(编号/标题/severity 逐条沿 sh 版头部——gate-checklist 配对登记表按 id 消费,不得增删改号):
16
+ // 1. 入口文档/spec/plan 同名配对(L1 必须有 plan;L2/L3 必须有 spec+plan) [hard-block]
17
+ // 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换) [warning]
18
+ // 3. incidents 复盘三件套完整性 + 状态严格枚举 + 新 intent 回路(回路断档=hard,其他=warning)
19
+ // 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在) [warning]
20
+ // 5. intent/spec/plan 状态字段 + L3 独立复核 [warning]
21
+ // 6. 子智能体角色契约 + OpenCode/Trae/ZCode Adapter 一致性(含旧委派残留/钉死模型) [warning]
22
+ // 7. 阶段索引同步(AGENTS.md 与 new-task.md 须双向索引全部阶段指令) [warning]
23
+ // 8. intent 验收标准对账(2026-09-12 起新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
24
+ // 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
25
+ // 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
26
+ // 11. workflow/INDEX.md 漂移(活跃层索引与磁盘不一致=warning,2026-09-21 检索层;调生成器 --check,口径单一)
27
+ // 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
28
+ // 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
29
+ // 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
30
+ // 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved`(确认环节留痕,2026-09-22;恒 advisory 永不升级 hard)
31
+ //
32
+ // 注:清单条目 5(状态字段+L3 复核)与 1(配对)在同一遍 intents/specs/plans 循环里实现(沿 sh 版代码结构);
33
+ // 条目 6 的旧委派残留/钉死模型子项在「角色契约与 Adapter」代码段实现。
34
+ //
35
+ // 运行模式:CHECK_LOOP_ROOT 注入 fixture 根(全扫不过滤,非 git 时检查 10/14 跳过);
36
+ // 缺省 = git 仓库根(不在 git 仓库内 stderr 提示后 exit 0 跳过);仓库模式只扫已提交(HEAD)内容
37
+ // (并行会话未跟踪半成品不拦别人的 push;ls-tree 失败退化为全扫,门禁不失效)。
38
+ // 文档协议:机器字段一律由文件头 YAML frontmatter(受限子集:每行 `键: 值`)承载,本脚本只扫 frontmatter 取字段;
39
+ // 叙述性字段(独立复核/复盘三件套/验收勾验)按正文行锚定。枚举单源 .agents/workflow-enums.txt
40
+ // (缺文件/缺键 fail-loud exit 1;经 workflow-enums.mjs 读取,CRLF 天然容忍)。
41
+ // 已确认状态: approved/done=已批或闭环; superseded/cancelled=放弃留档(仍算确认,不挡 push); incident: fixed/closed
42
+ // 用法:node .agents/scripts/check-loop.mjs (或经 check-loop.sh shim)
43
+ // 测试:node templates/_agents/scripts/check-loop.test.mjs(fixture 注入 CHECK_LOOP_ROOT)
44
+ import fs from 'node:fs';
45
+ import path from 'node:path';
46
+ import { spawnSync } from 'node:child_process';
47
+ import { fileURLToPath } from 'node:url';
48
+ import { loadEnums } from './workflow-enums.mjs';
49
+
50
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
51
+
52
+ // ---- 根目录与枚举单源 ----
53
+ let ROOT;
54
+ if (process.env.CHECK_LOOP_ROOT) {
55
+ ROOT = path.resolve(process.env.CHECK_LOOP_ROOT);
56
+ if (!fs.existsSync(ROOT)) {
57
+ console.error('check-loop: CHECK_LOOP_ROOT 不可访问:' + process.env.CHECK_LOOP_ROOT);
58
+ process.exit(1);
59
+ }
60
+ } else {
61
+ const top = gitOut(['rev-parse', '--show-toplevel']);
62
+ if (top === null) {
63
+ console.error('check-loop: 不在 git 仓库内,跳过扫描');
64
+ process.exit(0);
65
+ }
66
+ ROOT = top.trim();
67
+ }
68
+ // 枚举单源(<root>/.agents/workflow-enums.txt——fixture 契约:缺文件/缺键 fail-loud)
69
+ let ENUMS;
70
+ try {
71
+ ENUMS = loadEnums(path.join(ROOT, '.agents', 'workflow-enums.txt'));
72
+ } catch (e) {
73
+ const msg = String(e.message || '');
74
+ if (msg.includes('不可读')) {
75
+ console.error(`check-loop: 缺 ${path.join('.agents', 'workflow-enums.txt')}(枚举单源)——跑 flow-kit sync 恢复后重试`);
76
+ } else {
77
+ console.error('check-loop: 枚举单源 ' + msg);
78
+ }
79
+ process.exit(1);
80
+ }
81
+ const inSet = (v, arr) => arr.includes(v);
82
+ const stOkDoc = (st) => inSet(st, ENUMS['doc.status.confirmed']);
83
+
84
+ // git 调用(参数数组形式不走 shell——Windows cmd 不认单引号;git.exe 显式解析沿 doctor 先例)
85
+ function gitOut(args, { ok = () => true } = {}) {
86
+ const r = spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', args,
87
+ { cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
88
+ if (r.error || r.status !== 0 || !ok(r)) return null;
89
+ return r.stdout || '';
90
+ }
91
+
92
+ // ---- frontmatter 受限子集读取(fm_get 语义:首行 --- 进入、下一 --- 闭合、键在行首、首个命中、值两侧去空白)----
93
+ const fileLinesCache = new Map();
94
+ function linesOf(file) {
95
+ if (!fileLinesCache.has(file)) {
96
+ try { fileLinesCache.set(file, fs.readFileSync(file, 'utf8').split(/\r?\n/)); }
97
+ catch { fileLinesCache.set(file, null); }
98
+ }
99
+ return fileLinesCache.get(file);
100
+ }
101
+ function fmGet(file, key) {
102
+ const lines = linesOf(file);
103
+ if (!lines) return '';
104
+ const isDelim = (l) => /^---\s*$/.test(l);
105
+ if (!isDelim(lines[0] || '')) return '';
106
+ for (let i = 1; i < lines.length; i++) {
107
+ if (isDelim(lines[i])) return '';
108
+ if (lines[i].startsWith(key + ':')) return lines[i].slice(key.length + 1).trim();
109
+ }
110
+ return '';
111
+ }
112
+
113
+ const WF = 'workflow';
114
+ const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
115
+ const blockers = [];
116
+ const warnings = [];
117
+
118
+ // ---- 已提交(HEAD)过滤:仓库模式只扫 tracked(fixture 模式恒真;ls-tree 失败退化全扫)----
119
+ let trackedSet = null; // null = 不过滤
120
+ if (!process.env.CHECK_LOOP_ROOT) {
121
+ const out = gitOut(['ls-tree', '-r', '--name-only', 'HEAD', '--', 'workflow']);
122
+ if (out !== null) trackedSet = new Set(out.split('\n').map((l) => l.trim()).filter(Boolean));
123
+ }
124
+ const isTracked = (abs) => trackedSet === null || trackedSet.has(path.relative(ROOT, abs).split(path.sep).join('/'));
125
+ const docFiles = (sub) => {
126
+ const dir = path.join(ROOT, WF, sub);
127
+ if (!fs.existsSync(dir)) return [];
128
+ return fs.readdirSync(dir)
129
+ .filter((f) => f.endsWith('.md') && /^\d/.test(f))
130
+ .map((f) => path.join(dir, f))
131
+ .filter((f) => isTracked(f))
132
+ .sort();
133
+ };
134
+
135
+ // --- 1. intent/spec/plan 同名配对 + 状态确认 + L3 确认三件 [hard-block] ---
136
+ for (const intent of docFiles('intents')) {
137
+ const base = path.basename(intent);
138
+ const st = fmGet(intent, '状态');
139
+ const lvl = fmGet(intent, '级别');
140
+ if (!stOkDoc(st)) {
141
+ const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
142
+ if (lvl === 'L3') blockers.push(`- [状态未确认] L3 intent 必须为 approved/done/superseded/cancelled:${msg}`);
143
+ else warnings.push(`- [WARN 状态未确认] intent 必须为 approved/done/superseded/cancelled:${msg}`);
144
+ }
145
+ if (lvl === 'L2' || lvl === 'L3') {
146
+ if (!fs.existsSync(path.join(ROOT, WF, 'specs', base))) {
147
+ blockers.push(`- [配对断裂] intent 缺 spec:${base}(应在 ${WF}/specs/ 下同名)`);
148
+ }
149
+ }
150
+ if (!fs.existsSync(path.join(ROOT, WF, 'plans', base))) {
151
+ blockers.push(`- [配对断裂] intent 缺 plan:${base}(应在 ${WF}/plans/ 下同名)`);
152
+ }
153
+ }
154
+
155
+ for (const spec of docFiles('specs')) {
156
+ const base = path.basename(spec);
157
+ const st = fmGet(spec, '状态');
158
+ const lvl = fmGet(spec, '级别');
159
+ const hasEntry = fs.existsSync(path.join(ROOT, WF, 'intents', base)) || fs.existsSync(path.join(ROOT, WF, 'incidents', base));
160
+ if (!hasEntry) blockers.push(`- [配对断裂] spec 缺 intent/incident:${base}(应在 ${WF}/intents/ 或 ${WF}/incidents/ 下同名)`);
161
+ if (!stOkDoc(st)) {
162
+ const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
163
+ if (lvl === 'L3') blockers.push(`- [状态未确认] L3 spec 必须为 approved/done/superseded/cancelled:${msg}`);
164
+ else warnings.push(`- [WARN 状态未确认] spec 必须为 approved/done/superseded/cancelled:${msg}`);
165
+ }
166
+ // L3 确认三件:确认结果/确认时间读 frontmatter;独立复核按正文行锚定(放弃件豁免 = 枚举单源 abandoned 集)
167
+ if (lvl === 'L3' && !inSet(st, ENUMS['doc.status.abandoned'])) {
168
+ if (fmGet(spec, '确认结果') !== 'approved') blockers.push(`- [L3 确认缺失] ${base} 确认结果必须为 approved`);
169
+ if (!fmGet(spec, '确认时间')) blockers.push(`- [L3 确认缺失] ${base} 必须记录确认时间`);
170
+ if (!(linesOf(spec) || []).some((l) => /^- 独立复核:[ \t]*[^<\s]/.test(l))) {
171
+ blockers.push(`- [L3 复核缺失] ${base} 必须记录新会话独立复核结论`);
172
+ }
173
+ }
174
+ }
175
+
176
+ for (const plan of docFiles('plans')) {
177
+ const base = path.basename(plan);
178
+ const st = fmGet(plan, '状态');
179
+ const lvl = fmGet(plan, '级别');
180
+ const hasEntry = fs.existsSync(path.join(ROOT, WF, 'intents', base)) || fs.existsSync(path.join(ROOT, WF, 'incidents', base));
181
+ if (!hasEntry) blockers.push(`- [配对断裂] plan 缺 intent/incident:${base}(应在 ${WF}/intents/ 或 ${WF}/incidents/ 下同名)`);
182
+ if (!stOkDoc(st)) {
183
+ const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
184
+ if (lvl === 'L3') blockers.push(`- [状态未确认] L3 plan 必须为 approved/done/superseded/cancelled:${msg}`);
185
+ else warnings.push(`- [WARN 状态未确认] plan 必须为 approved/done/superseded/cancelled:${msg}`);
186
+ }
187
+ }
188
+
189
+ // --- 2. 模板字段占位符残留 [warning](排除运行时文件名格式与协议样板,口径沿 sh 版)---
190
+ {
191
+ const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L0 \/ L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
192
+ const boilerRe = /\.\.\/specs\/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('YYYY-MM-DD'\)|value-format="YYYY-MM-DD"/;
193
+ const groups = new Map(); // file -> [ "行号:内容" ](首现序)
194
+ for (const sub of DOC_DIRS) {
195
+ for (const f of docFiles(sub)) {
196
+ const hits = [];
197
+ (linesOf(f) || []).forEach((line, i) => {
198
+ if (phRe.test(line) && !boilerRe.test(line)) hits.push(`${i + 1}:${line}`);
199
+ });
200
+ if (hits.length) groups.set(f, hits);
201
+ }
202
+ }
203
+ for (const [f, hits] of groups) {
204
+ warnings.push(`- [WARN 模板未填] ${path.relative(ROOT, f).split(path.sep).join('/')} 含模板占位符:\n${hits.join('\n')}`);
205
+ }
206
+ }
207
+
208
+ // --- 3. incidents 复盘三件套 + 状态严格枚举 + 新 intent 回路(回路断档 = hard)---
209
+ for (const inc of docFiles('incidents')) {
210
+ const name = path.basename(inc);
211
+ const lines = linesOf(inc) || [];
212
+ const incLvl = fmGet(inc, '级别');
213
+ const incSt = fmGet(inc, '状态');
214
+
215
+ if (!inSet(incSt, ENUMS['incident.status.all'])) {
216
+ warnings.push(`- [WARN 状态非法] incident 状态必须为 open/fixed/closed:${name}(frontmatter 状态键当前值:『${incSt || '缺失'}』)`);
217
+ }
218
+
219
+ if (fmGet(inc, '流程') !== 'legacy') {
220
+ if (!inSet(incLvl, ENUMS['level.all'])) {
221
+ warnings.push(`- [WARN 级别缺失] ${name} 必须填写 L0/L1/L2/L3(配对检查已跳过——补级别后重跑本脚本校验配对)`);
222
+ } else {
223
+ if (['L1', 'L2', 'L3'].includes(incLvl) && !fs.existsSync(path.join(ROOT, WF, 'plans', name))) {
224
+ blockers.push(`- [配对断裂] incident 缺 plan:${name}(应在 ${WF}/plans/ 下同名)`);
225
+ }
226
+ if (['L2', 'L3'].includes(incLvl) && !fs.existsSync(path.join(ROOT, WF, 'specs', name))) {
227
+ blockers.push(`- [配对断裂] incident 缺 spec:${name}(应在 ${WF}/specs/ 下同名)`);
228
+ }
229
+ }
230
+ }
231
+
232
+ // 三件套:正文行锚定 /^1\. /^2\. /^3\. (sh awk 同款)
233
+ for (const n of [1, 2, 3]) {
234
+ if (!lines.some((l) => l.startsWith(`${n}. `))) {
235
+ warnings.push(`- [WARN 三件套不全] ${name} 缺复盘三件套之 ${n}`);
236
+ }
237
+ }
238
+ const hasParent = lines.some((l) => l.includes('是否需要新 intent'));
239
+ if (!hasParent) {
240
+ warnings.push(`- [WARN 三件套不全] ${name} 缺「是否需要新 intent」子项`);
241
+ continue;
242
+ }
243
+ const yesLine = lines.find((l) => l.includes('是 → '));
244
+ if (yesLine) {
245
+ const m = yesLine.match(/intents\/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[^ )]*\.md/);
246
+ const ref = m ? m[0] : null;
247
+ if (!ref) blockers.push(`- [回路断档] ${name} 选「是」但未指明 intent 文件路径`);
248
+ else if (!fs.existsSync(path.join(ROOT, WF, ref))) blockers.push(`- [回路断档] ${name} 引用的 intent 不存在:${ref}`);
249
+ }
250
+ }
251
+
252
+ // --- 4. 引用有效性 [warning](.agents/ 路径须存在;skills 特例允许用户级 ~/.agents/skills)---
253
+ {
254
+ const refRe = /\.agents\/(commands|hooks|scripts|skills|roles)\/[A-Za-z0-9_][A-Za-z0-9_./-]*/g;
255
+ const files = [];
256
+ if (fs.existsSync(path.join(ROOT, 'AGENTS.md'))) files.push('AGENTS.md');
257
+ for (const e of readdirOrNull(ROOT) || []) {
258
+ const p = path.join(ROOT, e, 'AGENTS.md');
259
+ if (isDirectoryOrNull(path.join(ROOT, e)) && fs.existsSync(p)) files.push(path.join(e, 'AGENTS.md'));
260
+ }
261
+ for (const f of readdirOrNull(path.join(ROOT, WF)) || []) {
262
+ if (f.endsWith('.md')) files.push(`${WF}/${f}`); // workflow 根级 *.md(README 等)
263
+ }
264
+ for (const sub of DOC_DIRS) {
265
+ const dir = path.join(ROOT, WF, sub);
266
+ for (const f of readdirOrNull(dir) || []) {
267
+ if (f.endsWith('.md') && (f === '_TEMPLATE.md' || /^\d/.test(f))) files.push(`${WF}/${sub}/${f}`);
268
+ }
269
+ }
270
+ const cmdDir = path.join(ROOT, '.agents', 'commands');
271
+ for (const f of readdirOrNull(cmdDir) || []) {
272
+ if (f.endsWith('.md')) files.push(`.agents/commands/${f}`);
273
+ }
274
+ for (const rel of files) {
275
+ const text = linesOf(path.join(ROOT, rel));
276
+ if (!text) continue;
277
+ const seen = new Set();
278
+ for (const line of text) {
279
+ for (const m of line.matchAll(refRe)) {
280
+ const ref = m[0];
281
+ if (seen.has(ref)) continue;
282
+ seen.add(ref);
283
+ if (fs.existsSync(path.join(ROOT, ref))) continue;
284
+ if (ref.startsWith('.agents/skills/') && fs.existsSync(path.join(process.env.HOME || process.env.USERPROFILE || '', ref))) continue;
285
+ warnings.push(`- [WARN 引用断档] ${rel.split(path.sep).join('/')} 引用不存在的文件:${ref}`);
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ // --- 5. 子智能体角色契约 + 宿主 Adapter 一致性 + 旧委派残留 + 钉死模型 [warning] ---
292
+ {
293
+ const cmdFiles = () => (readdirOrNull(path.join(ROOT, '.agents', 'commands')) || [])
294
+ .filter((f) => f.endsWith('.md')).map((f) => `.agents/commands/${f}`);
295
+ const textOf = (rel) => linesOf(path.join(ROOT, rel)) || [];
296
+ for (const role of ['implementer', 'independent-reviewer', 'ui-verifier']) {
297
+ const rolePath = `.agents/roles/${role}.md`;
298
+ if (!fs.existsSync(path.join(ROOT, rolePath))) {
299
+ warnings.push(`- [WARN 角色缺失] 公共角色契约不存在:${rolePath}`);
300
+ continue;
301
+ }
302
+ if (fs.existsSync(path.join(ROOT, '.opencode', 'agents'))) {
303
+ const p = `.opencode/agents/${role}.md`;
304
+ if (!fs.existsSync(path.join(ROOT, p))) warnings.push(`- [WARN Adapter 缺失] OpenCode 缺 ${role}:${p}`);
305
+ else {
306
+ if (!/^mode:\s*subagent\s*$/m.test(textOf(p).join('\n'))) warnings.push(`- [WARN Adapter 无效] OpenCode ${role} 未声明 mode:subagent:${p}`);
307
+ if (!textOf(p).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] OpenCode ${role} 未引用公共角色:${rolePath}`);
308
+ }
309
+ }
310
+ if (fs.existsSync(path.join(ROOT, '.trae', 'agents'))) {
311
+ const p = `.trae/agents/${role}.md`;
312
+ if (!fs.existsSync(path.join(ROOT, p))) warnings.push(`- [WARN Adapter 缺失] Trae 缺 ${role}:${p}`);
313
+ else {
314
+ if (!new RegExp(`^name:\\s*${role}\\s*$`, 'm').test(textOf(p).join('\n'))) warnings.push(`- [WARN Adapter 无效] Trae ${role} 的 name 与文件名不一致:${p}`);
315
+ if (!textOf(p).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] Trae ${role} 未引用公共角色:${rolePath}`);
316
+ }
317
+ }
318
+ const zc = `.zcode/agents/${role}.md`;
319
+ if (fs.existsSync(path.join(ROOT, zc))) { // 本地配置不入 git:缺失不告警,存在时校验一致性
320
+ if (!new RegExp(`^name:\\s*["']?${role}["']?\\s*$`, 'm').test(textOf(zc).join('\n'))) warnings.push(`- [WARN Adapter 无效] ZCode ${role} 的 name 与文件名不一致:${zc}`);
321
+ if (!textOf(zc).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] ZCode ${role} 未引用公共角色:${rolePath}`);
322
+ }
323
+ const cited = ['AGENTS.md', ...cmdFiles()].some((f) => textOf(f).some((l) => l.includes(rolePath)));
324
+ if (!cited) warnings.push(`- [WARN 命令断线] 工作流未引用公共角色:${rolePath}`);
325
+ }
326
+ // 旧委派残留 / 钉死模型(grep -nH 行格式 file:line:content)
327
+ const legacyRe = /(^subagent:|subagent_type=|general_purpose_task|browser_use)/;
328
+ const legacyHits = [];
329
+ for (const f of ['AGENTS.md', ...cmdFiles()]) {
330
+ textOf(f).forEach((l, i) => { if (legacyRe.test(l)) legacyHits.push(`${f}:${i + 1}:${l}`); });
331
+ }
332
+ if (legacyHits.length) warnings.push(`- [WARN 旧委派残留] 主规则或阶段命令仍含宿主特定/旧委派声明:\n${legacyHits.join('\n')}`);
333
+ const modelHits = [];
334
+ for (const h of ['.opencode', '.trae', '.zcode']) {
335
+ for (const f of readdirOrNull(path.join(ROOT, h, 'agents')) || []) {
336
+ if (!f.endsWith('.md')) continue;
337
+ const rel = `${h}/agents/${f}`;
338
+ textOf(rel).forEach((l, i) => { if (/^model:/.test(l) && !/^model:\s*inherit/.test(l)) modelHits.push(`${rel}:${i + 1}:${l}`); });
339
+ }
340
+ }
341
+ if (modelHits.length) warnings.push(`- [WARN 宿主耦合] 薄 Adapter 不应固定模型:\n${modelHits.join('\n')}`);
342
+ }
343
+
344
+ // --- 6. 阶段索引同步 [warning] ---
345
+ for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review']) {
346
+ for (const doc of ['AGENTS.md', '.agents/commands/new-task.md']) {
347
+ const text = linesOf(path.join(ROOT, doc));
348
+ if (text && !text.some((l) => l.includes(`.agents/commands/${cmd}.md`))) {
349
+ warnings.push(`- [WARN 阶段索引漂移] ${doc} 缺 ${cmd} 指令索引(两处阶段表须同步维护)`);
350
+ }
351
+ }
352
+ }
353
+
354
+ // --- 8. intent 验收标准对账(done 须逐条勾验并补证据;2026-09-12 起新建 hard,存量聚合 warning)---
355
+ {
356
+ const accCutoff = '2026-09-12';
357
+ let legacyUnaccounted = 0;
358
+ for (const intent of docFiles('intents')) {
359
+ if (fmGet(intent, '状态') !== 'done') continue;
360
+ const base = path.basename(intent);
361
+ const lines = linesOf(intent) || [];
362
+ // 节扫描:hs=有验收节;insec 至下一个二级标题;uc=未勾项;ne=勾选缺证据;ex=存量豁免声明
363
+ let hs = false, uc = false, ne = false;
364
+ let insec = false, ex = false;
365
+ for (const line of lines) {
366
+ if (/存量对账豁免(/.test(line)) ex = true;
367
+ if (/^\s*##\s+[^#]*验收标准/.test(line)) { hs = true; insec = true; continue; }
368
+ if (insec && /^\s*##\s/.test(line)) insec = false;
369
+ if (insec && /^\s*- \[ \]/.test(line)) uc = true;
370
+ if (insec && /^\s*- \[x\]/.test(line) && !/证据:/.test(line)) ne = true;
371
+ }
372
+ const filedate = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
373
+ const isNew = filedate !== '' && filedate >= accCutoff;
374
+ if (!hs) {
375
+ if (isNew) blockers.push(`- [验收未对账] done intent 缺「## 验收标准」节(勾验无从核对):${base}`);
376
+ else if (!ex) legacyUnaccounted++;
377
+ continue;
378
+ }
379
+ if (!uc && !ne) continue;
380
+ if (isNew) {
381
+ if (uc) blockers.push(`- [验收未对账] done intent 验收标准有未勾验项:${base}(逐条勾验并补证据:commit/用例/冒烟输出)`);
382
+ if (ne) warnings.push(`- [WARN 验收缺证据] ${base} 验收标准勾选项缺「证据:」标注`);
383
+ } else if (!ex) {
384
+ legacyUnaccounted++;
385
+ }
386
+ }
387
+ if (legacyUnaccounted > 0) {
388
+ warnings.push(`- [WARN 验收对账存量] ${legacyUnaccounted} 个存量 done intent 验收标准未对账(2026-09-12 前创建,豁免 hard,不回填)`);
389
+ }
390
+ }
391
+
392
+ // --- 9. 文件名英文 kebab-case [warning] ---
393
+ for (const sub of DOC_DIRS) {
394
+ const dir = path.join(ROOT, WF, sub);
395
+ for (const f of readdirOrNull(dir) || []) {
396
+ if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
397
+ const abs = path.join(dir, f);
398
+ if (!isTracked(abs)) continue;
399
+ if (!/^[\x20-\x7e]*$/.test(f)) {
400
+ warnings.push(`- [WARN 文件名非英文] 文件名含非 ASCII 字符,应为英文 kebab-case:${WF}/${sub}/${f}`);
401
+ }
402
+ }
403
+ }
404
+
405
+ // --- 10. 级别 vs 迁移文件一致性 [warning](启发式:入口文档加入提交触及迁移 SQL/Migrations → 疑似判低;非 git 跳过)---
406
+ if (gitOut(['rev-parse', '--git-dir']) !== null) {
407
+ const log = gitOut(['log', '--diff-filter=A', '--format=@%H', '--name-only']);
408
+ if (log !== null) {
409
+ const addpath = new Map(); // 文件 → 首次加入 commit(新→旧序首遇)
410
+ const cfiles = new Map(); // commit → 文件清单
411
+ let cur = null;
412
+ for (const line of log.split('\n')) {
413
+ if (line.startsWith('@')) { cur = line.slice(1); cfiles.set(cur, []); continue; }
414
+ if (line.trim() && cur && !addpath.has(line.trim())) {
415
+ addpath.set(line.trim(), cur);
416
+ cfiles.get(cur).push(line.trim());
417
+ }
418
+ }
419
+ const docs = [...docFiles('intents'), ...docFiles('incidents')];
420
+ for (const doc of docs) {
421
+ const rel = path.relative(ROOT, doc).split(path.sep).join('/');
422
+ const lvl = fmGet(doc, '级别');
423
+ if (lvl !== 'L1' && lvl !== 'L2') continue;
424
+ const c = addpath.get(rel);
425
+ if (!c) continue;
426
+ const hits = (cfiles.get(c) || []).filter((f) => /(^|\/)docs\/scripts\/.*\.sql$/.test(f) || /(^|\/)Migrations\//.test(f));
427
+ if (hits.length) {
428
+ warnings.push(`- [WARN 级别疑似判低] ${path.basename(doc)} 级别 ${lvl} 但其加入提交触及迁移文件(混合改动应就高不就低,请复核级别):\n${hits.join('\n')}`);
429
+ }
430
+ }
431
+ }
432
+ }
433
+
434
+ // --- 11. workflow/INDEX.md 漂移 [warning](调生成器 --check,口径单一不复刻渲染)---
435
+ {
436
+ const gen = path.join(ROOT, '.agents', 'scripts', 'gen-workflow-index.mjs');
437
+ if (fs.existsSync(gen)) {
438
+ const r = spawnSync(process.execPath, [gen, '--check'], { cwd: ROOT, encoding: 'utf8' });
439
+ if (r.status !== 0) {
440
+ const first = `${r.stdout || ''}${r.stderr || ''}`.split('\n')[0];
441
+ warnings.push(`- [WARN 索引漂移] workflow/INDEX.md 与磁盘不一致——跑 node .agents/scripts/gen-workflow-index.mjs 重新生成(${first})`);
442
+ }
443
+ }
444
+ }
445
+
446
+ // --- 12. frontmatter「模块:」合法性 [warning](词表单源;缺字段只对 2026-09-22 起新建提示)---
447
+ {
448
+ const modsFile = path.join(ROOT, '.agents', 'workflow-modules.txt');
449
+ if (fs.existsSync(modsFile)) {
450
+ const mods = linesOf(modsFile).filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.trim());
451
+ const vocab = new Set(mods);
452
+ for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
453
+ for (const doc of docFiles(sub)) {
454
+ const base = path.basename(doc);
455
+ const mod = fmGet(doc, '模块');
456
+ let d = fmGet(doc, '日期') || fmGet(doc, '发现');
457
+ if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
458
+ if (mod) {
459
+ if (!vocab.has(mod)) warnings.push(`- [WARN 模块元数据] ${base}「模块: ${mod}」不在词表(见 .agents/workflow-modules.txt)`);
460
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= '2026-09-22') {
461
+ warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(2026-09-22 起新建文档必填)`);
462
+ }
463
+ }
464
+ }
465
+ }
466
+ }
467
+
468
+ // --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.sh;无 sh 环境静默跳过——advisory,pre-commit 硬拦兜底)---
469
+ {
470
+ const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.sh');
471
+ const budgets = path.join(ROOT, '.agents', 'rule-budgets.txt');
472
+ if (fs.existsSync(rb) && fs.existsSync(budgets)) {
473
+ const r = spawnSync('sh', [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
474
+ if (!r.error && r.status !== 0) {
475
+ const head = `${r.stdout || ''}${r.stderr || ''}`.split('\n').slice(0, 5).map((l) => ' ' + l).join('\n');
476
+ warnings.push(`- [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):\n${head}`);
477
+ }
478
+ }
479
+ }
480
+
481
+ // --- 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved` [warning](恒 advisory;非 git 跳过)---
482
+ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '--verify', 'HEAD']) !== null) {
483
+ for (const sub of ['specs', 'plans']) {
484
+ for (const doc of docFiles(sub)) {
485
+ if (fmGet(doc, '状态') !== 'done') continue;
486
+ const base = path.basename(doc);
487
+ let d = fmGet(doc, '日期') || fmGet(doc, '发现');
488
+ if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
489
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || d < '2026-09-23') continue; // 生效 2026-09-23 起(规则发布次日)
490
+ const lines = linesOf(doc) || [];
491
+ if (lines.some((l) => l.includes('存量确认态豁免('))) continue;
492
+ const rel = path.relative(ROOT, doc).split(path.sep).join('/');
493
+ const hit = gitOut(['log', '-1', '--format=%H', '-G', '^状态:[[:space:]]*approved', '--', rel]);
494
+ if (hit !== null && hit.trim() === '') {
495
+ warnings.push(`- [WARN 确认态缺失] ${base} 状态已 done 但 git 历史中从未出现行首「状态: approved」——确认环节未留痕(draft 直跳 done)`);
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ // --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
502
+ if (blockers.length) {
503
+ process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
504
+ if (warnings.length) process.stderr.write(`WARN(advisory,不阻断):\n\n${warnings.join('\n')}\n`);
505
+ process.exit(1);
506
+ }
507
+ if (warnings.length) {
508
+ process.stderr.write(`check-loop.sh WARN(advisory,不阻断):\n\n${warnings.join('\n')}\n`);
509
+ }
510
+ process.exit(0);
511
+
512
+ function readdirOrNull(dir) {
513
+ try { return fs.readdirSync(dir); } catch { return null; }
514
+ }
515
+ function isDirectoryOrNull(p) {
516
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
517
+ }