Skip to content
7 changes: 5 additions & 2 deletions .claude/skills/open-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,13 @@ A `/verify-*` gate only counts if the **diff's own flow** was driven —
rendering a proxy or asserting a 200 is not verification.

**Changelog gate:** every non-exempt PR adds `changelog.d/<slug>.md`
before the PR opens and touches `CHANGELOG.md` not at all — that shared
before the PR opens and adds nothing to `CHANGELOG.md` — that shared
spot is where sibling PRs conflict (rule + exemptions in CLAUDE.md,
format in `changelog.d/README.md`; the `/pull_request` command enforces
the same gate and appends the PR number after creation). Locally:
the same gate and appends the PR number after creation). Correcting the
wording of a bullet `[Unreleased]` already carries is allowed — identity
is the bold title, so only a new title is an added bullet — and a
fragment never ships the bare `(#NNNNN)` placeholder. Locally:
`uv run python -m tools.changelog check --base origin/main`.

Then the local CI equivalents — the same commands the pipeline runs.
Expand Down
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ A companion guide `CLAUDE.md` at the repo root carries the shared rules for Clau

- **Always write in English** - All output text (code comments, commit messages, PR descriptions, issue comments, documentation) must be in English, even if the user writes in another language.
- **Repository prose follows the Google developer documentation style guide** - `docs/`, `README.md`, `agentic/docs/`, changelog entries, and PR/issue text use [Google style](https://developers.google.com/style) (sentence-case headings, second person, numbered procedures); the concrete rules and the house-style exception (`docs/reference/style-guide.md` governs website/brand surfaces) live in the `write-docs` skill (`.claude/skills/write-docs/SKILL.md`). Existing docs migrate on touch, not via bulk rewrites.
- **Changelog and releases** - see [Changelog + releases](#changelog--releases) below: every PR adds a fragment under `changelog.d/` and never edits `CHANGELOG.md`, a release folds the fragments and bumps the version files via a PR, and the GitHub release is that section condensed, never copied. When reviewing a non-exempt PR that lacks a fragment — or that writes a bullet into `[Unreleased]` — flag it.
- **Changelog and releases** - see [Changelog + releases](#changelog--releases) below: every PR adds a fragment under `changelog.d/` and adds nothing to `CHANGELOG.md`, a release folds the fragments and bumps the version files via a PR, and the GitHub release is that section condensed, never copied. When reviewing a non-exempt PR that lacks a fragment — or that ADDS a bullet to `[Unreleased]` — flag it; correcting the wording of a bullet already there is allowed and must not be flagged.
- **Never echo secret values into transcripts or logs** - Verify secrets by exit code or metadata, never by printing them.
- **Structural fix over symptomatic fix** - When a cheap symptomatic fix and a correct structural fix compete, take the structural one: fix the cause, never mute the alarm. Never modify working code to make a broken test pass — fix the test or flag it.
- **Never manually merge a pipeline PR, and never bypass the pipeline** - Specifications and implementations go through the GitHub Actions workflows: `spec-create.yml` writes `plots/{spec-id}/specification.md` and `.yaml`, `impl-merge.yml` merges implementation PRs and creates their `metadata/*.yaml`, promotes the preview images to GCS and sets the `impl:{library}:done` label. Merging one by hand skips all of that and leaves `quality_score: null`, no review data, missing GCS images and the issue open. So: never write those files by hand, never run `gh pr merge` on a spec or implementation PR, and add the `approved` label to the **issue**, never to the PR. The full DON'T/DO table is in `CLAUDE.md`. Ordinary (non-pipeline) PRs are merged by the repository owner, not by an agent.

## Changelog + releases

- **Every PR adds `changelog.d/<slug>.md`, and NEVER a bullet in `CHANGELOG.md`** — the fragment is a slice of the changelog in the changelog's own format (`### Category` over bold-titled English bullets with PR refs; the rules and an example are in `changelog.d/README.md`). That shared `[Unreleased]` spot is where sibling PRs used to conflict each other — three times in one night on 2026-09-02/03 — and the CI job "Changelog (fragment)" refuses both a missing fragment and a bullet written into `[Unreleased]` directly. Same check locally: `uv run python -m tools.changelog check --base origin/main`. **Exempt:** catalogue-only PRs (everything under `plots/`), the automated plot pipeline (`github-actions[bot]`) and Dependabot — summarized in aggregate at release time (see `agentic/commands/release.md`) — plus any PR labelled `skip-changelog`. This rule is duplicated in `CLAUDE.md` and `agentic/commands/pull_request.md`; keep all three in sync when changing it.
- **Every PR adds `changelog.d/<slug>.md`, and NEVER a new bullet in `CHANGELOG.md`** — the fragment is a slice of the changelog in the changelog's own format (`### Category` over bold-titled English bullets with PR refs; the rules and an example are in `changelog.d/README.md`). That shared `[Unreleased]` spot is where sibling PRs used to conflict each other — three times in one night on 2026-09-02/03 — and the CI job "Changelog (fragment)" refuses both a missing fragment and a bullet ADDED to `[Unreleased]` directly — added, not merely different: a bullet is identified by its bold title, so correcting the wording of one already there passes. It also refuses a bare `(#NNNNN)` placeholder: leave the reference out and let `/pull_request` append the real number. Same check locally: `uv run python -m tools.changelog check --base origin/main`. **Exempt:** catalogue-only PRs (everything under `plots/`), the automated plot pipeline (`github-actions[bot]`) and Dependabot — summarized in aggregate at release time (see `agentic/commands/release.md`) — plus any PR labelled `skip-changelog`. This rule is duplicated in `CLAUDE.md` and `agentic/commands/pull_request.md`; keep all three in sync when changing it.
- **A release** runs `uv run python -m tools.changelog release X.Y.Z --title "<codename>"`, which folds every fragment under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`), bumps the version files (`pyproject.toml` `project.version`, `uv.lock`, `app/package.json`), repoints the compare links and deletes the fragments. The two aggregate lines for the exempt classes stay by hand (the italic *Catalog* line, the single **Dependencies:** bullet) because they summarize a window rather than a PR. All of it on a `release/vX.Y.Z` PR. Every bullet already carries its PR reference — `/pull_request` appends it when the PR opens. After the merge the tag goes on the merge commit and the GitHub release is created from the section.
- **A GitHub release is that section condensed, never copied:** an intro line (merge count, PR range, link to `CHANGELOG.md`); the section's own `### Added / Changed / Removed / Fixed` headings in the section's order (an empty one is omitted); one bullet per NOTABLE entry — chores, dependency bumps and small fixes are left out, no fixed count — each at most two lines: its bold title, one clause with the essence or the headline number, its PR reference; a compare link (`compare/vPREV...vNEW`) as the last line. Numbers are copied exactly; only PR numbers from the section are cited; the full text lives only in the CHANGELOG — the release page is the index into it. The cut procedure itself is `agentic/commands/release.md`.

Expand Down
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Dependabot bumps ignore this template — they are summarized at release time.
## Checklist

- [ ] `changelog.d/<slug>.md` added (Keep-a-Changelog categories, bold-titled
bullets, PR ref — `changelog.d/README.md`), and `CHANGELOG.md` left alone
bullets, no `(#NNNNN)` placeholder — `changelog.d/README.md`), and no new
bullet in `CHANGELOG.md` (correcting one already there is fine)
— required for every non-pipeline PR
- [ ] Related documentation updated (e.g. `docs/reference/`, `docs/workflows/`,
`docs/contributing.md`) if behavior changed
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ All notable changes to this project are documented here. The format is based on
rather than library SemVer: major for milestone releases, minor for feature batches, patch for
fix-only (see `agentic/commands/release.md`).

**Do not edit this file in a feature PR.** Every non-exempt PR adds one fragment,
**Do not add to this file in a feature PR.** Every non-exempt PR adds one fragment,
`changelog.d/<slug>.md`, in this file's own format, each bullet with its PR reference. The shared
`[Unreleased]` spot is where sibling PRs used to conflict each other, so the CI job
"Changelog (fragment)" refuses both a missing fragment and a bullet written in here directly
(format and exemptions: `changelog.d/README.md`). A release runs
"Changelog (fragment)" refuses both a missing fragment and a bullet added in here directly
(format and exemptions: `changelog.d/README.md`). Correcting an entry `[Unreleased]` already
carries is allowed — a bullet is identified by its bold title, so a re-worded body is a change,
and only a NEW title (or one more copy of a title already there) is an added bullet. A release runs
`uv run python -m tools.changelog release X.Y.Z --title "<codename>"`, which folds every fragment
under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`), bumps `pyproject.toml`
(`project.version`), `uv.lock` and `app/package.json`, repoints the compare links at the bottom of
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A companion guide `.github/copilot-instructions.md` carries the shared rules for
- **Always write in English** - All output text (code comments, commit messages, PR descriptions, issue comments, documentation) must be in English, even if the user writes in another language.
- **Repository prose follows the Google developer documentation style guide** - `docs/`, `README.md`, `agentic/docs/`, changelog entries, and PR/issue text use [Google style](https://developers.google.com/style) (sentence-case headings, second person, numbered procedures); the concrete rules and the house-style exception (`docs/reference/style-guide.md` governs website/brand surfaces) live in the `write-docs` skill. Existing docs migrate on touch, not via bulk rewrites.
- **Update documentation when making changes** - When adding new features, events, or modifying behavior, always check if related documentation needs updating (e.g., `docs/reference/plausible.md` for analytics events, `docs/workflows/` for workflow changes, `docs/contributing.md` for user-facing changes).
- **Changelog and releases** - see [Changelog + releases](#changelog--releases) below: every PR adds a fragment under `changelog.d/` and never edits `CHANGELOG.md`, a release folds the fragments and bumps the version files via a PR, and the GitHub release is that section condensed, never copied.
- **Changelog and releases** - see [Changelog + releases](#changelog--releases) below: every PR adds a fragment under `changelog.d/` and adds nothing to `CHANGELOG.md` (correcting the wording of a bullet already under `[Unreleased]` is allowed), a release folds the fragments and bumps the version files via a PR, and the GitHub release is that section condensed, never copied.
- **External-system writes need explicit, named authorization** - Merging or closing PRs/issues this session did not create, bulk merges, and label changes on others' PRs are blocked by the permission classifier unless the user named that action; a generic "ok, sounds good" authorizes nothing. The same discipline covers every prod-touching action in interactive sessions — Cloud SQL writes/DDL, GCS production-folder changes, Secret Manager access, Cloud Build config: name the exact action, resource, and id, and ask before acting (the automated `spec-*`/`impl-*` workflows write these by design and are exempt). For any change to `.claude/settings*.json`, use the built-in `/update-config` skill (a Claude Code harness skill, not a repo command) immediately — direct writes are blocked as self-modification and retrying variants just burns round trips.
- **Never echo secret values into the transcript** - Verify secrets by exit code or metadata, never by printing them; never create a Secret Manager version via `echo` (the trailing newline corrupts the value).
- **Snapshot before destructive prod operations** - Before anything that can overwrite or delete shared prod DB data or GCS production objects (bulk UPDATE/DELETE, a data-rewriting migration, bulk GCS overwrite/delete): take a timestamped backup first (`pg_dump` to a new directory outside the working tree, `gsutil cp` to a backup prefix), sanity-check it (row/object counts — a silent empty snapshot is worse than none because it looks like safety), and never write into, delete, or rename an existing snapshot.
Expand All @@ -27,7 +27,7 @@ A companion guide `.github/copilot-instructions.md` carries the shared rules for

## Changelog + releases

- **Every PR adds `changelog.d/<slug>.md`, and NEVER a bullet in `CHANGELOG.md`** — the fragment is a slice of the changelog in the changelog's own format (`### Category` over bold-titled English bullets with PR refs; the rules and an example are in `changelog.d/README.md`). That shared `[Unreleased]` spot is where sibling PRs used to conflict each other — three times in one night on 2026-09-02/03 — and the CI job "Changelog (fragment)" refuses both a missing fragment and a bullet written into `[Unreleased]` directly. Same check locally: `uv run python -m tools.changelog check --base origin/main`; `… preview` prints the pending section. **Exempt:** catalogue-only PRs (everything under `plots/`), the automated plot pipeline (`github-actions[bot]`: spec-create, impl-generate/review/repair/merge, spec auto-polish, daily-regen) and Dependabot — those are summarized in aggregate at release time (see `agentic/commands/release.md`) — plus any PR labelled `skip-changelog`. This rule is duplicated in `.github/copilot-instructions.md` and `agentic/commands/pull_request.md`; keep all three in sync when changing it.
- **Every PR adds `changelog.d/<slug>.md`, and NEVER a new bullet in `CHANGELOG.md`** — the fragment is a slice of the changelog in the changelog's own format (`### Category` over bold-titled English bullets with PR refs; the rules and an example are in `changelog.d/README.md`). That shared `[Unreleased]` spot is where sibling PRs used to conflict each other — three times in one night on 2026-09-02/03 — and the CI job "Changelog (fragment)" refuses both a missing fragment and a bullet ADDED to `[Unreleased]` directly — added, not merely different: a bullet is identified by its bold title, so correcting the wording of one already there passes. It also refuses a bare `(#NNNNN)` placeholder: leave the reference out and let `/pull_request` append the real number. Same check locally: `uv run python -m tools.changelog check --base origin/main`; `… preview` prints the pending section. **Exempt:** catalogue-only PRs (everything under `plots/`), the automated plot pipeline (`github-actions[bot]`: spec-create, impl-generate/review/repair/merge, spec auto-polish, daily-regen) and Dependabot — those are summarized in aggregate at release time (see `agentic/commands/release.md`) — plus any PR labelled `skip-changelog`. This rule is duplicated in `.github/copilot-instructions.md` and `agentic/commands/pull_request.md`; keep all three in sync when changing it.
- **A release** runs `uv run python -m tools.changelog release X.Y.Z --title "<codename>"`, which folds every fragment under a new version heading (`## [X.Y.Z] — YYYY-MM-DD — <codename>`), bumps the version files (`pyproject.toml` `project.version`, `uv.lock`, `app/package.json`), repoints the compare links and deletes the fragments. The two aggregate lines for the exempt classes stay by hand, because they summarize a window rather than a PR: the italic *Catalog* line and the single **Dependencies:** bullet. All of it on a `release/vX.Y.Z` PR. Every bullet already carries its PR reference — `/pull_request` appends it when the PR opens. After the merge the tag goes on the merge commit and the GitHub release is created from the section.
- **A GitHub release is that section condensed, never copied:** an intro line (merge count, PR range, link to `CHANGELOG.md`); the section's own `### Added / Changed / Removed / Fixed` headings in the section's order (an empty one is omitted); one bullet per NOTABLE entry — chores, dependency bumps and small fixes are left out, no fixed count — each at most two lines: its bold title, one clause with the essence or the headline number, its PR reference; a compare link (`compare/vPREV...vNEW`) as the last line. Numbers are copied exactly; only PR numbers from the section are cited; the full text lives only in the CHANGELOG — the release page is the index into it. The cut procedure itself is `agentic/commands/release.md`.

Expand Down
11 changes: 7 additions & 4 deletions agentic/commands/pull_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ branch state matches what the PR description claims.
1. Run `git diff origin/main...HEAD --stat` to see changed files summary
2. Run `git log origin/main..HEAD --oneline` to see commits in this branch
3. Run `git branch --show-current` to get the current branch name
4. **Changelog gate** — the branch carries `changelog.d/<slug>.md` and does NOT touch
4. **Changelog gate** — the branch carries `changelog.d/<slug>.md` and does NOT add bullets to
`CHANGELOG.md` (Keep-a-Changelog categories, English, bold-titled bullets — the format and an
example are in `changelog.d/README.md`); add and commit the fragment if missing, and run
example are in `changelog.d/README.md`); write the bullets WITHOUT a PR reference (step 7 adds
the real number — a bare `(#NNNNN)` placeholder is refused, and correcting the wording of a
bullet `[Unreleased]` already carries is allowed: identity is the bold title, not the full
text); add and commit the fragment if missing, and run
`uv run python -m tools.changelog check --base origin/main`, which is the same check the CI job
"Changelog (fragment)" makes. Exempt: catalogue-only PRs (everything under `plots/`), automated
pipeline PRs (`github-actions[bot]`: spec-create, impl-*, auto-polish, daily-regen) and
Expand All @@ -56,8 +59,8 @@ EOF
)"
```

7. If step 4 added changelog entries without a PR reference, append the new PR number to those
bullets (e.g. `(#1234)`) and push the follow-up commit.
7. Append the new PR number to the fragment's bullets (e.g. `(#1234)` — the real number, never
the `(#NNNNN)` placeholder, which the gate refuses) and push the follow-up commit.
8. **The Copilot review runs ONCE**, when the PR opens (or leaves draft) — review-on-push is off in
the "Automated Copilot Code Review" ruleset (owner, 2026-09-03), so step 7's follow-up commit and
every later push trigger nothing. Do not re-request a review per push; the rule and its one
Expand Down
Loading
Loading