Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"name": "sage",
"source": "./",
"description": "Evidence-based learning coach with spaced repetition, retrieval practice, and mastery tracking",
"version": "1.0.2",
"version": "1.1.0",
"author": {
"name": "0-BSCode"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
"name": "sage",
"version": "1.0.2",
"version": "1.1.0",
"description": "Evidence-based learning coach with spaced repetition, retrieval practice, and mastery tracking",
"author": {
"name": "0-BSCode",
Expand Down
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ All notable changes to the sage plugin are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning rules: see [docs/RELEASING.md](docs/RELEASING.md).

## [1.1.0] - 2026-07-29

Over-engineering audit: ~1,900 lines removed from `tools/`, no feature lost.
Every cut was made against evidence of use — invocations recovered from session
transcripts, plus on-disk artifact state where a command writes. Minor, not
major: everything removed is an **Internal Tool**, outside the compatibility
surface.

### Removed

- Uninvoked tool subcommands: `journal_writer validate`, `weak_spot_writer
validate|fix`, `kmap_writer validate|fix-legend|ensure-sections`,
`assessment_engine coverage|stats|calibrate`, `coach_metrics trends|compare`,
and `srs_engine due --sort risk`. None had a caller in the shipped markdown
or a single invocation in five weeks of session transcripts.
- `tools/srs/find_duplicate_cards.py` — orphaned; `card_writer append` already
rejects duplicates at write time, which is the root-cause fix.
- Learner-level calibration. `estimated_level` was written only by `calibrate`
and read only by `stats`; the adaptive selector never consulted it. The
`learner_calibration` field stays in existing question banks and is ignored.
- Hook debug logging to `/tmp/sage-hook-debug.log`. Nothing read it, and under
`set -euo pipefail` an unwritable log could kill `enforce-cross-refs` before
it emitted its block decision — the guard would have failed open, silently.
- `plateau_detector`'s five threshold override flags, `session_wrapup`'s unused
`topic_slug` positional (extra arguments are ignored, so existing callers
keep working), and `session_duration`'s `find_transcript`/`run` wrappers.

### Changed

- The demo index is now `docs/demos/index.md` instead of `index.html`. The old
writer parsed its own generated HTML back out on every append and had already
dropped a live entry that way; rows are now kept as text and never re-parsed.
**Existing `index.html` files are not migrated** — convert by hand, or the
next appended demo starts a fresh `index.md`.
- `assessment_engine init` is now documented. It was always required — every
other subcommand fails without it — but appeared in no shipped markdown, so
it was being improvised. The agent is also told to pass `<topic>/learning/`
explicitly: a path one level too high silently creates a second, empty
question bank instead of erroring.
- `card_writer validate` and `demo_index_writer validate` documented as manual
diagnostics.

### Fixed

- The 68 assessment-engine tests never ran. They lived under `tools/`, which
`testpaths` excluded, so CI collected 296 tests instead of 364.

## [1.0.2] - 2026-07-20

### Added
Expand Down
7 changes: 5 additions & 2 deletions agents/artifact-clerk.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The `Project:` field is optional. When provided, use it as the canonical project
- `coach-insights.md` — coach behavioral rules. Read this file. If it does not exist, note "None — file not present" in the Coach Insights section.
- `metrics/dashboard.md` (coach effectiveness metrics, if present)
- `docs/references/index.md` (reference document index, if it exists)
- `docs/demos/index.html` (demo index, if it exists)
- `docs/demos/index.md` (demo index, if it exists)
- `../capstone/capstone.md` (capstone project spec, if it exists — lives in `capstone/` sibling to `learning/`)
- `cross-refs/INDEX.md` (cross-project topic registry index — look for the `cross-refs/` directory by walking up from the learning path to the repo root. Search up to 4 parent directories from the specified path.)
- From INDEX.md, find the current project's row and load `cross-refs/<current-project>.md` plus each file listed in the "Overlaps With" column. From overlapping project files, extract only rows where the current project appears in "Also Covered In."
Expand Down Expand Up @@ -423,6 +423,9 @@ Where `<json>` is:
python3 "$SAGE_ROOT/tools/srs/card_writer.py" fix <path>/cards.md
```
This normalizes all cards to canonical compact format. Run this even if no new cards were added — it catches drift from prior sessions.
- **Manual repair only:** if a hand-edit is suspected of breaking `cards.md`,
`card_writer.py validate <path>/cards.md` reports what is wrong without
changing anything. It is a diagnostic — `fix` is what repairs.

### Step 5: Run SRS sync
```bash
Expand Down Expand Up @@ -728,7 +731,7 @@ Duration: <wall time, e.g. "42m15s">
All artifact formats are defined in the Sage skill. You must match them exactly:
- Journal entry format: One file per session in `journal/session-NN.md`, starting with `## Session N — YYYY-MM-DD` with subsections
- Journal index: NEVER write to `journal/index.md` directly. Use `journal_writer.py append <path> --stdin`. Canonical 8-column format: `| # | Date | Type | Focus | Reviews | Avg Grade | Summary | File |`.
- Knowledge map: markdown table with columns `| Concept | Status | Introduced | Last Tested | Notes |`. The `Introduced` column is set once when a concept is first added (`S<N>` or `prior`) and never modified. Concepts table rows are managed by `kmap_writer.py` — use `add-concept` to add new rows and `update-status` to change status/last-tested/notes (preserves Introduced automatically). Status legend and Status Changelog use `fix-legend`, `ensure-sections`, `changelog-append` subcommands. Weak spot tracking has moved to `weak-spots.md` via `weak_spot_writer.py`.
- Knowledge map: markdown table with columns `| Concept | Status | Introduced | Last Tested | Notes |`. The `Introduced` column is set once when a concept is first added (`S<N>` or `prior`) and never modified. Concepts table rows are managed by `kmap_writer.py` — use `add-concept` to add new rows and `update-status` to change status/last-tested/notes (preserves Introduced automatically). The Status Changelog uses the `changelog-append` subcommand. Weak spot tracking has moved to `weak-spots.md` via `weak_spot_writer.py`.
- Cross-refs: table columns are exactly `| Concept | Also Covered In | Status | Notes |`. Do not rename or reorder columns.
- Cards: NEVER write to `cards.md` directly. Use `card_writer.py append <path> --stdin`. Canonical format: `**Q:**`, `**A:**`, `**Tags:**`.
- Weak spots and coach errors: NEVER write entries directly to `weak-spots.md` or `coach-errors.md`. Use `weak_spot_writer.py append --kind <WS|M|CE|CP> <path> --stdin`. Kind routes the entry to the correct file and prefix namespace. The writer refuses to write a coach entry to `weak-spots.md` and vice versa. Canonical formats: learner `## WS-[N] — [description]` (with Category, Correct model, History subsection), coach content `## CE-[N] — [description]`, coach process `## CP-[N] — [description]`. WS field set: Category, Session, Last tested, What happened, Correct model, Why it matters, Cards, Concepts, Status + History subsection. CE/CP field set: Session, What happened, Root cause, Correction, Why it matters, Follow-up, Source, Cards, Status.
21 changes: 21 additions & 0 deletions agents/assessment-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ SAGE_ROOT=$(cat /tmp/.sage-plugin-root)
```
Then use `$SAGE_ROOT/tools/...` in all subsequent commands within the same bash call.

## The Question Bank

Every operation below reads or writes `questions.json`, the per-topic question
bank. It does not exist until it is created, and every other subcommand fails
with `Error: … not found. Run \`init\` first.` until it does.

**Create it once, on first use for a topic:**
```bash
SAGE_ROOT=$(cat /tmp/.sage-plugin-root)
python3 "$SAGE_ROOT/tools/assessment/assessment_engine.py" init <path>
```
`init` reads the topic's `knowledge-map.md` and seeds one coverage entry per
concept. It refuses to overwrite an existing bank without `--force`, so running
it when unsure is safe.

**`<path>` is always the `learning/` directory** — `<topic-slug>/learning/`,
never the topic directory above it. The engine resolves any non-directory
argument to its parent, so a path pointed one level too high silently creates a
*second*, empty bank at `<topic-slug>/questions.json` while the real one sits
untouched in `learning/`. Two banks, split state, no error. Pass `learning/`.

## Operations

You support four operations, determined by the `Operation:` field in your prompt.
Expand Down
8 changes: 6 additions & 2 deletions agents/demo-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Write to `docs/demos/<weak-spot-slug>.html`.

### Step 7: Update the demo index

Do NOT write to `docs/demos/index.html` directly. Use the `demo_index_writer.py` script which guarantees canonical HTML format, handles deduplication by WS-number, and creates the index file if it doesn't exist.
Do NOT write to `docs/demos/index.md` directly. Use the `demo_index_writer.py` script which guarantees canonical markdown format, handles deduplication by WS-number, and creates the index file if it doesn't exist.

Build a JSON object from the demo metadata and pipe it to the script:
```bash
Expand All @@ -174,6 +174,10 @@ Where `<json>` is:
```
All fields except `related_reference` are required. If no related reference doc exists, omit the field or pass an empty string.

If the index looks wrong — a demo file listed that no longer exists, or an entry
that vanished — `demo_index_writer.py validate <path>/docs/demos/` reports the
problems without modifying anything.

### Step 8: Return confirmation

```markdown
Expand Down Expand Up @@ -209,7 +213,7 @@ Updates:
1. Read the existing demo file from `docs/demos/`
2. Read current state of `weak-spots.md` to get updated weak spot details
3. Apply the requested updates while preserving the interaction design
4. Update the index entry in `docs/demos/index.html`
4. Update the index entry in `docs/demos/index.md`
5. Return a confirmation showing what changed

---
Expand Down
10 changes: 5 additions & 5 deletions hooks/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Sage Hooks

## Debug Log
## Debugging

All hook scripts write timestamped traces to `/tmp/sage-hook-debug.log`. Check this file to verify whether hooks fired and which branch they took.
To see whether a hook fired and which branch it took, run it under `bash -x`
with a sample event on stdin:

```bash
cat /tmp/sage-hook-debug.log
echo '{"session_id":"test","cwd":"'"$PWD"'","stop_hook_active":false}' \
| bash -x hooks/scripts/enforce-cross-refs.sh
```

Each entry: `HH:MM:SS <hook-name>: <outcome>`.

## Hook Reference

| Hook | Event | Script | Purpose |
Expand Down
7 changes: 0 additions & 7 deletions hooks/scripts/checkpoint-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@ set -euo pipefail

INPUT=$(cat)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

# Only care about Agent tool calls
if [ "$TOOL_NAME" != "Agent" ]; then
exit 0
fi

SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty')
PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty')

Expand Down
10 changes: 0 additions & 10 deletions hooks/scripts/enforce-cross-refs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@

set -euo pipefail

DEBUG_LOG="/tmp/sage-hook-debug.log"

SAGE_DIR="${SAGE_DIR:-$(cat /tmp/.sage-learning-root 2>/dev/null)}"
if [ -z "$SAGE_DIR" ]; then
echo "$(date '+%H:%M:%S') cross-refs: skip — no SAGE_DIR" >> "$DEBUG_LOG"
exit 0
fi
THRESHOLD=1800 # 30 minutes
Expand All @@ -20,13 +17,11 @@ CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
echo "$(date '+%H:%M:%S') cross-refs: skip — stop_hook_active" >> "$DEBUG_LOG"
exit 0
fi

# Only fire in the sage repo (or a subdirectory)
if [[ "$CWD" != "$SAGE_DIR"* ]]; then
echo "$(date '+%H:%M:%S') cross-refs: skip — cwd=$CWD not in SAGE_DIR=$SAGE_DIR" >> "$DEBUG_LOG"
exit 0
fi

Expand All @@ -51,12 +46,10 @@ while IFS= read -r km; do
done < <(find "$SAGE_DIR" -name "knowledge-map.md" 2>/dev/null)

if [ "$KM_MODIFIED" != "true" ]; then
echo "$(date '+%H:%M:%S') cross-refs: skip — no recent knowledge-map changes" >> "$DEBUG_LOG"
exit 0
fi

if [ "$KM_HAS_PROMOTED" != "true" ]; then
echo "$(date '+%H:%M:%S') cross-refs: skip — knowledge-map modified but no concepts at developing+" >> "$DEBUG_LOG"
exit 0
fi

Expand All @@ -77,15 +70,12 @@ fi

# Knowledge map modified but cross-references weren't — block
if [ "$CR_UPDATED" != "true" ]; then
echo "$(date '+%H:%M:%S') cross-refs: BLOCK — km modified, cross-refs not updated" >> "$DEBUG_LOG"
cat <<'EOF'
{
"decision": "block",
"reason": "Knowledge map(s) were modified this session but cross-refs/ was not updated. Per CLAUDE.md Cross-Reference Protocol: upsert any concept that reached Developing or higher into cross-refs/<project>.md before ending the session."
}
EOF
else
echo "$(date '+%H:%M:%S') cross-refs: pass — km modified, cross-refs updated" >> "$DEBUG_LOG"
fi

exit 0
9 changes: 0 additions & 9 deletions hooks/scripts/reset-verification.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@

set -euo pipefail

DEBUG_LOG="/tmp/sage-hook-debug.log"
INPUT=$(cat)

SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

# Only care about Agent tool calls
if [ "$TOOL_NAME" != "Agent" ]; then
exit 0
fi

SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty')
PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty')
Expand All @@ -35,12 +28,10 @@ if [[ "$SUBAGENT_TYPE" == *"artifact-clerk" ]]; then
fi

if [[ "$SUBAGENT_TYPE" != *"verification-gate" ]]; then
echo "$(date '+%H:%M:%S') reset-verif: skip — subagent_type=$SUBAGENT_TYPE (not verification-gate)" >> "$DEBUG_LOG"
exit 0
fi

# Reset the message counter (creates it if first call)
echo "$(date '+%H:%M:%S') reset-verif: RESET counter (subagent_type=$SUBAGENT_TYPE)" >> "$DEBUG_LOG"
COUNTER_FILE="/tmp/claude-verif-counter-${SESSION_ID}"
echo "0" > "$COUNTER_FILE"

Expand Down
4 changes: 0 additions & 4 deletions hooks/scripts/verification-counter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

set -euo pipefail

DEBUG_LOG="/tmp/sage-hook-debug.log"
INPUT=$(cat)

SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
Expand All @@ -23,7 +22,6 @@ fi

# No counter file = not in a verification-tracked session
if [ ! -f "$COUNTER_FILE" ]; then
echo "$(date '+%H:%M:%S') verif-counter: skip — no counter file (session=$SESSION_ID)" >> "$DEBUG_LOG"
exit 0
fi

Expand All @@ -33,11 +31,9 @@ COUNT=$((COUNT + 1))
echo "$COUNT" > "$COUNTER_FILE"

# Warn once at 5+
echo "$(date '+%H:%M:%S') verif-counter: count=$COUNT (session=$SESSION_ID)" >> "$DEBUG_LOG"
if [ "$COUNT" -ge 5 ]; then
WARNED_FILE="/tmp/claude-verif-warned-${SESSION_ID}"
if [ ! -f "$WARNED_FILE" ]; then
echo "$(date '+%H:%M:%S') verif-counter: WARNING FIRED at count=$COUNT" >> "$DEBUG_LOG"
echo "1" > "$WARNED_FILE"
cat <<EOF
{
Expand Down
2 changes: 1 addition & 1 deletion references/ref-session-end.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ After all post-checkpoint work is complete, run the wrapup script:

```bash
SAGE_ROOT=$(cat /tmp/.sage-plugin-root)
python3 "$SAGE_ROOT/tools/session_wrapup.py" "$SAGE_ROOT" "<topic_path>" "<topic-slug>"
python3 "$SAGE_ROOT/tools/session_wrapup.py" "$SAGE_ROOT" "<topic_path>"
```

If `coach_metrics_flags` is non-empty, mention the flags in your session summary.
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
if p not in sys.path:
sys.path.insert(0, p)

# Top-level tools/ for session_duration, mcp_server
# Top-level tools/ for session_duration
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
Loading
Loading