release
SkillDocs & knowledgePrepare 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.
No other account needed.
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:
| Invocation | Arguments | Writes to disk |
|---|---|---|
/release [notes] [range] | optional range (default: last-tag..HEAD); use v1->v2 for explicit range | DRAFT.md |
/release notes [range] --changelog | optional range + flag | DRAFT.md + prepends CHANGELOG.md |
/release notes [range] --summary | optional range + flag | DRAFT.md + .temp/output-release-summary-<branch>-<date>.md |
/release notes [range] --migration | optional range + flag | DRAFT.md + .temp/output-release-migration-<branch>-<date>.md |
/release notes [range] --changelog --summary --migration | all flags | All four outputs |
/release notes --append | no range (derived from last-processed marker); compose with --changelog/--summary/--migration | Runs 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.0 | All artifacts in releases/<version>/: DRAFT.md + CHANGELOG.md + SUMMARY.md + MIGRATION.md + demo.py |
/release audit [version] | optional target version | Terminal 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:
demomode: 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--appendwith no valid marker ($MARKER_VALIDcomputed 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 (
--appendmerge 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:
- 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 - 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: trueplus 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>)
- 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 token | MODE | Routing |
|---|---|---|
prepare | prepare | Shared setup first, then Mode: prepare |
audit | audit | Shared setup first, then Mode: audit |
demo | demo | Shared setup first, then Mode: demo |
notes | notes | Strip notes token; parse flags and range from remainder |
| (bare range or flag) | notes | Parse flags and range from full string |
| (none) | notes | RANGE="", 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