cc-discipline 2.1.0 → 2.3.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/init.sh CHANGED
@@ -7,8 +7,12 @@
7
7
 
8
8
  set -e
9
9
 
10
- # ─── Version ───
11
- VERSION="2.0.0"
10
+ # ─── Version (single source: package.json) ───
11
+ _INIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ if command -v node &>/dev/null && [ -f "$_INIT_DIR/package.json" ]; then
13
+ VERSION=$(node -p "require('$_INIT_DIR/package.json').version" 2>/dev/null)
14
+ fi
15
+ VERSION="${VERSION:-unknown}"
12
16
 
13
17
  # ─── Parse CLI arguments ───
14
18
  ARG_STACK=""
@@ -276,6 +280,7 @@ cp "$SCRIPT_DIR/templates/.claude/hooks/post-error-remind.sh" .claude/hooks/
276
280
  cp "$SCRIPT_DIR/templates/.claude/hooks/streak-breaker.sh" .claude/hooks/
277
281
  cp "$SCRIPT_DIR/templates/.claude/hooks/session-start.sh" .claude/hooks/
278
282
  cp "$SCRIPT_DIR/templates/.claude/hooks/phase-gate.sh" .claude/hooks/
283
+ cp "$SCRIPT_DIR/templates/.claude/hooks/git-guard.sh" .claude/hooks/
279
284
  cp "$SCRIPT_DIR/templates/.claude/hooks/action-counter.sh" .claude/hooks/
280
285
  chmod +x .claude/hooks/*.sh
281
286
 
@@ -390,16 +395,45 @@ if [ ! -f "docs/debug-log.md" ]; then
390
395
  cp "$SCRIPT_DIR/templates/docs/debug-log.md" docs/
391
396
  fi
392
397
 
393
- # ─── Install auto memory ───
398
+ # ─── Install auto memory (symlink to .claude/memory/) ───
394
399
  echo -e "${GREEN}Installing auto memory...${NC}"
395
400
  MEMORY_PROJECT_KEY=$(echo "$PROJECT_DIR" | sed 's|/|-|g')
396
- MEMORY_DIR="$HOME/.claude/projects/${MEMORY_PROJECT_KEY}/memory"
397
- mkdir -p "$MEMORY_DIR"
398
- if [ ! -f "$MEMORY_DIR/MEMORY.md" ]; then
399
- cp "$SCRIPT_DIR/templates/memory/MEMORY.md" "$MEMORY_DIR/MEMORY.md"
400
- echo " ✓ Memory installed to $MEMORY_DIR/MEMORY.md"
401
+ MEMORY_CLAUDE_DIR="$HOME/.claude/projects/${MEMORY_PROJECT_KEY}"
402
+ MEMORY_LOCAL_DIR="$PROJECT_DIR/.claude/memory"
403
+
404
+ # Ensure local .claude/memory/ exists in repo
405
+ mkdir -p "$MEMORY_LOCAL_DIR"
406
+
407
+ # If MEMORY.md doesn't exist locally, seed from template
408
+ if [ ! -f "$MEMORY_LOCAL_DIR/MEMORY.md" ]; then
409
+ cp "$SCRIPT_DIR/templates/memory/MEMORY.md" "$MEMORY_LOCAL_DIR/MEMORY.md"
410
+ echo " ✓ Memory template installed to .claude/memory/MEMORY.md"
411
+ else
412
+ echo -e " ${YELLOW}.claude/memory/MEMORY.md already exists (preserved)${NC}"
413
+ fi
414
+
415
+ # Create symlink: ~/.claude/projects/<key>/memory → .claude/memory/
416
+ mkdir -p "$MEMORY_CLAUDE_DIR"
417
+ if [ -L "$MEMORY_CLAUDE_DIR/memory" ]; then
418
+ # Symlink already exists — update it
419
+ rm "$MEMORY_CLAUDE_DIR/memory"
420
+ ln -s "$MEMORY_LOCAL_DIR" "$MEMORY_CLAUDE_DIR/memory"
421
+ echo " ✓ Memory symlink updated"
422
+ elif [ -d "$MEMORY_CLAUDE_DIR/memory" ]; then
423
+ # Existing real directory — migrate files then replace with symlink
424
+ for f in "$MEMORY_CLAUDE_DIR/memory/"*; do
425
+ [ -f "$f" ] || continue
426
+ fname=$(basename "$f")
427
+ if [ ! -f "$MEMORY_LOCAL_DIR/$fname" ]; then
428
+ cp "$f" "$MEMORY_LOCAL_DIR/$fname"
429
+ fi
430
+ done
431
+ rm -rf "$MEMORY_CLAUDE_DIR/memory"
432
+ ln -s "$MEMORY_LOCAL_DIR" "$MEMORY_CLAUDE_DIR/memory"
433
+ echo " ✓ Existing memory migrated to .claude/memory/ and symlinked"
401
434
  else
402
- echo -e " ${YELLOW}MEMORY.md already exists, skipping (no overwrite)${NC}"
435
+ ln -s "$MEMORY_LOCAL_DIR" "$MEMORY_CLAUDE_DIR/memory"
436
+ echo " ✓ Memory symlinked: ~/.claude/projects/.../ → .claude/memory/"
403
437
  fi
404
438
 
405
439
  # ─── Install global rules (optional) ───
@@ -446,7 +480,7 @@ if [ "$INSTALL_MODE" = "fresh" ]; then
446
480
  echo -e " ${GREEN}.claude/settings.json${NC} ← Hooks configuration"
447
481
  echo -e " ${GREEN}docs/progress.md${NC} ← Progress log (maintained by Claude)"
448
482
  echo -e " ${GREEN}docs/debug-log.md${NC} ← Debug log (maintained by Claude)"
449
- echo -e " ${GREEN}~/.claude/projects/.../memory/${NC} ← Auto memory (cross-session)"
483
+ echo -e " ${GREEN}.claude/memory/${NC} ← Auto memory (symlinked, lives in repo)"
450
484
  echo ""
451
485
  echo -e "${YELLOW}Next steps:${NC}"
452
486
  echo " 1. Edit CLAUDE.md and fill in the [TODO] sections with project info"
package/lib/doctor.sh CHANGED
@@ -44,7 +44,7 @@ done
44
44
  # 3. Hooks
45
45
  echo ""
46
46
  echo "Hooks:"
47
- for hook in pre-edit-guard streak-breaker post-error-remind session-start phase-gate action-counter; do
47
+ for hook in pre-edit-guard streak-breaker post-error-remind session-start phase-gate action-counter git-guard; do
48
48
  if [ -f ".claude/hooks/${hook}.sh" ]; then
49
49
  if [ -x ".claude/hooks/${hook}.sh" ]; then
50
50
  ok "${hook}.sh"
@@ -62,7 +62,7 @@ echo "Hook registration:"
62
62
  if [ -f ".claude/settings.json" ]; then
63
63
  if command -v jq &>/dev/null; then
64
64
  CONTENT=$(cat .claude/settings.json)
65
- for hook in pre-edit-guard streak-breaker post-error-remind session-start phase-gate action-counter; do
65
+ for hook in pre-edit-guard streak-breaker post-error-remind session-start phase-gate action-counter git-guard; do
66
66
  if echo "$CONTENT" | grep -q "$hook"; then
67
67
  ok "${hook} registered"
68
68
  else
package/lib/status.sh CHANGED
@@ -51,8 +51,9 @@ HOOKS=""
51
51
  [ -f ".claude/hooks/session-start.sh" ] && HOOKS="${HOOKS}session-start "
52
52
  [ -f ".claude/hooks/phase-gate.sh" ] && HOOKS="${HOOKS}phase-gate "
53
53
  [ -f ".claude/hooks/action-counter.sh" ] && HOOKS="${HOOKS}action-counter "
54
+ [ -f ".claude/hooks/git-guard.sh" ] && HOOKS="${HOOKS}git-guard "
54
55
  HOOK_COUNT=$(echo "$HOOKS" | wc -w | tr -d ' ')
55
- echo -e "${GREEN}${HOOK_COUNT}/6${NC} (${HOOKS% })"
56
+ echo -e "${GREEN}${HOOK_COUNT}/7${NC} (${HOOKS% })"
56
57
 
57
58
  # Agents
58
59
  echo -n "Agents: "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-discipline",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
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.sh"
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # cc-discipline: Guard against destructive git commands
3
+ # PreToolUse on Bash — blocks git checkout/restore/reset --hard/clean -f without confirmation.
4
+
5
+ INPUT=$(cat)
6
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
7
+
8
+ if [ "$TOOL_NAME" != "Bash" ]; then
9
+ exit 0
10
+ fi
11
+
12
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
13
+
14
+ # Normalize: collapse whitespace, trim
15
+ CMD_NORM=$(echo "$CMD" | tr '\n' ' ' | sed 's/ */ /g')
16
+
17
+ BLOCKED=""
18
+ SUGGESTION=""
19
+
20
+ # git checkout . / git checkout -- <file> (discard working tree changes)
21
+ # But allow: git checkout <branch>, git checkout -b <branch>
22
+ if echo "$CMD_NORM" | grep -qE 'git\s+checkout\s+(\.|--\s)'; then
23
+ BLOCKED="git checkout (discards uncommitted changes)"
24
+ SUGGESTION="git stash"
25
+ fi
26
+
27
+ # git restore . / git restore <file> (without --staged)
28
+ if echo "$CMD_NORM" | grep -qE 'git\s+restore\s' && ! echo "$CMD_NORM" | grep -qE 'git\s+restore\s+--staged'; then
29
+ BLOCKED="git restore (discards uncommitted changes)"
30
+ SUGGESTION="git stash"
31
+ fi
32
+
33
+ # git reset --hard
34
+ if echo "$CMD_NORM" | grep -qE 'git\s+reset\s+--hard'; then
35
+ BLOCKED="git reset --hard (destroys all uncommitted changes)"
36
+ SUGGESTION="git stash && git reset"
37
+ fi
38
+
39
+ # git clean -f / -fd / -fx
40
+ if echo "$CMD_NORM" | grep -qE 'git\s+clean\s+-[a-z]*f'; then
41
+ BLOCKED="git clean -f (permanently deletes untracked files)"
42
+ SUGGESTION="git stash --include-untracked"
43
+ fi
44
+
45
+ # git branch -D (force delete unmerged branch)
46
+ if echo "$CMD_NORM" | grep -qE 'git\s+branch\s+-D\s'; then
47
+ BLOCKED="git branch -D (deletes branch even if not merged)"
48
+ SUGGESTION="git branch -d (safe delete, fails if not merged)"
49
+ fi
50
+
51
+ # git push --force / -f (to main/master)
52
+ if echo "$CMD_NORM" | grep -qE 'git\s+push\s+.*(-f|--force)' && echo "$CMD_NORM" | grep -qE '(main|master)'; then
53
+ BLOCKED="git push --force to main/master (rewrites shared history)"
54
+ SUGGESTION="git push --force-with-lease"
55
+ fi
56
+
57
+ if [ -n "$BLOCKED" ]; then
58
+ echo "GIT GUARD: Blocked destructive command: $BLOCKED. This command can permanently destroy uncommitted work. Before running this: (1) Check git status and git diff to see what would be lost. (2) If changes should be kept, run: $SUGGESTION first. (3) If you are certain the changes should be discarded, tell the user what will be lost and ask for explicit confirmation." >&2
59
+ exit 2
60
+ fi
61
+
62
+ exit 0
@@ -39,6 +39,15 @@
39
39
  }
40
40
  ]
41
41
  },
42
+ {
43
+ "matcher": "Bash",
44
+ "hooks": [
45
+ {
46
+ "type": "command",
47
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/git-guard.sh\""
48
+ }
49
+ ]
50
+ },
42
51
  {
43
52
  "matcher": "ExitPlanMode",
44
53
  "hooks": [