release

SkillDocs & knowledge

Prepare release communication and check readiness. Main mode: notes with optional flags --changelog, --summary, --migration, --append (incremental: reruns the full pipeline scoped to newly-landed commits, integrating results into existing DRAFT.md/CHANGELOG.md/SUMMARY.md/MIGRATION.md instead of full regenerate — non-destructive except revert/pivot); range as v1->v2. Other modes: prepare (full pipeline: audit → all artifacts), audit (pre-release readiness: blockers, docs alignment, version consistency, CVEs), demo (story-telling release notebook in jupytext # %% format). TRIGGER when: user requests release notes, CHANGELOG entry, migration guide, internal summary, release readiness audit, release demo, or an incremental update to an already-drafted release; phrases: 'draft release notes', 'prepare release', 'audit release readiness', 'generate CHANGELOG for v1->v2', 'release demo notebook', 'update release notes with the latest commits'. SKIP: actual git tagging or PyPI/registry upload (use git tag, gh release create, twine upload directly); release communication for a non-Python project where this skill's pytest-centric audit assumptions do not apply; PR-level review (use /oss:review); thread/issue analysis (use /oss:analyse).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the release skill

What this skill tells your AI

The instructions your AI receives, as published by borda/ai-rig in plugins/cc_oss/skills/release/SKILL.md and read by ahel’s review.

Prepare release communication from changes. Output adapts to audience — user-facing notes, CHANGELOG entry, internal summary, migration guide.

All outputs = documentation artifacts (CHANGELOG.md, DRAFT.md, MIGRATION.md, SUMMARY.md, demo.py). Released product = code/package published separately via project tooling (git tag, gh release create, PyPI upload). Skill prepares communication; doesn't perform release.

NOT for ecosystem impact without release (use oss:analyse (requires oss plugin)). NOT for contributor communication or post-release announcements (use oss:shepherd (requires oss plugin)). NOT for retrospective analysis — historical review → oss:analyse (requires oss plugin).

Mode comes first; range or flags follow:

InvocationArgumentsWrites to disk
/release [notes] [range]optional range (default: last-tag..HEAD); use v1->v2 for explicit rangeDRAFT.md
/release notes [range] --changelogoptional range + flagDRAFT.md + prepends CHANGELOG.md
/release notes [range] --summaryoptional range + flagDRAFT.md + .temp/output-release-summary-<branch>-<date>.md
/release notes [range] --migrationoptional range + flagDRAFT.md + .temp/output-release-migration-<branch>-<date>.md
/release notes [range] --changelog --summary --migrationall flagsAll four outputs
/release notes --appendno range (derived from last-processed marker); compose with --changelog/--summary/--migrationRuns the full pipeline scoped to the incremental range, integrating results into every existing artifact in place (DRAFT.md always; CHANGELOG.md/SUMMARY.md/MIGRATION.md when their flag is set) instead of regenerating from scratch
/release prepare <version>version to stamp, e.g. v1.3.0All artifacts in releases/<version>/: DRAFT.md + CHANGELOG.md + SUMMARY.md + MIGRATION.md + demo.py
/release audit [version]optional target versionTerminal readiness report; emits verdict: READY | NEEDS_ATTENTION | BLOCKED as final line for orchestrator consumption
/release demo [range]optional range (default: last-tag..HEAD)releases/<version>/demo.py or .temp/release-demo-<branch>-<date>.py

Range notation: v1->v2 (e.g. v1.2->v2.0) — converted internally to git range. No mode → defaults to notes. prepare = full pipeline — runs audit first, then all artifacts; use when cutting release, not drafting.

--append: assumes an earlier notes run already produced DRAFT.md (and, when their flags were used, CHANGELOG.md/SUMMARY.md/MIGRATION.md) and reruns the full pipeline — Gather changes through Draft executive summary, unchanged — scoped to only the commits landed since then, via a per-branch marker at .temp/release-last-processed-<branch> (see bin/release_append_marker.py). No marker found (first use, or history rewritten by rebase/force-push) → falls back to the default $LAST_TAG..HEAD range and full-overwrite write, same as plain notes — establishing the baseline for the next --append run. Every successful notes-mode write (append or full) refreshes the marker to current HEAD.

Non-destructive except revert/pivot: integration is purely additive — new bullets/blocks/paragraphs join existing artifacts without touching untouched content — unless this cycle's Classify/Truth-check phases detect that a new commit reverts or materially changes something a PRIOR cycle already wrote (see Gather changes' "Cross-cycle revert/pivot detection"); that stale entry is struck or superseded, never left stale alongside a contradicting new one. After merge, a Post-merge re-validation pass (see modes/release-draft-template.md) re-runs Truth check, Identify highlights re-ranking, Validate migration docs, and Validate docs against the FINAL merged content — catches prior-cycle content that went stale from THIS cycle's changes without being a clean detected revert/pivot (e.g. a Spotlight built on a commit a later cycle reverts).

Task hygiene: Call TaskList; triage found tasks (completed / deleted / in_progress).

Task tracking — create ALL tasks upfront, execute sequentially; mark completed as each phase finishes. After mode detection, mark inapplicable tasks deleted:

  • demo mode: mark deleted — Classify each change, Classify breaking changes, Validate migration docs, Audit changelog, Extract contributors, Draft migration guide, Draft executive summary, Write release draft, Post-merge re-validation
  • bug-fix-only release (no 🚀 Added items): mark deleted — Generate release demo
  • not --append, or --append with no valid marker ($MARKER_VALID computed false during Write release draft — see release-draft-template.md): mark deleted — Post-merge re-validation

Tasks:

  • Gather changes (git log + find common base tag)
  • Explore codebase (changed files, impl detail)
  • Validate docs alignment
  • Classify each change
  • Classify breaking changes (codemap-gated; skip without index)
  • Validate migration docs (skip when no migration doc found)
  • Audit changelog
  • Extract contributors
  • Identify highlights
  • Draft migration guide
  • Generate release demo (feature releases only)
  • Draft executive summary
  • Write release draft
  • Post-merge re-validation (--append merge only — re-runs Truth check, Identify highlights, Validate migration docs, Validate docs against the final merged DRAFT.md; see release-draft-template.md "Post-merge re-validation")

Sequential enforcement: never begin phase until prior marked completed. On failure (empty range, git error, demo fail), stop and report — no downstream phases.

Delegation strategy

In prepare and audit modes, delegate gather/explore/validate to subagent via file-based handoff (CLAUDE.md §2) — these phases produce large output, bloat main context:

  1. Pre-compute gather file path and create dir:
    # BRANCH, DATE from Shared setup below
    GATHER_FILE=".temp/release-gather-$BRANCH-$DATE.md"
    mkdir -p .temp  # timeout: 5000
    
  2. Assert variables before spawning:
    [ -n "$GATHER_FILE" ] && [ -n "$REPO_ROOT" ] && [ -n "$RANGE" ] || { echo "Error: GATHER_FILE, REPO_ROOT, or RANGE is empty — verify Shared setup and Gather changes completed"; exit 1; }  # timeout: 5000
    export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
    # reload SKILL_DIR (Check 41)
    IFS= read -r SKILL_DIR < "${TMPDIR:-/tmp}/release-setup-${CSID}/SKILL_DIR" 2>/dev/null || SKILL_DIR=""
    cat "$SKILL_DIR/templates/gather-prompt.md"  # timeout: 5000
    

Agent budget — each spawn costs ~120,851 tok of fixed overhead (~73 tool-calls' worth) plus ~12.0 s/call, so work under ~73 calls is cheaper done inline: spawn nothing. Keep each agent near ~55 tool-calls; past ~60 they stall without returning an envelope, forcing reconstruction from disk. Every spawn prompt must require an envelope even on exhaustion — partial: true plus what was finished.

Template (loaded above). Substitute <REPO_ROOT>, <RANGE>, <GATHER_FILE> with literal values. Spawn:

loads: gather-prompt.md Agent(subagent_type="foundry:sw-engineer", prompt=<substituted gather-prompt.md content>)

  1. Validate envelope; every "abort" is a hard exit 1:
    STATUS=$(echo "$ENVELOPE" | jq -r '.status' 2>/dev/null)
    GATHER_FILE=$(echo "$ENVELOPE" | jq -r '.file' 2>/dev/null)
    BREAKING=$(echo "$ENVELOPE" | jq -r '.breaking // 0' 2>/dev/null)  # default 0 — never skip migration guide on missing field
    UNCONFIRMED=$(echo "$ENVELOPE" | jq -r '.unconfirmed // 0' 2>/dev/null)
    UNCONFIRMED_BREAKING=$(echo "$ENVELOPE" | jq -r '.unconfirmed_breaking // 0' 2>/dev/null)
    if [ "$STATUS" != "done" ] || [ -z "$GATHER_FILE" ] || [ "$GATHER_FILE" = "null" ] || [ ! -f "$GATHER_FILE" ]; then
        echo "Error: delegation validation failed — status=$STATUS, file=$GATHER_FILE" >&2
        exit 1
    fi
    

When unconfirmed > 0, surface removed items as notification (not a gate — already removed). Read REMOVED log from $GATHER_FILE:

if [ "${UNCONFIRMED:-0}" -gt 0 ] 2>/dev/null; then
    REMOVED_ITEMS=$(grep '^REMOVED:' "$GATHER_FILE" | head -20)  # timeout: 3000
    echo "Truth check removed ${UNCONFIRMED} unverified claim(s) from release notes (not found in HEAD):"
    echo "$REMOVED_ITEMS"
fi

Pass $GATHER_FILE path to artifact phase — do NOT read gather file into main context; REMOVED log grep above = sole sanctioned exception.

Phases 5–6 parallel delegation (prepare/audit modes, after phases 1–4 complete): Audit changelog and Extract contributors are independent — delegate concurrently to reclaim tokens.

Pre-compute output paths and persist for downstream reload (fresh shell, Check 41):

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# fresh shell loses vars between blocks (Check 41)
IFS= read -r BRANCH < "${TMPDIR:-/tmp}/release-setup-${CSID}/BRANCH" 2>/dev/null || BRANCH=""
IFS= read -r DATE < "${TMPDIR:-/tmp}/release-setup-${CSID}/DATE" 2>/dev/null || DATE=""
IFS= read -r RANGE < "${TMPDIR:-/tmp}/release-range-${CSID}" 2>/dev/null || RANGE=""
IFS= read -r REPO_ROOT < "${TMPDIR:-/tmp}/release-setup-${CSID}/REPO_ROOT" 2>/dev/null || REPO_ROOT=""
GATHER_FILE=".temp/release-gather-$BRANCH-$DATE.md"
[ -f "$GATHER_FILE" ] || { echo "Error: GATHER_FILE missing — phases 1–4 must complete first"; exit 1; }  # timeout: 5000
CHANGELOG_AUDIT_FILE=".temp/release-changelog-audit-$BRANCH-$DATE.md"
CONTRIBUTORS_FILE=".temp/release-contributors-$BRANCH-$DATE.md"
mkdir -p .temp  # timeout: 5000
echo "${CHANGELOG_AUDIT_FILE:-}" > "${TMPDIR:-/tmp}/release-changelog-audit-${CSID}"
echo "${CONTRIBUTORS_FILE:-}" > "${TMPDIR:-/tmp}/release-contributors-${CSID}"
# reload SKILL_DIR (Check 41)
IFS= read -r SKILL_DIR < "${TMPDIR:-/tmp}/release-setup-${CSID}/SKILL_DIR" 2>/dev/null || SKILL_DIR=""
cat "$SKILL_DIR/modes/changelog-audit-prompt.md"  # timeout: 5000

Prompt (loaded above) — execute (spawn Agent A + Agent B per instructions in that file).

Validate both envelopes:

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
STATUS_A=$(echo "$ENVELOPE_A" | jq -r '.status' 2>/dev/null)
CHANGELOG_AUDIT_FILE=$(echo "$ENVELOPE_A" | jq -r '.file' 2>/dev/null)
CHANGELOG_FILE_FROM_A=$(echo "$ENVELOPE_A" | jq -r '.changelog_file // ""' 2>/dev/null)
STATUS_B=$(echo "$ENVELOPE_B" | jq -r '.status' 2>/dev/null)
CONTRIBUTORS_FILE=$(echo "$ENVELOPE_B" | jq -r '.file' 2>/dev/null)
if [ "$STATUS_A" != "done" ] || [ -z "$CHANGELOG_AUDIT_FILE" ] || [ ! -f "$CHANGELOG_AUDIT_FILE" ]; then
    echo "Error: changelog-audit delegation failed — status=$STATUS_A, file=$CHANGELOG_AUDIT_FILE" >&2; exit 1
fi
if [ "$STATUS_B" != "done" ] || [ -z "$CONTRIBUTORS_FILE" ] || [ ! -f "$CONTRIBUTORS_FILE" ]; then
    echo "Error: contributors delegation failed — status=$STATUS_B, file=$CONTRIBUTORS_FILE" >&2; exit 1
fi
# re-persist — subagent may canonicalize paths
echo "${CHANGELOG_AUDIT_FILE:-}" > "${TMPDIR:-/tmp}/release-changelog-audit-${CSID}"
echo "${CONTRIBUTORS_FILE:-}" > "${TMPDIR:-/tmp}/release-contributors-${CSID}"
[ -n "$CHANGELOG_FILE_FROM_A" ] && echo "${CHANGELOG_FILE_FROM_A}" > "${TMPDIR:-/tmp}/release-changelog-file-${CSID}"
ADDED=$(echo "$ENVELOPE_A" | jq -r '.added // 0' 2>/dev/null)
FLAGGED=$(echo "$ENVELOPE_A" | jq -r '.flagged // 0' 2>/dev/null)
SCOPE_FLAGGED=$(echo "$ENVELOPE_A" | jq -r '.scope_flagged // 0' 2>/dev/null)
COUNT=$(echo "$ENVELOPE_B" | jq -r '.count // 0' 2>/dev/null)
echo "Phases 5–6 delegated: $ADDED changelog entries added, $FLAGGED flagged, $SCOPE_FLAGGED scope-flagged (non-PR branch merge); $COUNT contributors extracted."  # timeout: 5000

$SCOPE_FLAGGED > 0 → read the "Scope check" section of $CHANGELOG_AUDIT_FILE; every row there (critical = already-released commit landed a second time, medium = unreviewed branch merge) must reach the Findings summary table in audit/prepare modes — see templates/audit-checks.md Output, "Changelog scope" row — never left inside the audit file only. This is the check the reviewer needs surfaced before approving the PR, not something to bury in a report nobody opens.

notes and demo modes: skip delegation — single-pass; run gather/explore/validate inline. Size guard: estimate commit count with git rev-list --count ${RANGE:-${LAST_TAG:-HEAD~20}..HEAD} 2>/dev/null. If >50, delegate to foundry:sw-engineer subagent same as prepare mode — inline gather with >50 commits causes context flood. Define GATHER_FILE before spawning so envelope-validation block above can resolve the path:

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# reload BRANCH/DATE (Check 41)
IFS= read -r BRANCH < "${TMPDIR:-/tmp}/release-setup-${CSID}/BRANCH" 2>/dev/null || BRANCH=""
IFS= read -r DATE < "${TMPDIR:-/tmp}/release-setup-${CSID}/DATE" 2>/dev/null || DATE=""
GATHER_FILE=".temp/release-gather-$BRANCH-$DATE.md"
mkdir -p .temp  # timeout: 5000

Mode Detection

First tokenMODERouting
prepareprepareShared setup first, then Mode: prepare
auditauditShared setup first, then Mode: audit
demodemoShared setup first, then Mode: demo
notesnotesStrip notes token; parse flags and range from remainder
(bare range or flag)notesParse flags and range from full string
(none)notesRANGE="", no flags; run all phases
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
DO_CHANGELOG=false; DO_SUMMARY=false; DO_MIGRATION=false; DO_APPEND=false
FIRST=$(echo "$ARGUMENTS" | awk '{print $1}')
# empty at single-word ARGUMENTS (cut -d' ' -f2- echoes whole line, no delimiter)
REST=""; case "$ARGUMENTS" in *" "*) REST="${ARGUMENTS#* }";; esac
echo "${REST:-}" > "${TMPDIR:-/tmp}/release-rest-${CSID}"
# strip mode token — else leaks into RANGE
_PARSE_INPUT="$ARGUMENTS"; case "$FIRST" in notes|prepare|audit|demo) _PARSE_INPUT="$REST";; esac
RANGE=$(echo "$_PARSE_INPUT" | grep -oE '[^ ]+([[:space:]]*->[[:space:]]*|\.\.)[^ ]+' | head -1 | tr -d '[:space:]')
for _a in $_PARSE_INPUT; do case "$_a" in --changelog) DO_CHANGELOG=true;; --summary) DO_SUMMARY=true;; --migration) DO_MIGRATION=true;; --append) DO_APPEND=true;; --*) echo "⚠ unknown flag: $_a";; *) [ -z "$RANGE" ] && RANGE="$_a";; esac; done
RANGE="${RANGE/->/..}"
# persist (Check 41) — Gather-changes needs this for marker-based RANGE resolution
echo "${DO_APPEND}" > "${TMPDIR:-/tmp}/release-do-append-${CSID}"

Unknown flags: if any ⚠ unknown flag: lines printed above, invoke AskUserQuestion — (a) Abort (stop, re-invoke) · (b) Continue ignoring. On Abort: stop.

Shared setup

Run this first — cold-start fallback (sets $_OSS_SHARED):

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
_OSS_SHARED=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/resolve_shared_path.py" oss skills/_shared 2>/dev/null)  # timeout: 5000
# persist (Check 41)
echo "${_OSS_SHARED:-}" > "${TMPDIR:-/tmp}/release-oss-shared-${CSID}"
# loads: oss-shared-resolver.md

Extracted to bin/release_setup.py — resolves SKILL_DIR, REPO_ROOT, BRANCH, DATE, LAST_TAG, CHERRY_PICK_SUBJECTS, SOURCE_TAG_REF. Writes each var under ${TMPDIR:-/tmp}/release-setup-${CSID}/; stable-branch banner and "no stable tag" warnings go to stderr.

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/release_setup.py"  # timeout: 10000
IFS= read -r SKILL_DIR < "${TMPDIR:-/tmp}/release-setup-${CSID}/SKILL_DIR" 2>/dev/null || SKILL_DIR=""
IFS= read -r REPO_ROOT < "${TMPDIR:-/tmp}/release-setup-${CSID}/REPO_ROOT" 2>/dev/null || REPO_ROOT=""
IFS= read -r BRANCH < "${TMPDIR:-/tmp}/release-setup-${CSID}/BRANCH" 2>/dev/null || BRANCH=""
IFS= read -r DATE < "${TMPDIR:-/tmp}/release-setup-${CSID}/DATE" 2>/dev/null || DATE=""
IFS= read -r LAST_TAG < "${TMPDIR:-/tmp}/release-setup-${CSID}/LAST_TAG" 2>/dev/null || LAST_TAG=""
IFS= read -r CHERRY_PICK_SUBJECTS < "${TMPDIR:-/tmp}/release-setup-${CSID}/CHERRY_PICK_SUBJECTS" 2>/dev/null || CHERRY_PICK_SUBJECTS=""
IFS= read -r SOURCE_TAG_REF < "${TMPDIR:-/tmp}/release-setup-${CSID}/SOURCE_TAG_REF" 2>/dev/null || SOURCE_TAG_REF=""
[ -z "$REPO_ROOT" ] && { echo "Error: release_setup.py failed — REPO_ROOT empty; verify oss plugin installation"; exit 1; }

When no stable tags exist, LAST_TAG resolves to initial commit — surface via AskUserQuestion ("No stable tags found. Range base is initial commit — proceed?"). Options: (a) Proceed with initial commit as base · (b) Abort — stop release process. On (b): stop, print "Release aborted — no stable tags found; create a tag first with git tag v0.1.0" and exit.

Gather changes

Find common base tag across ALL branches via git tag --list sorted by version, then git merge-base HEAD <tag-commit>. Use as range lower bound when current branch has no direct tag ancestry.

export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# reload Shared-setup vars (Check 41)
IFS= read -r LAST_TAG < "${TMPDIR:-/tmp}/release-setup-${CSID}/LAST_TAG" 2>/dev/null || LAST_TAG=""
IFS= read -r CHERRY_PICK_SUBJECTS < "${TMPDIR:-/tmp}/release-setup-${CSID}/CHERRY_PICK_SUBJECTS" 2>/dev/null || CHERRY_PICK_SUBJECTS=""
IFS= read -r BRANCH < "${TMPDIR:-/tmp}/release-setup-${CSID}/BRANCH" 2>/dev/null || BRANCH=""
IFS= read -r DO_APPEND < "${TMPDIR:-/tmp}/release-do-append-${CSID}" 2>/dev/null || DO_APPEND="false"
if [ -z "$RANGE" ] && [ "$DO_APPEND" = "true" ]; then
    # marker present+valid -> "<sha>..HEAD"; else "$LAST_TAG..HEAD" (non-append default)
    RANGE=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/release_append_marker.py" resolve --branch "$BRANCH" --last-tag "$LAST_TAG")  # timeout: 5000
else
    RANGE="${RANGE:-$LAST_TAG..HEAD}"
fi
[ -z "$RANGE" ] && echo "Error: could not determine commit range" && exit 1
# persist (Check 41)
echo "${RANGE:-}" > "${TMPDIR:-/tmp}/release-range-${CSID}"

# quote RANGE — tags may carry unusual chars (e.g. v1.2-rc.1+build.42)
git log "$RANGE" --oneline --no-merges # timeout: 3000
git log "$RANGE" --no-merges --format="--- %H%n%B" # timeout: 3000
git diff --stat "$(echo "$RANGE" | sed 's/\.\.\./\ /;s/\.\./\ /')" # timeout: 3000

# prefer gh; fallback git remote show origin; never hardcode main
TRUNK=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null)  # timeout: 6000
if [ -z "$TRUNK" ]; then
    TRUNK=$(git remote show origin 2>/dev/null | grep 'HEAD branch' | { read -r _ _ val; echo "$val"; })  # timeout: 5000
fi
if [ -n "$TRUNK" ]; then
    gh pr list --state merged --base "$TRUNK" --paginate \
        --json number,title,body,labels,mergedAt,author 2>/dev/null  # timeout: 15000
else
    echo "⚠ Could not detect default branch — listing all merged PRs"
    gh pr list --state merged --paginate \
        --json number,title,body,labels,mergedAt,author 2>/dev/null  # timeout: 15000
fi

Cross-reference commit bodies against PR descriptions — canonical source of truth for why change made. BREAKING CHANGE: footer = breaking change regardless of PR label.

Detect revert pairs: scan git log $RANGE --no-merges --format="%H %s" for subjects beginning with Revert ". For each: extract original subject, search range for matching commit. Both found → REVERT_SET pair (net effect zero).

Record all REVERT_SET pairs before Classify. Commits in REVERT_SET excluded from standard sections; collected for 🔄 Reverted. If only revert is in range (original predates range) → classify as ✗ Removed (or ⚠ Breaking Changes if API surface changed without prior deprecation) — NOT 🔄 Reverted; net user effect is non-zero.

Cross-cycle revert/pivot detection (--append only, when a prior DRAFT.md/$CHANGELOG_FILE exists — extends the "non-destructive except revert/pivot" rule across append cycles, not just within one range).

Two detection paths — patch-id provenance (deterministic, content-stable) for literal reverts, semantic judgment (best-effort) for pivots. Every prior cycle's write is recorded in .temp/release-provenance-$BRANCH.json (see <notes> "Provenance store (patch-id keyed)"): one entry per (patch-id, artifact, exact written text) tuple, keyed on git patch-id --stable output rather than raw commit sha — a bare sha changes on --amend, rebase, or cherry-pick even when the diff itself is untouched, so a raw-sha key would silently miss a revert of a commit that has since been reworded or cherry-picked; patch-id is a normalized hash of the diff content and survives all three. A genuine git revert commit always carries git's own auto-generated This reverts commit <sha>. trailer in its body — that sha identifies the reverted commit's current form, which is turned into its patch-id and looked up in the store (no text-matching involved). A pivot (a symbol's classification changes without a literal revert commit — e.g. deprecated this cycle after being added in a prior one) has no such trailer to key off; it still needs the model's semantic judgment, same as before. A revert commit without a usable trailer (manual revert, not made via git revert) also falls back to the semantic path — as does a trailer sha whose diff produces no stable patch-id (see step 2 below).

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026
Hacker News mentions
20
Advanced
Catalog kind
skill
Gateway key
release-borda
Source
github.com/borda/ai-rig