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 is src/__tests__/, not top-level tests/).
  • Effort: S.
  • Depends on: P0.2.1 (SHIPPED Wave A at 40cd679d).

Acceptance criteria (1:1 mapping continued in Step 2 contract)

  1. notify(event, payload) dispatches an event to configured channels.
  2. Channels: log (always on), mcp (MCP notification), webhook (optional).
  3. COLIBRI_WEBHOOK_URL env var enables the webhook channel — COLIBRI_* namespace only; AMS_* is NOT read.
  4. Events: task.completed, merkle.finalized, error.critical (no agent.spawned — agent runtime deferred per ADR-005).
  5. Fire-and-forget: notification failures MUST NOT block main execution.
  6. 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 call notify({ 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 call notify({ type: 'merkle.finalized', ... }) after merkle_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)

  1. stdout is sacred — MCP SDK’s StdioServerTransport writes JSON-RPC frames to process.stdout. The log channel MUST write to stderr only (console.error), never stdout. Donor bug #3 in CLAUDE.md heritage notes.
  2. Fire-and-forgetnotify() returns Promise<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.
  3. No node-fetch / axios — Node 20 has global fetch. Use it directly for the webhook POST.
  4. MCP notification degrade — if the caller doesn’t inject a notifier into notify(), the mcp channel logs [notify] mcp channel inactive to stderr and continues. No throw.
  5. Jest ESM + zod — do NOT use jest.isolateModulesAsync (broken with zod per P0.1.4 hazard memo). Tests mock with jest.spyOn(global, 'fetch' as never) and inject a mock Notifier directly — no module-level mocks needed.
  6. Cross-worktree leak guard — Step 1 began by running git status on the worktree; result: clean. No non-owned files modified.
  7. Do not edit: src/tools/merkle.ts (P0.8.3) and src/domains/proof/retention.ts (P0.8.2) — parallel sub-agents own those.
  8. Server.ts minimal coupling — the task design keeps src/server.ts untouched. The mcp channel is wired via an optional Notifier parameter that callers construct from ctx.server.server, not via a change to createServer/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 status showed working tree clean before edits).

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.