zz-meridian 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +1 -1
  2. package/dist/adopt.js +6 -2
  3. package/dist/migrations.js +24 -0
  4. package/dist/ownership.js +2 -2
  5. package/package.json +1 -1
  6. package/payload/CHANGELOG.md +38 -0
  7. package/payload/CONTRIBUTING.md +2 -1
  8. package/payload/README.md +3 -2
  9. package/payload/app/(dashboard)/(overview)/README.md +18 -1
  10. package/payload/app/(dashboard)/(overview)/page.tsx +13 -6
  11. package/payload/app/(dashboard)/analytics/README.md +13 -1
  12. package/payload/app/(dashboard)/analytics/page.tsx +10 -4
  13. package/payload/app/(dashboard)/customers/README.md +12 -1
  14. package/payload/app/(dashboard)/health/README.md +9 -1
  15. package/payload/app/(dashboard)/health/page.tsx +6 -5
  16. package/payload/app/(dashboard)/keys/README.md +8 -1
  17. package/payload/app/(dashboard)/members/README.md +12 -1
  18. package/payload/app/(dashboard)/requests/README.md +15 -1
  19. package/payload/app/(dashboard)/requests/[id]/README.md +12 -1
  20. package/payload/app/(dashboard)/requests/[id]/page.tsx +3 -1
  21. package/payload/app/(dashboard)/requests/page.tsx +2 -2
  22. package/payload/app/(dashboard)/settings/README.md +7 -0
  23. package/payload/app/api/assistant/route.ts +23 -3
  24. package/payload/app/embed/health/README.md +6 -1
  25. package/payload/app/embed/health/page.tsx +14 -9
  26. package/payload/app/embed/health/view.tsx +4 -11
  27. package/payload/app/embed/overview/README.md +6 -1
  28. package/payload/app/embed/overview/page.tsx +13 -17
  29. package/payload/app/embed/overview/view.tsx +15 -30
  30. package/payload/app/embed/proposal/README.md +8 -1
  31. package/payload/app/embed/proposal/page.tsx +2 -1
  32. package/payload/app/embed/proposal/view.tsx +28 -16
  33. package/payload/app/embed/requests/README.md +6 -1
  34. package/payload/app/embed/requests/page.tsx +9 -7
  35. package/payload/app/embed/requests/view.tsx +9 -15
  36. package/payload/app/sign-in/README.md +10 -0
  37. package/payload/app/sign-in/actions.ts +19 -0
  38. package/payload/app/sign-in/gated-panel.tsx +9 -0
  39. package/payload/app/sign-in/page.tsx +4 -2
  40. package/payload/app/sign-in/panel.tsx +36 -5
  41. package/payload/app/system/(atlas)/[section]/[id]/page.tsx +23 -1
  42. package/payload/decisions/0010-the-adopter-contract.md +6 -4
  43. package/payload/decisions/0011-one-context-two-agents.md +36 -0
  44. package/payload/docs/agents.md +36 -13
  45. package/payload/docs/assistant.md +8 -6
  46. package/payload/docs/benchmark.md +18 -0
  47. package/payload/docs/distribution.md +9 -5
  48. package/payload/docs/surfaces.md +5 -4
  49. package/payload/docs/voice.md +1 -1
  50. package/payload/gitignore +10 -0
  51. package/payload/package.json +1 -1
  52. package/payload/proxy.ts +37 -0
  53. package/payload/scripts/assistant.ts +50 -5
  54. package/payload/scripts/check.ts +9 -5
  55. package/payload/scripts/fake-llm.ts +9 -0
  56. package/payload/skills/zz-meridian/references/agents.md +36 -13
  57. package/payload/skills/zz-meridian/references/cache.md +16 -3
  58. package/payload/skills/zz-meridian/references/customize.md +8 -2
  59. package/payload/skills/zz-meridian/references/ownership.md +1 -1
  60. package/payload/skills/zz-meridian/references/update.md +19 -0
  61. package/payload/skills/zz-meridian/references/voice.md +1 -1
  62. package/payload/src/components/base/shell/index.tsx +21 -5
  63. package/payload/src/components/base/surface/README.md +2 -1
  64. package/payload/src/components/base/surface/index.tsx +40 -7
  65. package/payload/src/components/base/use-share-view.ts +12 -8
  66. package/payload/src/components/charts/meridian/README.md +1 -0
  67. package/payload/src/components/charts/meridian/index.tsx +12 -2
  68. package/payload/src/components/charts/trend-chart/README.md +2 -0
  69. package/payload/src/components/charts/trend-chart/index.tsx +12 -2
  70. package/payload/src/components/charts/trend-chart/preview.tsx +12 -0
  71. package/payload/src/components/patterns/ask-about/README.md +5 -2
  72. package/payload/src/components/patterns/ask-about/index.tsx +9 -3
  73. package/payload/src/components/patterns/assistant/index.tsx +17 -3
  74. package/payload/src/components/patterns/filter-bar/README.md +2 -1
  75. package/payload/src/components/patterns/filter-bar/index.tsx +4 -0
  76. package/payload/src/components/patterns/metric-tile/README.md +5 -3
  77. package/payload/src/components/patterns/metric-tile/index.tsx +34 -2
  78. package/payload/src/components/patterns/metric-tile/preview.tsx +12 -0
  79. package/payload/src/data/access.ts +16 -3
  80. package/payload/src/data/collections.ts +85 -2
  81. package/payload/src/data/metrics.ts +64 -0
  82. package/payload/src/data/request-methods.ts +2 -0
  83. package/payload/src/data/requests.ts +22 -4
  84. package/payload/src/data/sample.ts +3 -3
  85. package/payload/src/lib/agent-guidance.ts +11 -0
  86. package/payload/src/lib/assistant/prompt.ts +25 -5
  87. package/payload/src/lib/assistant/respond.ts +21 -3
  88. package/payload/src/lib/assistant/tools.ts +40 -6
  89. package/payload/src/lib/demo-gate.ts +39 -0
  90. package/payload/src/lib/host.ts +1 -1
  91. package/payload/src/lib/insight.ts +81 -0
  92. package/payload/src/lib/shared-context.ts +104 -0
  93. package/payload/src/system/page-stage.tsx +24 -8
  94. package/payload/src/views/analytics-context.ts +79 -0
  95. package/payload/src/views/analytics.tsx +23 -17
  96. package/payload/src/views/customers-context.ts +72 -0
  97. package/payload/src/views/customers.tsx +6 -3
  98. package/payload/src/views/health-context.ts +53 -0
  99. package/payload/src/views/health.tsx +14 -3
  100. package/payload/src/views/keys-context.ts +38 -0
  101. package/payload/src/views/keys.tsx +3 -0
  102. package/payload/src/views/members-context.ts +40 -0
  103. package/payload/src/views/members.tsx +4 -0
  104. package/payload/src/views/overview-context.ts +179 -0
  105. package/payload/src/views/overview.tsx +44 -29
  106. package/payload/src/views/request-context.ts +46 -0
  107. package/payload/src/views/request.tsx +4 -1
  108. package/payload/src/views/requests-context.ts +85 -0
  109. package/payload/src/views/requests.tsx +12 -6
  110. package/payload/src/views/sample-footer.tsx +20 -7
  111. package/payload/src/views/settings.tsx +40 -2
  112. package/payload/src/views/tools.ts +142 -0
  113. package/payload/tests/agents.test.tsx +12 -0
  114. package/payload/tests/assistant-guard.test.ts +12 -0
  115. package/payload/tests/demo-gate.test.ts +64 -0
  116. package/payload/tests/embed-requests-provenance.test.tsx +4 -2
  117. package/payload/tests/empty-views.test.tsx +3 -3
  118. package/payload/tests/request-view-replay.test.tsx +1 -1
  119. package/payload/tests/shared-context.test.tsx +167 -0
package/README.md CHANGED
@@ -9,7 +9,7 @@ dashboard depends on it afterwards.
9
9
  Give your coding agent (Codex, Claude Code, or any agent that can run a shell) this, with what you want in your own
10
10
  words at the end:
11
11
 
12
- > Run `npx zz-meridian@latest skill --global`, then follow the zz-meridian skill it installs to: <what you want>.
12
+ > Run `npx zz-meridian@latest skill --global`, then follow the zz-meridian skill it installs to: [what you want, in your own words].
13
13
 
14
14
  The skill picks the command from what you said and what is in the folder, and names it before running anything:
15
15
  `adopt` to change this Next.js App Router project in place ("make this admin panel look professional"), `create` for a
package/dist/adopt.js CHANGED
@@ -258,8 +258,12 @@ Rename or move them, then run adopt again.`);
258
258
  writes.set(`${dir}/icon.ts`, exactImports(`${dir}/icon.ts`, readPayload('app/icon.ts'), known));
259
259
  const ignore = path.join(root, '.gitignore');
260
260
  const ignored = fs.existsSync(ignore) ? fs.readFileSync(ignore, 'utf8') : '';
261
- if (!/^\/?out\/?$/m.test(ignored))
262
- writes.set('.gitignore', `${ignored.trimEnd()}\n\n# zz-meridian: verify's report and the screenshots\n/out/\n`);
261
+ // verify's report and screenshots, and an update in progress: Meridian's own, never the team's to commit. The manifest,
262
+ // keep.json and history/ are committed, since the next update reads them.
263
+ const bare = (l) => l.trim().replace(/^\//, '').replace(/\/$/, '');
264
+ const unignored = ['/out/', '/.meridian/update/', '/.meridian/update.lock'].filter((l) => !ignored.split('\n').some((x) => bare(x) === bare(l)));
265
+ if (unignored.length)
266
+ writes.set('.gitignore', `${ignored.trimEnd()}\n\n# zz-meridian: verify's report and an update in progress\n${unignored.join('\n')}\n`);
263
267
  // ── Write: every target checked first, so a bad one stops it before anything changes ─────────────────
264
268
  try {
265
269
  for (const rel of writes.keys())
@@ -205,7 +205,31 @@ export const RELEASE_MIGRATIONS = [
205
205
  instructions: 'Read "Writes authorize, then invalidate" and "The assistant route" in references/cache.md. Call `resolveAccess()` first in the route and in each Server Action, answer 401 or an error result when there is no session, and hand the assistant only the collections the caller may read. The route passes a guard that authorizes each approved change again when it runs, so `src/lib/assistant/tools.ts` and `src/lib/assistant/respond.ts` take that guard too: bring them over from the release with the route.',
206
206
  checks: GATE_AND_BUILD,
207
207
  },
208
+ {
209
+ id: 'share-view-context',
210
+ since: '0.9.0',
211
+ applies: (root) => matching(root, (_, src) => callArity(src, 'useShareView').some((n) => n === 2)),
212
+ summary: 'useShareView takes one shared context, not a sentence and an object.',
213
+ instructions: 'Read "Legible" in references/agents.md and "A shared context" in references/customize.md. Build the view\'s context with the fields of `SharedContext` from `src/lib/shared-context.ts` (scope, freshness, each figure with its unit, change and definition, what code finds, what the data cannot say) in a pure function beside the view, and pass it alone: `useShareView(context)`. Both agents read it: an MCP host and the console\'s assistant.',
214
+ checks: GATE_AND_BUILD,
215
+ },
216
+ {
217
+ id: 'agent-reads-section',
218
+ since: '0.9.0',
219
+ applies: pagesWithoutAgentReads,
220
+ summary: 'A page under app/(dashboard)/ or app/embed/ says what the agent reads.',
221
+ instructions: 'Read "A page spec" in references/customize.md. Under `## Agents` in the page\'s README.md, add `### What the agent reads`: what the page\'s shared context tells each agent, with a real line of it, and what the MCP App and the console\'s assistant each receive. `scripts/check.ts` requires it.',
222
+ checks: ['gate'],
223
+ },
208
224
  ];
225
+ /** The page specifications under the console and embed folders that lack the part the 0.9.0 check requires. */
226
+ function pagesWithoutAgentReads(root) {
227
+ const files = [];
228
+ for (const d of ['app/(dashboard)', 'app/embed'])
229
+ walk(root, d, files, /^README\.md$/);
230
+ const hits = files.filter((rel) => !/^## Agents\n[\s\S]*?^### What the agent reads$/m.test(text(root, rel) ?? ''));
231
+ return hits.length > 0 ? hits : null;
232
+ }
209
233
  /** The registry entries that apply to a project moving from `source` to `target`, as journal migrations. */
210
234
  export function releaseMigrations(root, source, target) {
211
235
  const out = [];
package/dist/ownership.js CHANGED
@@ -1,14 +1,14 @@
1
1
  const HASH = /^sha256-[0-9a-f]{64}$/;
2
2
  const SEMVER = /^\d+\.\d+\.\d+$/;
3
3
  /** The library modules Meridian's components and gates import; the rest of src/lib belongs to the template's pages. */
4
- export const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown', 'logo', 'collection', 'live'].map((n) => `src/lib/${n}.ts`);
4
+ export const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown', 'logo', 'collection', 'live', 'shared-context', 'agent-guidance', 'insight'].map((n) => `src/lib/${n}.ts`);
5
5
  /** The single files adopt copies besides the folders and the library: all of them must be in a payload that is whole. */
6
6
  export const FIXED = [...LIB, 'src/lib/assistant/prompt.ts', 'src/views/console-chrome.tsx', 'tests/setup.ts'];
7
7
  /** The one rule, a plain-English line per clause. `references/ownership.md` renders it, so the guidance cannot drift. */
8
8
  export const OWNERSHIP = [
9
9
  'Managed: the payload files under `tokens/`, `src/styles/` and `src/components/`, except each component\'s `README.md` and `preview.tsx`.',
10
10
  'Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the first-load baseline `optional:scripts/verify.baseline.json` and a team-local `optional:scripts/check.local.ts`.',
11
- 'Managed: the library helpers `src/lib/cn.ts`, `src/lib/format.ts`, `src/lib/format-date.ts`, `src/lib/period.ts`, `src/lib/color.ts`, `src/lib/host.ts`, `src/lib/preferences.ts`, `src/lib/csv.ts`, `src/lib/safe-markdown.ts`, `src/lib/logo.ts`, `src/lib/collection.ts` and `src/lib/live.ts`, and the assistant prompt `src/lib/assistant/prompt.ts`.',
11
+ 'Managed: the library helpers `src/lib/cn.ts`, `src/lib/format.ts`, `src/lib/format-date.ts`, `src/lib/period.ts`, `src/lib/color.ts`, `src/lib/host.ts`, `src/lib/preferences.ts`, `src/lib/csv.ts`, `src/lib/safe-markdown.ts`, `src/lib/logo.ts`, `src/lib/collection.ts`, `src/lib/live.ts`, `src/lib/shared-context.ts`, `src/lib/agent-guidance.ts` and `src/lib/insight.ts`, and the assistant prompt `src/lib/assistant/prompt.ts`.',
12
12
  'Managed: `src/views/console-chrome.tsx` and `tests/setup.ts`.',
13
13
  'Managed: `optional:scripts/package.json`, only when the adopt or create that set the project up generated it for this project shape.',
14
14
  'Managed: both installed skill trees, `optional:.agents/skills/zz-meridian/` and `optional:.claude/skills/zz-meridian/`, taken from the release\'s skill payload.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,44 @@
2
2
 
3
3
  Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
4
4
 
5
+ ## [0.9.0] · 2026-10-07
6
+
7
+ ### Added
8
+
9
+ - **Every page tells both agents the same thing (decision 0011).** A view builds one shared context (`src/lib/shared-context.ts`, one producer per page in `src/views/*-context.ts`): its scope and freshness, what the person points at, every figure with its unit, its change and its definition, what code computed from them (`src/lib/insight.ts`: a day against its median, a share, a ranking, a run of days, what nothing recorded explains), and what the data cannot say. An MCP host receives it through `ui/update-model-context`, inline and in fullscreen; the console's assistant receives it in its prompt, with the page's address and query. All nine console pages and the four embed views share one.
10
+ - **Every view has a tool.** `src/views/tools.ts` holds each view's contract: its name, its address as input, its `ui://` resource, and one read that returns what it renders and what the agents are told. The console's assistant offers each as a read-only `view_<name>`; `docs/agents.md` registers the same list on an MCP server, with the apply tool of a change registered view-only. The embed routes render from their tool's read.
11
+ - **The assistant is told its limits.** Its prompt lists what it cannot do or see, from each collection's `pageOnly`, `hidden` and missing operations, so it can say why.
12
+ - **Ask hands a card to the console's assistant.** With the assistant on, Ask appears on the featured card of Overview, Requests, Health and Analytics; a press opens the panel and sends the card's question with the page's context (`ConsoleSurface`). Ask renders only after hydration.
13
+ - **An MCP view says when the host refuses.** A refused context is sent once more, and Ask then carries the view's address; a refused Ask tells the person in a toast with the question's words.
14
+ - **The person sees the findings.** `MetricTile` takes `finding` (a line, with a day it points the Meridian there) and `baseline` ("2.7× usual" beside a day being read); `TrendChart` takes `baseline`, a dashed median line. The Overview uses all three.
15
+ - **An agent's change leaves an Activity line.** The assistant's guard takes an optional `record`; the template writes to a new `activity` collection, and the Overview's Activity is live.
16
+ - **The Overview, Analytics and Health read through `read()`.** Daily totals, endpoints, responses, services, incidents and activity are read-only collections (`src/data/metrics.ts`).
17
+ - **An address can name a day.** `?day=2026-09-22` opens the Overview pointed at it (`Meridian day`).
18
+
19
+ ### Breaking
20
+
21
+ `update` reports the two that touch the team's own files as migrations, `share-view-context` and `agent-reads-section`, with what to change (`references/update.md`, "Resolving 0.9.0's migrations"); `src/lib/shared-context.ts`, `src/lib/agent-guidance.ts` and `src/lib/insight.ts` are Meridian's now and arrive with the update.
22
+
23
+ - `useShareView(text, structured)` is `useShareView(context)`. Build a context with the fields in `SharedContext`; `contextText` writes the text both agents read.
24
+ - `scripts/check.ts` requires `### What the agent reads` under `## Agents` in the README of every page under `app/(dashboard)/` and `app/embed/`. Add it to a product's own pages, saying what the page's context tells each agent.
25
+ - `src/data/sample.ts` no longer exports `ENDPOINTS`, `STATUS_MIX`, `ACTIVITY`, `INCIDENTS`, `PAST_INCIDENTS`, `SERVICES` or `demoTotals`: read them through `src/data/metrics.ts`. `RequestView` takes `routeP95`.
26
+ - `OverviewBody`, `HealthBody` and `AnalyticsBody` take the period key, `updatedAt` and, for the first and last, the incidents and activity beside the figures; `RequestsView` takes `updatedAt` and `now`.
27
+
28
+ ## [0.8.0] · 2026-10-06
29
+
30
+ ### Added
31
+
32
+ - **A demo password in front of the whole product.** With `DEMO_PASSWORD` set at run time, `proxy.ts` sends every route to the sign-in page (an API answers 401), whose panel becomes "Open the demo": one password field, a 30-day signed session (`src/lib/demo-gate.ts`, keyed by `DEMO_SECRET` when set), and opening the page again signs out. Without it nothing changes: the panel is the product's sign-in and nothing is gated. The sign-in panel now streams in behind a Suspense boundary, so the rest of the page still prerenders. Meridian's own demo deploys to CapRover from this repository (`Dockerfile`, `captain-definition`, neither in the package).
33
+
34
+ ### Changed
35
+
36
+ - **`update` is tested from the last three releases.** The release updates a project of the release before, adopted and created, through finalize; the weekly run the last three. Every published origin used to run at each release, which took 11 of its 18 minutes and grew with every release. From an older project, update in steps (`references/update.md`).
37
+
38
+ ### Fixed
39
+
40
+ - **A project leaves an update in progress and each person's agent settings out of git.** Its `.gitignore` (the template's, for a created project; three lines `adopt` adds, for an adopted one) ignores `.meridian/update/` and `.meridian/update.lock`, the staged copies, backups and lock of an unfinished update, which a commit mid-session used to take in; a created project also ignores `.claude/settings.local.json` and `.claude/*.lock`. The manifest, `keep.json`, `.meridian/history/` and both copies of the skill stay committed: the next update and the team's agents read them. In a project made before 0.8.0, add those lines to `.gitignore`.
41
+ - **The one sentence shows its blank.** It ended in `<what you want>`, which GitHub and npm read as an HTML tag and dropped, so it read "installs to: .". It ends in `[what you want, in your own words]`.
42
+
5
43
  ## [0.7.0] · 2026-10-06
6
44
 
7
45
  ### Changed
@@ -39,7 +39,7 @@ Every card's README follows the same order, so a reader always knows where to lo
39
39
  4. **States**: rest, hover, pressed, focus, selected, disabled, loading, empty, error, each with its transition (property, duration token, curve).
40
40
  5. **Behaviour**: interaction, keyboard, timing.
41
41
  6. **Surfaces**: console, mobile (under 1024px) and embed (an MCP App): what changes on each, or "same", and why.
42
- 7. **Agents**: what an agent may read here, what it may do (and through which proposal), and how its work is marked. Write "Not applicable" for purely presentational parts.
42
+ 7. **Agents**: what an agent may read here, what it may do (and through which proposal), and how its work is marked. Write "Not applicable" for purely presentational parts. A page or a view that shares a context (`useShareView`, decision 0011) adds **What the agent reads** under it: what its context says, with a real line of it, and what each consumer receives (the MCP App through `ui/update-model-context`, the console's assistant in its prompt).
43
43
  8. **Accessibility**: roles, names, keyboard, and contrast quoted from `node scripts/contrast.ts --all` (the dark theme and the indigo accent, the defaults; add light where it differs in kind).
44
44
  9. **Content**: copy rules with real strings.
45
45
  10. **Do and do not.**
@@ -75,6 +75,7 @@ Write the status on the line under the title: `Status: beta`.
75
75
  - Under reduced motion it shows its final state with nothing moving.
76
76
  - Keyboard: every control reachable, operable and visibly focused; focus order follows reading order.
77
77
  - An agent's work in it is marked (Agent mark, "via" attribution, or a Proposal); no agent write happens without one.
78
+ - A page shares a context both agents read: its scope and freshness, every figure with its unit, change and definition, what code derived from them, and what the data cannot say, within 2,400 characters. A figure an agent would otherwise recompute is computed in the context.
78
79
 
79
80
  ## Tools
80
81
 
package/payload/README.md CHANGED
@@ -74,15 +74,16 @@ Dark is the default, written on `:root`; the light theme follows the operating s
74
74
  | `scripts/` | Generators, gates, `brand.ts` and `verify.ts` (see `CONTRIBUTING.md`) |
75
75
  | `skills/zz-meridian/` | The agent skill (Claude Code, Codex) that builds dashboards on this template or brings it into yours |
76
76
  | `cli/` | The `zz-meridian` npm package: `create`, `adopt`, `update`, `brand` and `skill`, its build, its smoke test and the fixture app (`docs/distribution.md`) |
77
- | `.github/workflows/release.yml` | The release, each check once: a timed default verify and the consumer path from the tarball (every published origin updated), then npm with provenance, the registry's bytes and provenance checked, then the tag (`.claude/commands/release.md`) |
77
+ | `.github/workflows/release.yml` | The release, each check once: a timed default verify and the consumer path from the tarball (the release before updated; the last three weekly), then npm with provenance, the registry's bytes and provenance checked, then the tag (`.claude/commands/release.md`) |
78
78
  | `.github/workflows/weekly.yml` | Weekly, never at release: `verify --full --perf` (perf a report) and the consumer smoke's recovery and failure cases; nothing waits on it |
79
+ | `Dockerfile`, `captain-definition` | The demo deployment: the template and the Atlas behind `DEMO_PASSWORD` (`proxy.ts`, `app/sign-in/README.md`), deployed to CapRover from this repository; never in the package |
79
80
 
80
81
  ## Get a dashboard on Meridian, in one sentence
81
82
 
82
83
  Give your coding agent (Codex, Claude Code, or any agent that can run a shell) this, with what you want in your own
83
84
  words at the end:
84
85
 
85
- > Run `npx zz-meridian@latest skill --global`, then follow the zz-meridian skill it installs to: <what you want>.
86
+ > Run `npx zz-meridian@latest skill --global`, then follow the zz-meridian skill it installs to: [what you want, in your own words].
86
87
 
87
88
  You do not choose between the package's commands; the skill does, from what you said and what is in the folder, and
88
89
  names the route before it runs anything:
@@ -41,7 +41,24 @@ From `src/system/fixtures/sample.ts`: `demoSeries(period)` (one point per day, a
41
41
 
42
42
  ## Agents
43
43
 
44
- The embed view shares the period and its totals, or the day the Meridian points at, with the model. Ask on the trend posts "Why did requests change on <day>?". The Activity feed marks the agent's own changes ("Claude … · for Jonas Weber").
44
+ The page shares one context with both agents (decision 0011), built by `overviewContext` in `src/views/overview-context.ts` from the figures the page draws. The Activity feed marks the agent's own changes ("Claude … · for Jonas Weber").
45
+
46
+ ### What the agent reads
47
+
48
+ The period with its dates, freshness, the four figures, each with its change against the previous period and its definition (the tiles' `hint`s come from the same `OVERVIEW_METRICS`), the day the Meridian points at against the same weekday and the period's median, and what code computed:
49
+
50
+ ```text
51
+ - Error rate was 2.6× to 2.7× the period's median of 0.81% on 21 and 22 Sept (2.13%, 2.18%); no other day passed 1.2×.
52
+ - Latency p95 rose on the same days: 525ms and 505ms against a median of 296ms.
53
+ Unknown:
54
+ - Nothing is recorded between 20 Sept and 23 Sept: no incident and no activity event, so nothing on this page explains the rise on 21 and 22 Sept.
55
+ ```
56
+
57
+ It also says which endpoint contributes most errors, that requests follow the week, what the status classes mean against the error rate's definition, which incident sits beside a spike when one does (in 90 days: "Upload failures for files over 50 MB"), and that today is partial.
58
+
59
+ - **MCP App:** the text above as `ui/update-model-context`, with the same context as its structured part, from the inline view and from fullscreen alike, on load and on every change.
60
+ - **Console assistant:** the same text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
61
+ - **Handoff:** Ask on the featured card hands the period, or the day the Meridian points at, to the assistant: "What stands out in requests, errors and latency over the last 30 days, and why?"
45
62
 
46
63
  ## Embed view
47
64
 
@@ -5,15 +5,20 @@ import { Freshness } from '@/components/patterns/freshness';
5
5
  import { PeriodSelect } from '@/components/patterns/period-select';
6
6
  import { Skeleton } from '@/components/ui/skeleton';
7
7
  import { OverviewBody } from '@/views/overview';
8
- import { DEMO_NOW, DEMO_UPDATED_AT, demoSeries, demoTotals, ENDPOINTS, STATUS_MIX, ACTIVITY } from '@/data/sample';
9
- import { parsePeriod, PERIOD_LABEL } from '@/lib/period';
8
+ import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
9
+ import { overviewTool } from '@/views/tools';
10
+ import { cache } from 'react';
11
+ import { parsePeriod } from '@/lib/period';
10
12
  import { app } from '@/app.config';
11
13
  import { Busy } from '../_loading';
12
14
  import { OverviewSkeleton } from './loading';
13
15
 
14
16
  export const metadata = { title: 'Overview' };
15
17
 
16
- type SearchParams = Promise<{ period?: string }>;
18
+ type SearchParams = Promise<{ period?: string; day?: string }>;
19
+
20
+ /** The page's one read, through the Overview tool (`src/views/tools.ts`), shared by the export and the body. */
21
+ const readPeriod = cache((period: ReturnType<typeof parsePeriod>, day?: string) => overviewTool.read({ period, day: /^\d{4}-\d{2}-\d{2}$/.test(day ?? '') ? day : undefined }));
17
22
 
18
23
  /** The masthead renders at once; the period select, the export and the body read the address inside their own boundaries. */
19
24
  export default function OverviewPage({ searchParams }: { searchParams: SearchParams }) {
@@ -38,15 +43,17 @@ export default function OverviewPage({ searchParams }: { searchParams: SearchPar
38
43
 
39
44
  async function Actions({ searchParams }: { searchParams: SearchParams }) {
40
45
  const period = parsePeriod((await searchParams).period);
46
+ const { data } = await readPeriod(period);
41
47
  return (
42
48
  <>
43
49
  <Suspense><PeriodSelect value={period} /></Suspense>
44
- <ExportButton rows={demoSeries(period).current} filename={`overview-${period}.csv`} noun="days" className="max-sm:hidden" />
50
+ <ExportButton rows={data.series} filename={`overview-${period}.csv`} noun="days" className="max-sm:hidden" />
45
51
  </>
46
52
  );
47
53
  }
48
54
 
49
55
  async function Body({ searchParams }: { searchParams: SearchParams }) {
50
- const period = parsePeriod((await searchParams).period);
51
- return <OverviewBody period={PERIOD_LABEL[period].toLowerCase()} series={demoSeries(period).current} totals={demoTotals(period)} endpoints={ENDPOINTS} mix={STATUS_MIX} activity={ACTIVITY} now={DEMO_NOW.toISOString()} />;
56
+ const { period, day } = await searchParams;
57
+ const { data } = await readPeriod(parsePeriod(period), day);
58
+ return <OverviewBody {...data} />;
52
59
  }
@@ -39,7 +39,19 @@ Status: beta
39
39
 
40
40
  ## Agents
41
41
 
42
- Not applicable: an agent answers analytical questions from the Overview and Requests views' shared context and the API, not by driving this page.
42
+ An agent drives nothing here; it reads the page through its shared context (decision 0011), built by `analyticsContext` in `src/views/analytics-context.ts`.
43
+
44
+ ### What the agent reads
45
+
46
+ The period, freshness, the busiest hour of the week and of the day, each region's share with its p50, every endpoint in the table's current order, the day the Meridian points at, and what code found: the same error spike as the Overview (by the same code), the slowest and the most failing endpoint against the median, the slowest region, and the weekend's share of a weekday. It says that the weekday-and-hour pattern, the regions and the endpoints are sums over the period, not a day.
47
+
48
+ ```text
49
+ - POST /v1/files fails most: 1.88% of its requests, 5.0× the 0.37% across endpoints. Evidence: /requests?q=%2Fv1%2Ffiles
50
+ ```
51
+
52
+ - **Console assistant:** this text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
53
+ - **MCP App:** no embed view yet; an MCP server can return this context as a tool's text (`docs/agents.md`).
54
+ - **Handoff:** Ask on Errors per day hands the pointed day, or the period, to the assistant: "Why did errors change on 22 Sept 2026?"
43
55
 
44
56
  ## Accessibility
45
57
 
@@ -6,7 +6,9 @@ import { Freshness } from '@/components/patterns/freshness';
6
6
  import { PeriodSelect } from '@/components/patterns/period-select';
7
7
  import { Skeleton } from '@/components/ui/skeleton';
8
8
  import { AnalyticsBody } from '@/views/analytics';
9
- import { DEMO_NOW, DEMO_UPDATED_AT, ENDPOINTS, demoHeatmap, demoSeries, REGION_LATENCY, requestsByHour } from '@/data/sample';
9
+ import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
10
+ import { analyticsTool } from '@/views/tools';
11
+ import { cache } from 'react';
10
12
  import { parsePeriod } from '@/lib/period';
11
13
  import { Busy } from '../_loading';
12
14
  import { AnalyticsSkeleton } from './loading';
@@ -36,17 +38,21 @@ export default function AnalyticsPage({ searchParams }: { searchParams: SearchPa
36
38
  );
37
39
  }
38
40
 
41
+ /** The page's one read, through the Analytics tool (`src/views/tools.ts`), shared by the export and the body. */
42
+ const readPeriod = cache((period: ReturnType<typeof parsePeriod>) => analyticsTool.read({ period }));
43
+
39
44
  async function Actions({ searchParams }: { searchParams: SearchParams }) {
40
45
  const period = parsePeriod((await searchParams).period);
46
+ const { data } = await readPeriod(period);
41
47
  return (
42
48
  <>
43
49
  <Suspense><PeriodSelect value={period} /></Suspense>
44
- <ExportButton rows={demoSeries(period).current} filename={`analytics-${period}.csv`} noun="days" className="max-sm:hidden" />
50
+ <ExportButton rows={data.series} filename={`analytics-${period}.csv`} noun="days" className="max-sm:hidden" />
45
51
  </>
46
52
  );
47
53
  }
48
54
 
49
55
  async function Body({ searchParams }: { searchParams: SearchParams }) {
50
- const period = parsePeriod((await searchParams).period);
51
- return <AnalyticsBody series={demoSeries(period).current} heat={demoHeatmap()} hours={requestsByHour()} regions={REGION_LATENCY} endpoints={ENDPOINTS} />;
56
+ const { data } = await readPeriod(parsePeriod((await searchParams).period));
57
+ return <AnalyticsBody {...data} />;
52
58
  }
@@ -39,7 +39,18 @@ None yet: a `zz_meridian_customer { name }` view would show one customer's card
39
39
 
40
40
  ## Agents
41
41
 
42
- An agent reads customers through their requests view; it never changes a plan or invites from here.
42
+ An agent reads customers through this page's shared context and their requests view; it never changes a plan or invites from here. The context is built by `customersContext` in `src/views/customers-context.ts`, and the tiles' `hint`s come from its `CUSTOMER_METRICS`.
43
+
44
+ ### What the agent reads
45
+
46
+ The three tiles with their definitions, every customer in the table's filter and order (with the columns a narrow screen drops: the error rate and the trend are still the person's to read), and what code found: how concentrated spend is, how combined requests moved over the last 7 days against the 7 before and who departs from that by 10 points or more, and which customers fail at twice the median rate. Lists stop at 25 and say how many more there are.
47
+
48
+ ```text
49
+ - Combined requests are up 15% over the last 7 days against the 7 before. Apart from that: Fernway Bank up 36%.
50
+ ```
51
+
52
+ - **Console assistant:** this text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
53
+ - **MCP App:** no embed view yet; an MCP server can return this context as a tool's text (`docs/agents.md`).
43
54
 
44
55
  ## Content
45
56
 
@@ -42,7 +42,15 @@ Without a live incident row 1 becomes one full-width featured card.
42
42
 
43
43
  ## Agents
44
44
 
45
- The page itself takes no agent action. Its embed shares the summary and the open incident with the model; an agent can be asked about the incident from the inline banner.
45
+ The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `healthContext` in `src/views/health-context.ts`.
46
+
47
+ ### What the agent reads
48
+
49
+ Every service's state, latency now and uptime; the average uptime with its definition; which services carry the days below operational ("Inference API accounts for 3 of them"); the open incident with its latest update by time, not by position; the longest incident resolved in the period. Unknowns: that a monitoring incident is not yet confirmed fixed, and that latency is one reading with no history to call it unusual.
50
+
51
+ - **MCP App:** the text above as `ui/update-model-context`, with the same context as its structured part, from the inline view and from fullscreen alike, on load and on every change.
52
+ - **Console assistant:** the same text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
53
+ - **Handoff:** Ask on the uptime card hands the open incident to the assistant: "Which service is hurting uptime, and is … over?"
46
54
 
47
55
  ## Embed view
48
56
 
@@ -2,22 +2,23 @@ import { app } from '@/app.config';
2
2
  import { PageFrame } from '@/components/base/shell';
3
3
  import { Freshness } from '@/components/patterns/freshness';
4
4
  import { PackedHealthBody, SubscribeButton } from '@/views/health';
5
+ import { healthTool } from '@/views/tools';
5
6
  import { packServices } from '@/components/patterns/status-list/summarise';
6
- import { DEMO_NOW, DEMO_UPDATED_AT, INCIDENTS, SERVICES, PAST_INCIDENTS } from '@/data/sample';
7
7
 
8
8
  export const metadata = { title: 'Health' };
9
9
 
10
- export default function HealthPage() {
11
- const current = INCIDENTS.find((i) => i.state !== 'resolved') ?? null;
10
+ /** Rendered from the Health tool's own read (`src/views/tools.ts`): the services and incidents the caller may read. */
11
+ export default async function HealthPage() {
12
+ const { data } = await healthTool.read({});
12
13
  return (
13
14
  <PageFrame
14
15
  kicker={<>{app.name} · {app.workspace}</>}
15
16
  title="Health"
16
17
  description="Every service, how it is right now, and what happened in the last 90 days."
17
- meta={<Freshness updatedAt={DEMO_UPDATED_AT} now={DEMO_NOW} />}
18
+ meta={<Freshness updatedAt={new Date(data.updatedAt)} now={new Date(data.now)} />}
18
19
  actions={<SubscribeButton />}
19
20
  >
20
- <PackedHealthBody services={packServices(SERVICES)} current={current} past={PAST_INCIDENTS} now={DEMO_NOW.toISOString()} />
21
+ <PackedHealthBody {...data} services={packServices(data.services)} />
21
22
  </PageFrame>
22
23
  );
23
24
  }
@@ -41,7 +41,14 @@ Not offered. Keys are secrets, and creating one is a page-only operation. In the
41
41
 
42
42
  ## Agents
43
43
 
44
- An agent may say which keys are unused (from the shared context of other views) and may propose revoking a key (a critical Proposal); it never creates one. The proposal waits for approval, and its approval removes the key through the same collection.
44
+ An agent may propose revoking a key (a critical Proposal); it never creates one, because a key's secret is shown once, on this page (`pageOnly`). The proposal waits for approval, and its approval removes the key through the same collection. The page shares its context through `keysContext` in `src/views/keys-context.ts`.
45
+
46
+ ### What the agent reads
47
+
48
+ Every key with its id, environment, hint (the first and last characters, never the secret), owner, scopes, when it was created and last used, and what code found: live keys unused for 30 days, live keys with every scope, and an owner holding several live keys. The secret and its hash are never in it (`secretHash` is `hidden`), and the context says so, with the rule that creating a key is the person's.
49
+
50
+ - **Console assistant:** this text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
51
+ - **MCP App:** no embed view yet; an MCP server can return this context as a tool's text (`docs/agents.md`).
45
52
 
46
53
  ## Content
47
54
 
@@ -42,7 +42,18 @@ Not offered. Inviting and removing people are decisions for the console, not a c
42
42
 
43
43
  ## Agents
44
44
 
45
- The assistant reads and proposes changes here through Proposals: it adds, changes and removes members only after a person approves, and what it changed shows on this page after a refresh. A change made here is what the assistant reads next.
45
+ The assistant reads and proposes changes here through Proposals: it adds, changes and removes members only after a person approves, and what it changed shows on this page after a refresh. A change made here is what the assistant reads next. The page shares its context through `membersContext` in `src/views/members-context.ts`.
46
+
47
+ ### What the agent reads
48
+
49
+ Counts by status, role (with what each role may do) and team, everyone with their role, team, status and when they were last seen (25 at most; `query_members` finds the rest), and what code found: active members unseen for 60 days or more, unanswered invitations, and who can change the workspace. It says that "last active" is a console sign-in, not use of the API.
50
+
51
+ ```text
52
+ - 3 active members have not been seen for 60 days or more: Alice Moreno (Viewer, Support, 94 days), Felix Andersson (Viewer, Engineering, 75 days), Ravi Patel (Viewer, Support, 71 days).
53
+ ```
54
+
55
+ - **Console assistant:** this text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
56
+ - **MCP App:** no embed view yet; an MCP server can return this context as a tool's text (`docs/agents.md`).
46
57
 
47
58
  ## Content
48
59
 
@@ -51,7 +51,21 @@ Export CSV is a link to `/api/export/requests` with the same filters and sort: t
51
51
 
52
52
  ## Agents
53
53
 
54
- An agent opens this page's views through `zz_meridian_requests`; it reads them through the shared context; it never exports or blocks from here. Its filters are marked until a person changes one.
54
+ An agent opens this page's views through `zz_meridian_requests`; it never exports or blocks from here. Its filters are marked until a person changes one. The page shares one context with both agents (decision 0011), built by `requestsContext` in `src/views/requests-context.ts` from the page's read: the rows on screen and the summary of the whole filtered set.
55
+
56
+ ### What the agent reads
57
+
58
+ The filters in words and the span of time the log covers; freshness; the three figures with their change against the half hour before and their definitions (the tiles' `hint`s come from `REQUEST_METRICS`); the page, the sort and the rows on screen by id; and what the matching requests and their errors have in common:
59
+
60
+ ```text
61
+ - 5 of the 9 errors and limits are in eu-west-1 (56%). Evidence: /requests?region=eu-west-1
62
+ ```
63
+
64
+ When a half hour holds fewer than 30 requests it says the change is too few to read as a trend, and when the summary reads only the newest 500 it says so.
65
+
66
+ - **MCP App:** the text above as `ui/update-model-context`, with the same context as its structured part, from the inline view and from fullscreen alike, on load and on every change.
67
+ - **Console assistant:** the same text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
68
+ - **Handoff:** Ask in the filter bar hands the filtered set to the assistant: "What do these requests with status 5xx have in common, and why?"
55
69
 
56
70
  ## Accessibility
57
71
 
@@ -41,7 +41,18 @@ None of its own. An agent shows a request through `zz_meridian_requests` (inline
41
41
 
42
42
  ## Agents
43
43
 
44
- Replay and blocking a key are a person's actions here. An agent that wants either sends a Proposal.
44
+ Replay is a person's action here: it is `pageOnly`, so no agent can run or propose it. The page shares its context through `requestContext` in `src/views/request-context.ts`.
45
+
46
+ ### What the agent reads
47
+
48
+ The request, its status, latency (with its definition), customer and region, every phase with its start and duration, the payloads' sizes (their text is in the page text), and what code found: where most of the time went, the latency against the route's p95, and, for a failed request, that it counts in the error rate. It says that one request is one sample, and that replaying it is the person's.
49
+
50
+ ```text
51
+ - Most of its time went to Rate limit (Over 1,200 requests a minute): 159ms, 93% of 171ms.
52
+ ```
53
+
54
+ - **Console assistant:** this text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
55
+ - **MCP App:** no embed view yet; an MCP server can return this context as a tool's text (`docs/agents.md`).
45
56
 
46
57
  ## Accessibility
47
58
 
@@ -1,4 +1,5 @@
1
1
  import { read } from '@/data/read';
2
+ import { readEndpoints } from '@/data/metrics';
2
3
  import { FEATURED_REQUEST_IDS, traceOf, payloadsOf, type RequestRow } from '@/data/sample';
3
4
  import { MissingPage } from '@/views/missing-page';
4
5
  import { RequestView } from '@/views/request';
@@ -23,5 +24,6 @@ export default async function RequestPage({ params }: { params: Promise<{ id: st
23
24
  const { request: r, observedAt } = await byId(id);
24
25
  // Rendered here rather than thrown, so the screen is in the first HTML (see MissingPage).
25
26
  if (!r) return <MissingPage />;
26
- return <RequestView request={r} trace={traceOf(r)} payloads={payloadsOf(r)} now={observedAt} replay={replayRequest} />;
27
+ const route = (await readEndpoints()).find((e) => e.method === r.method && e.route === r.route);
28
+ return <RequestView request={r} trace={traceOf(r)} payloads={payloadsOf(r)} routeP95={route?.p95 ?? null} now={observedAt} replay={replayRequest} />;
27
29
  }
@@ -65,6 +65,6 @@ async function RequestsExport({ searchParams }: { searchParams: SearchParams })
65
65
  }
66
66
 
67
67
  async function Requests({ searchParams }: { searchParams: SearchParams }) {
68
- const { rows, total, summary, state } = await readPage(await searchParams);
69
- return <RequestsView rows={rows} total={total} summary={summary} state={state} pageSize={REQUEST_PAGE} />;
68
+ const { rows, total, summary, state, observedAt } = await readPage(await searchParams);
69
+ return <RequestsView rows={rows} total={total} summary={summary} state={state} pageSize={REQUEST_PAGE} updatedAt={DEMO_UPDATED_AT.toISOString()} now={observedAt} />;
70
70
  }
@@ -42,6 +42,13 @@ Appearance reads and writes `usePreferences()` (stored on the device). Time zone
42
42
 
43
43
  This is where people govern agents: whether assistants may read, which are connected, and the rule (locked on) that nothing an agent proposes runs without approval, a removal included. The assistant section, shown only where the product has an assistant, holds the person's switch for its panel.
44
44
 
45
+ ### What the agent reads
46
+
47
+ Every section's current values, which neither an input's nor a switch's text carries: the workspace's name, address and time zone (and an unsaved edit, when there is one), each notification switch, this device's theme, accent and density, whether the assistant is shown, whether assistants may read dashboards and which are connected with what they may do, and that deleting the workspace is typed, never proposed. Each section reports its own line; `SettingsBody` shares them as one context. None is offered to an agent as a change, and the context says so.
48
+
49
+ - **Console assistant:** this text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
50
+ - **MCP App:** no embed view yet; an MCP server can return this context as a tool's text (`docs/agents.md`).
51
+
45
52
  ## Accessibility
46
53
 
47
54
  - Each section is a form with its own submit; switches carry their label and description.
@@ -2,9 +2,11 @@ import { safeValidateUIMessages } from 'ai';
2
2
  import { revalidateTag } from 'next/cache';
3
3
  import { assistantConfig } from '@/lib/assistant/config';
4
4
  import { clock, collections } from '@/data/collections';
5
- import { Unauthenticated, can, collectionFor, resolveAccess } from '@/data/access';
5
+ import { Unauthenticated, can, collectionFor, nameOf, resolveAccess } from '@/data/access';
6
6
  import { collectionTag } from '@/data/read';
7
7
  import { respond } from '@/lib/assistant/respond';
8
+ import type { AgentChange } from '@/lib/assistant/tools';
9
+ import { viewTools } from '@/views/tools';
8
10
 
9
11
  /** The panel sends at most the last 100 messages; a larger body than 2 MB is refused before it is parsed. */
10
12
  const MAX_MESSAGES = 100;
@@ -20,7 +22,7 @@ export async function POST(request: Request): Promise<Response> {
20
22
  const raw = await request.text();
21
23
  if (raw.length > MAX_BODY) return new Response(null, { status: 413 });
22
24
  const body: unknown = (() => { try { return JSON.parse(raw); } catch { return null; } })();
23
- const { messages, page } = (body ?? {}) as { messages?: unknown; page?: { path?: unknown; title?: unknown; text?: unknown } };
25
+ const { messages, page } = (body ?? {}) as { messages?: unknown; page?: { path?: unknown; title?: unknown; context?: unknown; text?: unknown } };
24
26
  if (!Array.isArray(messages) || messages.length === 0 || messages.length > MAX_MESSAGES) return new Response(null, { status: 400 });
25
27
  const valid = await safeValidateUIMessages({ messages });
26
28
  if (!valid.success) return new Response(null, { status: 400 });
@@ -35,14 +37,32 @@ export async function POST(request: Request): Promise<Response> {
35
37
  return now !== null && now.tenantId === scope.tenantId && now.subjectId === scope.subjectId && can(now, name, op, ids);
36
38
  },
37
39
  invalidate: (name: string) => revalidateTag(collectionTag(scope.tenantId, name), { expire: 0 }),
40
+ // Provenance (decision 0011): an approved change leaves an Activity line naming the agent and the person it acted
41
+ // for. Only where the product keeps activity and the person may add to it; the change stands either way.
42
+ record: async (change: AgentChange) => {
43
+ if (!(await can(scope, 'activity', 'create'))) return;
44
+ await collectionFor(scope, 'activity').create!({ at: clock().toISOString(), actor: (await nameOf(scope)) ?? 'You', via: 'Assistant', ...activityLine(change) });
45
+ revalidateTag(collectionTag(scope.tenantId, 'activity'), { expire: 0 });
46
+ },
38
47
  };
39
48
 
40
49
  return respond({
41
50
  ...config,
42
51
  collections: readable,
52
+ // Every view's read-only tool; each reads through `read()`, so it sees only what this caller may read.
53
+ views: viewTools,
43
54
  guard,
44
55
  now: clock(),
45
56
  messages: valid.data,
46
- page: { path: String(page?.path ?? ''), title: String(page?.title ?? ''), text: String(page?.text ?? '') },
57
+ page: { path: String(page?.path ?? ''), title: String(page?.title ?? ''), context: String(page?.context ?? ''), text: String(page?.text ?? '') },
47
58
  });
48
59
  }
60
+
61
+ /** An agent's change as an Activity line: "changed status to Suspended for Alice Moreno and Ravi Patel". */
62
+ function activityLine({ label, op, titles, set }: AgentChange): { verb: string; object: string } {
63
+ const names = titles.length <= 3 ? titles.join(titles.length === 2 ? ' and ' : ', ') : `${titles.length} ${label.toLowerCase()}`;
64
+ if (op === 'create') return { verb: 'added', object: `${names} to ${label.toLowerCase()}` };
65
+ if (op === 'remove') return { verb: 'removed', object: `${names} from ${label.toLowerCase()}` };
66
+ const fields = Object.entries(set ?? {}).map(([k, v]) => `${k} to ${Array.isArray(v) ? v.join(', ') : String(v)}`).join(', ');
67
+ return { verb: 'changed', object: `${fields} for ${names}` };
68
+ }
@@ -29,7 +29,12 @@ Embed only, at 360 to 760px inline and the host's full panel in fullscreen. The
29
29
 
30
30
  ## Agents
31
31
 
32
- Shared with the model on load and on every change: "ZZ Meridian health: 1 service degraded (Inference API degraded). Open incident: Elevated latency on Inference API in eu-west-1, monitoring." with `{ view, state, affected, incident }`. Ask on the banner posts "What is the impact of … on my traffic?".
32
+ The Health page's context (`app/(dashboard)/health/README.md` lists it), inline and in fullscreen. Ask on the banner posts "What is the impact of … on my traffic?".
33
+
34
+ ### What the agent reads
35
+
36
+ - **MCP App:** the text above as `ui/update-model-context`, with the same context as its structured part, from the inline view and from fullscreen alike, on load and on every change.
37
+ - **Console assistant:** the same text in its prompt's `<view-context>` block, with the page's address and query, and the page text after it for anything the context does not cover.
33
38
 
34
39
  ## Accessibility
35
40