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.
- package/README.md +41 -0
- package/bin/cli.js +76 -0
- package/package.json +28 -0
- package/template/.claude/commands/abandon.md +7 -0
- package/template/.claude/commands/add-anti-flag.md +7 -0
- package/template/.claude/commands/add-constitution-clause.md +7 -0
- package/template/.claude/commands/audit-spec.md +7 -0
- package/template/.claude/commands/commit.md +7 -0
- package/template/.claude/commands/constitution-edit.md +7 -0
- package/template/.claude/commands/db-schema.md +7 -0
- package/template/.claude/commands/exam.md +66 -0
- package/template/.claude/commands/execution-plan.md +7 -0
- package/template/.claude/commands/feature-draft.md +7 -0
- package/template/.claude/commands/handoff.md +7 -0
- package/template/.claude/commands/implement.md +7 -0
- package/template/.claude/commands/init.md +7 -0
- package/template/.claude/commands/next.md +7 -0
- package/template/.claude/commands/offload.md +7 -0
- package/template/.claude/commands/pickup.md +7 -0
- package/template/.claude/commands/recall.md +7 -0
- package/template/.claude/commands/remember.md +7 -0
- package/template/.claude/commands/slide.md +87 -0
- package/template/.claude/commands/spec-finalize.md +7 -0
- package/template/.claude/commands/test-fix.md +7 -0
- package/template/.claude/commands/uninstall.md +7 -0
- package/template/.claude/settings.json +161 -0
- package/template/.claude-plugin/plugin.json +41 -0
- package/template/.codex/config.toml +24 -0
- package/template/.codex/hooks.json +4 -0
- package/template/.codex/install-skills.sh +18 -0
- package/template/.codex/skills/exam/SKILL.md +61 -0
- package/template/.codex/skills/slide/SKILL.md +69 -0
- package/template/.harness/agents/README.md +70 -0
- package/template/.harness/agents/_template/junior-agent-template.md +116 -0
- package/template/.harness/agents/backend-reviewer.md +153 -0
- package/template/.harness/agents/frontend-reviewer.md +158 -0
- package/template/.harness/agents/security-reviewer.md +148 -0
- package/template/.harness/agents/test-fixer.md +147 -0
- package/template/.harness/docs/doc-sync.md +29 -0
- package/template/.harness/docs/git-hygiene.md +56 -0
- package/template/.harness/docs/spec-model.md +47 -0
- package/template/.harness/docs/tool-map.md +120 -0
- package/template/.harness/docs/workflow.md +59 -0
- package/template/.harness/scripts/README.md +70 -0
- package/template/.harness/scripts/auditor-gate.sh +388 -0
- package/template/.harness/scripts/bootstrap-check.sh +103 -0
- package/template/.harness/scripts/budget-monitor.sh +223 -0
- package/template/.harness/scripts/check-prereqs.sh +165 -0
- package/template/.harness/scripts/checkpoint-recall.sh +136 -0
- package/template/.harness/scripts/checkpoint-write.sh +281 -0
- package/template/.harness/scripts/decision-log-append.sh +90 -0
- package/template/.harness/scripts/env-check.sh +286 -0
- package/template/.harness/scripts/format-edit.sh +80 -0
- package/template/.harness/scripts/lint-bans.sh +110 -0
- package/template/.harness/scripts/memory-archive.sh +129 -0
- package/template/.harness/scripts/memory-recall.sh +197 -0
- package/template/.harness/scripts/memory-snapshot.sh +124 -0
- package/template/.harness/scripts/post-migration.sh +58 -0
- package/template/.harness/scripts/precommit-cycles.sh +74 -0
- package/template/.harness/scripts/precommit-typecheck.sh +69 -0
- package/template/.harness/scripts/scratchpad-recall.sh +83 -0
- package/template/.harness/scripts/scratchpad-update.sh +39 -0
- package/template/.harness/scripts/standalone-bootstrap.md +443 -0
- package/template/.harness/skills/abandon/SKILL.md +157 -0
- package/template/.harness/skills/add-anti-flag/SKILL.md +205 -0
- package/template/.harness/skills/add-constitution-clause/SKILL.md +244 -0
- package/template/.harness/skills/audit-spec/SKILL.md +395 -0
- package/template/.harness/skills/commit/SKILL.md +270 -0
- package/template/.harness/skills/constitution-edit/SKILL.md +292 -0
- package/template/.harness/skills/db-schema/SKILL.md +145 -0
- package/template/.harness/skills/db-schema/references/methodology.md +202 -0
- package/template/.harness/skills/execution-plan/SKILL.md +346 -0
- package/template/.harness/skills/feature-draft/SKILL.md +426 -0
- package/template/.harness/skills/handoff/SKILL.md +211 -0
- package/template/.harness/skills/implement/SKILL.md +355 -0
- package/template/.harness/skills/init/SKILL.md +805 -0
- package/template/.harness/skills/next/SKILL.md +245 -0
- package/template/.harness/skills/offload/SKILL.md +134 -0
- package/template/.harness/skills/pickup/SKILL.md +213 -0
- package/template/.harness/skills/recall/SKILL.md +159 -0
- package/template/.harness/skills/remember/SKILL.md +205 -0
- package/template/.harness/skills/spec-finalize/SKILL.md +196 -0
- package/template/.harness/skills/test-fix/SKILL.md +363 -0
- package/template/.harness/skills/uninstall/SKILL.md +370 -0
- package/template/.harness/state/install.json +83 -0
- package/template/AGENTS.md +262 -0
- package/template/CCC_MAGI_LICENSE +201 -0
- package/template/CCC_MAGI_README.md +986 -0
- package/template/CLAUDE.md +658 -0
- package/template/codex.md +39 -0
- package/template/constitution.md +164 -0
- package/template/course/README.md +15 -0
- package/template/course/course_code(example)/exam/README.md +2 -0
- package/template/course/course_code(example)/slide/slide_example-1.pdf +40 -0
- package/template/course/course_code(example)/slide/slide_example-2.pdf +40 -0
- package/template/docs/features/slide-query-implementation.md +79 -0
- package/template/docs/features/slide-query.md +211 -0
- package/template/docs-harness/README.md +42 -0
- package/template/docs-harness/adoption-playbook.md +373 -0
- package/template/docs-harness/ccc-step1-driver-template.md +288 -0
- package/template/docs-harness/cli-configs-README.md +78 -0
- package/template/docs-harness/context-architecture-v2.md +249 -0
- package/template/docs-harness/design-spec.md +437 -0
- package/template/docs-harness/memory-layer.md +135 -0
- package/template/docs-harness/retrospective-notes.md +204 -0
- 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
|
+
}'
|