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
5 changes: 3 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

---

- [ ] `CHANGELOG.md` updated under `## Unreleased` — or this change is
internal-only / test-only / docs-only (apply the `no-changelog` label).
- [ ] Changelog fragment added — `changelog.d/<slug>.<breaking|added|changed|fixed>.md`
(see `changelog.d/README.md`) — or this change is internal-only /
test-only / docs-only (apply the `no-changelog` label).

<!-- AI-assisted contributions are welcome and normal here — see
CONTRIBUTING.md for the attribution convention (footer + Co-Authored-By). -->
11 changes: 8 additions & 3 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ jobs:
with:
fetch-depth: 0

- name: Require CHANGELOG.md update when src/ changes
- name: Require changelog fragment when src/ changes
run: |
base="${{ github.event.pull_request.base.sha }}"
head="${{ github.event.pull_request.head.sha }}"
changed=$(git diff --name-only "$base...$head")
# Fragments must be *added* — a deleted or renamed fragment also
# appears in --name-only and must not satisfy the check.
added=$(git diff --name-only --diff-filter=A "$base...$head")
echo "Changed files:"
echo "$changed"
if echo "$changed" | grep -q '^src/' && ! echo "$changed" | grep -qx 'CHANGELOG.md'; then
echo "::error::This PR touches src/ but not CHANGELOG.md. Add an entry under 'Unreleased' (see CONTRIBUTING.md), or apply the 'no-changelog' label if the change is internal-only."
if echo "$changed" | grep -q '^src/' \
&& ! echo "$added" | grep -Eq '^changelog\.d/[^/]+\.(breaking|added|changed|fixed)\.md$' \
&& ! echo "$changed" | grep -qx 'CHANGELOG.md'; then
echo "::error::This PR touches src/ but carries no changelog entry. Add a fragment changelog.d/<slug>.<breaking|added|changed|fixed>.md (see CONTRIBUTING.md), or apply the 'no-changelog' label if the change is internal-only."
exit 1
fi
echo "OK"
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ permissions:

jobs:
build-and-test:
name: Build & Test (${{ matrix.os }})
name: Build & Test (${{ matrix.os }}, node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
# Both platforms: a package-lock.json regenerated over an existing
# node_modules tree records only that machine's native binaries, so a
# lock made on macOS breaks Linux installs and vice versa. Each runner
# catches the lock the other's platform produced. Tests run on ubuntu.
# Node: the engines floor and current LTS — stdio flushing semantics
# around process exit differ between versions and platforms.
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 24]

steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
Expand All @@ -30,7 +33,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 20
node-version: ${{ matrix.node }}

# Root lockfiles are deliberately gitignored in this repo, so plain
# npm install (matching publish.yml). No lock to guard here.
Expand All @@ -43,6 +46,7 @@ jobs:
- name: Build
run: npm run build

# Tests run on both platforms: process/stdio behaviour differs (pipes
# are asynchronous on macOS), and ubuntu-only runs have missed that.
- name: Test
if: matrix.os == 'ubuntu-latest'
run: npm test
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Notable changes to `@animalabs/agent-framework`, loosely following
[Keep a Changelog](https://keepachangelog.com/). Entries land with the change
that causes them — see [CONTRIBUTING.md](CONTRIBUTING.md#changelog).
that causes them, as fragment files in [`changelog.d/`](changelog.d/) that are
folded into a version section at release time — see
[CONTRIBUTING.md](CONTRIBUTING.md#changelog).

Releases up to and including 0.7.3 predate this file; for their contents see
`git log` and the
Expand Down
69 changes: 46 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ plus, when applicable, **Not verified**, **Out of scope**, and
- **Tests accompany behavior changes.** Review scrutinizes test substance,
not mere presence — a test that can't fail on the unfixed code will be
called out.
- **Changelog entry** under `## Unreleased` for anything behavior-affecting
- **Changelog fragment** in `changelog.d/` for anything behavior-affecting
(see below).

Conventional-commit-style titles (`feat(modules): …`, `fix(streaming): …`) are
Expand Down Expand Up @@ -85,38 +85,61 @@ that don't fail on unfixed code, or with claims the branch itself disproves.

## Changelog

`CHANGELOG.md` keeps a standing `## Unreleased` section with
`### Breaking` / `### Added` / `### Changed` / `### Fixed` subsections
(loosely [Keep a Changelog](https://keepachangelog.com/)).

- **The entry lands with the change** — same commit, or at least the same
Changelog entries land as **fragment files** in
[`changelog.d/`](changelog.d/) — one file per change — and are folded into
`CHANGELOG.md` (loosely [Keep a Changelog](https://keepachangelog.com/)) at
release time. One file per change is what keeps concurrent work from
conflicting: when every PR edited the same `## Unreleased` section, any PR
that outlived another merge hit a conflict in `CHANGELOG.md`; distinct files
never do.

- **Format:** `changelog.d/<slug>.<breaking|added|changed|fixed>.md`, a flat
file directly in `changelog.d/`, containing one or more markdown bullets
(`- …`) written exactly as they should appear in `CHANGELOG.md`:
continuation lines indent two spaces, nested bullets are fine, headings
and horizontal rules are refused (even indented — a heading inside a
fragment would corrupt the section structure). The slug just has to be
unique among pending fragments and filesystem-safe — the PR number works,
and so does the branch name with `/` replaced by `-`
(`115-tune-out.added.md`, `fix-locus-routing.fixed.md`). The release
script scans the directory fail-closed: a subdirectory, an unrecognized
category suffix, or any other stray file aborts the release rather than
silently stranding an entry.
- **The fragment lands with the change** — same commit, or at least the same
PR. This binds direct pushes to `main` just as much as PRs. On PRs, CI
enforces it softly: touching `src/` without touching `CHANGELOG.md` fails
the `changelog` check unless the `no-changelog` label is applied.
enforces it softly: touching `src/` without adding a fragment (or editing
`CHANGELOG.md`) fails the `changelog` check unless the `no-changelog`
label is applied.
- **What needs an entry:** anything a module developer, host, or downstream
consumer would notice — behavior, event/trace surfaces, tool namespacing,
config schema, agent state machine, public exports, defaults. Internal
refactors, test-only, and docs-only changes don't.
- **Breaking entries are audience-scoped.** Name the audience in the heading
(`### Breaking (module authors only)`) and cover: **who needs to act**,
- **Breaking entries are audience-scoped.** Open the bullet by naming who
needs to act (`- **Module authors:** …`) and cover: **who needs to act**,
**migration**, and **unchanged** (what readers might fear broke but
didn't). Because connectome-host and other hosts pin this package by
range, a breaking change here surfaces in their next install — spell out
the minimum sibling versions it requires.
- **Keep one `## Unreleased` heading.** Add entries under the existing one;
don't open a second. Only the first is cut at release time, so entries
filed under a later heading are silently never released — the release
script refuses to run if it finds more than one.
- **Editing `## Unreleased` in `CHANGELOG.md` directly still works** and is
merged with the fragments at release time — it remains the right place to
restructure pending entries, and the escape hatch for anything the
fragment format can't express (e.g. an audience-qualified
`### Breaking (module authors only)` heading, which `breaking` fragments
will then join). Keep one `## Unreleased` heading — the release script
refuses more than one, since only the first is ever cut.
- **Releases** (maintainers): `npm version <patch|minor|major>` does the
whole cut — the `version` hook retitles `Unreleased` to
`## X.Y.Z — YYYY-MM-DD` (keeping a fresh `Unreleased` above it, and
refusing to release when there are no entries), then npm commits and tags.
`git push --follow-tags` triggers CI, which refuses a tag with no matching
changelog section, publishes `@animalabs/agent-framework` to npm, and
creates the GitHub release with that section as its notes. The two release
jobs are independent: some consumers run github-clone checkouts, so
release notes must exist even when npm publish fails. Version bumps are a
maintainer release-time action, not part of feature PRs.
whole cut — the `version` hook folds the pending fragments plus any
entries filed directly under `Unreleased` into `## X.Y.Z — YYYY-MM-DD`
(subsections emitted in `### Breaking` / `### Added` / `### Changed` /
`### Fixed` order), deletes the consumed fragments, keeps a fresh empty
`Unreleased` above, and refuses to release when there is nothing to
release; npm then commits and tags. `git push --follow-tags` triggers CI,
which refuses a tag with no matching changelog section, publishes
`@animalabs/agent-framework` to npm, and creates the GitHub release with
that section as its notes. The two release jobs are independent: some
consumers run github-clone checkouts, so release notes must exist even
when npm publish fails. Version bumps are a maintainer release-time
action, not part of feature PRs.

## Building and testing

Expand Down
27 changes: 27 additions & 0 deletions changelog.d/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Pending changelog fragments

One file per change, so concurrent branches never conflict the way shared
`CHANGELOG.md` edits do. At release time `npm version` folds every fragment
here into the new version section of `CHANGELOG.md` and deletes it.

**Name:** `<slug>.<breaking|added|changed|fixed>.md`, as a flat file directly
in this directory. The slug just has to be unique among pending fragments and
filesystem-safe: the PR number works, and so does the branch name with `/`
replaced by `-` (`115-tune-out.added.md`, `fix-locus-routing.fixed.md`). The
release script refuses subdirectories and any other stray file here, so a
misplaced entry fails the release loudly instead of being left out.

**Content:** one or more markdown bullets, exactly as they should appear in
`CHANGELOG.md`. Continuation lines indent two spaces (nested bullets are
fine); headings and horizontal rules are refused even when indented, since a
heading inside a fragment would corrupt the section structure:

```markdown
- `tune_out` diverts a channel's traffic to a subconscious summarizer
instead of unsubscribing (#77). Continuation lines indent two spaces.
```

Breaking fragments open by naming who needs to act:
`- **Module authors:** …`.

See [CONTRIBUTING.md](../CONTRIBUTING.md#changelog) for what needs an entry.
5 changes: 5 additions & 0 deletions changelog.d/changelog-fragments.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- Changelog entries now land as per-change fragment files in `changelog.d/`
(`<slug>.<breaking|added|changed|fixed>.md`), folded into the version
section at release time — concurrent PRs no longer conflict in
`CHANGELOG.md`. Editing `## Unreleased` directly still works and is merged
at the same point.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"typecheck": "tsc --noEmit",
"pretest": "mkdir -p dist/test/fixtures && cp test/fixtures/*.mjs dist/test/fixtures/",
"test": "node --test --test-force-exit dist/test/*.test.js",
"version": "node scripts/release-changelog.mjs && git add CHANGELOG.md",
"version": "node scripts/release-changelog.mjs && git add CHANGELOG.md changelog.d",
"prepublishOnly": "npm run build"
},
"keywords": [
Expand Down
Loading