Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 47 additions & 31 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ for protocol types are in
`.github/instructions/general-instructions.instructions.md`. Release mechanics
are in [`RELEASING.md`](RELEASING.md).

## Updating CHANGELOGs
## Adding changelog fragments

This repo ships six independently-versioned artifacts (the spec plus
the Rust / Kotlin / Swift / TypeScript / Go clients), each with its
own `CHANGELOG.md` in Keep-a-Changelog format. The publish workflows
refuse to release a tag whose matching `## [X.Y.Z]` heading is
missing, so every user-visible change should land its CHANGELOG bullet
in the same PR as the code.
missing. To avoid merge conflicts in shared `CHANGELOG.md` files,
normal PRs add JSON changelog fragments under `docs/.changes/` instead
of editing the changelogs directly. Release PRs collapse those fragments
into the six changelogs with `npm run changelog:release`.

### When to add an entry

Add a one-line bullet under `## [Unreleased]` whenever your change is
Add a one-line fragment whenever your change is
**user-visible**:

- A new, removed, renamed, or behaviourally-changed action, command, state
Expand All @@ -28,48 +30,62 @@ Add a one-line bullet under `## [Unreleased]` whenever your change is
functions/types, transport options, reducer outputs, etc.).
- A bug fix that changes observable behaviour for a consumer of the spec or
any client.
- A security-relevant change (always also add a `### Security` subsection).
- A security-relevant change (use `"type": "security"`).

**Skip the CHANGELOG** when the change is purely:
**Skip the fragment** when the change is purely:

- Edits under `**/generated/**` (those mirror a `types/` change that should
have its own entry).
- Docs in `docs/`, `README.md`, comments, AGENTS.md, CONTRIBUTING.md.
- Tests, CI, lint config, formatting, internal refactors with no observable
effect.

### Which CHANGELOG(s) to update
### Which artifact(s) to target

Map source paths to changelogs:
Each fragment may specify a `targets` array. Omit `targets` when the same
entry applies to the spec and all clients (the common case for protocol
surface changes). Set `targets` to a subset when the change is only visible
to specific artifacts.

| Source path touched | CHANGELOG(s) that need an entry |
Map source paths to fragment targets:

| Source path touched | Fragment target(s) |
| --- | --- |
| `types/**` (protocol surface) | Root `CHANGELOG.md` **and** every `clients/<lang>/CHANGELOG.md` (a spec change ripples to every client). |
| `clients/rust/**` (non-generated) | `clients/rust/CHANGELOG.md` only. |
| `clients/kotlin/**` (non-generated) | `clients/kotlin/CHANGELOG.md` only. |
| `clients/swift/**` (non-generated) | `clients/swift/CHANGELOG.md` only. |
| `clients/typescript/**` (non-generated) | `clients/typescript/CHANGELOG.md` only. |
| `clients/go/**` (non-generated) | `clients/go/CHANGELOG.md` only. |
| `schema/**` | Root `CHANGELOG.md` (the schema is a spec output). |
| `scripts/generate*.ts` that changes any client's generated output | Every affected client's `CHANGELOG.md`. |
| `types/**` (protocol surface) | Omit `targets` (spec + all clients) unless the visibility is intentionally narrower. |
| `clients/rust/**` (non-generated) | `"targets": ["rust"]` |
| `clients/kotlin/**` (non-generated) | `"targets": ["kotlin"]` |
| `clients/swift/**` (non-generated) | `"targets": ["swift"]` |
| `clients/typescript/**` (non-generated) | `"targets": ["typescript"]` |
| `clients/go/**` (non-generated) | `"targets": ["go"]` |
| `schema/**` | `"targets": ["spec"]` |
| `scripts/generate*.ts` that changes any client's generated output | Omit `targets` or list every affected target. |

### Format

Use the standard Keep-a-Changelog subsection headers — `Added`, `Changed`,
`Deprecated`, `Removed`, `Fixed`, `Security` — under `## [Unreleased]`.
Create the subsection if it doesn't already exist. One bullet per change.

```markdown
## [Unreleased]
Create a uniquely named JSON file under `docs/.changes/`, usually
`docs/.changes/YYYYMMDD-short-slug.json`. Use lowercase Keep-a-Changelog
types: `added`, `changed`, `deprecated`, `removed`, `fixed`, `security`.
Do not include the leading Markdown bullet in `message`.

```json
{
"type": "added",
"message": "`session/cancelTurn` action for client-initiated turn cancellation.",
"issues": [123]
}
```

### Added
- `session/cancelTurn` action for client-initiated turn cancellation.
Scoped client-only example:

### Changed
- `AhpClient.connect` now rejects with `AhpProtocolError` (not
`Error`) on negotiation failure.
```json
{
"type": "fixed",
"message": "`AhpClient.connect` now rejects with `AhpProtocolError` on negotiation failure.",
"targets": ["typescript"]
}
```

Do **not** invent a `## [X.Y.Z]` heading — that's reserved for release time
and is added by the maintainer cutting the release per
[`RELEASING.md`](RELEASING.md).
Do **not** edit `CHANGELOG.md` files for normal feature/fix PRs and do
not invent a `## [X.Y.Z]` heading. Changelogs are updated by the release
maintainer per [`RELEASING.md`](RELEASING.md). Run
`npm run verify:change-fragments` to validate fragment JSON.
61 changes: 43 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ against them.
```bash
npm install # install root tooling
npm run generate # regenerate every client + schemas
npm test # typecheck + lint + verify:release-metadata + reducer tests
npm test # typecheck + lint + release/changelog verification + reducer tests
```

Per-client builds (run only what's relevant to your change):
Expand All @@ -51,34 +51,59 @@ and the one-time admin setup for each environment — live in
[`RELEASING.md`](RELEASING.md). For the protocol-level versioning policy,
see [`docs/specification/versioning.md`](docs/specification/versioning.md).

## Updating CHANGELOGs
## Adding changelog fragments

This repo ships five independently-versioned artifacts (spec + four clients),
This repo ships six independently-versioned artifacts (spec + five clients),
each with its own `CHANGELOG.md` in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
format. The publish workflows refuse to release a tag whose matching
`## [X.Y.Z]` heading is missing, so every user-visible change should land its
CHANGELOG bullet in the same PR as the code.
`## [X.Y.Z]` heading is missing. Normal PRs should not edit those shared
changelog files directly; add a JSON changelog fragment under `docs/.changes/`
instead. Release PRs collapse those fragments into the six changelogs.

**Add a one-line bullet under `## [Unreleased]`** when your change is
**Add a one-line fragment** when your change is
user-visible: a new / removed / renamed / behaviourally-changed action,
command, state field, error, notification, version constant, or public client
API; an observable bug fix; or anything security-relevant. **Skip the
CHANGELOG** for generated code (`**/generated/**`), docs, tests, CI, lint
fragment** for generated code (`**/generated/**`), docs, tests, CI, lint
config, formatting, or internal refactors with no observable effect.

Path → changelog map:
Fragments live directly under `docs/.changes/` and use this shape:

| Source path touched | CHANGELOG(s) that need an entry |
```json
{
"type": "added",
"message": "`session/cancelTurn` action for client-initiated turn cancellation.",
"issues": [123]
}
```

`type` must be one of `added`, `changed`, `deprecated`, `removed`, `fixed`, or
`security`. `message` is the changelog bullet text without a leading `-`.
`issues` is optional.

Omit `targets` when the entry applies to the spec and all clients (the common
case for protocol additions). Add `targets` to scope the entry to a subset:

```json
{
"type": "fixed",
"message": "`AhpClient.connect` now rejects with `AhpProtocolError` on negotiation failure.",
"targets": ["typescript"]
}
```

Path → fragment target map:

| Source path touched | Fragment target(s) |
| --- | --- |
| `types/**` (protocol surface) | Root `CHANGELOG.md` **and** every `clients/<lang>/CHANGELOG.md` (a spec change ripples to every client). |
| `clients/<lang>/**` (non-generated) | That client's `CHANGELOG.md` only. |
| `schema/**` | Root `CHANGELOG.md` (the schema is a spec output). |
| `scripts/generate*.ts` that changes any client's generated output | Every affected client's `CHANGELOG.md`. |

Use the standard Keep-a-Changelog subsection headers — `Added`, `Changed`,
`Deprecated`, `Removed`, `Fixed`, `Security` — under `## [Unreleased]`. One
bullet per change. Don't invent a `## [X.Y.Z]` heading — that's reserved for
release time per [`RELEASING.md`](RELEASING.md).
| `types/**` (protocol surface) | Omit `targets` (spec + all clients) unless intentionally narrower. |
| `clients/<lang>/**` (non-generated) | That client only, e.g. `["rust"]`. |
| `schema/**` | `["spec"]` |
| `scripts/generate*.ts` that changes any client's generated output | Omit `targets` or list every affected target. |

Run `npm run verify:change-fragments` to validate fragments. Don't invent a
`## [X.Y.Z]` heading or edit changelogs directly for normal PRs — that's
reserved for release time per [`RELEASING.md`](RELEASING.md).

This rule is also encoded in [`AGENTS.md`](AGENTS.md) so AI coding agents
working in the repo follow the same convention.
Expand Down
46 changes: 38 additions & 8 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ with its own release cadence. Each client release advertises which protocol
versions it supports via a generated `SUPPORTED_PROTOCOL_VERSIONS` constant
and a checked-in `clients/<lang>/release-metadata.json`.

## Changelog fragments

Normal feature/fix PRs do **not** edit `CHANGELOG.md` files directly. They add
one or more JSON fragments under `docs/.changes/`; omitting `targets` applies
the entry to the spec and all five clients, while `targets` can scope an entry
to any subset of `spec`, `rust`, `kotlin`, `typescript`, `swift`, and `go`.

Before tagging a coordinated release, collapse those fragments into the six
Keep-a-Changelog files:

```sh
npm run changelog:release -- --version X.Y.Z --date YYYY-MM-DD
```

By default the command targets all artifacts. For a single-artifact hotfix, pass
`--targets rust` (or `spec`, `kotlin`, `typescript`, `swift`, `go`; comma-join
multiple targets for a subset). The command creates or replaces the
`## [X.Y.Z] — YYYY-MM-DD` section in each selected artifact changelog, adds the
`Spec version` / `Implements AHP` line, groups fragment messages under standard
`Added` / `Changed` / `Fixed` subsections, and deletes consumed fragment files.
If a fragment also targets artifacts not included in `--targets`, the command
rewrites that fragment with the remaining targets so a later release can still
consume it. Run `npm run verify:change-fragments` before release if you only
want to validate fragment JSON without consuming it.

## Tag conventions

| Artifact | Tag pattern | Workflow | Registry / discovery |
Expand Down Expand Up @@ -59,8 +84,8 @@ and a checked-in `clients/<lang>/release-metadata.json`.
`[workspace.dependencies]` and any per-crate dependency declarations.
3. Run `npm run generate:metadata` and commit the regenerated
`clients/rust/release-metadata.json`.
4. Rotate `clients/rust/CHANGELOG.md`: move the `## [Unreleased]` section to
`## [X.Y.Z] — YYYY-MM-DD` with an `Implements AHP <version>` line.
4. Collapse Rust-scoped changelog fragments with
`npm run changelog:release -- --version X.Y.Z --targets rust`.
5. Merge to `main`.
6. Tag: `git tag rust/v0.X.Y && git push origin rust/v0.X.Y`.
7. `publish-rust.yml` validates, then publishes `ahp-types`, `ahp`, and
Expand All @@ -72,7 +97,8 @@ and a checked-in `clients/<lang>/release-metadata.json`.
`-SNAPSHOT` suffix (the publish workflow rejects snapshot tags).
2. Run `npm run generate:metadata` and commit the regenerated
`clients/kotlin/release-metadata.json`.
3. Rotate `clients/kotlin/CHANGELOG.md`.
3. Collapse Kotlin-scoped changelog fragments with
`npm run changelog:release -- --version X.Y.Z --targets kotlin`.
4. Merge to `main`.
5. Tag: `git tag kotlin/v0.X.Y && git push origin kotlin/v0.X.Y`.
6. `clients/kotlin/pipeline.yml` (Azure DevOps) validates the tag,
Expand All @@ -96,8 +122,8 @@ use PATs to trigger ADO from GHA.
2. `cd clients/typescript && npm install` to refresh the lockfile.
3. Run `npm run generate:metadata` from the repo root and commit the
regenerated `clients/typescript/release-metadata.json`.
4. Rotate `clients/typescript/CHANGELOG.md`: move `## [Unreleased]` to
`## [X.Y.Z] — YYYY-MM-DD` with an `Implements AHP <version>` line.
4. Collapse TypeScript-scoped changelog fragments with
`npm run changelog:release -- --version X.Y.Z --targets typescript`.
5. Merge to `main`.
6. Tag: `git tag typescript/v0.X.Y && git push origin typescript/v0.X.Y`.
7. The ADO pipeline picks up the tag, validates it against
Expand All @@ -118,7 +144,8 @@ trigger started the run.
leading `v`, no `-SNAPSHOT`).
2. Run `npm run generate:metadata` and commit the regenerated
`clients/swift/release-metadata.json`.
3. Rotate `clients/swift/CHANGELOG.md`.
3. Collapse Swift-scoped changelog fragments with
`npm run changelog:release -- --version X.Y.Z --targets swift`.
4. Merge to `main`.
5. Tag: `git tag v0.X.Y && git push origin v0.X.Y`. **Note the absence of
any prefix** — this is the one place in the repo where bare semver tags
Expand All @@ -134,7 +161,8 @@ trigger started the run.
`v`, no `-SNAPSHOT`).
2. Run `npm run generate:metadata` and commit the regenerated
`clients/go/release-metadata.json`.
3. Rotate `clients/go/CHANGELOG.md`.
3. Collapse Go-scoped changelog fragments with
`npm run changelog:release -- --version X.Y.Z --targets go`.
4. Merge to `main`.
5. Tag: `git tag clients/go/v0.X.Y && git push origin clients/go/v0.X.Y`.
**The `clients/go/` prefix is required** — it is what the Go module
Expand All @@ -156,7 +184,8 @@ trigger started the run.
new symbols.
3. Run `npm run generate` to refresh schemas, generated client sources,
and metadata.
4. Rotate the root `CHANGELOG.md`.
4. Collapse spec-scoped changelog fragments with
`npm run changelog:release -- --version X.Y.Z --targets spec`.
5. Merge to `main`.
6. Tag: `git tag spec/v0.X.Y && git push origin spec/v0.X.Y`.
7. `publish-spec.yml` validates, regenerates the JSON schemas from the
Expand All @@ -169,6 +198,7 @@ trigger started the run.
| --- | --- |
| `Version.generated.{rs,kt,swift,go}` ↔ `types/version/registry.ts` | Per-language CI job re-runs `npm run generate:<lang>` and fails on diff. |
| `release-metadata.json` ↔ native manifest + registry | `npm run verify:release-metadata` (also gated on every publish workflow). |
| `docs/.changes/*.json` shape | `npm run verify:change-fragments` validates fragment type, message, targets, and issue references before release collapse. |
| Native package version ↔ matching CHANGELOG entry | `npm run verify:changelog` (in CI, and re-run in `publish-rust.yml` / `publish-swift.yml` / `publish-go.yml` / both ADO `pipeline.yml`s). |
| Tag ↔ manifest version | Every tag-driven publish workflow's "Verify tag matches" step. |
| Tag-derived version ↔ CHANGELOG entry | Every tag-driven publish workflow's `grep -qE '^## \[<tag-version>\]'` step (defense-in-depth alongside `verify:changelog`). |
Expand Down
Empty file added docs/.changes/.gitkeep
Empty file.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
"generate:metadata": "tsx scripts/generate.ts --metadata",
"verify:release-metadata": "tsx scripts/verify-release-metadata.ts",
"verify:changelog": "tsx scripts/verify-changelog.ts",
"verify:change-fragments": "tsx scripts/verify-change-fragments.ts",
"changelog:release": "tsx scripts/consume-change-fragments.ts",
"docs:dev": "vitepress dev docs",
"docs:build": "npm run generate && vitepress build docs",
"docs:preview": "vitepress preview docs",
"typecheck": "tsc --noEmit -p types/tsconfig.json",
"lint": "eslint",
"test": "npm run typecheck && npm run lint && npm run verify:release-metadata && npm run verify:changelog && npx c8 --include types/reducers.ts --check-coverage --branches 100 tsx --test types/*.test.ts types/version/*.test.ts scripts/*.test.ts"
"test": "npm run typecheck && npm run lint && npm run verify:release-metadata && npm run verify:changelog && npm run verify:change-fragments && npx c8 --include types/reducers.ts --check-coverage --branches 100 tsx --test types/*.test.ts types/version/*.test.ts scripts/*.test.ts"
},
"repository": {
"type": "git",
Expand Down
Loading
Loading