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