diff --git a/.agents/skills/iterate-pr/SKILL.md b/.agents/skills/iterate-pr/SKILL.md index 7ed9b354..519815f4 100644 --- a/.agents/skills/iterate-pr/SKILL.md +++ b/.agents/skills/iterate-pr/SKILL.md @@ -252,6 +252,14 @@ a stacked PR targets `main` eventually and the filter matches that eventual target, so this workflow runs on mid-stack PRs as well. Expect to see the check on every PR in a stack and decide from stack position, not from the `on:` block. +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: diff --git a/.github/workflows/pr-check-openspec.yml b/.github/workflows/pr-check-openspec.yml index 3a81b214..e19a357b 100644 --- a/.github/workflows/pr-check-openspec.yml +++ b/.github/workflows/pr-check-openspec.yml @@ -6,8 +6,26 @@ name: PR OpenSpec Archive Check # tip of a stack (or a standalone PR) and is skipped on PRs that still have work # stacked on top of them. Tip = no other OPEN PR targets this PR's head branch # as its base. +# +# Drafts are skipped. 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 that has not been built yet — leaving the PR red for as long +# as the proposal is open, which is how teams learn to ignore red. A draft +# cannot merge, so the guarantee that no unarchived change reaches `main` is +# unaffected: the check runs when the PR is marked ready for review. on: pull_request: + # Both draft-transition events are REQUIRED, and neither is in the default + # set (opened/synchronize/reopened), because the archive job's condition + # depends on draft state: + # ready_for_review — without it a draft could be marked ready and merged + # on a stale green that was never re-evaluated. This + # one protects the guarantee. + # converted_to_draft — without it a PR that failed while ready keeps that + # failure after being converted back to draft, until + # some unrelated push happens to re-run it. + types: + [opened, synchronize, reopened, ready_for_review, converted_to_draft] permissions: contents: read @@ -53,7 +71,12 @@ jobs: check-openspec-archived: name: "stack: openspec-archived" needs: stack-position - if: needs.stack-position.outputs.is_tip == 'true' + # Skipped on drafts. A proposal-only PR is its own tip — nothing is stacked + # on it yet — so the gate would demand it archive a change whose + # implementation has not been written, and it would sit red for as long as + # the proposal is open. A draft cannot merge, so nothing can reach `main` + # unarchived; the check runs the moment it is marked ready for review. + if: needs.stack-position.outputs.is_tip == 'true' && github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions/checkout@v6