Sourced live from CHANGELOG.md in the Sage repository — every shipping change lands there with its version.
v1.2.0
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.
v1.1.0
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.
v1.0.2
Added
/sage archive <topic> — retire a topic's project by moving it to .archive/ under the learning root. One-way by design: artifacts stay readable, but returning to a topic means starting fresh.
Changed
**Breaking:** the /sage entry point now requires a leading verb — /sage learn <topic> or /sage archive <topic>. The free-form /sage <topic> grammar and the bare resume keywords (continue, resume, pick, list) are no longer accepted; learn subsumes them (bare /sage learn opens the project picker). *Note: this release predates the written release rules, under which an invocation change like this would be a major bump.*
Reference docs moved from docs/ to references/ so they ship with the plugin.
Session metrics tracking removed (session_metrics.py and its wrap-up integration).
Fixed
Session transcripts are resolved by CLAUDE_CODE_SESSION_ID instead of the working directory, so durations no longer come from the wrong transcript.
session_duration no longer reports a silently wrong duration when given a session id it cannot find — it now errors with exit code 1. (Classified as a patch: the tool is internal to the coach, outside the compatibility surface.)
Dead references to unshipped files removed from coach-facing docs.
Versions before 1.0.2 predate this changelog and are not individually documented.