create-ccc-tutor 0.1.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.
Files changed (106) hide show
  1. package/README.md +41 -0
  2. package/bin/cli.js +76 -0
  3. package/package.json +28 -0
  4. package/template/.claude/commands/abandon.md +7 -0
  5. package/template/.claude/commands/add-anti-flag.md +7 -0
  6. package/template/.claude/commands/add-constitution-clause.md +7 -0
  7. package/template/.claude/commands/audit-spec.md +7 -0
  8. package/template/.claude/commands/commit.md +7 -0
  9. package/template/.claude/commands/constitution-edit.md +7 -0
  10. package/template/.claude/commands/db-schema.md +7 -0
  11. package/template/.claude/commands/exam.md +66 -0
  12. package/template/.claude/commands/execution-plan.md +7 -0
  13. package/template/.claude/commands/feature-draft.md +7 -0
  14. package/template/.claude/commands/handoff.md +7 -0
  15. package/template/.claude/commands/implement.md +7 -0
  16. package/template/.claude/commands/init.md +7 -0
  17. package/template/.claude/commands/next.md +7 -0
  18. package/template/.claude/commands/offload.md +7 -0
  19. package/template/.claude/commands/pickup.md +7 -0
  20. package/template/.claude/commands/recall.md +7 -0
  21. package/template/.claude/commands/remember.md +7 -0
  22. package/template/.claude/commands/slide.md +87 -0
  23. package/template/.claude/commands/spec-finalize.md +7 -0
  24. package/template/.claude/commands/test-fix.md +7 -0
  25. package/template/.claude/commands/uninstall.md +7 -0
  26. package/template/.claude/settings.json +161 -0
  27. package/template/.claude-plugin/plugin.json +41 -0
  28. package/template/.codex/config.toml +24 -0
  29. package/template/.codex/hooks.json +4 -0
  30. package/template/.codex/install-skills.sh +18 -0
  31. package/template/.codex/skills/exam/SKILL.md +61 -0
  32. package/template/.codex/skills/slide/SKILL.md +69 -0
  33. package/template/.harness/agents/README.md +70 -0
  34. package/template/.harness/agents/_template/junior-agent-template.md +116 -0
  35. package/template/.harness/agents/backend-reviewer.md +153 -0
  36. package/template/.harness/agents/frontend-reviewer.md +158 -0
  37. package/template/.harness/agents/security-reviewer.md +148 -0
  38. package/template/.harness/agents/test-fixer.md +147 -0
  39. package/template/.harness/docs/doc-sync.md +29 -0
  40. package/template/.harness/docs/git-hygiene.md +56 -0
  41. package/template/.harness/docs/spec-model.md +47 -0
  42. package/template/.harness/docs/tool-map.md +120 -0
  43. package/template/.harness/docs/workflow.md +59 -0
  44. package/template/.harness/scripts/README.md +70 -0
  45. package/template/.harness/scripts/auditor-gate.sh +388 -0
  46. package/template/.harness/scripts/bootstrap-check.sh +103 -0
  47. package/template/.harness/scripts/budget-monitor.sh +223 -0
  48. package/template/.harness/scripts/check-prereqs.sh +165 -0
  49. package/template/.harness/scripts/checkpoint-recall.sh +136 -0
  50. package/template/.harness/scripts/checkpoint-write.sh +281 -0
  51. package/template/.harness/scripts/decision-log-append.sh +90 -0
  52. package/template/.harness/scripts/env-check.sh +286 -0
  53. package/template/.harness/scripts/format-edit.sh +80 -0
  54. package/template/.harness/scripts/lint-bans.sh +110 -0
  55. package/template/.harness/scripts/memory-archive.sh +129 -0
  56. package/template/.harness/scripts/memory-recall.sh +197 -0
  57. package/template/.harness/scripts/memory-snapshot.sh +124 -0
  58. package/template/.harness/scripts/post-migration.sh +58 -0
  59. package/template/.harness/scripts/precommit-cycles.sh +74 -0
  60. package/template/.harness/scripts/precommit-typecheck.sh +69 -0
  61. package/template/.harness/scripts/scratchpad-recall.sh +83 -0
  62. package/template/.harness/scripts/scratchpad-update.sh +39 -0
  63. package/template/.harness/scripts/standalone-bootstrap.md +443 -0
  64. package/template/.harness/skills/abandon/SKILL.md +157 -0
  65. package/template/.harness/skills/add-anti-flag/SKILL.md +205 -0
  66. package/template/.harness/skills/add-constitution-clause/SKILL.md +244 -0
  67. package/template/.harness/skills/audit-spec/SKILL.md +395 -0
  68. package/template/.harness/skills/commit/SKILL.md +270 -0
  69. package/template/.harness/skills/constitution-edit/SKILL.md +292 -0
  70. package/template/.harness/skills/db-schema/SKILL.md +145 -0
  71. package/template/.harness/skills/db-schema/references/methodology.md +202 -0
  72. package/template/.harness/skills/execution-plan/SKILL.md +346 -0
  73. package/template/.harness/skills/feature-draft/SKILL.md +426 -0
  74. package/template/.harness/skills/handoff/SKILL.md +211 -0
  75. package/template/.harness/skills/implement/SKILL.md +355 -0
  76. package/template/.harness/skills/init/SKILL.md +805 -0
  77. package/template/.harness/skills/next/SKILL.md +245 -0
  78. package/template/.harness/skills/offload/SKILL.md +134 -0
  79. package/template/.harness/skills/pickup/SKILL.md +213 -0
  80. package/template/.harness/skills/recall/SKILL.md +159 -0
  81. package/template/.harness/skills/remember/SKILL.md +205 -0
  82. package/template/.harness/skills/spec-finalize/SKILL.md +196 -0
  83. package/template/.harness/skills/test-fix/SKILL.md +363 -0
  84. package/template/.harness/skills/uninstall/SKILL.md +370 -0
  85. package/template/.harness/state/install.json +83 -0
  86. package/template/AGENTS.md +262 -0
  87. package/template/CCC_MAGI_LICENSE +201 -0
  88. package/template/CCC_MAGI_README.md +986 -0
  89. package/template/CLAUDE.md +658 -0
  90. package/template/codex.md +39 -0
  91. package/template/constitution.md +164 -0
  92. package/template/course/README.md +15 -0
  93. package/template/course/course_code(example)/exam/README.md +2 -0
  94. package/template/course/course_code(example)/slide/slide_example-1.pdf +40 -0
  95. package/template/course/course_code(example)/slide/slide_example-2.pdf +40 -0
  96. package/template/docs/features/slide-query-implementation.md +79 -0
  97. package/template/docs/features/slide-query.md +211 -0
  98. package/template/docs-harness/README.md +42 -0
  99. package/template/docs-harness/adoption-playbook.md +373 -0
  100. package/template/docs-harness/ccc-step1-driver-template.md +288 -0
  101. package/template/docs-harness/cli-configs-README.md +78 -0
  102. package/template/docs-harness/context-architecture-v2.md +249 -0
  103. package/template/docs-harness/design-spec.md +437 -0
  104. package/template/docs-harness/memory-layer.md +135 -0
  105. package/template/docs-harness/retrospective-notes.md +204 -0
  106. package/template/gitignore +106 -0
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # Post-edit format hook — runs the project's formatter on the file just edited.
3
+ #
4
+ # Called from .claude/settings.json + .codex/hooks.json on Edit|Write tool use.
5
+ # Picks the formatter by file extension; silently skips unknown extensions.
6
+
7
+ set -euo pipefail
8
+
9
+ # Ensure brew-installed tools (jq, etc.) are on PATH even in non-interactive
10
+ # shells where ~/.zprofile isn't loaded. macOS Apple Silicon path comes first.
11
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
12
+
13
+ # Edited file path is passed as $1 or via $CLAUDE_FILE_PATHS / stdin JSON
14
+ FILE="${1:-}"
15
+ if [ -z "$FILE" ]; then
16
+ # Try to extract from Claude Code's tool input JSON on stdin
17
+ if [ -t 0 ]; then
18
+ exit 0 # no stdin, no arg → nothing to format
19
+ fi
20
+ FILE=$(jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")
21
+ fi
22
+
23
+ if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
24
+ exit 0
25
+ fi
26
+
27
+ # ─────────────────────────────────────────────────────────────────────
28
+ # CUSTOMIZE: per-extension formatter
29
+ # ─────────────────────────────────────────────────────────────────────
30
+ # Add / remove cases as your stack requires.
31
+ # Failures here are silent (|| true) — formatter issues should not block edits.
32
+ # ─────────────────────────────────────────────────────────────────────
33
+
34
+ case "$FILE" in
35
+ *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.json|*.jsonc|*.md|*.mdx|*.html|*.css|*.scss|*.yaml|*.yml)
36
+ if command -v prettier >/dev/null 2>&1; then
37
+ prettier --write "$FILE" >/dev/null 2>&1 || true
38
+ elif command -v npx >/dev/null 2>&1; then
39
+ npx --no-install prettier --write "$FILE" >/dev/null 2>&1 || true
40
+ fi
41
+ ;;
42
+
43
+ *.py)
44
+ if command -v black >/dev/null 2>&1; then
45
+ black "$FILE" >/dev/null 2>&1 || true
46
+ elif command -v ruff >/dev/null 2>&1; then
47
+ ruff format "$FILE" >/dev/null 2>&1 || true
48
+ fi
49
+ ;;
50
+
51
+ *.go)
52
+ if command -v gofmt >/dev/null 2>&1; then
53
+ gofmt -w "$FILE" >/dev/null 2>&1 || true
54
+ fi
55
+ ;;
56
+
57
+ *.rs)
58
+ if command -v rustfmt >/dev/null 2>&1; then
59
+ rustfmt "$FILE" >/dev/null 2>&1 || true
60
+ fi
61
+ ;;
62
+
63
+ *.swift)
64
+ if command -v swift-format >/dev/null 2>&1; then
65
+ swift-format -i "$FILE" >/dev/null 2>&1 || true
66
+ fi
67
+ ;;
68
+
69
+ *.kt|*.kts)
70
+ if command -v ktlint >/dev/null 2>&1; then
71
+ ktlint -F "$FILE" >/dev/null 2>&1 || true
72
+ fi
73
+ ;;
74
+
75
+ *)
76
+ # No formatter configured for this extension. Silent.
77
+ ;;
78
+ esac
79
+
80
+ exit 0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-commit lint bans — scans staged changes for project-specific anti-patterns.
3
+ #
4
+ # This script enforces FORBIDDEN patterns, not the inverse anti-flag rules.
5
+ # Anti-flag rules in AGENTS.md say "X is correct, Y is BANNED — don't suggest Y."
6
+ # This script greps for Y in the diff and blocks if found.
7
+ #
8
+ # Constitution § 1 — anti-flag rules are part of the harness's signal-quality
9
+ # guarantee. Mechanical enforcement prevents banned patterns from leaking in.
10
+
11
+ set -euo pipefail
12
+
13
+ # Ensure brew-installed tools (jq, etc.) are on PATH even in non-interactive
14
+ # shells where ~/.zprofile isn't loaded. macOS Apple Silicon path comes first.
15
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
16
+
17
+ # Filter: only fire on `git commit` invocations. The PreToolUse hook contract
18
+ # passes the tool call payload via stdin; we parse it and silently exit if
19
+ # the Bash command isn't a git commit.
20
+ #
21
+ # (We do this filtering here instead of relying on settings.json's `if`
22
+ # clause because that clause was found NOT to be honored consistently —
23
+ # see harness-testing-2026-05-25.md § P0.Z.)
24
+
25
+ # Read stdin (Claude Code passes JSON with tool_input.command for Bash hooks)
26
+ HOOK_INPUT="$(cat 2>/dev/null || true)"
27
+ if [ -n "$HOOK_INPUT" ]; then
28
+ # Try to extract the Bash command. If jq is available, use it. Otherwise
29
+ # fall back to grep (best-effort).
30
+ if command -v jq >/dev/null 2>&1; then
31
+ BASH_CMD="$(printf '%s' "$HOOK_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")"
32
+ else
33
+ # Best-effort fallback: extract "command":"..." substring via grep
34
+ BASH_CMD="$(printf '%s' "$HOOK_INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"command"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' || echo "")"
35
+ fi
36
+ # Silent exit unless this is a git commit invocation
37
+ case "$BASH_CMD" in
38
+ git\ commit*|*\ git\ commit*)
39
+ # proceed
40
+ ;;
41
+ *)
42
+ exit 0
43
+ ;;
44
+ esac
45
+ fi
46
+
47
+ # === Original hook logic continues below ===
48
+
49
+ # ─────────────────────────────────────────────────────────────────────
50
+ # CUSTOMIZE: list of banned patterns (one regex per entry)
51
+ # ─────────────────────────────────────────────────────────────────────
52
+ # Each pattern is matched line-by-line on staged diff additions (lines starting
53
+ # with `+`). Edit this list to mirror AGENTS.md § "Anti-flag rules" inverses.
54
+ #
55
+ # Examples (delete what doesn't apply, add what does):
56
+ #
57
+ # React Native projects:
58
+ # PATTERNS+=('TouchableOpacity|TouchableHighlight|TouchableNativeFeedback')
59
+ # PATTERNS+=('from .react-native. import .*KeyboardAvoidingView')
60
+ # PATTERNS+=('StyleSheet\.create')
61
+ # PATTERNS+=('forwardRef\(')
62
+ #
63
+ # Postgres/Supabase projects:
64
+ # PATTERNS+=('\.select\(.\*.\)')
65
+ # PATTERNS+=("import.*from\s+['\"]@supabase/auth-helpers") # if deprecated
66
+ #
67
+ # Python projects:
68
+ # PATTERNS+=('print\(') # if logging-only is the rule
69
+ #
70
+ # Generic:
71
+ # PATTERNS+=('console\.log\(') # if console.log banned outside dev
72
+ # PATTERNS+=('debugger;')
73
+ # PATTERNS+=('@ts-ignore') # if @ts-expect-error is preferred
74
+ # ─────────────────────────────────────────────────────────────────────
75
+
76
+ PATTERNS=()
77
+
78
+ # ─────────────────────────────────────────────────────────────────────
79
+ # Run
80
+ # ─────────────────────────────────────────────────────────────────────
81
+ if [ ${#PATTERNS[@]} -eq 0 ]; then
82
+ # No patterns configured — skip silently. /init may pre-fill these based on stack.
83
+ exit 0
84
+ fi
85
+
86
+ # Get added lines from staged diff
87
+ ADDED_LINES=$(git diff --cached --unified=0 --no-color | grep -E '^\+[^+]' || true)
88
+
89
+ if [ -z "$ADDED_LINES" ]; then
90
+ exit 0
91
+ fi
92
+
93
+ FOUND=0
94
+ for pattern in "${PATTERNS[@]}"; do
95
+ matches=$(echo "$ADDED_LINES" | grep -E "$pattern" || true)
96
+ if [ -n "$matches" ]; then
97
+ echo "❌ Banned pattern found: $pattern"
98
+ echo "$matches" | sed 's/^/ /'
99
+ echo ""
100
+ FOUND=1
101
+ fi
102
+ done
103
+
104
+ if [ "$FOUND" -eq 1 ]; then
105
+ echo "Commit blocked. See AGENTS.md § Anti-flag rules for context."
106
+ echo "If this is a deliberate exception, edit .harness/scripts/lint-bans.sh"
107
+ echo "and document the change in your commit body."
108
+ fi
109
+
110
+ exit "$FOUND"
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ # memory-archive.sh — Tier 2 → Tier 3 migration (v2 context architecture).
3
+ #
4
+ # Scans .harness/memory/sessions/recall/*.jsonl. Entries with `ts` older than
5
+ # CCC_ARCHIVE_AGE_DAYS (default 30) are moved to
6
+ # .harness/memory/sessions/archive/<YYYY-MM>.jsonl (by entry's original month).
7
+ #
8
+ # CONTRACT (Claude Code hook spec):
9
+ # stdin: JSON (drained, not parsed)
10
+ # stdout: hookSpecificOutput JSON (silent if nothing archived)
11
+ # exit 0: always
12
+ #
13
+ # IDEMPOTENT: safe to run on every SessionStart. Skips silently if nothing
14
+ # qualifies for archival.
15
+ #
16
+ # Also assigns `id` to any entries lacking one (back-fill for v1 → v2
17
+ # migration). ID format: <KIND_PREFIX>-<YYYYMMDDNNN> where NNN is a 3-digit
18
+ # sequence number within the day.
19
+ #
20
+ # bash 3.2 compatible.
21
+
22
+ set -eu
23
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
24
+
25
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
26
+ RECALL_DIR="$PROJECT_DIR/.harness/memory/sessions/recall"
27
+ ARCHIVE_DIR="$PROJECT_DIR/.harness/memory/sessions/archive"
28
+ AGE_DAYS="${CCC_ARCHIVE_AGE_DAYS:-30}"
29
+
30
+ # Drain stdin
31
+ cat >/dev/null 2>&1 || true
32
+
33
+ # Required dirs
34
+ [ -d "$RECALL_DIR" ] || exit 0
35
+ mkdir -p "$ARCHIVE_DIR" 2>/dev/null || true
36
+
37
+ # jq required
38
+ if ! command -v jq >/dev/null 2>&1; then
39
+ exit 0
40
+ fi
41
+
42
+ # Compute cutoff timestamp (UTC ISO 8601)
43
+ CUTOFF=""
44
+ if date -u -v-${AGE_DAYS}d +%Y-%m-%dT%H:%M:%SZ >/dev/null 2>&1; then
45
+ CUTOFF=$(date -u -v-${AGE_DAYS}d +%Y-%m-%dT%H:%M:%SZ)
46
+ else
47
+ CUTOFF=$(date -u -d "${AGE_DAYS} days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
48
+ fi
49
+ [ -n "$CUTOFF" ] || exit 0
50
+
51
+ ARCHIVED_COUNT=0
52
+ RETAINED_COUNT=0
53
+
54
+ # Process each recall file
55
+ for RECALL_FILE in "$RECALL_DIR"/*.jsonl; do
56
+ [ -f "$RECALL_FILE" ] || continue
57
+ [ -s "$RECALL_FILE" ] || continue
58
+
59
+ TMP_RETAIN=$(mktemp /tmp/memory-archive.retain.XXXXXX)
60
+ trap 'rm -f "$TMP_RETAIN" 2>/dev/null || true' EXIT
61
+
62
+ # Read each line, classify retain vs archive
63
+ while IFS= read -r line || [ -n "$line" ]; do
64
+ [ -z "$line" ] && continue
65
+ if ! echo "$line" | jq empty >/dev/null 2>&1; then
66
+ # malformed: retain so user can fix
67
+ echo "$line" >> "$TMP_RETAIN"
68
+ RETAINED_COUNT=$((RETAINED_COUNT + 1))
69
+ continue
70
+ fi
71
+
72
+ entry_ts=$(echo "$line" | jq -r '.ts // ""')
73
+
74
+ # Back-fill id if missing
75
+ has_id=$(echo "$line" | jq -r 'has("id")')
76
+ if [ "$has_id" = "false" ]; then
77
+ kind=$(echo "$line" | jq -r '.kind // "observation"')
78
+ case "$kind" in
79
+ session-snapshot) prefix="SS" ;;
80
+ decision) prefix="DEC" ;;
81
+ failure) prefix="FAIL" ;;
82
+ *) prefix="OBS" ;;
83
+ esac
84
+ # Date part from ts (YYYYMMDD) + a content-hash-derived suffix
85
+ date_part=$(echo "$entry_ts" | cut -c1-10 | tr -d '-')
86
+ [ -z "$date_part" ] && date_part="00000000"
87
+ hash_suffix=$(echo "$line" | shasum 2>/dev/null | cut -c1-3 || echo "001")
88
+ new_id="${prefix}-${date_part}${hash_suffix}"
89
+ line=$(echo "$line" | jq -c --arg id "$new_id" '.id = $id')
90
+ fi
91
+
92
+ # Compare ts to cutoff (ISO 8601 UTC string-compare works)
93
+ if [ -n "$entry_ts" ] && [ "$entry_ts" \< "$CUTOFF" ]; then
94
+ # Archive — destination by year-month of entry
95
+ year_month=$(echo "$entry_ts" | cut -c1-7)
96
+ [ -z "$year_month" ] && year_month="undated"
97
+ ARCHIVE_FILE="$ARCHIVE_DIR/${year_month}.jsonl"
98
+ echo "$line" >> "$ARCHIVE_FILE"
99
+ ARCHIVED_COUNT=$((ARCHIVED_COUNT + 1))
100
+ else
101
+ echo "$line" >> "$TMP_RETAIN"
102
+ RETAINED_COUNT=$((RETAINED_COUNT + 1))
103
+ fi
104
+ done < "$RECALL_FILE"
105
+
106
+ # Replace recall file with retained content (atomic)
107
+ if [ -s "$TMP_RETAIN" ]; then
108
+ mv "$TMP_RETAIN" "$RECALL_FILE"
109
+ else
110
+ : > "$RECALL_FILE"
111
+ rm -f "$TMP_RETAIN" 2>/dev/null || true
112
+ fi
113
+ done
114
+
115
+ # If anything was archived, emit additionalContext so the AI knows
116
+ if [ "$ARCHIVED_COUNT" -gt 0 ]; then
117
+ jq -n \
118
+ --arg n "$ARCHIVED_COUNT" \
119
+ --arg r "$RETAINED_COUNT" \
120
+ --arg age "$AGE_DAYS" \
121
+ '{
122
+ hookSpecificOutput: {
123
+ hookEventName: "SessionStart",
124
+ additionalContext: ("📦 Memory layer maintenance: archived " + $n + " entries older than " + $age + " days. " + $r + " entries remain in recall. Use /recall --deep <query> to search archived entries.")
125
+ }
126
+ }'
127
+ fi
128
+
129
+ exit 0
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bash
2
+ # memory-recall.sh — SessionStart hook for CCC-MAGI v2 (manifest mode).
3
+ #
4
+ # v2 CHANGE: instead of injecting full entry bodies (~2-3KB), inject ONLY a
5
+ # manifest of one-line index entries (~80 tokens each) for the AI to scan. If
6
+ # the AI decides a body is relevant per CLAUDE.md § Memory Calling Rules, it
7
+ # fetches the body explicitly via the /recall <id> skill.
8
+ #
9
+ # This cuts SessionStart token cost by ~70% on average and prevents "context
10
+ # distraction" (Drew Breunig) from eager injection.
11
+ #
12
+ # SCANS:
13
+ # .harness/memory/sessions/recall/observations.jsonl
14
+ # .harness/memory/sessions/recall/snapshots.jsonl
15
+ # IGNORES (Tier 3):
16
+ # .harness/memory/sessions/archive/ (use /recall --deep instead)
17
+ #
18
+ # OUTPUT FORMAT (one line per entry):
19
+ # [<id>] feature=<f> kind=<k> date=<YYYY-MM-DD> focus="<≤80 chars>"
20
+ #
21
+ # RANKING:
22
+ # - Snapshots always sort BEFORE observations (higher signal density).
23
+ # - Within each, feature-match (+5) and recency (+1 if <7d) score apply.
24
+ # - Cap: top 12 entries OR ~1000 tokens of manifest, whichever first.
25
+ #
26
+ # bash 3.2 compatible.
27
+
28
+ set -eu
29
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
30
+
31
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
32
+ RECALL_DIR="$PROJECT_DIR/.harness/memory/sessions/recall"
33
+ OBS_FILE="$RECALL_DIR/observations.jsonl"
34
+ SNAP_FILE="$RECALL_DIR/snapshots.jsonl"
35
+
36
+ # Drain stdin
37
+ cat >/dev/null 2>&1 || true
38
+
39
+ # Bail if neither file exists with content
40
+ if [ ! -s "$OBS_FILE" ] && [ ! -s "$SNAP_FILE" ]; then
41
+ exit 0
42
+ fi
43
+
44
+ if ! command -v jq >/dev/null 2>&1; then
45
+ exit 0
46
+ fi
47
+
48
+ # Derive current feature from git branch
49
+ BRANCH=""
50
+ if command -v git >/dev/null 2>&1; then
51
+ BRANCH=$(cd "$PROJECT_DIR" 2>/dev/null && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
52
+ fi
53
+
54
+ FEATURE=""
55
+ if [ -n "$BRANCH" ]; then
56
+ case "$BRANCH" in
57
+ feat/*|fix/*)
58
+ rest="${BRANCH#*/}"
59
+ case "$rest" in
60
+ *-*) FEATURE="${rest%%-*}" ;;
61
+ *) FEATURE="$rest" ;;
62
+ esac
63
+ ;;
64
+ */*)
65
+ FEATURE="${BRANCH%%/*}"
66
+ ;;
67
+ esac
68
+ fi
69
+
70
+ # 7-day cutoff
71
+ CUTOFF=""
72
+ if date -u -v-7d +%Y-%m-%dT%H:%M:%SZ >/dev/null 2>&1; then
73
+ CUTOFF=$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ)
74
+ else
75
+ CUTOFF=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
76
+ fi
77
+
78
+ # Score one file's entries. Args: <file> <source_kind:snap|obs>
79
+ # Output to stdout: "<score>\t<ts>\t<id>\t<kind>\t<feature>\t<focus_or_summary>"
80
+ score_file() {
81
+ local file="$1"
82
+ local source_kind="$2"
83
+ [ -s "$file" ] || return 0
84
+ while IFS= read -r line || [ -n "$line" ]; do
85
+ [ -z "$line" ] && continue
86
+ if ! echo "$line" | jq empty >/dev/null 2>&1; then
87
+ continue
88
+ fi
89
+
90
+ local entry_id entry_ts entry_feature entry_kind entry_focus
91
+ entry_id=$(echo "$line" | jq -r '.id // ""')
92
+ entry_ts=$(echo "$line" | jq -r '.ts // ""')
93
+ entry_feature=$(echo "$line" | jq -r '.feature // ""')
94
+ entry_kind=$(echo "$line" | jq -r '.kind // "observation"')
95
+ # focus comes from .focus (snapshot) or .summary (observation)
96
+ entry_focus=$(echo "$line" | jq -r '.focus // .summary // ""')
97
+ # Truncate focus to 80 chars
98
+ if [ "${#entry_focus}" -gt 80 ]; then
99
+ entry_focus="${entry_focus:0:77}..."
100
+ fi
101
+ # Strip tabs and newlines from focus (output field separator safety)
102
+ entry_focus=$(printf '%s' "$entry_focus" | tr '\t\n' ' ')
103
+
104
+ [ -n "$entry_id" ] || continue # skip entries without id (v1 entries get id'd by memory-archive.sh)
105
+
106
+ local score=0
107
+ # Snapshot kind bonus
108
+ if [ "$source_kind" = "snap" ]; then
109
+ score=$((score + 10))
110
+ fi
111
+ # Feature match
112
+ if [ -n "$FEATURE" ] && [ "$entry_feature" = "$FEATURE" ]; then
113
+ score=$((score + 5))
114
+ fi
115
+ # Recency
116
+ if [ -n "$CUTOFF" ] && [ -n "$entry_ts" ]; then
117
+ if [ "$entry_ts" \> "$CUTOFF" ] || [ "$entry_ts" = "$CUTOFF" ]; then
118
+ score=$((score + 1))
119
+ fi
120
+ fi
121
+
122
+ printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$score" "$entry_ts" "$entry_id" "$entry_kind" "${entry_feature:-general}" "$entry_focus"
123
+ done < "$file"
124
+ }
125
+
126
+ SCORED=$(mktemp /tmp/memory-recall.XXXXXX)
127
+ trap 'rm -f "$SCORED" "$SCORED.sorted" 2>/dev/null || true' EXIT
128
+
129
+ # Score snapshots and observations
130
+ score_file "$SNAP_FILE" "snap" >> "$SCORED"
131
+ score_file "$OBS_FILE" "obs" >> "$SCORED"
132
+
133
+ if [ ! -s "$SCORED" ]; then
134
+ exit 0
135
+ fi
136
+
137
+ # Sort by score DESC, then ts DESC
138
+ sort -t "$(printf '\t')" -k1,1nr -k2,2r "$SCORED" > "$SCORED.sorted"
139
+
140
+ # Build manifest. Cap: 12 entries OR ~1000 tokens (~4000 chars)
141
+ MAX_ENTRIES=12
142
+ MAX_CHARS=4000
143
+
144
+ HEADER="## Recent project memory — index (v2)
145
+
146
+ Below is the **manifest** of recent recall-tier entries. Each line is one entry's index, NOT its body. The full body is fetched on demand via the \`/recall <id>\` skill.
147
+
148
+ **When to fetch a body (per CLAUDE.md § Memory Calling Rules)**:
149
+ - User explicitly references prior context (\"上次 / 之前 / before / previously / we decided\")
150
+ - Current task's feature exactly matches an entry's feature
151
+ - An entry's focus indicates a prior decision relevant to your upcoming action
152
+
153
+ **Hard caps**: ≤ 3 body fetches per session. Do NOT fetch \"for completeness\".
154
+
155
+ For older entries (>30d), use \`/recall --deep <query>\` (archive tier, ≤ 1 per session).
156
+
157
+ ---
158
+
159
+ "
160
+
161
+ BODY=""
162
+ count=0
163
+ total_chars=${#HEADER}
164
+
165
+ while IFS= read -r entry_line || [ -n "$entry_line" ]; do
166
+ [ "$count" -ge "$MAX_ENTRIES" ] && break
167
+
168
+ # Parse: score \t ts \t id \t kind \t feature \t focus
169
+ entry_id=$(printf '%s' "$entry_line" | cut -f3)
170
+ entry_kind=$(printf '%s' "$entry_line" | cut -f4)
171
+ entry_feature=$(printf '%s' "$entry_line" | cut -f5)
172
+ entry_focus=$(printf '%s' "$entry_line" | cut -f6-)
173
+ entry_date=$(printf '%s' "$entry_line" | cut -f2 | cut -c1-10)
174
+
175
+ manifest_line="[$entry_id] feature=$entry_feature kind=$entry_kind date=$entry_date focus=\"$entry_focus\""$'\n'
176
+
177
+ new_chars=$((total_chars + ${#manifest_line}))
178
+ if [ "$new_chars" -gt "$MAX_CHARS" ] && [ "$count" -gt 0 ]; then
179
+ break
180
+ fi
181
+
182
+ BODY="$BODY$manifest_line"
183
+ total_chars=$new_chars
184
+ count=$((count + 1))
185
+ done < "$SCORED.sorted"
186
+
187
+ if [ "$count" -eq 0 ]; then
188
+ exit 0
189
+ fi
190
+
191
+ FULL="$HEADER$BODY"
192
+ printf '%s' "$FULL" | jq -Rs '{
193
+ hookSpecificOutput: {
194
+ hookEventName: "SessionStart",
195
+ additionalContext: .
196
+ }
197
+ }'
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ # memory-snapshot.sh — PreCompaction hook (v2: deterministic harvest).
3
+ #
4
+ # v1 (DEPRECATED): asked Claude to write 3 summary entries before compaction.
5
+ # Problem: fights Sonnet 4.5+ native compaction; LLM-cost on the critical path.
6
+ #
7
+ # v2 (ACTIVE): deterministically harvests structured state already on disk
8
+ # into a single session-snapshot entry. No LLM call. Sources:
9
+ # 1. .harness/state/scratchpad.md (current objective + next step)
10
+ # 2. .harness/state/workflow-checkpoints/ (current feature's stage + files done)
11
+ # 3. .harness/memory/conventions.md (project conventions, sampled)
12
+ # 4. git status --short (files in flight)
13
+ #
14
+ # Result is written to .harness/memory/sessions/recall/snapshots.jsonl as a
15
+ # session-snapshot entry. Survives compaction because it's on disk, not in
16
+ # chat. /handoff (user-invoked) writes richer snapshots; this is the
17
+ # fallback for un-supervised auto-compaction.
18
+ #
19
+ # CONTRACT:
20
+ # stdin: JSON (drained)
21
+ # stdout: hookSpecificOutput JSON with a brief notification
22
+ # exit 0: always
23
+ #
24
+ # bash 3.2 compatible.
25
+
26
+ set -eu
27
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
28
+
29
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
30
+ SCRATCHPAD="$PROJECT_DIR/.harness/state/scratchpad.md"
31
+ CHECKPOINT_DIR="$PROJECT_DIR/.harness/state/workflow-checkpoints"
32
+ SNAP_FILE="$PROJECT_DIR/.harness/memory/sessions/recall/snapshots.jsonl"
33
+
34
+ # Drain stdin
35
+ cat >/dev/null 2>&1 || true
36
+
37
+ mkdir -p "$(dirname "$SNAP_FILE")" 2>/dev/null || true
38
+
39
+ if ! command -v jq >/dev/null 2>&1; then
40
+ exit 0
41
+ fi
42
+
43
+ # Determine current feature from git branch
44
+ BRANCH=""
45
+ FEATURE=""
46
+ if command -v git >/dev/null 2>&1; then
47
+ BRANCH=$(cd "$PROJECT_DIR" 2>/dev/null && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
48
+ if [ -n "$BRANCH" ]; then
49
+ case "$BRANCH" in
50
+ feat/*|fix/*)
51
+ rest="${BRANCH#*/}"
52
+ case "$rest" in
53
+ *-*) FEATURE="${rest%%-*}" ;;
54
+ *) FEATURE="$rest" ;;
55
+ esac
56
+ ;;
57
+ */*) FEATURE="${BRANCH%%/*}" ;;
58
+ esac
59
+ fi
60
+ fi
61
+
62
+ # Read scratchpad fields (best effort)
63
+ FOCUS=""
64
+ NEXT_INTENT=""
65
+ if [ -f "$SCRATCHPAD" ]; then
66
+ FOCUS=$(awk '/^## Current objective/{flag=1; next} /^## /{flag=0} flag' "$SCRATCHPAD" 2>/dev/null | sed '/^$/d' | head -1 | head -c 200)
67
+ NEXT_INTENT=$(awk '/^## Next step/{flag=1; next} /^## /{flag=0} flag' "$SCRATCHPAD" 2>/dev/null | sed '/^$/d' | head -1 | head -c 200)
68
+ fi
69
+ [ -z "$FOCUS" ] && FOCUS="Auto-snapshot at compaction (no scratchpad)"
70
+ [ -z "$NEXT_INTENT" ] && NEXT_INTENT="(unset)"
71
+
72
+ # Read latest checkpoint for the feature (if any)
73
+ CHECKPOINT_FILES_DONE="[]"
74
+ CHECKPOINT_STAGE="null"
75
+ if [ -n "$FEATURE" ] && [ -f "$CHECKPOINT_DIR/${FEATURE}.json" ]; then
76
+ CHECKPOINT_STAGE=$(jq -r '.current_stage // "null"' "$CHECKPOINT_DIR/${FEATURE}.json" 2>/dev/null || echo "null")
77
+ CHECKPOINT_FILES_DONE=$(jq -c '.stage_in_progress.files_done_list // []' "$CHECKPOINT_DIR/${FEATURE}.json" 2>/dev/null || echo "[]")
78
+ fi
79
+
80
+ # Files in flight from git status
81
+ FILES_TOUCHED="[]"
82
+ if command -v git >/dev/null 2>&1; then
83
+ FILES_TOUCHED=$(cd "$PROJECT_DIR" 2>/dev/null && git status --short 2>/dev/null | head -10 | awk '{print $NF}' | jq -R . | jq -sc '.' || echo "[]")
84
+ fi
85
+
86
+ TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
87
+ DATE_PART=$(echo "$TS" | cut -c1-10 | tr -d '-')
88
+ RAND_SUFFIX=$(printf '%03d' $((RANDOM % 1000)))
89
+ SS_ID="SS-${DATE_PART}${RAND_SUFFIX}"
90
+
91
+ # Build snapshot entry (auto kind)
92
+ ENTRY=$(jq -c -n \
93
+ --arg id "$SS_ID" \
94
+ --arg ts "$TS" \
95
+ --arg feature "$FEATURE" \
96
+ --arg focus "$FOCUS" \
97
+ --arg next_intent "$NEXT_INTENT" \
98
+ --argjson checkpoint_stage "$CHECKPOINT_STAGE" \
99
+ --argjson files_done "$CHECKPOINT_FILES_DONE" \
100
+ --argjson files_touched "$FILES_TOUCHED" \
101
+ '{
102
+ id: $id,
103
+ ts: $ts,
104
+ kind: "session-snapshot",
105
+ feature: (if $feature=="" then null else $feature end),
106
+ focus: $focus,
107
+ decisions: [],
108
+ open_problems: [],
109
+ next_intent: $next_intent,
110
+ files_touched: $files_touched,
111
+ checkpoint_stage: $checkpoint_stage,
112
+ checkpoint_files_done: $files_done,
113
+ source: "auto-precompaction"
114
+ }')
115
+
116
+ echo "$ENTRY" >> "$SNAP_FILE"
117
+
118
+ # Notify Claude (this is just observational; the snapshot is already saved)
119
+ jq -n --arg id "$SS_ID" '{
120
+ hookSpecificOutput: {
121
+ hookEventName: "PreCompaction",
122
+ additionalContext: ("📸 Auto-snapshot saved (id=" + $id + ") to sessions/recall/snapshots.jsonl. The next session will see this in its recall manifest. For a richer snapshot, use /handoff before compaction next time.")
123
+ }
124
+ }'