From 9a5677aeae52d6f03927ab5673881e9442edcc7a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 10:17:00 +0000 Subject: [PATCH 1/2] docs(agents),ci: release notes are release-owned, scoped re-verify, merge_group triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three multi-agent throughput fixes from one PR's merge history (#4458 went three full green CI cycles without landing — main merged 18 PRs in the same 6 hours, and two of its three conflicts were rows racing into the same release-notes table): 1. `content/docs/releases/` is RELEASE-OWNED — never edited in a code PR. Release notes are compiled centrally at release time from changesets + the ADR-0087 registries; a per-PR appended row made `releases/v.mdx` the repo's hottest conflict magnet. Inlined in CLAUDE.md (the must-never-miss set), added to the AGENTS.md Documentation Guardrails table, and the spec-property-retirement skill's checklist item — the one instruction that explicitly sent agents into that file — now routes through the changeset instead. 2. AGENTS.md multi-agent §10 scopes the post-merge re-verify. The first pull-main-and-full-suite round stays. Subsequent merges done only because main moved during CI: rebuild + `check:generated` when spec moved on either side, assert the branch's delta vs main is still exactly the PR's intent, and reserve the full typecheck+test for semantic overlap or non-mechanical conflicts. CI validates the merge commit either way; a 15-minute full lap per merge is what turns a busy main into a livelock. 3. `merge_group:` triggers on the three required-check workflows (ci.yml, lint.yml, spec-liveness-check.yml), so the repo can turn on GitHub's merge queue — the race-free version of §10, run by the platform. ci.yml details: the paths filter has no merge_group support, so queue builds treat everything as changed (a skipped filter step's empty output falls back to 'true'), and the full-suite step runs on queue builds (the queue result IS the next main). §7 now names the queue as the sanctioned path once an admin enables it — the opposite of the auto-merge it bans, since the queue lands only speculatively-merged-and-green results. pr-automation and docs-drift-check are deliberately NOT queue-triggered (PR-context-bound; must not be marked required). Enabling the queue itself is a branch-protection setting only an admin can flip; this commit makes the workflows ready for it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL --- .../skills/spec-property-retirement/SKILL.md | 14 +++--- .github/workflows/ci.yml | 25 ++++++++--- .github/workflows/lint.yml | 4 ++ .github/workflows/spec-liveness-check.yml | 4 ++ AGENTS.md | 45 +++++++++++++++---- CLAUDE.md | 18 ++++++-- 6 files changed, 88 insertions(+), 22 deletions(-) diff --git a/.claude/skills/spec-property-retirement/SKILL.md b/.claude/skills/spec-property-retirement/SKILL.md index 8c79f9359f..39b2557646 100644 --- a/.claude/skills/spec-property-retirement/SKILL.md +++ b/.claude/skills/spec-property-retirement/SKILL.md @@ -244,11 +244,15 @@ Work top to bottom; each line has a gate behind it. `tsc` finds these for you on the tombstone route. - [ ] **Published skills** — `skills/*/SKILL.md` teaching the key (tables, `defineX` examples) — gated by `check:skill-examples` and `check:skill-refs`. -- [ ] **Docs** — `content/docs/**` prose, tables and code blocks. Grep the key, - then read the surrounding files: a removed key hides in a `defineFlow` - example three sections from the reference table. -- [ ] **Release notes** — the `### Dead spec clusters removed` table in - `content/docs/releases/v.mdx` **plus** the upgrade checklist. +- [ ] **Docs** — `content/docs/**` prose, tables and code blocks — **EXCEPT + `content/docs/releases/`, which a code PR must never touch** (AGENTS.md + Documentation Guardrails). Release notes are written centrally at release + time from the changesets + the D2/D3 registries; the per-PR row this list + used to require made `releases/v.mdx` the repo's hottest conflict + magnet. Your changeset (next item) is the input that reaches them. For + the rest of `content/docs/**`: grep the key, then read the surrounding + files — a removed key hides in a `defineFlow` example three sections from + the reference table. - [ ] **Changeset** — `major` for `@objectstack/spec`. AGENTS.md: a breaking changeset must carry the FROM → TO mapping and the one-line fix; it ships as `CHANGELOG.md` in the npm package and is what an upgrading agent greps diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e12a08969a..59843115de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,13 @@ on: pull_request: branches: - main + # Merge queue: the queue builds each PR as speculatively merged onto the + # current main and only lands it if this workflow is green on that result — + # the race-free version of the "pull main and re-verify before merging" + # discipline (AGENTS.md multi-agent §7/§10). Every workflow that produces a + # branch-protection-required check MUST carry this trigger, or queue builds + # wait forever on a check that never reports. + merge_group: # Superseded runs on the same PR/branch waste runners and delay feedback; # cancel them. Push runs to main group by commit ref as well, so an in-flight @@ -23,15 +30,21 @@ jobs: contents: read pull-requests: read outputs: - docs: ${{ steps.changes.outputs.docs }} - core: ${{ steps.changes.outputs.core }} - console: ${{ steps.changes.outputs.console }} + # On merge_group, everything counts as changed: dorny/paths-filter has no + # merge_group support, and the queue build is the last validation before + # main — the one place a skipped job can never be the right answer. A + # skipped step's output is the empty string (falsy), so `|| 'true'` + # supplies the merge-group value without touching PR/push behavior. + docs: ${{ steps.changes.outputs.docs || 'true' }} + core: ${{ steps.changes.outputs.core || 'true' }} + console: ${{ steps.changes.outputs.console || 'true' }} steps: - name: Checkout repository uses: actions/checkout@v7 - uses: dorny/paths-filter@v4 id: changes + if: github.event_name != 'merge_group' with: filters: | docs: @@ -168,9 +181,11 @@ jobs: # workflow (coverage-nightly.yml) — instrumentation added minutes to # every main push for a trend artifact that is consulted occasionally at # best. Dogfood is excluded for the same reason as the PR step: the - # Dogfood job runs it. + # Dogfood job runs it. Merge-queue builds take this full-run path too: + # the queue result IS the next main, so it gets main's validation, not + # the PR's affected-only subset. - name: Run all tests (push) - if: github.event_name == 'push' + if: github.event_name == 'push' || github.event_name == 'merge_group' env: NODE_OPTIONS: --report-on-signal --report-signal=SIGUSR2 --report-directory=${{ runner.temp }}/stall-reports run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 151af2c049..1a4a90394f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,6 +7,10 @@ on: pull_request: branches: - main + # Merge queue (see ci.yml for the full note): required checks must report on + # queue builds or the queue stalls. This workflow has no PR-only steps, so + # the trigger alone is enough. + merge_group: # Same policy as ci.yml: superseded runs on the same PR/branch waste runners # and delay feedback; cancel them. Push runs to main group by commit ref, so an diff --git a/.github/workflows/spec-liveness-check.yml b/.github/workflows/spec-liveness-check.yml index 997e7a4d32..5c7a5f8b2d 100644 --- a/.github/workflows/spec-liveness-check.yml +++ b/.github/workflows/spec-liveness-check.yml @@ -24,6 +24,10 @@ on: # Same for the strictness ledger — it is a doc, and editing it can break # the gate that now holds it to the code. - 'docs/audits/**' + # Merge queue (see ci.yml for the full note). merge_group has no `paths` + # support, so queue builds run this unconditionally — acceptable: the whole + # job is ~a minute, and the queue result is the next main. + merge_group: permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index 0b132ddf61..fe614af199 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,6 +146,16 @@ Even inside your own worktree, operate defensively: Auto-merge can land a still-red PR onto shared `main` and break it for every parallel agent (see #1475). Merge serially; rebase other open branches before merging the next one. + **Once the repo's merge queue is enabled, "add to queue" IS the sanctioned + path** — it is the opposite of the auto-merge this rule bans: the queue + builds your PR *as merged onto the current `main`* and lands it only if that + speculative result is green, which is exactly the §10 re-verification, done + by the platform, race-free. The manual serial protocol above is the fallback + for when the queue is unavailable. (Why this matters: `main` can land a PR + every few minutes at peak; a manual merge–reverify loop takes ~25 minutes, + so under load it *never* wins the race — one PR went three full green + cycles without managing to land. That is a livelock, not a discipline + failure.) 8. **Testing needs a server? Start your own temporary one — never stop someone else's.** A running dev server you didn't start probably belongs to another agent or the user; killing it (or its port) breaks their in-flight work. Spin @@ -174,15 +184,31 @@ Even inside your own worktree, operate defensively: None of this is CI-visible: CI checks out fresh and installs clean. It costs only *your* time, which is exactly why it is worth recognising in one step rather than re-diagnosing per gate. -10. **A clean merge is not a working merge.** Git conflicts on overlapping lines; - nothing warns you when two changes are individually fine and jointly wrong. - Real examples from one branch's lifetime: a test asserting a response body's - exact shape landed while that shape was being changed elsewhere (merged clean, - failed CI); a domain file was deleted while another agent's guard still - declared it. **Before opening a PR, and again before merging, pull `main` and - re-run the suite** — the second CI round is where these surface, and the guards - in `scripts/check-*.mjs` exist largely because this class of breakage is - invisible to `git merge`. +10. **A clean merge is not a working merge — but scope the re-check to the + overlap.** Git conflicts on overlapping lines; nothing warns you when two + changes are individually fine and jointly wrong. Real examples from one + branch's lifetime: a test asserting a response body's exact shape landed + while that shape was being changed elsewhere (merged clean, failed CI); a + domain file was deleted while another agent's guard still declared it. + **Before opening a PR, pull `main`, refresh build state (§9), and run the + full suite once.** For the *subsequent* pre-merge merges of `main` — the + ones you do only because `main` moved again while CI ran — the full suite is + usually re-proving what three identical runs already proved, at ~15 minutes + per lap while `main` lands a PR every few. Scope it instead: + - **Always:** rebuild what the merge touched, and if `packages/spec` moved + on either side, `pnpm --filter @objectstack/spec build && pnpm --filter + @objectstack/spec check:generated` — generated snapshots (`api-surface`, + baselines) are the classic jointly-wrong artifact, and only a rebuild of + the merged source can validate them (never trust git's textual merge of a + generated file). Then assert your branch's *delta vs `main`* is still + exactly what your PR intends (e.g. "N removed / 0 added"). + - **Full `pnpm typecheck && pnpm test` again only when** the incoming + commits touch the same packages or the same behavior your diff does, or a + conflict occurred outside trivially-mechanical files. + - CI on the PR (and the merge queue, once enabled) validates the merge + commit itself — that second CI round is where joint breakage surfaces, and + the guards in `scripts/check-*.mjs` exist largely because this class of + breakage is invisible to `git merge`. --- @@ -255,6 +281,7 @@ Root also exports: `defineStack`, `composeStacks`, `defineView`, `defineApp`, `d | Path | Type | Rule | |:---|:---|:---| | `content/docs/references/` | **AUTO-GEN** | ❌ Never hand-edit. Regenerated by `packages/spec/scripts/build-docs.ts`. | +| `content/docs/releases/` | **RELEASE-OWNED** | ❌ Never edit in a code PR. Release notes are written **centrally at release time**, compiled from changesets + the ADR-0087 registries — not accreted a row per PR. Per-PR appends made `releases/v.mdx` the repo's hottest conflict magnet (three PRs raced the same table inside one afternoon), and every manual resolution risks dropping someone else's row. Your PR's input is its **changeset**; for spec removals also the D2/D3 registry entries. Factual error on a releases page → dedicated docs-only PR or an issue, never a rider on code changes. | | `**/translations/*.generated.ts` (nine packages — `platform-objects`, five plugins, three services) | **AUTO-GEN** | ❌ Never hand-edit the file *structure*. Run `node scripts/check-i18n-bundles.mjs --write` to regenerate all nine (merge mode — every existing translation is preserved); `pnpm i18n:extract` still covers `platform-objects` alone. Translation *values* are hand-written and expected to be: the gate compares against a merge-mode extract, so editing a string is fine, while adding or dropping keys is drift. `pnpm check:i18n` gates all nine in CI, and `pnpm check:i18n-coverage` ratchets untranslated declared labels. | | `content/docs/guides/` | hand-written | ✅ Update `meta.json` when adding pages. | | `content/docs/concepts/` | hand-written | ✅ | diff --git a/CLAUDE.md b/CLAUDE.md index 3e0603ef5c..060059c78f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,9 @@ # CLAUDE.md **[AGENTS.md](./AGENTS.md) is the source of truth for working in this repo — read it.** -Its Prime Directives are binding. Do not rely on this file alone; the two rules that must -never be missed are inlined here because missing either one wastes or corrupts other -agents' work. +Its Prime Directives are binding. Do not rely on this file alone; the three rules that +must never be missed are inlined here because missing any one of them wastes or corrupts +other agents' work. ## ⛔ Claim the issue before you write any code @@ -34,5 +34,17 @@ Then make all edits there. This applies **per repo**: if a task spans `framework file's own repo (so sibling repos are covered). Deliberate non-task exception: `OS_ALLOW_MAIN_EDITS=1`. Follow the rule because it's correct, not because the hook fires. +## ⛔ Never edit `content/docs/releases/` in a code PR + +Release notes are written **centrally, at release time** — not accreted one PR at a +time. Every code/feature/retirement PR appending its own row to the current +`releases/v.mdx` turns that file into the single hottest merge-conflict magnet in +the repo (with ~18 merges to `main` in a working day, the same table conflicts over and +over, and each resolution risks dropping someone else's row). Your PR's inputs to the +release notes are the **changeset** (`.changeset/*.md` — one file per change, never +conflicts) and, for spec removals, the ADR-0087 registries; the release process compiles +them. If you believe a releases page has a factual error, file an issue or make it a +dedicated docs-only PR — never a rider on code changes. + See **AGENTS.md** for the full playbook: branch hygiene, the dev stack, PR flow, and the rest of the Prime Directives. From 89688d0a9a3d7fdb759e99606825e19f409d231e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 10:22:17 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20empty-frontmatter=20changeset=20?= =?UTF-8?q?=E2=80=94=20this=20PR=20releases=20nothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Check Changeset gate requires each PR to declare its release impact; the sanctioned "releases nothing" declaration is an empty-frontmatter changeset (per the gate's own inline doc), which a docs+workflow-only PR is exactly the case for. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL --- .changeset/agents-releases-freeze-merge-queue.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/agents-releases-freeze-merge-queue.md diff --git a/.changeset/agents-releases-freeze-merge-queue.md b/.changeset/agents-releases-freeze-merge-queue.md new file mode 100644 index 0000000000..dd5b115156 --- /dev/null +++ b/.changeset/agents-releases-freeze-merge-queue.md @@ -0,0 +1,8 @@ +--- +--- + +Releases nothing — repo process + CI only. `content/docs/releases/` becomes +RELEASE-OWNED (never edited in code PRs; compiled centrally from changesets + +the ADR-0087 registries), AGENTS.md multi-agent §10 scopes the post-merge +re-verify, and the three required-check workflows gain `merge_group:` triggers +so the merge queue can be enabled. No package ships from this change.