diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..0b1c215 --- /dev/null +++ b/.github/workflows/tag.yml @@ -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 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 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" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 880f6b3..c92bb4b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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." diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f424fb --- /dev/null +++ b/CHANGELOG.md @@ -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 ` — 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 ` or `/sage archive `. The free-form + `/sage ` 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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..1892090 --- /dev/null +++ b/docs/RELEASING.md @@ -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` 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.