P0.9.3 — ν Notification Channels — Audit
Step 1 of the 5-step executor chain. Inventory of the surface that P0.9.3 must land into, the constraints that bound it, and the seams already in the codebase.
Task scope (from docs/guides/implementation/task-breakdown.md § P0.9.3)
- Output:
src/domains/integrations/notifications.ts+src/__tests__/domains/integrations/notifications.test.ts(test path corrected — project convention issrc/__tests__/, not top-leveltests/). - Effort: S.
- Depends on: P0.2.1 (SHIPPED Wave A at
40cd679d).
Acceptance criteria (1:1 mapping continued in Step 2 contract)
notify(event, payload)dispatches an event to configured channels.- Channels:
log(always on),mcp(MCP notification),webhook(optional). COLIBRI_WEBHOOK_URLenv var enables the webhook channel — COLIBRI_* namespace only;AMS_*is NOT read.- Events:
task.completed,merkle.finalized,error.critical(noagent.spawned— agent runtime deferred per ADR-005). - Fire-and-forget: notification failures MUST NOT block main execution.
- Tests verify each channel receives the correct payload for each event type.
Existing files this task depends on
| Path | Role here | Key lines |
|---|---|---|
src/config.ts |
Zod env validation from P0.1.4. Needs one new optional field: COLIBRI_WEBHOOK_URL. |
L43-59 schema; L26-34 AMS_* rejection stays untouched |
src/server.ts |
P0.2.1 α server. Holds the McpServer instance at ctx.server (type McpServer). The underlying Server — accessible via ctx.server.server — is a subclass of Protocol which exposes notification(n, opts?) for emitting server-initiated JSON-RPC notifications (see node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.d.ts L383). |
L187-244 createServer; L152-173 ColibriServerContext |
src/tools/health.ts |
Reference style for a domain module that exports named functions, keeps handlers pure, and stays away from ctx.logger inside the hot path. |
whole file |
docs/reference/extractions/nu-integrations-extraction.md |
Donor-era ν spec (R45). “Notification Channels: Adapter Pattern” at L128-139 describes four donor channels (Slack/Discord/Email/custom-webhook) with LRU history + secret redaction. Phase 0 P0.9.3 intentionally trims to three channels (log/mcp/webhook) — the donor’s HMAC + exponential backoff + LRU are deferred. | L128-139 channel table; L58-114 webhook retry mechanics (deferred) |
Existing Phase 0 surface references (not touched by this task)
src/domains/tasks/β — a future consumer; will callnotify({ type: 'task.completed', ... })when a task transitions to DONE. Not wired in this PR — this task only ships the library.src/domains/proof/η — another future consumer; will callnotify({ type: 'merkle.finalized', ... })aftermerkle_finalize. Not wired in this PR.src/tools/merkle.ts— owned by a parallel worktree (P0.8.3). Do not touch.src/domains/proof/retention.ts— owned by a parallel worktree (P0.8.2). Do not touch.
New files / directories this task creates
src/domains/integrations/ ← new subdirectory (first ν lander)
src/domains/integrations/notifications.ts ← impl
src/domains/integrations/index.ts ← barrel (export * from './notifications.js')
src/__tests__/domains/integrations/ ← new test subdirectory
src/__tests__/domains/integrations/notifications.test.ts
docs/audits/notifications-audit.md ← this file (Step 1)
docs/contracts/notifications-contract.md ← Step 2
docs/packets/notifications-packet.md ← Step 3
docs/verification/notifications-verification.md ← Step 5
Files modified (beyond new ones above)
src/config.ts— add one line to the Zod schema:COLIBRI_WEBHOOK_URL: z.string().url().optional(). No other change.
Environment variables added
| Name | Schema | Default | Purpose |
|---|---|---|---|
COLIBRI_WEBHOOK_URL |
z.string().url().optional() |
undefined | When set, enables the webhook channel; notify() POSTs the event payload as JSON to this URL. |
Namespace rule (from CLAUDE.md §1 + P0.1.4 contract): COLIBRI_* only. The donor AMS_* prefix remains blocked by assertNoDonorNamespace in src/config.ts L26-34.
Constraints and hazards (memorized)
- stdout is sacred — MCP SDK’s
StdioServerTransportwrites JSON-RPC frames toprocess.stdout. Thelogchannel MUST write tostderronly (console.error), neverstdout. Donor bug #3 inCLAUDE.mdheritage notes. - Fire-and-forget —
notify()returnsPromise<void>that always resolves. Every channel dispatch is wrapped in try/catch; failures log to stderr and are swallowed. No exception ever propagates back to the caller. - No
node-fetch/axios— Node 20 has globalfetch. Use it directly for the webhook POST. - MCP notification degrade — if the caller doesn’t inject a notifier into
notify(), the mcp channel logs[notify] mcp channel inactiveto stderr and continues. No throw. - Jest ESM + zod — do NOT use
jest.isolateModulesAsync(broken with zod per P0.1.4 hazard memo). Tests mock withjest.spyOn(global, 'fetch' as never)and inject a mockNotifierdirectly — no module-level mocks needed. - Cross-worktree leak guard — Step 1 began by running
git statuson the worktree; result: clean. No non-owned files modified. - Do not edit:
src/tools/merkle.ts(P0.8.3) andsrc/domains/proof/retention.ts(P0.8.2) — parallel sub-agents own those. - Server.ts minimal coupling — the task design keeps
src/server.tsuntouched. Themcpchannel is wired via an optionalNotifierparameter that callers construct fromctx.server.server, not via a change tocreateServer/bootstrap. Decision recorded in Step 2.
Inferred MCP notification wire shape
The underlying Server class (from node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.d.ts) is a Protocol<ServerRequest, ServerNotification, ServerResult> and exposes notification(notification, options?) (inherited from Protocol, declared at shared/protocol.d.ts L383). A custom server-initiated notification therefore looks like:
await ctx.server.server.notification({
method: 'notifications/colibri/event',
params: { type: 'task.completed', task_id: '...', at: '2026-04-17T...' },
});
Phase 0 uses the custom method namespace notifications/colibri/event so it cannot collide with SDK-standard notifications (notifications/message, notifications/resources/updated, notifications/tools/list_changed, etc.).
Step 1 exit criteria
- Task scope transcribed.
- Existing files catalogued with line refs.
- New files enumerated.
- Env vars listed.
- Constraints memorized.
- Worktree clean (
git statusshowed working tree clean before edits).