## What's broken
Every GitHub Release this repo has ever cut has a body of `Release
<tag>` and nothing else — `v1.70.0`, `v1.69.3`, `channels/v0.6.0`,
`angular/v0.4.0`, all of them. The `#engr` Slack announcement links
"Release notes" at that page, so that link has always pointed at a blank
release.
The notes *were* being generated. The `angular/v0.5.0` create-pr run
logged:
```
Raw release notes written to release-notes.md
Generating AI-enhanced release notes...
AI-enhanced release notes written to release-notes.md
```
They just never left the runner. `release-notes.md` was gitignored
(`.gitignore:72-73`), so `peter-evans/create-pull-request` skipped it,
the file never reached the release branch, and `publish-release.yml`'s
`readFileSync("./release-notes.md")` missed and fell through to `body =
\`Release ${name}\``.
The same ignore rule severed the Notion round-trip:
`release-notes-notion.json` was ignored too, so the publish job could
never read an edited draft back. That path had never run either.
Meanwhile the repo carried **29 changelog files that no tooling had
written since April**. They are changesets-era leftovers, and nothing in
`scripts/` or `.github/` reads or writes them:
```
$ git grep -n "CHANGELOG" -- 'scripts/**' '.github/**' '*.json' ':!*/CHANGELOG.md'
(no matches)
```
They stopped at `1.55.2` while the monorepo lane shipped `1.69.3`, and
`packages/angular/CHANGELOG.md` still claimed `1.54.3` — a version from
before angular split onto its own `0.x` line. So the only changelog a
reader could find in the tree named the wrong version for the wrong
lane.
## What this changes
**1. The notes become a source-controlled changelog, one file per
release lane.**
| lane | file |
|---|---|
| `monorepo` | `CHANGELOG.md` |
| `angular` | `packages/angular/CHANGELOG.md` |
| `channels` | `packages/channels/CHANGELOG.md` |
Per lane rather than one root file because the lanes version
independently: a shared file would interleave `1.70.0`, `angular/0.5.0`
and `channels/0.9.0` into one sequence where no reader can follow any
single line. (Concurrent writes are *not* the reason —
`stable-release.yml` already fails if any release PR is open.)
The flow: `prepare-release.ts` writes the raw notes,
`generate-ai-release-notes.ts` polishes them, **`write-changelog.ts`**
prepends them as this version's section, `create-pull-request` commits
the changelog (a tracked file, so `git add -A` always stages it), and
**`extract-release-notes.ts`** reads that section back in the publish
job as the GitHub Release body.
The changelog is therefore both the durable record and the review
surface: edit the top section on the release PR to change what ships.
`release-notes.md` goes back to being gitignored scratch, so the same
notes never exist as two editable copies with no rule about which one
wins.
**2. The 29 stale changelogs are deleted**, and a test pins the tracked
changelog set to exactly the three lane files, so they cannot creep back
and contradict the real versions again. Their content stays recoverable
from git history (`git show v1.69.3:packages/core/CHANGELOG.md`).
**3. Notes are selected per PR, scoped to the lane.** Selection was
`--no-merges` over every commit since the scope's tag. Two bugs:
- *No path filter* — a scope inherited every other lane's work.
- *`--no-merges` is backwards here* — this repo merges PRs as merge
commits, so the merge **is** the unit of change and the only commit
carrying `(#1234)`. `--no-merges` dropped every PR boundary and kept the
intermediate branch commits.
Now: `--first-parent` over the scope's package directories, minus
commits no consumer would read about (`test`/`ci`/`style`, `chore`
except `chore(deps)`, and the release commit itself).
| scope | before | after |
|---|---|---|
| `angular` v0.5.0 | 159 entries | **4** |
| `channels` (unreleased) | 600+ entries | **9** |
**4. Breaking-change footers still survive.** `--first-parent` alone
silently dropped `BREAKING CHANGE:` footers written on branch commits
rather than in the PR description — measured at **2 of 2 lost** across
`v1.60.0..HEAD`. Each merge's branch messages are now folded into its
body before extraction, so the entry list stays one-per-PR while the
footer scan sees the whole PR. Re-measured: **0 lost**.
**5. The AI prompt is scoped and the API call is correct.** It was
passing a repo-wide `git log -50` as "context" and asserting the release
was "CopilotKit vX.Y.Z, an open-source AI agent framework for React
applications" — wrong commits, wrong framing, and wrong release title
for any non-monorepo lane. Now it gets the lane's own commits, the names
of the packages actually being published, and an instruction to write
about nothing else. Also fixed in the same call: `max_tokens: 2048`
(truncates a large release mid-section, and the truncated text is what
ships as the body), and a response reader that took `content[0].text`
rather than selecting the text block by type. The model pin is left
alone — `main` already carries a current, undated id.
**6. Notion is removed**, not repaired — the release PR is already the
review surface.
### Failure behavior on the publish side
`extract-release-notes.ts` runs **after** `npm publish`, so it never
exits non-zero: failing there would leave the packages published and the
tag unpushed. A missing section prints a `::error::` annotation and
falls through to the workflow's existing `Release <tag>` fallback. Worst
case is the blank body we have today, never a half-finished release.
## Testing
Baseline on `main`: `15 files / 162 tests`. On this branch: **`16 files
/ 197 tests`**.
```
$ npx vitest run --config scripts/release/vitest.config.mts
Test Files 16 passed (16)
Tests 197 passed (197)
```
**The whole lane round-trips end to end.** A real `prepare-release.ts
--scope channels --bump minor` run (versions reverted afterward), then
the two new halves:
```
$ pnpm tsx scripts/release/write-changelog.ts 0.10.0 channels
Recorded 0.10.0 in packages/channels/CHANGELOG.md
$ rm release-notes.md
$ pnpm tsx scripts/release/extract-release-notes.ts 0.10.0 channels
Release body written to release-notes.md from packages/channels/CHANGELOG.md (861 chars)
```
The extracted body is the 9 PR-numbered entries under Features / Fixes /
Other, with **no duplicated version heading** (`grep -c '^## '
release-notes.md` → `0`) — the raw generator's own `## v0.10.0
(channels)` line is stripped when the section heading is written. The
miss path was exercised too:
```
$ pnpm tsx scripts/release/extract-release-notes.ts 9.9.9 channels
::error title=Release notes::No section for 9.9.9 in packages/channels/CHANGELOG.md. ...
exit: 0
```
**The staging behavior is verified against the pinned action, not
assumed.** `peter-evans/create-pull-request@5f6978f` stages with `git
add -A` when `add-paths` is unset. In-repo, after a real notes run:
```
$ git add -A --dry-run | grep -iE "changelog|release-notes"
add 'packages/channels/CHANGELOG.md'
$ git check-ignore -v release-notes.md
.gitignore:77:release-notes.md release-notes.md
```
The changelog is staged; the scratch file is invisible to the commit.
The publish job checks out `ref: main` at `fetch-depth: 0`, and the
release PR merges the changelog into main, so the section is present
when the extractor runs.
**The selection reproduces a hand-curated list exactly.**
`angular/v0.5.0`'s release body was written by hand from its four real
PRs. Running the new selection over that same range returns exactly
those four, release commit correctly dropped:
```
#6098 feat(runtime): use managed Intelligence authority (#6098)
#6756 chore(deps): bump @ag-ui/* to 0.0.59 (#6756)
#6773 feat(angular): add registerComponent ... (refs OSS-1034) (#6773)
#6586 fix(angular): resolve human-in-the-loop results without the bus envelope (#6586)
```
**Breaking-change regression measured, not assumed** — differential
comparison of extracted notes, old selection vs new, over two ranges:
```
range v1.60.0..HEAD old: 2 new: 2 LOST: 0
range v1.50.0..HEAD old: 2 new: 2 LOST: 0
```
(Before the fold was added, the same probe reported `LOST: 2` — that is
how the bug was caught.)
**Every new test was mutation-checked** — the mechanism was broken and
the test confirmed failing:
| mutation | result |
|---|---|
| `--first-parent` → `--no-merges` | 2 failed |
| drop the pathspec filter | 2 failed |
| `isNoiseCommit` always false | 2 failed |
| `parsePrNumber` always null | 2 failed |
| `withBranchMessages` → no-op | 1 failed |
| code-fence tracking disabled | 1 failed |
| `stripVersionHeading` → no-op | 3 failed |
| `prependSection` appends instead | 1 failed |
| `extractSection` keeps the heading | 5 failed |
| `upsertSection` stops replacing | 1 failed |
| re-ignore a lane changelog | 1 failed |
| re-ignore all `packages/*/CHANGELOG.md` | 2 failed |
| an orphan changelog creeps back | 1 failed |
| a lane changelog goes missing | 1 failed |
| *(restored)* | **all green** |
One of those mutations found a bug **in the test itself**: `git
check-ignore <path>` reports nothing for a path that is already tracked,
so the ignore assertion passed against a rule that would still strand
the next lane's file. It now runs `git check-ignore --no-index`, and the
mutation fails as it should. The flagless form is why the row above
exists at all.
Also run: `verify-release-scope-dropdowns.sh` (all OK), YAML parse of
both edited workflows, `oxfmt` (no-op after formatting), `oxlint` (0
warnings, 44 files).
**Not verified:** the live Claude API call. No `ANTHROPIC_API_KEY` was
available locally, so only the no-key fallback path (raw changelog) and
the CLI arg validation were exercised. A generation failure is already
caught and falls back to the raw notes, so the worst case is un-polished
notes rather than a blank body.
The commit is `--no-verify`: the pre-commit nx lane cannot run in this
worktree (`packages/core` and `packages/channels-ui` have no
`node_modules`, and `nx run @copilotkit/core:build` fails identically
with the tree clean). The only change under `packages/**` is deleting
orphan markdown that no build or test reads. CI on this PR runs the real
lane.
## Not in this PR
Slack-side drafting/massaging in a dedicated channel, with write-back to
the release body. Deliberately separate — that lane needs its own
channel and webhook, and must not run through `#engr`. The `notify` job
and the `#engr` announcement are untouched here.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Release notes are now organized by release lane and recorded in
dedicated changelogs.
* GitHub Releases can automatically use the matching lane changelog
section.
* Release notes are scoped to packages included in each release lane.
* **Documentation**
* Added guidance for supported release lanes and changelog workflows.
* **Changes**
* Historical package and example changelog entries were removed or
replaced with the lane-based format.
* Notion-based release-note drafting and PR links are no longer used.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@copilotkit/web-inspector
Trusted project context
The Web Inspector reads optional InspectorMetadataV1 data from
@copilotkit/core. It parses the value again at the UI boundary and renders each
valid module on its own:
identityshows the organization and project on the Home project card.planshows the plan label on Home and in the Threads footer.actioncan show one trusted link in the Inspector sidebar, in the Threads footer, or in the locked Threads view.usageshows trusted Thread counts on Home and detailed usage and expiry data in the Threads footer.
Missing or invalid metadata hides only the affected trusted module. Home still renders its project, runtime, services, and What's New preview with safe empty states. The existing debug views and Threads endpoint behavior remain available. A licensed Runtime without Threads endpoints offers a static, docs-backed coding-agent prompt and links to the public route setup guide.
The footer sits at the bottom of the Threads list sidebar. It stays out of the account strip, other navigation groups, and Settings. Usage and the footer action render on their own, so either module can appear without the other.
Home is the first pane on a new or upgraded installation. Later opens restore the last selected pane. The live sidebar groups navigation into Home and What's New, Workbench (Threads and Memory), and Inspect (Agent, AG-UI Events, optional Frontend Tools and Capabilities, and Context). Its Talk to an Engineer link stays in the footer, followed by Intelligence and live Runtime connection status. Home previews the latest update and opens the dedicated What's New pane. Docked-left and narrow layouts use a compact icon rail; wider layouts can also be collapsed manually. A top-right light/dark theme control follows the Inspector between sessions without changing the host application's theme. Unread announcements animate the closed launcher, appear as a Home preview, and mark the What's New sidebar entry until the update is opened.
Metadata is display-only: it never authorizes or gates Thread work. Core starts
real Thread work only for object-valued threadEndpoints with list !== false.
Absent endpoints, literal false, or an endpoint object with list: false
produce zero list, subscribe, inspect, messages, events, and state requests.
License and action matrix
| Effective license state | Threads footer | Locked Threads view |
|---|---|---|
valid |
Shows Manage Your Plan below 90% finite usage and a purple Upgrade Your Plan at 90% or higher for a trusted manage_plan action |
Copies a coding-agent repair prompt and links to the Rich Threads route setup guide when the Runtime has no Threads endpoints |
none |
No footer action | Shows Enable Intelligence only for a trusted enable_intelligence action |
expired |
No footer action | Shows Renew for renew, or Manage Your Plan for manage_plan |
unknown |
No footer action | Uses neutral unavailable copy with no action |
Finite usage shows used / limit Threads with a native progress bar. The bar is
green below 90%, orange from 90% up to the limit, and red at or above the limit.
At 90%, a trusted manage_plan footer link changes from Manage Your Plan to
the purple Upgrade Your Plan action without changing its URL or action kind. An overage shows
limit+ / limit Threads and caps the bar at 100%. Unlimited limits use text
only. An unknown limit shows the trusted used count with Limit unavailable;
it invents neither a numeric limit nor progress. A known zero expiry count stays
visible; missing or malformed expiry data stays hidden.
Expiring Soon describes a future retention-policy threshold in the next 24
hours. The Inspector does not enforce retention, lock or delete Threads, or run
the thread culler.
Managed Enterprise metadata has no manage-plan action, and Team Self-Hosted metadata has no hosted action. Any supplied action must match the effective license state and action kind in the matrix above.
The Inspector compares metadata license state with licenseStatus from the
runtime-info response. If both are known and disagree, it uses the Runtime
status for copy and hides the action. This avoids sending a user to an action
that does not match the runtime's current state without hiding valid usage.
Every action opens the exact URL accepted by the shared parser. The Inspector does not add query parameters, derive URLs from names or IDs, or provide a hard-coded signup fallback for the locked Threads metadata action.
Thread selection stays unchanged
Metadata arrival, refresh, failure, and removal do not select or reselect a thread. The Inspector keeps the existing selected row and detail view.
Mixed versions
| Combination | Result |
|---|---|
| Old producer with new Shared and Runtime | V1 usage remains valid without expiringSoonCount; expiry stays absent. |
| New producer with pre-expiry Shared or Runtime | The older consumer ignores or removes the additive expiry leaf and keeps valid base V1 usage. |
| Old App API with new Runtime | The provider 404 becomes a private 204; Core stays connected and metadata stays absent. |
| New App API with old Runtime | The Runtime makes no metadata request, and the current Inspector behavior stays unchanged. |
| New Runtime or Core with old Inspector | The old Inspector ignores metadata it does not render. |
| New Inspector with old Core or Runtime | The Inspector feature-detects support and renders the safe missing-metadata fallback. |
These combinations do not require synchronized deployment. Roll out the
Intelligence producer first, then release each consumer when ready. Explicit
threadEndpoints remain the authority in every mix; metadata never enables
Thread work, and a license conflict suppresses an incompatible action without
suppressing valid usage.
Privacy allowlist
The UI may render only the parsed organization name, project name, plan label,
license bucket, action kind, trusted action URL, and trusted Thread usage fields:
used count, limit kind and value, and expiry count. Metadata telemetry is
coarse: its feature-specific properties may include only module,
action_kind, license_bucket, usage_bucket, expiry_bucket, group_key,
leaf_key, and action_placement. It must never copy exact usage, limits,
expiry counts, content, names, URLs, or Thread, agent, message, account, project,
or other product IDs into those events. It retains only the anonymous
identifiers already used by Inspector telemetry.
The usage UI does not add usage impressions or values to telemetry. The trusted metadata footer action remains visible only on Threads. The existing metadata action impression and click events keep their coarse allowlist.