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
32 changes: 32 additions & 0 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Tag release

# Merging to main is publishing (see docs/RELEASING.md). This workflow gives
# every published version a checkout-able identity: it tags v<version> from
# plugin.json if that tag does not already exist. Tags are never cut by hand.

on:
push:
branches: [main]

permissions:
contents: write

jobs:
tag:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Tag v<version> from plugin.json if missing
run: |
version=$(jq -r .version .claude-plugin/plugin.json)
tag="v$version"
if git ls-remote --tags origin "refs/tags/$tag" | grep -q .; then
echo "$tag already exists; nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$tag" -m "Release $tag"
git push origin "$tag"
59 changes: 59 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,62 @@ jobs:

- name: Run tests
run: pytest -v

release-hygiene:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- 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)
if [ "$plugin" != "$marketplace" ]; then
echo "::error::marketplace.json ($marketplace) must mirror plugin.json ($plugin) — see docs/RELEASING.md"
exit 1
fi
echo "Versions in sync: $plugin"

- name: Shipping changes must bump version and update changelog
if: github.event_name == 'pull_request'
run: |
base="origin/${{ github.base_ref }}"
base_version=$(git show "$base":.claude-plugin/plugin.json | jq -r .version)
head_version=$(jq -r .version .claude-plugin/plugin.json)

# 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
case "$f" in
.claude-plugin/plugin.json) strip='del(.version)' ;;
.claude-plugin/marketplace.json) strip='del(.plugins[].version)' ;;
*) shipped="$shipped"$'\n'"$f"; continue ;;
esac
if [ "$(git show "$base":"$f" | jq "$strip")" != "$(jq "$strip" "$f")" ]; then
shipped="$shipped"$'\n'"$f"
fi
done
shipped=$(printf '%s\n' "$shipped" | grep -v '^$' || true)
if [ -z "$shipped" ]; then
if [ "$base_version" != "$head_version" ]; then
echo "::error::Version bumped ($base_version -> $head_version) but no shipped files changed — docs/tests/CI-only changes must not bump the version. See docs/RELEASING.md"
exit 1
fi
echo "No shipping changes; no bump required."
exit 0
fi
if [ "$base_version" = "$head_version" ]; then
echo "::error::Shipping change without a version bump (still $head_version). See docs/RELEASING.md. Shipped files changed:"
echo "$shipped"
exit 1
fi
if [ -z "$(git diff --name-only "$base"...HEAD -- CHANGELOG.md)" ]; then
echo "::error::Version bumped to $head_version but CHANGELOG.md was not updated — see docs/RELEASING.md"
exit 1
fi
echo "Shipping change OK: $base_version -> $head_version with changelog entry."
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Changelog

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.0.2] - 2026-07-20

### 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.

## Earlier versions

Versions before 1.0.2 predate this changelog and are not individually
documented.
42 changes: 42 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Releasing

There is no release pipeline: **merging to main is publishing**, because
installs track this repository directly. These rules exist so every published
change has a version identity and a stated reason.

## The rules

1. **`plugin.json` is authoritative.** The `version` in
`.claude-plugin/plugin.json` is the single source of truth. The copy in
`.claude-plugin/marketplace.json` is a mirror and must always be equal.
CI fails the build if they diverge.

2. **Every shipping change bumps the version.** A *shipping change* is any
change under `SKILL.md`, `agents/`, `hooks/`, `references/`, `tools/`, or
`.claude-plugin/` — the things a user actually installs and runs. Bump the
version in the same PR. Changes confined to repo docs, tests, or CI must
*not* bump the version. CI enforces both directions of this on PRs.

3. **Every version bump gets a changelog entry.** Add a section to
`CHANGELOG.md` in the same PR. CI fails a PR that bumps the version
without touching the changelog.

4. **Tags are cut by CI, never by hand.** On every push to main, CI tags
`v<version>` from `plugin.json` if that tag does not already exist
(`.github/workflows/tag.yml`). Do not create or push release tags manually.

## What the bump size means

The plugin is on the 1.x line but is still maturing; treat versions as:

- **Patch** — fixes and internal changes, including observable behavior
changes to *internal tools* (CLI tools and agents only the coach invokes —
they upgrade in lockstep with the skill and are outside the compatibility
surface). Precedent: `session_duration`'s exit-code change was a patch.
- **Minor** — new user-facing capability.
- **Major** — a change to the **compatibility surface**, which is exactly two
things:
1. *Invocation* — how the user invokes or resumes the skill.
2. *Artifacts* — the learning-journey files written into a user's project.
These outlive upgrades: a new version must read artifacts written by any
earlier 1.x version, or ship a migration.
Loading