From 46c9cc5fa6579676765aca619260acdddd007d5f Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 3 Aug 2026 16:53:02 -0700 Subject: [PATCH 1/2] fix(ci): skip the OpenSpec archive gate on draft PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A spec-only proposal is its own tip until its implementation is stacked on top, so the gate demanded it archive a change nobody had built yet. #70, #71, and #72 have failed this check on every run since July 28 for exactly that reason — weeks of red on PRs that were never merge-eligible, which is how a team learns to stop reading red. A draft cannot merge, so skipping it costs nothing: no unarchived change can reach `main` either way. `ready_for_review` is added to the trigger types because the default set (opened/synchronize/reopened) does not include it — without that, a draft could be marked ready and merged on a green that was never re-evaluated. That event is what preserves the guarantee. The tip rule is unchanged for PRs that are ready for review. --- .agents/skills/iterate-pr/SKILL.md | 8 ++++++++ .github/workflows/pr-check-openspec.yml | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) 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..f860b512 100644 --- a/.github/workflows/pr-check-openspec.yml +++ b/.github/workflows/pr-check-openspec.yml @@ -6,8 +6,20 @@ 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: + # `ready_for_review` is REQUIRED here, and is not in the default set + # (opened/synchronize/reopened). The archive job is skipped while a PR is a + # draft, so without this event a draft could be marked ready and merged on a + # stale green that was never re-evaluated. + types: [opened, synchronize, reopened, ready_for_review] permissions: contents: read @@ -53,7 +65,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 From 18c0d812d0334f25df6007db57f0ac277bd27a25 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 3 Aug 2026 22:43:10 -0700 Subject: [PATCH 2/2] fix(ci): re-run the archive gate when a PR is converted back to draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The archive job's condition depends on draft state, so both draft transitions have to be trigger events. `ready_for_review` was already there because it protects the guarantee. `converted_to_draft` is the other half: without it a PR that failed the gate while ready keeps that failure after being converted back to draft, until some unrelated push re-runs it — a stale red on a PR that is no longer merge-eligible, which is the state this workflow change exists to remove. Found in review by Copilot on #84. --- .github/workflows/pr-check-openspec.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-check-openspec.yml b/.github/workflows/pr-check-openspec.yml index f860b512..e19a357b 100644 --- a/.github/workflows/pr-check-openspec.yml +++ b/.github/workflows/pr-check-openspec.yml @@ -15,11 +15,17 @@ name: PR OpenSpec Archive Check # unaffected: the check runs when the PR is marked ready for review. on: pull_request: - # `ready_for_review` is REQUIRED here, and is not in the default set - # (opened/synchronize/reopened). The archive job is skipped while a PR is a - # draft, so without this event a draft could be marked ready and merged on a - # stale green that was never re-evaluated. - types: [opened, synchronize, reopened, ready_for_review] + # 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