From 4da7ef0453914f8c0c36f1dfa04017f4c89db25b Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 5 Aug 2026 15:10:42 +0700 Subject: [PATCH] =?UTF-8?q?docs(lanes):=20design=20F1=20=E2=80=94=20ccam?= =?UTF-8?q?=20lanes=20mcp=20sync=20(F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scopes the first of F's four pieces to .mcp.json relocation + Chromium profile seeding only. Drops Shipyard's permission/auto-approval writes (no existing CCAM pattern for a backend command granting permissions, and the rules named agents not in scope: pr-reviewer has no driving skill to port, ticketer is a later task) and the node-version wrapper (environment-specific workaround, no evidence this repo needs it). --- .../specs/2026-08-05-mcp-sync-design.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-05-mcp-sync-design.md diff --git a/docs/superpowers/specs/2026-08-05-mcp-sync-design.md b/docs/superpowers/specs/2026-08-05-mcp-sync-design.md new file mode 100644 index 0000000..113c63d --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-mcp-sync-design.md @@ -0,0 +1,68 @@ +# F1 — `ccam lanes mcp sync` + +**Status:** approved 2026-08-05. Roadmap it belongs to: +`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **F**). + +## Problem + +`.claude/skills/ship-feature-lane/SKILL.md`'s Setup section documents today's fallback for a lane missing its required Playwright MCP servers: "tell the human to configure `.mcp.json` manually and restart the session." Shipyard's source, `lane-mcp-sync.sh` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/bin/lane-mcp-sync.sh`), automates this — but its source does three distinct things, only two of which belong in this repo's architecture. This is the first of F's four pieces (mcp-sync, CI-wait, tracker, dev-QC); it has no dependency on the other three. + +## Scope + +**In scope:** +1. `server/lib/lane-mcp.js` — reads the lane's `source_repo`'s already-configured MCP servers from `~/.claude.json`, relocates any absolute path under `source_repo` to the lane's own `cwd`, pins a `playwright`-family server's `--output-dir` to the lane's `.playwright-mcp`, writes `/.mcp.json`. +2. Seeding the lane's `.playwright-mcp/profiles/*` Chromium profile directories from the source repo's own (preserves saved MCP browser logins — a real QA-account login only needs to happen once per machine, not once per lane). +3. `.mcp.json` added to the lane's local `.git/info/exclude`. +4. `ccam lanes mcp sync []` CLI + `POST /api/lanes/:id/mcp/sync` route. +5. `SKILL.md` Setup section edit: replace the manual-fallback text with the real command. +6. `docs/LANES.md` + roadmap progress line. + +**Deliberately out of scope (deviates from Shipyard, by explicit decision — not an oversight):** +- **Permission/auto-approval writing.** Shipyard's script also writes broad `allow` rules into `/.claude/settings.local.json` — `enableAllProjectMcpServers`, an `mcp__` rule per synced server, a `gh pr comment/review/create/edit/view/diff/list/checks/status` + `git push` allow-list, and an `autoMode.allow` bypass-style rule that names the `ticketer`/`pr-reviewer` sub-agents specifically. CCAM has no existing pattern anywhere of a backend process writing permission grants on a session's behalf — every other primitive in this codebase (`proof-link`, `agents install`, `sync-base`) is explicit-and-inert until a session calls it, never a permission change. `pr-reviewer` isn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) and `ticketer` isn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approving `gh pr comment`/`git push` through the normal permission prompt, same as any other command, is the correct default until a real need for automation is demonstrated. +- **The `mcp-node-exec.sh` node-version wrapper.** Shipyard's script routes `npx`/`node`-based MCP servers through a wrapper that resolves the newest Node ≥ 18 at spawn time, working around an nvm/PATH resolution bug specific to that environment. Not a problem this repo has evidence of; add it later if it actually bites someone (YAGNI). +- **Merging with an existing `.mcp.json`.** This is a full overwrite from the source repo's current MCP config, same "reinstall, not merge" shape E3's `agents install` already established — not a per-server incremental sync. + +## Decisions + +| | Choice | Rationale | +|---|---|---| +| MCP server source | `~/.claude.json`'s `projects[lane.source_repo].mcpServers` | `source_repo` is already a lane field (every managed lane has one). This mirrors Shipyard's own approach exactly — the human configures MCP servers project-scope for the source repo once (normal Claude Code usage), and this command just relocates that config into each lane. | +| Missing config | Throws `ENOMCPCONFIG` with a clear message pointing at `SKILL.md`'s Setup section, not a silent no-op | A lane with zero MCP servers synced would silently break Stage 3/6 later, at a much less debuggable point. Fail loud, fail early, matching `requireProfile`'s existing shape for a missing profile. | +| Overwrite semantics | Full overwrite of `.mcp.json` on every call, profiles seeded only if the destination directory doesn't already exist | Matches E3's "reinstall, not merge" for `.mcp.json` (config should always reflect the source repo's current state) but "never clobber a working login" for profiles (an existing profile dir means a session already logged in there — copying over it would lose that). | +| `--output-dir` pinning | Only added to a server whose args reference `@playwright/mcp` and don't already declare `--output-dir` | Direct port of Shipyard's own conditional — this is what makes proof screenshots land in the lane's own canonical `.playwright-mcp/proof` (the same path the proof gallery, C, already reads) instead of wherever the server's cwd happens to be. | +| Permission/auto-approval writes | Dropped entirely (see Scope) | No existing CCAM pattern for a backend command granting permissions; the two agents the original rules named aren't both in scope (`pr-reviewer` isn't ported at all, `ticketer` is a later task). | + +## Command mapping + +| Shipyard | CCAM | Status | +|---|---|---| +| `lane-mcp-sync.sh ` | `ccam lanes mcp sync []` | **new in this task** | +| `ensure_proof_link "$DIR"` (called at the top of `sync_lane`) | not called — proof-link is `ccam lanes proof-link`, a separate explicit primitive this repo already has; mcp-sync doesn't need to chain into it | n/a (already exists, not this task's job to invoke) | +| Chromium profile seeding | ported directly, same logic | **new in this task** | +| `.mcp.json` write + relocate + `--output-dir` pin | ported directly, same logic | **new in this task** | +| `settings.local.json` permission/autoMode writes | dropped | n/a (see Scope) | +| `mcp-node-exec.sh` wrapper routing | dropped | n/a (see Scope) | + +## Design + +### `server/lib/lane-mcp.js` + +- `readSourceMcpServers(sourceRepo)` — reads `~/.claude.json`, returns `cfg.projects[sourceRepo]?.mcpServers`. Throws `ENOMCPCONFIG` if absent/empty. +- `relocateServers(servers, sourceRepo, laneDir)` — deep-walks the server config objects, replacing any string containing `sourceRepo` with the same string with `laneDir` substituted in (same recursive string/array/object walk Shipyard's Python does). +- `pinPlaywrightOutputDir(servers, laneDir)` — for each server whose `args` includes an entry starting with `@playwright/mcp` and has no `--output-dir` already, appends `["--output-dir", path.join(laneDir, ".playwright-mcp")]`. +- `seedProfiles(sourceRepo, laneDir)` — for each subdirectory of `/.playwright-mcp/profiles/`, if `/.playwright-mcp/profiles/` doesn't already exist, recursively copy it there and delete any `Singleton*` files in the copy (stale lock files from the source's own last-run browser process — copying them would make the lane's browser fail to start, thinking another instance already holds the profile). +- `syncMcp(lane)` — orchestrates the above, writes `/.mcp.json` as `{mcpServers: }`, appends `.mcp.json` to `.git/info/exclude` (same idempotent read-then-append pattern as E2/E3's `info/*` writes — though `.mcp.json` sits directly under the lane root, not `.git/info/`, so this reuses the exclude-file mechanics without needing `--git-common-dir` resolution the way `.git/info/exclude`'s own location does). Returns `{servers: string[], profilesSeeded: string[]}`. + +### Route + CLI + +`POST /api/lanes/:id/mcp/sync` — synchronous (JSON parse + a few file copies, not a long-running process), 200 with the `syncMcp` result, 400 `ENOMCPCONFIG` when the source repo has no MCP servers configured. `ccam lanes mcp sync []` — same `resolveLaneArg` pattern as `lanes agents install`. + +## Invariants + +1. **CCAM still does not orchestrate; this doesn't add a new automation surface either.** `mcp sync` copies static config and files; it never grants a permission, launches a process, or triggers anything. +2. **Never a silent partial state.** A lane with no source-repo MCP config gets a clear error, not an empty-but-present `.mcp.json`. +3. **A lane's existing browser-profile login is never overwritten**, only ever seeded once. + +## Verify + +Unit tests against a fake `~/.claude.json` (via `HOME` override in the test process, same isolation technique the rest of this test suite already uses for `DASHBOARD_DB_PATH`/`LANES_ROOT`) and a fixture source-repo directory with a `.playwright-mcp/profiles/` tree: relocation correctness, `--output-dir` pinning only where absent, profile seeding skips an existing destination, `Singleton*` files are stripped from a freshly-seeded copy, `.git/info/exclude` idempotency, and the `ENOMCPCONFIG` error path for a source repo with nothing configured.