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
@@ -1,809 +1,6 @@
1
1
  #!/bin/sh
2
- # check-loop: AI-Native 闭环骨架断档扫描(agentic-flow-kit 通用版)
3
- # 扫描 workflow/ 目录,输出 intent/spec/plan 配对、模板字段占位符、incidents 复盘三件套、新 intent 回路等断档
4
- # 本脚本全部为通用逻辑:宿主 Adapter 检查按「目录存在才校验」(flow-kit init --hosts 决定装哪些宿主)
5
- #
6
- # 严重性分级(2026-09-09 按用户决策调整):
7
- # - hard-block(阻断 push / CI):配对断裂 + 回路断档(影响三件套可追溯性与闭环电路)
8
- # - warning(打印提示,exit 0):其余卫生项(状态未确认 / 模板占位残留 / 引用漂移 / Adapter 一致性等)
9
- # 违例输出到 stderr 并 exit 1;只剩警告时 exit 0
10
- #
11
- # 检查项:
12
- # 1. 入口文档/spec/plan 同名配对(L1 必须有 plan;L2/L3 必须有 spec+plan) [hard-block]
13
- # 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换) [warning]
14
- # 3. incidents 复盘三件套完整性 + 状态严格枚举 + 新 intent 回路(回路断档=hard,其他=warning)
15
- # 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在) [warning]
16
- # 5. intent/spec/plan 状态字段 + L3 独立复核 [warning]
17
- # 6. 子智能体角色契约 + OpenCode/Trae/ZCode Adapter 一致性 [warning]
18
- # 7. 阶段索引同步(AGENTS.md 与 new-task.md 须双向索引全部阶段指令) [warning]
19
- # 8. intent 验收标准对账(2026-09-12 起新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
20
- # 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
21
- # 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
22
- # 11. workflow/INDEX.md 漂移(活跃层索引与磁盘不一致=warning,2026-09-21 检索层;调生成器 --check,口径单一)
23
- # 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
24
- # 13. 常驻面体积预算(AGENTS.md / backend / frontend / rules / commands 超限=warning,2026-09-21 规则面精简;
25
- # 上限=瘦身后实测+余量,新增须先删除或下沉——一进一出)
26
- # 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved`(确认环节留痕,2026-09-22 papercut;
27
- # 生效 2026-09-23=规则发布次日,不追溯发布当日已在途文档,口径同检查 12)[warning]
28
- # 2026-09-23 裁定演进(源自 Shipyard 回流):本检查恒为 advisory,永不升级 hard-block;确认后主智能体须立即
29
- # docs(*) 提交 approved 留痕(根 AGENTS.md 提交约定),本警告即「遗漏提交」提醒——直跳 done 无留痕仍属流程瑕疵,仅提示
30
- #
31
- # 文档协议(2026-09-13 结构化改造,papercut #4 防复发):
32
- # 状态/级别/日期/发现/流程/确认结果/确认时间 等机器字段一律由文件头 YAML frontmatter(受限子集)承载:
33
- # --- 换行后每行 `键: 值`,值严格枚举(状态/级别)或日期;附注自由文本放 `备注:` 键,本脚本不解析备注内容
34
- # 本脚本只扫 frontmatter 块取字段(不再全文正则猜中文格式),正文叙述性字段(独立复核/复核结论)仍按节锚定扫描。
35
- # 测试:sh .agents/scripts/check-loop.test.sh(fixture 注入 CHECK_LOOP_ROOT)
36
- #
37
- # 已确认状态: approved/done=已批或闭环; superseded/cancelled=放弃留档(仍算确认,不挡 push); incident: fixed/closed
38
-
39
- # 根目录:默认 git 仓库根;测试经 CHECK_LOOP_ROOT 注入 fixture 根
40
- if [ -n "${CHECK_LOOP_ROOT:-}" ]; then
41
- cd "$CHECK_LOOP_ROOT" 2>/dev/null || {
42
- echo "check-loop: CHECK_LOOP_ROOT 不可访问:$CHECK_LOOP_ROOT" >&2
43
- exit 1
44
- }
45
- else
46
- cd "$(git rev-parse --show-toplevel 2>/dev/null)" || {
47
- echo "check-loop: 不在 git 仓库内,跳过扫描" >&2
48
- exit 0
49
- }
50
- fi
51
-
52
- # fm_get <file> <key>: 读 frontmatter 受限子集键值(首行 --- 起至闭合 --- 止,行首 `键: 值`)
53
- # 无 frontmatter / 键不存在时输出空串
54
- # fm_is_delim <line>: 该行是否为 frontmatter 分隔符(--- 后仅可跟空白)
55
- fm_is_delim() {
56
- __d="$1"
57
- while :; do
58
- case "$__d" in
59
- *[[:space:]]) __d="${__d%?}" ;;
60
- *) break ;;
61
- esac
62
- done
63
- [ "$__d" = "---" ]
64
- }
65
-
66
- # 2026-09-20 性能:原实现每取一个键就 fork 一次 awk。本脚本每次运行要对 400+ 份文档
67
- # 取 2~4 个键,在 MSYS/Windows 下单次 fork 约 45ms,累计上千次 → 整脚本曾需 6 分钟以上,
68
- # 被误判为「卡死」。改为纯 sh 内建(while read + case + 参数展开),语义与原 awk 逐条对齐:
69
- # · 第 1 行须是 --- (后可跟空白) 才进入 frontmatter,否则直接返回空
70
- # · 遇下一个 --- 即闭合退出
71
- # · 键须出现在行首(原 index($0, k ":") == 1),取首个命中
72
- # · 值两侧去空白(原去除键前缀 + 去尾空白)
73
- # 注:未用 local —— 本文件须保持 POSIX sh 可执行(pre-push 注「其它环境 sh 兼容」)
74
- fm_get() {
75
- __key="$2"
76
- __state=before
77
- while IFS= read -r __line || [ -n "$__line" ]; do
78
- if [ "$__state" = "before" ]; then
79
- fm_is_delim "$__line" || return 0
80
- __state=in
81
- continue
82
- fi
83
- fm_is_delim "$__line" && return 0
84
- case "$__line" in
85
- "$__key":*)
86
- __v="${__line#"$__key":}"
87
- while :; do
88
- case "$__v" in
89
- [[:space:]]*) __v="${__v#?}" ;;
90
- *) break ;;
91
- esac
92
- done
93
- while :; do
94
- case "$__v" in
95
- *[[:space:]]) __v="${__v%?}" ;;
96
- *) break ;;
97
- esac
98
- done
99
- printf '%s\n' "$__v"
100
- return 0
101
- ;;
102
- esac
103
- done < "$1" 2>/dev/null
104
- }
105
-
106
- # 状态值域:intent/spec/plan 与 incident 各一套
107
- st_ok_doc() { case "$1" in approved|done|superseded|cancelled) return 0 ;; *) return 1 ;; esac; }
108
-
109
- WF="workflow"
110
- blockers=""
111
- warnings=""
112
-
113
- # 仓库模式只扫已提交(HEAD)内容(2026-09-18 papercut 升级,用户点名「只扫已跟踪文件」):
114
- # 共享工作区里并行会话的未跟踪/未提交半成品(如 draft intent 尚未配 plan)会拦住别人的 push——
115
- # 推送门只为已提交内容负责。fixture 模式(CHECK_LOOP_ROOT 注入)不过滤,保持全扫;ls-tree 失败(空仓库等)
116
- # 退化为全扫,门禁不失效。
117
- TRACKED_WF=""
118
- TRACKED_OK=0
119
- if [ -z "${CHECK_LOOP_ROOT:-}" ]; then
120
- if TRACKED_WF=$(git ls-tree -r --name-only HEAD -- workflow 2>/dev/null); then
121
- TRACKED_OK=1
122
- fi
123
- fi
124
- # is_tracked <相对根的文件路径>: fixture 模式/HEAD 清单不可用时恒真;否则按 HEAD 清单整行精确匹配
125
- is_tracked() {
126
- [ "$TRACKED_OK" = "1" ] || return 0
127
- case "
128
- $TRACKED_WF
129
- " in *"
130
- $1
131
- "*) return 0 ;; *) return 1 ;; esac
132
- }
133
-
134
- # --- 1. intent/spec/plan 三文件同名配对(除 _TEMPLATE) [hard-block] ---
135
- for intent in "$WF"/intents/[0-9]*.md; do
136
- [ -f "$intent" ] || continue
137
- is_tracked "$intent" || continue
138
- base=${intent##*/}
139
- st=$(fm_get "$intent" 状态)
140
- lvl=$(fm_get "$intent" 级别)
141
- if ! st_ok_doc "$st"; then
142
- if [ "$lvl" = "L3" ]; then
143
- blockers="$blockers
144
- - [状态未确认] L3 intent 必须为 approved/done/superseded/cancelled:$base(frontmatter 状态键当前值:『${st:-缺失}』)"
145
- else
146
- warnings="$warnings
147
- - [WARN 状态未确认] intent 必须为 approved/done/superseded/cancelled:$base(frontmatter 状态键当前值:『${st:-缺失}』)"
148
- fi
149
- fi
150
- case "$lvl" in
151
- L2|L3)
152
- [ -f "$WF/specs/$base" ] || blockers="$blockers
153
- - [配对断裂] intent 缺 spec:$base(应在 $WF/specs/ 下同名)"
154
- ;;
155
- esac
156
- [ -f "$WF/plans/$base" ] || blockers="$blockers
157
- - [配对断裂] intent 缺 plan:$base(应在 $WF/plans/ 下同名)"
158
- done
159
-
160
- for spec in "$WF"/specs/[0-9]*.md; do
161
- [ -f "$spec" ] || continue
162
- is_tracked "$spec" || continue
163
- base=${spec##*/}
164
- st=$(fm_get "$spec" 状态)
165
- lvl=$(fm_get "$spec" 级别)
166
- if [ ! -f "$WF/intents/$base" ] && [ ! -f "$WF/incidents/$base" ]; then
167
- blockers="$blockers
168
- - [配对断裂] spec 缺 intent/incident:$base(应在 $WF/intents/ 或 $WF/incidents/ 下同名)"
169
- fi
170
- if ! st_ok_doc "$st"; then
171
- if [ "$lvl" = "L3" ]; then
172
- blockers="$blockers
173
- - [状态未确认] L3 spec 必须为 approved/done/superseded/cancelled:$base(frontmatter 状态键当前值:『${st:-缺失}』)"
174
- else
175
- warnings="$warnings
176
- - [WARN 状态未确认] spec 必须为 approved/done/superseded/cancelled:$base(frontmatter 状态键当前值:『${st:-缺失}』)"
177
- fi
178
- fi
179
-
180
- # L3 确认三件:确认结果/确认时间读 frontmatter;独立复核为叙述性内容,按正文行锚定(放弃件 superseded/cancelled 豁免)
181
- if [ "$lvl" = "L3" ]; then
182
- case "$st" in
183
- superseded|cancelled) ;;
184
- *)
185
- [ "$(fm_get "$spec" 确认结果)" = "approved" ] || blockers="$blockers
186
- - [L3 确认缺失] $base 确认结果必须为 approved"
187
- [ -n "$(fm_get "$spec" 确认时间)" ] || blockers="$blockers
188
- - [L3 确认缺失] $base 必须记录确认时间"
189
- grep -qE '^- 独立复核:[[:space:]]*[^<[:space:]].*$' "$spec" 2>/dev/null || blockers="$blockers
190
- - [L3 复核缺失] $base 必须记录新会话独立复核结论"
191
- ;;
192
- esac
193
- fi
194
- done
195
-
196
- for plan in "$WF"/plans/[0-9]*.md; do
197
- [ -f "$plan" ] || continue
198
- is_tracked "$plan" || continue
199
- base=${plan##*/}
200
- st=$(fm_get "$plan" 状态)
201
- lvl=$(fm_get "$plan" 级别)
202
- if [ ! -f "$WF/intents/$base" ] && [ ! -f "$WF/incidents/$base" ]; then
203
- blockers="$blockers
204
- - [配对断裂] plan 缺 intent/incident:$base(应在 $WF/intents/ 或 $WF/incidents/ 下同名)"
205
- fi
206
- if ! st_ok_doc "$st"; then
207
- if [ "$lvl" = "L3" ]; then
208
- blockers="$blockers
209
- - [状态未确认] L3 plan 必须为 approved/done/superseded/cancelled:$base(frontmatter 状态键当前值:『${st:-缺失}』)"
210
- else
211
- warnings="$warnings
212
- - [WARN 状态未确认] plan 必须为 approved/done/superseded/cancelled:$base(frontmatter 状态键当前值:『${st:-缺失}』)"
213
- fi
214
- fi
215
- done
216
-
217
- # --- 2. 模板字段占位符残留 [warning] ---
218
- placeholder_re='YYYY-MM-DD|<主题>|<日期 主题>|L0 / L1 / L2 / L3|draft / approved / done|open / fixed / closed'
219
- # 排除文件名日期约定(如「导入错误清单_YYYY-MM-DD.xlsx」)——它是运行时文件名格式,非未填占位符
220
- boilerplate_re='\.\./specs/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('"'"'YYYY-MM-DD'"'"'\)|value-format="YYYY-MM-DD"'
221
- # 2026-09-20 性能:原为每文件 2 次 grep fork(426 份 → 852 次子进程)。改为「收集受检文件
222
- # → 一次 grep 批量扫 → 临时文件回放聚合」,告警文本与顺序与原实现完全一致(glob 序、行号升序)。
223
- ph_files=""
224
- for f in "$WF"/intents/[0-9]*.md "$WF"/specs/[0-9]*.md "$WF"/plans/[0-9]*.md "$WF"/incidents/[0-9]*.md; do
225
- [ -f "$f" ] || continue
226
- is_tracked "$f" || continue
227
- ph_files="$ph_files
228
- $f"
229
- done
230
- if [ -n "$ph_files" ]; then
231
- ph_tmp="${TMPDIR:-/tmp}/check-loop-ph.$$"
232
- grep -nHE "$placeholder_re" $ph_files 2>/dev/null | grep -vE "$boilerplate_re" > "$ph_tmp"
233
- ph_cur=""
234
- ph_buf=""
235
- while IFS= read -r ph_ln; do
236
- case "$ph_ln" in
237
- *:*) ;;
238
- *) continue ;;
239
- esac
240
- ph_f="${ph_ln%%:*}"
241
- ph_rest="${ph_ln#*:}"
242
- if [ "$ph_f" != "$ph_cur" ]; then
243
- if [ -n "$ph_cur" ]; then
244
- warnings="$warnings
245
- - [WARN 模板未填] $ph_cur 含模板占位符:
246
- $ph_buf"
247
- fi
248
- ph_cur="$ph_f"
249
- ph_buf="$ph_rest"
250
- else
251
- ph_buf="$ph_buf
252
- $ph_rest"
253
- fi
254
- done < "$ph_tmp"
255
- if [ -n "$ph_cur" ]; then
256
- warnings="$warnings
257
- - [WARN 模板未填] $ph_cur 含模板占位符:
258
- $ph_buf"
259
- fi
260
- rm -f "$ph_tmp"
261
- fi
262
-
263
- # date_ge <YYYY-MM-DD> <YYYY-MM-DD>:前者不早于后者则返回 0(纯 sh,零 fork;原为 awk)
264
- date_ge() {
265
- __ay=${1%%-*}
266
- __am=${1#*-}
267
- __am=${__am%%-*}
268
- __ad=${1##*-}
269
- __by=${2%%-*}
270
- __bm=${2#*-}
271
- __bm=${__bm%%-*}
272
- __bd=${2##*-}
273
- if [ "$__ay" -ne "$__by" ]; then [ "$__ay" -gt "$__by" ]; return $?; fi
274
- if [ "$__am" -ne "$__bm" ]; then [ "$__am" -gt "$__bm" ]; return $?; fi
275
- [ "$__ad" -ge "$__bd" ]
276
- }
277
-
278
- # --- 3. incidents 复盘三件套完整性(纯字段缺失=warning)+ 新 intent 回路(断档=hard-block) ---
279
- # 2026-09-20 性能:原为每份 incident 5 次 fork(grep -qE×3 + grep -q + grep|head×2,73 份 → 365 次)。
280
- # 改为循环前单次 awk 预扫写入临时文件,循环内用 fd 3 顺序读(与下方 for 同一 glob/过滤/顺序,天然对齐)。
281
- inc_list=""
282
- for inc in "$WF"/incidents/[0-9]*.md; do
283
- [ -f "$inc" ] || continue
284
- is_tracked "$inc" || continue
285
- inc_list="$inc_list
286
- $inc"
287
- done
288
- inc_tmp="${TMPDIR:-/tmp}/check-loop-inc.$$"
289
- : > "$inc_tmp"
290
- if [ -n "$inc_list" ]; then
291
- # 每份一行: <文件名>|三件套1|2|3|是否有「是否需要新 intent」|回路 intent 路径(- = 选了是但未写路径)
292
- awk '
293
- FNR == 1 { flush(); curf = FILENAME }
294
- /^1\. / { s1 = 1 }
295
- /^2\. / { s2 = 1 }
296
- /^3\. / { s3 = 1 }
297
- index($0, "是否需要新 intent") > 0 { hp = 1 }
298
- index($0, "是 → ") > 0 && !yseen {
299
- yseen = 1
300
- if (match($0, /intents\/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[^ )]*\.md/))
301
- ref = substr($0, RSTART, RLENGTH)
302
- else
303
- ref = "-"
304
- }
305
- END { flush() }
306
- function flush() {
307
- if (curf != "") {
308
- n = split(curf, p, "/")
309
- printf "%s|%d|%d|%d|%d|%s\n", p[n], s1, s2, s3, hp, ref
310
- }
311
- s1 = 0; s2 = 0; s3 = 0; hp = 0; ref = ""; yseen = 0
312
- }
313
- ' $inc_list > "$inc_tmp" 2>/dev/null
314
- fi
315
-
316
- exec 3< "$inc_tmp"
317
- for inc in "$WF"/incidents/[0-9]*.md; do
318
- [ -f "$inc" ] || continue
319
- is_tracked "$inc" || continue
320
- # 与预扫表逐行对齐(同一 glob/过滤/顺序):此处必须最先读,不能落在任何 continue 之后
321
- IFS='|' read -r inc_key inc_s1 inc_s2 inc_s3 inc_hasparent inc_ref <&3
322
- name=${inc##*/}
323
- inc_lvl=$(fm_get "$inc" 级别)
324
- inc_st=$(fm_get "$inc" 状态)
325
-
326
- # incident 状态严格枚举(README「文档协议」对齐;2026-09-16 审查补口:此前状态零断言)
327
- case "$inc_st" in
328
- open|fixed|closed) ;;
329
- *) warnings="$warnings
330
- - [WARN 状态非法] incident 状态必须为 open/fixed/closed:$name(frontmatter 状态键当前值:『${inc_st:-缺失}』)" ;;
331
- esac
332
-
333
- if [ "$(fm_get "$inc" 流程)" != "legacy" ]; then
334
- case "$inc_lvl" in
335
- L0|L1|L2|L3)
336
- case "$inc_lvl" in
337
- L1|L2|L3)
338
- [ -f "$WF/plans/$name" ] || blockers="$blockers
339
- - [配对断裂] incident 缺 plan:$name(应在 $WF/plans/ 下同名)"
340
- ;;
341
- esac
342
- case "$inc_lvl" in
343
- L2|L3)
344
- [ -f "$WF/specs/$name" ] || blockers="$blockers
345
- - [配对断裂] incident 缺 spec:$name(应在 $WF/specs/ 下同名)"
346
- ;;
347
- esac
348
- ;;
349
- *)
350
- warnings="$warnings
351
- - [WARN 级别缺失] $name 必须填写 L0/L1/L2/L3(配对检查已跳过——补级别后重跑本脚本校验配对)"
352
- ;;
353
- esac
354
- fi
355
-
356
- if [ "$inc_s1" = "0" ]; then
357
- warnings="$warnings
358
- - [WARN 三件套不全] $name 缺复盘三件套之 1"
359
- fi
360
- if [ "$inc_s2" = "0" ]; then
361
- warnings="$warnings
362
- - [WARN 三件套不全] $name 缺复盘三件套之 2"
363
- fi
364
- if [ "$inc_s3" = "0" ]; then
365
- warnings="$warnings
366
- - [WARN 三件套不全] $name 缺复盘三件套之 3"
367
- fi
368
-
369
- if [ "$inc_hasparent" = "0" ]; then
370
- warnings="$warnings
371
- - [WARN 三件套不全] $name 缺「是否需要新 intent」子项"
372
- continue
373
- fi
374
-
375
- if [ "$inc_ref" = "-" ]; then
376
- blockers="$blockers
377
- - [回路断档] $name 选「是」但未指明 intent 文件路径"
378
- elif [ -n "$inc_ref" ] && [ ! -f "$WF/$inc_ref" ]; then
379
- blockers="$blockers
380
- - [回路断档] $name 引用的 intent 不存在:$inc_ref"
381
- fi
382
- done
383
- exec 3<&-
384
- rm -f "$inc_tmp"
385
-
386
- # --- 4. 引用有效性 [warning] ---
387
- # 2026-09-20 性能:原为每文件 2 次 fork(grep -o + sort,445 份 → 890 次)。改为一次
388
- # grep -oHE 批量抽取 + 临时文件按文件去重回放。原实现用 sort -u 排序输出,此处按首次
389
- # 出现序去重;两者仅在「同一文件存在多处引用断档」时告警顺序不同(当前仓库该告警为 0)。
390
- rf_files=""
391
- for f in AGENTS.md */AGENTS.md "$WF"/*.md "$WF"/*/[0-9]*.md "$WF"/*/_TEMPLATE.md .agents/commands/*.md; do
392
- [ -f "$f" ] || continue
393
- rf_files="$rf_files
394
- $f"
395
- done
396
- if [ -n "$rf_files" ]; then
397
- rf_tmp="${TMPDIR:-/tmp}/check-loop-rf.$$"
398
- grep -ohHE '\.agents/(commands|hooks|scripts|skills|roles)/[A-Za-z0-9_][A-Za-z0-9_./-]*' $rf_files > "$rf_tmp" 2>/dev/null
399
- rf_cur=""
400
- rf_seen=""
401
- while IFS= read -r rf_ln; do
402
- rf_f="${rf_ln%%:*}"
403
- ref="${rf_ln#*:}"
404
- [ -n "$ref" ] || continue
405
- if [ "$rf_f" != "$rf_cur" ]; then
406
- rf_cur="$rf_f"
407
- rf_seen=""
408
- fi
409
- case "
410
- $rf_seen
411
- " in
412
- *"
413
- $ref
414
- "*) continue ;;
415
- esac
416
- rf_seen="$rf_seen
417
- $ref"
418
- if [ ! -e "$ref" ]; then
419
- # .agents/skills/ 技能库装在用户级(~/.agents/skills/,AGENTS.md「技能辅助」节),
420
- # 仓库内无而用户级有不算断档;其余路径仍须仓库内存在
421
- case "$ref" in
422
- .agents/skills/*) [ -e "$HOME/$ref" ] || warnings="$warnings
423
- - [WARN 引用断档] $rf_f 引用不存在的文件:$ref" ;;
424
- *) warnings="$warnings
425
- - [WARN 引用断档] $rf_f 引用不存在的文件:$ref" ;;
426
- esac
427
- fi
428
- done < "$rf_tmp"
429
- rm -f "$rf_tmp"
430
- fi
431
-
432
- # --- 5. 子智能体角色契约与宿主 Adapter 一致性 [warning] ---
433
- for role in implementer independent-reviewer ui-verifier; do
434
- role_path=".agents/roles/$role.md"
435
- if [ ! -f "$role_path" ]; then
436
- warnings="$warnings
437
- - [WARN 角色缺失] 公共角色契约不存在:$role_path"
438
- continue
439
- fi
440
-
441
- opencode_adapter=".opencode/agents/$role.md"
442
- trae_adapter=".trae/agents/$role.md"
443
-
444
- if [ ! -d .opencode/agents ]; then
445
- # 未安装 OpenCode 宿主(init 未选或已移除)——静默跳过
446
- :
447
- elif [ ! -f "$opencode_adapter" ]; then
448
- warnings="$warnings
449
- - [WARN Adapter 缺失] OpenCode 缺 $role:$opencode_adapter"
450
- else
451
- grep -q '^mode:[[:space:]]*subagent[[:space:]]*$' "$opencode_adapter" 2>/dev/null || warnings="$warnings
452
- - [WARN Adapter 无效] OpenCode $role 未声明 mode:subagent:$opencode_adapter"
453
- grep -qF "$role_path" "$opencode_adapter" 2>/dev/null || warnings="$warnings
454
- - [WARN Adapter 断线] OpenCode $role 未引用公共角色:$role_path"
455
- fi
456
-
457
- if [ ! -d .trae/agents ]; then
458
- # 未安装 Trae 宿主(init 未选或已移除)——静默跳过
459
- :
460
- elif [ ! -f "$trae_adapter" ]; then
461
- warnings="$warnings
462
- - [WARN Adapter 缺失] Trae 缺 $role:$trae_adapter"
463
- else
464
- grep -q "^name:[[:space:]]*$role[[:space:]]*$" "$trae_adapter" 2>/dev/null || warnings="$warnings
465
- - [WARN Adapter 无效] Trae $role 的 name 与文件名不一致:$trae_adapter"
466
- grep -qF "$role_path" "$trae_adapter" 2>/dev/null || warnings="$warnings
467
- - [WARN Adapter 断线] Trae $role 未引用公共角色:$role_path"
468
- fi
469
-
470
- zcode_adapter=".zcode/agents/$role.md"
471
- if [ ! -f "$zcode_adapter" ]; then
472
- # 2026-09-13 起 .zcode/agents/ 为本地配置不入 git(AGENTS.md「指令与子代理调用」节),
473
- # 干净 clone / 未维护本地的环境必然缺失——缺失不再告警,宿主按 fallback: main 兜底;
474
- # 存在时仍校验一致性,作为「本地自行维护」的质量门
475
- :
476
- else
477
- grep -qE "^name:[[:space:]]*[\"']?$role[\"']?[[:space:]]*$" "$zcode_adapter" 2>/dev/null || warnings="$warnings
478
- - [WARN Adapter 无效] ZCode $role 的 name 与文件名不一致:$zcode_adapter"
479
- grep -qF "$role_path" "$zcode_adapter" 2>/dev/null || warnings="$warnings
480
- - [WARN Adapter 断线] ZCode $role 未引用公共角色:$role_path"
481
- fi
482
-
483
- if ! grep -qF "$role_path" AGENTS.md .agents/commands/*.md 2>/dev/null; then
484
- warnings="$warnings
485
- - [WARN 命令断线] 工作流未引用公共角色:$role_path"
486
- fi
487
- done
488
-
489
- legacy_agent_refs=$(grep -nHE '(^subagent:|subagent_type=|general_purpose_task|browser_use)' AGENTS.md .agents/commands/*.md 2>/dev/null)
490
- if [ -n "$legacy_agent_refs" ]; then
491
- warnings="$warnings
492
- - [WARN 旧委派残留] 主规则或阶段命令仍含宿主特定/旧委派声明:
493
- $legacy_agent_refs"
494
- fi
495
-
496
- pinned_models=$(grep -nH '^model:' .opencode/agents/*.md .trae/agents/*.md .zcode/agents/*.md 2>/dev/null | grep -v 'model:[[:space:]]*inherit')
497
- if [ -n "$pinned_models" ]; then
498
- warnings="$warnings
499
- - [WARN 宿主耦合] 薄 Adapter 不应固定模型:
500
- $pinned_models"
501
- fi
502
-
503
- # --- 6. 阶段索引同步 [warning] ---
504
- for cmd in plan design build test deploy maintain review; do
505
- for doc in AGENTS.md .agents/commands/new-task.md; do
506
- grep -q "\.agents/commands/$cmd\.md" "$doc" 2>/dev/null || warnings="$warnings
507
- - [WARN 阶段索引漂移] $doc 缺 $cmd 指令索引(两处阶段表须同步维护)"
508
- done
509
- done
510
-
511
- # --- 8. intent 验收标准对账(闭环最后一公里:done 必须逐条勾验并补证据) ---
512
- # 2026-09-12 起新建的 intent:done 状态下验收标准有未勾项 = hard-block(闭环断档类);
513
- # 勾选但缺「证据:」= warning;存量 intent 不回填,聚合为一条 warning 提示,豁免 hard。
514
- acc_cutoff="2026-09-12"
515
- legacy_unaccounted=0
516
- # 2026-09-20 性能:原为每份 done intent 7 次 fork(printf|grep + awk 日期 + grep -qE 节存在
517
- # + 2 awk 勾选扫描 + grep 豁免,约 100 份 → 约 700 次)。改为循环前单次 awk 预扫写临时文件,
518
- # 循环内 fd 4 顺序读;日期改纯 sh 比较。
519
- acc_list=""
520
- for intent in "$WF"/intents/[0-9]*.md; do
521
- [ -f "$intent" ] || continue
522
- is_tracked "$intent" || continue
523
- acc_list="$acc_list
524
- $intent"
525
- done
526
- acc_tmp="${TMPDIR:-/tmp}/check-loop-acc.$$"
527
- : > "$acc_tmp"
528
- if [ -n "$acc_list" ]; then
529
- # 每份一行: <文件名>|有无验收节|有无未勾项|有无勾选缺证据|是否含存量对账豁免声明
530
- awk '
531
- FNR == 1 { flush(); curf = FILENAME }
532
- /^[[:space:]]*##[[:space:]]+[^#]*验收标准/ { hs = 1; insec = 1; next }
533
- insec && /^[[:space:]]*##[[:space:]]/ { insec = 0 }
534
- insec && /^[[:space:]]*- \[ \]/ { uc = 1 }
535
- insec && /^[[:space:]]*- \[x\]/ && $0 !~ /证据:/ { ne = 1 }
536
- index($0, "存量对账豁免(") > 0 { ex = 1 }
537
- END { flush() }
538
- function flush() {
539
- if (curf != "") {
540
- n = split(curf, p, "/")
541
- printf "%s|%d|%d|%d|%d\n", p[n], hs, uc, ne, ex
542
- }
543
- hs = 0; uc = 0; ne = 0; ex = 0; insec = 0
544
- }
545
- ' $acc_list > "$acc_tmp" 2>/dev/null
546
- fi
547
-
548
- exec 4< "$acc_tmp"
549
- for intent in "$WF"/intents/[0-9]*.md; do
550
- [ -f "$intent" ] || continue
551
- is_tracked "$intent" || continue
552
- # 与预扫表逐行对齐:此读必须最先执行(不能落在任何 continue 之后)
553
- IFS='|' read -r acc_key acc_has_sec acc_uc acc_ne acc_ex <&4
554
- base=${intent##*/}
555
- [ "$(fm_get "$intent" 状态)" = "done" ] || continue
556
- # 日期改纯 sh:文件名以 YYYY-MM-DD 开头,取前 10 字符后用 date_ge 数值比较
557
- __d="${base%.md}"
558
- filedate=""
559
- __i=0
560
- while [ "$__i" -lt 10 ]; do
561
- filedate="$filedate${__d%"${__d#?}"}"
562
- __d="${__d#?}"
563
- __i=$((__i + 1))
564
- done
565
- case "$filedate" in
566
- [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;;
567
- *) filedate="" ;;
568
- esac
569
- is_new=0
570
- if [ -n "$filedate" ] && date_ge "$filedate" "$acc_cutoff"; then
571
- is_new=1
572
- fi
573
- # 节存在性(2026-09-16 补口):done intent 必须含「## 验收标准」节(允许前导空白与标题内编号),缺节不得静默视为已对账
574
- if [ "$acc_has_sec" = "0" ]; then
575
- if [ "$is_new" = "1" ]; then
576
- blockers="$blockers
577
- - [验收未对账] done intent 缺「## 验收标准」节(勾验无从核对):$base"
578
- else
579
- [ "$acc_ex" = "1" ] || legacy_unaccounted=$((legacy_unaccounted+1))
580
- fi
581
- continue
582
- fi
583
- # 勾选扫描:允许前导空白(与看板 parseAcceptance 同口径);节结束=下一个二级标题
584
- [ "$acc_uc" = "0" ] && [ "$acc_ne" = "0" ] && continue
585
- if [ "$is_new" = "1" ]; then
586
- if [ "$acc_uc" = "1" ]; then
587
- blockers="$blockers
588
- - [验收未对账] done intent 验收标准有未勾验项:$base(逐条勾验并补证据:commit/用例/冒烟输出)"
589
- fi
590
- if [ "$acc_ne" = "1" ]; then
591
- warnings="$warnings
592
- - [WARN 验收缺证据] $base 验收标准勾选项缺「证据:」标注"
593
- fi
594
- else
595
- # 存量豁免:intent 含「存量对账豁免(日期,用户拍板不追溯)」声明即视为已对账
596
- # (2026-09-12 用户拍板 UI 二轮实测不追溯,commit 1069c94;此处为该决策的机器识别。
597
- # 放在 else 分支:新建 intent 的 hard 判定不受豁免声明影响,不可借声明逃避勾验)
598
- [ "$acc_ex" = "1" ] || legacy_unaccounted=$((legacy_unaccounted+1))
599
- fi
600
- done
601
- exec 4<&-
602
- rm -f "$acc_tmp"
603
- if [ "$legacy_unaccounted" -gt 0 ]; then
604
- warnings="$warnings
605
- - [WARN 验收对账存量] $legacy_unaccounted 个存量 done intent 验收标准未对账(2026-09-12 前创建,豁免 hard,不回填)"
606
- fi
607
-
608
- # 2026-09-20 性能:原为每文件 2 次 fork(printf + grep -q),426 文件 → 852 次子进程。
609
- # 纯 sh case 判定:含任意非 ASCII/不可打印字符即返回 1(等价于 LC_ALL=C grep '[^ -~]')
610
- is_ascii_name() {
611
- case "$1" in *[!\ -~]*) return 1 ;; *) return 0 ;; esac
612
- }
613
-
614
- # --- 9. 文件名英文 kebab-case(非 ASCII 文件名 = warning,2026-09-11 规则) ---
615
- for f in "$WF"/intents/*.md "$WF"/specs/*.md "$WF"/plans/*.md "$WF"/incidents/*.md; do
616
- [ -f "$f" ] || continue
617
- is_tracked "$f" || continue
618
- base=${f##*/}
619
- [ "$base" = "_TEMPLATE.md" ] && continue
620
- if ! is_ascii_name "$base"; then
621
- warnings="$warnings
622
- - [WARN 文件名非英文] 文件名含非 ASCII 字符,应为英文 kebab-case:$f"
623
- fi
624
- done
625
-
626
- # --- 10. 级别 vs 迁移文件一致性(启发式:疑似判低)[warning] ---
627
- # 依据「入口文档随代码同一提交」约定,定位 L1/L2 intent/incident 首次加入的 commit;
628
- # 该提交触及 docs/scripts/ 迁移 SQL 或 **/Migrations/ 时提示复核级别(混合改动应就高不就低)。
629
- # 启发式有漏报(代码与文档分 commit 提交、merge commit 首次引入)与误报可能,仅 advisory 不阻断;
630
- # 不在 git 仓库内(fixture 测试)时整体跳过。
631
- # 性能:单次 git log 拉全量「文件→首次加入 commit→该提交文件清单」映射,单 awk 内完成匹配,
632
- # 避免逐文档起 git 进程(Windows 下每次 fork ~0.6s,95 文档逐查曾耗时 ~2min)。
633
- if git rev-parse --git-dir >/dev/null 2>&1; then
634
- # 单次 git log 走管道进 awk(stdin=add 映射),md 文件由 awk 按相对路径 getline 读取;
635
- # 不落临时文件——Git Bash 的 gawk 是原生 Windows 程序,打不开 mktemp 的 MSYS 虚拟路径(/tmp/...)。
636
- # 输出以 \x1e(记录分隔符)切条:每条 = 描述首行 + 换行 + 命中的迁移文件行
637
- hits=$(git log --diff-filter=A --format=@%H --name-only 2>/dev/null | awk -v docs="$(echo "$WF"/intents/[0-9]*.md "$WF"/incidents/[0-9]*.md)" '
638
- function check(f, c, arr, n, i, hit, p, m, base) {
639
- c = addpath[f]
640
- if (c == "") return
641
- n = split(cfiles[c], arr, "\n")
642
- hit = ""
643
- for (i = 1; i <= n; i++)
644
- if (arr[i] ~ /(^|\/)docs\/scripts\/.*\.sql$/ || arr[i] ~ /(^|\/)Migrations\//)
645
- hit = hit "\n" arr[i]
646
- if (hit != "") {
647
- m = split(f, p, "/"); base = p[m]
648
- printf "%s 级别 %s 但其加入提交触及迁移文件(混合改动应就高不就低,请复核级别):\n%s\x1e", base, lvl, substr(hit, 2)
649
- }
650
- }
651
- { # 主输入 = git log 的「@commit + 文件清单」流
652
- if ($0 ~ /^@/) { c = substr($0, 2); next }
653
- if (NF && c != "" && !($0 in addpath)) { # 逆序输出(新→旧):仅首次遇及,同名文件删后重建取最新 add
654
- addpath[$0] = c
655
- cfiles[c] = cfiles[c] "\n" $0
656
- }
657
- }
658
- END {
659
- nd = split(docs, dl, " ")
660
- for (i = 1; i <= nd; i++) {
661
- f = dl[i]; fm2 = 0; lvl = ""; ln = 0
662
- while ((getline line < f) > 0) {
663
- ln++
664
- if (ln == 1) { fm2 = (line ~ /^---[[:space:]]*$/) ? 1 : 0; continue }
665
- if (fm2 && line ~ /^---[[:space:]]*$/) { fm2 = 0; continue }
666
- if (fm2 && index(line, "级别:") == 1) {
667
- s = line; sub(/^级别:[[:space:]]*/, "", s); sub(/[[:space:]]+$/, "", s); lvl = s
668
- }
669
- }
670
- close(f)
671
- if (lvl == "L1" || lvl == "L2") check(f)
672
- }
673
- }
674
- ')
675
- if [ -n "$hits" ]; then
676
- set -f
677
- IFS="$(printf '\036')"
678
- for rec in $hits; do
679
- [ -n "$rec" ] || continue
680
- warnings="$warnings
681
- - [WARN 级别疑似判低] $rec"
682
- done
683
- unset IFS
684
- set +f
685
- fi
686
- fi
687
-
688
- # --- 11. workflow/INDEX.md 漂移(活跃层索引与磁盘不一致 = warning,2026-09-21 检索层) ---
689
- # 口径与生成器一致(扫磁盘):直接调生成器 --check,不在本脚本复刻渲染逻辑(避免双写漂移)。
690
- # 根下无生成器(fixture 未提供)/无 node 时自然跳过,不误报、门禁不失效。
691
- if [ -f .agents/scripts/gen-workflow-index.mjs ] && command -v node >/dev/null 2>&1; then
692
- idx_out=$(node .agents/scripts/gen-workflow-index.mjs --check 2>&1)
693
- if [ $? -ne 0 ]; then
694
- idx_first=$(printf '%s' "$idx_out" | head -1)
695
- warnings="$warnings
696
- - [WARN 索引漂移] workflow/INDEX.md 与磁盘不一致——跑 node .agents/scripts/gen-workflow-index.mjs 重新生成($idx_first)"
697
- fi
698
- fi
699
-
700
- # --- 12. frontmatter「模块:」合法性(2026-09-21 检索层)[warning] ---
701
- # 词表单源 .agents/workflow-modules.txt;缺字段只对「2026-09-22 起新建」(日期/发现 ≥ 2026-09-22)提示——
702
- # 规则发布次日生效,不追溯发布当日已在途的并行文档。词表缺失(fixture 未提供)时整体跳过。
703
- # 性能:单次 awk 完成全部文档扫描(对照检查 10 的预扫思路),避免逐文档 fork。
704
- if [ -f .agents/workflow-modules.txt ]; then
705
- mods=$(grep -v '^#' .agents/workflow-modules.txt | grep -v '^[[:space:]]*$')
706
- mod_docs=""
707
- for f in "$WF"/intents/[0-9]*.md "$WF"/specs/[0-9]*.md "$WF"/plans/[0-9]*.md "$WF"/incidents/[0-9]*.md; do
708
- [ -f "$f" ] || continue
709
- is_tracked "$f" || continue
710
- mod_docs="$mod_docs $f"
711
- done
712
- if [ -n "$mod_docs" ]; then
713
- mod_hits=$(awk -v modules="$mods" -v docs="$mod_docs" 'BEGIN {
714
- n = split(modules, m, "\n")
715
- for (i = 1; i <= n; i++) if (m[i] != "") vocab[m[i]] = 1
716
- nd = split(docs, dl, " ")
717
- for (k = 1; k <= nd; k++) {
718
- f = dl[k]; fm = 0; ln = 0; d = ""; mod = ""
719
- while ((getline line < f) > 0) {
720
- ln++
721
- if (ln == 1) { fm = (line ~ /^---[[:space:]]*$/) ? 1 : 0; continue }
722
- if (fm && line ~ /^---[[:space:]]*$/) break
723
- if (!fm) continue
724
- if (index(line, "模块:") == 1) { s = line; sub(/^模块:[[:space:]]*/, "", s); sub(/[[:space:]]+$/, "", s); mod = s }
725
- else if (index(line, "日期:") == 1) { s = line; sub(/^日期:[[:space:]]*/, "", s); sub(/[[:space:]]+$/, "", s); d = s }
726
- else if (index(line, "发现:") == 1) { s = line; sub(/^发现:[[:space:]]*/, "", s); sub(/[[:space:]]+$/, "", s); d = s }
727
- }
728
- close(f)
729
- m3 = split(f, p, "/"); base = p[m3]
730
- # 日期回退文件名前缀(plan/spec 的 frontmatter 无日期字段,口径同 gen-workflow-index.mjs / 看板)
731
- if (d == "") d = substr(base, 1, 10)
732
- if (mod != "") {
733
- if (!(mod in vocab)) printf "%s「模块: %s」不在词表(见 .agents/workflow-modules.txt)\036", base, mod
734
- } else if (d ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/ && d >= "2026-09-22") {
735
- printf "%s 缺「模块:」字段(2026-09-22 起新建文档必填)\036", base
736
- }
737
- }
738
- }' </dev/null)
739
- if [ -n "$mod_hits" ]; then
740
- set -f
741
- IFS="$(printf '\036')"
742
- for rec in $mod_hits; do
743
- [ -n "$rec" ] || continue
744
- warnings="$warnings
745
- - [WARN 模块元数据] $rec"
746
- done
747
- unset IFS
748
- set +f
749
- fi
750
- fi
751
- fi
752
-
753
- # --- 13. 常驻面体积预算(超限 = warning,不阻断;2026-09-21 规则面精简配套) ---
754
- # 判定逻辑与预算表**单源**在 .agents/scripts/rule-budget.sh + .agents/rule-budgets.txt(与 pre-commit 的
755
- # --staged 硬拦共用),本脚本只把退出码转成 WARN——push 侧提示,commit 侧已硬拦;不在此复刻判定。
756
- # 脚本 / 预算表缺失(fixture 未提供)时自然跳过,不误报。
757
- if [ -f .agents/scripts/rule-budget.sh ] && [ -f .agents/rule-budgets.txt ]; then
758
- if ! rb_out=$(sh .agents/scripts/rule-budget.sh --all 2>&1); then
759
- warnings="$warnings
760
- - [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):
761
- $(printf '%s' "$rb_out" | head -5 | sed 's/^/ /')"
762
- fi
763
- fi
764
-
765
- # --- 14. 新 done 的 spec/plan 须在 git 历史里出现过 approved(确认环节留痕)[warning] ---
766
- # 规则生效 2026-09-22(papercut 2026-09-22,用户点名):`approved` 是确认环节的机器可见态,`done` 只在关单出现;
767
- # 从 draft 直跳 done 会让确认环节在文档里消失——而检查 5 视 approved/done 同权,此前无任何门禁能发现跳态。
768
- # 2026-09-23 裁定演进(源自 Shipyard 回流):此警告恒为 advisory、永不升级 hard-block;确认后 docs(*) 提交留痕为必做
769
- # (根 AGENTS.md 提交约定),本警告即「遗漏提交」提醒。备注含「存量确认态豁免(…)」声明者出账(口径同检查 8)。
770
- # 作用域:日期 ≥ 2026-09-23(生效次日,不追溯发布当日已在途的并行文档;存量同理——避免对无法诚实补正的旧件刷屏,
771
- # 口径同检查 12:frontmatter 日期/发现 → 文件名前缀回退)。
772
- # 判据:当前状态为 done 的文件,须在 git 历史中至少有一个提交版本的**行首**出现过 `状态: approved`
773
- # (用 -G 行级正则而非 -S 计数:前者按行匹配,配 ^ 锚定即与 fm_get「键须在行首」口径一致,且一次调用覆盖空格变体)。
774
- # 依赖 git:非 git 目录(fixture 未 git init)/ 尚无提交的空仓库自动跳过;候选集仅新文档,每候选一次 git 调用,成本可控。
775
- if git rev-parse --git-dir >/dev/null 2>&1 && git rev-parse -q --verify HEAD >/dev/null 2>&1; then
776
- for f in "$WF"/specs/[0-9]*.md "$WF"/plans/[0-9]*.md; do
777
- [ -f "$f" ] || continue
778
- is_tracked "$f" || continue
779
- [ "$(fm_get "$f" 状态)" = "done" ] || continue
780
- base=${f##*/}
781
- d=$(fm_get "$f" 日期)
782
- [ -n "$d" ] || d=$(fm_get "$f" 发现)
783
- [ -n "$d" ] || d=${base%${base#??????????}}
784
- case "$d" in
785
- 2026-09-2[3-9]|2026-09-[3-9][0-9]|2026-1[0-2]-[0-3][0-9]|202[7-9]-*|20[3-9][0-9]-*|2[1-9][0-9][0-9]-*) ;;
786
- *) continue ;;
787
- esac
788
- # 出账:备注含「存量确认态豁免(…)」声明者跳过(口径同检查 8 的存量对账豁免;留痕规则生效前的存量 done 件)
789
- [ -n "$(grep -m1 '存量确认态豁免(' "$f" 2>/dev/null)" ] && continue
790
- [ -n "$(git log -1 --format=%H -G'^状态:[[:space:]]*approved' -- "$f" 2>/dev/null)" ] && continue
791
- warnings="$warnings
792
- - [WARN 确认态缺失] $base 状态已 done 但 git 历史中从未出现行首「状态: approved」——确认环节未留痕(draft 直跳 done)"
793
- done
794
- fi
795
-
796
- # --- 输出 ---
797
- # hard-block 阻断(配对断裂/回路断档),warning 提示不阻断
798
- if [ -n "$blockers" ]; then
799
- printf '闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n%s\n\n' "$blockers" >&2
800
- if [ -n "$warnings" ]; then
801
- printf 'WARN(advisory,不阻断):\n%s\n' "$warnings" >&2
802
- fi
803
- exit 1
804
- fi
805
-
806
- if [ -n "$warnings" ]; then
807
- printf 'check-loop.sh WARN(advisory,不阻断):\n%s\n' "$warnings" >&2
808
- fi
809
- exit 0
2
+ # check-loop.sh — 兼容 shim(2026-09-26 check-loop-node:实现已全量迁移 node,本文件只保留调用面兼容)
3
+ # 实现与检查项清单(14 项 `// N.` 注释)见同目录 check-loop.mjs 头部;判定语义/输出契约/exit 码不变。
4
+ # 保留原因:.githooks/pre-push 与既有文档/适配层按「check-loop.sh」文件名引用——文件名即稳定接口。
5
+ # 检查项清单与判据「以 check-loop.mjs 头部注释为准」(根 AGENTS.md「闭环兜底」节口径经本行承接)。
6
+ exec node "$(dirname "$0")/check-loop.mjs" "$@"