# Retire `mcp-stderr-tee` — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Delete the `mcp-stderr-tee` module, its 21 call sites, and every doc claim that a per-conversation MCP stream log exists — leaving `mcp-spawn-tee`'s per-session raw sink as the single, honest MCP-stderr surface.

**Architecture:** This is removal work, not feature work. Each task asserts the current state (grep/build), makes the deletion, then asserts the new state. There is no new runtime behaviour to test; the invariants are grep counts and a clean build. The order is fixed: remove the importers before the module they import, or the build breaks mid-plan.

**Tech Stack:** TypeScript (platform workspace at `maxy-code/platform/`), npm workspaces, Node ≥ 22.6.

## Global Constraints

- Work happens in worktree `worktree-task-1736-retire-mcp-stderr-tee`. All paths below are relative to its `maxy-code/` subdirectory unless absolute.
- Node ≥ 22.6: `export PATH="$HOME/.nvm/versions/node/v22.22.0/bin:$PATH"` before any node/npm command.
- Deps already installed at `maxy-code/platform/` via `npm install --ignore-scripts`.
- **Collision guard (Task 5):** task 1737 owns `mcp-spawn-tee`'s stale marker names (`[mcp-spawn-tee-attached]`, `[mcp-spawn-tee-exit]`). Do not rename, restate, or fix them. Where a paragraph this plan edits also contains one, edit only the `initStderrTee`/`STREAM_LOG_PATH` clause and leave the stale name untouched for 1737.
- **Success invariants (whole plan):** after all tasks, in the live tree (excluding `.tasks/archive/` and `docs/superpowers/`): `initStderrTee` → 0 hits, `mcp-stderr-tee` → 0 hits, `STREAM_LOG_PATH` → 0 hits.
- Shipped-doc prose rule: the rewritten `plugins-guide.md` prose carries zero em-dashes (per repo doctrine for shipped markdown).

---

### Task 1: Remove the 21 `initStderrTee` call sites

**Files:**
- Modify: 20 plugin MCP entry files `platform/plugins/<name>/mcp/src/index.ts` for name in: admin, aeo, browser, connector, contacts, email, filesystem, graph-viewer, joblogic, memory, quickbooks, replicate, scheduling, storage-broker, telegram, url-get, voice-mirror, whatsapp, work, workflows
- Modify: `platform/lib/graph-mcp/src/index.ts` (import line 31, call line 63 — NOT adjacent; leave `syncEmit` for Task 3)

**Interfaces:**
- Consumes: nothing.
- Produces: nothing. This removes a call; no symbol is exported.

**Non-uniform sites to handle by hand, not by a blind 2-line delete:**
- `voice-mirror`: import is line 1, call is line 3. Line 2 is `import { lifelineTool } ...` — KEEP line 2.
- `graph-mcp`: import line 31 (`import { initStderrTee } from "../../mcp-stderr-tee/dist/index.js";`), call line 63 (`initStderrTee(SERVER_NAME);`). Two separate deletions. Keep line 30 (`StringDecoder`, used at line 1001) and `SERVER_NAME` (used by `syncEmit` at line 83).
- All other 19: import and call are adjacent (e.g. memory lines 1-2; whatsapp lines 8-9; admin 1-2; storage-broker 8-9; url-get 9-10; graph-viewer 7-8). Delete both lines.

- [ ] **Step 1: Assert the starting count is 21**

Run:
```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
/usr/bin/grep -rl 'initStderrTee' platform/plugins/*/mcp/src/index.ts platform/lib/graph-mcp/src/index.ts | wc -l
```
Expected: `21`

- [ ] **Step 2: Remove the import + call from each of the 20 plugin files**

For the 19 adjacent sites, delete the `import { initStderrTee } ...` line and the `initStderrTee("<name>");` line. For `voice-mirror`, delete only the import (line 1) and the call (line 3), preserving the `lifelineTool` import. Use Edit per file with the exact two strings; do not delete by line number blindly (line numbers shift as you go).

- [ ] **Step 3: Remove the import + call from graph-mcp**

Delete `import { initStderrTee } from "../../mcp-stderr-tee/dist/index.js";` (line 31) and `initStderrTee(SERVER_NAME);` (line 63). Leave the surrounding `StringDecoder` import and `SERVER_NAME` const in place.

- [ ] **Step 4: Assert zero call sites remain**

Run:
```bash
/usr/bin/grep -rn 'initStderrTee' platform/plugins platform/lib | /usr/bin/grep -v node_modules | /usr/bin/grep -v '/dist/'
```
Expected: no output.

- [ ] **Step 5: Build the lib dependency dists (needed for plugin typecheck), then typecheck the plugins**

The plugin MCPs import sibling lib dists (`mcp-lifeline` etc.). Build libs first, EXCLUDING the soon-to-be-deleted `mcp-stderr-tee` (its dist may still exist from install; that's fine for this step — it just won't be imported anymore).

Run:
```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code/platform
export PATH="$HOME/.nvm/versions/node/v22.22.0/bin:$PATH"
npm run build:lib
NODE_OPTIONS='--max-old-space-size=8192' npx tsc -b plugins/*/mcp/tsconfig.json lib/graph-mcp/tsconfig.json
```
Expected: exit 0, no errors. A wall of `TS7031 implicitly any` here means the lib dists did not build — fix setup, do not touch code (per project memory).

- [ ] **Step 6: Commit**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee
git add -A
git commit -m "$(printf 'refactor: remove 21 initStderrTee call sites\n\nThe call is a no-op in production (STREAM_LOG_PATH is never set).\nmcp-spawn-tee already captures the same stderr per session.\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')"
```

---

### Task 2: Delete the module and its build entries

**Files:**
- Delete: `platform/lib/mcp-stderr-tee/` (whole directory)
- Modify: `platform/package.json` lines 9 (`build`) and 10 (`build:lib`) — remove ` && tsc -p lib/mcp-stderr-tee/tsconfig.json` from both

**Interfaces:**
- Consumes: nothing (Task 1 removed all importers).
- Produces: nothing.

- [ ] **Step 1: Assert no importer remains before deleting**

Run:
```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
/usr/bin/grep -rl 'mcp-stderr-tee/dist' platform premium-plugins | /usr/bin/grep -v node_modules | /usr/bin/grep -v '/dist/' | /usr/bin/grep -v '.tasks/'
```
Expected: no output. (If any hit, return to Task 1 — deleting the module now would break that importer.)

- [ ] **Step 2: Delete the module directory**

```bash
git rm -r platform/lib/mcp-stderr-tee
```

- [ ] **Step 3: Remove both tsconfig entries from package.json**

Edit `platform/package.json`: in the `build` script (line 9) and the `build:lib` script (line 10), delete the exact substring ` && tsc -p lib/mcp-stderr-tee/tsconfig.json` (it appears once per line, immediately after `lib/models/tsconfig.json`).

- [ ] **Step 4: Verify build:lib still runs clean without the entry**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code/platform
export PATH="$HOME/.nvm/versions/node/v22.22.0/bin:$PATH"
npm run build:lib
```
Expected: exit 0. No reference to `mcp-stderr-tee`.

- [ ] **Step 5: Commit**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee
git add -A
git commit -m "$(printf 'refactor: delete mcp-stderr-tee module and its build entries\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')"
```

---

### Task 3: Neutralize the remaining `STREAM_LOG_PATH` readers

Two readers survive Task 1: graph-mcp's `syncEmit` writes `[mcp:graph]` to `process.env.STREAM_LOG_PATH` on exit paths, and the memory boot-smoke harness sets it. Both point at the same never-produced destination. The `syncEmit` stderr fallback already reaches the `mcp-spawn-tee` sink, so removing the dead branch loses nothing.

**Files:**
- Modify: `platform/lib/graph-mcp/src/index.ts` — `syncEmit` (approx lines 66-92): remove the `STREAM_LOG_PATH` branch and its now-unused locals
- Modify: `platform/plugins/memory/mcp/scripts/boot-smoke.sh:46` — remove the `STREAM_LOG_PATH="$LOG_DIR/stream.log" \` line

**Interfaces:**
- Consumes: nothing.
- Produces: `syncEmit(line: string): void` — unchanged signature, still called at all 5 exit paths; only its body shrinks.

- [ ] **Step 1: Assert the three graph-mcp hits and one boot-smoke hit exist**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
/usr/bin/grep -n 'STREAM_LOG_PATH\|streamLogPath' platform/lib/graph-mcp/src/index.ts
/usr/bin/grep -n 'STREAM_LOG_PATH' platform/plugins/memory/mcp/scripts/boot-smoke.sh
```
Expected: 3 graph-mcp lines (80, 81, 83), 1 boot-smoke line (46).

- [ ] **Step 2: Rewrite `syncEmit` to drop the dead branch**

Replace the current body (which reads `const iso`, `const streamLogPath = process.env.STREAM_LOG_PATH`, the `if (streamLogPath) { try { appendFileSync(...) } catch {} }` block, then the stderr write) with:

```typescript
function syncEmit(line: string): void {
  const msg = `[graph-mcp] ${line}`;
  // The mcp-spawn-tee shim (PLUGIN.md spawns this entry under it) installs an
  // appendFileSync-backed stderr writer, so this line reaches the raw sink
  // synchronously and survives an immediate process.exit().
  try {
    process.stderr.write(`${msg}\n`);
  } catch {
    /* stderr closed — nothing else to do */
  }
}
```

This drops the `iso` local and the `appendFileSync` call. Then remove `appendFileSync` from the `node:fs` import at line 28 (it has no other use — the file uses `accessSync`, `constants`, `readFileSync`, `statSync`). Verify before removing:

```bash
/usr/bin/grep -n 'appendFileSync' platform/lib/graph-mcp/src/index.ts
```
Expected after the syncEmit rewrite: only the line-28 import mention remains, so remove it from the import list.

- [ ] **Step 3: Remove the boot-smoke assignment**

Delete the line `  STREAM_LOG_PATH="$LOG_DIR/stream.log" \` from `platform/plugins/memory/mcp/scripts/boot-smoke.sh` (line 46, inside the `env` block). The surrounding `LOG_DIR=` and `NEO4J_URI=` lines stay.

- [ ] **Step 4: Assert zero `STREAM_LOG_PATH` hits in the live tree**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
/usr/bin/grep -rn 'STREAM_LOG_PATH' platform premium-plugins packages | /usr/bin/grep -v node_modules | /usr/bin/grep -v '/dist/' | /usr/bin/grep -v '.tasks/' | /usr/bin/grep -v 'docs/superpowers/'
```
Expected: no output.

- [ ] **Step 5: Typecheck graph-mcp and confirm boot-smoke still parses**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code/platform
export PATH="$HOME/.nvm/versions/node/v22.22.0/bin:$PATH"
npx tsc -p lib/graph-mcp/tsconfig.json
bash -n plugins/memory/mcp/scripts/boot-smoke.sh
```
Expected: both exit 0.

- [ ] **Step 6: Commit**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee
git add -A
git commit -m "$(printf 'refactor: drop the two remaining dead STREAM_LOG_PATH readers\n\ngraph-mcp syncEmit and the memory boot-smoke harness both wrote to a\nvariable no spawner sets. syncEmit keeps its stderr write, which the\nmcp-spawn-tee shim already captures to the raw sink.\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')"
```

---

### Task 4: Strip the three comment-only references

**Files:**
- Modify: `platform/services/claude-session-manager/src/rc-daemon.ts:460` — comment naming `mcp-stderr-tee`
- Modify: `platform/scripts/logs-rotate.sh:182` — comment naming `mcp-stderr-tee`
- Modify: `packages/create-maxy-code/scripts/bundle.js:563` — the lib-rewrite comment example

**Interfaces:** none — comments only.

- [ ] **Step 1: Read each comment in context**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
sed -n '458,462p' platform/services/claude-session-manager/src/rc-daemon.ts
sed -n '180,184p' platform/scripts/logs-rotate.sh
sed -n '561,565p' packages/create-maxy-code/scripts/bundle.js
```

- [ ] **Step 2: Edit each so the sentence stays true without the dead module name**

- `rc-daemon.ts:460`: the comment says the sink is off `mcp-stderr-tee` "which no longer reads LOG_DIR". Reword to describe the current state without the retired module (the sink is `mcp-spawn-tee`'s `mcp-<name>-<SESSION_ID>.log`).
- `logs-rotate.sh:182`: the comment references why an earlier draft could not purge `mcp-*` because `mcp-stderr-tee` held the file open. Reword to name `mcp-spawn-tee` (the current holder) or drop the historical clause.
- `bundle.js:563`: change the example path in the comment from `mcp-stderr-tee` to a lib premium plugins actually import (e.g. `mcp-lifeline`). Verify the replacement is real:
  ```bash
  /usr/bin/grep -rl 'mcp-lifeline/dist' premium-plugins | /usr/bin/grep -v node_modules | head -1
  ```
  Expected: at least one premium plugin imports it.

- [ ] **Step 3: Assert zero `mcp-stderr-tee` hits in the live tree**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
/usr/bin/grep -rn 'mcp-stderr-tee' platform premium-plugins packages | /usr/bin/grep -v node_modules | /usr/bin/grep -v '/dist/' | /usr/bin/grep -v '.tasks/' | /usr/bin/grep -v 'docs/superpowers/' | /usr/bin/grep -v 'plugins/docs/references/plugins-guide.md' | /usr/bin/grep -v 'platform-architecture/SKILL.md'
```
Expected: no output. (plugins-guide.md and SKILL.md are handled in Tasks 5-6.)

- [ ] **Step 4: Commit**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee
git add -A
git commit -m "$(printf 'refactor: strip dead mcp-stderr-tee name from three comments\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')"
```

---

### Task 5: Rewrite `plugins-guide.md` so it stops claiming a per-conversation stream log

**Files:**
- Modify: `platform/plugins/docs/references/plugins-guide.md` (the `## MCP Plugin Observability` section, ~lines 153-197)

**Interfaces:** none — documentation.

**Keep / remove / rewrite map** (verify line numbers against the file first — they shift as you edit):

| Content | Action |
|---|---|
| 155 intro ("Every `console.error` line ... teed into the per-conversation stream log") | REWRITE: `console.error` from a plugin's MCP server is captured by the `mcp-spawn-tee` shim into the per-session raw file `mcp-<name>-<SESSION_ID>.log`. |
| 157-161 Opt-in block (`import { initStderrTee }` + call) | REMOVE entirely — no opt-in; capture is automatic under the shim. |
| 163 ("After this ... `mcp-your-plugin-name-{sessionId}.log` ... That is where you grep") | KEEP — true; it names the real sink. Merge with the rewritten intro. |
| 166 ("`initStderrTee` is *intended* to also mirror ... no-op in production ... Tracked as 1736") | REMOVE — the whole reason to retire; do not leave a "tracked as 1736" pointer to a closed task. |
| 168 Premium-plugins paragraph (ships `mcp-stderr-tee/{dist,package.json}`) | REMOVE — it documents shipping the deleted module. The generic bundler behaviour is not plugin-author guidance and lives in `bundle.js`. |
| 170 ("How the tee decides ... platform sets `STREAM_LOG_PATH` on every spawn") | REMOVE — false claim. |
| 174-177 Retrieve block: `logs-read {type:"system"}` → grep stream log | REMOVE the `type:"system"` bullet (returns nothing). KEEP the `type:"mcp"` bullet (real raw feed). |
| 179 Tee-state markers (`[mcp-tee-attach/skip/detach]` land in stream log) | REMOVE — no such log. |
| 181 Main-subprocess stderr (`[subproc-stderr]`) | REMOVE — describes the retired admin server. |
| 185 (`initStderrTee` `createWriteStream` write-race paragraph) | REMOVE — about the deleted module. |
| 187-191 "Two layers" — plugin-side sync-write + `mcp-spawn-tee` wrapper | KEEP, but in the wrapper paragraph edit only clauses that name `initStderrTee` (e.g. "module-load throws before `initStderrTee` runs" → "module-load throws before any plugin logging runs"). DO NOT touch `[mcp-spawn-tee-attached]` / SIGTERM-forwarding wording — that is 1737's collision surface. |

- [ ] **Step 1: Re-read the section with current line numbers**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
sed -n '153,197p' platform/plugins/docs/references/plugins-guide.md
```

- [ ] **Step 2: Apply the keep/remove/rewrite map**

Edit the section per the table. Prose carries zero em-dashes. The section should end shorter, asserting one surface: automatic capture by the shim into the per-session raw sink, retrievable via `logs-read {type:"mcp"}`, with the two-layer failure contract intact minus the `initStderrTee` clauses.

- [ ] **Step 3: Assert the false surfaces are gone and 1737's names are untouched**

```bash
/usr/bin/grep -n 'initStderrTee\|STREAM_LOG_PATH\|subproc-stderr\|mcp-tee-attach\|claude-agent-stream\|logs-read { type: "system"' platform/plugins/docs/references/plugins-guide.md
```
Expected: no output.
```bash
/usr/bin/grep -c 'mcp-spawn-tee-attached' platform/plugins/docs/references/plugins-guide.md
```
Expected: `1` (unchanged — still 1737's to fix, not zero).

- [ ] **Step 4: Commit**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee
git add -A
git commit -m "$(printf 'docs: plugins-guide no longer claims a per-conversation MCP stream log\n\nThe mcp-spawn-tee per-session raw sink is the single honest surface.\nLeaves 1737 stale marker names untouched.\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')"
```

---

### Task 6: Regenerate the architecture skill and pass the no-drift gate

**Files:**
- Regenerate: `platform/plugins/admin/skills/platform-architecture/SKILL.md` (and any brand twins) via the assembler
- Possibly regenerate: `docs/public/llms-full.*.txt` corpus via copy-docs

**Interfaces:** none — generated artefact.

- [ ] **Step 1: Refresh the doc corpus and reassemble the skill**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
export PATH="$HOME/.nvm/versions/node/v22.22.0/bin:$PATH"
node scripts/copy-docs.mjs 2>&1 | tail -3
node scripts/assemble-architecture-skill.mjs 2>&1 | tail -3
```
Expected: both complete without error. (If `copy-docs.mjs` needs args or a different path, read `check-architecture-skill-no-drift.mjs` for the exact corpus path it expects and match it.)

- [ ] **Step 2: Run the no-drift gate**

```bash
node platform/scripts/check-architecture-skill-no-drift.mjs
```
Expected: exit 0, no "out of sync" failure.

- [ ] **Step 3: Confirm the regenerated skill dropped the false surface**

```bash
/usr/bin/grep -n 'initStderrTee\|STREAM_LOG_PATH\|subproc-stderr' platform/plugins/admin/skills/platform-architecture/SKILL.md
```
Expected: no output.

- [ ] **Step 4: Commit**

```bash
git add -A
git commit -m "$(printf 'chore: regenerate platform-architecture skill after plugins-guide edit\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')"
```

---

### Task 7: Correct the reasoning in the task file and LANES

The archive MOVE happens in the sprint's Land phase (6c). This task fixes the CONTENT — both records currently argue "retire because 287/288 dismantled the consumers," which is wrong. Do this before the archive so the archived file carries the correct reason.

**Files:**
- Modify: `.tasks/1736-nothing-sets-stream-log-path-so-the-per-conversation-mcp-stream-log-does-not-exist.md` (reasoning already partly corrected in an earlier session; verify it names the real reason: `mcp-spawn-tee` already serves this consumer per session)
- Modify: `.tasks/LANES.md` row 1736 (same correction)

**Interfaces:** none — tracker prose.

- [ ] **Step 1: Verify current task-file reasoning names the real reason**

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1736-retire-mcp-stderr-tee/maxy-code
/usr/bin/grep -n 'mcp-spawn-tee already\|287\|288\|dismantled' .tasks/1736-*.md
```
The task file must state the retire reason as redundancy with `mcp-spawn-tee`, and must not present "287/288 retired the consumers" as the load-bearing argument. If the current text still leans on 287/288 as the reason, reword it. (Note: the reasoning correction may already be applied from a prior session; this step confirms rather than assumes.)

- [ ] **Step 2: Confirm LANES row 1736 matches**

```bash
/usr/bin/grep -n '287 retired the script_stream\|mcp-spawn-tee already writes' .tasks/LANES.md
```
Ensure the row's DECIDED block frames the reason as redundancy, with 287 named only as what it actually did (retired `script_stream`), not as the reason to retire this.

- [ ] **Step 3: Commit any corrections**

```bash
git add -A
git commit -m "$(printf 'docs: correct the retire reasoning in task 1736 and its LANES row\n\nSession: 71944c42-a91c-452a-96d2-b7bbcfe888fb')" || echo "nothing to correct — already accurate"
```

---

## Self-Review

**Spec coverage:** module deletion (T2), 21 call sites (T1), build entries (T2), boot-smoke assignment (T3), graph-mcp second reader (T3 — an addition the spec implied via "zero STREAM_LOG_PATH hits" but did not enumerate), comment refs (T4), doc rewrite (T5), skill regen (T6), reasoning correction (T7). The `logs-read {type:"system"}` open question is handled inside T5 (the doc bullet is removed) with the tool-level bug filed separately if implementation finds it reads a producerless file. All spec sections map to a task.

**Placeholder scan:** no TBD/TODO; every edit names exact files, strings, and expected grep output.

**Type consistency:** the only signature touched is `syncEmit(line: string): void` (T3), unchanged. No cross-task symbol references.

## Deviation from spec, flagged

Task 3's graph-mcp `syncEmit` edit is the one place the change rewrites live failure-path logic rather than deleting a no-op. It is in scope (it is a second reader of the same dead `STREAM_LOG_PATH`, and the spec's "zero hits" criterion requires it), and it is behaviour-preserving (the stderr write already reaches the sink). Called out here so a reviewer sees it deliberately, not buried in a bulk deletion.
