Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b126ac9
Add release-manager agent and verify-release skill
jeffhandley Aug 4, 2026
3f0e9e2
Require a fresh, upstream-synced worktree before preparing a release
jeffhandley Aug 5, 2026
13a1ebf
Review release notes content with the user before opening the release PR
jeffhandley Aug 5, 2026
1f6b141
Teach ApiCompat baseline-transition suppression auditing
jeffhandley Aug 5, 2026
894f6ff
Actively monitor the release PR after it is opened
jeffhandley Aug 5, 2026
cbafdd1
Watch for release publication instead of waiting to be told
jeffhandley Aug 5, 2026
77556c1
Measure release time instead of inferring it
jeffhandley Aug 5, 2026
eafb282
Show interaction time per stage in the wrap-up table
jeffhandley Aug 5, 2026
4e08074
Let the tilde carry the estimate on its own
jeffhandley Aug 5, 2026
d262c02
Separate "took no time" from "we did not measure it"
jeffhandley Aug 5, 2026
a3d2c69
Pin the draft release to the commit the user actually approved
jeffhandley Aug 5, 2026
d5fea95
Audit an abandoned preparation instead of trusting or discarding it
jeffhandley Aug 5, 2026
891ddd0
Say that a trailing validation baseline is correct, not stale
jeffhandley Aug 5, 2026
2fb63bf
Close four ways the release process could report a confident wrong an…
jeffhandley Aug 5, 2026
89b143f
Say the anti-pattern plainly instead of quoting it
jeffhandley Aug 5, 2026
5e5a1b7
Describe the review artifacts instead of populating them with real pe…
jeffhandley Aug 5, 2026
9cc808d
Fix relative link depth in the release-manager references
jeffhandley Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions .github/agents/release-manager.agent.md

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions .github/agents/release-manager/references/delegation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Delegation and Worktrees

The release-manager session is an **orchestrator**. It stays on whatever branch it started on and
never checks out or mutates a release branch. Work that creates commits happens in a **child session
on its own worktree**, based on the target release branch.

This mirrors how [`docs.yml`](../../../workflows/docs.yml) already works: the orchestration scripts run
from a single fixed checkout, while each version's content is built from its own tag in a separate
worktree.

## Why

- **Current orchestration.** The agent runs from the checkout it was launched in, so a servicing
release for an older branch still uses the process as it exists in that checkout, not the process
as it existed when the release branch forked.
- **A clean working tree.** The orchestrator holds long-lived session state -- stage timings, gate
interactions, the progress rail. Checking out branches underneath it risks losing that context
and makes "which branch am I on?" a source of error at exactly the moment precision matters.
- **Isolation of the risky part.** Only stage 1 writes to the repository. Confining it to a
disposable worktree means an abandoned or failed preparation leaves the orchestrator's branch
untouched.
- **Concurrency.** A `2.0.0-preview.2` preparation and a `1.3.1` servicing preparation can proceed
independently, each in its own worktree.

## What runs where

| Stage | Mutates the repo? | Runs where |
|---|---|---|
| 1. Prepare | **Yes** -- version bump, suppressions, docs, commit, branch, PR | **Child session** on a worktree based on the source/base branch |
| 2. Review and merge | No -- reads CI and PR state | Orchestrator, in place |
| 3. Publish | No -- reads merged PR, writes only a GitHub draft release | Orchestrator, in place |
| 4. Release | No -- human action in the GitHub UI | Orchestrator, in place |
| 5. Verify | No -- reads workflow runs and published artifacts | Orchestrator, in place |

Stage 3 does edit `src/PACKAGE.md` and `README.md` when the README checklist finds issues. **The
release branch is already merged by this point, so those fixes cannot land on it.** They go to the
base branch the release ships from — `main` or `release/{MAJOR}.x` — which is protected, so they
need their own small PR, reviewed and merged like any other change.

Delegate that PR the same way as stage 1: a child session on a fresh worktree based on the base
branch. Do not push directly to the base branch, and do not commit into the orchestrator's worktree.

A corrective commit merged at this point **is not in the draft release's tag**, because the draft is
pinned to the merge commit the user approved. After the fix merges, re-target the draft to the new
head and regenerate the notes per
[publish-release Step 9](../../../skills/publish-release/SKILL.md). Skipping the re-target ships a
tag that predates the fix while the notes describe the fixed state.

## Confirm the orchestrator's location

Before starting any stage, note the branch this session started on and confirm the working tree is
clean. Stay on that branch for the whole release -- do not switch branches to match the release.

- **Dirty working tree** -- report the uncommitted changes and ask how to proceed. Do not stash,
reset, or commit unrelated work.
- **Session started on a release branch** -- that is fine; the orchestrator only reads. Still
delegate stage 1 to a worktree rather than committing in place.

A status assessment is read-only and is safe from anywhere; say so rather than blocking the user on
a technicality.

## Delegating stage 1

Create the child session with the **source/base branch** selected in prepare-release Step 1 as its
base -- `main` or `release/{MAJOR}.x`. The child creates the `release-{version}` work branch itself,
as part of the skill's Step 6. Do not create that branch yourself, and do not pass it as the base.

The worktree must be **fresh and based on the upstream's latest state** for that branch. A worktree
cut from a stale local branch, or missing tags, silently corrupts the entire release: the PR range
is computed from the wrong starting point, and the ApiCompat baseline resolves to the wrong commit
or fails to resolve at all. Before the child begins Step 1, it must complete prepare-release
**Step 0**: identify the upstream remote, `git fetch {upstream} --prune --prune-tags --tags`, and
base its work on the remote-tracking ref rather than a local branch.

Reuse of an existing worktree is the common way this goes wrong. Prefer creating a new one per
release. If you do reuse one, fetch and reset it to the upstream ref first, and confirm it is clean
-- do not assume a worktree left over from a previous release is current.

The child's kickoff prompt must carry everything it needs, because it does not share your context:

1. The instruction to run the **prepare-release** skill, **starting at Step 0**.
2. The source/base branch, already selected.
3. The target commit or ref, if the user chose one.
4. Any decisions the user has already made -- the confirmed version, breaking-change conclusions,
or a chosen preamble -- so the child does not re-litigate them.
5. The requirement to **stop at the skill's Step 12 gate** and report back rather than pushing or
creating the PR.
6. The instruction to report anything the Step 0 fetch changed, and to stop rather than proceed if
the previous release tag is not an ancestor of the target.
7. The requirement to **stop at the skill's Step 10b gate** and bring the categorization table and
acknowledgements roster back to you, so the user reviews notes content before a PR exists.

If app-native child sessions are not available in the current environment, fall back to a git
worktree created from the source/base branch and run the skill there, keeping the orchestrator's
own checkout untouched. The invariant is the worktree, not the mechanism.

## Recording the child

The moment you dispatch a child, write its identity into `release_session` -- `child_session_id`,
`child_worktree_path`, and `child_branch`. A release routinely outlives the session that started
it, and a worktree with no recorded owner is very hard to tell apart from the dozens of unrelated
worktrees a busy repository accumulates.

## Recovering an interrupted preparation

A child can stop anywhere: it fails, the user closes it, or the orchestrator session ends while the
child is mid-flight. Recovery starts from what the worktree actually contains, never from the fact
that it exists.

**Existence is not progress.** A `release-{version}` worktree proves only that a preparation was
started. Read its state before deciding anything:

| Evidence in the child's worktree | Where the preparation stopped |
|---|---|
| No `release-{version}` branch | Before Step 6; nothing to salvage |
| Branch exists, working tree dirty, no commit | Mid-preparation, somewhere in Steps 6-11 |
| Branch has a commit, nothing pushed | At the Step 12 gate, prepared and awaiting approval |
| Branch pushed, no PR | Interrupted inside Step 13 |
| PR open | Step 13 finished; this is stage 2, not stage 1 |

Then apply three rules:

- **Never reset or recreate a branch that has a commit on it.** It may hold work the user already
reviewed and corrected -- release-note categorization, acknowledgement edits, a chosen preamble --
none of which is reproducible from the repository. Read the commit and the drafted notes and
continue from there.
- **Never inherit a validation result.** Build, pack, and ApiCompat outcomes leave no trace in git.
A commit proves the files were written, not that anything passed. Re-run the checks rather than
assuming the interrupted run got that far.
- **Prefer resuming the recorded child over launching a replacement.** It still holds the context.
If it is gone, dispatch a replacement pointed at the *existing* worktree and branch, and tell it
to audit what is already there before continuing -- not to start over.

Report the stopping point and the evidence you read, and let the user confirm before continuing.

Decisions the user made at a gate are the hardest thing to recover, because session tracking does
not survive the session. Their durable form is the artifact itself: the drafted release notes carry
the categorization, and the acknowledgements roster carries the exclusions. On resume, re-derive the
decisions by reading the drafted notes, and present them as *previously decided* for confirmation.
Silently re-deriving them from scratch will quietly undo corrections the user already made once.

## Gates stay with the orchestrator

The human gates belong to the orchestrator session. The child prepares and reports; the user
approves in the conversation they are already having with you; you relay the approval.

Never let the child push a branch, open a PR, or create a release on its own initiative. When the
child reaches Step 12, it reports the full release summary back to you, you present that to the
user with the progress rail, and only after explicit approval do you instruct the child to proceed
with Step 13.

## Timing across sessions

Session tracking stays in the **orchestrator**. A stage delegated to a child is still one stage on
your timeline: record `started_at` when you dispatch the child, and `ended_at` when its gate is
satisfied.

Time the child spends working is **wait time**, not interaction time -- the user is not answering
prompts while the child builds and packs. Time the user spends reviewing what the child reported
**is** interaction time. See [session-tracking.md](session-tracking.md).

## Cleaning up

When a release is complete, offer to remove the worktrees created for it. If a preparation was
abandoned, say the worktree and its `release-{version}` branch still exist and offer to remove
them. Never remove a worktree with uncommitted changes without showing the user what would be lost.
170 changes: 170 additions & 0 deletions .github/agents/release-manager/references/monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Monitoring

Two things in this process are easy to hand off passively and should not be: the release PR after
it is opened, and the draft release after it is created. In both cases the agent has the context
needed to interpret what happens next, and the user should not have to come back and report an
outcome the agent could have observed.

Monitoring is **automatic and read-only**. It never merges, never pushes, and never publishes.
Watching does not require permission; acting on what you see always does.

## Monitoring the release PR

Opening the release PR ends stage 1 and immediately begins stage 2, which owns the watch: it runs
until every check reaches a terminal state. Reporting the PR URL and stopping leaves the user to
discover failures themselves, which is exactly backwards.

Record the time accordingly. Stage 1 ends when the PR is created, and the CI watch that follows --
including any red checks, corrective pushes, and re-runs -- belongs to stage 2. Attributing that
time to stage 1 makes preparation look expensive and review look cheap, which is the opposite of
what the summary should reveal.

### When to start a watch

Start, or restart, monitoring:

- Immediately after the release PR is created (prepare-release Step 13).
- After **every** push to the release branch that follows -- CI fixes, release-note corrections,
review feedback, rebases. Each push produces a new head SHA with its own set of runs.
- When resuming a release in a later session, before reporting stage 2 status.

A restart is a fresh watch against the **new head SHA**. Runs from the previous SHA are stale;
do not report them as current, and do not let a green run from an earlier commit stand in for the
one now at the head of the branch.

### Running the watch

1. Resolve the current head SHA of the release branch.
2. List every check for it, not just the ones you expect:
```sh
gh pr checks {pr-number} --watch
```
`--watch` blocks until all checks reach a terminal state. Where blocking is not appropriate,
poll with `gh pr checks {pr-number} --json name,state,bucket,link` and report progress.
3. Wait for **terminal** completion. A check that is queued, in progress, or pending is not a
result. Do not summarize a partially-complete run as passing.
4. Confirm the run set is complete. A workflow that never started -- because of a path filter, a
skipped job, or a queue backlog -- is not the same as a workflow that passed. Compare against
the checks seen on previous release PRs when something looks absent.

### Reporting

Report a compact per-check table plus a single overall verdict:

| Check | Result |
|---|---|
| Build / build (ubuntu-latest, net10.0) | ✅ |
| Pack / APICompat | ❌ |
| CodeQL / csharp | ✅ |
| markdown-link-check | ✅ |

**Verdict: blocked** -- Pack / APICompat failed.

Use three states and name them explicitly: **green**, **running**, **blocked**. "Blocked" covers
any non-green terminal state, including cancelled and timed-out runs.

### On failure

Diagnose before proposing anything. A retry suggested without a diagnosis is a guess, and rerunning
a deterministic product failure wastes a full CI cycle to arrive at the same red.

1. **Retrieve the logs automatically.** Do not ask the user to paste them.
```sh
gh run view {run-id} --log-failed
```
2. **Classify the failure**, because the two classes call for opposite responses:

| Class | Signals | Response |
|---|---|---|
| **Product / API validation** | ApiCompat or package validation errors, compile errors, assertion failures, behavior differences | Real. Diagnose it. Never rerun to make it go away |
| **Infrastructure / tooling** | Runner allocation, network or feed timeouts, artifact upload, rate limits, cancelled by concurrency | A rerun is reasonable, once, with the reason stated |

Flaky tests sit between the two. Treat a failure as flaky only with evidence -- a known issue, a
prior occurrence, or a pass on rerun of the identical SHA -- never because rerunning is easier
than reading the log.

3. **For ApiCompat and package validation failures specifically**, apply the interpretation rules in
[apicompat-apidiff.md](../../../skills/prepare-release/references/apicompat-apidiff.md) before
concluding the release is breaking. `Unnecessary suppressions found` and a stale baseline
produce large, convincing, and entirely phantom break listings.

4. **Present the diagnosis with a proposed fix, and stop.** Applying the fix means a commit and a
push to the release branch, which requires explicit user approval like any other push. Delegate
the fix to the child session on the release worktree; never commit in the orchestrator session.

5. After an approved fix is pushed, **restart the watch** for the new SHA without being asked.

### Stage 2 handoff

Stage 2 stays **blocked** until the checks are green, or until the user explicitly decides to
proceed anyway. Record that decision and who made it.

When handing off, lead with CI status rather than only inviting review:

> **CI: green** -- all {n} checks passed on `{sha}`. PR #{number} is ready for your review and merge.

or

> **CI: blocked** -- {check name} failed on `{sha}`. Diagnosis below. PR #{number} is not ready
> to merge yet.

or

> **CI: running** -- {done} of {n} checks complete, none failed. I am still watching and will report when
> they finish.

Never say only "the PR is up, please review and merge." Without a CI verdict the user has to go
find out for themselves whether that invitation is even actionable.

## Monitoring the draft release

Creating the draft release ends stage 3. Stage 4 is a human action in the GitHub UI, and the
temptation is to hand off and wait to be told it happened. Do not. Publishing is the moment the
release becomes irreversible and the moment two workflows start, so it is the least useful point in
the process to be uninformed about.

Watch the release until it is no longer a draft:

```sh
gh release view v{version} --json isDraft,publishedAt,tagName,isPrerelease
```

Poll at a modest interval. This gate is human-paced and may sit for hours or span a session, so
prefer periodic checks over a tight loop, and say that you are watching rather than going silent.

**`isDraft: false` is the trigger.** The moment it flips:

1. Record the stage 4 end time from `publishedAt`, not from when you noticed. The user published
when they published; polling latency is yours, not theirs, and it should not inflate the stage
duration in the closing summary.
2. Confirm the details that were the user's to choose and cannot be inferred: the tag actually
created, and whether the release was marked as a prerelease. A stable release mistakenly left
unflagged, or a prerelease flagged as stable, changes what consumers receive.
3. **Begin stage 5 immediately** via the verify-release skill. Publishing starts the Release and
Publish Docs workflows in parallel right away; waiting to be told to verify means arriving after
the interesting part. Announce the transition rather than asking permission -- stage 5 is
read-only, and the irreversible act has already occurred.

### What else the watch can find

Not every change to the draft means it was published, and the difference matters:

| Observation | Meaning | Response |
|---|---|---|
| `isDraft: false` | Published | Start stage 5 |
| Still a draft, body changed | The user is editing the notes, possibly removing the AI disclosure | Nothing. Do not re-add anything they removed |
| Draft no longer exists | Deleted, or published under a different tag | Check for a published release before assuming it was abandoned; ask |
| Published with an unexpected tag | The tag differs from the prepared version | Stop and confirm before verifying. Verifying the wrong version is worse than not verifying |

If the user says they published but the API still reports a draft, trust the API and say so plainly
-- an unsaved draft or a failed publish looks identical to success from the browser.

### Stage 4 handoff

Hand off with the action and the watch, so the user knows they do not need to come back and report:

> The draft release for **v2.1.0** is ready. Review the notes line by line, set the prerelease flag
> if applicable, and click **Publish release**. Once you have signed off you may remove the AI
> disclosure from the notes.
>
> I am watching for publication and will start verification automatically when it happens.
Loading