cc-discipline 2.13.6 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ Without the hook, Claude would keep editing `auth.py` indefinitely — each fix
65
65
 
66
66
  ---
67
67
 
68
- **`pre-edit-guard.sh`** — Before a source edit, checks `docs/debug-log.md` for hypotheses still marked pending, flags unusually large diffs, and reminds you to register newly created scripts. These are context notes, not blocks.
68
+ **`pre-edit-guard.sh`** — Before a source edit, checks `docs/debug-log.md` for hypotheses still marked pending, and flags unusually large diffs. These are context notes, not blocks.
69
69
 
70
70
  > Downgraded from a hard block on 2026-07-30. Combined with the debugging rules it had become a trap: write three hypotheses into the log as the rules ask, and you were then barred from editing source until three were confirmed — including for unrelated planned work. The hypothesis discipline stays in the rules; only the enforcement was relaxed.
71
71
 
@@ -133,7 +133,7 @@ Auto-injected markdown in `.claude/rules/` — Claude sees them when operating o
133
133
  - Pre-edit checklist: understand the file, know the impact, fix root cause
134
134
  - Mole-whacking detection: recognize the pattern, stop, report
135
135
  - Phase discipline: stay in research/plan/implement, don't jump ahead
136
- - Multi-task gates: complete tasks in order, confirm each before moving on
136
+ - Multi-task discipline: work in order, report done only when it is done, stop and report on failure
137
137
  - Tech stack rules for Python, JS/TS, embedded, RTL, mobile
138
138
 
139
139
  ### Subagents (the auditors)
@@ -163,14 +163,14 @@ The installer is interactive — pick your tech stack, name your project, done.
163
163
 
164
164
  ```
165
165
  .claude/
166
- ├── rules/ # Auto-injected when Claude operates on matching files
166
+ ├── rules/ # Core rules load every session; stack rules load for matching files
167
167
  │ ├── 00-core-principles.md
168
168
  │ ├── 01-debugging.md
169
- │ ├── 02-before-edit.md
170
169
  │ ├── 03-context-mgmt.md
171
170
  │ ├── 04-no-mole-whacking.md
172
171
  │ ├── 05-phase-discipline.md
173
172
  │ ├── 06-multi-task.md
173
+ │ ├── 07-integrity.md
174
174
  │ └── stacks/ # Picked during install
175
175
  ├── hooks/ # Shell scripts, exit 2 = block operation
176
176
  │ ├── streak-breaker.sh
@@ -183,11 +183,14 @@ The installer is interactive — pick your tech stack, name your project, done.
183
183
  │ └── commit/SKILL.md # /commit: test → update docs → commit
184
184
  └── settings.json # Hook registration
185
185
  docs/
186
- ├── progress.md # Claude maintains this, read after compact
186
+ ├── progress.md # What happened — Claude maintains it, read after compact
187
+ ├── todo.md # What's still open — Now / Later, edited by you and Claude
187
188
  └── debug-log.md # Debug session tracking
188
189
  CLAUDE.md # Your project info (you fill this in)
189
190
  ```
190
191
 
192
+ **Three record files, three jobs.** `progress.md` records what happened — status, milestones, decisions — and only grows. `todo.md` holds only what is still open: *Now* for the next steps, *Later* for deferred work, each with a condition for when to revisit it; an item is deleted once it's done. `debug-log.md` tracks hypotheses while debugging. At every session start and after each compaction, a hook injects progress.md's Current Status and todo.md's *Now* list, and counts the *Later* items.
193
+
191
194
  ## Customization
192
195
 
193
196
  **Adjust hook strictness:**
@@ -204,8 +207,8 @@ STOP_THRESHOLD=10 # Hard block after N edits
204
207
  ```bash
205
208
  cat > .claude/rules/my-rule.md << 'EOF'
206
209
  ---
207
- globs: "src/api/**/*"
208
- description: "API layer rules"
210
+ paths:
211
+ - "src/api/**/*"
209
212
  ---
210
213
  - All API changes must be backwards-compatible
211
214
  - New endpoints need OpenAPI spec updates
@@ -226,7 +229,7 @@ See [Claude Code hooks docs](https://docs.anthropic.com/en/docs/claude-code/hook
226
229
  No. The hooks are the real enforcement — they're shell scripts that physically block operations. The rules are supplementary structure.
227
230
 
228
231
  **Does it slow things down?**
229
- No. Hooks are lightweight shell scripts, typically <100ms. Rules add ~8KB to context (~2%).
232
+ Somewhat. Measured on Windows with Git Bash, the hooks on the edit path add about 150 ms to an edit of a docs or config file and about 650 ms to an edit of a source file. Rules add about 15 KB to context (under 2% of a 200K window), and stack rules load only when Claude reads a matching file.
230
233
 
231
234
  **Should I commit `.claude/` to git?**
232
235
  Yes. Team members get the same guardrails. Hook paths use `$CLAUDE_PROJECT_DIR`, so they work across machines.
package/README.zh-CN.md CHANGED
@@ -25,7 +25,6 @@
25
25
  │ Layer 1: Rules(自动注入) │
26
26
  │ core-principles — 核心原则 │
27
27
  │ debugging — 调试纪律 │
28
- │ before-edit — 修改前检查 │
29
28
  │ context-mgmt — 上下文管理 │
30
29
  │ no-mole-whacking — 反打地鼠 │
31
30
  │ stacks/* — 技术栈特有规则 │
@@ -73,9 +72,11 @@ your-project/
73
72
  │ ├── rules/
74
73
  │ │ ├── 00-core-principles.md # 核心工作原则
75
74
  │ │ ├── 01-debugging.md # 调试纪律
76
- │ │ ├── 02-before-edit.md # 修改前检查
77
75
  │ │ ├── 03-context-mgmt.md # 上下文管理
78
76
  │ │ ├── 04-no-mole-whacking.md # 反打地鼠
77
+ │ │ ├── 05-phase-discipline.md # 阶段纪律
78
+ │ │ ├── 06-multi-task.md # 多任务纪律
79
+ │ │ ├── 07-integrity.md # 诚信纪律
79
80
  │ │ └── stacks/ # 技术栈特有规则
80
81
  │ │ ├── rtl.md # RTL / IC 设计
81
82
  │ │ ├── embedded.md # 嵌入式 C/C++
@@ -92,12 +93,15 @@ your-project/
92
93
  │ └── skills/
93
94
  │ └── commit/SKILL.md # /commit 智能提交
94
95
  ├── docs/
95
- │ ├── progress.md # 进度记录(Claude 维护)
96
+ │ ├── progress.md # 进度记录:已经发生的事(Claude 维护)
97
+ │ ├── todo.md # 待办:Now / Later(你和 Claude 都可以改)
96
98
  │ └── debug-log.md # 调试日志(Claude 维护)
97
99
  └── ~/.claude/
98
100
  └── CLAUDE.md # 全局通用纪律(可选)
99
101
  ```
100
102
 
103
+ **三个记录文件,各管一件事**:`progress.md` 记录已经发生的事(状态、里程碑、决策),只增不减;`todo.md` 只放还没做的事——*Now* 是接下来的具体步骤,*Later* 是推迟的工作,每条都写明什么时候、什么条件下再看,做完就删;`debug-log.md` 记录调试中的假设。每次开会话和每次压缩上下文之后,hook 会注入 progress.md 的 Current Status 和 todo.md 的 *Now* 列表,并报出 *Later* 的条数。
104
+
101
105
  ## 各层详解
102
106
 
103
107
  ### Rules(自动注入)
@@ -109,10 +113,7 @@ Claude 无法选择性忽略这些规则——它们在操作匹配文件时会
109
113
  - `00-core-principles.md` — 先理解再动手、不锁定第一解释、连续3次失败必须停下
110
114
  - `01-debugging.md` — 四阶段调试流程(收集→假设→验证→修复)
111
115
  - `03-context-mgmt.md` — 主动检查点、调研隔离、compact 策略
112
- - `04-no-mole-whacking.md` — 打地鼠检测和汇报模板
113
-
114
- **源码规则**(编辑 src/ 等目录时生效):
115
- - `02-before-edit.md` — 修改前的 5 项强制检查
116
+ - `04-no-mole-whacking.md` — 打地鼠检测
116
117
 
117
118
  **技术栈规则**(按文件类型触发):
118
119
  - `.v/.sv/.vhd` → RTL 规则(时序意识、CDC 检查、综合/仿真区分)
@@ -251,8 +252,8 @@ Claude: 先别急着改。测试期望 200 但拿到了 401。
251
252
  # Create a new rule
252
253
  cat > .claude/rules/my-rule.md << 'EOF'
253
254
  ---
254
- globs: "src/api/**/*"
255
- description: "API 层特有规则"
255
+ paths:
256
+ - "src/api/**/*"
256
257
  ---
257
258
  ## API 修改规则
258
259
  - 所有 API 变更必须向后兼容
@@ -289,7 +290,7 @@ EOF
289
290
  A: CLAUDE.md 在对话开始时读取一次。Rules 是按文件路径匹配自动注入的——当 Claude 操作匹配 glob 的文件时,对应规则会自动出现在它的上下文中。Rules 更可靠,因为它们不依赖 Claude "记得去看"。
290
291
 
291
292
  **Q: Hooks 会影响性能吗?**
292
- A: 几乎不会。它们是轻量的 shell 脚本,执行时间通常 <100ms。
293
+ A: 有一些。在 Windows 的 Git Bash 下实测:编辑文档或配置文件时,每次编辑约增加 150 ms;编辑源码文件约增加 650 ms。规则约占 15 KB 上下文(不到 200K 窗口的 2%),技术栈规则只在读到对应文件时才加载。
293
294
 
294
295
  **Q: 可以把 .claude/ 提交到 git 吗?**
295
296
  A: 强烈建议提交。这样团队成员都能共享同一套纪律。注意 `.claude/settings.json` 中的 hooks 路径是相对路径,团队成员不需要额外配置。
package/bin/cli.js CHANGED
@@ -144,4 +144,9 @@ const result = spawnSync(bash, [unixScript, ...scriptArgs], {
144
144
  cwd: process.cwd(),
145
145
  });
146
146
 
147
- process.exit(result.status || 0);
147
+ if (result.error) {
148
+ console.error(`cc-discipline: could not start bash (${result.error.code || result.error.message})`);
149
+ process.exit(1);
150
+ }
151
+ // status is null when bash was killed by a signal: a failure, not a success.
152
+ process.exit(result.status === null ? 1 : result.status);
package/global/CLAUDE.md CHANGED
@@ -39,7 +39,7 @@ Don't skip the first three steps and jump straight to the fourth.
39
39
 
40
40
  ## Context Hygiene
41
41
 
42
- - Use subagents for research. Don't read large volumes of files in the main conversation.
43
- - Update docs/progress.md after completing each milestone.
42
+ - Delegate to subagents only for sizeable, independent work that would otherwise flood the conversation; do small reads, edits and checks yourself.
43
+ - Update docs/progress.md after completing each milestone; keep open work in docs/todo.md.
44
44
  - Don't proactively warn about context running low or suggest compacting. The system auto-compacts at 0% — you cannot see the percentage. Keep progress.md updated instead.
45
- - First thing after compact: read `docs/progress.md` and `docs/debug-log.md`.
45
+ - First thing after compact: read `docs/progress.md`, `docs/todo.md` and `docs/debug-log.md`.
package/init.sh CHANGED
@@ -191,6 +191,18 @@ fi
191
191
  # Parse choices into array
192
192
  IFS=' ' read -ra STACKS <<< "$STACK_CHOICES"
193
193
 
194
+ # Stack rules are framework files, like the core rules, so anything other than a
195
+ # fresh install refreshes every stack already installed, whatever was chosen
196
+ # above. Without this an `upgrade` (which passes --auto, and --auto defaults the
197
+ # stack to 7, meaning none) or an interactive "Enter to keep current" copied no
198
+ # stack file at all, so changes to stack rules never reached an existing install.
199
+ if [ "$INSTALL_MODE" != "fresh" ]; then
200
+ detect_installed_stacks
201
+ for s in $INSTALLED_STACKS; do
202
+ case " ${STACKS[*]} " in *" $s "*) ;; *) STACKS+=("$s") ;; esac
203
+ done
204
+ fi
205
+
194
206
  # ─── Project name ───
195
207
  if [ -n "$ARG_NAME" ]; then
196
208
  PROJECT_NAME="$ARG_NAME"
@@ -237,13 +249,24 @@ mkdir -p docs
237
249
  echo -e "${GREEN}Installing core rules...${NC}"
238
250
  cp "$SCRIPT_DIR/templates/.claude/rules/00-core-principles.md" .claude/rules/
239
251
  cp "$SCRIPT_DIR/templates/.claude/rules/01-debugging.md" .claude/rules/
240
- cp "$SCRIPT_DIR/templates/.claude/rules/02-before-edit.md" .claude/rules/
241
252
  cp "$SCRIPT_DIR/templates/.claude/rules/03-context-mgmt.md" .claude/rules/
242
253
  cp "$SCRIPT_DIR/templates/.claude/rules/04-no-mole-whacking.md" .claude/rules/
243
254
  cp "$SCRIPT_DIR/templates/.claude/rules/05-phase-discipline.md" .claude/rules/
244
255
  cp "$SCRIPT_DIR/templates/.claude/rules/06-multi-task.md" .claude/rules/
245
256
  cp "$SCRIPT_DIR/templates/.claude/rules/07-integrity.md" .claude/rules/
246
257
 
258
+ # Retired core rules. Core rules are framework files that every upgrade overwrites,
259
+ # so a rule dropped from templates/ has to be deleted here as well — otherwise the
260
+ # old copy stays in every existing install indefinitely. The pre-upgrade backup in
261
+ # .claude/.backup-* still holds it.
262
+ # 02-before-edit.md — retired 2026-09-23: every item duplicated another rule or a
263
+ # trained default, and it loaded unconditionally although written as a per-file check.
264
+ # Only in upgrade mode: in append mode the project has never had cc-discipline,
265
+ # so a file by that name is the user's own.
266
+ if [ "$INSTALL_MODE" = "upgrade" ]; then
267
+ rm -f .claude/rules/02-before-edit.md
268
+ fi
269
+
247
270
  # ─── Copy stack-specific rules based on selection ───
248
271
  if [ ${#STACKS[@]} -gt 0 ] && [ -n "${STACKS[0]}" ]; then
249
272
  echo -e "${GREEN}Installing stack rules...${NC}"
@@ -281,15 +304,138 @@ if [ ${#STACKS[@]} -gt 0 ] && [ -n "${STACKS[0]}" ]; then
281
304
  done
282
305
  fi
283
306
 
307
+ # ─── Content hashing (skills and hooks) ───
308
+ # Two properties matter here, and both were learned the hard way.
309
+ #
310
+ # 1. Record WHICH algorithm produced a digest. The fallback below resolves
311
+ # differently depending on what is installed, so a manifest holding a bare
312
+ # digest silently mis-compares the moment tool availability changes — every
313
+ # hash mismatches, every retired skill looks user-modified, and the
314
+ # retirement pass does nothing. (fixed 2026-08-28, before 2.13.0 shipped.)
315
+ #
316
+ # 2. Hash line-ending-NORMALIZED content, not raw bytes. Projects that commit
317
+ # .claude/ and run with core.autocrlf=true and no .gitattributes get their
318
+ # SKILL.md rewritten to CRLF by git on checkout. Byte-exact comparison then
319
+ # reports every skill as modified, with the same two consequences as above
320
+ # plus a .new file dropped beside every skill on every upgrade. Measured on
321
+ # one install: the file differed from its manifest entry by exactly 44 bytes
322
+ # across 44 lines, and matched perfectly once normalized. (fixed 2026-08-28)
323
+ #
324
+ # sha256sum and `shasum -a 256` produce identical digests, so both label sha256.
325
+
326
+ _cc_digest_with() { # $1=tool $2=file $3=norm|raw -> bare digest
327
+ if [ "$3" = "norm" ]; then
328
+ case "$1" in
329
+ sha256sum) sed 's/\r$//' "$2" | sha256sum | cut -d' ' -f1 ;;
330
+ shasum) sed 's/\r$//' "$2" | shasum -a 256 | cut -d' ' -f1 ;;
331
+ md5sum) sed 's/\r$//' "$2" | md5sum | cut -d' ' -f1 ;;
332
+ md5) sed 's/\r$//' "$2" | md5 -q ;;
333
+ cksum) sed 's/\r$//' "$2" | cksum | cut -d' ' -f1 ;;
334
+ esac
335
+ else
336
+ case "$1" in
337
+ sha256sum) sha256sum "$2" | cut -d' ' -f1 ;;
338
+ shasum) shasum -a 256 "$2" | cut -d' ' -f1 ;;
339
+ md5sum) md5sum "$2" | cut -d' ' -f1 ;;
340
+ md5) md5 -q "$2" ;;
341
+ cksum) cksum "$2" | cut -d' ' -f1 ;;
342
+ esac
343
+ fi
344
+ }
345
+
346
+ _cc_algo() {
347
+ for _a in sha256sum shasum md5sum md5 cksum; do
348
+ command -v "$_a" >/dev/null 2>&1 && { echo "$_a"; return; }
349
+ done
350
+ echo cksum
351
+ }
352
+
353
+ _cc_label() {
354
+ case "$1" in sha256sum|shasum) echo sha256 ;; md5sum|md5) echo md5 ;; *) echo cksum ;; esac
355
+ }
356
+
357
+ # What gets written into the manifest: normalized, algorithm-labelled.
358
+ _cc_hash() {
359
+ _a=$(_cc_algo)
360
+ echo "$(_cc_label "$_a"):$(_cc_digest_with "$_a" "$1" norm)"
361
+ }
362
+
363
+ # True when manifest entry $2 still describes file $1. Accepts, in order of how
364
+ # the entry was most likely written:
365
+ # - "<algo>:<digest>" from 2.13.1+, normalized
366
+ # - "<algo>:<digest>" from 2.13.0, byte-exact
367
+ # - a bare digest from 2.12.2/2.12.3, algorithm and normalization both unknown
368
+ # Anything that matches under any of those readings counts as pristine. Being
369
+ # permissive here is the safe direction: a false "modified" freezes a skill in
370
+ # place forever and buries the user in .new files, while a false "pristine" can
371
+ # only overwrite a file that is byte-identical to what we would install anyway.
372
+ _cc_hash_matches() {
373
+ _f="$1"; _rec="$2"
374
+ # The manifest itself has no file extension, so .gitattributes rules keyed on
375
+ # *.sh/*.md/*.json never cover it and git may hand it back with CRLF. A
376
+ # trailing CR then rides along in the recorded digest and every comparison
377
+ # fails — the same bug this function exists to fix, one level up. Strip it.
378
+ _rec=${_rec%$'\r'}
379
+ [ -n "$_rec" ] || return 1
380
+ [ -f "$_f" ] || return 1
381
+ case "$_rec" in
382
+ *:*)
383
+ _rl=${_rec%%:*}; _rd=${_rec#*:}
384
+ case "$_rl" in
385
+ sha256) _cands="sha256sum shasum" ;;
386
+ md5) _cands="md5sum md5" ;;
387
+ *) _cands="cksum" ;;
388
+ esac
389
+ for _al in $_cands; do
390
+ command -v "$_al" >/dev/null 2>&1 || continue
391
+ [ "$(_cc_digest_with "$_al" "$_f" norm)" = "$_rd" ] && return 0
392
+ [ "$(_cc_digest_with "$_al" "$_f" raw)" = "$_rd" ] && return 0
393
+ done
394
+ return 1
395
+ ;;
396
+ esac
397
+ for _al in sha256sum shasum md5sum md5 cksum; do
398
+ command -v "$_al" >/dev/null 2>&1 || continue
399
+ [ "$(_cc_digest_with "$_al" "$_f" norm)" = "$_rec" ] && return 0
400
+ [ "$(_cc_digest_with "$_al" "$_f" raw)" = "$_rec" ] && return 0
401
+ done
402
+ return 1
403
+ }
404
+
284
405
  # ─── Install hooks ───
406
+ # Framework hooks are always replaced. They are enforcement code, and a
407
+ # customized copy left in place would keep whatever holes the new version
408
+ # closes: a stale git-guard keeps every bypass fixed since. But a local change
409
+ # must not vanish silently, as HUB_Rev1_FW's session-start did in the 2.14.0
410
+ # rollout. A hook counts as locally modified when it matches none of: the
411
+ # template being installed, the hash recorded when we last installed it, or any
412
+ # template version shipped up to 2.14.0 (lib/hook-hashes, for installs that
413
+ # predate the hooks manifest). The backup made above keeps the old copy, and
414
+ # the end of this script lists what was replaced.
285
415
  echo -e "${GREEN}Installing hooks...${NC}"
286
- cp "$SCRIPT_DIR/templates/.claude/hooks/pre-edit-guard.sh" .claude/hooks/
287
- cp "$SCRIPT_DIR/templates/.claude/hooks/post-error-remind.sh" .claude/hooks/
288
- cp "$SCRIPT_DIR/templates/.claude/hooks/streak-breaker.sh" .claude/hooks/
289
- cp "$SCRIPT_DIR/templates/.claude/hooks/session-start.sh" .claude/hooks/
290
- cp "$SCRIPT_DIR/templates/.claude/hooks/phase-gate.sh" .claude/hooks/
291
- cp "$SCRIPT_DIR/templates/.claude/hooks/git-guard.sh" .claude/hooks/
292
- cp "$SCRIPT_DIR/templates/.claude/hooks/action-counter.sh" .claude/hooks/
416
+ HOOKS_MANIFEST=".claude/.cc-discipline-hooks.manifest"
417
+ KNOWN_HOOK_HASHES="$SCRIPT_DIR/lib/hook-hashes"
418
+ _cc_hook_pristine() { # $1=installed file $2=template $3=hook file name
419
+ _cc_hash_matches "$1" "$(_cc_hash "$2")" && return 0
420
+ _hrec=$(grep "^$3 " "$HOOKS_MANIFEST" 2>/dev/null | head -1 | cut -d' ' -f2)
421
+ [ -n "$_hrec" ] && _cc_hash_matches "$1" "$_hrec" && return 0
422
+ [ -f "$KNOWN_HOOK_HASHES" ] || return 1
423
+ _ha=$(_cc_algo)
424
+ [ "$(_cc_label "$_ha")" = sha256 ] || return 1
425
+ # CRs stripped first: a CRLF checkout of this list must not flag every hook
426
+ tr -d '\r' < "$KNOWN_HOOK_HASHES" | grep -q "^$3 sha256:$(_cc_digest_with "$_ha" "$1" norm)\$"
427
+ }
428
+ MODIFIED_HOOKS=""
429
+ NEW_HOOKS_MANIFEST=$(mktemp 2>/dev/null || echo ".claude/.hooks-manifest.tmp")
430
+ for hook in pre-edit-guard post-error-remind streak-breaker session-start phase-gate git-guard action-counter; do
431
+ if [ -f ".claude/hooks/$hook.sh" ] && \
432
+ ! _cc_hook_pristine ".claude/hooks/$hook.sh" "$SCRIPT_DIR/templates/.claude/hooks/$hook.sh" "$hook.sh"; then
433
+ MODIFIED_HOOKS="$MODIFIED_HOOKS $hook.sh"
434
+ fi
435
+ cp "$SCRIPT_DIR/templates/.claude/hooks/$hook.sh" .claude/hooks/
436
+ echo "$hook.sh $(_cc_hash ".claude/hooks/$hook.sh")" >> "$NEW_HOOKS_MANIFEST"
437
+ done
438
+ mv "$NEW_HOOKS_MANIFEST" "$HOOKS_MANIFEST"
293
439
  chmod +x .claude/hooks/*.sh
294
440
 
295
441
  # ─── Check jq availability ───
@@ -321,9 +467,13 @@ else
321
467
  TEMP_SETTINGS=$(mktemp)
322
468
  MERGE_OK=false
323
469
 
470
+ # is_cc must name every hook this framework registers. It once omitted
471
+ # git-guard, so each jq upgrade kept the old git-guard entry and appended
472
+ # another; one install had accumulated eleven. With git-guard listed, the
473
+ # next upgrade filters every old entry out and adds exactly one back.
324
474
  if jq -s '
325
475
  .[0] as $e | .[1] as $t |
326
- def is_cc: .hooks | any(.command | test("pre-edit-guard|streak-breaker|post-error-remind|session-start|phase-gate|action-counter"));
476
+ def is_cc: .hooks | any(.command | test("pre-edit-guard|streak-breaker|post-error-remind|session-start|phase-gate|action-counter|git-guard"));
327
477
  def merge($ev): (($e.hooks[$ev] // []) | map(select(is_cc | not))) + ($t.hooks[$ev] // []);
328
478
  $e * {
329
479
  hooks: (($e.hooks // {}) + {
@@ -403,104 +553,6 @@ KEPT_RETIRED=""
403
553
  OLD_MANIFEST=$(mktemp 2>/dev/null || echo ".claude/.skills-manifest.old")
404
554
  [ -f "$SKILLS_MANIFEST" ] && cp "$SKILLS_MANIFEST" "$OLD_MANIFEST"
405
555
 
406
- # ─── Skill content hashing ───
407
- # Two properties matter here, and both were learned the hard way.
408
- #
409
- # 1. Record WHICH algorithm produced a digest. The fallback below resolves
410
- # differently depending on what is installed, so a manifest holding a bare
411
- # digest silently mis-compares the moment tool availability changes — every
412
- # hash mismatches, every retired skill looks user-modified, and the
413
- # retirement pass does nothing. (fixed 2026-08-28, before 2.13.0 shipped.)
414
- #
415
- # 2. Hash line-ending-NORMALIZED content, not raw bytes. Projects that commit
416
- # .claude/ and run with core.autocrlf=true and no .gitattributes get their
417
- # SKILL.md rewritten to CRLF by git on checkout. Byte-exact comparison then
418
- # reports every skill as modified, with the same two consequences as above
419
- # plus a .new file dropped beside every skill on every upgrade. Measured on
420
- # one install: the file differed from its manifest entry by exactly 44 bytes
421
- # across 44 lines, and matched perfectly once normalized. (fixed 2026-08-28)
422
- #
423
- # sha256sum and `shasum -a 256` produce identical digests, so both label sha256.
424
-
425
- _cc_digest_with() { # $1=tool $2=file $3=norm|raw -> bare digest
426
- if [ "$3" = "norm" ]; then
427
- case "$1" in
428
- sha256sum) sed 's/\r$//' "$2" | sha256sum | cut -d' ' -f1 ;;
429
- shasum) sed 's/\r$//' "$2" | shasum -a 256 | cut -d' ' -f1 ;;
430
- md5sum) sed 's/\r$//' "$2" | md5sum | cut -d' ' -f1 ;;
431
- md5) sed 's/\r$//' "$2" | md5 -q ;;
432
- cksum) sed 's/\r$//' "$2" | cksum | cut -d' ' -f1 ;;
433
- esac
434
- else
435
- case "$1" in
436
- sha256sum) sha256sum "$2" | cut -d' ' -f1 ;;
437
- shasum) shasum -a 256 "$2" | cut -d' ' -f1 ;;
438
- md5sum) md5sum "$2" | cut -d' ' -f1 ;;
439
- md5) md5 -q "$2" ;;
440
- cksum) cksum "$2" | cut -d' ' -f1 ;;
441
- esac
442
- fi
443
- }
444
-
445
- _cc_algo() {
446
- for _a in sha256sum shasum md5sum md5 cksum; do
447
- command -v "$_a" >/dev/null 2>&1 && { echo "$_a"; return; }
448
- done
449
- echo cksum
450
- }
451
-
452
- _cc_label() {
453
- case "$1" in sha256sum|shasum) echo sha256 ;; md5sum|md5) echo md5 ;; *) echo cksum ;; esac
454
- }
455
-
456
- # What gets written into the manifest: normalized, algorithm-labelled.
457
- _cc_hash() {
458
- _a=$(_cc_algo)
459
- echo "$(_cc_label "$_a"):$(_cc_digest_with "$_a" "$1" norm)"
460
- }
461
-
462
- # True when manifest entry $2 still describes file $1. Accepts, in order of how
463
- # the entry was most likely written:
464
- # - "<algo>:<digest>" from 2.13.1+, normalized
465
- # - "<algo>:<digest>" from 2.13.0, byte-exact
466
- # - a bare digest from 2.12.2/2.12.3, algorithm and normalization both unknown
467
- # Anything that matches under any of those readings counts as pristine. Being
468
- # permissive here is the safe direction: a false "modified" freezes a skill in
469
- # place forever and buries the user in .new files, while a false "pristine" can
470
- # only overwrite a file that is byte-identical to what we would install anyway.
471
- _cc_hash_matches() {
472
- _f="$1"; _rec="$2"
473
- # The manifest itself has no file extension, so .gitattributes rules keyed on
474
- # *.sh/*.md/*.json never cover it and git may hand it back with CRLF. A
475
- # trailing CR then rides along in the recorded digest and every comparison
476
- # fails — the same bug this function exists to fix, one level up. Strip it.
477
- _rec=${_rec%$'\r'}
478
- [ -n "$_rec" ] || return 1
479
- [ -f "$_f" ] || return 1
480
- case "$_rec" in
481
- *:*)
482
- _rl=${_rec%%:*}; _rd=${_rec#*:}
483
- case "$_rl" in
484
- sha256) _cands="sha256sum shasum" ;;
485
- md5) _cands="md5sum md5" ;;
486
- *) _cands="cksum" ;;
487
- esac
488
- for _al in $_cands; do
489
- command -v "$_al" >/dev/null 2>&1 || continue
490
- [ "$(_cc_digest_with "$_al" "$_f" norm)" = "$_rd" ] && return 0
491
- [ "$(_cc_digest_with "$_al" "$_f" raw)" = "$_rd" ] && return 0
492
- done
493
- return 1
494
- ;;
495
- esac
496
- for _al in sha256sum shasum md5sum md5 cksum; do
497
- command -v "$_al" >/dev/null 2>&1 || continue
498
- [ "$(_cc_digest_with "$_al" "$_f" norm)" = "$_rec" ] && return 0
499
- [ "$(_cc_digest_with "$_al" "$_f" raw)" = "$_rec" ] && return 0
500
- done
501
- return 1
502
- }
503
-
504
556
  for skill_dir in "$SCRIPT_DIR"/templates/.claude/skills/*/; do
505
557
  [ -d "$skill_dir" ] || continue
506
558
  skill_name=$(basename "$skill_dir")
@@ -640,6 +692,9 @@ fi
640
692
  if [ ! -f "docs/debug-log.md" ]; then
641
693
  cp "$SCRIPT_DIR/templates/docs/debug-log.md" docs/
642
694
  fi
695
+ if [ ! -f "docs/todo.md" ]; then
696
+ cp "$SCRIPT_DIR/templates/docs/todo.md" docs/
697
+ fi
643
698
 
644
699
  # ─── Install auto memory (symlink to .claude/memory/) ───
645
700
  echo -e "${GREEN}Installing auto memory...${NC}"
@@ -762,6 +817,23 @@ echo -e "${CYAN}║ Setup complete! ║${NC}"
762
817
  echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}"
763
818
  echo ""
764
819
 
820
+ # Anything the user has to act on goes here, where it cannot scroll past
821
+ # unseen. Rollouts that capture this output must surface these lines too.
822
+ if [ -n "$MODIFIED_HOOKS" ] || [ -n "$PRESERVED_SKILLS" ]; then
823
+ echo -e "${YELLOW}Needs your attention:${NC}"
824
+ if [ -n "$MODIFIED_HOOKS" ]; then
825
+ echo -e " ${YELLOW}Framework hooks with local changes were REPLACED:${NC}$MODIFIED_HOOKS"
826
+ echo " Your versions are in ${BACKUP_DIR}/hooks/"
827
+ echo " Framework hooks are overwritten on every upgrade. Keep project-specific"
828
+ echo " behaviour in a hook of your own: its own file, registered in settings.json."
829
+ fi
830
+ if [ -n "$PRESERVED_SKILLS" ]; then
831
+ echo -e " ${YELLOW}Skills you edited were kept; the new templates wait beside them:${NC}${PRESERVED_SKILLS}"
832
+ echo " Compare: diff .claude/skills/<name>/SKILL.md{,.new}"
833
+ fi
834
+ echo ""
835
+ fi
836
+
765
837
  if [ "$INSTALL_MODE" = "fresh" ]; then
766
838
  echo -e "Created files:"
767
839
  echo -e " ${GREEN}CLAUDE.md${NC} ← Project rules (fill in [TODO] sections)"
@@ -771,6 +843,7 @@ if [ "$INSTALL_MODE" = "fresh" ]; then
771
843
  echo -e " ${GREEN}.claude/skills/${NC} ← Skills (run 'npx cc-discipline status' to list)"
772
844
  echo -e " ${GREEN}.claude/settings.json${NC} ← Hooks configuration"
773
845
  echo -e " ${GREEN}docs/progress.md${NC} ← Progress log (maintained by Claude)"
846
+ echo -e " ${GREEN}docs/todo.md${NC} ← Open work: Now / Later"
774
847
  echo -e " ${GREEN}docs/debug-log.md${NC} ← Debug log (maintained by Claude)"
775
848
  echo -e " ${GREEN}.claude/memory/${NC} ← Auto memory (symlinked, lives in repo)"
776
849
  echo ""
@@ -794,6 +867,7 @@ else
794
867
  echo -e "What was ${GREEN}NOT${NC} touched:"
795
868
  echo -e " CLAUDE.md ← Your project info is safe"
796
869
  echo -e " docs/progress.md ← Your progress records are safe"
870
+ echo -e " docs/todo.md ← Your open items are safe (created only if missing)"
797
871
  echo -e " docs/debug-log.md ← Your debug logs are safe"
798
872
  echo -e " Your custom rules/agents ← Untouched (we only add our files)"
799
873
  echo ""
package/lib/doctor.sh CHANGED
@@ -32,7 +32,7 @@ fi
32
32
  # 2. Core rules
33
33
  echo ""
34
34
  echo "Core rules:"
35
- for i in 00 01 02 03 04 05 06 07; do
35
+ for i in 00 01 03 04 05 06 07; do # 02 retired 2026-09-23
36
36
  FILE=$(ls .claude/rules/${i}-*.md 2>/dev/null | head -1)
37
37
  if [ -n "$FILE" ]; then
38
38
  ok "$(basename "$FILE")"
@@ -60,23 +60,18 @@ done
60
60
  echo ""
61
61
  echo "Hook registration:"
62
62
  if [ -f ".claude/settings.json" ]; then
63
- if command -v jq &>/dev/null; then
64
- CONTENT=$(cat .claude/settings.json)
65
- for hook in pre-edit-guard streak-breaker post-error-remind session-start phase-gate action-counter git-guard; do
66
- if echo "$CONTENT" | grep -q "$hook"; then
67
- ok "${hook} registered"
68
- else
69
- fail "${hook} NOT registered in settings.json"
70
- fi
71
- done
72
- else
73
- warn "jq not installed — cannot verify hook registration details"
74
- if grep -q "pre-edit-guard" .claude/settings.json 2>/dev/null; then
75
- ok "settings.json contains hook references"
63
+ # A text search is all this needs, so it runs the same with or without jq.
64
+ # The old jq-less branch looked only for pre-edit-guard, so an install whose
65
+ # settings.json never registered git-guard (possible after a jq-less upgrade,
66
+ # which leaves settings.json alone) was reported healthy.
67
+ CONTENT=$(cat .claude/settings.json)
68
+ for hook in pre-edit-guard streak-breaker post-error-remind session-start phase-gate action-counter git-guard; do
69
+ if echo "$CONTENT" | grep -q "$hook"; then
70
+ ok "${hook} registered"
76
71
  else
77
- fail "settings.json doesn't reference any hooks"
72
+ fail "${hook} NOT registered in settings.json"
78
73
  fi
79
- fi
74
+ done
80
75
  else
81
76
  fail "settings.json missing"
82
77
  fi
@@ -102,6 +97,13 @@ for skill_dir in .claude/skills/*/; do
102
97
  SKILL_FOUND=$((SKILL_FOUND + 1))
103
98
  done
104
99
  [ "$SKILL_FOUND" -eq 0 ] && warn "No skills installed (optional)"
100
+ # A skill the user edited is kept on upgrade and the new template is written
101
+ # beside it. Nothing else reminds anyone that it is waiting.
102
+ PENDING=""
103
+ for f in .claude/skills/*/SKILL.md.new; do
104
+ [ -f "$f" ] && PENDING="$PENDING $(basename "$(dirname "$f")")"
105
+ done
106
+ [ -n "$PENDING" ] && warn "New skill templates waiting to be merged:$PENDING — diff .claude/skills/<name>/SKILL.md{,.new}"
105
107
 
106
108
  # 7. jq
107
109
  echo ""
@@ -130,6 +132,11 @@ if [ -f "docs/progress.md" ]; then
130
132
  else
131
133
  warn "No docs/progress.md"
132
134
  fi
135
+ if [ -f "docs/todo.md" ]; then
136
+ ok "docs/todo.md"
137
+ else
138
+ warn "No docs/todo.md — run upgrade to create it"
139
+ fi
133
140
 
134
141
  # Summary
135
142
  echo ""
@@ -143,3 +150,6 @@ else
143
150
  echo "Run 'npx cc-discipline upgrade' to fix missing files."
144
151
  fi
145
152
  echo ""
153
+
154
+ # Non-zero when there are critical issues, so scripts and CI can act on the result.
155
+ [ "$ISSUES" -eq 0 ] || exit 1
@@ -0,0 +1,26 @@
1
+ # sha256 of every version of each framework hook ever committed to templates/,
2
+ # up to 2.14.0, CRs stripped before hashing. init.sh uses it to tell an older
3
+ # shipped hook from a locally modified one in installs that predate the hooks
4
+ # manifest. Frozen: installs from 2.15.0 on record what they got in
5
+ # .claude/.cc-discipline-hooks.manifest instead. Generated 2026-09-24.
6
+ action-counter.sh sha256:2c3b514017c528771c78997c69db454e2b5dcdbcb813d70f4a18392d4da02f89
7
+ action-counter.sh sha256:6befc0b53a4f245fa4b9cbbadc4fca4c524ea66a8d000cc7e388363a906509a2
8
+ action-counter.sh sha256:9a213ff10ade78193e761f4443c51cc9d8a5242525a6177f0d084d81fa256070
9
+ git-guard.sh sha256:11a210bebccb6c82ce7e42de4a29d8fb10507722cff1d5a2346ebfc1863a5bb0
10
+ git-guard.sh sha256:70fe3e335b6b142bb193912b76735386553d8187ddf2d934f1f49a7e949d424e
11
+ git-guard.sh sha256:c391e51a8c0c20a7278695aeee4c46ed464614dccdb1acc5ca7728221ae0c84c
12
+ git-guard.sh sha256:cb650c0532cd6bb6595ddff375195ad2338df422172663769dccdcbf41b3c354
13
+ git-guard.sh sha256:dc55636e9847cc76f31c1ced71b5b6c7ea2ba9a8380347a8fca5332e162e8e6d
14
+ phase-gate.sh sha256:7cbd2ec05d5ab9d1e2919c327a975a1b082ef27684e5d0effdf664f17efbedff
15
+ post-error-remind.sh sha256:389ab860dfb217cd24253e85d28add3961155158961beffaabb9e2e6fa725c1f
16
+ post-error-remind.sh sha256:83b9cbb836af45889bd06e62feebc71a44c741c0cc8b40d8dff2e6d7bf21c7a6
17
+ pre-edit-guard.sh sha256:07c8a3b9e2c87feff1d11dcb195175cf3d6f421968dea17981e9fc573e9caf24
18
+ pre-edit-guard.sh sha256:3be58e3f0a62842ac44a20581275a9bb3cc597439db2e16602bb87bcf3762fb9
19
+ pre-edit-guard.sh sha256:6daf162b6c9835e80960f873d0f9b514065fe726f70dda7b524dbc9cf3675f52
20
+ pre-edit-guard.sh sha256:ae88735efb05dab465f40a1e6bcf7b808fc8dc14d7695aad0e9f4a273d7b5fb8
21
+ pre-edit-guard.sh sha256:c11aaf1da9fab45501478079ed3937321d2d6213c84d374671e5bd6699d08738
22
+ session-start.sh sha256:5d147dae576fe2443b13410849f97bc7edf0d6c92ad26ddf1fd5b1ef32055890
23
+ session-start.sh sha256:60dc913a7d35f6c119ee465730e00f42deabd057e68c935f96a00511663feaaa
24
+ session-start.sh sha256:7cd80aa4c1873851a4c9c0797369bfa9bc1fbce12672ce830748ba8d469977d3
25
+ streak-breaker.sh sha256:7782280af6c40b9dfd58c99f7c7bcffc58f53e247edfab242d5b5454a56cf79a
26
+ streak-breaker.sh sha256:cb667aee973a72380958241e349359b07ea7711b405abdf68ab626ef5211f1f4
package/lib/status.sh CHANGED
@@ -72,6 +72,11 @@ for skill_dir in .claude/skills/*/; do
72
72
  done
73
73
  SKILL_COUNT=$(echo "$SKILLS" | wc -w | tr -d ' ')
74
74
  echo -e "${GREEN}${SKILL_COUNT}${NC} (${SKILLS% })"
75
+ PENDING=""
76
+ for f in .claude/skills/*/SKILL.md.new; do
77
+ [ -f "$f" ] && PENDING="$PENDING /$(basename "$(dirname "$f")")"
78
+ done
79
+ [ -n "$PENDING" ] && echo -e " ${YELLOW}New templates waiting to be merged:${NC}${PENDING} (see: npx cc-discipline doctor)"
75
80
 
76
81
  # Settings
77
82
  echo -n "Settings: "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-discipline",
3
- "version": "2.13.6",
3
+ "version": "2.15.1",
4
4
  "description": "Discipline framework for Claude Code — rules, hooks, and agents that keep AI on track",
5
5
  "bin": {
6
6
  "cc-discipline": "bin/cli.js"