cc-discipline 2.12.1 → 2.12.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.
package/init.sh CHANGED
@@ -377,14 +377,84 @@ echo -e "${GREEN}Installing skills...${NC}"
377
377
  # v2.12.1. Symptom on an affected machine: a stray .claude/skills/SKILL.md plus
378
378
  # skill dirs frozen at their first-install date. status/doctor could not catch
379
379
  # it because they glob `skills/*/` and a bare file is not a directory.
380
+ # Skills are NOT force-overwritten. Users legitimately tune them per repo
381
+ # (/self-check's "Project-specific Checks" section exists precisely to be filled
382
+ # in), and until v2.12.2 every upgrade silently replaced those edits — a 73KB
383
+ # hand-written self-check was wiped twice in one day before this was fixed.
384
+ #
385
+ # The dpkg-conffile approach: remember the hash of the template WE installed. On
386
+ # the next upgrade, if the file on disk still matches that hash the user never
387
+ # touched it and we can update freely; if it differs the user owns it, so we keep
388
+ # their file and drop the new template beside it as SKILL.md.new.
389
+ #
390
+ # The manifest hash is always set to the template we shipped this run, including
391
+ # in the preserved case. That is what makes adoption self-healing: if the user
392
+ # later replaces their file with the .new one, the next run sees disk == manifest
393
+ # and resumes silent updates.
394
+ SKILLS_MANIFEST=".claude/.cc-discipline-skills.manifest"
395
+ NEW_MANIFEST=$(mktemp 2>/dev/null || echo ".claude/.skills-manifest.tmp")
396
+ PRESERVED_SKILLS=""
397
+
398
+ _cc_hash() {
399
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
400
+ elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
401
+ elif command -v md5sum >/dev/null 2>&1; then md5sum "$1" | cut -d' ' -f1
402
+ elif command -v md5 >/dev/null 2>&1; then md5 -q "$1"
403
+ else cksum "$1" | cut -d' ' -f1; fi
404
+ }
405
+
380
406
  for skill_dir in "$SCRIPT_DIR"/templates/.claude/skills/*/; do
381
407
  [ -d "$skill_dir" ] || continue
382
408
  skill_name=$(basename "$skill_dir")
383
409
  mkdir -p ".claude/skills/$skill_name"
384
- cp -R "$skill_dir"* ".claude/skills/$skill_name/"
385
- echo " ✓ /$skill_name"
410
+ skill_status="updated"
411
+
412
+ for src in "$skill_dir"*; do
413
+ [ -f "$src" ] || continue
414
+ base=$(basename "$src")
415
+ rel="$skill_name/$base"
416
+ dst=".claude/skills/$rel"
417
+ tpl_hash=$(_cc_hash "$src")
418
+
419
+ if [ ! -f "$dst" ]; then
420
+ cp "$src" "$dst" # new file — nothing to protect
421
+ else
422
+ disk_hash=$(_cc_hash "$dst")
423
+ recorded=$(grep "^$rel " "$SKILLS_MANIFEST" 2>/dev/null | head -1 | cut -d' ' -f2)
424
+ if [ "$disk_hash" = "$tpl_hash" ]; then
425
+ : # already current
426
+ elif [ -n "$recorded" ] && [ "$disk_hash" = "$recorded" ]; then
427
+ cp "$src" "$dst" # pristine older version — safe
428
+ elif [ -z "$recorded" ]; then
429
+ # No manifest yet (first run on an existing install) and the file
430
+ # differs from the template. Cannot tell "older version" from
431
+ # "user edited", so assume the user's work matters.
432
+ cp "$src" "$dst.new"
433
+ skill_status="preserved"
434
+ else
435
+ cp "$src" "$dst.new" # user-modified — keep theirs
436
+ skill_status="preserved"
437
+ fi
438
+ fi
439
+ echo "$rel $tpl_hash" >> "$NEW_MANIFEST"
440
+ done
441
+
442
+ if [ "$skill_status" = "preserved" ]; then
443
+ PRESERVED_SKILLS="$PRESERVED_SKILLS $skill_name"
444
+ echo -e " ${YELLOW}~ /$skill_name (your version kept; new template at SKILL.md.new)${NC}"
445
+ else
446
+ echo " ✓ /$skill_name"
447
+ fi
386
448
  done
387
449
 
450
+ [ -f "$NEW_MANIFEST" ] && mv "$NEW_MANIFEST" "$SKILLS_MANIFEST"
451
+
452
+ if [ -n "$PRESERVED_SKILLS" ]; then
453
+ echo -e " ${YELLOW}Locally modified skills were not overwritten:${PRESERVED_SKILLS}${NC}"
454
+ echo -e " ${YELLOW}Compare with: diff .claude/skills/<name>/SKILL.md{,.new}${NC}"
455
+ echo -e " ${YELLOW}Take the new one with: mv .claude/skills/<name>/SKILL.md{.new,}${NC}"
456
+ fi
457
+
388
458
  # Clean up the stray file left behind by the v2.11.0–v2.12.0 bug above. A bare
389
459
  # SKILL.md directly under skills/ is never a valid skill (skills live in
390
460
  # subdirectories), so this only ever removes that debris.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-discipline",
3
- "version": "2.12.1",
3
+ "version": "2.12.3",
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"
@@ -43,42 +43,62 @@ if echo "$CMD_NORM" | grep -qE 'hooks[/\\][a-z-]+\.sh'; then
43
43
  exit 0
44
44
  fi
45
45
 
46
+ # Blank out the CONTENT of message-style arguments before matching. A commit
47
+ # message that merely *describes* a destructive command is data — it can never
48
+ # execute — but the text match could not tell the difference, so writing about
49
+ # these commands blocked the commit:
50
+ # git commit -m "revert the git reset --hard change" -> must pass
51
+ # git commit -m "x" && git reset --hard -> must STILL block
52
+ #
53
+ # Only the argument to -m/--message/-F/--file is blanked, never quotes in
54
+ # general. Stripping all quoted text would open a real hole, because these DO
55
+ # execute what they quote and must keep matching:
56
+ # bash -c "git reset --hard" eval "git clean -fd" sudo git reset --hard
57
+ #
58
+ # Escaped quotes inside a message end the match early, leaving the tail to be
59
+ # scanned — that direction is a false positive (one wasted turn), never a miss.
60
+ SQ="'"
61
+ CMD_SCAN=$(printf '%s' "$CMD_NORM" \
62
+ | sed -E 's/(^|[[:space:]])(-m|--message|-F|--file)[[:space:]]*"[^"]*"/\1\2 ARG/g' \
63
+ | sed -E "s/(^|[[:space:]])(-m|--message|-F|--file)[[:space:]]*${SQ}[^${SQ}]*${SQ}/\1\2 ARG/g" \
64
+ | sed -E 's/(^|[[:space:]])(-m|--message|-F|--file)[[:space:]]+[^[:space:]]+/\1\2 ARG/g')
65
+
46
66
  BLOCKED=""
47
67
  SUGGESTION=""
48
68
 
49
69
  # git checkout . / git checkout -- <file> (discard working tree changes)
50
70
  # But allow: git checkout <branch>, git checkout -b <branch>
51
- if echo "$CMD_NORM" | grep -qE 'git\s+checkout\s+(\.|--\s)'; then
71
+ if echo "$CMD_SCAN" | grep -qE 'git\s+checkout\s+(\.|--\s)'; then
52
72
  BLOCKED="git checkout (discards uncommitted changes)"
53
73
  SUGGESTION="git stash"
54
74
  fi
55
75
 
56
76
  # git restore . / git restore <file> (without --staged)
57
- if echo "$CMD_NORM" | grep -qE 'git\s+restore\s' && ! echo "$CMD_NORM" | grep -qE 'git\s+restore\s+--staged'; then
77
+ if echo "$CMD_SCAN" | grep -qE 'git\s+restore\s' && ! echo "$CMD_SCAN" | grep -qE 'git\s+restore\s+--staged'; then
58
78
  BLOCKED="git restore (discards uncommitted changes)"
59
79
  SUGGESTION="git stash"
60
80
  fi
61
81
 
62
82
  # git reset --hard
63
- if echo "$CMD_NORM" | grep -qE 'git\s+reset\s+--hard'; then
83
+ if echo "$CMD_SCAN" | grep -qE 'git\s+reset\s+--hard'; then
64
84
  BLOCKED="git reset --hard (destroys all uncommitted changes)"
65
85
  SUGGESTION="git stash && git reset"
66
86
  fi
67
87
 
68
88
  # git clean -f / -fd / -fx
69
- if echo "$CMD_NORM" | grep -qE 'git\s+clean\s+-[a-z]*f'; then
89
+ if echo "$CMD_SCAN" | grep -qE 'git\s+clean\s+-[a-z]*f'; then
70
90
  BLOCKED="git clean -f (permanently deletes untracked files)"
71
91
  SUGGESTION="git stash --include-untracked"
72
92
  fi
73
93
 
74
94
  # git branch -D (force delete unmerged branch)
75
- if echo "$CMD_NORM" | grep -qE 'git\s+branch\s+-D\s'; then
95
+ if echo "$CMD_SCAN" | grep -qE 'git\s+branch\s+-D\s'; then
76
96
  BLOCKED="git branch -D (deletes branch even if not merged)"
77
97
  SUGGESTION="git branch -d (safe delete, fails if not merged)"
78
98
  fi
79
99
 
80
100
  # git push --force / -f (to main/master)
81
- if echo "$CMD_NORM" | grep -qE 'git\s+push\s+.*(-f|--force)' && echo "$CMD_NORM" | grep -qE '(main|master)'; then
101
+ if echo "$CMD_SCAN" | grep -qE 'git\s+push\s+.*(-f|--force)' && echo "$CMD_SCAN" | grep -qE '(main|master)'; then
82
102
  BLOCKED="git push --force to main/master (rewrites shared history)"
83
103
  SUGGESTION="git push --force-with-lease"
84
104
  fi