cc-discipline 2.13.0 → 2.13.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.
Files changed (2) hide show
  1. package/init.sh +81 -27
  2. package/package.json +1 -1
package/init.sh CHANGED
@@ -403,41 +403,95 @@ KEPT_RETIRED=""
403
403
  OLD_MANIFEST=$(mktemp 2>/dev/null || echo ".claude/.skills-manifest.old")
404
404
  [ -f "$SKILLS_MANIFEST" ] && cp "$SKILLS_MANIFEST" "$OLD_MANIFEST"
405
405
 
406
- # Emit "<algo>:<digest>", never a bare digest. The fallback chain below resolves
407
- # differently depending on what is installed, so a manifest that stores only the
408
- # digest silently mis-compares the moment tool availability changes — the same
409
- # class of environment drift that made the jq-only reads fail. When that happens
410
- # every recorded hash mismatches, every retired skill looks user-modified, and
411
- # the retirement pass does nothing. (fixed 2026-08-28, before 2.13.0 shipped.)
412
- # sha256sum and `shasum -a 256` produce identical digests, so both record sha256.
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.
413
457
  _cc_hash() {
414
- if command -v sha256sum >/dev/null 2>&1; then echo "sha256:$(sha256sum "$1" | cut -d' ' -f1)"
415
- elif command -v shasum >/dev/null 2>&1; then echo "sha256:$(shasum -a 256 "$1" | cut -d' ' -f1)"
416
- elif command -v md5sum >/dev/null 2>&1; then echo "md5:$(md5sum "$1" | cut -d' ' -f1)"
417
- elif command -v md5 >/dev/null 2>&1; then echo "md5:$(md5 -q "$1")"
418
- else echo "cksum:$(cksum "$1" | cut -d' ' -f1)"; fi
458
+ _a=$(_cc_algo)
459
+ echo "$(_cc_label "$_a"):$(_cc_digest_with "$_a" "$1" norm)"
419
460
  }
420
461
 
421
- # True when manifest entry $2 still describes the current content of file $1.
422
- # Handles the "<algo>:<digest>" form written from 2.13.0 and the bare digest
423
- # written by 2.12.2/2.12.3: for a legacy entry the algorithm is unknown, so try
424
- # each available one and accept any match rather than calling the file modified.
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.
425
471
  _cc_hash_matches() {
426
472
  _f="$1"; _rec="$2"
427
473
  [ -n "$_rec" ] || return 1
474
+ [ -f "$_f" ] || return 1
428
475
  case "$_rec" in
429
- *:*) [ "$(_cc_hash "$_f")" = "$_rec" ] && return 0; return 1 ;;
476
+ *:*)
477
+ _rl=${_rec%%:*}; _rd=${_rec#*:}
478
+ case "$_rl" in
479
+ sha256) _cands="sha256sum shasum" ;;
480
+ md5) _cands="md5sum md5" ;;
481
+ *) _cands="cksum" ;;
482
+ esac
483
+ for _al in $_cands; do
484
+ command -v "$_al" >/dev/null 2>&1 || continue
485
+ [ "$(_cc_digest_with "$_al" "$_f" norm)" = "$_rd" ] && return 0
486
+ [ "$(_cc_digest_with "$_al" "$_f" raw)" = "$_rd" ] && return 0
487
+ done
488
+ return 1
489
+ ;;
430
490
  esac
431
- for _algo in sha256sum shasum md5sum md5 cksum; do
432
- command -v "$_algo" >/dev/null 2>&1 || continue
433
- case "$_algo" in
434
- sha256sum) _got=$(sha256sum "$_f" | cut -d' ' -f1) ;;
435
- shasum) _got=$(shasum -a 256 "$_f" | cut -d' ' -f1) ;;
436
- md5sum) _got=$(md5sum "$_f" | cut -d' ' -f1) ;;
437
- md5) _got=$(md5 -q "$_f") ;;
438
- cksum) _got=$(cksum "$_f" | cut -d' ' -f1) ;;
439
- esac
440
- [ "$_got" = "$_rec" ] && return 0
491
+ for _al in sha256sum shasum md5sum md5 cksum; do
492
+ command -v "$_al" >/dev/null 2>&1 || continue
493
+ [ "$(_cc_digest_with "$_al" "$_f" norm)" = "$_rec" ] && return 0
494
+ [ "$(_cc_digest_with "$_al" "$_f" raw)" = "$_rec" ] && return 0
441
495
  done
442
496
  return 1
443
497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-discipline",
3
- "version": "2.13.0",
3
+ "version": "2.13.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"