spec: the platform changelog page (carlosframework.com/platform/changelog)
Rulings from Paul 2026-10-06: a change is a merged branch, every release back to v0.1.0, change lines are records, the join runs here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+296
−0
docs/superpowers/specs/2026-10-06-platform-changelog-design.md+257 −0docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md+39 −0
CI passed — run details
diff --git a/docs/superpowers/specs/2026-10-06-platform-changelog-design.md b/docs/superpowers/specs/2026-10-06-platform-changelog-design.md| new file mode 100644 |
| index 000000000..e5c7fdd17 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-10-06-platform-changelog-design.md |
| @@ -0,0 +1,257 @@ |
| +# 🤖 The platform changelog (carlosframework.com/platform/changelog) |
| + |
| +Status: design, 2026-10-06. Asked for by Paul the same day: "a page which |
| +lists every change for every release, links to docs, links to the Amadan |
| +branch/discussion and shows who did the work". |
| + |
| +## Rulings (Paul, 2026-10-06; not to reopen) |
| + |
| +1. **A change is a merged branch.** Each release lists every first-parent |
| + commit that landed on `main` since the previous tag, which since v0.20.0 |
| + is one merged branch each. Groundwork, docs and fixes all count. The |
| + release's published notes sit on top as the summary. |
| +2. **Every release, back to v0.1.0, with a sliding link depth.** From the |
| + Amadan era on, a change links to its Amadan branch and discussions. |
| + Before that it links to its commit on Amadan (public) and credits the git |
| + authors. The GitHub mirror is private, so no GitHub PR links. |
| +3. **Change lines are records, not copy.** A change shows its branch name |
| + and squash subject verbatim, styled as data, under a visible 🤖. Only |
| + the page's own framing and the already-reviewed release notes are prose. |
| + The framing goes through copy review. |
| +4. **The join runs in the platform repo.** A generator writes a committed |
| + `docs/site/changelog.json`; the website vendors it with the docs corpus |
| + and renders it at build time. No network at website build. |
| + |
| +## What exists (verified 2026-10-06) |
| + |
| +- 28 tags, v0.1.0 (2026-08-02) to v0.24.2. v0.2.0 and v0.22.1 were tagged |
| + but never published on `carlosframework/releases`. |
| +- Merge shapes on `main`'s first-parent line: amadan squashes ("Squashed |
| + from X into main." in the body), amadan merges ("Merge branch 'X' into |
| + main"), GitHub merges ("Merge pull request #N from carlosframework/X"), |
| + and before those, direct commits. All 55 first-parent commits since |
| + v0.20.0 name their branch. |
| +- Amadan keeps a tombstone ref for each merged branch at |
| + `refs/amadan/closed/<branch>` (151 today), so a squashed branch's own |
| + commits stay reachable. Its page is `https://amadan.net/carlos/platform/-/branch/<url-escaped name>`; |
| + a commit is `https://amadan.net/carlos/platform/commit/<sha>`; a |
| + discussion is `https://amadan.net/carlos/platform/-/discussion/<id>`. |
| +- `amadan discuss show carlos/platform <id> -json` and `amadan discuss |
| + list carlos/platform -branch <b> -state all -json` read discussions. The |
| + unfiltered list caps at 50. There is no read for a branch's description. |
| +- Authors: people (`Paul Campbell`, `Bill Horsman`, `James Adam`, `Tony |
| + Wood`, under several emails) and agents whose email carries their |
| + operator (`claude~paul@keymail.dev`, `codex~paul@keymail.dev`, |
| + `claude-code~bill@keymail.dev`). Older person-authored commits carry |
| + `Co-Authored-By: Claude …` trailers. |
| +- Release notes changed shape. v0.19.0 on: a `carlos vX (platform …)` line, |
| + `- **Bold lead.** …` bullets, an operator section (`**For operators**`, |
| + `**Before you roll**`, `**Before you roll a console to this release**`), |
| + and a closing "Verify a download…" line. Before v0.19.0: free markdown |
| + with `##` headings, tables, fences, an `## Install` section and a |
| + "🤖 Generated with Claude Code" line. |
| +- carlosframework.com is `carlosframework/website` (Eleventy, plain CSS, no |
| + browser JS but the AI disclaimer). `hack/sync-docs.mjs` vendors |
| + `docs/site/` from a platform checkout; `hack/check-docs.mjs` checks |
| + the build's internal links. Its rule: every AI-written page carries a |
| + visible 🤖, and one on the top heading covers the page. |
| + |
| +## The data: `docs/site/changelog.json` |
| + |
| +```json |
| +{ |
| + "//": "generated by go run ./cmd/changelog; do not edit", |
| + "repo": "https://amadan.net/carlos/platform", |
| + "releases": [ |
| + { |
| + "tag": "v0.24.2", |
| + "date": "2026-10-06", |
| + "sha": "7f403a77…", |
| + "published": true, |
| + "notes_url": "https://github.com/carlosframework/releases/releases/tag/v0.24.2", |
| + "notes": { |
| + "form": "bullets", |
| + "sections": [ |
| + {"title": "", "bullets": [ |
| + {"md": "**CI can deploy through the console.** …", |
| + "branches": ["design-deploy-credential"], |
| + "docs": ["/docs/cli#carlos-services"]} |
| + ]}, |
| + {"title": "For operators", "bullets": [ … ]} |
| + ] |
| + }, |
| + "changes": [ |
| + { |
| + "sha": "d1dab9a9…", |
| + "date": "2026-10-06", |
| + "subject": "deploy-credential: a rung-scoped deploy credential so CI promotes through the console (#229)", |
| + "branch": "design-deploy-credential", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/design-deploy-credential", |
| + "commits": 14, |
| + "discussions": [{"id": 229, "title": "CI cannot promote …", "state": "closed"}], |
| + "docs": ["/docs/cli#carlos-services"], |
| + "credit": [{"person": "Paul Campbell", "agents": ["claude"]}] |
| + } |
| + ] |
| + } |
| + ] |
| +} |
| +``` |
| + |
| +- `notes.form` is `"bullets"` for the v0.19.0-on shape and `"markdown"` |
| + otherwise. A markdown body is kept whole as `notes.md`, with headings |
| + demoted two levels (the release is the page's `h2`) and the |
| + boilerplate removed: the `carlos vX (platform …)` line, the "Verify a |
| + download" line, an `## Install` section, and the "Generated with Claude |
| + Code" line. Unpublished tags have `"notes": null`. |
| +- Bullet sections keep the notes' own operator heading text, minus the |
| + bold markers. |
| +- `subject` is the commit's subject line. `branch_url` is set only when |
| + the branch has an Amadan record (a tombstone ref, a live head, or a |
| + squash/merge made on Amadan). A GitHub-era branch keeps its `branch` |
| + name with no link. |
| +- No email address ever appears in the file. The generator fails if one |
| + would. |
| + |
| +## The generator: `cmd/changelog`, logic in `internal/changelog` |
| + |
| +`go run ./cmd/changelog [-out docs/site/changelog.json] [-since <tag>]` |
| + |
| +Run from a full checkout after `git fetch origin --tags |
| +'+refs/amadan/closed/*:refs/amadan/closed/*'`. It needs `gh` (release |
| +bodies) and `amadan` (discussions) signed in. It writes the file and |
| +prints one summary line per release. |
| + |
| +**Inputs, each behind an interface** so tests run on fakes and a scratch |
| +git repo: |
| + |
| +- `Git`: tags with dates, first-parent commits in a range, a commit's |
| + message, parents and changed `docs/site/*.md` hunks, and the commits |
| + reachable from a tombstone but not from `main`. |
| +- `Releases`: release body by tag, or not-found. |
| +- `Discussions`: one by id, and the list for a branch. |
| + |
| +**Ranges.** Tags sort by semver. Release N covers |
| +`git rev-list --first-parent <tag N-1>..<tag N>`; v0.1.0 covers |
| +everything up to it. |
| + |
| +**Branch name**, from the first match in the message: `Squashed from (\S+) |
| +into`, `Merge branch '([^']+)'`, `Merge pull request #\d+ from |
| +[^/\s]+/(\S+)`. None: a direct commit, `branch` empty. |
| + |
| +**The branch's own commits**, for credit and discussion references: |
| + |
| +- a two-parent merge: `<first parent>..<second parent>`; |
| +- a squash with a tombstone `refs/amadan/closed/<branch>`: |
| + `<squash>^..<tombstone>`, non-merge commits only; |
| +- otherwise the commit itself. |
| + |
| +**Credit.** For each own commit, read the author and every |
| +`Co-Authored-By` trailer. An identity resolves to a person, an agent, or |
| +both: |
| + |
| +- `<agent>~<keymail>`: agent `<agent>` (`claude-code` reads `claude`), |
| + person = `people[<keymail>]`. |
| +- `noreply@anthropic.com` or a name starting `Claude`: agent `claude`, no |
| + person. `codex` names: agent `codex`. |
| +- Anything else: person = `people[<email>]`. |
| +- `noreply@github.com` and `ledger@amadan` are skipped. |
| + |
| +`docs/site/people.json` maps every email and keymail to a display name. |
| +An identity with no entry fails the run and names the address, so a new |
| +contributor is a one-line decision, not a leaked email. Agent-only |
| +identities (no person) attach to the commit's author's person when the |
| +author is a person, so "Paul Campbell with claude" survives a |
| +`Co-Authored-By` trailer. Credit groups by person, ordered by commit |
| +count, agents listed per person; an agent with no person anywhere is its |
| +own entry. |
| + |
| +**Discussions.** References come from the squash message and the own |
| +commits' messages: `#(\d+)`, `discussion (\d+)`, `amadan #(\d+)`. A |
| +number counts only when `discuss show` finds it and it was created before |
| +the commit, which rejects GitHub-era issue and PR numbers. Plus every |
| +discussion `discuss list -branch <branch>` returns. Deduped, sorted by id. |
| +Each lookup is cached for the run. |
| + |
| +**Docs links.** For a change: each `docs/site/<page>.md` the commit (the |
| +squash, or the merge's diff against its first parent) adds lines to, |
| +mapped to the nearest enclosing heading in the new file, then kept only |
| +if that heading still exists in today's corpus (`docsite.Load`, anchors |
| +by `docsite.Anchor`). A page that no longer exists drops out. For a |
| +release-note bullet: each inline code span starting `carlos <verb>` whose |
| +longest matching CLI heading (`carlos <verb>`, `carlos <verb> <sub>`) exists |
| +on the CLI page. Deduped, at most five per change. |
| + |
| +**Bullet to branch links.** `docs/site/changelog-links.json`, hand-kept: |
| + |
| +```json |
| +{"v0.24.2": {"CI can deploy through the console.": ["design-deploy-credential"]}} |
| +``` |
| + |
| +keyed by the bullet's bold lead. The generator fails on a key that |
| +matches no bullet in that release, or a branch that is not one of that |
| +release's changes. Bullets without an entry have no branch links. The |
| +backfill covers v0.19.0 on; older notes are markdown and get none. |
| + |
| +## The page: `/platform/changelog/` |
| + |
| +In `carlosframework/website`: |
| + |
| +- `hack/sync-docs.mjs` vendors `docs/site/changelog.json` as |
| + `src/_data/docschangelog.json` with the usual `"//"` notice (the sync |
| + already owns `src/_data/docs*.json`). |
| +- `src/changelog.njk` with `permalink: /platform/changelog/index.html`, |
| + in the platform page's world: its masthead, the platform blue accent, |
| + `site.css`, a page-local `<style>` only where `site.css` has nothing. |
| +- The top heading carries the visible 🤖, which covers the page. A short |
| + intro (copy-reviewed) says what a change is, that lines are verbatim |
| + records, and how far back links go. |
| +- One `<section id="v0-24-2">` per release, newest first: the tag as `h2` |
| + with a self link, date, a link to the published notes (or "not |
| + published"), then the notes (bullets with their branch and docs links; |
| + the operator section in a `<details>`; markdown notes rendered by the |
| + site's markdown-it), then "Changes (N)" as a list. Each change: branch |
| + name (linked when it has a record) or short SHA for a direct commit, |
| + the subject in mono, discussions as `#229` links titled with the |
| + discussion title, docs links, the credit ("Paul Campbell with claude"), |
| + and the short SHA linked to its commit. |
| +- A release with more than 25 changes shows the first 25 and the rest in |
| + a `<details>`. No script. |
| +- `/platform/` gets one link to the changelog (copy-reviewed). |
| +- `hack/check-docs.mjs` also checks that the page built, that every |
| + release has its section id, and that every `/docs` link on it |
| + resolves, using the code it already has for docs pages. |
| + |
| +## Gates and upkeep |
| + |
| +- `internal/docsite` gains `changelog_test.go`: the file parses; tags are |
| + unique and semver-ordered newest first; every `/docs` link resolves in |
| + the corpus; every `branch_url` and discussion link is on the repo URL; |
| + no address from `people.json` and nothing shaped `<x>~<y>@` appears |
| + anywhere in the file; every `changelog-links.json` key |
| + matched (the generator already enforces it, and the test re-checks the |
| + committed file). The test does not need git tags or network, so it runs |
| + in CI as is. |
| +- `internal/changelog` has unit tests for each parser and resolver on |
| + fakes, and one test on a scratch git repo that builds tags, a squash with |
| + a tombstone, a two-parent merge and a direct commit, and checks the whole |
| + output. |
| +- The README's release recipe gains a step after Publish: regenerate the |
| + changelog, add the new release's bullet links, commit on a branch, merge, |
| + then re-sync and ship the website. (Copy-reviewed.) |
| + |
| +## Asks of Amadan (filed, not worked around) |
| + |
| +1. A read for a branch's metadata, including merged ones: description, |
| + landed SHA, who pushed. Until then a change's line is its squash |
| + subject. |
| +2. Paging on `discuss list` (it caps at 50). The generator does not need |
| + it: it reads by id and by branch. |
| + |
| +## Out of scope |
| + |
| +- An RSS or Atom feed. It's easy to add from the same data later. |
| +- Per-change prose summaries (ruling 3). |
| +- Changing how release notes are written. |
| +- Rolling or deploying anything beyond the website's own ship and |
| + promote, which waits for the copy review. |
diff --git a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md| new file mode 100644 |
| index 000000000..d48a2e7b6 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md |
| @@ -0,0 +1,39 @@ |
| +# 🤖 Review brief: the platform changelog spec |
| + |
| +You are reviewing `docs/superpowers/specs/2026-10-06-platform-changelog-design.md` |
| +in this repo (carlos/platform), at the commit you are run on. You are the |
| +independent second reader; the author is Claude. |
| + |
| +The website it renders on is a separate checkout at |
| +`/home/paulca/github.com/carlosframework/website` (read `AGENTS.md`, |
| +`eleventy.config.js`, `hack/sync-docs.mjs`, `hack/check-docs.mjs` there). |
| + |
| +## Fixed: do not reopen |
| + |
| +The four rulings at the top of the spec are Paul's. Report only where the |
| +mechanism fails to honour them, not whether they are right. |
| + |
| +## What to look for |
| + |
| +1. **Wrong facts.** Check every claim in "What exists" against the repo: |
| + run `git log --first-parent`, `git for-each-ref refs/amadan/closed` |
| + (may need `git fetch origin '+refs/amadan/closed/*:refs/amadan/closed/*'` |
| + first; if you cannot fetch, say so), and read `internal/docsite`. |
| +2. **Mechanisms that produce wrong output.** Branch-name parsing, the |
| + own-commit ranges (squash with tombstone, two-parent merge, direct), |
| + credit resolution, discussion-number filtering, docs-link derivation, |
| + notes parsing across both shapes. Name a concrete commit or tag in |
| + this repo where the rule gives a wrong or misleading line. |
| +3. **Privacy and accuracy on a public page.** Anything that could publish |
| + an email address, a private repo link, or credit the wrong person. |
| +4. **Gaps the gate misses.** What could be wrong in the committed JSON |
| + and still pass `changelog_test.go` and the website's check. |
| +5. **Scope.** Anything that should be cut. |
| + |
| +## Output |
| + |
| +A numbered list of findings, each with severity (P1 breaks the page or |
| +publishes something wrong; P2 misleading or fragile; P3 nit), the spec |
| +line it is about, the evidence (command and output, or file:line), and a |
| +proposed fix. Then a one-line verdict: accept, accept with fixes, or |
| +reject. Do not edit any file. |