Claude
Skill
release
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/MIGRATIO
Virus-scanned
Reviewed automatically before listing.
Download
Borda-AI-Rig-plugins_cc_oss_skills_release-39e3a48.zip · 61 KB
Install
skills CLI
npx skills add https://github.com/Borda/AI-Rig/tree/main/plugins/cc_oss/skills/release
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install borda-ai-rig@llmmart
Git
git clone https://github.com/Borda/AI-Rig.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole borda/ai-rig collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Files (ai-rig)
-
guidelines
-
numbers-reference.md 3.9 KB
# Numbers & Claims Reference Source of truth for numeric limits in `release/SKILL.md`. Update when number changes — no undocumented changes. ## Numeric Limits ### PR list limit — `--limit 500` | Field | Value | | -- | -- | | Location | `Gather changes` → `gh pr list` | | Current value | `500` | | Floor | GitHub CLI default: **30** (silent truncation) | | Evidence | `rules/external-data.md` §GitHub CLI: "set at least 10× higher than expected"; typical release = 2–4 sprints × 10–50 PRs/sprint = 40–200 PRs | | If insufficient | At exactly 500 results, recover with `gh api --paginate --slurp`; otherwise the result is below the documented cap. | ### Release convention scan — `--limit 5` | Field | Value | | -- | -- | | Location | `Write release draft` → `gh release list` for style-matching | | Current value | `5` | | Evidence | 5 covers interleaved stable/LTS/minor releases; recency + sample variety without latency penalty | | If insufficient | Bump to 10; read-only, no correctness risk | ### Latest non-prerelease tag detection — `--limit 100` | Field | Value | | -- | -- | | Location | `Write release draft` → `LATEST_TAG` detection | | Current value | `100` | | Evidence | GitHub release API reverse-chronological; gap between stable releases exceeding `--limit` returns wrong tag silently; 100 handles repos with ≥80 pre-release tags between stable cuts | | If insufficient | Use `gh api --paginate 'repos/{owner}/{repo}/releases'` and select the first non-prerelease match; `gh release list` has no `--paginate` flag | ### Demo execution timeout — 10 minutes (`timeout: 600000`) | Field | Value | | -- | -- | | Location | `Generate release demo` and `Mode: prepare / Phase 4a` | | Current value | `600000` ms (10 min) | | Evidence | `rules/claude-config.md` §Bash Timeouts: P90 for test suite = 3 min → 3× = 600000 ms; demo P90 ≈ 2–3 min (model load + inference + dataset); 3× headroom = 600000 ms | | Gate | Demo must exit 0 within 10 min; if legitimately longer, redesign to use cached weights or smaller fixture | | If wrong | Do not reduce below 300000 (5 min); if timing out, diagnose network I/O or cold-start deps | ## Count Guidance Design choices — adjust when release output feels too sparse or dense. ### Highlights count — 3–5 | Field | Value | | -- | -- | | Location | `Identify highlights` | | Range | 3 to 5 | | Rationale | >5 = not highlight; \<3 = thin for non-trivial releases; range covers sparse (one breaking change) vs. dense (major version) releases | | If wrong | Fewer than 3 for major version → expand; more than 5 → split into highlights + "Also in this release" | ### Demo headline features — 2–3 | Field | Value | | -- | -- | | Location | `Mode: demo / Phase 1` | | Range | 2 to 3 | | Rationale | Demo narrative; >3 sections fragment attention; \<2 thin for release demo | | If wrong | Adjust per release density; >3 allowed for major versions with distinct user-visible features | ### Contributor summary — 3–6 words | Field | Value | | -- | -- | | Location | `Extract contributors` | | Range | 3 to 6 words | | Rationale | Fits credits line without wrapping; specific enough to mean something; short enough to scan | | If wrong | Expand to phrase if contribution spans multiple areas | ## Performance Claims — Fact-Check Gate Quantitative claims ("2× faster", "50% memory reduction", "latency −30 ms") in commit messages or PR bodies need evidence before inclusion in release notes. Two tiers: | Tier | Source | Inclusion rule | | -- | -- | -- | | Supported | PR body cites benchmark run, CI artifact, profiling output, or timing table | Include claim verbatim | | Unsupported | Claim from commit subject only — no artifact linked | Rewrite to "improved performance" without number | **Never include raw numeric claims from commit subjects alone** — commit subjects unreviewed author claims; PR body + artifacts ground truth. -
writing-rules.md 3.7 KB
Write for reader, not commit author. | Element | Rule | | -- | -- | | Feature heading | Bold title, period, then plain-English description — no jargon | | PR numbers (CHANGELOG) | Full Markdown link — `([#947](https://github.com/owner/repo/pull/947))` | | PR numbers (DRAFT.md) | Short inline ref — `(#947)` — never `[#947](url)`; strip full links when sourcing from intermediate files | | PR ref + fenced code block | Place `(#N)` at end of description text **before** opening fence — never after closing fence; trailing refs after fenced blocks invisible in rendered output | | Issue refs | Never include `closes #N` / `fixes #N` in CHANGELOG or DRAFT.md | | Code examples | Real usage showing new surface; not pseudocode | | Tables | Use for option/preset comparisons; skip for single-item features | | Breaking changes | Rare — use sparingly; false alarms scare users more than change itself | | Fix items | Say what was broken and under what condition — not just "fixed X" | | Changed items | Behaviour changes only — old behaviour → new behaviour | | Deprecated items | Name old API and replacement; omit removal version if unknown | | Removed items | State deprecated-since version and migration target | > **Breaking vs Deprecated**: Normal flow is deprecate → announce removal version → Removed. Breaking Changes = rare case where **public API or user-facing behaviour** breaks **immediately** on upgrade, no prior warning, no fallback — including dependency version incompatibilities affecting users directly. Private API and test changes never Breaking Changes. Old behaviour still works (even with deprecation warning) → belongs in Deprecated, not here. When in doubt, not Breaking Changes Bad/good examples: - Bad: `"refactor: extract UserService from monolith"` → Good: `"User management is now ~40% faster"` - Bad: `"Fix auth bug"` → Good: `"Fixed login failure for email addresses containing special characters"` **Contributors rules:** - List **every** PR author in range — human and bot alike; community acknowledgement essential for growth - **Bots**: collect all bot handles (accounts ending in `[bot]` or known bots like `dependabot`, `renovate`, `github-actions`), render as single italic line at bottom of section: `*Automated contributions: @bot1, @bot2*` — never list bots individually - **NEVER guess or hallucinate real name.** Wrong name in public release notes = serious error. When in doubt, omit name entirely. - **Name lookup protocol** — run for every human contributor @handle before writing entry: 1. `gh api /users/<handle> --jq '.name'` — if non-null and non-empty, use as real name (high confidence) 2. LinkedIn already resolved by **Extract contributors** phase (SKILL.md — ordered grounding chain: GitHub Social Accounts API → `.blog` field → personal-page scan → past-releases reuse; never by name at any step) — reuse value carried in `$CONTRIBUTORS_FILE` verbatim, don't re-look-up or re-derive here 3. If name still uncertain: use `@handle` only — no name field - Format when name confirmed: `* **Full Name** (@handle) ([LinkedIn](url)) – *noun phrase*` - Format when name not confirmed: `* @handle – *noun phrase*` - LinkedIn optional — include only when already resolved by Extract contributors; never construct URL by guessing, never re-look-up here - New contributors get welcome sentence above list - Maintainer always listed last with infra / CI / docs scope - Precede Contributors section with `---` separator **Last line (required):** Every release entry must end with: `**Full changelog**: https://github.com/[org]/[repo]/compare/vPREV...vNEXT` - Must be final line of entry, after Contributors section — no notes, annotations, separators, or prose may follow - Never omit, even for patch releases
-
-
modes
-
adversarial-review.md 4.8 KB
<!-- oss:release adversarial review — executed via: cat "$SKILL_DIR/modes/adversarial-review.md"; execute --> <!-- Variables available: $SKILL_DIR, $_OSS_SHARED, $BRANCH, $RANGE, $GATHER_FILE, assembled draft content --> Challenge every factual claim in assembled draft against codebase, project docs. Runs before voice/tone polish — facts correct before prose refined. **Scope**: applies to `notes` mode (DRAFT.md), `prepare` mode (releases/$VERSION/DRAFT.md), `--migration` flag output. Skip for `--summary` (internal audience), `--changelog` entries (structured format, not claim-heavy prose). ```bash # expand to literal value before spawning ADVERSARIAL_DIR=".temp/release-adversarial-$(git branch --show-current 2>/dev/null | tr '/' '-' || echo 'main')-$(date +%Y-%m-%d)" mkdir -p "$ADVERSARIAL_DIR" # timeout: 5000 ``` Write full assembled draft content to `$ADVERSARIAL_DIR/draft-to-review.md` using Write tool. Spawn adversarial reviewer — use `foundry:sw-engineer` for reliable tool execution (bash + read): ```text Agent(subagent_type="foundry:sw-engineer", prompt="Adversarial review of a release draft against the project codebase and docs. Working directory: <REPO_ROOT>. Your job is to REFUTE claims, not confirm them — treat every stated fact as wrong until you prove it correct. Read the release draft at: <$ADVERSARIAL_DIR/draft-to-review.md> Challenge across 4 dimensions: 1. FACTUAL ACCURACY — for every named symbol in the draft ('adds X', 'fixes Y', 'removes Z'), verify the symbol is actually DEFINED in the codebase at HEAD — not just mentioned in a comment, docstring, or leftover stub. Prefer codemap over grep (codemap finds definitions; grep hits leftovers): check `codemap-py query find-symbol '^<symbol>$' 2>/dev/null` first; if codemap unavailable (`codemap-py query list` returns empty), fall back to `git grep -wl 'def <symbol>\|class <symbol>' HEAD -- '*.py' 2>/dev/null` (definition pattern) then `git grep -wl '<symbol>' HEAD -- '*.ts' '*.js' '*.go' '*.rs' 2>/dev/null`. Quote the output. Zero output for a claimed-added symbol = critical finding. Presence for a claimed-removed symbol = critical finding. Do not infer from GATHER_FILE — verify each symbol directly against HEAD. 2. COMPLETENESS — use GATHER_FILE (below) as the ground-truth commit list: significant commits (breaking changes, new public API) not mentioned in draft = high finding. Minor/internal commits absent is expected. 3. SEMVER CORRECTNESS — if draft says 'no breaking changes', run `git diff <RANGE> -- '*.py' '*.ts' '*.js' '*.go' '*.rs'` and scan for removed or renamed public API, changed function signatures, removed config keys. Flag contradictions. 4. DOCS ALIGNMENT — for each new API or behavior claimed in draft, confirm docs/ or README covers it. Missing doc = medium finding. <gather context> $([ -n \"<GATHER_FILE>\" ] && [ -f \"<GATHER_FILE>\" ] && echo 'Commit classification (for dimension 2 COMPLETENESS only): <GATHER_FILE> — read it to find commits the draft should cover. Do NOT use this file to verify factual accuracy — use HEAD git grep for dimension 1.' || echo 'Use the git range <RANGE> directly: git log <RANGE> --no-merges --oneline to get the change list.') </gather context> For each finding: severity (critical=claim directly contradicts HEAD state | high=significant missing change or SemVer misclassification | medium=overstated claim or minor missing item | low=style/wording inaccuracy). For critical and high findings: quote exact draft text AND quote git grep output or codebase evidence. Write full findings report to <$ADVERSARIAL_DIR/adversarial-review.md> using the Write tool. Return ONLY on your final line: {\"status\":\"done\",\"file\":\"<$ADVERSARIAL_DIR/adversarial-review.md>\",\"critical\":N,\"high\":N,\"medium\":N,\"low\":N,\"confidence\":0.N}") ``` Expand `<REPO_ROOT>`, `<RANGE>`, `<GATHER_FILE>`, `<$ADVERSARIAL_DIR>` to literal values before spawning — never pass variable names literally. **Pre-handover truth check loop** — no output reaches user until all critical and high findings resolved: 1. Read `$ADVERSARIAL_DIR/adversarial-review.md` from spawned reviewer 2. **Critical or high findings present**: fix every claim in draft contradicted by HEAD (remove unverified API names, correct wrong descriptions, remove symbols absent from codebase); re-spawn adversarial reviewer on updated draft; repeat until zero critical/high findings or 3 total iterations reached 3. **After max 3 iterations with persistent critical/high findings**: surface remaining findings to user, stop — don't hand over draft with known false claims 4. **Medium or low findings**: append as `> ⚠ Reviewer notes: <summary>` to response; these don't block handover 5. **0 critical/high findings**: proceed to Polish Log each fix: `[CORRECTED] <original claim> → <what changed and why>`. -
audit.md 10.7 KB
<!-- oss:release Mode: audit — executed via: cat "$SKILL_DIR/modes/audit.md"; execute --> <!-- Variables available: $SKILL_DIR, $_OSS_SHARED, $LAST_TAG, $BRANCH, $DATE, $RANGE, $VERSION, $REPO_ROOT, $SOURCE_TAG_REF, $CHERRY_PICK_SUBJECTS --> **Trigger**: `/release audit [version]` **Purpose**: Pre-release readiness check — surfaces outstanding work, alignment gaps, blockers before cutting release ```bash # LAST_TAG, REPO_ROOT, SKILL_DIR from Shared setup above # audit mode: REST = optional version token (not range); RANGE defaults to LAST_TAG..HEAD RANGE="${RANGE:-$LAST_TAG..HEAD}" export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r SOURCE_TAG_REF < "${TMPDIR:-/tmp}/release-setup-${CSID}/SOURCE_TAG_REF" 2>/dev/null || SOURCE_TAG_REF="" IFS= read -r CHERRY_PICK_SUBJECTS < "${TMPDIR:-/tmp}/release-setup-${CSID}/CHERRY_PICK_SUBJECTS" 2>/dev/null || CHERRY_PICK_SUBJECTS="" RELEASE_MODE="linear"; PENDING_CHERRY_PICKS=0 [ -n "$SOURCE_TAG_REF" ] && RELEASE_MODE="stable-branch" && PENDING_CHERRY_PICKS=$(printf '%s\n' "$CHERRY_PICK_SUBJECTS" | grep -c . 2>/dev/null || echo 0) echo "release mode: $RELEASE_MODE (source=$SOURCE_TAG_REF, pending=$PENDING_CHERRY_PICKS)" ``` ### Phase 0: Release-model guardrail `$RELEASE_MODE=stable-branch` (i.e. `$SOURCE_TAG_REF` non-empty) → HEAD does not descend from develop/main's tip; commits merged there after `$LAST_TAG` are **expected** to be absent from this branch unless their subject matches `$CHERRY_PICK_SUBJECTS`. Never diff this branch against `develop`/`main`/any non-ancestor branch and report the gap as blocking "drift" — that comparison assumes a linear model this branch isn't using. A PR or commit missing from HEAD in this mode is normal, not evidence of an accidental cut, and must never by itself produce a `critical`/`BLOCKED` finding. The only legitimate cherry-pick-mode red flag is a **Truth check** hit (`modes/classify-truth-check.md`, run inside the Phase 1 gather subagent): a symbol referenced by an in-range commit (docstring, import, call site) but not defined anywhere in HEAD. That is a real dangling-reference bug — scope the finding to that specific commit/PR only. It is evidence that one commit is incomplete, never evidence that the whole selective cut was accidental — don't generalize a single dangling reference into a branch-wide divergence verdict. `$RELEASE_MODE=linear` → this branch is a direct descendant of the branch it was cut from; standard fast-forward/ancestor expectations apply, and unexpected missing commits are worth investigating as `NEEDS_ATTENTION` (not automatically `BLOCKED` — confirm with the user before treating a gap as unintentional; branches get selectively rebased or filtered for reasons a diff alone can't show). ### Phase 1: Gather and explore changes Use **Delegation strategy** above — spawn gather subagent for `$RANGE`, run gather/explore/validate phases, write findings to `GATHER_FILE`. Read returned JSON envelope only. Audit agent (Phase 2) reads `GATHER_FILE` directly — don't pull into main context. ### Phase 1a: Deprecation-removal check Verify all APIs scheduled for removal at `$TARGET` absent from HEAD. Runs after gather, before readiness checks. **Step 1** — find all scheduled removals from two sources (run in parallel): ```bash # Source A: pyDeprecate remove_in= markers (includes deprecated_class, deprecated_instance) git -C "$REPO_ROOT" grep -n 'remove_in=' HEAD -- '*.py' 2>/dev/null | grep -v '^\s*#' # timeout: 5000 # Source B: CHANGELOG 🗑️ Deprecated entries — prior releases may name removal version in prose CHANGELOG_FILE=$(find "$REPO_ROOT" -maxdepth 2 -name "CHANGELOG.md" 2>/dev/null | head -1) [ -f "$CHANGELOG_FILE" ] && grep -n '🗑️\|Deprecated\|remove_in\|scheduled for removal\|will be removed' "$CHANGELOG_FILE" 2>/dev/null | head -60 # timeout: 3000 ``` **Step 2** — for each `remove_in="V.W"` found, compare with `$TARGET`: - Skip version comparison when `$TARGET` empty or `"next"` — report informational only - OVERDUE = `remove_in ≤ $TARGET`: ```bash # stdlib version parse — no packaging dep python3 -c " import sys, re def parse_ver(v): v = v.strip('\"v ') parts = [int(x) for x in re.sub(r'[^0-9.]', '', v).split('.') if x] return tuple(parts) if parts else (0,) rv, tv = parse_ver(sys.argv[1]), parse_ver(sys.argv[2]) print('OVERDUE' if rv <= tv else 'FUTURE') " "<remove_in_value>" "${TARGET:-0}" # timeout: 5000 ``` **Step 3** — for each OVERDUE item, check if old symbol still present in HEAD. Read 3-line context around `remove_in=` match to extract deprecated function or class name (typically decorated name 1–2 lines above), then: ```bash # definition-level only — comments/docstrings may reference removed names git -C "$REPO_ROOT" grep -n "^def <symbol>\|^class <symbol>\| def <symbol>\| class <symbol>" HEAD -- '*.py' 2>/dev/null # timeout: 3000 ``` **Outcomes per symbol**: - Absent from HEAD → ✓ correctly removed - Still present AND OVERDUE → ✗ **CRITICAL** — add to Phase 2 findings table as: `` | Scheduled removal overdue | ✗ `<symbol>` still present (remove_in="V.W") | <file:line> | critical | `` - `$TARGET` not set → surface OVERDUE candidates as informational (`⚠ remove_in="V.W" scheduled but target version unknown`) ### Phase 1b: Upstream review verdict check Verify no blocking `/oss:review` (or codex-lineage review) verdict already exists for the current branch before declaring readiness — closes the gap where `audit` re-derives blockers a prior review already found instead of surfacing them immediately as a pre-flight failure. ```bash REVIEW_FILE=$(ls -t .reports/review/*/review-report.md .reports/review/*/*/review-report.md .reports/codex/review/*/review-notes.md 2>/dev/null | head -1) ``` - No match → skip this check (informational: no prior review on file). - Match under `.reports/review/pr-*/run-*/review-report.md` or the legacy pre-rename `.reports/review/*/review-report.md` (both oss lineage) → grep its `Outcome:` YAML field. `✗` or `⚠ NEEDS_ATTENTION` with unresolved blocking findings → add to Phase 2 Findings summary: `| Upstream review blocking | ✗ <REVIEW_FILE> reports <Outcome value> | <path> | critical |`. - Match under `.reports/codex/review/*/review-notes.md` (codex lineage) → grep `Recommendation:` / `Blocking findings:` lines. `needs-more-work` with non-empty `Blocking findings:` → same critical finding, quoting the blocking finding IDs. - Only act on a review whose header/scope names the branch or PR currently being released — a review for a different branch/PR is not a blocker here; note it as informational context instead. ### Phase 2: Readiness checks Execute all checks from `templates/audit-checks.md`. Checks cover: version consistency across manifests, docs/CHANGELOG alignment, open blocking issues, dependency CVE scan, unreleased commits since last tag, changelog scope (commits landed via a non-PR branch merge inside `$RANGE` — catches an unrelated already-released commit re-appearing in this release's section by accident, see Audit changelog's Scope check in `SKILL.md`). Every `critical` row from the Scope check (already-released commit landed a second time) → add to the Findings summary table below, `severity: critical` — this is the check that must be visible to the reviewer before the PR merges, not something the audit file alone carries. ```bash cat "$SKILL_DIR/templates/audit-checks.md" # timeout: 5000 ``` After readiness table, if issues found, append **Findings summary** table: | # | Issue | Location | Severity | | -- | -- | -- | -- | | 1 | <what is wrong> | <section or file> | critical/high/medium/low | Every finding needs explicit location, severity, action — matches structured output format of `notes` and `changelog` modes. ### Phase 2a: Codex adversarial audit (if available) Checklist checks above (version/docs/CVE/deprecation) are static and can't catch semantic gaps — e.g. a public example contradicting the release's own behavior change, or a migration note describing the wrong version. When Codex is installed, dispatch it as an independent adversarial pass over the same `RANGE` before declaring a verdict. ```bash CODEX_STATUS=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/check_bridge.py" --status 2>/dev/null || echo "absent") # timeout: 5000 if [ "$CODEX_STATUS" = "available" ]; then CODEX_AVAILABLE=1; echo "bridge@borda-ai-rig available"; else CODEX_AVAILABLE=0; echo "⚠ bridge@borda-ai-rig is ${CODEX_STATUS} — skipping adversarial audit pass"; fi ``` `CODEX_AVAILABLE=0` → skip this phase entirely, no finding added. `CODEX_AVAILABLE=1` → ```text Skill(skill="bridge:review", args="Read-only adversarial release-readiness audit. Working directory: <REPO_ROOT>. Range: <RANGE>. Target version: <TARGET or 'next'>. Treat every user-facing claim as wrong until proven correct by reading the actual source at HEAD. Check specifically: (1) do public docs/examples touched or implied by this range still match current default behavior — not just changed files, but examples elsewhere in docs/ that call the same code path; (2) does the migration guide (if any) place new behavior under the correct version step, not backported into an earlier historical entry; (3) does the changelog/PR narrative account for every commit in range, including ones bundled into another PR; (4) any new or changed optional dependency reaching a platform without a recorded review/disposition. Write full findings to <ADVERSARIAL_AUDIT_FILE> using the Write tool. Return ONLY: {\"status\":\"done\",\"file\":\"<path>\",\"critical\":N,\"high\":N,\"medium\":N,\"low\":N,\"confidence\":0.N}") ``` Expand `<REPO_ROOT>`, `<RANGE>`, `<TARGET>`, `<ADVERSARIAL_AUDIT_FILE>` (`.temp/release-codex-audit-$BRANCH-$DATE.md`) to literal values before spawning. Read the returned file. Every `critical`/`high` finding → add a row to the Findings summary table above (`severity: critical` or `high`), quoting the finding text. `medium`/`low` → append as `> ⚠ Codex audit notes: <summary>` below the table, non-blocking. ### Output routing Write full report to `.reports/release/$BRANCH-$DATE.md` (create dir with `mkdir -p .reports/release` if needed) — **not** `.temp/`. Overrides quality-gates default `.temp/output-...` path. Print verdict line and executive summary to terminal per quality-gates rules. ### Verdict line (mandatory final output) Print exactly one verdict line immediately before `## Confidence` block so callers (e.g. `prepare` Phase 1) can pattern-match without parsing prose: - `verdict: READY` — no CRITICAL or HIGH findings - `verdict: NEEDS_ATTENTION` — one or more HIGH findings, no CRITICAL - `verdict: BLOCKED` — one or more CRITICAL findings (also written when readiness checks cannot complete) End response with `## Confidence` block per CLAUDE.md output standards. -
changelog-audit-prompt.md 5.5 KB
<!-- file: changelog-audit-prompt.md — consumers: release/SKILL.md (Delegation strategy, Phases 5-6 parallel delegation) --> Spawn both in same response turn (two Agent() calls, one response — parallel). Expand `$REPO_ROOT`, `$RANGE`, `$GATHER_FILE`, `$CHANGELOG_AUDIT_FILE`, `$CONTRIBUTORS_FILE` to literal values before spawning Agent A — Audit changelog (`subagent_type="foundry:doc-scribe"` — mechanical cross-check/insertion, no opus reasoning needed): ```text Working directory: <REPO_ROOT>. Read classified change table from <GATHER_FILE>. Find CHANGELOG: check <REPO_ROOT>/CHANGELOG.md, then <REPO_ROOT>/docs/CHANGELOG.md, then any CHANGELOG* one level deep (excluding node_modules/, .venv/, vendor/). Cross-check against unreleased section: items absent from CHANGELOG → add (same emoji format); items in CHANGELOG not matching classified → flag (no auto-delete). For each REVERT_SET pair in classified table: add "🔄 Reverted: <original description> (introduced and reverted in this release)". Preserve historical entries; remove only the exact matching Unreleased entry for an original change that has not shipped, when that stale claim is confirmed. Scope check — before writing entries, find non-PR merge commits inside <RANGE>: `git -C <REPO_ROOT> log <RANGE> --merges --pretty='%H %P %s'`, keep rows whose subject does NOT match `merge pull request #[0-9]+|\(#[0-9]+\)` (case-insensitive) — a real PR merge always carries one of those markers; anything else is a raw branch merge landed inside the range, the "bad merge into PR" case. For each such merge commit, diff its two parents (`git -C <REPO_ROOT> log <parent1>..<parent2> --oneline`) to list the commits it actually pulled in. For every pulled-in commit already present as a CHANGELOG entry (any section, not just Unreleased) — it shipped in a prior release and just landed a second time in this one's scope by accident: flag `critical`, "off-scope: <sha> <subject> — already released in <section>, pulled in via non-PR merge <merge-sha>". For a pulled-in commit with no prior CHANGELOG entry, flag `medium`, "unreviewed branch merge: <sha> <subject> via <merge-sha> — confirm it belongs to this release before merging the PR". Add every flagged row to <CHANGELOG_AUDIT_FILE> under its own "Scope check" heading, distinct from the add/flag list above. Write full audit findings to <CHANGELOG_AUDIT_FILE>. Return ONLY: {"status":"done","file":"<CHANGELOG_AUDIT_FILE>","changelog_file":"<path-found>","added":N,"flagged":N,"scope_flagged":N} ``` Agent B — Extract contributors (`subagent_type="foundry:doc-scribe"` — script-driven extraction + formatting, no opus reasoning needed): ```text Read <GATHER_FILE> for verified commit-to-PR associations and PR author identities. Build the contributor inventory as the union of Git authors/coauthors and every PR author verified to belong to this release range, not the whole merged-PR list or merge dates alone. Include PR-only authors of maintainer-authored squash commits even without a coauthor trailer. Reconcile duplicates only through verified login/email linkage, including GitHub privacy-email handles; display-name similarity is not identity proof. Keep unmatched Git people and PR-only people; use a known login when the display name is unavailable, never invent an email or name. Missing association, unavailable/deleted PR identity or incomplete author metadata is an explicit contributor-coverage gap, not complete Git-only coverage. For PR-only humans without a known email, summarize their verified in-range PR changes instead of an empty Git author lookup. Aggregate bots before individual formatting or profile lookup; only human handles enter the LinkedIn chain below. Preserve and return any coverage gaps in <CONTRIBUTORS_FILE> for the parent to report. Working directory: <REPO_ROOT>. Range: <RANGE>. Run: python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/extract_contributors.py" --repo <REPO_ROOT> --range "<RANGE>" --include-bots — emits one `Name <email>` line per contributor, already deduplicated by email. Keep GitHub privacy-email humans. Aggregate bot handles into the one italic automated-contributions line required by `guidelines/writing-rules.md`; do not list bots individually. For each human contributor inspect commits in range (git -C <REPO_ROOT> log "<RANGE>" --no-merges --author="<email>" --oneline); pick up to 3 most significant (rank: new public API > major UX > significant fix > internal > docs/typo). Resolve GitHub handle from PR author data (author.login). For each resolved handle find LinkedIn via ordered chain, stop at first hit, never guess/infer/match by contributor name at any step (handle or a real fetched-page anchor href only): 1) `gh api "/users/<login>/social_accounts" --jq '.[] | select(.provider=="linkedin") | .url'` non-empty → use. 2) else `gh api /users/<login> --jq '{blog:.blog}'` — `.blog` contains linkedin.com → use. 3) else `.blog` is a non-empty non-linkedin URL → WebFetch it, scan actual returned content for `linkedin.com/in/...` anchor hrefs; exactly one distinct match → use, zero or multiple → omit. 4) else search <REPO_ROOT>/CHANGELOG.md, docs/CHANGELOG.md, releases/*/SUMMARY.md, releases/*/DRAFT.md, and `gh release list`/`gh release view` bodies for a prior Contributors entry keyed by this EXACT handle already carrying a resolved `[LinkedIn](url)` → reuse verbatim. 5) else omit LinkedIn. Format: - **Name** (@handle, [LinkedIn](url)) — <what they did>. Write contributors list to <CONTRIBUTORS_FILE>. Return ONLY: {"status":"done","file":"<CONTRIBUTORS_FILE>","count":N} ``` -
classify-truth-check.md 9.9 KB
<!-- file: classify-truth-check.md — consumers: release/SKILL.md (Classify each change + Truth check + Breaking-change classification phases; loaded once, all three phases read from that single load) --> ## Classify each change **Net-state principle**: classify only HEAD state, not development journey. Feature added then removed within range = net effect zero — omit. **Cross-cycle extension** (`--append` only): the Net-state principle above applies within `$RANGE`; Gather changes' "Cross-cycle revert/pivot detection" extends it *across* append cycles — a revert or symbol pivot that supersedes a bullet a PRIOR cycle already wrote into `DRAFT.md`/`$CHANGELOG_FILE` nets to a removal of that stale entry, not an additive one. See Gather changes for the detection rule; classify each in-scope item against `CROSS_CYCLE_MATCH` before finalizing this table. **PR accumulation**: list ALL contributing PR numbers for net-surviving entry. **Same category only** — two PRs merge under one bullet only when both classify into SAME section. Later PR fixing bug in same-range feature = own 🔧 Fixed entry. **Trivial-fix exception**: fix or doc tweak with no standalone user-visible effect folds into parent Added bullet. When in doubt: separate entries safer **Commit-label distrust**: `fix:`/`feat:`/etc. type prefix and subject line are self-reported by the author, not verified — a commit titled `fix: progress bar` can in fact reintroduce a feature, and a mislabeled subject slips through unnoticed more often than a mislabeled body trailer. Classify from the actual diff (files touched, symbols added/changed, net behavior at HEAD), never from the type prefix or subject text alone — treat both as a hint to check, not a verdict. **SHA tracking (for provenance)**: alongside PR numbers, also retain the full 40-char commit sha(s) contributing to each net-surviving entry — already visible in context from Gather changes' `git log $RANGE --no-merges --format="--- %H%n%B"` output. Needed downstream by the Provenance record step (`release-draft-template.md` "Post-write bookkeeping"), which derives each sha's content-stable `git patch-id --stable` for the actual store key — a bullet folding N squashed commits keeps all N shas, each mapped to that one bullet's final text once written. Section order (fixed): 🚀 Added → ⚠️ Breaking Changes → 🌱 Changed → 🗑️ Deprecated → ❌ Removed → 🔧 Fixed → 🔒 Security → 🔄 Reverted | Category | Section | What goes here | | -- | -- | -- | | New Features | 🚀 Added | User-visible additions | | Breaking Changes | ⚠️ Breaking Changes | Existing code stops working immediately — no prior deprecation period. Prior release deprecated → ❌ Removed instead. | | Improvements | 🚀 Added or 🌱 Changed | Enhancements to existing behavior | | Performance | 🚀 Added / 🔧 Fixed / 🌱 Changed | Quantitative claims require benchmark evidence; else rewrite to "improved performance" without number. | | Deprecations | 🗑️ Deprecated | Old API still works; scheduled removal; replacement exists | | Removals | ❌ Removed | Previously deprecated — users had warning. Not ⚠️ Breaking Changes. | | Bug Fixes | 🔧 Fixed | Correctness fixes | | Security | 🔒 Security | Security fixes + CVE dep updates. Security-intent keywords in body always classify here regardless of commit type. OMIT-INTERNAL does NOT apply. | | Internal | *(omit)* | Refactors, CI/tooling, deps, housekeeping — omit unless user-impacting | | Reverted | 🔄 Reverted | Introduced AND reverted within range (REVERT_SET) — net effect zero | **Same-release feature+fix dedup**: 🔧 Fixed targeting code introduced same release = never shipped = fold into 🚀 Added or omit. **Exception**: fix's commit body itself states a caller-visible behavior change (e.g. "changes behavior for any caller currently relying on...") → still surface a ⚠️ Breaking Changes entry, or a labeled breaking-behavior callout within the Added bullet — never silently absorbed into plain feature prose. **Breaking vs Deprecated vs Removed**: old call still works → Deprecated. Deprecated in prior release, now removed → Removed. **Prior-deprecation body-signal**: commit body contains "deprecated in vX", "previously deprecated", "was deprecated", "emits DeprecationWarning since", or "deprecated since" → treat as Removed regardless of `feat!:`/`BREAKING CHANGE:` markers. **Bug fixed to match spec**: classify as 🌱 Changed when users relied on buggy behavior; ⚠️ Breaking Changes only if load-bearing, causes widespread breakage. **OMIT-INTERNAL body-signal override**: commit body contains "No code changes", "no user-facing changes", "internal only", "no public API changes", "internal buffer changes only", "internal restructure" OR all paths under `.github/`, `ci/`, `scripts/`, `Makefile`, `*.yml` under `.github/` → classify as Internal. **Exception**: BREAKING CHANGE footer or confirmed user-visible breakage overrides. **Cherry-pick annotation (stable-branch mode)**: when `$CHERRY_PICK_SUBJECTS` set, match subject against it. Match → append "(backported from $SOURCE_TAG_REF)". Subject-text matching is heuristic — verify manually for generic subjects. **Self-correction discipline**: present only final corrected table — no intermediate classifications. ## Truth check Gate — runs after Classify, before Audit changelog. **Scope**: 🚀 Added, ⚠️ Breaking Changes, 🌱 Changed naming a symbol. Skip: 🔧 Fixed, 🔒 Security, 🗑️ Deprecated, ❌ Removed, 🔄 Reverted. For each in-scope change — prefer codemap (immune to false positives from comments/stubs): ```bash # codemap index (installed by /codemap-py:scan-codebase) CODEMAP_OK=$(codemap-py query list 2>/dev/null | wc -l) # timeout: 5000 # non-zero = index loaded; else grep fallback codemap-py query find-symbol '^<symbol_name>$' 2>/dev/null # timeout: 5000 # grep fallback: definition-pattern only — skips comments/stubs git grep -wl "def <symbol_name>\|class <symbol_name>" HEAD -- '*.py' 2>/dev/null || \ git grep -wl "<symbol_name>" HEAD -- '*.ts' '*.js' '*.go' '*.rs' 2>/dev/null # timeout: 3000 # removals/breaking: confirm absent at HEAD git grep -wl "def <symbol_name>\|class <symbol_name>" HEAD -- '*.py' 2>/dev/null \ && echo "PRESENT (unexpected)" || echo "ABSENT (confirmed)" # timeout: 3000 # behavior changes: confirm changed path at HEAD git show HEAD:<changed_file> | grep -n "<distinguishing_pattern>" # timeout: 3000 ``` Outcomes: confirmed present → keep (note "truth-checked"); not found → remove, log `[REMOVED] <description>`; cannot determine → keep with "(not HEAD-verified)" qualifier. Gate loop (max 3 iterations): truth-check → remove unverified → re-run on updated set → after 3 iterations surface remaining unverified claims and proceed. Runs before Identify highlights — highlights and demo must never reference unverified items. ## Breaking-change classification Gate — runs after Truth check, before Audit changelog. Labels each diff-derived public symbol **Breaking** (external caller) or **internal** (same-package caller only), and drafts migration evidence lines. **Codemap-gated** — `fn-rdeps` needs a v3 index. No index → skip; keep the human Classify labels as-is. ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # Reload RANGE (Check 41: fresh shell) IFS= read -r RANGE < "${TMPDIR:-/tmp}/release-range-${CSID}" 2>/dev/null || RANGE="" CODEMAP_OK=$(codemap-py query list 2>/dev/null | wc -l) # timeout: 5000 # 0 = no index → skip this phase entirely (human Classify labels stand) ``` When `CODEMAP_OK` non-zero: 1. Extract changed public symbols (diff-derived, `__init__.py` surface): ```bash CHANGED_SYMBOLS=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/extract_changed_symbols.py" "$RANGE") # timeout: 15000 [ -z "$CHANGED_SYMBOLS" ] && echo "No changed public symbols — skipping Breaking classification" ``` 2. Resolve each bare name to a `module::symbol` qname (skip test modules) and build one `fn-rdeps --exclude-tests` batch query per resolved qname. Removed public name (no `find-symbol` match) → still add its `<pkg>::<name>` qname so `fn-rdeps` errors and the helper labels it Breaking-removed. Write the query array to `$QUERIES_FILE`: ```json [{"cmd": "fn-rdeps", "args": ["<module>::<symbol>", "--exclude-tests"]}] ``` 3. Classify in one batched pass (one process, one coverage block) — pipe batch output through the classifier: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" 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="" BREAKING_FILE=".temp/release-breaking-$BRANCH-$DATE.json" mkdir -p .temp # timeout: 5000 codemap-py query batch "$QUERIES_FILE" 2>/dev/null \ | python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/classify_breaking.py" > "$BREAKING_FILE" # timeout: 15000 echo "${BREAKING_FILE:-}" > "${TMPDIR:-/tmp}/release-breaking-file-${CSID}" ``` `classify_breaking.py` output: `{breaking:[{symbol,package,external_callers|reason}], internal:[...], query_complete, migration_lines}`. "External caller" = caller whose top-level package differs from the symbol's own package. **Apply**: - Every `breaking` symbol not already under ⚠️ Breaking Changes → move it there (or add), citing its external callers as evidence. - `migration_lines` = the affected call-site draft — carry into **Draft migration guide** (`breaking_callers` findings); each external call site gets a before→after entry. - `internal` symbols → leave under their human Classify label (🚀 Added / 🌱 Changed); a same-package-only caller is not a downstream break. - `query_complete:false` → label the evidence "possibly-incomplete (codemap coverage partial)" rather than dropping it; do not silently trust it as exhaustive. **Do not block** — this phase re-labels and drafts evidence; it never removes classified items. -
demo.md 6.7 KB
<!-- oss:release Mode: demo — executed via: cat "$SKILL_DIR/modes/demo.md"; execute --> <!-- Variables available: $SKILL_DIR, $_OSS_SHARED, $LAST_TAG, $BRANCH, $DATE, $RANGE, $VERSION, $REPO_ROOT, $GATHER_FILE --> **Trigger**: `/release demo [range]` **Purpose**: Story-telling release notebook — self-contained Python script in jupytext percent (`# %%`) format. Highlights 2–3 most significant contributions with narrative prose, runnable code cells. Suitable for Colab, local Jupyter, or blog embeds. ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # Reload vars from Shared setup — fresh shell (Check 41) IFS= read -r REST < "${TMPDIR:-/tmp}/release-rest-${CSID}" 2>/dev/null || REST="" IFS= read -r LAST_TAG < "${TMPDIR:-/tmp}/release-setup-${CSID}/LAST_TAG" 2>/dev/null || LAST_TAG="" RANGE="${REST:+${REST/->/../}}" RANGE="${RANGE:-$LAST_TAG..HEAD}" echo "$RANGE" > "${TMPDIR:-/tmp}/release-demo-range-${CSID}" # persist for later blocks (Check 41) ``` ### Phase 1: Gather and pick headline features Run gather/explore/validate inline for `$RANGE` (no delegation — demo single-pass like `notes` mode). Use same commands as **Gather changes** section above (git log, gh pr list). For diff stat, prefer three-dot range: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r RANGE < "${TMPDIR:-/tmp}/release-demo-range-${CSID}" 2>/dev/null || RANGE="" # reload (Check 41) git diff --stat "$(echo "$RANGE" | sed 's/\.\./.../')" # three-dot range preferred; timeout: 3000 ``` From gathered commits and diffs, select 2–3 headline features: - Prefer: new public API, breaking changes, significant performance wins, major UX improvements - Exclude: internal refactors, CI/tooling, dep bumps, doc-only changes For each headline feature, read actual diff or changed source file to understand before/after interface — demo cells must show real API, not paraphrase. ### Phase 2: Generate demo script **Real-world data constraint**: demo must use actual project artifacts, real API calls against package under release, or genuine example data already in repo. Fabricated/synthetic inputs not acceptable by default. Sources in priority order: repo test fixtures, example scripts shipped with package, public datasets referenced in project docs, real CLI invocations against installed package. **Fallback protocol — if real demo cannot be assembled** (no usable fixtures, installed package not functional, API requires live credentials): before writing any synthetic script, execute these steps in order: 1. **Document each failed attempt**: output `## Demo attempts` block to terminal listing every approach tried and specific reason rejected (e.g. "test fixtures require database connection", "example script imports non-installable C extension"). Minimum one entry per attempt. 2. **Ask Codex (if available)**: ```bash CODEX_STATUS=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/check_bridge.py" --status 2>/dev/null || echo "absent") # timeout: 5000 [ "$CODEX_STATUS" = "available" ] && CODEX_OK="available" || CODEX_OK="" ``` If `$CODEX_OK` non-empty, call `Skill(skill="bridge:advise", args="Read the complete ## Demo attempts log printed above and inspect <REPO_ROOT>. Locate a real-world demo using existing project artifacts, tests, examples, or documented APIs. Return one viable approach with exact source paths and commands; do not modify files.")`. If Codex returns a viable approach, use it — stop, skip steps 3–4. 3. **Ask user**: invoke `AskUserQuestion` with `## Demo attempts` log (and Codex outcome if attempted), asking user to either provide real-world assets or explicitly approve synthetic demo. 4. **Synthetic demo only on explicit approval**: proceed with synthetic/fabricated demo content only if step 3 `AskUserQuestion` response explicitly authorises it. Write Python script in jupytext percent format. Structure in order: 1. **Jupytext header** — prepend verbatim as first block of generated script: ```python # --- # jupyter: # jupytext: # cell_metadata_filter: -all # formats: ipynb,py:percent # text_representation: # extension: .py # format_name: percent # format_version: '1.3' # jupytext_version: 1.16.0 # --- ``` 2. **Title cell** (`# %% [markdown]`): - `# <PackageName> <VERSION>: <tagline — one clause per headline feature>` - Colab badge placeholder: `[](<repo-url>/blob/main/releases/<VERSION>/demo.ipynb)` - `**What you'll learn:**` — bullet per headline feature - `**Sections:**` — numbered TOC with anchor links - 2–3 narrative paragraphs: what release adds, why it matters; `> **Breaking change:**` blockquote if breaking changes present 3. **Install cell** (`# %%`): `# !pip install <package>==<VERSION>` 4. **Config cell** (`# %%`): all notebook-level constants (`OUTPUT_DIR`, `BATCH_SIZE`, etc.); `num_workers` pattern for macOS/Windows safety if training involved 5. **One section per headline feature** — for each: - Markdown cell: `## N. <Feature name>` + prose (before/after, motivation, API shape) - Code cell(s): demonstrate feature; if showing old→new migration, old API in commented block above - Verification cell where output confirms feature works (e.g. print, assertion, plot) 6. **Next steps cell** (`# %% [markdown]`): - `## <N+1>. Next steps` header - Bullet list: docs link, changelog link (GitHub compare URL), migration guide link if breaking changes, links to prior release demos; use `<placeholder-url>` format — never invent real URLs Content rules: - All code must be syntactically valid Python - Placeholder URLs use `<repo-url>`, `<docs-url>` — never invent real URLs - Narrative cells explain WHY, not just what — write for developer who hasn't seen release - No class docstrings or multi-line comment blocks in demo code cells; inline `# comments` only - Breaking changes get both `> **Breaking change:**` callout in title cell AND comparison cell in relevant section ### Phase 3: Write output ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # Reload vars from Shared setup — fresh shell (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="" # LAST_TAG = previous release (range lower bound) — not release being drafted # always .temp/; prepare mode uses releases/$VERSION/ DEMO_OUT=".temp/release-demo-$BRANCH-$DATE.py" mkdir -p .temp # timeout: 5000 ``` Write generated script to `$DEMO_OUT` using Write tool. Notify: `→ written to $DEMO_OUT` > Convert `.py` → `.ipynb` with `jupytext --to notebook $DEMO_OUT` — user runs this; skill does not execute it. -
prepare.md 8.7 KB
<!-- oss:release Mode: prepare — executed via: cat "$SKILL_DIR/modes/prepare.md"; execute --> <!-- Variables available: $SKILL_DIR, $_OSS_SHARED, $LAST_TAG, $BRANCH, $DATE, $RANGE, $VERSION, $REPO_ROOT, $GATHER_FILE, $CHANGELOG_FILE --> **Trigger**: `/release prepare <version>` (e.g., `prepare v1.3.0` or `prepare 1.3.0`) **Purpose**: Full release pipeline — audit first, generate all artifacts. Use when cutting release; individual modes for drafting. ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # fresh shell loses vars; Mode Detection persists REST to tmpdir IFS= read -r REST < "${TMPDIR:-/tmp}/release-rest-${CSID}" 2>/dev/null || REST="" IFS= read -r LAST_TAG < "${TMPDIR:-/tmp}/release-setup-${CSID}/LAST_TAG" 2>/dev/null || LAST_TAG="" VERSION="${REST%% *}" [[ "$VERSION" != v* ]] && VERSION="v$VERSION" RANGE="${RANGE:-$LAST_TAG..HEAD}" echo "$VERSION" > "${TMPDIR:-/tmp}/release-prepare-version-${CSID}" # persist for later blocks (Check 41) echo "$RANGE" > "${TMPDIR:-/tmp}/release-range-${CSID}" # reuses the shared sentinel written by release/SKILL.md:256, read at :125,:442 and classify-truth-check.md:84 echo "range: $RANGE" # BRANCH, DATE, REPO_ROOT, SKILL_DIR from Shared setup above ``` ### Phase 1: Readiness audit Run all checks from **Mode: audit** with `$VERSION` as target. `| Check | Status | Detail |` readiness table must appear inline in terminal before proceeding — audit-checks.md requires this even in sub-phase context. If table absent from response after running audit, re-execute terminal output step from audit-checks.md before continuing. **If verdict is BLOCKED**: stop. List blockers, tell user to resolve before re-running `/release prepare $VERSION`. Write no artifacts. **If verdict is READY or NEEDS_ATTENTION**: surface warnings, continue to Phase 2. ### Phase 2: Gather, classify, and changelog **a. Gather and classify** — spawn gather subagent per **Delegation strategy** for `$RANGE`; write findings to `GATHER_FILE`. Read returned JSON envelope; pass file path downstream. Don't read gather file into main context. Note `breaking` count from envelope — gates Phase 3b (migration guide). After envelope validation, check `unconfirmed_breaking` from envelope: if > 0, apply post-validation truth-check gate from **Delegation strategy** (partial `[UNCONFIRMED]` read + `AskUserQuestion` per breaking item) before proceeding to 2b. **b. Audit changelog** — apply **Audit changelog** logic inline: locate `$CHANGELOG_FILE` (per search order in Audit changelog section), cross-check classified changes from `$GATHER_FILE`, add missing entries, stamp unreleased section as `## [$VERSION] — $DATE`. Report: "N items added, M flagged." ### Phase 3: Highlights and migration Set up release directory, back up existing artifacts: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r VERSION < "${TMPDIR:-/tmp}/release-prepare-version-${CSID}" 2>/dev/null || VERSION="" # reload (Check 41) IFS= read -r CHANGELOG_FILE < "${TMPDIR:-/tmp}/release-changelog-file-${CSID}" 2>/dev/null || CHANGELOG_FILE="" # written by Phase 2b's changelog-audit envelope IFS= read -r REPO_ROOT < "${TMPDIR:-/tmp}/release-setup-${CSID}/REPO_ROOT" 2>/dev/null || REPO_ROOT="." [ -n "$CHANGELOG_FILE" ] || CHANGELOG_FILE=$(find "$REPO_ROOT" -maxdepth 2 -name "CHANGELOG.md" 2>/dev/null | head -1) # audit.md search order RELEASE_DIR="releases/$VERSION" # Hard-fail: this script's .bak backup loop is the only thing standing between a re-run and silent # loss of hand-edited HIGHLIGHTS/MIGRATION/SUMMARY/DRAFT, which Phases 3a-5 overwrite unconditionally. python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/setup_release_dir.py" "$RELEASE_DIR" "$CHANGELOG_FILE" \ || { echo "! BLOCKED — setup_release_dir failed (RELEASE_DIR='$RELEASE_DIR' CHANGELOG_FILE='$CHANGELOG_FILE'); artifact backups did not run — refusing to continue into the overwriting Write phases"; exit 1; } # timeout: 5000 ``` **a. Identify highlights** — apply **Identify highlights** logic using classified changes from `$GATHER_FILE`: rank top 3–5 most significant changes (breaking > new public API > major UX > notable fixes), pull one concrete code example per highlight from diff. Write to `releases/$VERSION/HIGHLIGHTS.md`. Source of truth for demo, executive summary, release draft spotlights **b. Draft migration guide** — apply **Draft migration guide** logic using breaking/deprecated changes from `$GATHER_FILE`. No breaking changes → single line: `No breaking changes in this release.` Shepherd voice review applies. Write to `releases/$VERSION/MIGRATION.md`. ### Phase 4: Demo and summary **a. Demo notebook** — reuse `$GATHER_FILE` and `releases/$VERSION/HIGHLIGHTS.md` from Phase 3. Apply demo generation logic from **Mode: demo**, Phase 2 (Generate demo script). Output path: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r VERSION < "${TMPDIR:-/tmp}/release-prepare-version-${CSID}" 2>/dev/null || VERSION="" # reload (Check 41) DEMO_OUT="releases/$VERSION/demo.py" echo "$DEMO_OUT" > "${TMPDIR:-/tmp}/release-prepare-demo-out-${CSID}" # persist for next block (Check 41) ``` Write generated script to `$DEMO_OUT` using Write tool. **Execution gate** — run: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r DEMO_OUT < "${TMPDIR:-/tmp}/release-prepare-demo-out-${CSID}" 2>/dev/null || DEMO_OUT="" # reload (Check 41) python "$DEMO_OUT" # timeout: 600000 ``` <!-- policy-sibling: plugins/cc_oss/skills/release/SKILL.md §Generate release demo (demo retry bound) --> Fix and re-run until exits 0 with expected output — **max 3 attempts total**. Don't proceed to 4b until gate passes. Still failing after the 3rd attempt: stop retrying, invoke `AskUserQuestion` ("Demo still failing after 3 attempts. Exclude from release and continue, or abort?") — (a) **Exclude and continue**: mark demo excluded, drop the `demo.py` bullet from the Written list and the `jupytext` item from Next steps, note the exclusion + last failure reason in the output, and run 4b from `HIGHLIGHTS.md` alone · (b) **Abort**: stop, report the 3 failed attempts, write no further artifacts. **b. Executive summary** — apply **Draft executive summary** logic using `releases/$VERSION/HIGHLIGHTS.md` and demo output (demo excluded → HIGHLIGHTS.md alone). Write to `releases/$VERSION/SUMMARY.md`. ### Phase 5: Write release draft `releases/$VERSION/DRAFT.md` — final assembly. Source: `releases/$VERSION/HIGHLIGHTS.md` (spotlights), `releases/$VERSION/MIGRATION.md`, `releases/$VERSION/SUMMARY.md`. Apply **Write release draft** logic (release-draft.md format). Adversarial review applies (use `$GATHER_FILE` from Phase 2a as gather context). Shepherd voice review applies. ### Output ```markdown ## Release prepare: $VERSION ### Audit Reproduce the full Phase-1 readiness table verbatim — the `| Check | Status | Detail |` markdown table from audit-checks.md with ALL check rows (Working tree, CI, Blocking issues, Open PRs, README aligned, CHANGELOG entry, Version consistent, Dependency CVEs, Scheduled removals, Doc proportionality) and their Status glyphs (`✓`/`⚠`/`✗`). "Condensed" applies to the Detail column only (trim verbose detail) — never to row count. Do NOT replace this table with a finding-bullet digest, and do NOT substitute a different table (e.g. a `File | Status` artifacts box). [any warnings carried forward] ### Written (documentation artifacts — complementary to the release, not the release itself) Render as the markdown bullet list below — NOT a box-drawing (`┌─┬─┐`) `File | Status` table. - `$CHANGELOG_FILE` — $VERSION entry stamped (Phase 2b); `releases/$VERSION/CHANGELOG.md` symlinks here - `releases/$VERSION/HIGHLIGHTS.md` — top 3–5 spotlights with code examples (Phase 3a) - `releases/$VERSION/MIGRATION.md` — migration guide (N breaking changes, or "No breaking changes") (Phase 3b) - `releases/$VERSION/demo.py` — story-telling jupytext notebook (Phase 4a; omit if demo excluded) - `releases/$VERSION/SUMMARY.md` — internal executive summary (Phase 4b) - `releases/$VERSION/DRAFT.md` — user-facing release notes, final assembly (Phase 5) ### Next steps 1. Review all written files 2. Bump version in the project manifest 3. Commit, push, open PR 4. On merge: publish the release — `gh release create $VERSION --notes-file releases/$VERSION/DRAFT.md` (user-run; DRAFT.md is source for release notes, not the release itself) 5. Upload package to PyPI (or relevant registry) — separate step after GitHub release 6. Convert demo: `jupytext --to notebook releases/$VERSION/demo.py` (skip if demo excluded) ``` End terminal response (not written artifacts) with `## Confidence` block per CLAUDE.md output standards: `**Score**: 0.0–1.0 — [label]`; omit Refinements if 0 passes. -
release-draft-template.md 25.4 KB
<!-- file: release-draft-template.md — consumers: release/SKILL.md (## Write release draft section) --> ### CHANGELOG Entry (`--changelog` flag) Use this format: ```markdown ## [version] — [date] ### Added ### Changed ### Deprecated ### Removed ### Fixed ### Security ``` ### Internal Release Summary (`--summary` flag) Use this format: ```markdown ## Release [version] **What shipped**: [2-3 sentence summary of the most important changes] **Impact**: [who is affected and how] **Action required**: [anything ops/support/consumers need to do] **Rollback**: [safe to roll back? any caveats?] ``` ### Semantic consistency review Runs on full draft after adversarial review, before writing to disk. Check for each: | Check | What to look for | Flag format | | -- | -- | -- | | **Double-mention** | Same concept named twice under different labels (e.g. "async functions" and "async generators" as separate entries for same change) | `DUPLICATE: "<A>" and "<B>" describe the same change — merge or drop one` | | **Impossible fix** | 🔧 Fixed entry whose subject was introduced in this same release (can't fix what was never shipped) | `IMPOSSIBLE-FIX: "<entry>" — feature added this release, can't be a fix` | | **Causation non sequitur** | "X: Y" where Y doesn't explain or follow from X | `NON-SEQUITUR: "<X>: <Y>" — Y doesn't explain X` | | **Contradictory claim** | Headline or first sentence asserts X; immediate caveat or next sentence denies X | `CONTRADICTION: "<headline>" contradicted by "<caveat>"` | | **Verbatim duplication** | Identical or near-identical sentence appearing in ≥2 sections (Summary, Spotlight, Notable changes, Migration guide) | `VERBATIM-DUP: "<sentence>" appears in <section A> and <section B>` | | **Misclassified scope** | Internal-only change (dead code removal, doc reformat, test-only, CI config) appearing in user-facing section | `SCOPE: "<entry>" is internal-only — move to Internal or remove` | For each finding: emit one flag line with location (`§<section-name>`, item text). Collect all findings before taking action — don't fix inline during scan. **After scan**: zero findings → proceed to Polish. Findings present → list all; fix each; re-scan once; proceed only when clean. ### Polish and write to disk ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # Reload SKILL_DIR (Check 41: fresh shell) IFS= read -r SKILL_DIR < "${TMPDIR:-/tmp}/release-setup-${CSID}/SKILL_DIR" 2>/dev/null || SKILL_DIR="" [ -f "$SKILL_DIR/guidelines/writing-rules.md" ] && cat "$SKILL_DIR/guidelines/writing-rules.md" # timeout: 5000 ``` Follow above (if present). If absent, proceed without style guidelines. Dispatch shepherd for public-facing voice/tone review before writing to disk. Check availability first: ```bash SHEPHERD_AVAILABLE=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/check_agent.py" oss shepherd 2>/dev/null) # timeout: 5000 # expand to literal value before spawning SHEPHERD_DIR=".temp/release-shepherd-$(git branch --show-current 2>/dev/null | tr '/' '-' || echo 'main')-$(date +%Y-%m-%d)" mkdir -p "$SHEPHERD_DIR" # timeout: 5000 ``` If `$SHEPHERD_AVAILABLE` equals `true`: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r _OSS_SHARED < "${TMPDIR:-/tmp}/release-oss-shared-${CSID}" 2>/dev/null || _OSS_SHARED="" [ -f "$_OSS_SHARED/shepherd-voice.md" ] || { echo "⚠ shepherd-voice.md not found — falling back"; SHEPHERD_AVAILABLE=false; } # timeout: 5000 ``` If still `true`, write draft to `$SHEPHERD_DIR/draft.md`, then spawn: ```text Agent(subagent_type="oss:shepherd", prompt="Review the full release draft at <$SHEPHERD_DIR/draft.md> for public-facing voice and tone. Apply shepherd voice guidelines: human and direct, no internal jargon, no staff names, no internal maintenance details. Write the revised content to <$SHEPHERD_DIR/shepherd-revised.md>. Return ONLY: {\"status\":\"done\",\"changes\":N,\"file\":\"<$SHEPHERD_DIR/shepherd-revised.md>\"}") ``` If `oss:shepherd` not available, use draft content directly — skip shepherd review. Humanizer is independent of shepherd availability: still run a `foundry:humanizer` pass (requires `foundry` plugin) on the draft before writing to disk on this branch — shepherd's absence doesn't exempt the draft from it. Read `$SHEPHERD_DIR/shepherd-revised.md` → validate: `if [ -s "$SHEPHERD_DIR/shepherd-revised.md" ]; then SHEPHERD_REVISED_PATH="$SHEPHERD_DIR/shepherd-revised.md"; else echo "⚠ shepherd output empty or missing — using original draft"; SHEPHERD_REVISED_PATH="$SHEPHERD_DIR/draft.md"; fi`. Shepherd runs once per invocation. Write to disk: Shepherd review policy (applies when `$SHEPHERD_AVAILABLE == true`): <!-- branch: draft-exists — only when DRAFT.md non-empty (notes/prepare path); call 1 of ≤2 on notes+changelog path --> - **notes** (always): shepherd review → write to `DRAFT.md` at repo root. - **Append merge** — gate computed once, folding all three preconditions (`--append` set, DRAFT.md present, marker valid) into `$MARKER_VALID`: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # Reload BRANCH, DO_APPEND, LAST_TAG (Check 41: fresh shell) 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" IFS= read -r LAST_TAG < "${TMPDIR:-/tmp}/release-setup-${CSID}/LAST_TAG" 2>/dev/null || LAST_TAG="" if [ "$DO_APPEND" = "true" ] && [ -s DRAFT.md ]; then # is-valid also rejects a marker superseded by a later release tag (see release_append_marker.py) — # keeps this gate in agreement with what Gather changes' `resolve` actually used for $RANGE MARKER_VALID=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/release_append_marker.py" is-valid --branch "$BRANCH" --last-tag "$LAST_TAG") # timeout: 5000 else MARKER_VALID=false fi ``` When `$MARKER_VALID == true`: skip the Overwrite guard below entirely — the incremental range already guarantees only genuinely new commits are in scope, so a merge cannot duplicate prior content. This is a **full pipeline pass scoped to the incremental `$RANGE`** — Gather changes through Draft executive summary already ran above exactly as they do for a full `notes` run, just against fewer commits; this step only changes how the *results* land on disk. Build a merge plan — `$APPEND_ITEMS_FILE` (`.temp/release-append-items-$BRANCH-$DATE.json`), a JSON object mapping section header (exact key text, no markdown prefix) to `{"add": [...], "remove": [...]}`: - `"🚀 Added"` / `"⚠️ Breaking Changes"` / `"🌱 Changed"` / `"🗑️ Deprecated"` / `"❌ Removed"` / `"🔧 Fixed"` — `add` from Classify each change (this run's incremental classification) - `"🏆 Contributors"` — `add` from Extract contributors - `"✨ Spotlights / highlights"` — `add` from Identify highlights (one string per spotlight, its own `### <Feature>` sub-heading + write-up) - `"🔄 Migration guide"` — `add` from Draft migration guide (one string per breaking/deprecated symbol's before→after write-up) - `"📋 Summary"` — `add`: a single short paragraph from Draft executive summary describing just this increment (lands under a "### Since last draft" subheading — never rewrites the original summary paragraph) - `remove` on any of the above: the `matched_text` values from Gather changes' `CROSS_CYCLE_MATCH` list targeting DRAFT.md — a revert/pivot superseding a bullet/block a PRIOR cycle already wrote. Omit or empty-list `add`/`remove` on any section with nothing new/stale. This plan file has two consumers: shepherd (voice review, one artifact to review in a single pass — below) and the Apply step directly below it — **no script ingests this file**; the model reads it and applies it itself. If `$SHEPHERD_AVAILABLE`, dispatch shepherd on this JSON directly (same availability check as above): `Agent(subagent_type="oss:shepherd", prompt="Review wording of every string in each section's \"add\" array in the JSON at <$APPEND_ITEMS_FILE> for public-facing voice/tone (same guidelines as full release notes — human and direct, no internal jargon, no staff names). Never touch \"remove\" arrays — those are match substrings, not published prose. Preserve every key exactly; write the same JSON shape with only \"add\" string values reworded to <$SHEPHERD_DIR/append-items-revised.json>. Return ONLY: {\"status\":\"done\",\"file\":\"<$SHEPHERD_DIR/append-items-revised.json>\"}")`; on success use the revised file as `$APPEND_ITEMS_FILE`, else keep the unrevised one. **Apply the plan to `DRAFT.md` — Read + Edit tool, never a parsing script:** 1. Read `DRAFT.md` (Read tool) — get its actual current section structure; never assume the canonical template order, use whatever heading text is genuinely present (tolerant of a hand-edit dropping an emoji variation selector, changing case, or extra whitespace — still the same section). 2. For each section key in `$APPEND_ITEMS_FILE` with a non-empty `remove`: locate that section. For each `remove` value (an exact bullet/block/line copied verbatim from `CROSS_CYCLE_MATCH`): Edit tool with `old_string` = that exact text plus enough surrounding context (its own blank-line neighbours, or its full `### <name>` sub-heading for a block item) to make the match unique — never `replace_all`. A section emptied down to nothing by removal (all items struck, nothing added) → drop its header line too, no dangling empty stub. 3. For each section key with a non-empty `add`: same located section. No matching heading exists yet → add one in a sensible position near thematically-similar existing content (`templates/release-draft.md`'s section order is the tie-breaker when nothing nearby suggests a better spot — e.g. a new `### 🌱 Changed` heading belongs near `### 🚀 Added` / `### 🗑️ Deprecated`, not at a random spot). Edit tool to insert each new bullet/block into the section body — same unique-context discipline as removal. 4. **Contributors**: dedup by bolded `**Name**` — an existing entry with the same bolded name is never duplicated; a genuinely new name is inserted as a new bullet. 5. **Summary**: never rewrite the original paragraph. New content always lands as an additional paragraph under a `### Since last draft` subheading — search for it inside `## 📋 Summary`'s own body (don't assume a fixed line offset); create it on first use, append to it on later cycles. 6. **Spotlights re-ranking** (when Identify highlights recomputes the top 3–5 over the union of surviving old + newly classified candidates — the winning set can drop or reorder entries, not just add): Edit the ENTIRE section body (every `### <Feature>` block between the heading and the next boundary) to the freshly computed final set, in final order. Carried-over entries keep their existing write-up verbatim; newly promoted ones get a fresh write-up. Same Read-locate-Edit pattern applies uniformly across every mergeable DRAFT.md section — prose (Summary), block (Spotlights, Migration guide), list (Notable-changes subsections, Contributors) — the model adapts each edit to whatever shape the section's actual content has; no separate code path per section kind. Report per-section add/remove counts from what was actually applied. Notify: `→ merged N new item(s), struck M stale item(s) from DRAFT.md`. - **Post-merge re-validation** (only after the Merge above succeeds): the 4 gates below ran scoped to just this cycle's incremental classify output, earlier in the pipeline — re-running them against the FINAL merged DRAFT.md catches drift a prior cycle's content develops from THIS cycle's changes without being a clean, detected `CROSS_CYCLE_MATCH`. Input for every check below is "current merged file content", not "`$RANGE` diff": 1. **Truth check re-run** — same codemap/grep-fallback mechanism and scope rule as `modes/classify-truth-check.md`'s Truth check, applied to every symbol named in the merged DRAFT.md's 🚀 Added/⚠️ Breaking Changes/🌱 Changed bullets AND every Spotlights entry (old survivors + new) — not just this cycle's newly classified set. Not found in current HEAD → same `[REMOVED]` outcome as the original gate; capture its exact bullet/spotlight text into a `POST_MERGE_REMOVE` list, section-scoped (same exact-text discipline as `CROSS_CYCLE_MATCH` — never a bare symbol). 2. **Identify highlights re-rank** — re-run SKILL.md's Identify highlights ranking rule (breaking > new public API > major UX > notable fix) over the union of: current Spotlights entries (post-merge, minus anything `POST_MERGE_REMOVE` struck) + this cycle's own newly classified changes (already in context). Pick top 3–5. Resulting set or order differs from what's currently in DRAFT.md's Spotlights → build `updated_spotlights`: the complete ranked list, verbatim `### <Feature>` write-up for every carried-over entry, freshly drafted for anything newly promoted. 3. **Validate migration docs re-run** — gather every ⚠ Breaking Changes / 🗑️ Deprecated / ❌ Removed bullet now in the merged DRAFT.md (old + new); re-run SKILL.md's Validate migration docs coverage check (same `$MIGRATION_DOC` detection + grep-based outcome table) against all of them, not just this cycle's new items — catches a gap that reopened (e.g. cross-cycle removal struck a bullet but not the migration doc's now-orphaned instructions). 4. **Validate docs re-run** — re-run SKILL.md's Validate docs doc-alignment + doc-weight proportionality check over the full accumulated 🚀 Added set (old + new) in the merged DRAFT.md, same formula and UNDERTREATED threshold. 5. **Audit changelog** — no re-run needed: it already reads live `$CHANGELOG_FILE` state directly at its own invocation time and writes immediately (not deferred to this merge step), so every run is inherently "post-merge" already — nothing to change here. **Apply** — same Read+Edit tool mechanism as Append merge above, scoped to just what these gates found: skip entirely when `$POST_MERGE_REMOVE` is empty and the re-rank produced no change to the Spotlights set (true no-op case). Otherwise: strike each `POST_MERGE_REMOVE` entry from its Notable-changes section (exact-text Edit, same discipline as any cross-cycle removal); if the re-rank changed the Spotlights set or order, Edit the whole Spotlights section body to `updated_spotlights` in final order (drop-or-reorder — not an add/remove delta). Report what changed. Surface migration/doc-alignment gaps as warnings in the final report — same non-blocking, self-correcting pattern as the upstream single-pass gates; never silently ship a draft known to reference a removed symbol or a stale spotlight. **Worked example** (stale spotlight superseded by a later revert): Cycle 1's Identify highlights picks "Async batch API" as a top-3 spotlight (from commit `abc123`); the Provenance record step (see "Post-write bookkeeping" below) computes `abc123`'s patch-id (`patchid1`) and records `{patch_id: patchid1, sha: abc123, artifact: DRAFT.md, anchor_text: "- **Async batch API** — new bulk-processing endpoint. (#10)"}` and a matching entry for the Spotlights block. Cycle 2's incremental range includes `Revert "feat: add async batch API"` — Gather changes' cross-cycle detection reads that revert commit's own `This reverts commit abc123...` trailer, computes `abc123`'s current patch-id (still `patchid1` even if `abc123` was reworded or cherry-picked since Cycle 1 — the diff, not the sha, is what's tracked), looks up `patchid1` in the provenance store, gets both matches back directly (no grep, no semantic confirmation needed) and strikes the Notable-changes bullet via `CROSS_CYCLE_MATCH`. Post-merge Truth check re-run finds the async-batch symbol no longer in HEAD → the "Async batch API" Spotlight is now also unverifiable → added to `POST_MERGE_REMOVE`. Identify highlights re-rank recomputes top 3–5 over the surviving candidates + this cycle's new changes — "Async batch API" drops out; the next-best candidate (say, a previously 4th-ranked fix now promoted to 3rd) takes its place. `updated_spotlights` replaces the whole section in one Edit — the stale entry is gone, never left sitting beside the new set. - **Normal write** — `$MARKER_VALID == false` (no `--append`, `--append` with no valid marker — first use or history rewritten, same as today's full regenerate; the `resolve` note already printed "establishing first append baseline" during Gather changes — or DRAFT.md missing/empty): **Overwrite guard** — if `DRAFT.md` non-empty, invoke `AskUserQuestion` ("DRAFT.md already exists — overwrite, append, or abort?") with: (a) **Overwrite** · (b) **Append** (after `---` separator) · (c) **Abort**. Skip prompt only when DRAFT.md is empty or missing. Notify: `→ written to DRAFT.md` / `→ appended to DRAFT.md` / `→ DRAFT.md unchanged — aborted`. <!-- branch: changelog-confirm — only with --changelog flag; call 2 of ≤2 on notes+changelog path; max 4 total on prepare+changelog+draft path --> - **`--changelog`** (if set): no shepherd (structured, internal) → invoke `AskUserQuestion`: "Ready to prepend to `$CHANGELOG_FILE`?" Options: (a) Proceed · (b) Preview only. On (b): display content, stop. On (a): derive `VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "")` and `VERSION_BARE="${VERSION#v}"`. **Idempotency check**: if `$CHANGELOG_FILE` already contains version header in any supported form (`grep -qF "## [${VERSION_BARE}]" "$CHANGELOG_FILE"` for Keep-a-Changelog `## [1.2.0]`, OR `grep -qF "## [${VERSION}]" "$CHANGELOG_FILE"` for `## [v1.2.0]`, OR `grep -qE "^## v?${VERSION_BARE}([^0-9.]|$)" "$CHANGELOG_FILE"` for `## v1.2.0` / `## 1.2.0`) → skip prepend, notify `→ CHANGELOG.md already contains version header — prepend skipped`; otherwise prepend after `# Changelog` heading (create if missing). Notify: `→ prepended to CHANGELOG.md` **Collapse guard** (used by the `SUMMARY.md`/`MIGRATION.md` merges below — the two whole-file artifacts with no section structure of their own to sanity-check against; `$ARTIFACT` = whichever of the two is being merged this cycle). A cheap, mechanical byte-count trip-wire, not a parser — the one destructive-irreversible failure mode a purely LLM-driven merge can't self-correct from (the actual historical bug here was a whole-file wipe of MIGRATION.md), kept deliberately minimal so it never reintroduces the schema-brittleness this redesign retires. DRAFT.md's own sections are NOT guarded this way — a section legitimately emptying down to a dropped header (all items struck, nothing added) is intended behavior, not corruption, since the rest of the file is still there to sanity-check against. Before this cycle's Read+Edit merge against `$ARTIFACT` begins: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" _BEFORE=$(wc -c < "$ARTIFACT" 2>/dev/null || echo 0) echo "${_BEFORE:-0}" > "${TMPDIR:-/tmp}/release-collapse-guard-${CSID}" # timeout: 3000 ``` After every Edit-tool operation against `$ARTIFACT` this cycle completes: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r _BEFORE < "${TMPDIR:-/tmp}/release-collapse-guard-${CSID}" 2>/dev/null || _BEFORE=0 _AFTER=$(wc -c < "$ARTIFACT" 2>/dev/null || echo 0) if [ "${_BEFORE:-0}" -gt 200 ] && [ "${_AFTER:-0}" -lt 20 ]; then echo "⚠ $ARTIFACT content collapsed from ${_BEFORE}B to ${_AFTER}B during merge" fi # timeout: 3000 ``` Tripped → restore `$ARTIFACT` to the exact content read via the Read tool at the start of this merge cycle (Write tool — the model still holds it in context; no git dependency), stop, surface `⚠ $ARTIFACT merge refused — content would collapse from a substantial file to near-empty; restored pre-merge content, review the cross-cycle strike list manually.` Never leave a collapsed file in place. The 200B/20B threshold is a coarse "was this substantively non-empty before, is it now essentially gone" check — not a markdown-structure inspection. - **`--summary`** (if set): no shepherd (internal). - `$MARKER_VALID == true`: stable path `SUMMARY.md` at repo root — merge, don't overwrite. `SUMMARY.md` missing (first append) → `cp "<executive-summary-draft>" SUMMARY.md` directly, no merge needed. `SUMMARY.md` exists → Read it (Read tool), locate `### Since last draft` inside it (create on first use, append to it on later cycles — same pattern as DRAFT.md's own Summary section, Append-merge step 5 above), Edit tool in this increment's paragraph, guarded by **Collapse guard** above (`$ARTIFACT=SUMMARY.md`) — a merge failure must never silently fall through to an overwrite. Notify: `→ merged into SUMMARY.md` / `→ created SUMMARY.md` (first append) / `→ SUMMARY.md merge refused — left untouched` (Collapse guard tripped). - Otherwise (no `--append`, or no valid marker): Draft executive summary saved to `.temp/output-release-summary-$BRANCH-$DATE.md` — confirm written. Notify: `→ saved to .temp/output-release-summary-<branch>-<date>.md` - **`--migration`** (if set): shepherd review (public-facing). - `$MARKER_VALID == true`: stable path `MIGRATION.md` at repo root — merge. `MIGRATION.md` missing (first append) → `cp "<migration-guide-draft>" MIGRATION.md` directly, no merge needed. `MIGRATION.md` exists → Read it (Read tool); for each new symbol write-up (Draft migration guide), Edit tool to append a new `### <symbol>` block; for each `CROSS_CYCLE_MATCH` targeting `MIGRATION.md`, Edit tool to strike the exact matched block (unique surrounding context, same discipline as DRAFT.md). Guarded by **Collapse guard** above (`$ARTIFACT=MIGRATION.md`) — this is the artifact whose whole-file wipe was the actual historical bug (a headerless MIGRATION.md collapsing to one item, a firing strike emptying it entirely); the guard is the mechanical backstop that replaces the retired script's hard refusal, without reintroducing any markdown-parsing brittleness. Notify: `→ merged into MIGRATION.md` / `→ created MIGRATION.md` (first append) / `→ MIGRATION.md merge refused — left untouched` (Collapse guard tripped). - Otherwise: save to `.temp/output-release-migration-$BRANCH-$DATE.md`. Notify: `→ saved to .temp/output-release-migration-<branch>-<date>.md` **Post-write bookkeeping** (unconditional — runs once, after every artifact bullet above has executed, regardless of which flags were set): - **Provenance record**: for every bullet/block just written or merged this cycle that traces to specific commit(s) — DRAFT.md's Notable-changes subsections, Spotlights, Migration guide; `$CHANGELOG_FILE`'s Unreleased entries; standalone `MIGRATION.md` blocks — record its provenance. **Excluded by design** (never looked up, never struck, so never worth recording): Summary paragraphs (DRAFT.md's own `## 📋 Summary` section and standalone `SUMMARY.md` — both additive-only, no cross-cycle removal path exists for prose) and Contributors (credited per-person, not per-commit-revertible). ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" 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="" PROVENANCE_FILE=".temp/release-provenance-$BRANCH.json" [ -f "$PROVENANCE_FILE" ] || echo "[]" > "$PROVENANCE_FILE" # timeout: 3000 ``` Read `$PROVENANCE_FILE` (Read tool). For each qualifying bullet/block, build one record per contributing commit sha (from Classify each change's sha tracking — see `modes/classify-truth-check.md` "PR accumulation") sharing that bullet's `anchor_text`. Compute each sha's content-stable identity first — this, not the sha, is the record's matching key: ```bash PATCH_ID=$(git show "<full-40-char-sha>" | git patch-id --stable | awk '{print $1}') # timeout: 3000 # empty for a merge commit shown without -m, or a genuinely empty commit — rare here since a # qualifying bullet traces to a real user-facing diff; record patch_id: null when this happens, # the entry then can only ever be struck via the semantic path (documented gap, not a bug) ``` `{"patch_id": "<40-hex patch-id, or null>", "sha": "<full 40-char sha — debug metadata only, never a matching key>", "subject": "<original commit subject — human debugging only, never a matching key>", "artifact": "DRAFT.md"|"CHANGELOG.md"|"MIGRATION.md", "anchor_text": "<exact text just written, verbatim>", "written_at": "$DATE"}`. Append every new record to the array just read; write the complete updated array back to `$PROVENANCE_FILE` (Write tool). A `CROSS_CYCLE_MATCH`-struck bullet from THIS cycle is never re-recorded — it was removed, not written. - **Marker refresh**: persists the baseline for future `--append` runs. ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r BRANCH < "${TMPDIR:-/tmp}/release-setup-${CSID}/BRANCH" 2>/dev/null || BRANCH="" HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "") # timeout: 3000 [ -n "$HEAD_SHA" ] && python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/release_append_marker.py" write --branch "$BRANCH" --sha "$HEAD_SHA" # timeout: 5000 ``` **Human gate** — stop, hand off after writing files. GitHub release must be created with project-level tooling (`gh release create`). Exact release steps: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r _OSS_SHARED < "${TMPDIR:-/tmp}/release-oss-shared-${CSID}" 2>/dev/null || _OSS_SHARED="" cat "$_OSS_SHARED/release-checklist.md" # timeout: 5000 ``` > Confidence block — notes mode: end response here with `## Confidence` block per CLAUDE.md output standards.
-
-
templates
-
audit-checks.md 6.7 KB
<!-- file: audit-checks.md — consumers: oss skills/release/modes/audit.md (Phase 2), oss skills/release SKILL.md (Write release draft phase pre-flight) --> ```bash eval "$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/parse-skill-flags.py" --flags changelog,summary,migration,append "$ARGUMENTS")" # timeout: 5000 TARGET=$(echo "$CLEAN_ARGS" | awk '{print $2}') # optional target version, positional — flags stripped first # use caller RANGE if set (branch-aware detection, Shared setup) # fallback: git describe, stable-tag-only filter — matches skill detection if [ -z "$RANGE" ]; then LAST_TAG=$(git describe --tags --abbrev=0 --exclude='*rc*' --exclude='*dev*' --exclude='*alpha*' --exclude='*beta*' 2>/dev/null || git rev-list --max-parents=0 HEAD) RANGE="$LAST_TAG..HEAD" fi ``` ### Data gathering (Checks 1, 2, 3, 4a, 4b, 5, 6 + gh-auth preflight) In `bin/run_audit_checks.py` — emits sectioned output (`--- check: <name> ---` banners): repo state (`git status`, unreleased commits), CI health (`gh run list`), open issues+PRs, files changed in range, version-declaration grep, release-blocking TODO/FIXME/HACK grep, pip-audit CVE scan. Invokes `bin/parse_audit_json.py` to summarize pip-audit JSON. Caller captures one buffer for parsing: ```bash # loads: run_audit_checks.py, parse_audit_json.py AUDIT_OUT=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/run_audit_checks.py" \ --repo "$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null)" \ ${TARGET:+--tag "$TARGET"} \ ${RANGE:+--range "$RANGE"}) # timeout: 60000 echo "$AUDIT_OUT" ``` Script exits `2` if `gh` not authenticated — surface as `BLOCKED` verdict immediately, skip interpretive steps below. ### Check 4a interpretation (after data is in `$AUDIT_OUT`) Read `README.md`: install/usage match current API, version refs not stale, deprecated APIs have notes. `docs/` exists → read all changed public API sections. Check `CHANGELOG.md`: `[Unreleased]` or `$TARGET` section covers `$RANGE` commits? ### Check 4b: Doc weight ratio (🚀 Added features) For each 🚀 Added entry in CHANGELOG `$TARGET`/`[Unreleased]` naming a significant new entity (public skill, command, agent, submodule, mode): compute **doc weight** for it and 2–3 comparable existing features (same task type, mode category, conceptual peer) in relevant README/docs. Doc weight = `header_score + coverage_score + example_score`: - `header_score`: H2 = 3, H3 = 2, H4/deeper = 1, no heading = 0 - `coverage_score`: `min(non_blank_lines_in_section / 5, 5)` — non-blank lines from feature heading to next same-or-higher heading - `example_score`: fenced code blocks in section, capped at 3 Weight ratio = `new_feature_weight / mean(comparable_weights)`. Flag **HIGH** (UNDERTREATED) if ratio < 0.5. Report: `- [UNDERTREATED] <feature>: weight N vs peers M1/M2 (ratio R)`. Add to findings table, `severity: high`. ### Check 5 interpretation All version declarations in `version-consistency` section must match. `$TARGET` given → verify or flag needs bump. ### Check 6 interpretation: pip-audit gap (after data is in `$AUDIT_OUT`) `run_audit_checks.py` stays non-interactive — if `pip-audit` missing, prints machine-readable line (`pip-audit-status: not-installed`) instead of prompting. Check: ```bash echo "$AUDIT_OUT" | grep -q '^pip-audit-status: not-installed' && PIP_AUDIT_MISSING=1 || PIP_AUDIT_MISSING=0 ``` `PIP_AUDIT_MISSING=1` → invoke `AskUserQuestion`: "pip-audit not installed — CVE dependency scan will be skipped. Install now (`pip install pip-audit`) and rerun the scan?" Options: (a) Skip CVE check for this run (b) Install and rerun — Recommended. - **Install and rerun**: install, re-run only CVE step (no need to re-run full data-gathering script — gh/git checks already in `$AUDIT_OUT`): ```bash pip install pip-audit # timeout: 90000 pip-audit --format=json | python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/parse_audit_json.py" # timeout: 30000 ``` Fill Dependency CVEs row with real `N deps, M vulns` result, same as any successful scan. - **Skip**: Dependency CVEs row stays `⚠ pip-audit not installed — CVE scan skipped` — informational soft gap, same convention as Phase 1a's "target version unknown" case (`modes/audit.md`): never added to Findings summary table as own finding, doesn't block `READY` alone. ### Output Print readiness report: ```markdown --- repo: [repo-name] date: [YYYY-MM-DD] version: [version or "next"] range: [range] mode: [linear | stable-branch (cherry-pick)] commits: [N non-merge commits] verdict: [READY | NEEDS_ATTENTION | BLOCKED] --- ## Release Readiness — [repo] [version or "next release"] Date: [date] | Range: [last-tag]..HEAD ([N] commits) | Check | Status | Detail | |------------------|--------|--------| | Release mode | Linear / Stable-branch (cherry-pick) | [N pending cherry-pick subjects, or "—" if linear — informational only, never blocking by itself] | | Working tree | ✓ Clean / ⚠ N files | [filenames if dirty] | | CI (last 5 runs) | ✓ Passing / ✗ N failing | [failing job names] | | Blocking issues | ✓ None / ✗ N open | [#N title] | | Open PRs (main) | ✓ None / ⚠ N open | [PR titles] | | README aligned | ✓ / ⚠ Review needed | [reason if flagged] | | CHANGELOG entry | ✓ Present / ✗ Missing | [section name or "add [Unreleased]"] | | Changelog scope | ✓ Clean / ✗ N flagged | [sha + non-PR merge sha, or "already released in <section>"] | | Version consistent | ✓ / ⚠ Mismatch | [files and values] | | Dependency CVEs | ✓ Clean / ⚠ N vulns | [package names] | | Scheduled removals | ✓ All removed / ✗ N still present | [symbol names with `remove_in` version] | | Doc proportionality | ✓ / ⚠ N features undertreated | [feature names — no dedicated section / no example / thin coverage] | | Upstream review verdict | ✓ None blocking / ✗ Blocking | [review report path + outcome, Phase 1b] | | Codex adversarial audit | ✓ Clean / ⚠ N findings / — skipped (codex unavailable) | [finding summary, Phase 2a] | ### Verdict **READY** — no blockers. Run `/release prepare <version>` to write artifacts. — or — **NEEDS_ATTENTION** — N items before release: - ✗ [blocking item] - ⚠ [recommended item] ### Next steps [e.g., "resolve open PRs → re-run `/release audit v1.3.0` to verify → `/release prepare v1.3.0`"] ``` **Terminal output** — after writing report file, print readiness check table (`| Check | Status | Detail |` rows only, no YAML header, no verdict prose) directly to terminal, inline in Claude response. Mandatory even when audit runs as sub-phase of `/release prepare` — never treat table as intermediate pipeline output or route only to report file; must appear in terminal before prepare proceeds to Phase 2. -
gather-prompt.md 3.8 KB
<!-- file: gather-prompt.md — consumers: oss/skills/release/SKILL.md (Delegation strategy) --> Working directory: \<REPO_ROOT>. Run all git commands from there (git -C \<REPO_ROOT> <cmd>, or cd \<REPO_ROOT> first). For git range <RANGE>: Run gather phase: git log, git diff --stat, gh pr list. Retain verified commit-to-PR associations and each in-range PR author's known login/display name in \<GATHER_FILE> for contributor extraction. Establish range membership from release commits and verified PR linkage — not merge dates or full merged-PR list. Include authors absent from Git authors/coauthors (e.g. maintainer-authored squash merges without original-author trailers). Keep this identity inventory even when classification groups changes or drops net-zero claims. Missing linkage, deleted identities, or incomplete PR metadata must remain explicit contributor-coverage gaps; never label Git-only coverage complete or invent identities. For every SHA from `git -C <REPO_ROOT> rev-list "<RANGE>"`, run `gh api --method GET --paginate --slurp "repos/{owner}/{repo}/commits/<sha>/pulls" -f per_page=100` from \<REPO_ROOT>. Retain all pages and each SHA's merged-PR associations (PR number, author, base, merge commit); never restrict lookup to default branch. Verify actual repo/commit/diff membership before crediting. Successful empty result retains Git authors/coauthors without inventing a PR; failed, deleted, or ambiguous metadata stays a coverage gap in \<GATHER_FILE>. Do not substitute default-branch merged-PR list after lookup failure. Run classify phase: classify NET state at HEAD, not each intermediate commit. When multiple commits in range touch same API/feature (add then modify, add then remove, add then rewrite), describe only what exists in HEAD — don't include features added and later undone within same range, regardless of whether removal was explicit revert or follow-up PR. When entry survives (net-effect non-zero), collect ALL PR numbers contributing to final state under SAME category — never attribute to only initial or last PR. Group under one bullet with cumulated PR refs ONLY when all contributing PRs classify into same section (both Added, both Changed, both Fixed); when a PR fixes a bug or changes behavior in a feature added by an earlier PR in same range, that fix gets its own 🔧 Fixed or 🌱 Changed entry — never folded into Added. Exception: trivial fixes (one-line cleanup, doc tweak inside new code, no standalone user-visible effect) fold into parent Added bullet. Run explore phase: top 3–5 most significant changed files (read actual diffs). Run truth check phase: for each item classified 🚀 Added or ⚠️ Breaking Changes naming a specific symbol (function, class, method, config key, CLI flag), verify symbol is DEFINED in codebase at HEAD — not just mentioned in comment, docstring, or leftover reference. Prefer codemap over grep: check if codemap index available (`codemap-py query list 2>/dev/null | wc -l` — non-zero = available), then run `codemap-py query find-symbol '^<symbol>$' 2>/dev/null`; empty output = absent. When codemap unavailable, fall back to definition-pattern grep: `git -C <REPO_ROOT> grep -wl 'def <symbol>\|class <symbol>' HEAD -- '*.py' 2>/dev/null` for Python, then `git -C <REPO_ROOT> grep -wl '<symbol>' HEAD -- '*.ts' '*.js' '*.go' '*.rs' 2>/dev/null` for other languages. If both empty, symbol absent: remove from classified section, log 'REMOVED: <item> — symbol not found in HEAD'. Repeat for newly revealed dependencies. Track removed-item count (unconfirmed_total) and how many were ⚠️ Breaking Changes (unconfirmed_breaking). Write full findings (commit list, verified-only classified change table, diff excerpts, REMOVED log) to \<GATHER_FILE> via Write tool. Return ONLY: {"status":"done","file":"\<GATHER_FILE>","changes":N,"breaking":N,"unconfirmed":N,"unconfirmed_breaking":N,"confidence":0.N} -
release-draft.md 1.5 KB
# <version>: <release name> ## 📋 Summary \<2–4 sentence para — what changed + why matters to users/devs> ## ✨ Spotlights / highlights \<top 3–5 features or fixes, each with short code example> ## 🔄 Migration guide \<Breaking change = worked before, fails/differs now, no prior warning or deprecation shim. API deprecated (with warning + forwarding) in prior release then removed → classify as ❌ Removed, not ⚠️ Breaking Changes. Guide migration; include before/after code for each breaking change. If none: "No migration required for this release."> <!-- Use Draft migration guide content — do not regenerate independently. --> ## 📝 Notable changes \<Significant changes, grouped by area/component; list all PRs/commits.> ### 🚀 Added - **Feature Name** — what it does and why it matters. (#PR or commit) ### ⚠️ Breaking Changes - **[Area]** — what changed and what callers must do to migrate. (#PR) ### 🌱 Changed - Behaviour change: old behaviour → new behaviour. (#PR) ### 🗑️ Deprecated - `OLD_NAME` deprecated in favour of `NEW_NAME`. (#PR) ### ❌ Removed - `OLD_API` removed (deprecated since vX.Y). Migrate to `NEW_API`. (#PR) ### 🔧 Fixed - Fixed what was broken when condition. (#PR) ## 🏆 Contributors - **Name** (@github_handle, [LinkedIn](https://linkedin.com/in/handle)) — brief what they did ______________________________________________________________________ **Full changelog**: https://github.com/[org]/[repo]/compare/vPREV...vNEXT
-
-
SKILL.md 58.1 KB
--- name: release description: "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)." argument-hint: '[notes] [v1->v2] [--changelog] [--summary] [--migration] [--append] | prepare <version> | audit [version] | demo [range]' allowed-tools: Read, Write, Edit, Bash, TaskList, TaskCreate, TaskUpdate, Agent, AskUserQuestion, WebFetch model: sonnet effort: high --- <objective> 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). </objective> <inputs> 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). </inputs> <workflow> **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: ```bash # BRANCH, DATE from Shared setup below GATHER_FILE=".temp/release-gather-$BRANCH-$DATE.md" mkdir -p .temp # timeout: 5000 ``` 2. Assert variables before spawning: ```bash [ -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>)` 3. Validate envelope; every "abort" is a hard `exit 1`: ```bash 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`: ```bash 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): ```bash 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 ``` <!-- loads: modes/changelog-audit-prompt.md --> Prompt (loaded above) — execute (spawn Agent A + Agent B per instructions in that file). Validate both envelopes: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/parse_release_envelopes.py" --envelope-a "$ENVELOPE_A" --envelope-b "$ENVELOPE_B" # 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: ```bash 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 | ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" eval "$(python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/parse-skill-flags.py" --flags changelog,summary,migration,append "$ARGUMENTS")" # timeout: 5000 DO_CHANGELOG="$FLAG_CHANGELOG"; DO_SUMMARY="$FLAG_SUMMARY"; DO_MIGRATION="$FLAG_MIGRATION"; DO_APPEND="$FLAG_APPEND" FIRST=$(echo "$CLEAN_ARGS" | awk '{print $1}') # REST keeps the raw blob's tail — downstream readers of the sentinel expect flags intact REST=""; case "$ARGUMENTS" in *" "*) REST="${ARGUMENTS#* }";; esac echo "${REST:-}" > "${TMPDIR:-/tmp}/release-rest-${CSID}" # strip mode token — else leaks into RANGE _PARSE_INPUT="$CLEAN_ARGS"; case "$FIRST" in notes|prepare|audit|demo) case "$CLEAN_ARGS" in *" "*) _PARSE_INPUT="${CLEAN_ARGS#* }";; *) _PARSE_INPUT="";; esac;; esac RANGE=$(echo "$_PARSE_INPUT" | grep -oE '[^ ]+([[:space:]]*->[[:space:]]*|\.\.)[^ ]+' | head -1 | tr -d '[:space:]') for _a in $_PARSE_INPUT; do case "$_a" in --*) 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}" ``` <!-- branch: unsupported-flags — isolated; ≤1 call; fires only when unknown flags present --> **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`): ```bash 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. ```bash 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; } ``` <!-- branch: no-stable-tags — isolated; ≤1 call; fires only when repo has no stable git tags --> 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. ```bash 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 PR_FIELDS="number,title,body,labels,mergedAt,author" if [ -n "$TRUNK" ]; then PRS=$(gh pr list --state merged --base "$TRUNK" --limit 500 --json "$PR_FIELDS") || { echo "Error: gh pr list failed; cannot classify release PRs" >&2; exit 1; } # timeout: 15000 else echo "⚠ Could not detect default branch — listing all merged PRs" PRS=$(gh pr list --state merged --limit 500 --json "$PR_FIELDS") || { echo "Error: gh pr list failed; cannot classify release PRs" >&2; exit 1; } # timeout: 15000 fi PR_COUNT=$(printf '%s' "$PRS" | jq 'length') || { echo "Error: could not count gh pr list results" >&2; exit 1; } if [ "$PR_COUNT" -eq 500 ]; then echo "⚠ gh pr list reached its 500-item cap; recovering the complete merged-PR history" PR_API_ARGS=(--method GET --paginate --slurp 'repos/{owner}/{repo}/pulls' -f state=closed -f per_page=100) if [ -n "$TRUNK" ]; then PR_API_ARGS+=(-f "base=$TRUNK") fi PRS=$(gh api "${PR_API_ARGS[@]}") || { echo "Error: exhaustive PR recovery failed; do not claim release coverage is complete" >&2; exit 1; } # timeout: 60000 PRS=$(printf '%s' "$PRS" | jq '[.[][] | select(.merged_at != null) | {number, title, body, labels, mergedAt: .merged_at, author: .user}]') || { echo "Error: could not normalize exhaustive PR recovery" >&2; exit 1; } fi printf '%s\n' "$PRS" ``` Cross-reference commit bodies against PR descriptions — canonical source of truth for *why* change made. `BREAKING CHANGE:` footer = breaking change regardless of PR label. The default-branch list is discovery context, not the contributor inventory. Query every candidate commit independently of the default branch so maintenance-branch and squash-only PR authors cannot disappear: ```bash # Commit association follows the release range, including maintenance-branch merges. COMMIT_SHAS=$(git rev-list "$RANGE") || { echo "Error: release candidate enumeration failed" >&2; exit 1; } while IFS= read -r CANDIDATE_SHA; do [ -z "$CANDIDATE_SHA" ] && continue PR_ASSOCIATIONS=$(gh api --method GET --paginate --slurp "repos/{owner}/{repo}/commits/$CANDIDATE_SHA/pulls" -f per_page=100) || { echo "Error: PR association coverage unavailable for $CANDIDATE_SHA; retain gap, do not claim complete credits" >&2; exit 1; } # timeout: 15000 printf '%s' "$PR_ASSOCIATIONS" | jq --arg sha "$CANDIDATE_SHA" '{commit: $sha, pull_requests: [.[][] | select(.merged_at != null) | {number, title, author: .user, base: .base.ref, merge_commit_sha}]}' || { echo "Error: invalid PR association evidence" >&2; exit 1; } done <<< "$COMMIT_SHAS" ``` Retain each commit's association result, including empty results, in gathered evidence. Fetch the associated PR's metadata when needed to verify repository and commit/diff membership; do not discard an association because its base is not the default branch. Empty successful lookup means no associated merged PR was returned, not a fabricated author; keep Git authors/coauthors. Failed lookup, deleted author, or ambiguous association remains an explicit gap. Do not silently continue with the default-branch list after an association failure. Retain verified commit-to-PR associations and each in-range PR author's known login/display name for Extract contributors, including PR-only authors of maintainer-authored squash commits. Preserve this identity inventory through classification, including grouped or net-zero changes; record missing association/identity metadata as contributor-coverage gaps. When delegated, write this evidence to `$GATHER_FILE`; merge dates and membership in the collected PR list alone do not establish release-range membership. **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, 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). 1. **Read** the current `DRAFT.md` Notable-changes bullets and `$CHANGELOG_FILE`'s Unreleased section into context (Read tool — actual content, not just piped grep output). 2. **Revert case — patch-id lookup first**: for each revert whose original predates `$RANGE` (the "only revert in range" case above), extract the original sha from the revert commit's own trailer, then convert it to a patch-id — the sha itself is never the lookup key, since it may point at a commit reworded/rebased/cherry-picked since it was recorded, but its diff content (hence patch-id) is unaffected by any of those: ```bash ORIGINAL_SHA=$(git log -1 --format=%B "<revert-commit-sha>" | grep -oE 'This reverts commit [0-9a-f]{40}' | grep -oE '[0-9a-f]{40}') # timeout: 3000 [ -n "$ORIGINAL_SHA" ] && ORIGINAL_PID=$(git show "$ORIGINAL_SHA" | git patch-id --stable | awk '{print $1}') # timeout: 3000 # ORIGINAL_PID empty despite found sha → merge commit w/o -m, or empty commit — rare, treat as "no trailer" below ``` Trailer found and patch-id non-empty → look it up: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r BRANCH < "${TMPDIR:-/tmp}/release-setup-${CSID}/BRANCH" 2>/dev/null || BRANCH="" PROVENANCE_FILE=".temp/release-provenance-$BRANCH.json" [ -f "$PROVENANCE_FILE" ] && jq --arg pid "$ORIGINAL_PID" '[.[] | select(.patch_id == $pid)]' "$PROVENANCE_FILE" # timeout: 3000 ``` One or more matches → each is a confirmed `CROSS_CYCLE_MATCH` directly (step 4) — a patch-id match is definitive, no semantic confirmation needed, even when the matched record's stored `sha` differs from `$ORIGINAL_SHA` (expected whenever the original commit was reworded/rebased/cherry-picked since it was recorded — the diff, not the sha, is what's being matched). No matches → the reverted commit predates any drafted artifact (already published, or never drafted), or its diff has genuinely changed since — falls through to normal ✗ Removed/⚠ Breaking classification, same as today. No trailer found (manual/non-`git revert` revert commit), or trailer found but patch-id computation produced empty output → fall through to step 3's grep-narrow-then-confirm path for this revert, same as a pivot. 3. **Pivot case (and any revert without a usable trailer) — semantic judgment, best-effort not a guarantee**: grep is only a *narrowing hint*, never the decision. Grep for the de-`Revert`-wrapped subject/PR title (revert-without-trailer) or the changed symbol name (pivot: a newly-classified ⚠ Breaking Changes / 🌱 Changed / 🗑️ Deprecated / ❌ Removed item). A grep hit is a candidate to inspect — never an automatic match; a bare substring (e.g. `run`, `Config`) can hit unrelated bullets, treat every hit as "maybe." **Confirm semantically**: does the candidate line genuinely describe the same feature/symbol this new commit reverts or supersedes? Only a positive judgment call proceeds to step 4. Known limitation, accepted: a prior cycle's bullet reworded into human prose by `oss:shepherd` can defeat this grep hint — e.g. raw subject `Revert "feat: add ConfigLoaderV2"` vs. shepherded bullet "dropped the legacy config loader." Don't chase this with fuzzier matching, which only trades false-negatives for false-positives — the patch-id path above already removes this failure mode for the common case of a real `git revert`. 4. **Record** `CROSS_CYCLE_MATCH: {artifact: <file>, matched_text: <exact current line/bullet, copied verbatim>, via: "patch_id"|"semantic"}` — matched_text must always be a full existing line/bullet, **never a bare symbol/subject substring**, so the Edit-tool strike (see `modes/release-draft-template.md` "Append merge") stays scoped to the one confirmed entry instead of risking a match on every bullet that happens to contain the token. Treat as **net-state removal** — same Net-state principle as within-range `REVERT_SET`, just spanning cycles: the reader never saw the reverted/superseded content ship in a published release, so it vanishes from both artifacts entirely, no redundant ✗ Removed/⚠ Breaking bullet added. No patch-id match, no candidate found, or a candidate inspected but not confirmed → proceed as a normal, purely-additive item (default — never manufacture a match speculatively). A stale bullet surviving the semantic path is an accepted best-effort gap, not silent data loss — the Semantic consistency review pass (`modes/release-draft-template.md`, runs before every write) is the last line of defense that can still catch a surviving contradiction. Collect all `CROSS_CYCLE_MATCH` entries for Audit changelog and Write release draft to consume. ## Explore codebase For top 3–5 significant changes (features, breaking, major behavior), read actual diff or changed files: ```bash git diff "$RANGE" -- <file> # timeout: 3000 git show <commit>:<file> # timeout: 3000 ``` Goal: understand new APIs, parameters, behavior — notes describe real functionality, not just commit subjects. Skip trivial changes (typos, dep bumps, CI config). ## Validate docs Check public API surface in docs/ (or README) matches diff. Flag public symbol added/renamed/removed in Gather changes but absent from docs. Report: `- [MISSING/STALE] <symbol> in <doc-file>`. Empty list = docs aligned. **Doc weight check** — for each 🚀 Added change identifying significant new entity (new public skill, new command, new agent, new submodule, new mode): compute **doc weight** for that feature and 2–3 comparable existing features of same nature in relevant README or docs file. Doc weight = `header_score + coverage_score + example_score`: - `header_score`: H2 = 3, H3 = 2, H4/deeper = 1, no heading = 0 - `coverage_score`: `min(non_blank_lines_in_section / 5, 5)` — lines from feature heading to next same-or-higher heading - `example_score`: fenced code blocks in section, capped at 3 Weight ratio = `new_feature_weight / mean(comparable_weights)`. Flag UNDERTREATED when ratio < 0.5. Report: `- [UNDERTREATED] <feature> in <doc-file> — weight N vs peers M1/M2 (ratio R)`. Collect as `doc_proportionality` list in findings. ## Classify each change <!-- loads: modes/classify-truth-check.md (single load — Classify/Truth check/Breaking-change classification below run sequentially in this same path, no branch skips an earlier phase while running a later one; file content stays in context for the two "Follow above" refs below) --> ```bash 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/modes/classify-truth-check.md" # timeout: 5000 ``` Follow above and execute. Contains: category table, PR accumulation rules, dedup rules, OMIT-INTERNAL body-signal override, cherry-pick annotation. ## Truth check Follow `modes/classify-truth-check.md` (Truth check section, loaded above) and execute. Gate: runs after Classify, before Audit changelog. Verifies 🚀 Added / ⚠ Breaking Changes / 🌱 Changed symbols exist in HEAD via codemap or grep fallback. Max 3 loop iterations. ## Breaking-change classification Follow `modes/classify-truth-check.md` (Breaking-change classification section, loaded above) and execute. Codemap-gated (skips without a v3 index). For each diff-derived public symbol, `fn-rdeps --exclude-tests` labels it Breaking (caller outside its own package) or internal; Breaking symbols move to ⚠ Breaking Changes with caller evidence, and `migration_lines` feed the Draft migration guide as `breaking_callers` findings. ## Validate migration docs Gate — runs after Truth check. Only when project has migration docs page. **Detect** — migration doc OR any alternative describing API changes between versions: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # reload REPO_ROOT (Check 41) IFS= read -r REPO_ROOT < "${TMPDIR:-/tmp}/release-setup-${CSID}/REPO_ROOT" 2>/dev/null || REPO_ROOT="" MIGRATION_DOC=$(find "$REPO_ROOT" -maxdepth 3 \( \ -iname "MIGRATION*" -o -iname "UPGRADING*" -o \ -iname "migration.md" -o -iname "upgrading.md" -o \ -iname "CHANGELOG*" -o -iname "BREAKING*" -o \ -iname "api-changes*" -o -iname "release-notes*" \ \) -not -path "*/node_modules/*" -not -path "*/.venv/*" -not -path "*/.git/*" \ | head -1) # timeout: 5000 [ -z "$MIGRATION_DOC" ] && MIGRATION_DOC=$(find "$REPO_ROOT/docs" -maxdepth 2 \( \ -iname "migration*" -o -iname "upgrade*" -o -iname "breaking*" -o -iname "api-changes*" \ \) 2>/dev/null | head -1) # timeout: 5000 ``` Skip entirely when `$MIGRATION_DOC` empty — no migration/upgrade docs exist in project. **When found**: for every classified item in ⚠ Breaking Changes, 🗑️ Deprecated, and ✗ Removed — verify present and described in `$MIGRATION_DOC`. "Present" = migration doc contains symbol name or semantically equivalent reference with upgrade instructions. Check each item: ```bash grep -i "<symbol_or_key>" "$MIGRATION_DOC" 2>/dev/null # timeout: 3000 ``` **Outcomes**: - Found with upgrade path → `✓ <symbol> covered` - Found but no upgrade path → `[SHALLOW] <symbol> in <doc> — present but missing upgrade instructions` - Not found → `[MISSING-MIGRATION] <symbol> — ⚠ Breaking/🗑️ Deprecated but absent from <doc>` Collect all findings as `migration_gaps` list. Zero findings → migration doc complete. Report before proceeding. **Do not block** on `[SHALLOW]` findings — flag and continue. `[MISSING-MIGRATION]` findings surface as warnings; Draft migration guide phase must fill the gaps. ## Audit changelog **`prepare`/`audit` modes**: delegated in parallel (see Delegation strategy). Reload paths: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r CHANGELOG_AUDIT_FILE < "${TMPDIR:-/tmp}/release-changelog-audit-${CSID}" 2>/dev/null || CHANGELOG_AUDIT_FILE="" IFS= read -r CHANGELOG_FILE < "${TMPDIR:-/tmp}/release-changelog-file-${CSID}" 2>/dev/null || CHANGELOG_FILE="" ``` Read `$CHANGELOG_AUDIT_FILE` for audit findings; report added/flagged counts from delegation envelope. If file missing (delegation skipped), fall back to inline below. **`notes` mode or delegation fallback**: Search order: `CHANGELOG.md` at repo root, `docs/CHANGELOG.md`, any `CHANGELOG*` one level deep (excluding `node_modules/`, `.venv/`, `vendor/`). Store as `$CHANGELOG_FILE`. If exists: cross-check against unreleased section. Items absent → add (same emoji format). Items in CHANGELOG not matching classified → flag for review (no auto-delete). For each REVERT_SET pair: add `🔄 Reverted: <original change description> (introduced and reverted in this release)`. Preserve historical entries; remove only the exact matching Unreleased entry for an original change that has not shipped, when that stale claim is confirmed. Reverted items never in highlights or migration guide. For each `CROSS_CYCLE_MATCH` targeting `$CHANGELOG_FILE` (from Gather changes' cross-cycle detection — original predates `$RANGE`, matched text found in Unreleased from a prior `--append` cycle): strike/remove the matched entry the same way — do not add a redundant `🔄 Reverted` bullet for something the reader never saw shipped in this visible cycle. If missing: create `CHANGELOG.md`; populate with `# Changelog` header and `## [Unreleased]` from Classify. **Scope check** (same rule as delegated Agent A — see `modes/changelog-audit-prompt.md`): `git log $RANGE --merges --pretty='%H %P %s'`, keep rows whose subject doesn't match `merge pull request #[0-9]+|\(#[0-9]+\)` — a raw branch merge landed inside `$RANGE`, not a real PR merge. Diff each such commit's two parents to list what it pulled in; any pulled-in commit already shipped in a prior CHANGELOG section is `critical` — it's about to appear a second time in the wrong release under this PR by accident. Report as its own line, never folded into the add/flag count silently. Always report: "N items added, M flagged for review, K scope-flagged (non-PR branch merge)." This phase owns CHANGELOG-format classification; Write release draft reads from it — does NOT copy. DRAFT.md uses different format. ## Extract contributors **`prepare`/`audit` modes**: delegated in parallel (see Delegation strategy). Reload path: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r CONTRIBUTORS_FILE < "${TMPDIR:-/tmp}/release-contributors-${CSID}" 2>/dev/null || CONTRIBUTORS_FILE="" ``` Read `$CONTRIBUTORS_FILE` for formatted contributors list. If file missing (delegation skipped), fall back to inline below. **`notes` mode or delegation fallback**: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # reload $RANGE (Check 41) IFS= read -r RANGE < "${TMPDIR:-/tmp}/release-range-${CSID}" 2>/dev/null || RANGE="" python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_oss}/bin/extract_contributors.py" --range "$RANGE" --include-bots # timeout: 5000 ``` `extract_contributors.py --include-bots` emits one `Name <email>` line per contributor, already deduplicated by email. Keep GitHub privacy-email humans; separate bot identities into the single automated-contributions line required by `guidelines/writing-rules.md`. Every commit counts, including docs and typo fixes. Build the contributor inventory as the union of Git authors/coauthors and every PR author verified to belong to this release range. Use Gather changes' commit-to-PR associations, not the entire merged-PR list or merge dates alone. Include PR-only authors even when a maintainer authored the squash commit and no coauthor trailer exists. Preserve the PR-to-commit evidence in the gathered change table passed to the delegated path. If association or PR-author coverage is unavailable, flag incomplete contributor coverage; never claim completeness from Git alone. Reconcile duplicates only through verified login/email linkage, including GitHub privacy-email handles; display-name similarity alone is not identity proof. Keep unmatched Git people and PR-only people; use the known login when a PR author's display name is unavailable, never invent an email or name. Deleted/unavailable PR identities remain an explicit coverage gap. Aggregate bot identities before individual formatting or profile lookup; never send bots through those human-only steps. For each human contributor, inspect commits in range (`git log "$RANGE" --no-merges --author="<email>" --oneline`) when their email is known; for PR-only authors, use their verified in-range PR changes instead. Pick up to 3 most significant contributions. Rank: new public API > major UX improvement > significant fix > internal change > docs/typo. No PR numbers, no issue links, no `(#N)` references in public credit prose. Resolve GitHub handles from the verified PR author data (`author.login`) or confirmed Git identity linkage. If unresolved, omit the handle. For each resolved human handle, find a LinkedIn link via this ordered chain — stop at first hit, never guess/infer/search-by-name at any step. Contributor **name is never a matching key anywhere in this chain** — only the resolved GitHub handle (steps 1, 2, 4 below) or an anchor href read directly from fetched page content (step 3): 1. **Primary — Social Accounts API** (strongest signal: explicitly added by the person to their own GitHub profile): ```bash gh api "/users/<login>/social_accounts" --jq '.[] | select(.provider=="linkedin") | .url' 2>/dev/null # timeout: 6000 ``` Non-empty output → use directly, done. 2. **Fallback A — `.blog` field**: ```bash gh api /users/<login> --jq '{blog: .blog, twitter: .twitter_username}' 2>/dev/null # timeout: 6000 ``` `.blog` contains `linkedin.com` → use directly, done. 3. **Fallback B — personal-page exception**: `.blog` is a non-empty URL, NOT a `linkedin.com` URL → `WebFetch` that page, scan its actual returned content for `linkedin.com/in/...` anchor links (real hrefs read from the page, never inferred from surrounding text). Exactly one distinct such link found → use it, done. Zero or multiple distinct links found → do not guess; omit LinkedIn for this contributor. 4. **Fallback C — past releases**: search for a Contributors entry, keyed by this EXACT GitHub handle, that a prior release already resolved and credited with a `[LinkedIn](...)` link: ```bash grep -rn "@<login>" CHANGELOG.md docs/CHANGELOG.md releases/*/SUMMARY.md releases/*/DRAFT.md 2>/dev/null | grep -m1 '\[LinkedIn\](' # timeout: 5000 # only when the above finds nothing — scan published release bodies (stop at first match): for tag in $(gh release list --limit 100 --json tagName --jq '.[].tagName' 2>/dev/null); do # timeout: 15000 gh release view "$tag" --json body --jq '.body' 2>/dev/null | grep -m1 "@<login>.*\[LinkedIn\](" # timeout: 6000 done | head -1 ``` Found → reuse that URL verbatim, done. Not found → omit. 5. No step produced a link → omit LinkedIn, same as today's no-match behavior. Format unchanged: `- **Name** (@github_handle, [LinkedIn](https://linkedin.com/in/handle)) — <brief what they did>`. Omit `@handle` when unresolvable. `<brief what they did>` picks up to 3 most significant contributions per line 440's ranking and describes the contribution itself (feature/fix/area) — never a PR number, issue link, or `(#N)` reference. ## Identify highlights Pick top 3–5 most significant changes from Classify. Ranking: breaking changes > new public API > major UX improvements > notable fixes. Pull concrete code example from explore-codebase diff for each. Drives Summary paragraph and Spotlights section. ## Draft migration guide Always produce. No breaking changes → single line "No breaking changes in this release." Deprecations/removals → show before→after code examples. State in preamble: API deprecated in prior release and now removed → ✗ Removed (not Breaking). If `migration_gaps` non-empty (from Validate migration docs): for each `[MISSING-MIGRATION]` item, add dedicated section covering that symbol with before→after example. For each `[SHALLOW]` item, expand existing coverage to add concrete upgrade instructions. If `breaking_callers` non-empty (from Breaking-change classification): for each Breaking symbol, add a before→after section citing its external call sites (the `migration_lines`) so downstream consumers see exactly which of their call sites must change. When that phase reported `query_complete:false`, prefix the section "Affected call sites (possibly-incomplete — codemap coverage partial)". ## Generate release demo **Only for feature releases** (≥1 🚀 Added items). Skip bug-fix-only releases. Self-contained Python script in jupytext percent (`# %%`) format. Full story: install → setup → demonstrate each highlight → verify output. ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # reload Shared-setup vars (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="" DEMO_OUT=".temp/release-demo-$BRANCH-$DATE.py" mkdir -p .temp # timeout: 5000 echo "${DEMO_OUT:-}" > "${TMPDIR:-/tmp}/release-demo-out-${CSID}" ``` Write demo to `$DEMO_OUT`. (`prepare` mode: `releases/$VERSION/demo.py` — see Phase 4.) **Gate: demo must execute to completion before proceeding to Draft executive summary.** <!-- branch: demo-approval — only in demo/prepare mode; isolated from notes/changelog path; ≤1 call --> Invoke `AskUserQuestion` — "Ready to run demo script `$DEMO_OUT`?" Options: (a) Run now · (b) Review first · (c) Skip and **exclude from release artifacts**. On option (c): mark demo excluded, skip to Draft executive summary — do NOT invoke the failure-path AskUserQuestion below. On (a) or (b) confirmed: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r DEMO_OUT < "${TMPDIR:-/tmp}/release-demo-out-${CSID}" 2>/dev/null || DEMO_OUT="" python "$DEMO_OUT" # timeout: 600000 DEMO_EXIT=$? echo "${DEMO_EXIT}" > "${TMPDIR:-/tmp}/release-demo-exit-${CSID}" ``` <!-- policy-sibling: plugins/cc_oss/skills/release/modes/prepare.md §Phase 4a execution gate (demo retry bound) --> **Guard**: only proceed to failure handling when `$DEMO_EXIT -ne 0` after attempting a fix. Success (`$DEMO_EXIT = 0`) → proceed directly to Draft executive summary — no AskUserQuestion. Failure → fix and re-run (max 3 iterations total). Only after 3 failed attempts invoke `AskUserQuestion` ("Demo still failing after 3 attempts. Exclude from release and continue, or abort?"). Self-contained: package installed in current env; no live API calls or network deps; deterministic synthetic data; `# !pip install` lines are Python comments — interpreter skips. ## Draft executive summary 1–2 paragraph executive summary: what release is, why it matters, who benefits. Based on Identify highlights. Save `.temp/output-release-summary-$BRANCH-$DATE.md`. ## Write release draft Pre-flight — verify all templates present before proceeding: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" # reload Shared-setup vars (Check 41) IFS= read -r SKILL_DIR < "${TMPDIR:-/tmp}/release-setup-${CSID}/SKILL_DIR" 2>/dev/null || SKILL_DIR="" 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 DO_APPEND < "${TMPDIR:-/tmp}/release-do-append-${CSID}" 2>/dev/null || DO_APPEND="false" [ -z "$SKILL_DIR" ] && echo "Error: could not locate release skill directory" && exit 1 for tmpl in release-draft.md audit-checks.md gather-prompt.md; do # timeout: 5000 [ -f "$SKILL_DIR/templates/$tmpl" ] || { echo "Missing template: $tmpl — aborting" exit 1 } done ``` Before writing, fetch last 2–3 releases to check formatting conventions: ```bash gh release list --limit 5 # timeout: 30000 LATEST_TAG=$(gh release list --limit 100 --json tagName --jq '[.[] | select(.tagName | test("rc|dev|alpha|beta"; "i") | not)] | .[0].tagName // empty') # timeout: 30000 [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ] && echo "No releases found — using template defaults" || gh release view "$LATEST_TAG" # timeout: 15000 ``` Existing releases deviate from templates → match tone and prose style only. **Never** use `# Changelog` structure for DRAFT.md — always use `release-draft.md` structure. `gh release list` empty → use template defaults. Fetch origin URL for full changelog link: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" ORIGIN_URL=$(git remote get-url origin 2>/dev/null || echo "") # timeout: 3000 # 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/release-draft.md" # timeout: 5000 ``` **DRAFT.md format guard**: must NOT start with `# Changelog`, must NOT use CHANGELOG section structure. CHANGELOG-format classification = internal working doc only — derive sections from it, don't copy verbatim. Template (loaded above). Replace `[org]/[repo]` with actual values from `$ORIGIN_URL`. Omit empty sections. Key difference from `prepare`: phases run inline (no subagent delegation); output to `DRAFT.md` and root `CHANGELOG.md`. ### Adversarial review ```bash 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/modes/adversarial-review.md" # timeout: 5000 ``` Follow above and execute. <!-- loads: modes/release-draft-template.md --> ```bash 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/modes/release-draft-template.md" # timeout: 5000 ``` Follow above and execute (format templates, semantic consistency review, polish, shepherd spawn, write to disk). ## Mode: prepare ```bash 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="" [ -f "$SKILL_DIR/modes/prepare.md" ] || { echo "Error: modes/prepare.md not found at $SKILL_DIR/modes/prepare.md — verify oss plugin installation"; exit 1; } cat "$SKILL_DIR/modes/prepare.md" # timeout: 5000 ``` Follow above and execute. > Confidence block — prepare mode: end response with `## Confidence` block per CLAUDE.md output standards after prepare.md completes. ## Mode: audit ```bash 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="" [ -f "$SKILL_DIR/modes/audit.md" ] || { echo "Error: modes/audit.md not found at $SKILL_DIR/modes/audit.md — verify oss plugin installation"; exit 1; } # refuse to audit already-published release IFS= read -r _REST_LINE < "${TMPDIR:-/tmp}/release-rest-${CSID}" 2>/dev/null || _REST_LINE=""; _AUDIT_VERSION=${_REST_LINE%% *} if [ -n "$_AUDIT_VERSION" ]; then if gh release view "$_AUDIT_VERSION" --json tagName --jq .tagName >/dev/null 2>&1; then # timeout: 15000 echo "! BLOCKED — $_AUDIT_VERSION is already a published release on GitHub. /release audit checks FORWARD readiness only." echo " For retrospective analysis use: /oss:analyse (requires oss plugin)" exit 1 fi fi cat "$SKILL_DIR/modes/audit.md" # timeout: 5000 ``` Follow above and execute. > Confidence block — audit mode: end response with `## Confidence` block per CLAUDE.md output standards after audit.md completes. ## Mode: demo ```bash 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="" [ -f "$SKILL_DIR/modes/demo.md" ] || { echo "Error: modes/demo.md not found at $SKILL_DIR/modes/demo.md — verify oss plugin installation"; exit 1; } cat "$SKILL_DIR/modes/demo.md" # timeout: 5000 ``` Follow above and execute. > Confidence block — demo mode: end response with `## Confidence` block per CLAUDE.md output standards after demo.md completes. </workflow> <notes> - **Doc artifacts ≠ released product**: CHANGELOG.md, DRAFT.md, MIGRATION.md, SUMMARY.md, demo.py = communication artifacts; released product published separately via `git tag`, `gh release create`, PyPI upload. - **AskUserQuestion usage**: spread across `notes`/`prepare`/`audit`/`demo` modes; each call in distinct branch-path (no single path has >4 sequential calls); compliant with sequential-call limit. - **Numbers reference**: numeric limits documented with rationale in `guidelines/numbers-reference.md`; update whenever limits change - Filter noise (CI config, dep bumps, typos) unless user-impacting - **Public-facing content policy**: user-visible changes only. Never include: internal staff names, internal maintenance, CI/tooling, internal dep bumps, housekeeping with no user impact. - **Contributor email privacy**: `.temp/` must be in `.gitignore` — emails from `git log --format="%aN <%aE>"` must not leak into repo. - Public-facing output co-authored with `oss:shepherd` (requires `oss` plugin) — follow shepherd voice protocol: ```bash export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}" IFS= read -r _OSS_SHARED < "${TMPDIR:-/tmp}/release-oss-shared-${CSID}" 2>/dev/null || _OSS_SHARED="" cat "$_OSS_SHARED/shepherd-voice.md" # timeout: 5000 ``` - **Demo mode output**: jupytext percent format — convert with `jupytext --to notebook <file>.py`; replace placeholder URLs before publishing; Colab badge URL must point to actual notebook after upload <!-- branch: demo-synthetic-fallback — only when real data unavailable; isolated deep in demo path --> - **Demo real-world-only policy**: use actual project data/fixtures/API — synthetic requires explicit user approval; fallback: (1) document each failed attempt in `## Demo attempts`, (2) ask Codex if available, (3) ask user via `AskUserQuestion`, (4) synthetic only on explicit approval - **Changelog audit preserves history**: add missing entries and flag unrelated or uncertain extras. Remove only the exact confirmed stale Unreleased claim for an unshipped revert/pivot; preserve historical entries and every unrelated item. - **`--append` marker**: `.temp/release-last-processed-<branch>` — not date-stamped like sibling `.temp/release-*` artifacts (must survive across days/sessions); losing it (TTL cleanup, gitignore) degrades safely to the existing full-range/full-overwrite behavior, never to corruption — see `bin/release_append_marker.py` docstring for the full rationale. - **Provenance store (patch-id keyed)**: `.temp/release-provenance-<branch>.json` — same cross-session lifecycle reasoning as the marker (per-branch, `.temp/`, gitignored, not date-stamped). Array of `{patch_id, sha, subject, artifact, anchor_text, written_at}` records, one per (contributing commit, artifact, exact-written-text) tuple; every `notes`-mode write (full regenerate or `--append` merge) appends a record for each newly-written bullet/entry that traces to specific commit(s) — see "Post-write bookkeeping" → "Provenance record" in `release-draft-template.md`. Consumed by Gather changes' cross-cycle revert detection: a genuine `git revert` commit's own `This reverts commit <sha>.` trailer is converted to its `git patch-id --stable` and looked up here — a hit means an exact, deterministic `{artifact, anchor_text}` strike, no text-matching guesswork. **`patch_id` is the only lookup key**: a raw commit sha changes on `--amend`, `rebase`, or `cherry-pick` even when the diff is untouched, so a sha-keyed store would silently miss a revert of a commit reworded or cherry-picked since it was recorded; `git patch-id --stable` is a normalized hash of the diff content and survives all three (verified empirically: identical patch-id across `--amend` and cherry-pick onto another branch, while the sha changed each time). `sha` and `subject` are carried for human debugging only, never used as a matching key. A commit whose diff produces no stable patch-id (a merge commit shown without `-m`, or a genuinely empty commit) is recorded with `patch_id: null` and can only ever be struck via the semantic path — a documented gap, not a bug. Recorded artifacts in practice: DRAFT.md (Notable-changes, Spotlights, Migration guide), `$CHANGELOG_FILE`, standalone `MIGRATION.md` — never Summary (DRAFT.md's own section or standalone `SUMMARY.md`, both additive-only prose with no removal path) or Contributors (per-person, not per-commit-revertible). Losing the store (TTL cleanup) degrades the revert path to the same semantic-grep fallback already used for pivots — never to corruption or a silently-missed strike (Semantic consistency review is still the backstop). - **`--append` integration scope**: covers every DRAFT.md section (Summary, Spotlights, Migration guide, Notable-changes subsections, Contributors), plus root-level `SUMMARY.md`/`MIGRATION.md` when their flags are set — all merged via Read + Edit tool (see `release-draft-template.md` "Append merge"), not a parsing script. Purely additive except a detected cross-cycle revert/pivot (Gather changes' `CROSS_CYCLE_MATCH`), which strikes the specific stale entry instead of leaving a contradicting pair. - **Post-merge re-validation gates only the merge path**: Truth check / Identify highlights / Validate migration docs / Validate docs re-run against the final merged DRAFT.md only when `$MARKER_VALID == true` (see release-draft-template.md). A full regenerate (no marker, or `prepare`) is already single-pass-valid — nothing accumulated from a prior cycle to re-check. - **Collapse guard**: `SUMMARY.md`/`MIGRATION.md` merges (whole-file artifacts, no section structure to sanity-check against) carry a mechanical byte-count trip-wire — content collapsing from substantial to near-empty during a merge cycle is refused and restored from the pre-merge Read, never silently written (see `release-draft-template.md` "Collapse guard"). DRAFT.md's own sections can legitimately empty down to a dropped header (all items struck, nothing added) — that's intended, not guarded against; the guard is scoped to the two headerless artifacts where a full-file wipe was the actual historical bug. - **Every `notes`-mode write refreshes the marker** — including plain (non-`--append`) runs. New side effect for users who've never used `--append`: a `.temp/release-last-processed-<branch>` file now appears. Harmless (gitignored, seeds a correct baseline for later `--append` adoption) and does not change `DRAFT.md`/`CHANGELOG.md` output — noted here so it's not a surprise. - **"### Since last draft" accumulates, never reconciles**: each `--append` cycle's Summary paragraph piles up under this subheading (DRAFT.md's `📋 Summary` section and standalone `SUMMARY.md` both) with no `remove` path across cycles — cosmetic drift after many cycles, not a correctness or data-loss issue. Reconciling into the main paragraph (or trimming) happens, if at all, at the next full regenerate (no marker / `prepare`). - Follow-up chains: - Readiness check → `/oss:release prepare <version>` runs built-in audit first; use standalone `/oss:release audit [version]` only for readiness check without cutting release - Breaking changes → `/oss:analyse` (requires `oss` plugin) for ecosystem impact - Notes/changelog written → `gh release create` must be user-run via project tooling - `migration` content written → add to project docs, link from CHANGELOG entry </notes> <calibration> Registered: `notes` mode — classification accuracy (change type, section assignment, noise filtering). Future candidates (not yet registered): `prepare` (pipeline completeness), `audit` (verdict accuracy: READY/NEEDS_ATTENTION/BLOCKED), `demo` (headline feature selection, narrative quality, code cell correctness). </calibration>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.