@aibyzero/byz 0.1.1 → 0.1.3

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 (114) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +34 -24
  3. package/dist/cli.js +22 -14
  4. package/dist/core/export-html/template.css +1066 -0
  5. package/dist/core/export-html/template.html +55 -0
  6. package/dist/core/export-html/template.js +1864 -0
  7. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  8. package/dist/core/export-html/vendor/marked.min.js +78 -0
  9. package/dist/fast.js +64 -0
  10. package/dist/modes/interactive/assets/clankolas.png +0 -0
  11. package/dist/modes/interactive/theme/dark.json +90 -0
  12. package/dist/modes/interactive/theme/light.json +89 -0
  13. package/dist/modes/interactive/theme/theme-schema.json +352 -0
  14. package/dist/runtime/bundle/chunks/{chunk-CCRJHU72.js → chunk-QY7DJQRI.js} +2 -2
  15. package/dist/runtime/bundle/chunks/github-copilot.js +1 -1
  16. package/dist/runtime/bundle/cli.js +1 -1
  17. package/dist/runtime/bundle/index.js +1 -1
  18. package/dist/runtime/bundle/rpc-entry.js +1 -1
  19. package/dist/runtime/core/resource-loader.d.ts +1 -0
  20. package/dist/runtime/core/resource-loader.d.ts.map +1 -1
  21. package/dist/runtime/core/resource-loader.js +8 -3
  22. package/dist/runtime/core/resource-loader.js.map +1 -1
  23. package/dist/workflows.js +4 -131
  24. package/package.json +2 -1
  25. package/workflows/cm-plugin/LICENSE +21 -0
  26. package/workflows/cm-plugin/README.md +195 -0
  27. package/workflows/cm-plugin/VERSION +1 -0
  28. package/workflows/cm-plugin/agents/cm-plugin-backend-agent.md +34 -0
  29. package/workflows/cm-plugin/agents/cm-plugin-extension-agent.md +35 -0
  30. package/workflows/cm-plugin/agents/cm-plugin-ui-agent.md +34 -0
  31. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N1-init.md +60 -0
  32. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N2-enter-feature.md +51 -0
  33. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N3-execute-task.md +32 -0
  34. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N4-review.md +58 -0
  35. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N5-mark-done.md +80 -0
  36. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N6-qa-eval.md +69 -0
  37. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N7-context.md +23 -0
  38. package/workflows/cm-plugin/commands/cm-plugin-ai-nodes/N8-finish.md +61 -0
  39. package/workflows/cm-plugin/commands/cm-plugin-prd-modes/brownfield.md +13 -0
  40. package/workflows/cm-plugin/commands/cm-plugin-prd-modes/change-mode.md +130 -0
  41. package/workflows/cm-plugin/commands/cm-plugin-prd-modes/greenfield.md +82 -0
  42. package/workflows/cm-plugin/commands/cm-plugin:ai.md +75 -0
  43. package/workflows/cm-plugin/commands/cm-plugin:check.md +66 -0
  44. package/workflows/cm-plugin/commands/cm-plugin:fix.md +100 -0
  45. package/workflows/cm-plugin/commands/cm-plugin:idea.md +29 -0
  46. package/workflows/cm-plugin/commands/cm-plugin:init.md +145 -0
  47. package/workflows/cm-plugin/commands/cm-plugin:prd.md +403 -0
  48. package/workflows/cm-plugin/commands/cm-plugin:refactor.md +147 -0
  49. package/workflows/cm-plugin/commands/cm-plugin:rewrite.md +90 -0
  50. package/workflows/cm-plugin/commands/cm-plugin:scout.md +202 -0
  51. package/workflows/cm-plugin/docs//346/265/213/350/257/225/346/214/207/345/274/225-/345/277/253/351/200/237/344/270/212/346/211/213.md +189 -0
  52. package/workflows/cm-plugin/docs//351/207/215/346/236/204/346/265/201/347/250/213/350/256/276/350/256/241/README.md +17 -0
  53. package/workflows/cm-plugin/docs//351/207/215/346/236/204/346/265/201/347/250/213/350/256/276/350/256/241/refactor-flow.excalidraw +3650 -0
  54. package/workflows/cm-plugin/docs//351/207/215/346/236/204/346/265/201/347/250/213/350/256/276/350/256/241/refactor-flow.mp4 +0 -0
  55. package/workflows/cm-plugin/docs//351/207/215/346/236/204/346/265/201/347/250/213/350/256/276/350/256/241/refactor-flow.png +0 -0
  56. package/workflows/cm-plugin/docs//351/207/215/346/236/204/346/265/201/347/250/213/350/256/276/350/256/241/refactor-flow.spec.json +223 -0
  57. package/workflows/cm-plugin/package.json +33 -0
  58. package/workflows/cm-plugin/skills/cm-plugin-backend-engineer/SKILL.md +100 -0
  59. package/workflows/cm-plugin/skills/cm-plugin-devops-engineer/NOTICE.md +14 -0
  60. package/workflows/cm-plugin/skills/cm-plugin-devops-engineer/SKILL.md +120 -0
  61. package/workflows/cm-plugin/skills/cm-plugin-devops-engineer/references/cws-ci-cd.md +139 -0
  62. package/workflows/cm-plugin/skills/cm-plugin-devops-engineer/references/cws-submission-checklist.md +87 -0
  63. package/workflows/cm-plugin/skills/cm-plugin-doc-syncer/SKILL.md +149 -0
  64. package/workflows/cm-plugin/skills/cm-plugin-extension-engineer/SKILL.md +105 -0
  65. package/workflows/cm-plugin/skills/cm-plugin-product-manager/SKILL.md +83 -0
  66. package/workflows/cm-plugin/skills/cm-plugin-qa-engineer/NOTICE.md +14 -0
  67. package/workflows/cm-plugin/skills/cm-plugin-qa-engineer/SKILL.md +134 -0
  68. package/workflows/cm-plugin/skills/cm-plugin-qa-engineer/references/cws-scan-checklist.md +136 -0
  69. package/workflows/cm-plugin/skills/cm-plugin-qa-engineer/references/cws-violation-codes.md +68 -0
  70. package/workflows/cm-plugin/skills/cm-plugin-ui-engineer/SKILL.md +94 -0
  71. package/workflows/cm-plugin/skills/codebase-context/SKILL.md +489 -0
  72. package/workflows/cm-plugin/skills/darwin-skill/NOTICE.md +24 -0
  73. package/workflows/cm-plugin/skills/darwin-skill/README.md +272 -0
  74. package/workflows/cm-plugin/skills/darwin-skill/SKILL.md +492 -0
  75. package/workflows/cm-plugin/skills/darwin-skill/references/runtime-neutrality.md +68 -0
  76. package/workflows/cm-plugin/skills/darwin-skill/references/skilllens-evidence.md +142 -0
  77. package/workflows/cm-plugin/skills/darwin-skill/scripts/screenshot.mjs +71 -0
  78. package/workflows/cm-plugin/skills/darwin-skill/templates/result-card-dark.html +698 -0
  79. package/workflows/cm-plugin/skills/darwin-skill/templates/result-card-white.html +444 -0
  80. package/workflows/cm-plugin/skills/darwin-skill/templates/result-card.html +616 -0
  81. package/workflows/cm-plugin/skills/idea-to-prd/SKILL.md +289 -0
  82. package/workflows/cm-plugin/skills/idea-to-prd/references/domains/trading.md +97 -0
  83. package/workflows/cm-plugin/skills/idea-to-prd/references/example-prd.md +92 -0
  84. package/workflows/cm-plugin/templates/arch-reference.md +55 -0
  85. package/workflows/cm-plugin/templates/auto-update/cm-announce.sh +19 -0
  86. package/workflows/cm-plugin/templates/auto-update/cm-update.sh +251 -0
  87. package/workflows/cm-plugin/templates/dashboard/dashboard.html +153 -0
  88. package/workflows/cm-plugin/templates/dashboard/serve.sh +10 -0
  89. package/workflows/cm-plugin/templates/e2e/extension-harness.ts +110 -0
  90. package/workflows/cm-plugin/templates/e2e/smoke.spec.example.ts +51 -0
  91. package/workflows/cm-plugin/templates/hooks/pre-commit-cm-task-check +33 -0
  92. package/workflows/cm-plugin/templates/pixel/cm-pixel.html +388 -0
  93. package/workflows/cm-plugin/templates/pixel/cm-pixel.sh +212 -0
  94. package/workflows/cm-plugin/templates/pixel/dev/README.md +20 -0
  95. package/workflows/cm-plugin/templates/pixel/dev/atlas-preview.png +0 -0
  96. package/workflows/cm-plugin/templates/pixel/dev/build.py +55 -0
  97. package/workflows/cm-plugin/templates/pixel/dev/sheets/roguelikeChar_transparent.png +0 -0
  98. package/workflows/cm-plugin/templates/pixel/dev/sheets/roguelikeCity_tilemap.png +0 -0
  99. package/workflows/cm-plugin/templates/pixel/dev/sheets/roguelikeIndoor_transparent.png +0 -0
  100. package/workflows/cm-plugin/templates/pixel/dev/template.html +388 -0
  101. package/workflows/cm-plugin/templates/pixel/serve.sh +12 -0
  102. package/workflows/cm-plugin/templates/refactor/cm-refactor-denies.json +14 -0
  103. package/workflows/cm-plugin/templates/rules/backend-api.md +36 -0
  104. package/workflows/cm-plugin/templates/rules/chrome-extension.md +53 -0
  105. package/workflows/cm-plugin/templates/rules/coding-style.md +44 -0
  106. package/workflows/cm-plugin/templates/rules/frontend.md +32 -0
  107. package/workflows/cm-plugin/templates/rules/git-workflow.md +25 -0
  108. package/workflows/cm-plugin/templates/rules/security.md +31 -0
  109. package/workflows/cm-plugin/templates/rules/testing.md +36 -0
  110. package/workflows/cm-plugin/templates/scripts/cm-plugin-codex.sh +52 -0
  111. package/workflows/cm-plugin/templates/scripts/cm-plugin-log.sh +41 -0
  112. package/workflows/cm-plugin/templates/scripts/cm-plugin-preflight.sh +60 -0
  113. package/workflows/cm-plugin/templates/statusline/cm-plugin-statusline.sh +73 -0
  114. package/workflows.lock.json +4 -3
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env bash
2
+ # cm-plugin-workflow 自动更新:检测上游有新提交则拉取并安装
3
+ # 用法: cm-update.sh [--throttle] [--force] [--check]
4
+ # 分发源: cm-plugin-workflow 仓库 templates/auto-update/(本文件由 install.sh 装到 ~/.cm-plugin-workflow/,
5
+ # 更新自己时经 tmp+mv 原子替换,不会截断正在运行的实例)
6
+ set -uo pipefail
7
+
8
+ ROOT="$HOME/.cm-plugin-workflow"
9
+ REPO="$ROOT/repo"
10
+ DEST="${CLAUDE_HOME:-$HOME/.claude}"
11
+ MANIFEST="$ROOT/manifest.txt"
12
+ LOG="$ROOT/update.log"
13
+ STAMP="$ROOT/.last-check"
14
+ LOCK="$ROOT/.lock"
15
+ ANNOUNCE="$ROOT/pending-announce" # 下次 Claude Code 启动时播报,由 cm-announce.sh 消费
16
+ REMOTE="${CM_UPDATE_REMOTE:-git@github.com:kingxiaozhe/cm-plugin-workflow.git}" # 团队 fork 用环境变量覆盖,别改文件(会被自愈还原)
17
+ BRANCH="${CM_UPDATE_BRANCH:-main}"
18
+ THROTTLE_SECS=900 # --throttle 模式下两次 fetch 的最小间隔
19
+
20
+ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
21
+ export GIT_TERMINAL_PROMPT=0
22
+ export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15"
23
+
24
+ mkdir -p "$ROOT"
25
+
26
+ log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >>"$LOG"; }
27
+ notify() { osascript -e "display notification \"$2\" with title \"$1\"" >/dev/null 2>&1 || true; }
28
+ die() { log "✗ $*"; exit 1; }
29
+
30
+ THROTTLE=0; FORCE=0; CHECK_ONLY=0
31
+ for a in "$@"; do
32
+ case "$a" in
33
+ --throttle) THROTTLE=1 ;;
34
+ --force) FORCE=1 ;;
35
+ --check) CHECK_ONLY=1 ;;
36
+ esac
37
+ done
38
+
39
+ # ---- 单实例锁(mkdir 原子;超过 30 分钟视为陈旧锁)----
40
+ if ! mkdir "$LOCK" 2>/dev/null; then
41
+ if [ -n "$(find "$LOCK" -maxdepth 0 -mmin +30 2>/dev/null)" ]; then
42
+ rmdir "$LOCK" 2>/dev/null && mkdir "$LOCK" 2>/dev/null || exit 0
43
+ log "清除陈旧锁"
44
+ else
45
+ exit 0 # 另一个实例在跑
46
+ fi
47
+ fi
48
+ trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT
49
+
50
+ # ---- 限流:启动 hook 频繁触发时避免每次都连网 ----
51
+ if [ "$THROTTLE" = 1 ] && [ "$FORCE" = 0 ] && [ -f "$STAMP" ]; then
52
+ if [ -z "$(find "$STAMP" -maxdepth 0 -mmin +$((THROTTLE_SECS / 60)) 2>/dev/null)" ]; then
53
+ exit 0
54
+ fi
55
+ fi
56
+
57
+ # ---- 首次运行:clone ----
58
+ if [ ! -d "$REPO/.git" ]; then
59
+ log "首次克隆 $REMOTE"
60
+ rm -rf "$REPO"
61
+ git clone --quiet "$REMOTE" "$REPO" || die "克隆失败(检查 SSH 凭证)"
62
+ fi
63
+
64
+ cd "$REPO" || die "仓库目录不可用: $REPO"
65
+
66
+ if ! git fetch --quiet origin "$BRANCH" 2>>"$LOG"; then
67
+ log "fetch 失败(离线或凭证问题),本次跳过"
68
+ exit 0
69
+ fi
70
+ touch "$STAMP"
71
+
72
+ local_sha="$(git rev-parse HEAD 2>/dev/null)"
73
+ remote_sha="$(git rev-parse "origin/$BRANCH" 2>/dev/null)"
74
+
75
+ if [ "$CHECK_ONLY" = 1 ]; then
76
+ if [ "$local_sha" = "$remote_sha" ]; then
77
+ echo "已是最新: $(cat "$DEST/templates/cm-plugin-VERSION" 2>/dev/null) ($(git rev-parse --short HEAD))"
78
+ else
79
+ echo "有更新: $(git rev-parse --short HEAD) → $(git rev-parse --short "origin/$BRANCH")"
80
+ git log --oneline "HEAD..origin/$BRANCH" | sed 's/^/ /'
81
+ fi
82
+ exit 0
83
+ fi
84
+
85
+ # ---- 运行中工作流保护:/cm-plugin:ai 正在跑时不换安装文件(中途换版 = 混装风险)----
86
+ # 实证事故:2026-07-17 10:06 自动更新 v0.9.27 恰逢 diff-lens 运行中(10:04–10:17),
87
+ # 节点文件被中途替换。状态新鲜(30分钟内有写入)才算真在跑;僵死的 running 不拦更新。
88
+ PTR="$HOME/.claude/cm-plugin-current-specs"
89
+ if [ -f "$PTR" ]; then
90
+ ACTIVE_SPECS="$(head -1 "$PTR" 2>/dev/null | tr -d '[:space:]')"
91
+ ACTIVE_ST="$ACTIVE_SPECS/.cm-status.json"
92
+ if [ -n "$ACTIVE_SPECS" ] && [ -f "$ACTIVE_ST" ] \
93
+ && grep -q '"state":"running"' "$ACTIVE_ST" 2>/dev/null \
94
+ && [ -n "$(find "$ACTIVE_ST" -maxdepth 0 -mmin -30 2>/dev/null)" ]; then
95
+ log "工作流运行中($ACTIVE_SPECS),本轮跳过安装,避免中途换版"
96
+ exit 0
97
+ fi
98
+ fi
99
+
100
+ # ---- 源文件 ↔ 安装目标 的配对(必须与 install.sh 的拷贝映射一致)----
101
+ # 输出 "源路径<TAB>目标路径"。安装清单和完整性校验都基于它,只此一份映射。
102
+ TAB="$(printf '\t')"
103
+ build_pairs() {
104
+ local part
105
+ for part in commands skills agents; do
106
+ if [ -d "$REPO/$part" ]; then
107
+ ( cd "$REPO/$part" && find . -type f ) \
108
+ | sed "s|^\./\(.*\)$|$REPO/$part/\1$TAB$DEST/$part/\1|"
109
+ fi
110
+ done
111
+ if [ -d "$REPO/templates/rules" ]; then
112
+ ( cd "$REPO/templates/rules" && find . -type f ) \
113
+ | sed "s|^\./\(.*\)$|$REPO/templates/rules/\1$TAB$DEST/templates/cm-plugin-rules/\1|"
114
+ fi
115
+ if [ -f "$REPO/templates/arch-reference.md" ]; then
116
+ echo "$REPO/templates/arch-reference.md$TAB$DEST/templates/cm-plugin-arch-reference.md"
117
+ fi
118
+ if [ -d "$REPO/templates/scripts" ]; then
119
+ ( cd "$REPO/templates/scripts" && find . -type f ) \
120
+ | sed "s|^\./\(.*\)$|$REPO/templates/scripts/\1$TAB$DEST/templates/cm-plugin-scripts/\1|"
121
+ fi
122
+ if [ -d "$REPO/templates/e2e" ]; then
123
+ ( cd "$REPO/templates/e2e" && find . -type f ) \
124
+ | sed "s|^\./\(.*\)$|$REPO/templates/e2e/\1$TAB$DEST/templates/cm-plugin-e2e/\1|"
125
+ fi
126
+ if [ -d "$REPO/templates/dashboard" ]; then
127
+ ( cd "$REPO/templates/dashboard" && find . -type f ) \
128
+ | sed "s|^\./\(.*\)$|$REPO/templates/dashboard/\1$TAB$DEST/templates/cm-plugin-dashboard/\1|"
129
+ fi
130
+ if [ -f "$REPO/templates/statusline/cm-plugin-statusline.sh" ]; then
131
+ echo "$REPO/templates/statusline/cm-plugin-statusline.sh$TAB$DEST/templates/cm-plugin-statusline.sh"
132
+ fi
133
+ if [ -d "$REPO/templates/pixel" ]; then
134
+ ( cd "$REPO/templates/pixel" && find . -type f -not -path './dev/*' ) \
135
+ | sed "s|^\./\(.*\)$|$REPO/templates/pixel/\1$TAB$DEST/templates/cm-plugin-pixel/\1|"
136
+ fi
137
+ if [ -f "$REPO/templates/hooks/pre-commit-cm-task-check" ]; then
138
+ echo "$REPO/templates/hooks/pre-commit-cm-task-check$TAB$DEST/templates/cm-plugin-task-check-hook"
139
+ fi
140
+ if [ -f "$REPO/templates/refactor/cm-refactor-denies.json" ]; then
141
+ echo "$REPO/templates/refactor/cm-refactor-denies.json$TAB$DEST/templates/cm-plugin-refactor-denies.json"
142
+ fi
143
+ # 更新器自身也纳入校验与自愈(安装走 tmp+mv 原子替换;清理循环有 $DEST 前缀守卫,不会误删这里)
144
+ if [ -d "$REPO/templates/auto-update" ]; then
145
+ echo "$REPO/templates/auto-update/cm-update.sh$TAB$ROOT/cm-update.sh"
146
+ echo "$REPO/templates/auto-update/cm-announce.sh$TAB$ROOT/cm-announce.sh"
147
+ fi
148
+ # cm-VERSION 由 install.sh 从 VERSION 生成,内容与之逐字节相同,故可直接配对校验
149
+ echo "$REPO/VERSION$TAB$DEST/templates/cm-plugin-VERSION"
150
+ }
151
+
152
+ build_manifest() { build_pairs | cut -d"$TAB" -f2; }
153
+
154
+ # 校验已装文件是否仍与仓库一致;输出漂移清单(空 = 一切正常)
155
+ verify_install() {
156
+ build_pairs | while IFS="$TAB" read -r src dest; do
157
+ if [ ! -e "$dest" ]; then
158
+ echo "缺失 ${dest#$DEST/}"
159
+ elif ! cmp -s "$src" "$dest"; then
160
+ echo "被改动 ${dest#$DEST/}"
161
+ fi
162
+ done
163
+ }
164
+
165
+ # ---- 无新提交时:校验完整性,只有被改坏/误删才自愈重装 ----
166
+ REPAIR=0
167
+ if [ "$local_sha" = "$remote_sha" ] && [ -f "$MANIFEST" ] && [ "$FORCE" = 0 ]; then
168
+ drift="$(verify_install)"
169
+ if [ -z "$drift" ]; then
170
+ exit 0 # 无更新且文件完好,静默退出
171
+ fi
172
+ REPAIR=1
173
+ drift_n="$(echo "$drift" | wc -l | tr -d ' ')"
174
+ log "无新提交,但 $drift_n 个文件与仓库不一致,自愈重装:"
175
+ log "$(echo "$drift" | sed 's|^| |')"
176
+ fi
177
+
178
+ new_commits="$(git log --oneline "HEAD..origin/$BRANCH" 2>/dev/null | head -5)"
179
+ git reset --hard --quiet "origin/$BRANCH" || die "reset 到 origin/$BRANCH 失败"
180
+ git clean -qfd
181
+ VERSION="$(cat "$REPO/VERSION" 2>/dev/null || echo 未知)"
182
+ if [ "$REPAIR" = 0 ]; then
183
+ log "发现更新 ${local_sha:0:7} → ${remote_sha:0:7} (v$VERSION)"
184
+ [ -n "$new_commits" ] && log "$(echo "$new_commits" | sed 's/^/ /')"
185
+ fi
186
+
187
+ NEW_MANIFEST="$(mktemp)"
188
+ build_manifest | sort -u >"$NEW_MANIFEST"
189
+ [ -s "$NEW_MANIFEST" ] || { rm -f "$NEW_MANIFEST"; die "清单为空,中止安装(仓库结构异常?)"; }
190
+
191
+ # ---- 清理:只删“上一版装过、这一版没有”的文件,绝不碰清单外的东西 ----
192
+ removed=0
193
+ if [ -f "$MANIFEST" ]; then
194
+ while IFS= read -r f; do
195
+ [ -n "$f" ] || continue
196
+ case "$f" in "$DEST"/*) ;; *) continue ;; esac # 安全网:只删 ~/.claude 内
197
+ if [ -e "$f" ]; then
198
+ rm -f "$f" && removed=$((removed + 1)) && log " - 删除 ${f#$DEST/}"
199
+ fi
200
+ done < <(comm -23 <(sort -u "$MANIFEST") "$NEW_MANIFEST")
201
+ fi
202
+
203
+ # ---- 安装(yes 应答 install.sh 的覆盖确认,避免无人值守卡住)----
204
+ # 两处规避,别随手改掉:
205
+ # 1) 关 pipefail:install.sh 结束后 yes 会吃到 SIGPIPE(141),否则被误判为失败
206
+ # 2) LC_ALL=C:老版本 install.sh 里 "$part?" 这类写法,在 UTF-8 locale + bash 3.2 下
207
+ # 会把全角字符并进变量名,触发 set -u 报错。上游 v0.9.21 (eade178) 已改用 ${part},
208
+ # 这里保留纯属防御:万一回滚到旧提交,或哪天又写回这种形式,更新链路不至于断掉。
209
+ set +o pipefail
210
+ install_out="$(yes | LC_ALL=C LANG=C bash "$REPO/install.sh" 2>&1)"
211
+ install_rc=$?
212
+ set -o pipefail
213
+ if [ $install_rc -ne 0 ]; then
214
+ log "✗ install.sh 失败 (rc=$install_rc)"
215
+ log "$(echo "$install_out" | tail -15 | sed 's/^/ /')"
216
+ rm -f "$NEW_MANIFEST"
217
+ notify "cm-workflow 更新失败" "install.sh 报错,详见 ~/.cm-plugin-workflow/update.log"
218
+ exit 1
219
+ fi
220
+
221
+ mv "$NEW_MANIFEST" "$MANIFEST"
222
+
223
+ # 清掉清理后可能残留的空目录
224
+ find "$DEST/skills" "$DEST/commands" "$DEST/agents" "$DEST/templates" -type d -empty -delete 2>/dev/null || true
225
+
226
+ installed="$(cat "$DEST/templates/cm-plugin-VERSION" 2>/dev/null || echo "$VERSION")"
227
+ if [ "$REPAIR" = 1 ]; then
228
+ log "✓ 自愈完成,已恢复到 v$installed (${remote_sha:0:7})"
229
+ else
230
+ log "✓ 已更新到 v$installed (${remote_sha:0:7}),清理 $removed 个旧文件"
231
+ fi
232
+
233
+ # 播报走两条路,互不依赖:
234
+ # 1) 待播报文件 → 下次 Claude Code 启动时由 cm-announce.sh 报出来。这是主路径:
235
+ # 更新本来就要重开 Claude Code 才生效,在启动时说时机正好,且不看系统权限脸色。
236
+ # 2) macOS 通知 → 锦上添花。本机 osascript 未在通知中心注册,实测弹不出来,
237
+ # 所以绝不能依赖它;哪天授权了会自己开始工作。
238
+ if [ "$REPAIR" = 1 ]; then
239
+ {
240
+ echo "cm-plugin-workflow 检测到 $drift_n 个文件被改动或误删,已从仓库恢复(v${installed}):"
241
+ echo "$drift" | sed 's/^/ /'
242
+ } >"$ANNOUNCE"
243
+ notify "cm-plugin-workflow 已自愈 $drift_n 个文件" "恢复到 v$installed"
244
+ else
245
+ {
246
+ echo "cm-plugin-workflow 已更新到 v${installed}(${local_sha:0:7} → ${remote_sha:0:7})"
247
+ [ "$removed" -gt 0 ] && echo "清理了 $removed 个上游已删除的旧文件。"
248
+ [ -n "$new_commits" ] && { echo "新提交:"; echo "$new_commits" | sed 's/^/ /'; }
249
+ } >"$ANNOUNCE"
250
+ notify "cm-plugin-workflow 已更新到 v$installed" "$( [ "$removed" -gt 0 ] && echo "清理 $removed 个旧文件;" )重开 Claude Code 生效"
251
+ fi
@@ -0,0 +1,153 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>cm 工作流看板</title>
6
+ <style>
7
+ :root{--bg:#f7f8f6;--card:#fff;--ink:#1e2628;--muted:#55646a;--line:#dde3e1;
8
+ --accent:#0e7a6b;--soft:#e3f0ed;--warn:#9a6a14;--warnbg:#f5eddc;--drop:#b4231f;
9
+ --num:ui-monospace,"SF Mono",Menlo,monospace}
10
+ *{box-sizing:border-box}
11
+ body{margin:0;background:var(--bg);color:var(--ink);
12
+ font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;font-size:14px}
13
+ .wrap{max-width:980px;margin:0 auto;padding:28px 20px}
14
+ header{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:8px}
15
+ h1{font-size:20px;margin:0}
16
+ #meta{color:var(--muted);font-size:12px}
17
+ .tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}
18
+ .tile{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px 16px}
19
+ .tile b{display:block;font-size:22px;font-family:var(--num)}
20
+ .tile span{color:var(--muted);font-size:12px}
21
+ .feature{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 18px;margin-bottom:14px}
22
+ .fhead{display:flex;justify-content:space-between;align-items:center;gap:10px}
23
+ .fhead h2{font-size:15px;margin:0}
24
+ .bar{height:8px;background:var(--soft);border-radius:99px;margin:10px 0;overflow:hidden}
25
+ .bar i{display:block;height:100%;background:var(--accent);border-radius:99px;transition:width .5s}
26
+ .task{display:flex;gap:8px;padding:3px 0;font-size:13px;align-items:baseline}
27
+ .task .id{font-family:var(--num);color:var(--muted);min-width:52px}
28
+ .done{color:var(--muted)}.done .title{text-decoration:line-through}
29
+ .current{background:var(--warnbg);border-radius:6px;padding:3px 8px;margin:2px -8px}
30
+ .current .st{color:var(--warn);font-weight:700}
31
+ .dropped{color:var(--drop);opacity:.7}.dropped .title{text-decoration:line-through}
32
+ .pct{font-family:var(--num);font-size:12px;color:var(--muted)}
33
+ table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:8px;font-size:12.5px}
34
+ th,td{text-align:left;padding:7px 12px;border-bottom:1px solid var(--line)}
35
+ th{color:var(--muted);font-weight:600}
36
+ tr:last-child td{border-bottom:none}
37
+ td{font-family:var(--num)}
38
+ h3{font-size:14px;margin:24px 0 10px}
39
+ #lessons li{font-size:13px;color:var(--muted);margin:4px 0}
40
+ .empty{color:var(--muted);padding:30px;text-align:center}
41
+ /* 状态机横条 */
42
+ #pipeline{display:none;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 18px;margin:16px 0}
43
+ #statusline{font-size:13px;margin-bottom:10px}
44
+ #statusline b{color:var(--accent)}
45
+ .nodes{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
46
+ .node{padding:6px 10px;border-radius:6px;border:1px solid var(--line);font-size:12px;color:var(--muted);background:var(--bg);white-space:nowrap}
47
+ .node.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700;animation:pulse 1.2s ease-in-out infinite}
48
+ .node.passed{background:var(--soft);color:var(--accent);border-color:var(--soft)}
49
+ .arrowc{color:var(--line);font-size:11px}
50
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.55}}
51
+ #pausebanner{display:none;background:var(--warnbg);border:1px solid var(--warn);color:var(--warn);
52
+ border-radius:8px;padding:10px 16px;margin:12px 0;font-weight:700;font-size:14px}
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <div class="wrap">
57
+ <header>
58
+ <h1>cm 工作流看板 <span id="dot" style="color:var(--accent)">●</span></h1>
59
+ <div id="meta">加载中…</div>
60
+ </header>
61
+ <div id="pausebanner"></div>
62
+ <div id="pipeline">
63
+ <div id="statusline"></div>
64
+ <div class="nodes" id="nodes"></div>
65
+ </div>
66
+ <div class="tiles" id="tiles"></div>
67
+ <div id="features"><div class="empty">正在读取 specs…(确认由 serve.sh 启动且 specs 目录下有编号 feature)</div></div>
68
+ <h3>📊 METRICS(每任务度量)</h3>
69
+ <div id="metrics"><div class="empty">尚无 METRICS.md——执行到第一个 N5 后出现</div></div>
70
+ <h3>📝 LESSONS(经验库)</h3>
71
+ <ul id="lessons"><li>尚无记录</li></ul>
72
+ </div>
73
+ <script>
74
+ const $=id=>document.getElementById(id);
75
+ async function txt(p){try{const r=await fetch(p+'?t='+Date.now());return r.ok?await r.text():null}catch{return null}}
76
+
77
+ function parseTasks(md){
78
+ const out=[];if(!md)return out;
79
+ for(const m of md.matchAll(/^- \[( |x)\] (T-\d+):?\s*(.+)$/gm)){
80
+ const line=m[3];
81
+ out.push({id:m[2],done:m[1]==='x',dropped:/\[DROPPED/.test(line),changed:/\[CHANGED/.test(line),
82
+ title:line.replace(/`\[[^\]]*\]`?/g,'').replace(/~.*$/,'').trim().slice(0,60)});
83
+ }
84
+ return out;
85
+ }
86
+
87
+ const NODES=[['N1','初始化'],['N2','进Feature'],['N3','执行'],['N4','审查'],['N5','标记'],['N6','QA'],['N7','轮换'],['N8','收尾']];
88
+ async function renderStatus(){
89
+ const raw=await txt('./.cm-status.json');
90
+ const pl=$('pipeline'),pb=$('pausebanner');
91
+ if(!raw){pl.style.display='none';pb.style.display='none';return}
92
+ let s;try{s=JSON.parse(raw)}catch{return}
93
+ pl.style.display='block';
94
+ const curIdx=NODES.findIndex(n=>n[0]===s.node);
95
+ $('nodes').innerHTML=NODES.map((n,i)=>{
96
+ const cls=s.state==='done'?'passed':(i===curIdx?'active':(i<curIdx||['N3','N4','N5','N6','N7'].includes(s.node)&&i<2?'passed':''));
97
+ return `<span class="node ${cls}">${n[0]} ${n[1]}</span>`;
98
+ }).join('<span class="arrowc">▸</span>');
99
+ $('statusline').innerHTML=s.state==='done'
100
+ ?`🎉 全部完成 · ${s.at||''}`
101
+ :`当前: <b>${s.feature||'—'}</b> / <b>${s.task||'—'}</b> / <b>${s.node}</b> · ${s.detail||''} · ${s.at||''}`;
102
+ if(s.state==='paused_for_human'){pb.style.display='block';pb.textContent=`⏸ 等待人工介入: ${s.detail||'请查看会话'}`}
103
+ else pb.style.display='none';
104
+ }
105
+ async function load(){
106
+ renderStatus();
107
+ const idx=await txt('./');if(!idx){$('meta').textContent='无法读取 specs 目录';return}
108
+ const dirs=[...new Set([...idx.matchAll(/href="(\d+\.[^"\/]+)\//g)].map(m=>decodeURIComponent(m[1])))]
109
+ .sort((a,b)=>parseInt(a)-parseInt(b));
110
+ let total=0,done=0,fh='';
111
+ for(const d of dirs){
112
+ const tasks=parseTasks(await txt(`./${d}/tasks.md`));
113
+ const act=tasks.filter(t=>!t.dropped);
114
+ const dn=act.filter(t=>t.done).length;
115
+ total+=act.length;done+=dn;
116
+ const cur=act.find(t=>!t.done);
117
+ const pct=act.length?Math.round(dn/act.length*100):0;
118
+ fh+=`<div class="feature"><div class="fhead"><h2>📂 ${d}</h2><span class="pct">${dn}/${act.length} · ${pct}%</span></div>
119
+ <div class="bar"><i style="width:${pct}%"></i></div>`+
120
+ tasks.map(t=>{
121
+ const cls=t.dropped?'dropped':t.done?'done':(t===cur?'current':'');
122
+ const st=t.dropped?'🚫':t.done?'✅':(t===cur?'▶':'⬜');
123
+ return `<div class="task ${cls}"><span class="st">${st}</span><span class="id">${t.id}</span><span class="title">${t.title}${t.changed?' 🔁':''}</span></div>`;
124
+ }).join('')+`</div>`;
125
+ }
126
+ $('features').innerHTML=fh||'<div class="empty">未发现编号 feature 目录</div>';
127
+
128
+ const met=await txt('./METRICS.md');
129
+ let rows=[],human=0,firstPass=0;
130
+ if(met){
131
+ rows=[...met.matchAll(/^\|\s*(T-\d+)\s*\|(.+)$/gm)].map(m=>[m[1],...m[2].split('|').map(s=>s.trim()).slice(0,7)]);
132
+ for(const r of rows){human+=parseInt(r[7])||0;if((r[4]||'')==='1')firstPass++}
133
+ if(rows.length){
134
+ $('metrics').innerHTML='<table><tr><th>任务</th><th>Feature</th><th>开始</th><th>结束</th><th>审查轮</th><th>Codex</th><th>QA</th><th>人工介入</th></tr>'+
135
+ rows.map(r=>'<tr>'+r.map(c=>`<td>${c}</td>`).join('')+'</tr>').join('')+'</table>';
136
+ }
137
+ }
138
+ const lessons=await txt('./LESSONS.md');
139
+ const ls=lessons?[...lessons.matchAll(/^## (.+)$/gm)].map(m=>m[1]):[];
140
+ $('lessons').innerHTML=ls.length?ls.slice(-8).reverse().map(l=>`<li>${l}</li>`).join(''):'<li>尚无记录</li>';
141
+
142
+ $('tiles').innerHTML=`
143
+ <div class="tile"><b>${done}/${total}</b><span>任务完成</span></div>
144
+ <div class="tile"><b>${rows.length?(human/rows.length).toFixed(1):'—'}</b><span>人工介入/任务</span></div>
145
+ <div class="tile"><b>${rows.length?Math.round(firstPass/rows.length*100)+'%':'—'}</b><span>一次通过率</span></div>
146
+ <div class="tile"><b>${ls.length}</b><span>LESSONS 条目</span></div>`;
147
+ $('meta').textContent=`数据源: specs 落盘文件 · 只读 · ${new Date().toLocaleTimeString()} 刷新`;
148
+ $('dot').style.color='var(--accent)';
149
+ }
150
+ load();setInterval(load,2000);
151
+ </script>
152
+ </body>
153
+ </html>
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # cm 工作流可视化看板 · 纯只读,零侵入(只消费 specs 落盘文件)
3
+ # 用法: ./serve.sh /path/to/specs [端口,默认 8788]
4
+ set -euo pipefail
5
+ SPECS="${1:?用法: ./serve.sh /path/to/specs [端口]}"
6
+ PORT="${2:-8788}"
7
+ [ -d "$SPECS" ] || { echo "specs 目录不存在: $SPECS"; exit 1; }
8
+ cp "$(cd "$(dirname "$0")" && pwd)/dashboard.html" "$SPECS/.cm-dashboard.html"
9
+ echo "✓ 看板已启动: http://localhost:$PORT/.cm-dashboard.html (Ctrl+C 停止)"
10
+ cd "$SPECS" && exec python3 -m http.server "$PORT" --bind 127.0.0.1
@@ -0,0 +1,110 @@
1
+ // cm-plugin E2E harness —— MV3 扩展 Playwright 测试的久经考验底座。
2
+ // 这套代码封装的每一条都是 dogfood 实测踩出来的坑,新插件直接拷进 tests/e2e/ 用,别重踩。
3
+ // 依赖: @playwright/test。适配 WXT 构建产物(.output/{browser}-mv3)。
4
+ //
5
+ // 为什么要有它(五个坑,散文记不住,代码才靠得住):
6
+ // 1. 系统 Chrome(2026 版)默认屏蔽 --load-extension → 扩展静默不加载,SW 永不注册。只有 Chrome for Testing 能加载。
7
+ // 2. headless:false 依赖真实显示,在无头 shell / CI 里退化 flaky → 用 --headless=new(新无头支持 MV3 扩展)。
8
+ // 3. SW 从 onInstalled 注册后会很快 idle 停机 → 事后 serviceWorkers() 为空且 waitForEvent 等不到新事件,竞态。
9
+ // 4. 复用旧 SW 引用做 evaluate → "Worker was closed"。要用前唤醒取活引用。
10
+ // 5. CfT 路径随架构/OS 变(mac-arm64/mac-x64/linux),且多缓存版本要选最新。
11
+
12
+ import { chromium, type BrowserContext, type Worker } from '@playwright/test';
13
+ import path from 'node:path';
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+
17
+ /** 跨架构/OS 发现 playwright 缓存的 Chrome for Testing 二进制,按版本降序选最新。找不到返回 undefined(调用方回退 channel)。 */
18
+ export function resolveChromeForTesting(): string | undefined {
19
+ const cache =
20
+ process.env.PLAYWRIGHT_BROWSERS_PATH ||
21
+ (process.platform === 'darwin'
22
+ ? path.join(os.homedir(), 'Library/Caches/ms-playwright')
23
+ : process.platform === 'win32'
24
+ ? path.join(os.homedir(), 'AppData/Local/ms-playwright')
25
+ : path.join(os.homedir(), '.cache/ms-playwright'));
26
+ if (!fs.existsSync(cache)) return undefined;
27
+ const layouts = [
28
+ 'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
29
+ 'chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
30
+ 'chrome-linux/chrome',
31
+ 'chrome-win/chrome.exe',
32
+ ];
33
+ const dirs = fs
34
+ .readdirSync(cache)
35
+ .filter((d) => d.startsWith('chromium-'))
36
+ .sort((a, b) => Number(b.split('-')[1] || 0) - Number(a.split('-')[1] || 0)); // 版本降序
37
+ for (const dir of dirs) {
38
+ for (const layout of layouts) {
39
+ const p = path.join(cache, dir, layout);
40
+ if (fs.existsSync(p)) return p;
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ /** 用 CfT + --headless=new 加载解包扩展。extPath 指向构建产物目录(含 manifest.json)。 */
47
+ export async function launchExtension(extPath: string): Promise<BrowserContext> {
48
+ if (!fs.existsSync(path.join(extPath, 'manifest.json'))) {
49
+ throw new Error(`扩展产物缺失: ${extPath}/manifest.json —— 先跑 build`);
50
+ }
51
+ const executablePath = resolveChromeForTesting();
52
+ return chromium.launchPersistentContext('', {
53
+ ...(executablePath ? { executablePath } : { channel: 'chromium' }),
54
+ headless: true,
55
+ args: [
56
+ '--headless=new', // 新无头模式支持 MV3 扩展(旧 headless 不支持,headless:false 无头环境退化)
57
+ `--disable-extensions-except=${extPath}`,
58
+ `--load-extension=${extPath}`,
59
+ ],
60
+ });
61
+ }
62
+
63
+ /** 取 SW 引用:已存在 / 新注册事件 / 轮询已注册(含停机的) 三路取先到,彻底避开注册-停机竞态。 */
64
+ export function acquireServiceWorker(ctx: BrowserContext, timeoutMs = 12_000): Promise<Worker> {
65
+ const now = ctx.serviceWorkers();
66
+ if (now[0]) return Promise.resolve(now[0]);
67
+ const byEvent = ctx.waitForEvent('serviceworker');
68
+ const byPoll = (async () => {
69
+ const deadline = Date.now() + timeoutMs;
70
+ while (Date.now() < deadline) {
71
+ const [sw] = ctx.serviceWorkers();
72
+ if (sw) return sw;
73
+ await new Promise((r) => setTimeout(r, 200));
74
+ }
75
+ throw new Error('service worker 未在超时内注册');
76
+ })();
77
+ return Promise.race([byEvent, byPoll]);
78
+ }
79
+
80
+ /** 从 SW url 取扩展 id(32 位小写)。 */
81
+ export function extensionId(sw: Worker): string {
82
+ return new URL(sw.url()).host;
83
+ }
84
+
85
+ /**
86
+ * 唤醒并取"活的"SW,再做 sw.evaluate。SW 会 idle 停机,复用旧引用 evaluate 会 "Worker was closed"——
87
+ * 本函数先开一个扩展页触发 SW 启动,再取活引用。凡 sw.evaluate(读 storage 等)前都用它。
88
+ */
89
+ export async function wakeServiceWorker(ctx: BrowserContext, extId: string): Promise<Worker> {
90
+ const ping = await ctx.newPage();
91
+ await ping.goto(`chrome-extension://${extId}/manifest.json`).catch(() => {});
92
+ const sw = await acquireServiceWorker(ctx);
93
+ await ping.close();
94
+ return sw;
95
+ }
96
+
97
+ /**
98
+ * afterAll 里关闭 context。dogfood 实测:带扩展的 persistent context `ctx.close()` 会挂 30s 超时
99
+ * (扩展 SW/浏览器进程收尾卡住),afterAll 超时且进程残留累积 → 后续测试环境退化 flaky。
100
+ * 本函数先关所有页,再 close() 但用超时兜底(race)——超时就放手,残留进程由 playwright teardown/OS 收。
101
+ * 用法: `test.afterAll(() => closeExtension(context))`。
102
+ */
103
+ export async function closeExtension(ctx: BrowserContext | undefined, timeoutMs = 5000): Promise<void> {
104
+ if (!ctx) return;
105
+ for (const p of ctx.pages()) await p.close().catch(() => {});
106
+ await Promise.race([
107
+ ctx.close().catch(() => {}),
108
+ new Promise<void>((r) => setTimeout(r, timeoutMs)),
109
+ ]);
110
+ }
@@ -0,0 +1,51 @@
1
+ // cm-plugin E2E 冒烟模板 —— bootstrap T-005 拷进 tests/e2e/smoke.spec.ts,把 {占位} 换成本项目实际值。
2
+ // 用 extension-harness.ts 封装的健壮底座,不要自己写 launchPersistentContext / SW 获取(会踩全套坑)。
3
+ import { test, expect, type BrowserContext, type Worker } from '@playwright/test';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import {
8
+ launchExtension,
9
+ acquireServiceWorker,
10
+ extensionId,
11
+ closeExtension,
12
+ } from './extension-harness';
13
+
14
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
+ // {构建产物路径} —— WXT 默认 .output/chrome-mv3;按脚手架实际改
16
+ const EXT_PATH = path.resolve(here, '../../.output/chrome-mv3');
17
+
18
+ let context: BrowserContext;
19
+ let swReady: Promise<Worker>;
20
+
21
+ test.beforeAll(async () => {
22
+ context = await launchExtension(EXT_PATH);
23
+ swReady = acquireServiceWorker(context); // 立即捕获,避开注册-停机竞态
24
+ });
25
+
26
+ test.afterAll(async () => {
27
+ await closeExtension(context); // 带扩展的 context.close() 会挂,用 harness 的超时兜底版
28
+ });
29
+
30
+ test('service worker 注册且 manifest 名称正确', async () => {
31
+ const sw = await swReady;
32
+ expect(sw.url()).toContain('background.js'); // WXT 产物;其他脚手架按实际 SW 文件名
33
+ expect(extensionId(sw)).toMatch(/^[a-z]{32}$/);
34
+ // 标题声称"名称正确"须真断言——读被加载的产物 manifest(N5 运行观察闸:断言要对得上标题)
35
+ const manifest = JSON.parse(fs.readFileSync(path.join(EXT_PATH, 'manifest.json'), 'utf-8'));
36
+ expect(manifest.name).toBe('{扩展名}'); // ← 换成本项目 manifest.name
37
+ });
38
+
39
+ test('popup 能打开', async () => {
40
+ const extId = extensionId(await swReady);
41
+ const page = await context.newPage();
42
+ await page.goto(`chrome-extension://${extId}/popup.html`); // 无 popup 表面则删本用例
43
+ await expect(page.locator('body')).not.toBeEmpty(); // ← 换成本项目 popup 的稳定锚点(如品牌字标)
44
+ await page.close();
45
+ });
46
+
47
+ // ── 真实引擎集成(可选,强烈建议):起本地 http server 放一个假的目标场景,验证 content script 真处理 ──
48
+ // content script 的行为纯单测证不了,只有真实页面能证。参考:
49
+ // import http from 'node:http';
50
+ // 起 server 返回带目标元素的 fixture → page.goto → expect.poll 页面状态变化。
51
+ // 读 storage 用 wakeServiceWorker(ctx, extId) 取活 SW 再 sw.evaluate(避免 "Worker was closed")。
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # cm 工作流 · 任务标记双保险(灰度阶段启用,可选)
3
+ #
4
+ # 用途:防止 N5 漏标记——代码提交时校验 tasks.md 的勾选状态有同步更新。
5
+ # 安装(在代码仓库内):
6
+ # export CM_SPECS_DIR=/path/to/specs # specs 文件夹路径(含 N.feature 目录)
7
+ # cp templates/hooks/pre-commit-cm-task-check .git/hooks/pre-commit
8
+ # chmod +x .git/hooks/pre-commit
9
+ #
10
+ # 逻辑:本次提交包含源码变更、且 CM_SPECS_DIR 已设置时,检查 specs 下
11
+ # 是否存在"最近修改时间晚于上次提交"的 tasks.md——没有则给出警告。
12
+ # 默认仅警告不阻断;置 CM_TASK_CHECK_STRICT=1 时阻断提交。
13
+ set -uo pipefail
14
+
15
+ [ -z "${CM_SPECS_DIR:-}" ] && exit 0 # 未启用则直接放行
16
+ [ -d "$CM_SPECS_DIR" ] || exit 0
17
+
18
+ # 本次提交是否包含源码变更(排除纯文档)
19
+ changed=$(git diff --cached --name-only | grep -vE '\.(md|txt)$' || true)
20
+ [ -z "$changed" ] && exit 0
21
+
22
+ last_commit_ts=$(git log -1 --format=%ct 2>/dev/null || echo 0)
23
+ fresh_tasks=$(find "$CM_SPECS_DIR" -name tasks.md -newermt "@$last_commit_ts" 2>/dev/null || true)
24
+
25
+ if [ -z "$fresh_tasks" ]; then
26
+ echo "⚠ [cm] 本次提交含源码变更,但 $CM_SPECS_DIR 下没有新更新的 tasks.md。" >&2
27
+ echo " 若刚完成某个任务,请确认 N5 已将其标记为 [x](漏标会导致断点恢复时重复执行)。" >&2
28
+ if [ "${CM_TASK_CHECK_STRICT:-0}" = "1" ]; then
29
+ echo " 严格模式已开启:提交被阻断。确认无碍可用 git commit --no-verify 跳过。" >&2
30
+ exit 1
31
+ fi
32
+ fi
33
+ exit 0