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.1.0",
"version": "1.2.0",
"author": {
"name": "0-BSCode"
},
Expand Down
7 changes: 4 additions & 3 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
"name": "sage",
"version": "1.1.0",
"version": "1.2.0",
"description": "Evidence-based learning coach with spaced repetition, retrieval practice, and mastery tracking",
"author": {
"name": "0-BSCode",
"email": "bipsanchez.work@gmail.com"
},
"skills": [
"./"
]
"./skills/sage"
],
"hooks": "./hooks/claude-codex-hooks.json"
}
29 changes: 29 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "sage",
"version": "1.2.0",
"description": "Evidence-based learning coach with spaced repetition, retrieval practice, and mastery tracking",
"author": {
"name": "0-BSCode"
},
"homepage": "https://github.com/0-BSCode/sage",
"repository": "https://github.com/0-BSCode/sage",
"license": "MIT",
"keywords": [
"learning",
"spaced-repetition",
"flashcards",
"mastery-tracking",
"socratic-tutoring"
],
"skills": "./skills/",
"hooks": "./hooks/claude-codex-hooks.json",
"interface": {
"displayName": "Sage",
"shortDescription": "Evidence-based learning coach with spaced repetition",
"longDescription": "Coaches durable mastery of a topic through retrieval practice, spaced repetition, and Socratic questioning rather than lecturing.",
"developerName": "0-BSCode",
"category": "Productivity",
"capabilities": ["Instructions", "Lifecycle hooks"],
"websiteURL": "https://github.com/0-BSCode/sage"
}
}
24 changes: 20 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,25 @@ jobs:
with:
fetch-depth: 0

- name: Manifests must pass schema validation
# Catches schema-level rot the runtime tolerates (unrecognized fields,
# missing metadata). Complements tests/test_plugin_manifests.py, which
# checks manifest-vs-layout coherence instead.
run: npx -y @anthropic-ai/claude-code plugin validate . --strict

- name: Version fields must match (plugin.json is authoritative)
run: |
plugin=$(jq -r .version .claude-plugin/plugin.json)
marketplace=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)
codex=$(jq -r .version .codex-plugin/plugin.json)
if [ "$plugin" != "$marketplace" ]; then
echo "::error::marketplace.json ($marketplace) must mirror plugin.json ($plugin) — see docs/RELEASING.md"
exit 1
fi
if [ "$plugin" != "$codex" ]; then
echo "::error::.codex-plugin/plugin.json ($codex) must mirror .claude-plugin/plugin.json ($plugin) — see docs/RELEASING.md"
exit 1
fi
echo "Versions in sync: $plugin"

- name: Shipping changes must bump version and update changelog
Expand All @@ -51,13 +62,18 @@ jobs:
# Shipped files. Manifest edits count only when something OTHER than
# the version fields changed — otherwise a bump alone would make a
# docs-only PR look like a shipping change and dodge the rejection.
shipped=$(git diff --name-only "$base"...HEAD -- SKILL.md agents hooks references tools)
for f in $(git diff --name-only "$base"...HEAD -- .claude-plugin); do
shipped=$(git diff --name-only "$base"...HEAD -- SKILL.md agents hooks references skills tools)
for f in $(git diff --name-only "$base"...HEAD -- .claude-plugin .codex-plugin); do
case "$f" in
.claude-plugin/plugin.json) strip='del(.version)' ;;
.claude-plugin/marketplace.json) strip='del(.plugins[].version)' ;;
.claude-plugin/plugin.json|.codex-plugin/plugin.json) strip='del(.version)' ;;
.claude-plugin/marketplace.json) strip='del(.plugins[].version)' ;;
*) shipped="$shipped"$'\n'"$f"; continue ;;
esac
# A manifest absent from base is new — a shipping change, and
# `git show` on it would fail the step.
if ! git cat-file -e "$base:$f" 2>/dev/null; then
shipped="$shipped"$'\n'"$f"; continue
fi
if [ "$(git show "$base":"$f" | jq "$strip")" != "$(jq "$strip" "$f")" ]; then
shipped="$shipped"$'\n'"$f"
fi
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
context/
__pycache__
CONTEXT.md
docs/adr/
graveyard/
.obsidian/
72 changes: 72 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,78 @@ 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.2.0] - 2026-08-04

Multi-Host support: Sage now installs on Codex CLI with one command, alongside
Claude Code. Minor, not major — the **Compatibility Surface** is untouched:
`/sage learn <topic>` still works and every existing **Artifact** reads unchanged.
Most of this improves the Claude build too, because it is mostly deletion.

Design and decisions: ADRs 0006–0008.

### Added

- `.codex-plugin/plugin.json` — Codex install. Both manifests point at one
`hooks/claude-codex-hooks.json`; `${CLAUDE_PLUGIN_ROOT}` expands on both Hosts
and Codex normalizes the event names, so there is no per-Host hook config.
- `skills/sage/agents/openai.yaml` — Codex UI metadata and invocation policy.
- `tools/cross_refs_check.py` — the cross-refs invariant, extracted from the hook.
- `tests/test_plugin_manifests.py` — manifest/layout coherence, Host-neutral prose,
and router grammar messages.
- `scripts/link-skills.sh` — development only; links the working tree into every
Host's skill directory.

### Changed

- **Layout:** `SKILL.md` and `references/` moved under `skills/sage/`. Codex's
`skills` field points at a *container* of skill directories. `agents/`, `tools/`,
and `hooks/` stay at the plugin root, so `$SAGE_ROOT` is unchanged.
- **Invocation:** Sage is now user-invoked in both harnesses
(`disable-model-invocation: true`, `allow_implicit_invocation: false`). It
side-effects on invocation, so no Host should fire it implicitly. There is no
longer a conversational path back into a session — type the command.
- **The coach no longer parses the invocation.** The learner's request is passed to
`session_router.py` verbatim; `parse_invocation` remains the only parser on every
Host. Router grammar messages dropped their `/sage` prefix — on a Host without
slash commands they were naming a command that does not exist.
- **Delegation is prose.** All 14 `Task(subagent_type=…)` call sites now name the
Clerk and lead with its spec pointer. No Host API appears in the prompt layer.
- **Hooks:** `checkpoint-guard` moved to `SubagentStart`, `reset-verification` to
`SubagentStop`. Both identify a Clerk by registered type *or* by the spec pointer
in the prompt, since Sage registers no Codex agents. All scripts now fail open and
carry a 5s timeout.
- **The one blocking invariant moved into a tool.** `enforce-cross-refs.sh` is the
only hook that blocks; its check now lives in `tools/cross_refs_check.py` and runs
from `session_wrapup.py` too, so it holds on Hosts with no hooks. The hook is the
automatic trigger on Claude and Codex.
- Every bootstrap line prefers an exported root:
`SAGE_ROOT="${SAGE_ROOT:-$(cat /tmp/.sage-plugin-root 2>/dev/null)}"`.
- Hook state files renamed `/tmp/claude-*` → `/tmp/sage-*`.
- `ref-subagents.md` gained the three operations it always omitted —
`coach-reflect`, `patch-metrics`, `verify-demo` — all of which the prompt layer
already called.

### Fixed

- **Duration could be fabricated off-Claude.** With no `CLAUDE_CODE_SESSION_ID`,
`session_duration.py` fell back to "newest transcript under the cwd" and returned
an unrelated session's wall time with exit 0, straight into the journal.
`session_wrapup.py` now skips the call entirely when no session id identifies a
transcript, and the Clerk asks the learner instead (the degradation ADR 0004 built).
- `tests/test_enforce_cross_refs.py` depended on `/tmp/.sage-plugin-root` existing,
so it only passed on a machine with Sage installed.
- `enforce-cross-refs.sh` cited a `CLAUDE.md` Cross-Reference Protocol that does not
exist in this repo.
- The session-metrics removal plan read `Status: not started` long after the code
shipped.

### Notes

- `verification-gate`'s `audit` operation is documented as **not reachable** from the
current grammar — nothing calls it, and adding a verb is an ADR 0002 decision.
- Known issue, accepted: `/tmp/.sage-plugin-root` is one global slot shared by every
Host. See `docs/KNOWN-ISSUES.md` for the escape hatch.

## [1.1.0] - 2026-07-29

Over-engineering audit: ~1,900 lines removed from `tools/`, no feature lost.
Expand Down
132 changes: 132 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Sage

A plugin that turns an agentic coding tool into an evidence-based tutor, teaching through questioning rather than lecturing. Ships to Claude Code and Codex CLI; see **Host** below.

## Language

### Learning Structure

**Learner**:
The person using Sage to build durable mastery of a topic through coached sessions.
_Avoid_: student, user

**Learning Root**:
The user-configured directory where all learning topic directories live.
_Avoid_: project root, sage directory, base path

**Topic**:
A single subject the learner is studying (e.g., "react-hooks", "statistics"). The *subject* — distinct from the **Project** directory that realizes it on disk.
_Avoid_: course, module, subject

**Project**:
The on-disk container for a **Topic** — the `<slug>/` directory under the learning root that holds `learning/`, any `capstone/`, and is keyed by the topic's **Cross-Refs** shard. Exactly one Project per Topic. "Project" is the right word when the referent is the directory/unit (discovery, archival, the cross-ref registry); "Topic" is the right word when the referent is the subject. The code has always used "project" for this; it is now a defined term, not a loose synonym for Topic.
_Avoid_: folder (when the container-with-artifacts meaning is intended), topic (when the directory, not the subject, is meant)

**Archive** (verb):
To retire a **Project** by moving its directory to `<learning_root>/.archive/<slug>/` (numeric-suffixed on collision), co-locating its cross-ref shard there, and scrubbing every reference to it from the cross-refs `INDEX.md`. An **Archived Project** no longer appears in the `learn` picker. Archival is **one-way by design**: there is no `unarchive` command and none is planned. Nothing is deleted — the artifacts stay readable under `.archive/` for reference, and an `archive-meta.json` stash preserves the removed INDEX fragments so a human can restore by hand — but returning to a topic means starting a fresh **Project**, not reactivating the old one. Archiving is a deliberate, confirmed choice to give up the **Knowledge Map**, **Card**, and SRS state, keeping only the artifacts as a record.
_Avoid_: delete, remove, retire (as the on-disk operation); unarchive, restore (no such operation exists)

**Artifact**:
A structured file within a topic's `learning/` directory that tracks learning state. Includes plan, journal, knowledge map, cards, weak spots, and coach errors.
_Avoid_: file, document, output

**Session**:
A single learning interaction between the coach and the learner. Produces journal entries, card updates, and a savepoint. **One Session is exactly one Sitting** — a Session is assumed to be an unbroken stretch of work, so a break long enough to end the **Sitting** ends the Session. Distinct from the *Claude Code session*, the editor's process-level unit (`CLAUDE_CODE_SESSION_ID`, one transcript file), which survives compact/resume and so can span several Sessions.
_Avoid_: conversation, chat; bare "session" when the Claude Code session is meant — always qualify it

**Sitting**:
An unbroken stretch of activity in a Claude Code transcript, bounded by a quiet gap longer than 30 minutes. A **Session**'s recorded `Duration` is the wall time of its Sitting. Load-bearing in code well before it was a defined term (`SITTING_GAP_SECONDS`, `current_sitting()` in `tools/session_duration.py`). Because one Session is one Sitting, the *last* Sitting in a transcript is by definition the current Session's — which is why duration is measured from the last long gap rather than from the top of the file.
_Avoid_: session (the transcript-level unit), block, stretch, sprint

**Savepoint**:
A snapshot of where a session ended, enabling seamless resume. Stored in the journal entry.
_Avoid_: checkpoint, bookmark

**Host**:
The agentic coding tool Sage runs inside — Claude Code, Codex CLI, Gemini CLI, Cursor. A Host supplies the skill entry point, and may or may not supply lifecycle hooks and a subagent facility. Sage's protocol and engine are Host-neutral; only the manifests and the hook wiring are per-Host. Distinct from the *Claude Code session*, which is one Host's process-level unit.
_Avoid_: agent (means a **Clerk** in Sage's vocabulary), harness, editor, platform

**Clerk**:
One of the six operational subagents (`agents/*.md`) the coach delegates to — artifact-clerk, assessment-agent, verification-gate, reference-clerk, demo-generator, capstone-architect. Clerks exist for context isolation and make no pedagogical decisions. A Host without a subagent facility runs their operations inline, at the cost of context, not correctness.
_Avoid_: agent (unqualified — ambiguous with **Host**), subagent (when the Sage-defined role is meant), helper

### Mastery Tracking

**Knowledge Map**:
A table tracking every concept within a topic, its mastery status, and when it was last tested.
_Avoid_: progress tracker, skill tree

**Card**:
A flashcard with a question and answer, scheduled for spaced review by the SRS engine.
_Avoid_: flashcard, quiz item

**Weak Spot**:
A specific misconception or knowledge gap identified during a session, tracked for targeted drilling.
_Avoid_: error, mistake, gap

**Coach Error**:
A mistake made by the coach (wrong fact, incorrect grading), distinct from learner weak spots. Tracked separately in `coach-errors.md`.
_Avoid_: bug, mistake

### Cross-Topic

**Cross-Refs**:
A registry of concept overlaps between topics, stored in `cross-refs/` at the learning root. Updated when knowledge maps change.
_Avoid_: cross-references, links, connections

### Versioning & Release

**Plugin Version**:
The version of the plugin, as declared in `.claude-plugin/plugin.json` — the single source of truth. The copy in `marketplace.json` is a **mirror** that must always be equal; a disagreement is a defect in the mirror, never in `plugin.json`.
_Avoid_: treating the marketplace copy as independently meaningful

**Shipping Change**:
A change to anything a user actually installs and runs: the skill definition, tools, agents, hooks, references, or plugin manifests. Every Shipping Change bumps the Plugin Version in the same change set; changes confined to repo docs, tests, or CI must not.
_Avoid_: release (the merge event), change (unqualified)

**Release**:
The landing of a Shipping Change on the sage repo's main branch. There is no separate release pipeline — merging to main *is* publishing, because installs track the repository directly. Every Release is identified by its Plugin Version (tag `v<version>`) and described by a Changelog entry.
_Avoid_: deploy, publish (as a distinct later step — no such step exists)

**Changelog**:
The user-facing record of Releases (`CHANGELOG.md` in the sage repo), one entry per Plugin Version. The website reflects it; the file is the source of truth.
_Avoid_: release notes (no separate artifact exists)

**Compatibility Surface**:
The two things a Breaking Change can break: **invocation** (how the learner invokes and resumes the skill) and **Artifacts** (which outlive upgrades — a new version must read artifacts written by any earlier 1.x version, or ship a migration).
_Avoid_: API (nothing here is an API in the conventional sense)

**Breaking Change**:
A change that alters the Compatibility Surface; requires a major version bump. Changes to Internal Tools are never Breaking Changes on their own, even when observable behavior changes.
_Avoid_: breaking (for internal-tool behavior changes)

**Internal Tool**:
A CLI tool or agent only the coach invokes — never the learner directly. Internal Tools upgrade in lockstep with the skill and sit outside the Compatibility Surface. (Precedent: `session_duration`'s exit-code change was a patch.)
_Avoid_: API, public tool

## Relationships

- A **Learning Root** contains one or more **Projects**
- A **Topic** is realized on disk as exactly one **Project** (subject ↔ container, 1:1)
- A **Project** contains multiple **Artifacts** in its `learning/` directory
- **Archiving** a **Project** moves its directory under `.archive/` and removes it from `learn` discovery; its **Cross-Refs** shard is co-located and its **INDEX.md** references are scrubbed
- A **Session** produces updates to **Artifacts** and ends with a **Savepoint**
- A **Session** occupies exactly one **Sitting**; one Claude Code session may contain several **Sessions**, each its own **Sitting**, all appended to one transcript
- A **Knowledge Map** tracks **Concepts**, each at a mastery level
- A **Card** belongs to a **Topic** and is scheduled by the SRS engine
- A **Weak Spot** is a learner gap; a **Coach Error** is a coach mistake — they are never mixed
- **Cross-Refs** track overlaps between **Topics** at the **Learning Root** level

## Example dialogue

> **Dev:** "When a learner starts a new **Session**, does the coach create a new **Topic**?"
> **Domain expert:** "No — the **Topic** directory is created during the first session's planning phase. On resume, the coach loads the existing **Artifacts** and continues from the **Savepoint**."

> **Dev:** "Are **Weak Spots** and **Coach Errors** stored in the same file?"
> **Domain expert:** "Never. **Weak Spots** go in `weak-spots.md`, **Coach Errors** go in `coach-errors.md`. The `weak_spot_writer.py` tool enforces this — it refuses to write a CE entry to `weak-spots.md`."

## Flagged ambiguities

- "learning root" vs "sage directory" — resolved: **Learning Root** is the canonical term. It's user-configured, not hardcoded.
- "session" (three meanings) — resolved: **Session** is the learning interaction; **Sitting** is the gap-bounded stretch of activity the duration is measured over; the *Claude Code session* is the editor's process-level unit and must always be named in full. Surfaced by the `session_duration.py` cwd-resolution fix, which reads `CLAUDE_CODE_SESSION_ID` and reports a Sitting's wall time as a Session's `Duration` — legitimate only under the **one Session = one Sitting** assumption, which is now stated rather than implied by a constant.
- "topic" vs "project" — resolved: they are *not* synonyms. **Topic** is the subject; **Project** is the on-disk container (1:1). "Project" was undefined-but-load-bearing in the code (`list_projects`, `cross-refs/<project>.md`, INDEX's `| Project |`); it is now a defined term. Use "Project" for the directory/unit, "Topic" for the subject. Surfaced by the `/sage archive` feature, which operates on the container.
Loading
Loading