diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index afb2398..c3c07f8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,9 @@ -# Mandatory review on anything that runs on user machines at install time. +# Mandatory review on anything that runs on user machines at install time, and +# on producer ops that determine what ships. # See: master plan risk row "Shell scripts in producer repo execute on user machines". -scripts/** @brettdavies -.github/workflows/** @brettdavies -.github/CODEOWNERS @brettdavies +bundle/scripts/** @brettdavies +scripts/** @brettdavies +.github/workflows/** @brettdavies +.github/rulesets/** @brettdavies +.github/CODEOWNERS @brettdavies diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..90919fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: A check produces wrong results, a script fails, or a doc says one thing while the code does another. +title: "bug: " +labels: bug +--- + +## What happened + + + +## What you expected + + + +## How to reproduce + +1. 1. 1. + +```bash +# exact commands; redact paths/credentials +``` + +## Environment + +- Bundle version (`cat bundle/VERSION` if cloned, or the tag you installed): vX.Y.Z +- Host (Claude Code / Cursor / Codex / other): +- Target tool the checker was run against (if applicable): owner/repo @ commit +- OS and shell: +- `bundle/scripts/check-compliance.sh --principle N` output (if relevant): + +```text + +``` + +## Why this is a bundle bug, not a target-tool bug + + diff --git a/.github/ISSUE_TEMPLATE/principle_proposal.md b/.github/ISSUE_TEMPLATE/principle_proposal.md new file mode 100644 index 0000000..7f233a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/principle_proposal.md @@ -0,0 +1,62 @@ +--- +name: Principle proposal +about: Propose a new principle, a substantive change to an existing principle, a new check, or a new template. +title: "proposal: " +labels: proposal +--- + +## Problem statement + + + +## Proposal + + + +## Type of change + +- [ ] New principle (P8 or later) +- [ ] Tightening an existing principle's MUST/SHOULD/MAY semantics +- [ ] Removing or relaxing an existing principle (major version bump) +- [ ] New automated check under `bundle/scripts/checks/` +- [ ] New template under `bundle/templates/` +- [ ] Other (describe) + +## Prior art + + + +- - + +## Draft of the change + +### `bundle/SKILL.md` diff sketch + +```diff + +``` + +### `bundle/references/principles-deep-dive.md` diff sketch + +```diff + +``` + +### Check / template additions + + + +## Backward compatibility + +- [ ] Backward-compatible (existing tools that pass today still pass after this change) +- [ ] Tightens — some currently-passing tools would start failing or warning. List representative examples: +- [ ] Breaking — explicit major version bump justified because: + +## Open questions + + + +- diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9d917c7..fdead33 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -90,6 +90,8 @@ **Created:** +**Renamed:** + **Deleted:** ## Key Features diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 125b397..a0876fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ on: - dev - "feat/**" - "fix/**" + - "chore/**" + - "release/**" pull_request: branches: - main @@ -39,11 +41,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Run ShellCheck + - name: Run ShellCheck (bundle scripts — ship to consumers) uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 env: # style severity surfaces all suggestions; CI fails on error+warning by default SHELLCHECK_OPTS: "--severity=style" + with: + scandir: ./bundle/scripts + additional_files: bundle/scripts/checks/_helpers.sh + - name: Run ShellCheck (producer scripts) + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 + env: + SHELLCHECK_OPTS: "--severity=style" with: scandir: ./scripts - additional_files: scripts/checks/_helpers.sh diff --git a/AGENTS.md b/AGENTS.md index fd3f49c..3a2b7a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,74 +2,82 @@ Project-level agent instructions for `agentnative-skill` — the producer repo for the `agent-native-cli` skill bundle. -This repo is **not** a Rust CLI tool. It is a content + script bundle that ships at the repo root and is consumed via -`git clone` into a host's skills directory (Claude Code, Cursor, Codex, etc.). The bundle defines the standard for -agent-native CLI design and ships an automated compliance checker that consumers run against their own tools. +This repo is **not** a Rust CLI tool. It is a content + script bundle plus producer-side ops scaffolding. The bundle +defines the standard for agent-native CLI design and ships an automated compliance checker that consumers run against +their own tools. ## Layout -| Path | Purpose | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `SKILL.md` | The standard itself: 7 agent-readiness principles, when to trigger, how to use. | -| `checklists/` | Task-shaped checklists for downstream consumers (e.g., `new-tool.md`). | -| `references/` | Deep-dive references: principle specifications, framework idioms, project structure, Rust/clap patterns. | -| `scripts/check-compliance.sh` | Driver script that runs all 24 compliance checks against a target Rust CLI repo. | -| `scripts/checks/` | Individual check scripts (`check-p1-*.sh`, `check-p4-*.sh`, etc.) plus shared `_helpers.sh`. | -| `templates/` | Drop-in starting points for downstream tools (`AGENTS.md`, clap main, error types, output format). | -| `.github/rulesets/` | Version-controlled GitHub repository rulesets (applied post-public-flip — see `.github/rulesets/README.md`). | -| `.github/workflows/` | CI: markdownlint, shellcheck. Plus `guard-main-docs.yml` to keep engineering docs off `main`. | -| `docs/plans/` | Engineering plans (`dev`-only — guarded out of `main`). | +The repo is split into **the bundle** (what consumers install) and **producer-side ops** (governance, CI, plans). +Consumers only see the bundle. + +| Path | Ships to consumers? | Purpose | +| -------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| `bundle/SKILL.md` | ✓ | The standard itself: 7 agent-readiness principles, when to trigger, how to use. | +| `bundle/checklists/` | ✓ | Task-shaped checklists for downstream consumers (e.g., `new-tool.md`). | +| `bundle/references/` | ✓ | Deep-dive references: principle specifications, framework idioms, project structure, Rust/clap patterns. | +| `bundle/scripts/check-compliance.sh` | ✓ | Driver script that runs all 24 compliance checks against a target Rust CLI repo. | +| `bundle/scripts/checks/` | ✓ | Individual check scripts (`check-p1-*.sh`, `check-p4-*.sh`, etc.) plus shared `_helpers.sh`. | +| `bundle/templates/` | ✓ | Drop-in starting points for downstream tools (`agents-md-template.md`, clap main, error types, output format). | +| `AGENTS.md`, `RELEASES.md`, `CONTRIBUTING.md` | — | Producer-repo docs. Not part of the skill. | +| `.github/rulesets/` | — | Version-controlled GitHub repository rulesets (applied post-public-flip — see `.github/rulesets/README.md`). | +| `.github/workflows/` | — | CI: markdownlint, shellcheck. Plus `guard-main-docs.yml` to keep engineering docs off `main`. | +| `.github/ISSUE_TEMPLATE/` | — | Bug report + principle proposal templates. | +| `docs/plans/` | — | Engineering plans (`dev`-only — guarded out of `main`). | +| `.markdownlint-cli2.yaml`, `.shellcheckrc`, `.gitattributes`, `.gitignore` | — | Local lint configs and repo metadata. | ## Lint & Format ```bash markdownlint-cli2 '**/*.md' '!node_modules/**' -shellcheck --severity=style scripts/check-compliance.sh scripts/checks/*.sh +shellcheck --severity=style bundle/scripts/check-compliance.sh bundle/scripts/checks/*.sh actionlint .github/workflows/*.yml ``` The repo ships a local `.markdownlint-cli2.yaml` (canonical 120-char line length) and `.shellcheckrc` (three narrow disables documented inline) so CI and local tooling agree. -## Branch model +## Branch + release model -`feat/* → dev (squash) → main (squash)`. `dev` and `main` are both forever; release branches are not used. +`feat/* → dev (squash) → release/ from origin/main → main (squash)`. Cherry-pick the non-docs commits from `dev` +onto the `release/*` branch. `dev` and `main` are both forever branches; `release/*` branches are short-lived and +auto-deleted on merge. Engineering docs (`docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, `docs/reviews/`) live on `dev` only. -`guard-main-docs.yml` blocks them from reaching `main`. +`guard-main-docs.yml` blocks any `added` or `modified` files under those paths from reaching `main`. The release-branch +cherry-pick pattern handles this naturally: docs commits stay on `dev`, only feature commits go onto `release/*`. -See [`RELEASES.md`](./RELEASES.md) for the full workflow and version-bump procedure. - -## Releases - -Tags `v*` on `main`. Tag protection (deletion + force-push + update blocked) is in `.github/rulesets/protect-tags.json`; -applied post-public-flip. The site at [`anc.dev/install`](https://anc.dev/install) pins to the commit SHA of the latest -tag. +See [`RELEASES.md`](./RELEASES.md) for the full workflow, version-bump procedure, and the verified status-check context +table. ## What an agent should NEVER do -- Edit shell scripts in `scripts/checks/` casually. They run on user machines at install time. CODEOWNERS gates - `scripts/**` and `.github/workflows/**` for that reason. -- Commit anything in `docs/plans/` or similar engineering docs to a branch heading toward `main`. Use `dev`. -- Modify `SKILL.md`'s `name` or `description` frontmatter without coordinating with consumers — those fields drive skill - discovery on every host. +- Edit shell scripts in `bundle/scripts/checks/` casually. They run on user machines at install time. CODEOWNERS gates + `bundle/scripts/**` and `.github/workflows/**` for that reason. +- Commit anything under `docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, or `docs/reviews/` directly to a + `release/*` branch — those paths are filtered by the cherry-pick pattern. Add to `dev` instead. +- Modify `bundle/SKILL.md`'s `name` or `description` frontmatter without coordinating with consumers — those fields + drive skill discovery on every host. - Re-tag a published version. Tags are immutable historical anchors that the install endpoints pin to. - Add Rust/Cargo scaffolding. There is no Rust code in this repo and there should be none — the standard is language-prescriptive but the bundle itself is shell + markdown. +- Move producer-ops files into `bundle/`. The split exists deliberately so consumers don't pull repo-management + artifacts into their skills directories. ## Common pitfalls -- The bundle's `templates/agents-md-template.md` is for downstream Rust CLI tools (`cargo build`, `cargo test`, etc.). - This `AGENTS.md` describes the producer repo and is intentionally different. +- The bundle's `bundle/templates/agents-md-template.md` is for downstream Rust CLI tools (`cargo build`, `cargo test`, + etc.). This top-level `AGENTS.md` describes the producer repo and is intentionally different. - `markdownlint-cli2` does NOT consult a global config — every repo needs its own `.markdownlint-cli2.yaml`. If line wrapping looks wrong, the local copy has drifted from `~/.markdownlint-cli2.yaml`. -- The `_helpers.sh` file in `scripts/checks/` is sourced (`source _helpers.sh`), not executed. It is intentionally not - marked +x. +- The `_helpers.sh` file in `bundle/scripts/checks/` is sourced (`source _helpers.sh`), not executed. It is + intentionally not marked +x. ## References -- [`SKILL.md`](./SKILL.md) — the standard -- [`README.md`](./README.md) — what this repo is, install pointer +- [`bundle/SKILL.md`](./bundle/SKILL.md) — the standard +- [`README.md`](./README.md) — what this repo is, repo layout, install pointer - [`SECURITY.md`](./SECURITY.md) — vulnerability disclosure - [`RELEASES.md`](./RELEASES.md) — release procedure +- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — how to propose changes - [`.github/rulesets/README.md`](./.github/rulesets/README.md) — branch + tag protection apply procedure diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..915cd48 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing to `agentnative-skill` + +Thanks for your interest. This repo defines a north-star standard for agent-native CLI tools and ships an automated +compliance checker. Substantive proposals should engage with the principles directly. + +## Where to start + +| You want to… | Read first | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Understand the standard | [`bundle/SKILL.md`](./bundle/SKILL.md) and [`bundle/references/principles-deep-dive.md`](./bundle/references/principles-deep-dive.md) | +| File a bug in the compliance checker or a check | Open an issue with the **Bug report** template | +| Propose a new principle, check, or template | Open an issue with the **Principle proposal** template | +| Add or fix something concrete | Read [`RELEASES.md`](./RELEASES.md) for the branch model, then open a PR | +| Operate as an agent in this repo | [`AGENTS.md`](./AGENTS.md) (lint commands, hard rules, common pitfalls) | + +## Branch model (TL;DR) + +```text +feat/* → PR to dev (squash merge) + → cherry-pick non-docs commits to release/ + → PR release/* to main (squash merge) + → tag v + GitHub Release +``` + +`dev` is the integration branch. `main` is what consumers install. Engineering docs (`docs/plans/`, `docs/solutions/`, +`docs/brainstorms/`, `docs/reviews/`) live on `dev` only and are blocked from `main` by `guard-main-docs.yml`. Full +procedure in [`RELEASES.md`](./RELEASES.md). + +## Pull requests + +- **Title format**: [Conventional Commits](https://www.conventionalcommits.org/) — `type(scope): description`. +- **Body**: follow [`.github/pull_request_template.md`](.github/pull_request_template.md). The `## Changelog` section is + the source of truth for `CHANGELOG.md` entries — write for users, not implementers. +- **Scope**: keep PRs small and single-purpose where possible. A bundle change, a check tightening, and a docs refactor + should be three PRs, not one. +- **Tests**: there is no test runner in this repo, but every PR must pass `markdownlint`, `shellcheck`, and (when + targeting `main`) `guard-docs / check-forbidden-docs`. Run locally before pushing: + + ```bash + markdownlint-cli2 '**/*.md' '!node_modules/**' + shellcheck --severity=style bundle/scripts/check-compliance.sh bundle/scripts/checks/*.sh + actionlint .github/workflows/*.yml + ``` + +## Bundle vs producer-ops boundary + +The repository is split into: + +- **`bundle/`** — what consumers install via `anc.dev/install`. SKILL.md, checklists, references, scripts, templates. +- **Everything else** — producer-side ops: governance (`AGENTS.md`, `RELEASES.md`, `CONTRIBUTING.md`, `SECURITY.md`), CI + (`.github/workflows/`), rulesets (`.github/rulesets/`), engineering plans (`docs/plans/`). + +Do not move producer-ops files into `bundle/`. Do not move bundle content out of `bundle/`. The split is what keeps +consumer skill directories clean. + +## Substantive changes to the standard + +The 7 principles are stable. Proposing a new principle, removing one, or materially changing existing semantics +requires: + +1. An issue using the **Principle proposal** template, including the problem statement, prior art, and a draft of how + `bundle/SKILL.md` and `bundle/references/principles-deep-dive.md` would change. +2. Discussion in the issue. Maintainer signoff before any PR. +3. A PR that updates SKILL.md, the deep-dive, the relevant `bundle/scripts/checks/check-p*-*.sh` scripts, the templates, + and the `CHANGELOG.md` together. Partial coverage isn't merged. + +Tightening a check's pass criteria is a `Changed` (minor or major depending on how many existing tools regress). Adding +a new check is `Added`. Removing a check is `Removed` (major). See SemVer guidance in `RELEASES.md`. + +## Security + +See [`SECURITY.md`](./SECURITY.md). Do not file security issues in the public tracker — use the GitHub private security +advisories channel. + +## License + +By contributing, you agree your contributions are licensed under the MIT license that covers this repository (see +[`LICENSE`](./LICENSE)). diff --git a/README.md b/README.md index 6bfb16c..fc957b9 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,78 @@ # agentnative-skill -The producer repo for the [`agent-native-cli`](./SKILL.md) skill bundle — a north-star standard for CLI tools designed -to be operated by AI agents. - -This repo ships the bundle at the root so that `git clone` directly into a host's skills directory IS install. +The producer repo for the [`agent-native-cli`](./bundle/SKILL.md) skill bundle — a north-star standard for CLI tools +designed to be operated by AI agents. + +## Repository layout + +```text +agentnative-skill/ +├── bundle/ ← THE SKILL — what consumers install +│ ├── SKILL.md skill metadata + the standard itself +│ ├── checklists/ task-shaped checklists (new-tool.md) +│ ├── references/ deep-dive specs, framework idioms, project structure +│ ├── scripts/ compliance checker + 24 individual checks +│ └── templates/ drop-in starter files (AGENTS, clap-main, error-types, output-format) +├── docs/plans/ engineering plans (dev-only — guarded out of main) +├── .github/ workflows, rulesets, issue templates, PR template +├── AGENTS.md project-level agent instructions FOR THIS REPO (not the bundle) +├── CONTRIBUTING.md how to propose changes +├── RELEASES.md release procedure (cherry-pick from dev → release/* → main) +├── SECURITY.md vulnerability disclosure +├── CHANGELOG.md released versions +├── VERSION single-line current version +├── LICENSE MIT +└── README.md this file +``` + +The skill bundle is **`bundle/`**. Everything outside `bundle/` is producer-side ops and **does not ship to consumers**. ## Install -See [anc.dev/install](https://anc.dev/install) for the cloned-in-place install model and supported hosts (Claude Code, -Cursor, Codex, etc.). +See [anc.dev/install](https://anc.dev/install) for the supported hosts (Claude Code, Cursor, Codex, etc.) and the exact +install commands. + +The install fetches `bundle/` (only) at a tagged commit SHA into the host's skills directory — for example +`~/.claude/skills/agent-native-cli/`. The installed layout looks like: + +```text +~/.claude/skills/agent-native-cli/ +├── SKILL.md +├── checklists/ +├── references/ +├── scripts/ +└── templates/ +``` + +The host then auto-discovers `SKILL.md` at the root of the skill directory. -## What's inside +## Bundle contents -- [`SKILL.md`](./SKILL.md) — the standard itself: 7 agent-readiness principles, when to trigger, how to use. -- [`checklists/`](./checklists/) — task-shaped checklists (e.g., starting a new tool). -- [`references/`](./references/) — deep-dive references: principle specifications, framework idioms, project structure, - Rust/clap patterns. -- [`scripts/`](./scripts/) — automated compliance checker (`check-compliance.sh`) plus 24 individual checks across 9 - groups under `scripts/checks/`. -- [`templates/`](./templates/) — drop-in starting points (`AGENTS.md`, clap main, error types, output format). +- [`bundle/SKILL.md`](./bundle/SKILL.md) — the standard itself: 7 agent-readiness principles, when to trigger, how to + use. +- [`bundle/checklists/`](./bundle/checklists/) — task-shaped checklists (e.g., starting a new tool). +- [`bundle/references/`](./bundle/references/) — deep-dive references: principle specifications, framework idioms, + project structure, Rust/clap patterns. +- [`bundle/scripts/`](./bundle/scripts/) — automated compliance checker (`check-compliance.sh`) plus 24 individual + checks across 9 groups under `bundle/scripts/checks/`. +- [`bundle/templates/`](./bundle/templates/) — drop-in starting points (`agents-md-template.md`, clap main, error types, + output format). The principles are also published as a stable web reference at [anc.dev/p1](https://anc.dev/p1) through `/p7`. ## Versioning Tagged releases follow [SemVer](https://semver.org/). The current version lives in [`VERSION`](./VERSION); release notes -are in [`CHANGELOG.md`](./CHANGELOG.md). +are in [`CHANGELOG.md`](./CHANGELOG.md). Each tag has a corresponding GitHub Release with the same notes. ## Contributing -Issues and PRs welcome. The bundle's content is the authoritative source of truth for what "agent-native CLI" means in -this ecosystem — substantive proposals should engage with the principles in [`SKILL.md`](./SKILL.md) and -[`references/principles-deep-dive.md`](./references/principles-deep-dive.md). +Issues and PRs welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). The bundle's content is the authoritative source of +truth for what "agent-native CLI" means in this ecosystem; substantive proposals should engage with the principles in +[`bundle/SKILL.md`](./bundle/SKILL.md) and +[`bundle/references/principles-deep-dive.md`](./bundle/references/principles-deep-dive.md). -Branch model: `feat/*` off `dev`, squash-merged back to `dev`; `dev` → `main` PRs cut releases. +Branch + release model documented in [`RELEASES.md`](./RELEASES.md). ## Security diff --git a/RELEASES.md b/RELEASES.md index db75c32..9d649d7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,34 +5,28 @@ PR number in its squash commit message, which keeps the history scannable, attri ```text feature branch (feat/*, fix/*, chore/*, docs/*) → PR to dev (squash merge) - → PR dev → main (squash merge) - → tag v* on main → site pins to commit SHA + → cherry-pick non-docs commits to release/ + → PR release/* to main (squash merge) + → tag v* on main → GitHub Release → site re-pins to commit SHA ``` -This is the **lightweight** variant of the brettdavies release pattern. It omits `release/*` cherry-pick branches -because: - -1. The repo is content + scripts, not compiled artifacts. There is no crates.io publish, no Homebrew dispatch, no - cross-platform build to gate. -2. The release scope is small enough that "everything currently on dev" is almost always what we want to ship. -3. `guard-main-docs.yml` keeps engineering docs (`docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, `docs/reviews/`) - off `main` mechanically — we don't need a cherry-pick step to filter them out. - -If those assumptions stop holding (e.g., we start shipping a binary, or we need to hold back specific dev commits from a -release), upgrade to the full `release/*` cherry-pick pattern from the canonical -[`~/.claude/skills/github-repo-setup/references/RELEASES.md`](../../.claude/skills/github-repo-setup/references/RELEASES.md). +This is the canonical brettdavies release pattern with `release/*` cherry-pick branches. Plans live on `dev` forever and +`guard-main-docs.yml` blocks any `added` or `modified` engineering-doc files in PRs targeting `main`. The release-branch +cherry-pick handles this cleanly: docs commits stay on `dev`, only feature/fix/chore commits go onto `release/*`. ## Branches -| Branch | Role | Lifetime | Protection | -| -------------------------------------- | ------------------------------------------------------- | -------------------------------------- | ------------------------------------ | -| `main` | Released bundle. Only commits ready to ship. | Forever. | `.github/rulesets/protect-main.json` | -| `dev` | Integration. All feature PRs land here. Default branch. | Forever. Never delete. | `.github/rulesets/protect-dev.json` | -| `feat/*`, `fix/*`, `chore/*`, `docs/*` | Feature work. | One PR's worth. Auto-deleted on merge. | None — squash into `dev` freely. | +| Branch | Role | Lifetime | Protection | +| -------------------------------------- | ------------------------------------------------------- | ------------------------------------------- | ------------------------------------ | +| `main` | Released bundle. Only release-merged commits. | Forever. | `.github/rulesets/protect-main.json` | +| `dev` | Integration. All feature PRs land here. Default branch. | Forever. Never delete. | `.github/rulesets/protect-dev.json` | +| `feat/*`, `fix/*`, `chore/*`, `docs/*` | Feature work. | One PR's worth. Auto-deleted on merge. | None — squash into `dev` freely. | +| `release/*` | Head of a `release/* → main` PR. | One release's worth. Auto-deleted on merge. | None. | -`dev` is a **forever branch**. Never delete it locally or remotely, even after a `dev → main` merge. The repo's -`delete_branch_on_merge: true` setting doesn't touch `dev` because `dev` is the base, not the head, of the release-time -PR. +`dev` is a **forever branch**. Never delete it locally or remotely, even after a `release/* → main` merge. The next +release cycle reuses the same `dev`. The repo's `delete_branch_on_merge: true` setting doesn't touch `dev` because `dev` +is never the head of a PR — using a short-lived `release/*` head is what keeps the setting compatible with a forever +integration branch. ## Daily development (feature → dev) @@ -51,48 +45,100 @@ gh pr create --base dev --title "feat(scope): what changed" ## Releasing dev to main -`dev` accumulates feature, fix, and docs commits. To cut a release, open one PR from `dev` to `main`: +Engineering docs (`docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, `docs/reviews/`) live on `dev` only. +`guard-main-docs.yml` blocks any `added` or `modified` files under those paths from reaching `main`. Branching from +`dev` and deleting docs on the way produces `add/add` merge conflicts whenever `dev` and `main` have diverged (the norm +after the first squash merge). The cherry-pick pattern avoids this. + +**Branch naming**: `release/v` (preferred) or `release/-`. Keep the slug short and descriptive. ```bash -git checkout dev && git pull +# 1. Cut release/* from main, NOT dev. Branching from dev causes add/add +# conflicts when dev and main have divergent histories. +git fetch origin +git checkout -b release/v origin/main -# Sanity: list what's on dev not yet on main. +# 2. List the dev commits not yet on main: git log --oneline dev --not origin/main -# Open the release PR. Title: a short summary of the version, not "Merge dev to main". -gh pr create --base main --head dev --title "release: v" -``` +# 3. Cherry-pick non-docs commits onto release/v. Docs commits +# (anything that touched only docs/plans/, docs/solutions/, +# docs/brainstorms/, or docs/reviews/) stay on dev. +git cherry-pick ... -**`guard-main-docs` runs on every PR with base `main`.** If any commit on `dev` added or modified a file under -`docs/plans/`, `docs/solutions/`, `docs/brainstorms/`, or `docs/reviews/`, the check fails and the PR is blocked. Two -ways to handle this: +# 4. Verify no guarded paths leaked through: +git diff origin/main --name-only | grep -E '^docs/(plans|solutions|brainstorms|reviews)/' && \ + echo 'FAIL: guarded docs in release branch' || echo 'OK: no guarded docs' -- **Preferred**: keep all engineering docs deletions or unchanged at PR time. Plans should land on `dev` and stay there - for posterity; they don't need to ship. -- **Override**: if a doc legitimately needs to ship to `main` (e.g., user-facing under `docs/`), add an exception in the - reusable workflow at `brettdavies/.github`, not here. +# 5. Bump VERSION on the release branch. +echo '' > VERSION + +# 6. Generate CHANGELOG entries from PR bodies. NEVER hand-edit CHANGELOG.md — +# the script is authoritative. It reads cliff.toml + each cherry-picked PR's +# ## Changelog section and prepends a versioned [] entry. +scripts/generate-changelog.sh +# (the script extracts from the branch name release/v) + +# 7. Commit the version bump and generated changelog. +git add VERSION CHANGELOG.md +git commit -m "chore(release): v" + +# 8. Push and open the PR: +git push -u origin release/v +gh pr create --base main --head release/v --title "release: v" +``` When the PR merges: 1. The squash commit lands on `main` with the PR body as its message. -2. Tag `v` on the new `main` HEAD: `git checkout main && git pull && git tag -a v -m "v" && git - push origin v`. -3. The site at `anc.dev/install` re-pins via its own PR (separate repo, separate session). +2. `release/v` is auto-deleted. +3. Tag the new `main` HEAD: `git checkout main && git pull && git tag -a v -m "v" && git push origin + v`. +4. Create the GitHub Release using the generated CHANGELOG section: + + ```bash + gh release create v --title "v" \ + --notes "$(awk '/^## \[\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md)" + ``` + +5. The site at `anc.dev/install` re-pins via its own PR (separate repo, separate session). The handoff is the new commit + SHA plus a note if the bundle layout changed. + +`dev` keeps moving forward. Never reset or rebase `dev` after a release — it is forever. + +### CHANGELOG is generated, never hand-written + +`scripts/generate-changelog.sh` (with `cliff.toml`) is the only sanctioned way to update `CHANGELOG.md`. The script: + +- Runs `git-cliff` to prepend a versioned entry for commits since the last tag. +- Walks each squash-merged PR's body, extracts the `## Changelog` section's `### Added` / `### Changed` / `### Fixed` / + `### Documentation` subsections, and replaces the auto-generated bullets with the curated PR-body content (with author + and PR-link attribution). + +If a PR's `## Changelog` section is empty, that PR's entry is omitted from the changelog (the convention in +[`.github/pull_request_template.md`](.github/pull_request_template.md): empty section = no user-facing change). To fix a +wrong CHANGELOG entry, fix the input — edit the squash-merged PR body, then re-run the script. Do **not** edit +`CHANGELOG.md` directly. + +`scripts/generate-changelog.sh --check` verifies that `CHANGELOG.md` has a versioned section (not just `[Unreleased]`) — +wire this into the release-branch CI if/when one is added. + +### Why branch from main, not dev -`dev` keeps moving forward. Don't reset or rebase `dev` after a release — it is forever. +Branching from `dev` and then `gio trash`-ing the guarded paths seems simpler but produces `add/add` merge conflicts +whenever `dev` and `main` have diverged. The file appears as "added" on both sides with different content. Always branch +from `origin/main` and cherry-pick onto it. ## Version bump procedure -Before opening the `dev → main` release PR: +The version bump and CHANGELOG generation both happen on the `release/v` branch (steps 5–6 of the cherry-pick +flow above). There is no separate version-bump PR to `dev`. Picking the version is the only manual decision: -1. Decide the new version per SemVer. **Patch** = doc updates, internal cleanups, non-substantive script tweaks. - **Minor** = new principles, new checks, new templates, new bundle files (backward-compatible additions). **Major** = - breaking changes to the bundle's contract — renaming `SKILL.md` frontmatter fields, changing exit codes of - `check-compliance.sh`, restructuring directory layout in ways that break existing skill installations. -2. Bump `VERSION` (single line, `\n`, no metadata). -3. Add a section to `CHANGELOG.md` under `## [Unreleased]` (if present) with the new version + date, populated from the - `## Changelog` sections of every PR squash-merged into `dev` since the last release. -4. Commit the bump on a `chore/release-vX.Y.Z` branch off `dev`, PR it into `dev`, then open the `dev → main` PR. +- **Patch** — doc updates, internal cleanups, non-substantive script tweaks. +- **Minor** — new principles, new checks, new templates, new bundle files (backward-compatible additions). +- **Major** — breaking changes to the bundle's contract: renaming `bundle/SKILL.md` frontmatter fields, changing exit + codes of `bundle/scripts/check-compliance.sh`, restructuring directory layout in ways that break existing skill + installations, or moving content between `bundle/` and the producer-ops root. ## PRs and changelog generation @@ -165,6 +211,7 @@ gh api repos/brettdavies/agentnative-skill/commits//check-runs --jq '.check ## Related docs - [`AGENTS.md`](./AGENTS.md) — repo layout, lint commands, what agents must not do. +- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — how to propose changes. - [`.github/pull_request_template.md`](.github/pull_request_template.md) — PR body structure with changelog sections. - [`.github/rulesets/README.md`](.github/rulesets/README.md) — ruleset apply + verify procedure. - [`CHANGELOG.md`](./CHANGELOG.md) — released versions and their notes. diff --git a/SKILL.md b/bundle/SKILL.md similarity index 100% rename from SKILL.md rename to bundle/SKILL.md diff --git a/checklists/new-tool.md b/bundle/checklists/new-tool.md similarity index 100% rename from checklists/new-tool.md rename to bundle/checklists/new-tool.md diff --git a/references/framework-idioms-other-languages.md b/bundle/references/framework-idioms-other-languages.md similarity index 100% rename from references/framework-idioms-other-languages.md rename to bundle/references/framework-idioms-other-languages.md diff --git a/references/framework-idioms.md b/bundle/references/framework-idioms.md similarity index 100% rename from references/framework-idioms.md rename to bundle/references/framework-idioms.md diff --git a/references/principles-deep-dive.md b/bundle/references/principles-deep-dive.md similarity index 100% rename from references/principles-deep-dive.md rename to bundle/references/principles-deep-dive.md diff --git a/references/project-structure.md b/bundle/references/project-structure.md similarity index 100% rename from references/project-structure.md rename to bundle/references/project-structure.md diff --git a/references/rust-clap-patterns.md b/bundle/references/rust-clap-patterns.md similarity index 100% rename from references/rust-clap-patterns.md rename to bundle/references/rust-clap-patterns.md diff --git a/scripts/check-compliance.sh b/bundle/scripts/check-compliance.sh similarity index 100% rename from scripts/check-compliance.sh rename to bundle/scripts/check-compliance.sh diff --git a/scripts/checks/_helpers.sh b/bundle/scripts/checks/_helpers.sh similarity index 100% rename from scripts/checks/_helpers.sh rename to bundle/scripts/checks/_helpers.sh diff --git a/scripts/checks/check-code-env-flags.sh b/bundle/scripts/checks/check-code-env-flags.sh similarity index 100% rename from scripts/checks/check-code-env-flags.sh rename to bundle/scripts/checks/check-code-env-flags.sh diff --git a/scripts/checks/check-code-naked-println.sh b/bundle/scripts/checks/check-code-naked-println.sh similarity index 100% rename from scripts/checks/check-code-naked-println.sh rename to bundle/scripts/checks/check-code-naked-println.sh diff --git a/scripts/checks/check-code-unwrap.sh b/bundle/scripts/checks/check-code-unwrap.sh similarity index 100% rename from scripts/checks/check-code-unwrap.sh rename to bundle/scripts/checks/check-code-unwrap.sh diff --git a/scripts/checks/check-p1-headless-auth.sh b/bundle/scripts/checks/check-p1-headless-auth.sh similarity index 100% rename from scripts/checks/check-p1-headless-auth.sh rename to bundle/scripts/checks/check-p1-headless-auth.sh diff --git a/scripts/checks/check-p1-non-interactive.sh b/bundle/scripts/checks/check-p1-non-interactive.sh similarity index 100% rename from scripts/checks/check-p1-non-interactive.sh rename to bundle/scripts/checks/check-p1-non-interactive.sh diff --git a/scripts/checks/check-p2-structured-output.sh b/bundle/scripts/checks/check-p2-structured-output.sh similarity index 100% rename from scripts/checks/check-p2-structured-output.sh rename to bundle/scripts/checks/check-p2-structured-output.sh diff --git a/scripts/checks/check-p3-progressive-help.sh b/bundle/scripts/checks/check-p3-progressive-help.sh similarity index 100% rename from scripts/checks/check-p3-progressive-help.sh rename to bundle/scripts/checks/check-p3-progressive-help.sh diff --git a/scripts/checks/check-p3-version-flag.sh b/bundle/scripts/checks/check-p3-version-flag.sh similarity index 100% rename from scripts/checks/check-p3-version-flag.sh rename to bundle/scripts/checks/check-p3-version-flag.sh diff --git a/scripts/checks/check-p4-error-types.sh b/bundle/scripts/checks/check-p4-error-types.sh similarity index 100% rename from scripts/checks/check-p4-error-types.sh rename to bundle/scripts/checks/check-p4-error-types.sh diff --git a/scripts/checks/check-p4-exit-codes.sh b/bundle/scripts/checks/check-p4-exit-codes.sh similarity index 100% rename from scripts/checks/check-p4-exit-codes.sh rename to bundle/scripts/checks/check-p4-exit-codes.sh diff --git a/scripts/checks/check-p4-process-exit.sh b/bundle/scripts/checks/check-p4-process-exit.sh similarity index 100% rename from scripts/checks/check-p4-process-exit.sh rename to bundle/scripts/checks/check-p4-process-exit.sh diff --git a/scripts/checks/check-p4-try-parse.sh b/bundle/scripts/checks/check-p4-try-parse.sh similarity index 100% rename from scripts/checks/check-p4-try-parse.sh rename to bundle/scripts/checks/check-p4-try-parse.sh diff --git a/scripts/checks/check-p5-safe-retries.sh b/bundle/scripts/checks/check-p5-safe-retries.sh similarity index 100% rename from scripts/checks/check-p5-safe-retries.sh rename to bundle/scripts/checks/check-p5-safe-retries.sh diff --git a/scripts/checks/check-p6-completions.sh b/bundle/scripts/checks/check-p6-completions.sh similarity index 100% rename from scripts/checks/check-p6-completions.sh rename to bundle/scripts/checks/check-p6-completions.sh diff --git a/scripts/checks/check-p6-global-flags.sh b/bundle/scripts/checks/check-p6-global-flags.sh similarity index 100% rename from scripts/checks/check-p6-global-flags.sh rename to bundle/scripts/checks/check-p6-global-flags.sh diff --git a/scripts/checks/check-p6-no-color.sh b/bundle/scripts/checks/check-p6-no-color.sh similarity index 100% rename from scripts/checks/check-p6-no-color.sh rename to bundle/scripts/checks/check-p6-no-color.sh diff --git a/scripts/checks/check-p6-no-pager.sh b/bundle/scripts/checks/check-p6-no-pager.sh similarity index 100% rename from scripts/checks/check-p6-no-pager.sh rename to bundle/scripts/checks/check-p6-no-pager.sh diff --git a/scripts/checks/check-p6-sigpipe.sh b/bundle/scripts/checks/check-p6-sigpipe.sh similarity index 100% rename from scripts/checks/check-p6-sigpipe.sh rename to bundle/scripts/checks/check-p6-sigpipe.sh diff --git a/scripts/checks/check-p6-timeout.sh b/bundle/scripts/checks/check-p6-timeout.sh similarity index 100% rename from scripts/checks/check-p6-timeout.sh rename to bundle/scripts/checks/check-p6-timeout.sh diff --git a/scripts/checks/check-p6-tty-detection.sh b/bundle/scripts/checks/check-p6-tty-detection.sh similarity index 100% rename from scripts/checks/check-p6-tty-detection.sh rename to bundle/scripts/checks/check-p6-tty-detection.sh diff --git a/scripts/checks/check-p7-output-clamping.sh b/bundle/scripts/checks/check-p7-output-clamping.sh similarity index 100% rename from scripts/checks/check-p7-output-clamping.sh rename to bundle/scripts/checks/check-p7-output-clamping.sh diff --git a/scripts/checks/check-p7-quiet-flag.sh b/bundle/scripts/checks/check-p7-quiet-flag.sh similarity index 100% rename from scripts/checks/check-p7-quiet-flag.sh rename to bundle/scripts/checks/check-p7-quiet-flag.sh diff --git a/scripts/checks/check-project-agents-md.sh b/bundle/scripts/checks/check-project-agents-md.sh similarity index 100% rename from scripts/checks/check-project-agents-md.sh rename to bundle/scripts/checks/check-project-agents-md.sh diff --git a/scripts/checks/check-project-dependencies.sh b/bundle/scripts/checks/check-project-dependencies.sh similarity index 100% rename from scripts/checks/check-project-dependencies.sh rename to bundle/scripts/checks/check-project-dependencies.sh diff --git a/templates/agents-md-template.md b/bundle/templates/agents-md-template.md similarity index 100% rename from templates/agents-md-template.md rename to bundle/templates/agents-md-template.md diff --git a/templates/clap-main.rs b/bundle/templates/clap-main.rs similarity index 100% rename from templates/clap-main.rs rename to bundle/templates/clap-main.rs diff --git a/templates/error-types.rs b/bundle/templates/error-types.rs similarity index 100% rename from templates/error-types.rs rename to bundle/templates/error-types.rs diff --git a/templates/output-format.rs b/bundle/templates/output-format.rs similarity index 100% rename from templates/output-format.rs rename to bundle/templates/output-format.rs diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..4176efc --- /dev/null +++ b/cliff.toml @@ -0,0 +1,25 @@ +[changelog] +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {{ commit.message | upper_first }}\ + {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\ + {% if commit.remote.pr_number %} in \ + [#{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }}){%- endif %} + {%- endfor %} +{% endfor %}\n +""" + +[remote.github] +owner = "brettdavies" +repo = "agentnative-skill" diff --git a/scripts/generate-changelog.sh b/scripts/generate-changelog.sh new file mode 100755 index 0000000..b111442 --- /dev/null +++ b/scripts/generate-changelog.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# Generate or update CHANGELOG.md using git-cliff with PR body expansion. +# +# Usage: +# generate-changelog.sh [--tag vX.Y.Z] [repo-path] +# generate-changelog.sh --check [repo-path] +# +# Options: +# --tag vX.Y.Z Override version tag (default: extracted from branch name) +# --check Verify CHANGELOG.md has a versioned section (exit 1 if only [Unreleased]) +# +# The version tag is extracted from the branch name by matching the pattern +# release/vN.N.N (with optional suffix like release/v1.0.5:ci-migration). +# Use --tag to override when not on a release branch. +# +# Generates entries for commits since the last tag, prepends to existing +# CHANGELOG.md, then expands squash commit entries by fetching categorized +# changelog sections (### Added, ### Changed, ### Fixed, ### Documentation) +# from each PR body's ## Changelog section. +# +# Falls back to ## Changes (flat list) for PRs using the old template. +# +# Run this on a release branch before opening a PR to main. + +set -euo pipefail + +CHECK_MODE=false +REPO_PATH="." +TAG="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --check) + CHECK_MODE=true + shift + ;; + --tag) + TAG="$2" + shift 2 + ;; + *) + REPO_PATH="$1" + shift + ;; + esac +done + +cd "$REPO_PATH" + +# Verify prerequisites +if [[ ! -f cliff.toml ]]; then + echo "error: cliff.toml not found in $(pwd)" >&2 + exit 1 +fi + +if ! command -v git-cliff &>/dev/null; then + echo "error: git-cliff is not installed" >&2 + echo " Install: cargo install git-cliff" >&2 + echo " Or: brew install git-cliff" >&2 + exit 1 +fi + +if $CHECK_MODE; then + if [[ ! -f CHANGELOG.md ]]; then + echo "FAIL: CHANGELOG.md does not exist" >&2 + exit 1 + fi + + # Check for a versioned section (not just [Unreleased]) + LATEST_SECTION=$(awk '/^## \[/{print; exit}' CHANGELOG.md) + if echo "$LATEST_SECTION" | grep -q '\[Unreleased\]'; then + echo "FAIL: CHANGELOG.md has [Unreleased] instead of a versioned section" >&2 + echo "Run: generate-changelog.sh (on a release/vX.Y.Z branch)" >&2 + exit 1 + fi + + echo "OK: CHANGELOG.md has versioned section" + exit 0 +fi + +# Extract version from branch name if --tag not provided +if [[ -z "$TAG" ]]; then + BRANCH=$(git branch --show-current 2>/dev/null || true) + if [[ "$BRANCH" =~ ^release/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + TAG="v${BASH_REMATCH[1]}" + echo "Detected version $TAG from branch $BRANCH" + else + echo "error: could not detect version from branch '$BRANCH'" >&2 + echo "Either use a release/vX.Y.Z branch or pass --tag vX.Y.Z" >&2 + exit 1 + fi +fi + +# Ensure GitHub token is available for remote integration (PR links, authors) +if [[ -z "${GITHUB_TOKEN:-}" ]]; then + if command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1; then + export GITHUB_TOKEN + GITHUB_TOKEN=$(gh auth token) + fi +fi + +# Step 1: Run git-cliff to prepend entries tagged with the release version +CLIFF_ARGS=(--unreleased --tag "$TAG") +if [[ -f CHANGELOG.md ]]; then + CLIFF_ARGS+=(--prepend CHANGELOG.md) +else + CLIFF_ARGS+=(-o CHANGELOG.md) +fi +git cliff "${CLIFF_ARGS[@]}" + +# Step 2: Expand squash commit entries using PR body changelog sections +OWNER=$(awk -F'"' '/^\[remote\.github\]/{found=1} found && /^owner/{print $2; exit}' cliff.toml) +REPO=$(awk -F'"' '/^\[remote\.github\]/{found=1} found && /^repo/{print $2; exit}' cliff.toml) + +# Strip leading v for version matching in the changelog +VERSION="${TAG#v}" + +if [[ -z "$OWNER" || -z "$REPO" ]] || ! command -v gh &>/dev/null; then + echo "Updated CHANGELOG.md (skipping PR expansion — missing [remote.github] or gh CLI)" + echo "" + echo "Next steps:" + echo " git add CHANGELOG.md" + echo " git commit -m 'docs: update CHANGELOG.md'" + exit 0 +fi + +# Extract PR numbers from the new version section only +VERSION_SECTION=$(awk -v ver="$VERSION" ' + /^## \[/{ + if (found) exit + if (index($0, "[" ver "]")) found=1 + } + found{print} +' CHANGELOG.md) +PR_NUMBERS=$(echo "$VERSION_SECTION" | grep -oP '\(#\K\d+' | sort -un) + +if [[ -z "$PR_NUMBERS" ]]; then + echo "Updated CHANGELOG.md" + echo "" + echo "Next steps:" + echo " git add CHANGELOG.md" + echo " git commit -m 'docs: update CHANGELOG.md'" + exit 0 +fi + +# Pass PR numbers as comma-separated arg to python +PR_LIST=$(echo "$PR_NUMBERS" | tr '\n' ',' | sed 's/,$//') + +python3 - "$OWNER" "$REPO" "$PR_LIST" "CHANGELOG.md" "$VERSION" "$TAG" << 'PYEOF' +import json, re, subprocess, sys + +owner = sys.argv[1] +repo = sys.argv[2] +pr_numbers = [int(n) for n in sys.argv[3].split(',')] +changelog_path = sys.argv[4] +version = sys.argv[5] +tag = sys.argv[6] if len(sys.argv) > 6 else f'v{version}' +tag_prefix = 'v' if tag.startswith('v') else '' + +CATEGORIES = ['Added', 'Changed', 'Fixed', 'Documentation'] + +def fetch_pr(num): + """Fetch PR body and author from GitHub API.""" + try: + result = subprocess.run( + ['gh', 'api', f'repos/{owner}/{repo}/pulls/{num}', + '--jq', '{body: .body, author: .user.login}'], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0: + return json.loads(result.stdout) + except Exception: + pass + return None + +def extract_changelog_sections(body): + """Extract categorized bullets from ## Changelog section with ### subsections.""" + sections = {} + if not body: + return sections + + changelog_match = re.search(r'^## Changelog\s*$', body, re.MULTILINE) + if not changelog_match: + return sections + + rest = body[changelog_match.end():] + next_h2 = re.search(r'^## ', rest, re.MULTILINE) + changelog_content = rest[:next_h2.start()] if next_h2 else rest + + current_section = None + for line in changelog_content.split('\n'): + h3_match = re.match(r'^### (.+)', line) + if h3_match: + current_section = h3_match.group(1).strip() + if current_section not in sections: + sections[current_section] = [] + elif current_section and re.match(r'^- ', line): + sections[current_section].append(line) + elif current_section and sections.get(current_section) and re.match(r'^ \S', line): + # Continuation line (indented, part of previous bullet) — join to last bullet + sections[current_section][-1] = sections[current_section][-1].rstrip() + ' ' + line.strip() + + return sections + +def extract_flat_changes(body): + """Fallback: extract flat bullet list from ## Changes section.""" + bullets = [] + if not body: + return bullets + + changes_match = re.search(r'^## Changes\s*$', body, re.MULTILINE) + if not changes_match: + return bullets + + rest = body[changes_match.end():] + next_h2 = re.search(r'^## ', rest, re.MULTILINE) + changes_content = rest[:next_h2.start()] if next_h2 else rest + + for line in changes_content.split('\n'): + if re.match(r'^- ', line): + bullets.append(line) + elif bullets and re.match(r'^ \S', line): + # Continuation line (indented, part of previous bullet) — join to last bullet + bullets[-1] = bullets[-1].rstrip() + ' ' + line.strip() + + return bullets + +# Collect all categorized entries from PR bodies +all_entries = {} # category -> list of bullets +for num in pr_numbers: + pr_data = fetch_pr(num) + if not pr_data: + continue + + body = pr_data.get('body', '') or '' + author = pr_data.get('author', '') + attrib = f' by @{author} in [#{num}](https://github.com/{owner}/{repo}/pull/{num})' if author else '' + + # Try new template format first (## Changelog with ### subsections) + sections = extract_changelog_sections(body) + + if sections: + for category, bullets in sections.items(): + if not bullets: + continue + if category not in all_entries: + all_entries[category] = [] + first = True + for bullet in bullets: + if first and ' by @' not in bullet: + all_entries[category].append(bullet + attrib) + else: + all_entries[category].append(bullet) + first = False + else: + # Fallback: flat ## Changes section + flat = extract_flat_changes(body) + if flat: + category = 'Changed' + if category not in all_entries: + all_entries[category] = [] + first = True + for bullet in flat: + if first and ' by @' not in bullet: + all_entries[category].append(bullet + attrib) + else: + all_entries[category].append(bullet) + first = False + +if not all_entries: + sys.exit(0) + +# Read the changelog +with open(changelog_path, 'r') as f: + content = f.read() + +# Find the version section header line (preserve it with the date) +header_pattern = rf'^## \[{re.escape(version)}\].*$' +header_match = re.search(header_pattern, content, re.MULTILINE) +if not header_match: + sys.exit(0) + +header_line = header_match.group(0) + +# Build the new version section content +new_section = header_line + '\n' +for cat in CATEGORIES: + if cat in all_entries and all_entries[cat]: + new_section += f'\n### {cat}\n\n' + for bullet in all_entries[cat]: + new_section += bullet + '\n' + +# Include any categories not in the standard list +for cat in all_entries: + if cat not in CATEGORIES and all_entries[cat]: + new_section += f'\n### {cat}\n\n' + for bullet in all_entries[cat]: + new_section += bullet + '\n' + +# Find the previous version tag for the Full Changelog link +prev_match = re.search(rf'## \[{re.escape(version)}\].*?\n## \[([^\]]+)\]', content, re.DOTALL) +if prev_match: + prev_version = prev_match.group(1) + new_section += f'\n**Full Changelog**: [{tag_prefix}{prev_version}...{tag_prefix}{version}](https://github.com/{owner}/{repo}/compare/{tag_prefix}{prev_version}...{tag_prefix}{version})\n' + +# Replace the version section in the file +section_pattern = rf'## \[{re.escape(version)}\].*?(?=\n## \[|\Z)' +new_content = re.sub(section_pattern, new_section.rstrip() + '\n', content, count=1, flags=re.DOTALL) + +with open(changelog_path, 'w') as f: + f.write(new_content) +PYEOF + +echo "Updated CHANGELOG.md" +echo "" +echo "Next steps:" +echo " git add CHANGELOG.md" +echo " git commit -m 'docs: update CHANGELOG.md'"