Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 27 additions & 66 deletions .agents/skills/iterate-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,72 +238,33 @@ Run `${CLAUDE_SKILL_ROOT}/scripts/fetch_pr_checks.py` to get structured failure

**Wait if pending:** If review bot checks (sentry, warden, cursor, bugbot, seer, codeql) are still running, wait before proceeding—they post actionable feedback that must be evaluated. Informational bots (codecov) are not worth waiting for.

#### Stacked PRs: ignore the OpenSpec Archive Check unless this is the last PR in the chain

The `PR OpenSpec Archive Check` (workflow `pr-check-openspec.yml`) fails whenever
any unarchived directory exists under `openspec/changes/`. A change is archived
exactly once, at the END of the work — so a PR that still carries an in-progress
change directory will fail this check. Archiving on an intermediate PR is wrong:
it would remove the change docs before the implementation PRs above it merge.

This workflow carries **no `branches:` filter** — that is why it runs on every
PR in a stack, and it is the reliable way to get that behavior. Expect to see
the check on every PR in a stack and decide from stack position, not from the
`on:` block.

Do not generalize from workflows that DO filter on `branches: [main]`. GitHub
sometimes resolves a stacked PR's eventual target and matches on that, so such a
workflow may appear on mid-stack PRs — but it stops without warning (see
"two other failures that are structural" below). A filter-less trigger is the
only dependable way to run everywhere.

The archive job is also skipped while a PR is a **draft**. A spec-only proposal
is its own tip until its implementation is stacked on top, so the gate would
otherwise demand it archive a change nobody has built yet, and it would sit red
for as long as the proposal is open. A draft cannot merge, and the check runs on
`ready_for_review`, so nothing unarchived can reach `main` — if a proposal PR is
red on this check, mark it ready only when its implementation is stacked
beneath it.

When the archive check does run and fail, decide ONE thing before treating it as
actionable: **is this PR the last in the chain (the tip)?** A PR is the tip when
no other OPEN PR targets its head branch as a base:

```bash
HEAD=$(gh pr view --json headRefName --jq '.headRefName')
gh pr list --state open --base "$HEAD" --json number
```

An empty list → nothing is stacked on top → this PR is the tip.

Then:

- **Not the tip** (some open PR is stacked on this one) → IGNORE the
`PR OpenSpec Archive Check` failure. Do NOT archive the change on this PR.
Treat the check as expected-red and do not let it block the iterate loop
(still address every other failing check and all feedback normally).
- **The tip** (nothing stacked on top — including an ordinary standalone PR) →
the change MUST be archived before merge. Archive it via the OpenSpec archive
flow, which moves `openspec/changes/<name>/` to the dated archive directory
`openspec/changes/archive/YYYY-MM-DD-<name>/` (do not invent a different
location), then commit and push so the check goes green.

This rule applies ONLY to the OpenSpec Archive Check. Every other check is
handled normally regardless of stack position.

#### Stacked PRs: two other failures that are structural, not regressions

**`Require a changeset` on the bottom PR.** The check looks for a
`.changeset/*.md` added in that PR's own diff, so the bottom PR — the one
targeting `main` — is the only place a changeset can satisfy it. If it fails
there, move the changeset DOWN to the bottom branch rather than labelling
anything `skip-changeset`; every branch above inherits it, since a child
contains its ancestors' commits. Extend that one file as later PRs land; never
add a second changeset per PR.

Mid-stack PRs bypass the check on their base ref. If you see one failing it,
look at that guard rather than reaching for the label, which would wrongly
record the change as shipping no release note.
#### No PR check asks whether the OpenSpec change is archived

A change is archived exactly once, at the END of the work, so an unarchived
directory under `openspec/changes/` is the normal state of a pull request. There
is no PR-time gate for it — the earlier one had to infer stack position to avoid
firing on in-flight work, and a check that is expected-red on most of a stack
teaches people to ignore red.

The archive signal lives on `main` instead: a step in `validate.yml` runs on
push events only and fails while `main` carries an unarchived change directory.
If you see it red on `main`, the fix is to archive the change via the OpenSpec
archive flow — which moves `openspec/changes/<name>/` to
`openspec/changes/archive/YYYY-MM-DD-<name>/` — or to land the stack that is
still holding it open.

Practically, on a PR: archive when the PR is the last in the chain, and leave
the change directory alone otherwise. Nothing will fail either way.

#### Stacked PRs: two other check behaviours worth knowing

**`Changeset` warns, it does not fail.** It looks for a `.changeset/*.md` added
or modified anywhere between `main` and this PR's head, which covers every
branch below it in the stack. A warning means no changeset exists anywhere in
the stack. If the change ships user-visible behaviour, add one on the BOTTOM
branch rather than labelling anything `skip-changeset`; every branch above
inherits it, since a child contains its ancestors' commits. Extend that one file
as later PRs land; never add a second changeset per PR.

**`on: pull_request: branches: [main]` tells you nothing dependable about where
a workflow runs.** GitHub sometimes resolves a stacked PR's eventual target and
Expand Down
96 changes: 96 additions & 0 deletions .github/workflows/changeset.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Changeset

# Advisory, never blocking. A pull request that ships a release note should add
# a `.changeset/<name>.md`; this job says so when one is missing and gets out of
# the way otherwise.
#
# It used to fail, and the failure did not survive contact with stacked pull
# requests. One change gets ONE changeset, which lives on the bottom branch, so
# the file is *added* in exactly one PR's own diff and inherited by every branch
# above it. A per-PR gate therefore had to reason about stack position to know
# whether an absent changeset was a real omission — via a base-ref guard that
# GitHub's stacked-PR handling made unreliable in both directions, and a
# `skip-changeset` label that got applied to silence the check rather than to
# record "this ships no release note." What was left was a red check people had
# learned to route around, which is worse than no check.
#
# So the question it asks is now the one that has a dependable answer: does a
# changeset exist ANYWHERE in this branch's stack — the full diff against
# `main`, which includes every ancestor branch's commits? If yes, the change is
# covered no matter which PR carries the file. If no, warn, and leave the call
# to the author.

on:
# No `branches:` filter: the check is advisory, so it should reach every PR in
# a stack, and a filter is not a dependable way to scope one anyway. It runs
# everywhere and answers the stack question itself, below. `ready_for_review`
# is not in the default event set and must be named.
pull_request:
types: [opened, reopened, synchronize, ready_for_review]

permissions:
contents: read

jobs:
changeset:
name: Changeset
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # need `origin/main` to diff the whole stack against

- name: Look for a changeset in this stack
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_REF: ${{ github.head_ref }}
LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
run: |
set -euo pipefail

# The changesets "Version Packages" PR consumes changesets — removing
# them is its whole job — so it legitimately has none.
if [ "$HEAD_REF" = "changeset-release/main" ]; then
echo "Version Packages PR (changeset-release/main) — no changeset expected."
exit 0
fi

# Kept only to keep deliberate no-release-note PRs (docs, CI, chores,
# the vale-binaries bot) quiet. It suppresses a warning now, not a
# failure, so it can no longer be used to force a merge through.
case ",${LABELS}," in
*,skip-changeset,*)
echo "The 'skip-changeset' label is present — no release note expected."
exit 0
;;
esac

# Three dots: merge-base of `main` and this head, so the range is
# everything the stack has added, including the ancestor branches
# below this PR. A changeset on the bottom branch counts for every PR
# above it, which is the arrangement this repo asks for.
#
# Additions AND modifications, because one change gets one changeset:
# a stack landing forward finds its release note already on `main` and
# the right move is to extend that file, not add a second entry for
# something that ships once.
FOUND=$(git diff --name-only --diff-filter=AM "origin/main...$HEAD_SHA" -- '.changeset/*.md' \
| grep -viE '/README\.md$' || true)

if [ -n "$FOUND" ]; then
echo "Changeset(s) present in this stack:"
echo "$FOUND" | sed 's/^/ - /'
exit 0
fi

echo "::warning::No changeset found anywhere in this stack. If this pull request changes published behaviour, run \`pnpm changeset\` on the bottom branch of the stack. If it ships no release note (docs / CI / chore), ignore this or apply the \`skip-changeset\` label."
{
echo "### No changeset in this stack"
echo ""
echo "Nothing under \`.changeset/\` was added or modified between \`main\` and this branch, including the branches below it."
echo ""
echo "- Ships user-visible behaviour → run \`pnpm changeset\` on the **bottom** branch of the stack, so every branch above inherits it."
echo "- Ships no release note (docs, CI, chore) → nothing to do. Apply \`skip-changeset\` to silence this."
echo ""
echo "_This is a warning. It does not block the merge._"
} >> "$GITHUB_STEP_SUMMARY"
72 changes: 0 additions & 72 deletions .github/workflows/ci.yml

This file was deleted.

114 changes: 0 additions & 114 deletions .github/workflows/openspec-rot.yml

This file was deleted.

Loading
Loading