# Plugins Guide

## What a Plugin Is

A plugin extends what {{productName}} can do. Each plugin adds a focused capability — contacts management, Telegram messaging, scheduling, email, research. Plugins are modular: you enable only what you need.

{{productName}}'s own capabilities are plugins too. Marketplace plugins (like Stripe) work the same way — {{productName}} manages all of them through conversation.

The tables below are the install catalogue — every plugin the platform can ship. They are not evidence of what is enabled on the current account. For the live install set, ask {{productName}} to call `capabilities-here`.

## Plugin Groups

### Core (always active)

These are part of {{productName}}'s foundation and cannot be disabled:

| Plugin | What it does |
|--------|-------------|
| `admin` | Platform management — system status, account settings, logs, session control. Also hosts the cross-cutting `plainly` skill: every text-producing agent (admin, public, every specialist) applies a plain-English precision pass to prose returned to humans, as a prime-directive prerogative. Agent-to-machine payloads (image-generate prompts, memory-write arguments, cypher) pass through verbatim. This is a prompt-level skill contract, not a hook: each agent's IDENTITY loads `skill-load skillName=plainly` on its first text-producing turn and applies the pass thereafter. Hosts the `superpowers-sprint` skill: structured sprint workflow built on the `superpowers` and `code-review` upstream plugins, dispatched on "run a sprint" or any `.tasks/NNN-*.md` invocation. |
| `memory` | Graph memory — search, write, reindex, and ingest knowledge |
| `browser` | Headless browser rendering — `browser-render` runs a JavaScript-heavy page in the device's Chromium and returns its rendered DOM. The JS-rendering leg of retrieval: WebFetch (summary) / `url-get` (verbatim, server-rendered) / `browser-render` (JS-rendered). |
| `maxy-guide` | User guide and platform documentation (this plugin) |
| `cloudflare` | Cloudflare Tunnel — remote access via your custom domain |
| `scheduling` | Calendar and scheduling — events, appointments, recurring triggers. The current instant — now, today, today's weekday — is in the injected `<datetime>` block; read it there rather than computing it by hand. To interpret a *specific* datetime (a stored timestamp, a future meeting time, its day of week), pass its ISO to the `time-resolve` tool, which returns the operator's locale view; `time-resolve` requires an ISO input and cannot read the clock. |
| `email` | Agent email account — setup, read, send, reply, search, auto-respond |
| `tasks` | Task lifecycle — create, update, list, relate, complete |
| `workflows` | Persistent named workflows — reusable instruction sets |
| `contacts` | CRM contact management — create, lookup, update, list |
| `prompt-optimiser` | Prompt optimiser — two modes. Chat-app mode turns a rough draft or task description into a single finished, copy-pasteable prompt tuned for Opus 4.7 adaptive thinking (claude.ai, Mac, iOS). In-session mode is applied automatically: a standing `UserPromptSubmit` directive hook (`admin/hooks/prompt-optimiser-directive.sh`) injects context every turn telling the admin agent to restate each non-trivial prompt through this skill and act on the restatement, skipped for one-word confirmations, slash-commands, and direct continuations. Compliance is behavioural — the hook steers the agent, it cannot force the skill call. The hook also persists the exact injected `additionalContext` per turn to `$LOG_DIR/prompt-optimiser-directives/<session>/<epoch>-<pid>.txt` and records `len`, `sha256`, and the stored `file=` path in `$LOG_DIR/prompt-optimiser-directive.log`, so any past turn's full ~17 KB directive is recoverable and verifiable (the JSONL attachment row Claude Code keeps is truncated to ≤2.6 KB). `platform/scripts/prompt-optimiser-audit.sh` reconciles breadcrumbs against stored files per session (`injected=N stored=M`; `N!=M` is the leak signature, and any breadcrumb whose `file=` is missing is named). |
| `url-get` | Faithful page retrieval — fetches a server-rendered page, writes a verbatim markdown copy to an account-scoped reference file (no model in the path, so no copyright refusal), and returns the cleaned page text (capped) plus the file path. No summary and no subprocess: a caller that wants a summary invokes url-get from a delegated subagent. Use instead of WebFetch when a faithful copy is needed (e.g. ingesting your own published writing). |

### {{productName}} Plugins (user-selectable)

These are enabled during onboarding and can be added or removed at any time. Some plugins enhance a specific specialist role — when enabled, that specialist gains additional capabilities.

| Plugin | What it does | Enhances |
|--------|-------------|----------|
| `business-assistant` | Customer enquiries, scheduling, quoting, invoicing, daily briefings | Personal assistant |
| `sweep` | Catch-up composition — on "catch me up" / "what did I miss" / "morning sync" / "sweep", reads every connected inbox, calendar, and contact source, plus the site captures published sites write to Cloudflare D1 (enquiry forms, e-signatures, and other form completions), dedupes against the graph, and proposes to-dos, calendar items, contacts, and captured enquiries for approval. Composes `work`, `scheduling`, `contacts`, and `memory`; adds no tools and never sends. | Personal assistant |
| `sales` | Buying signal detection, closing techniques, objection handling | Personal assistant |
| `deep-research` | Structured multi-source research — query decomposition, source evaluation, citations | Research assistant |
| `projects` | Structured project execution — phased sprints, investigations, reviews, retrospectives, plus method references for stakeholder mapping, risk pre-mortems, task prioritization, and meeting-to-tasks | Project manager |
| `telegram` | Telegram bot — BotFather setup, messaging, channels | Personal assistant |
| `whatsapp` | WhatsApp messaging, pairing, and conversation browsing | Personal assistant |
| `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
| `web-designer` | Design and build a single-page website for any business and put it live on Cloudflare — guided intake, brand extraction from a URL, a domain-neutral builder, social tiles, and deploy with D1 form capture, gated access, and e-signing. Composes `cloudflare` deploy and `business-assistant` e-signing; adds no specialist and writes to no graph. | Content producer |
| `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
| `notion-import` | Import a Notion workspace export (markdown + CSV) — pages, databases, hierarchy, attachments, schema-bounded relations, `@person` mentions account-filtered | Librarian |
| `obsidian-import` | Import an extracted Obsidian vault — pages map to `:KnowledgeDocument`, wikilinks resolve to intra-vault pages or existing entities, tags become `:DefinedTerm`, embedded images become `:DigitalDocument`. Two-phase tool (dry-run → operator disambiguation → commit). | Librarian |
| `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `sessionId` ingests as one `:ConversationArchive` (`source='x-dm'`, keyed on `conversationIdentity`) via `conversation-archive-ingest.sh`. Mentions, replies, and quote-tweet authors resolve to `:Person` on lowercased `xHandle`; every handle and DM senderId confirms against existing nodes (no auto-create). Per-thread KD granularity and `:Post` / `:DirectMessage` labels are explicitly rejected. | Librarian |
| `substack-import` | Import a Substack "Export your data" archive — per-essay `:KnowledgeDocument {kind:'substack-post'}` via librarian/document-ingest with synthetic stable `attachmentId = "substack-post-${substackPostId}"` (survives Substack edits); one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` per import run with `:MENTIONS {mentionContext:'substack-subscription', tier, totalOpens, totalClicks, lastOpenedAt, lastClickedAt, engagementWindowDays}` to each subscriber `:Person` MERGEd on `(accountId, email)`. Engagement aggregates parsed from `email_activity.csv` (or `subscriber_activity.csv` / `emails.csv`); overwrite-on-reimport. No new label, no new edge type, no new graph writer. Images attach via canonical `:HAS_ENCLOSURE` (or `:MENTIONS` fallback). Bulk-gate at >200 posts or >2000 subscribers. | Librarian |
| `memory/skills/conversation-archive` | Source-agnostic conversation transcript ingest. One skill for WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom transcript, meeting minutes, iMessage, Slack, X DMs — `--source <enum>` selects the per-source normaliser. Single Bash entry — `bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source <enum> --participant-person-ids <csv> --scope <admin\|public>` — runs normalise → operator-confirms owner + every distinct sender (owner derived from env via Cypher, no flag) → sessionize at the fixed 8h gap → emit one JSON line carrying prepared sessions (turn-attributed text + per-session cursor). The dispatched specialist iterates the sessions in-turn, produces a typed-section JSON chunking for each, and calls the `memory-ingest` MCP tool with `conversationIdentity` set (writes `:ConversationArchive`, source=<enum>) once per session — chunks + cursor advance commit atomically inside one Cypher transaction, so a kill mid-archive resumes from the next session on re-issue without re-classifying anything already written. Re-imports are delta-append. Auto-creating participants is forbidden — any sender outside the operator-confirmed closed set LOUD-FAILs with `parser-miss`. Distinct from the live `whatsapp` plugin (Baileys). | Database operator |
| `memory/skills/conversation-archive-enrich` | Phase 2 for any named `:ConversationArchive` — source-agnostic per-row insight derivation. Operator-triggered (never auto-fires on Phase 1 completion). Walks the parent's `:Section` chunks in pages via the read-only MCP tool `mcp__plugin_memory_memory__conversation-archive-list-chunks`; the dispatched specialist reads each chunk in-turn and emits claims under the four-kind contract (`mention`, `task`, `preference`, `observed-relationship`); the skill hands those claims to `mcp__plugin_memory_memory__conversation-archive-derive-insights` for per-kind cypher emission, then runs the per-row operator gate (`wire / skip / reject`). Idempotent on `(elementId(chunk), kind, contentHash)` — re-runs collapse identical claims. Confidence floor is a hedging-avoidance instruction the skill embeds in the specialist's per-chunk prompt, not a numeric post-filter; the LLM step runs in-turn from the dispatched specialist rather than as a server-side OAuth round-trip. | Database operator |

### Claude Official (marketplace)

Third-party plugins from the Claude marketplace:

| Plugin | What it does |
|--------|-------------|
| `stripe` | Live access to payment and business data |

### Claude Anthropic Verticals (marketplace, opt-in)

Optional plugins from Anthropic's vertical marketplaces. The installer registers `claude-for-financial-services` and `knowledge-work-plugins` so the install commands work; none are auto-installed. You pick each deliberately during first-run onboarding (Step 1) or by name at any time.

| Plugin | Marketplace | What it does |
|--------|-------------|-------------|
| `kyc-screener` | `claude-for-financial-services` | Parses onboarding documents, runs a rules engine, flags gaps. Outputs are draft work product for human review — your compliance specialist owns sign-off. Relevant to UK estate agents under MLR 2017. |
| `meeting-prep-agent` | `claude-for-financial-services` | Briefing pack before every client meeting, FSI-flavoured templates. Overlaps with the business-assistant calendar-prep flow — choose one deliberately. |
| `pdf-viewer` | `knowledge-work-plugins` | Live interactive viewer to view, annotate, and sign PDFs — mark up contracts, fill forms, stamp approvals, place signatures, download the annotated copy. Click-through replaces conversation for this surface (v0.2.0, different shape from chat-driven skills). |

Install verbatim:

- `claude plugin install kyc-screener@claude-for-financial-services`
- `claude plugin install meeting-prep-agent@claude-for-financial-services`
- `claude plugin install pdf-viewer@knowledge-work-plugins`

### Premium Plugins

Brand decides which premium plugins ship. The brand's `shipsPremiumBundles` field in `brand.json` is the gate; three shapes are supported:

- **omitted / false** — ship nothing from `premium-plugins/` (the legacy Real Agent default).
- **`true`** — ship every bundle under `premium-plugins/*` (Real Agent / `realagent-code`).
- **`["bundle-a", "bundle-b"]`** — ship only the named bundle directories (Real Agent Code's `["venture-studio"]`). Names with no matching directory on disk are silently dropped; non-allowlisted bundles are stripped from any account that was previously stamped with them.

There is no per-account purchase record; the brand decides the shipping set.

| Plugin | Type | What it does | Public agent |
|--------|------|-------------|-------------|
| `teaching` | Skills | Interactive tutoring, lesson planning, and study pack generation from your knowledge base | Yes — all 3 skills serve students and parents |
| `real-agent` | Bundle (13 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM (five value pillars: auto-respond, viewing lifecycle, pipeline mining, listings prospecting, maintenance & preferences), PropertyData market analytics (valuation, sold prices, £/sqft baselines, £/sqft growth, demand-rent, area risk, planning precedent, UPRN matching, property-type distribution), gov.uk EPC floor-area lookup, property brochures, social-share image cards, A4 market reports, and single-address preval packs (full UK address → 4-page A4 PDF covering valuation, area, and demand). 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
| `writer-craft` | Skills + Agent | Manuscript review and writing craft — story architecture, reader engagement, prose craft, editorial practice, and multi-level review | No — writing craft serves the author |
| `venture-studio` | Skills + Agent | Founding-a-business workflow — office-hours discovery, brand pack, zero-to-prototype validation, the full investor data room (business plan, prospectus, term sheet, deck blueprint, A4 print pipeline), and a `legal-essentials` skill that drafts an NDA and a UK-GDPR/PECR privacy policy. Carries citation-routed method references for market sizing (TAM/SAM/SOM), pricing, monetization, competitor analysis, battlecards, strategy frameworks (SWOT/PESTLE/Porter's/Ansoff), and a pre-fundraise strategy red-team. Pre-seeds a `Project` with one `Task` per artefact so nothing gets forgotten. | No — founder-facing only |
| `management-consulting` | Skills | 21 MBB-style strategy skills for client engagements — situation assessment, growth-barrier and assumption diagnosis, market mapping, competitive intel, segmentation, profit-pool analysis, strategic options, pricing, business cases, portfolio review, operating-model design, initiative prioritization, transformation roadmaps, war-gaming, risk registers, KPI design, value realization, stakeholder alignment, executive narrative, and decision memos. No MCP server. | No — consultant-facing only |

**How it works:** Every boot {{productName}} delivers the brand's premium plugins from staging into `platform/plugins/` and stamps `enabledPlugins` against what is actually on disk. No conversation needed — the brand's full set is active from the first turn after install. Updates and reinstalls re-deliver from staging.

Some premium plugins are **bundles** — multiple sub-plugins shipped under one directory in `premium-plugins/`, each independently activatable. For example, Real Agent ships 10 sub-plugins covering different aspects of estate agency work. They are all enabled by default. Sub-plugins you don't want active can be turned off individually with "disable <name>"; enabling or disabling individual sub-plugins does not affect the others.

A bundle's sub-plugins carry the skills, so each must be registered as its own Claude Code plugin for its skills to load — the installer does this at install time for every bundle the brand ships. When a sub-plugin's skills are missing (a specialist dispatches but its skill answers *Unknown skill*), the manager names the gap at boot: grep `server.log` for `[skill-registration] op=drift` — each line gives the account, the sub-plugin, and which registration surface is missing. The fix is to re-run the installer for this brand; a clean boot instead logs `startup-self-test skill-registration=ok`.

The same split applies to a premium plugin's **tools**: its MCP server mounts only when the generated plugin manifest carries an `mcpServers` block (set by `mcp-manifest: auto` in the plugin's `PLUGIN.md`). When a tool-plugin is enabled but its server never mounts — its tools are listed but every call finds an empty toolbox — the manager names it at boot: grep `server.log` for `[tool-registration] op=drift`, which gives the account, the plugin, and the reason (`no-mcpServers-manifest`, `dist-missing`, or `not-installed`). A clean boot instead logs `startup-self-test tool-registration=ok`.

If you ask {{productName}} about a tool from a plugin your brand does not ship (for example, a Real Agent install asking about a Real Agent Loop CRM tool), {{productName}} responds with a structured `<tool-surface-error>` envelope naming the missing plugin and the remedy, rather than improvising with a generic alternative.

**Public agent embedding:** Premium plugins marked as public-eligible have their full content (skills and reference knowledge) embedded in public agent prompts. This means a public agent for a Real Agent member can handle buyer enquiries, book viewings, deliver coaching content, and onboard new applicants — all powered by the premium plugin's domain knowledge. Plugins marked admin-only (listings, vendors, leads, business) are only available to the account owner's admin agent.

Some premium plugins include specialist helpers that {{productName}} can dispatch for specific tasks (e.g. the writer-craft plugin includes a manuscript reviewer). These are activated automatically when the plugin is enabled.

Some premium plugins include pre-built public agent templates — ready-made configurations for customer-facing agents. When you enable the plugin, {{productName}} shows you what templates are available and offers to create agents from them. You review and approve every file before the agent is created. The template is a starting point — you can edit the identity, personality, plugins, and settings to make it yours. The result is a standard public agent, indistinguishable from one you created from scratch.

Some premium plugins ship pre-built workflows that are created when the plugin is enabled. These workflows are fully yours — you can inspect, edit, run, and manage them through conversation, exactly like workflows you create yourself. The plugin provides the starting point; you own the result.

**If a premium plugin ever stops working** — `documents`, `teaching`, anything else you've paid for — and {{productName}} responds as if it doesn't have those tools, the platform's health check (`/api/health.missingPlugins`) will name the affected plugin. Tell {{productName}} "deliver the {{plugin}} plugin" — it re-runs the same delivery step that fires automatically at session start. If the plugin isn't in the device's staging area, re-run the installer for this brand.

## Choosing Plugins

During first-time setup, {{productName}} presents a plugin selection screen where you choose which plugins to activate. Core plugins are pre-selected and locked. Recommended plugins are pre-selected but optional. You can change your mind later.

## Adding or Removing Plugins

Tell {{productName}}:

- "Enable the Telegram plugin"
- "Add the Stripe plugin"
- "Disable the deep-research plugin"

{{productName}} handles the installation or removal. If the plugin requires any setup (API keys, bot tokens, configuration), {{productName}} will walk you through it.

## Viewing Your Plugins

Ask {{productName}}: "What plugins do I have?" or "List my plugins."

## Operator-Authored Plugins (skill-builder output)

Skills you create at runtime through the admin `skill-builder` skill are saved on disk as their own plugin under `data/accounts/{accountId}/plugins/{pluginName}/`. The admin agent calls `mcp__plugin_admin_admin__store-skill`, which composes `PLUGIN.md` (on first call) and `skills/{skillName}/SKILL.md` plus any reference files. The agent supplies `pluginName`, `skillName`, `description`, `publicEmbed`, `body`, and optional references — the path is computed by the tool, never by the agent.

These operator-authored plugins survive reinstall because the installer's wipe zone excludes `data/`. They are loaded directly from `data/accounts/{accountId}/plugins/*`: the spawn manifest and the admin skill resolvers (`skill-load`, `skill-find`, `skill-search`, `plugin-read`) walk that per-account root alongside the shipped and premium plugin roots, so no copy into the platform plugins tree is made or needed. `store-skill` confirms the written skill resolves through that same walk before reporting success. Operator plugins are always active for their own account; the admin agent sees every operator skill by default, and per-skill `publicEmbed: true|false` controls which skills surface to the public agent.

To edit an operator-authored skill later, ask {{productName}} to update it — the admin agent re-runs `store-skill` for the same `pluginName`/`skillName` and the new content overwrites in place. To remove one, delete the directory under `data/accounts/{accountId}/plugins/{pluginName}/skills/{skillName}/` (or the whole plugin) — the next session no longer surfaces it, since skills load directly from that path.

`pluginName` collisions with shipped plugin names are refused by `store-skill` with a structured error. See [.docs/agents.md](../../../../.docs/agents.md) § "Operator-authored skills as plugin files" for the full contract.

## Brand Templating (for plugin and skill authors)

Skill content, plugin manifests, agent templates, and reference files reference the operator-visible brand name only via the literal `{{productName}}` placeholder. The platform substitutes from `brand.json.productName` at read time — Real Agent installs render `Real Agent`, Real Agent installs render `Real Agent`, all from the same source content.

**Author rule:** never write the literal string `Real Agent` (or any brand name) in shipped skill, plugin, or template content. Use `{{productName}}` whenever the operator should see the brand name. The audit grep `grep -rn "\bMaxy\b" platform/plugins/admin/skills/ platform/plugins/*/skills/ platform/templates/agents/` must return zero matches; a literal brand name is a defect, not a stylistic choice.

The runtime substitution happens at every read site that flows content into a system prompt or operator-visible UI: the admin agent's `plugin-read` tool (references + `PLUGIN.md`), the `skill-load` tool (SKILL.md by skill name — one-call resolver+reader, the canonical primitive for SKILL.md), the public agent's recursive plugin assembly, and `IDENTITY` / `SOUL` / `AGENTS` / `KNOWLEDGE` markdown reads. Missing or empty `productName` hard-fails — there is no fallback to a default brand string. See [.docs/agents.md](../../.docs/agents.md) § "Brand templating" for the full contract.

## Tool risk classes (for plugin authors)

Every tool a plugin declares in its `PLUGIN.md` `tools:` block carries three mandatory
fields. `publicAllowlist` and `adminAllowlist` say who may call the tool. `riskClass`
says what happens when they do, and takes one of four values:

- `read` changes no state anywhere. A remote lookup counts as `read`; reaching the
  network is not by itself consequential.
- `write_local` mutates account files or the graph on this device and transmits nothing.
- `exec` runs an operating-system command, or code supplied by the caller or the operator.
- `external` transmits to, or changes state on, a system outside this device, or delivers
  a message to a person.

Leaving the field out is a refusal-to-boot condition, not a silent default. The platform
refuses to start rather than treat an undeclared consequence as harmless, because a tool
that quietly reads as `read` escapes every check downstream of it.

Declaring the class does not yet gate anything. It gives any consumer one honest answer
to the question "is this tool consequential?" without maintaining a list of tool names,
which is what the checks in `platform/plugins/admin/hooks/` have had to do until now.

A publish-time gate flags declarations that contradict what the tool's own name says it
does, and a boot line reports the census across all four classes. The classification
rule, the exception mechanism, and the diagnostic commands are in
`.docs/tool-risk-classes.md`.

## MCP Plugin Observability (for plugin authors)

Every `console.error` line from a plugin's MCP server is captured to a per-server raw log. Capture is automatic and needs no import or call: the `mcp-spawn-tee` shim spawns every MCP server and mirrors its stderr to `mcp-<name>-<sessionId>.log` (or `mcp-<name>-nosession.log` for a spawn with no session). That file is keyed by session, which is the conversation, and that is where you grep. Prefix your diagnostics `[your-tool]` so a grep isolates one tool.

There is no per-conversation stream log that interleaves MCP stderr with agent events. An investigator who greps a conversation's stream log for `[mcp:<name>]` and finds nothing has looked in the wrong place, not found silence: the per-server raw file above is the record.

**Bash commands stream straight into the PTY.** Real Agent Code's admin and public chat run on the native Claude Code PTY. The per-conversation server-side stream log that the retired web-UI dispatcher tailed is gone; agent-invoked Bash commands (including direct `cloudflared` invocations for Cloudflare setup) print their stdout and stderr directly, and the PTY renders the output in chat verbatim.

**Retrieve MCP diagnostic lines for a conversation:**

- `logs-read { type: "mcp" }` → tails the most recent `mcp-<name>-*.log` (per-server, keyed by session).

## Failure-path observability contract

The raw sink is written with `appendFileSync`, which returns once the kernel has queued the write, so a diagnostic `console.error(…)` followed by an immediate `process.exit(…)` still lands. There is no async buffer to lose it. The same holds for a synchronous module-load throw: Node writes the stack to raw fd 2, which the shim is already teeing, before the process exits.

**Two layers close the gap, each load-bearing on its own:**

1. **Plugin-side sync-write discipline.** A plugin that calls `process.exit` during module load (rare; `graph-mcp` is the in-tree example, spawning a child at boot to proxy upstream stdio) writes its failure cause to `process.stderr` synchronously at every named exit path, so the cause reaches the raw sink before the process terminates. The write is wrapped in its own try/catch, so a closed stderr does not mask the primary failure. This is the discipline any plugin author who knows their failure paths should follow.

2. **Platform-side `mcp-spawn-tee` shim.** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` shim rather than `node <entry>` directly: Claude Code runs `node <shim> <entry>`, and the shim imports the real entry into its own process via dynamic `import()` — one node runtime, not two, and the shim is not the entry's parent. It replaces `process.stderr.write` with a tee before importing, so every stderr chunk the entry writes lands in `${LOG_DIR}/mcp-${name}-${SESSION_ID}.log` (or `mcp-${name}-nosession.log` when `SESSION_ID` is unset) via `appendFileSync` and is still passed through to the real stderr (Claude Code's consumer is unchanged). It is the only writer of that raw sink: the `mcp-${name}-stderr-<date>.log` twin, which a since-removed tee also wrote, has been retired — every line used to land there twice. Synchronous `appendFileSync` survives `process.exit`, so the sink captures even (a) a module-load throw before any plugin logging runs, (b) `MODULE_NOT_FOUND` on the entry script itself, and (c) anything else a plugin author missed. The shim emits `[mcp-helper] op=spawn session=<id8> server=<name> pid=<n> entry=<path>` before it imports the entry, and installs its own `SIGTERM`/`SIGINT`/`SIGHUP` handlers — there is no child to forward a signal to. It drives the exit itself only when the imported entry installed no handler of its own; when the entry has one, the shim defers and the entry's handler decides the exit code. This is the layer that makes capture independent of plugin discipline. Playwright stays unwrapped because it spawns via `npx`, not `node`.

No `[mcp-init-error]` probe exists in live code; `claude-agent.ts` has not carried one for some time. The shim's own `[mcp-helper] op=boot` and `op=exit` lines are the record of a failed MCP boot.

**Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-helper] op=spawn session=<id8> server=<name> pid=<n> entry=<path>` (proof the shim started, and which entry it loaded), `[mcp-helper] op=boot session=<id8> server=<name> head=<first stderr bytes>` (proof the entry reached its own stderr), `[mcp-helper] op=exit session=<id8> server=<name> pid=<n> code=<n> signal=<s> lifetimeMs=<n> stderr-tail=<…>` (proof the shim saw the exit), `[mcp-helper] op=error session=<id8> server=<name> reason=<…>` (no entry was given, or the entry failed to import), and optionally `[<plugin>] …` lines from plugin-side sync-writes. Their union gives the investigator independent sources for the same failure. `op=exit` fires from the shim's own `process.on("exit")`, so it covers a clean exit, a non-zero exit, a post-load uncaught throw, and a catchable signal — an `op=spawn` with neither a matching `op=exit` nor an `op=error` is an uncatchable `SIGKILL`, not a broken shim. (`op=error` is the entry failing to import; that path suppresses `op=exit`.)

**Boot-smoke as publish-time gate.** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
