diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47b3a42d..2a91837a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,22 @@ new version heading in the same commit.
## [Unreleased]
+## [0.433.0] - 2026-09-10
+### Added
+- **What's new — a user-facing feed generated from this changelog.** The changelog is written for the
+ people who build Agentric; the people who use it need one sentence of "you can now …" and where to try
+ it. An entry now opts in with a `**For users:**` (or `**For admins:**`) line as the last line of its
+ bullet, and `src/edge/whats-new.ts` parses those lines into a **What's new** link under the version in
+ the console sidebar: a dialog grouped by release, with a per-member unread badge (the version they last
+ opened it at, stored in `settings`; a member who never opened it sees the last 14 days as new), "New"
+ highlights, admin-only entries filtered by role, and console links in an entry acting as its "try it".
+ Unreleased entries never show. One source of truth — a feature can't reach the feed without a changelog
+ entry, and the feed can't drift from what shipped. `GET /api/whats-new`, `POST /api/whats-new/seen`.
+ Backfilled for v0.430.0–v0.432.0; the convention is in CLAUDE.md → Versioning. Pinned by
+ `scripts/whats-new-test.cjs`.
+ **For users:** This panel. Each release, it lists what you can now do — with a link to try it. The
+ badge counts what's new since you last looked.
+
## [0.432.0] - 2026-09-10
### Added
- **`/agentric` helper commands — work the Tasks board from chat without starting an agent.** A small,
@@ -24,6 +40,9 @@ new version heading in the same commit.
needs the sender resolved to a member (identity map / ClickUp email); an unmapped sender is told how to
get linked instead of editing the shared board anonymously. Audited `chat.command`. Pinned by
`scripts/agentric-commands-test.cjs`.
+ **For users:** Work your tasks from Slack, Discord, Telegram or a ClickUp comment without starting an
+ agent: `/agentric tasks` lists yours, `/agentric task new
@agent` files one and hands it off,
+ and `/agentric status`, `done` and `reopen` update one. Type `/agentric help` to see them all.
## [0.431.0] - 2026-09-10
### Added
@@ -40,6 +59,9 @@ new version heading in the same commit.
so a racing duplicate webhook re-reads the task rather than filing a second one. Audited
`clickup.task.linked` / `clickup.task.discussed` / `clickup.task.failed`. Pinned by
`scripts/clickup-task-bridge-test.cjs`; design in `docs/clickup-task-bridge-plan.md`.
+ **For users:** Comment `/agentric ` on a ClickUp ticket and it gets ONE Agentric task, titled
+ `#`; every later `/agentric` comment adds to that same task. `/agentric
+ ` also puts that agent on it, and reopens it if it was done.
### Fixed
- **`/agentric ` only worked in Slack.** The roster reply tells people to type `/agentric …`
@@ -47,6 +69,7 @@ new version heading in the same commit.
ClickUp the namespace wasn't recognised, so the message was read as addressing an agent called
`agentric` and answered with "I don't have an agent named `agentric`". The chat normaliser now strips
`/agentric` alongside `/agent-os` / `/agentos` (`CHAT_NAMESPACE_RE`, shared with the ClickUp ingress).
+ **For users:** `/agentric ` now works in Discord, Telegram and ClickUp, not just Slack.
## [0.430.0] - 2026-09-10
### Added
@@ -56,14 +79,21 @@ new version heading in the same commit.
lens (`Anyone · I filed · People · Agents`, each with a facet count), names the filer on the card and in
the list row whenever that isn't the assignee, lists **Group: Filed by** in the list view, and stamps
`filed by …` in the task room's meta line.
+ **For users:** Find your own tasks fast: the Tasks board's new **Filed by** filter has **I filed**,
+ **People** and **Agents** — pair **I filed** with **Done** to pick finished work back up. Cards now say
+ who filed each task. [Open Tasks](#/tasks)
- **Tasks: a top-level lens.** A one-click *Top level* filter hides every task with a parent — the
decomposition an agent split out of its own work — leaving the work somebody actually asked for. Shown
only when the board holds sub-tasks, with a count of what it would hide. Board cards now also show a
`↳ ` chip linking to the task they were split from.
+ **For users:** Hide the sub-tasks agents split out of their work with the **Top level** filter; a
+ sub-task's card links back to its parent with `↳`. [Open Tasks](#/tasks)
- Creating a task **opens it**. The create form used to drop you back on the board with the new card
somewhere in it; you then had to find your own task to add detail or dispatch it. It now navigates to
`#/tasks/` — the room, where the next action is. (Board is refreshed first, so closing the room
lands on a list that already holds the card.)
+ **For users:** Creating a task now takes you straight into it, ready to add detail or hand it to an
+ agent.
### Fixed
- **List view: `Group: Chain` could not be selected.** The mode was implemented and named in the group
diff --git a/CLAUDE.md b/CLAUDE.md
index 61868bfb..da18ece0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -751,6 +751,13 @@ a new version heading in the same commit. The sidebar version therefore tells yo
build a long-running server is holding in memory — the first thing to check when a change "isn't
taking".
+**What's new is generated from the changelog.** An entry a USER would notice also carries a
+`**For users:**` line (or `**For admins:**` for owner/admin-only features) as the last line of its
+bullet: one or two sentences of "you can now …", in plain words, optionally ending with a console link
+(`[Open Tasks](#/tasks)`). `src/edge/whats-new.ts` parses those lines into the console's **What's new**
+panel (sidebar, per-member unread badge). Internal fixes and refactors get no tag — the feed is only
+useful while it stays short. Entries under **Unreleased** never show; they appear once the version ships.
+
**Bump with `npm version`, never by hand-editing `package.json`.** The lockfile also records the root
version (in `.version` AND `.packages[""].version`), and npm rewrites both on the next `npm install`.
So a hand bump leaves `package-lock.json` behind, and the drift surfaces on the DEPLOY box:
diff --git a/package-lock.json b/package-lock.json
index bbd3a087..6962eb9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "agent-os",
- "version": "0.432.0",
+ "version": "0.433.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agent-os",
- "version": "0.432.0",
+ "version": "0.433.0",
"license": "MIT",
"bin": {
"agent-os": "bin/agent-os"
diff --git a/package.json b/package.json
index b53e6c4d..4f974af6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "agent-os",
- "version": "0.432.0",
+ "version": "0.433.0",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
@@ -27,7 +27,7 @@
"check-deps": "bash scripts/install-deps.sh --check",
"dev": "ts-node src/cli.ts serve",
"demo:dev": "ts-node src/demo.ts",
- "test:governance": "node scripts/version-sync-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs",
+ "test:governance": "node scripts/version-sync-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs",
"test:alert-staleness": "node scripts/alert-staleness-test.cjs",
"test:deps": "node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-login-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs",
"test:dm-continuity": "node scripts/dm-continuity-test.cjs",
diff --git a/scripts/whats-new-test.cjs b/scripts/whats-new-test.cjs
new file mode 100644
index 00000000..5ab94052
--- /dev/null
+++ b/scripts/whats-new-test.cjs
@@ -0,0 +1,81 @@
+/* "What's new" — the user-facing feed generated from CHANGELOG.md's `**For users:**` lines
+ * (src/edge/whats-new.ts).
+ *
+ * Pins the parse (a tag runs to the end of its bullet, Unreleased never shows, `**For admins:**` is
+ * role-scoped, untagged entries never leak), the per-member unseen rule (newer than the version you last
+ * opened it at; a first visit sees only the recent window), and that the REAL changelog yields the
+ * backfilled entries — so a reformat of CHANGELOG.md that silently empties the feed fails the gate. */
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+const HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'aos-whatsnew-test-'));
+process.env.AGENT_OS_HOME = HOME;
+process.env.AGENT_OS_TENANT = 'testco';
+process.env.AOS_NO_TTYD = '1';
+
+let pass = 0, fail = 0;
+const assert = (c, name, d) => c ? (pass++, console.log(` \x1b[32m✓\x1b[0m ${name}`)) : (fail++, console.log(` \x1b[31m✗ ${name}\x1b[0m${d ? ' — ' + d : ''}`));
+
+const wn = require(path.join(ROOT, 'dist/edge/whats-new.js'));
+
+console.log('\nParse');
+const md = `# Changelog
+
+## [Unreleased]
+### Added
+- **Half-built thing.** Dev prose.
+ **For users:** This must NOT show — it hasn't shipped.
+
+## [2.1.0] - 2026-09-10
+### Added
+- **Filed-by lens.** Long developer prose about createdBy
+ that wraps over two lines.
+ **For users:** Filter the board by who filed a task —
+ pick **I filed**. [Open Tasks](#/tasks)
+- **Internal refactor.** No tag, so it never reaches users.
+- **Settings knob.** Dev prose.
+ **For admins:** A new cap in Settings → Runtime.
+### Fixed
+- **A fix.** Dev prose.
+ **For users:** Chat commands work outside Slack.
+
+## [2.0.0] - 2026-08-01
+### Added
+- **Old thing.** Prose.
+ **For users:** An older feature.
+`;
+const e = wn.parseWhatsNew(md);
+assert(e.length === 4, 'four tagged entries, none from Unreleased or untagged bullets', JSON.stringify(e.map((x) => x.text)));
+assert(e[0].version === '2.1.0' && e[0].date === '2026-09-10', 'carries its release version + date');
+assert(e[0].text === 'Filter the board by who filed a task — pick **I filed**. [Open Tasks](#/tasks)', 'a tag runs over its indented continuation lines, markdown kept', e[0].text);
+assert(!e.some((x) => /must NOT show|No tag|Long developer prose/.test(x.text)), 'developer prose and untagged entries never leak');
+assert(e[1].audience === 'admins' && e[0].audience === 'all', '`**For admins:**` is admin-scoped');
+assert(e[2].text === 'Chat commands work outside Slack.', 'a heading ends the previous tag');
+assert(e[3].version === '2.0.0', 'older releases follow, newest first');
+
+console.log('\nUnseen');
+assert(wn.compareVersions('0.432.0', '0.431.9') === 1 && wn.compareVersions('0.99.0', '0.100.0') === -1, 'versions compare numerically, not as strings');
+assert(wn.unseen(e, '2.0.0').length === 3, 'everything released after the version you last opened it at');
+assert(wn.unseen(e, '2.1.0').length === 0, 'nothing once you have seen the current release');
+const now = Date.parse('2026-09-12');
+assert(wn.unseen(e, null, now).length === 3, 'a first visit counts only the recent window — not all of history');
+
+console.log('\nThe real changelog');
+const real = wn.whatsNew();
+const versions = new Set(real.map((x) => x.version));
+assert(['0.430.0', '0.431.0', '0.432.0', '0.433.0'].every((v) => versions.has(v)), 'the backfilled releases are in the feed', [...versions].slice(0, 8).join(', '));
+assert(real.some((x) => /Filed by/.test(x.text)) && real.some((x) => /\/agentric help/.test(x.text)), 'with their plain-language lines');
+assert(real.every((x) => x.text.length < 600), 'every entry is short — it is a feed, not release notes', String(Math.max(...real.map((x) => x.text.length))));
+
+console.log('\nPer-member seen marker');
+const { loadAgentOS } = require(path.join(ROOT, 'dist/kernel.js'));
+const aos = loadAgentOS();
+assert(aos.settings.whatsNewSeen('m1') === null, 'never opened → null');
+aos.settings.setWhatsNewSeen('m1', '0.433.0');
+assert(aos.settings.whatsNewSeen('m1') === '0.433.0' && aos.settings.whatsNewSeen('m2') === null, 'stored per member');
+
+console.log(`\n${pass} passed, ${fail} failed`);
+fs.rmSync(HOME, { recursive: true, force: true });
+process.exit(fail ? 1 : 0);
diff --git a/src/edge/whats-new.ts b/src/edge/whats-new.ts
new file mode 100644
index 00000000..81cab9d7
--- /dev/null
+++ b/src/edge/whats-new.ts
@@ -0,0 +1,108 @@
+/**
+ * "What's new" — the user-facing feed, GENERATED from `CHANGELOG.md` rather than kept beside it.
+ *
+ * The changelog is written for the people who build Agentric: each entry explains the bug, the mechanism
+ * and the trap. That is the wrong register for the people who USE it, who need one sentence of "you can
+ * now …" and where to try it. So an entry opts into the feed with a tagged line, written in the same PR as
+ * the entry itself, and this module turns those lines into the feed:
+ *
+ * ## [0.430.0] - 2026-09-10
+ * ### Added
+ * - **Tasks: who filed it.** `createdBy` has always been stored… (developer prose)
+ * **For users:** Filter the Tasks board by who filed a task — pick **I filed** to find everything
+ * you asked for. [Open Tasks](#/tasks)
+ *
+ * `**For users:**` reaches everyone; `**For admins:**` only owners and admins. The tag runs to the end of
+ * its bullet (continuation lines are the indented ones that follow). Markdown is kept — links into the
+ * console (`#/tasks`) are the "try it" button. Entries under **Unreleased** are never shown: nothing in
+ * them is running yet.
+ *
+ * One source of truth: a feature can't reach the feed without a changelog entry, and the feed can't
+ * drift from what shipped. The file is read once per process — it only changes on a deploy, which
+ * restarts the server.
+ */
+import * as fs from 'fs';
+import * as path from 'path';
+
+export interface WhatsNewEntry {
+ /** `:` — stable for a given changelog, used as a React key. */
+ id: string;
+ version: string;
+ /** The release date from the heading (`YYYY-MM-DD`), when it has one. */
+ date?: string;
+ /** Markdown, as written after the tag. */
+ text: string;
+ audience: 'all' | 'admins';
+}
+
+const VERSION_HEADING = /^##\s+\[(\d+\.\d+\.\d+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/;
+const ANY_HEADING = /^#{1,3}\s/;
+const TAG = /^\s*(?:[-*]\s+)?\*\*For (users|admins):\*\*\s*(.*)$/i;
+const BULLET = /^\s{0,1}[-*]\s/;
+
+/** Pull every tagged line out of a changelog, newest release first (the file's own order). */
+export function parseWhatsNew(md: string): WhatsNewEntry[] {
+ const out: WhatsNewEntry[] = [];
+ let version: string | null = null;
+ let date: string | undefined;
+ let open: WhatsNewEntry | null = null;
+ let n = 0;
+ const close = () => { if (open) { open.text = open.text.trim(); if (open.text) out.push(open); open = null; } };
+
+ for (const line of md.split('\n')) {
+ const vh = line.match(VERSION_HEADING);
+ if (vh || ANY_HEADING.test(line)) {
+ close();
+ if (vh) { version = vh[1]; date = vh[2]; n = 0; } else if (/^##\s/.test(line)) { version = null; } // `## [Unreleased]`
+ continue;
+ }
+ const tag = line.match(TAG);
+ if (tag) {
+ close();
+ if (version) open = { id: `${version}:${n++}`, version, date, text: tag[2], audience: tag[1].toLowerCase() === 'admins' ? 'admins' : 'all' };
+ continue;
+ }
+ if (!open) continue;
+ // A continuation is an indented, non-empty line that doesn't start the next bullet.
+ if (!line.trim() || BULLET.test(line) || !/^\s/.test(line)) { close(); continue; }
+ open.text += ' ' + line.trim();
+ }
+ close();
+ return out;
+}
+
+/** -1 / 0 / 1 for dotted numeric versions. Anything unparsable sorts as 0.0.0. */
+export function compareVersions(a: string, b: string): number {
+ const pa = a.split('.').map((x) => parseInt(x, 10) || 0);
+ const pb = b.split('.').map((x) => parseInt(x, 10) || 0);
+ for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) < (pb[i] ?? 0) ? -1 : 1;
+ return 0;
+}
+
+/** How long a member who has NEVER opened the feed treats an entry as new. Without a bound, the first
+ * visit would badge every tagged entry in history; with one, it badges what shipped recently. */
+export const FIRST_VISIT_WINDOW_DAYS = 14;
+
+/**
+ * Entries this member hasn't seen. `seen` is the version they last opened the feed at: everything
+ * released after it is new. Never opened → whatever shipped in the last {@link FIRST_VISIT_WINDOW_DAYS}.
+ */
+export function unseen(entries: WhatsNewEntry[], seen: string | null, now = Date.now()): WhatsNewEntry[] {
+ if (seen) return entries.filter((e) => compareVersions(e.version, seen) > 0);
+ const since = now - FIRST_VISIT_WINDOW_DAYS * 86_400_000;
+ return entries.filter((e) => e.date && Date.parse(e.date) >= since);
+}
+
+let cache: WhatsNewEntry[] | null = null;
+
+/** The running build's feed, parsed from the `CHANGELOG.md` shipped beside it (dist/edge → repo root).
+ * A missing or unreadable file is an empty feed, never a boot failure. */
+export function whatsNew(): WhatsNewEntry[] {
+ if (cache) return cache;
+ try {
+ cache = parseWhatsNew(fs.readFileSync(path.join(__dirname, '..', '..', 'CHANGELOG.md'), 'utf8'));
+ } catch {
+ cache = [];
+ }
+ return cache;
+}
diff --git a/src/governance/settings.ts b/src/governance/settings.ts
index fefbef21..0ed424e1 100644
--- a/src/governance/settings.ts
+++ b/src/governance/settings.ts
@@ -151,6 +151,14 @@ export class SettingsStore {
.run(key, value, Date.now(), by ?? null);
}
+ /** The version a member last opened "What's new" at (null = never) — what separates new from seen. */
+ whatsNewSeen(memberId: string): string | null {
+ return this.getRow(`whats_new_seen:${memberId}`)?.value || null;
+ }
+ setWhatsNewSeen(memberId: string, version: string): void {
+ this.set(`whats_new_seen:${memberId}`, version, memberId);
+ }
+
/** The Company context document + the code-review policy + who last touched each. */
company(): CompanySettings {
const row = this.getRow(COMPANY_KEY);
diff --git a/src/server.ts b/src/server.ts
index b414738f..c0032218 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -46,6 +46,7 @@ import { checkClaudeToken, credentialDirHasLogin, readConfigDirToken, keychainHa
import { refreshStaleUsage } from './edge/runtime-account-usage';
import { runtimePresence, installRuntime } from './edge/runtime-install';
import { ClickupIngress } from './edge/clickup-ingress';
+import { whatsNew, unseen } from './edge/whats-new';
import { DiscordSocket } from './edge/discord-socket';
import { TelegramSocket } from './edge/telegram-socket';
import { AppSupervisor } from './edge/app-supervisor';
@@ -2516,6 +2517,18 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req:
// Host pressure for the sidebar chip (see host-metrics.ts). Deliberately tiny and DB-free: it is polled
// on a timer by every open console tab, and the whole point is to stay honest when the box is struggling.
if (method === 'GET' && p === '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/api/host') return sendJson(res, 200, hostMetrics());
+ // "What's new" — the user-facing feed generated from CHANGELOG.md's `**For users:**` / `**For admins:**`
+ // lines (src/edge/whats-new.ts). Admin-only entries are filtered by role; `unseen` is per member.
+ if (method === 'GET' && p === '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/api/whats-new') {
+ const admin = me.role === 'owner' || me.role === 'admin';
+ const entries = whatsNew().filter((e) => admin || e.audience === 'all').slice(0, 40);
+ const seen = os.settings.whatsNewSeen(me.id);
+ return sendJson(res, 200, { entries, unseen: unseen(entries, seen).map((e) => e.id), seen, version: VERSION });
+ }
+ if (method === 'POST' && p === '/api/whats-new/seen') {
+ os.settings.setWhatsNewSeen(me.id, VERSION);
+ return sendJson(res, 200, { ok: true, seen: VERSION });
+ }
if (method === 'GET' && p === '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/api/state') {
// Members see only the agents they're allowed to run; owner/admin see all.
const agents = terminalAgents(os).filter((a) => os.team.canRun(me, a.id));
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 83f4b2b9..0e67e6d8 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -14,7 +14,7 @@ import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
-import { api, isDraftTask, EFFORTS, PERMISSION_MODES, type PermissionMode, type StateResp, type HostMetrics, type RequestMetricsSnapshot, type AgentInfo, type Session, type Msg, type Member, type Role, type TeamResp, type AgentAccess, type MemberIdentity, type IdentityProvider, IDENTITY_PROVIDERS, type Automation, type Task, type TaskEvent, type TaskAttachment, type TaskChild, type TaskRun, type TaskPr, type TaskPrSummary, type TaskWorkers, type TaskTimelineEntry, type TaskDiscussionSummary, type TaskDiscussionDelivery, type TaskStatus, type AddTaskReq, type Goal, type GoalEvent, type GoalStatus, type GoalCounts, type GoalProgress, type AddGoalReq, type MemoryRecord, type MemoryHealth, type MemoryBackend, type MemorySettings, type MemorySettingsReq, type OllamaStatus, type KbPage, type KbRevision, type AgentRevision, type AgentStats, type AgentProposalTrust, type Recommendation, type DigestConfig, type DigestModel, type DreamingState, type Measurement, type Insights, type ImprovementTile, type MemoryCleanupPlan, type KbTidyPlan, type TaskReconcilePlan, type LibraryTidyPlan, type SessionTidyPlan, type StuckGoal, type TroubledAutomation, type PolicyDocument, type PolicyRule, type PolicyOutcome, type PolicyOp, type PolicyProposal, type PolicyRevision, type PolicyDrift, type AutomationProposal, type AgentUpdateProposal, type GoalUpdateProposal, type DirListing, type FileEntry, type FileContent, type Artifact, type AppInfo, type AppFile, type AppCapabilities, type SkillSummary, type SkillsResp, type CatalogSkill, type CatalogAgent, type SkillSource, type RemoteSkill, type SkillshHit, type SkillRequest, type SecretRequest, type IntegrationsResp, type SlackStatus, type DiscordStatus, type TelegramStatus, type AuditEvent, type Effort, type RuntimeTuning, type RuntimeTuningPatch, type OutputStylesResp, type OutputStyleAdoption, type Concurrency, type RuntimeAccount, type RuntimeAccountKind, type RuntimeAccountsResp, type RuntimePresence, type RuntimeLogin, type SecretMeta, type UpdateStatus, type UpdateApplyResult, type UpdateWatchConfig, type UpdateWatchMode, type ActivityEvent, type ActivitySummaryRow, type SystemMetrics, type DepsReport, type DepStatus, type DepsInstallResult, type ChatTurn, type ChatArtifactRef, type ChatKbRef, type ChatAppRef, type RouterPreviewResp, type RouterCard, type SessionChain, type ChainNode, type ChainPending, type SessionProgress } from '@/lib/api'
+import { api, isDraftTask, EFFORTS, PERMISSION_MODES, type PermissionMode, type StateResp, type HostMetrics, type RequestMetricsSnapshot, type AgentInfo, type Session, type Msg, type Member, type Role, type TeamResp, type AgentAccess, type MemberIdentity, type IdentityProvider, IDENTITY_PROVIDERS, type Automation, type Task, type TaskEvent, type TaskAttachment, type TaskChild, type TaskRun, type TaskPr, type TaskPrSummary, type TaskWorkers, type TaskTimelineEntry, type TaskDiscussionSummary, type TaskDiscussionDelivery, type TaskStatus, type AddTaskReq, type Goal, type GoalEvent, type GoalStatus, type GoalCounts, type GoalProgress, type AddGoalReq, type MemoryRecord, type MemoryHealth, type MemoryBackend, type MemorySettings, type MemorySettingsReq, type OllamaStatus, type KbPage, type KbRevision, type AgentRevision, type AgentStats, type AgentProposalTrust, type Recommendation, type DigestConfig, type DigestModel, type DreamingState, type Measurement, type Insights, type ImprovementTile, type MemoryCleanupPlan, type KbTidyPlan, type TaskReconcilePlan, type LibraryTidyPlan, type SessionTidyPlan, type StuckGoal, type TroubledAutomation, type PolicyDocument, type PolicyRule, type PolicyOutcome, type PolicyOp, type PolicyProposal, type PolicyRevision, type PolicyDrift, type AutomationProposal, type AgentUpdateProposal, type GoalUpdateProposal, type DirListing, type FileEntry, type FileContent, type Artifact, type AppInfo, type AppFile, type AppCapabilities, type SkillSummary, type SkillsResp, type CatalogSkill, type CatalogAgent, type SkillSource, type RemoteSkill, type SkillshHit, type SkillRequest, type SecretRequest, type IntegrationsResp, type SlackStatus, type DiscordStatus, type TelegramStatus, type AuditEvent, type Effort, type RuntimeTuning, type RuntimeTuningPatch, type OutputStylesResp, type OutputStyleAdoption, type Concurrency, type RuntimeAccount, type RuntimeAccountKind, type RuntimeAccountsResp, type RuntimePresence, type RuntimeLogin, type SecretMeta, type UpdateStatus, type UpdateApplyResult, type UpdateWatchConfig, type UpdateWatchMode, type ActivityEvent, type ActivitySummaryRow, type SystemMetrics, type DepsReport, type DepStatus, type DepsInstallResult, type ChatTurn, type ChatArtifactRef, type ChatKbRef, type ChatAppRef, type RouterPreviewResp, type RouterCard, type SessionChain, type ChainNode, type ChainPending, type SessionProgress, type WhatsNewEntry } from '@/lib/api'
import { type Branding, type PublicBranding, type NotificationPrefs, DEFAULT_NOTIFICATION_PREFS, type PromptShortcut, type SessionMetrics, type Brief, type AutoApproval, type FeedItem, type FeedResponse, type FeedFilter, type TaskRunState, type GoalChatState } from '@/lib/api'
import { applyAccent, applyFavicon, faviconDataUri, readableOn } from '@/lib/branding'
import { ENTITY_ID_SRC, entityHref, isEntityId } from '@/lib/entity-links'
@@ -1158,6 +1158,65 @@ function HostChip() {
)
}
+/**
+ * "What's new" — the sidebar link + dialog over the feed generated from CHANGELOG.md's `**For users:**`
+ * lines (`src/edge/whats-new.ts`). The badge counts what shipped since THIS member last opened it; opening
+ * marks it seen server-side, while the dialog keeps the "New" highlights until it closes. Renders nothing
+ * when the running build has no tagged entries. A console link in an entry (`#/tasks`) is its "try it".
+ */
+function WhatsNew() {
+ const [data, setData] = useState<{ entries: WhatsNewEntry[]; unseen: string[] } | null>(null)
+ const [open, setOpen] = useState(false)
+ useEffect(() => { api.whatsNew().then((r) => setData({ entries: r.entries ?? [], unseen: r.unseen ?? [] })).catch(() => {}) }, [])
+ if (!data || !data.entries.length) return null
+ const fresh = new Set(data.unseen)
+ const show = () => { setOpen(true); if (fresh.size) api.markWhatsNewSeen().catch(() => {}) }
+ const close = () => { setOpen(false); setData((d) => (d ? { ...d, unseen: [] } : d)) }
+ const groups: { version: string; date?: string; items: WhatsNewEntry[] }[] = []
+ for (const e of data.entries) {
+ const g = groups[groups.length - 1]
+ if (g && g.version === e.version) g.items.push(e)
+ else groups.push({ version: e.version, date: e.date, items: [e] })
+ }
+ return (
+ <>
+
+
+ >
+ )
+}
+
function UpdateNotice({ compact = false }: { compact?: boolean } = {}) {
const [status, setStatus] = useState(null)
const [open, setOpen] = useState(false)
@@ -1845,6 +1904,7 @@ function Console({ me }: { me: Member }) {
{state &&