CB-634: let a member use the IDE MCP against its own worktree — opt-in per profile #162

Open
opened 2026-08-23 15:46:19 +02:00 by ltms · 2 comments
Owner

Raised 2026-08-23 from the kb host, after wiring its lead to the IDE Index MCP. Operator ask: the fleet should use IDE MCP tools, not just the lead.

Status: design agreed, ready to build. Full design note lives on the kb host at fleetd/docs/CB-634-Worker-IDE-Worktree.md (untracked); this ticket carries its essence so it is durable.

Relationship to other tickets

  • Implements the "yes — safely" answer to #117 (CB-614). #117 §3 concluded that sharing the IDE mount with members is the wrong shape. That conclusion was correct on the evidence it had. New measurements below change it: the Index MCP is application-scoped and fails closed, and the enabled tool set is navigation-only. So a bounded, opt-in, per-worktree mount is now safe. #117 finding 1 (the lead's own IDE step is a no-op because no project is open, plus a possibly-wrong project_path in CLAUDE.md) stays separate — it is an operator action, not this code change.
  • Lighter alternative to #124 (CB-620). #124 proposes a language server per worktree — new infrastructure. CB-634 reuses a server that already runs. If CB-634 lands, #124 is the heavier path we did not take.

Goal

Let a member use IntelliJ code intelligence against its own worktree — opt-in per profile, default off — without reopening the CB-523/CB-525 path-confusion bugs.

Today a member gets nothing: GitWorktrees.isolateToolSurface neutralizes each worktree's .mcp.json to {"mcpServers": {}}, and parityOverlay withholds .claude/settings.local.json. That is correct and this ticket does not undo it. The member's mount arrives by launcher flag or not at all.

Why this is newly safe (the rebuttal to #117 §3)

Measured live on the kb host with two projects open:

ide_index_status {}                                     -> isError=true
  {"error":"multiple_projects_open", "available_projects":[{"name":"fleetd"},{"name":"kb"}]}
ide_index_status {"project_path":"/home/ltms/LTMS/kb"}  -> isError=false
  • The IDE Index MCP is application-scoped, not checkout-scoped. One server on 127.0.0.1:29170 serves every open project; each call selects its project with a project_path argument.
  • It fails closed. With more than one project open, a call with no project_path errors instead of guessing. So the CB-525 failure — a worker silently navigating the primary's tree — becomes structurally preventable by pinning project_path to the member's own worktree, rather than by withholding the server.
  • The enabled tool surface is navigation + refactor only (16 of 54 tools on this build). read_file, file_structure, and the build/terminal tools are disabled, so this does not hand a member a second way to run commands (answers #117 §3(b)).

Design

  1. Profile opt-in. Add ideMcpUrl (String, null/blank means off) to FleetConfig.Profile. A URL, not a boolean — host and port are host-specific, mirroring mcpUrl. Default off. So a member gets the IDE only on a host where the IDE is actually mounted (e.g. kb), and nothing on a host with no IDE (e.g. the Mac today).
  2. Mount via launcher flags, not .mcp.json. In ClaudeCodeLauncher.argvWithFleet, extend the inline --mcp-config JSON with a second server named intellij when ideMcpUrl is set. ⚠ The current cfg.hasMcp() gate keys on mcpUrl alone and must also fire when only ideMcpUrl is set, or the flag is never emitted.
  3. Guidance as charter, gated on the same key. When ideMcpUrl is set, append an IDE charter fragment: "prefer IDE MCP tools over grep; every ide_* call must pass project_path=<your worktree> and no other." CB-618 forbids --append-system-prompt and --append-system-prompt-file on the same command line, so this folds into the existing charter combination, ordered role -> ide -> reply. The reply charter stays last — it is the rule that must survive. This is what replaces the hand-pasted "IDE MCP Tools" block some projects added to their own CLAUDE.md: the rule now rides the launch charter, gated on the mount actually being present.
  4. Lifecycle is bridged-owned. At spawn, after git worktree add, shell idea <worktree> (the native launcher forwards into the running IDE). At release, close the project before Worktrees.remove(). Ordering is load-bearing: a worktree remove --force under a live IDE project leaves a stale project pointing at a deleted directory. A member must not own this.
  5. Enable lifecycle management in the IDE plugin settings (off by default) to get ide_set_project_mode / ide_release_project. The idea CLI has no "close project" verb, so this is the only close mechanism.

Files (from the note)

File Change
config/FleetConfig.java Profile.ideMcpUrl component + factory overloads + compact-constructor default
config/ConfigRef.java field equality for hot-reload diffing
member/ClaudeCodeLauncher.java argvWithFleet: second MCP server + IDE charter fragment; fix the hasMcp() gate
session/GitWorktrees.java open-on-provision; leave isolateToolSurface untouched
session/SessionManager.java close-before-remove ordering
bridged/fleetd.example.yaml document ideMcpUrl; also fix the stale parityOverlay default that still lists .claude/settings.local.json
tests FleetConfigTest, GitWorktreesTest, ClaudeCodeLauncher argv tests

Guard rails — do not regress

  • Never add .mcp.json to parityOverlay (CB-525); keep isolateToolSurface neutralizing it unconditionally.
  • Never add .claude/settings.local.json to parityOverlay. It pre-approves mcp__intellij-* grants a member must not hold ambiently.
  • The canonical bridge-block rule "The primary's IDE tooling is still not yours, whatever you see" still holds verbatim. A member's own launcher-mounted server, pinned to its own worktree, is not the primary's tooling. Any wording change goes to the wiki template first, then out to each project — never edited inline.
  • Opt-in stays default-off. N members means N open IDE projects means N indexes. Consider a cap, or defaulting members to the lifecycle dormant mode.

Non-goals

  • The opencode adapter. It mounts MCP via OPENCODE_CONFIG, not inline flags (CB-402), and does not read CLAUDE.md at all. Extending both the mount and the guidance to opencode is a separate follow-on ticket.
  • Any change to .mcp.json neutralization.
  • Attaching worktrees to the primary's IDE window — rejected, it reintroduces the CB-525 path ambiguity.

Verification

  1. mvn clean install in fleetd.
  2. Spawn a worker with ideMcpUrl set; fleet_whoami -> confirm its worktree.
  3. ide_project_status lists the worker's worktree as its own project.
  4. A bare ide_* call errors multiple_projects_open; the pinned call succeeds and returns only paths under the member's worktree — never a path in the primary checkout.
  5. The worktree's .mcp.json is still {"mcpServers": {}} and still --skip-worktree.
  6. Release the member; confirm the IDE project closed and the worktree is gone.
Raised 2026-08-23 from the `kb` host, after wiring its lead to the IDE Index MCP. Operator ask: the fleet should use IDE MCP tools, not just the lead. **Status:** design agreed, ready to build. Full design note lives on the `kb` host at `fleetd/docs/CB-634-Worker-IDE-Worktree.md` (untracked); this ticket carries its essence so it is durable. ## Relationship to other tickets - **Implements the "yes — safely" answer to #117 (CB-614).** #117 §3 concluded that sharing the IDE mount with members is the wrong shape. That conclusion was correct **on the evidence it had**. New measurements below change it: the Index MCP is application-scoped and fails closed, and the enabled tool set is navigation-only. So a bounded, opt-in, per-worktree mount is now safe. #117 finding 1 (the lead's own IDE step is a no-op because no project is open, plus a possibly-wrong `project_path` in `CLAUDE.md`) stays separate — it is an operator action, not this code change. - **Lighter alternative to #124 (CB-620).** #124 proposes a language server per worktree — new infrastructure. CB-634 reuses a server that already runs. If CB-634 lands, #124 is the heavier path we did not take. ## Goal Let a **member** use IntelliJ code intelligence against **its own worktree** — opt-in per profile, default off — without reopening the CB-523/CB-525 path-confusion bugs. Today a member gets nothing: `GitWorktrees.isolateToolSurface` neutralizes each worktree's `.mcp.json` to `{"mcpServers": {}}`, and `parityOverlay` withholds `.claude/settings.local.json`. That is correct and this ticket does not undo it. The member's mount arrives by launcher flag or not at all. ## Why this is newly safe (the rebuttal to #117 §3) Measured live on the `kb` host with two projects open: ``` ide_index_status {} -> isError=true {"error":"multiple_projects_open", "available_projects":[{"name":"fleetd"},{"name":"kb"}]} ide_index_status {"project_path":"/home/ltms/LTMS/kb"} -> isError=false ``` - **The IDE Index MCP is application-scoped, not checkout-scoped.** One server on `127.0.0.1:29170` serves every open project; each call selects its project with a `project_path` argument. - **It fails closed.** With more than one project open, a call with no `project_path` errors instead of guessing. So the CB-525 failure — a worker silently navigating the *primary's* tree — becomes structurally preventable by *pinning* `project_path` to the member's own worktree, rather than by withholding the server. - **The enabled tool surface is navigation + refactor only** (16 of 54 tools on this build). `read_file`, `file_structure`, and the build/terminal tools are disabled, so this does not hand a member a second way to run commands (answers #117 §3(b)). ## Design 1. **Profile opt-in.** Add `ideMcpUrl` (`String`, null/blank means off) to `FleetConfig.Profile`. A URL, not a boolean — host and port are host-specific, mirroring `mcpUrl`. Default off. So a member gets the IDE only on a host where the IDE is actually mounted (e.g. `kb`), and nothing on a host with no IDE (e.g. the Mac today). 2. **Mount via launcher flags, not `.mcp.json`.** In `ClaudeCodeLauncher.argvWithFleet`, extend the inline `--mcp-config` JSON with a second server named `intellij` when `ideMcpUrl` is set. ⚠ The current `cfg.hasMcp()` gate keys on `mcpUrl` alone and must also fire when only `ideMcpUrl` is set, or the flag is never emitted. 3. **Guidance as charter, gated on the same key.** When `ideMcpUrl` is set, append an IDE charter fragment: "prefer IDE MCP tools over grep; every `ide_*` call must pass `project_path=<your worktree>` and no other." CB-618 forbids `--append-system-prompt` and `--append-system-prompt-file` on the same command line, so this folds into the existing charter combination, ordered **role -> ide -> reply**. The reply charter stays **last** — it is the rule that must survive. This is what replaces the hand-pasted "IDE MCP Tools" block some projects added to their own `CLAUDE.md`: the rule now rides the launch charter, gated on the mount actually being present. 4. **Lifecycle is bridged-owned.** At spawn, after `git worktree add`, shell `idea <worktree>` (the native launcher forwards into the running IDE). At release, close the project **before** `Worktrees.remove()`. Ordering is load-bearing: a `worktree remove --force` under a live IDE project leaves a stale project pointing at a deleted directory. A member must not own this. 5. **Enable lifecycle management** in the IDE plugin settings (off by default) to get `ide_set_project_mode` / `ide_release_project`. The `idea` CLI has no "close project" verb, so this is the only close mechanism. ## Files (from the note) | File | Change | |---|---| | `config/FleetConfig.java` | `Profile.ideMcpUrl` component + factory overloads + compact-constructor default | | `config/ConfigRef.java` | field equality for hot-reload diffing | | `member/ClaudeCodeLauncher.java` | `argvWithFleet`: second MCP server + IDE charter fragment; fix the `hasMcp()` gate | | `session/GitWorktrees.java` | open-on-provision; **leave `isolateToolSurface` untouched** | | `session/SessionManager.java` | close-before-remove ordering | | `bridged/fleetd.example.yaml` | document `ideMcpUrl`; also fix the stale `parityOverlay` default that still lists `.claude/settings.local.json` | | tests | `FleetConfigTest`, `GitWorktreesTest`, `ClaudeCodeLauncher` argv tests | ## Guard rails — do not regress - **Never** add `.mcp.json` to `parityOverlay` (CB-525); keep `isolateToolSurface` neutralizing it unconditionally. - **Never** add `.claude/settings.local.json` to `parityOverlay`. It pre-approves `mcp__intellij-*` grants a member must not hold ambiently. - The canonical bridge-block rule *"The primary's IDE tooling is still not yours, whatever you see"* still holds verbatim. A member's own launcher-mounted server, pinned to its own worktree, is not the primary's tooling. Any wording change goes to the wiki template first, then out to each project — never edited inline. - Opt-in stays default-off. N members means N open IDE projects means N indexes. Consider a cap, or defaulting members to the lifecycle `dormant` mode. ## Non-goals - **The opencode adapter.** It mounts MCP via `OPENCODE_CONFIG`, not inline flags (CB-402), and does not read `CLAUDE.md` at all. Extending both the mount and the guidance to opencode is a separate follow-on ticket. - Any change to `.mcp.json` neutralization. - Attaching worktrees to the *primary's* IDE window — rejected, it reintroduces the CB-525 path ambiguity. ## Verification 1. `mvn clean install` in `fleetd`. 2. Spawn a worker with `ideMcpUrl` set; `fleet_whoami` -> confirm its `worktree`. 3. `ide_project_status` lists the worker's worktree as its own project. 4. A bare `ide_*` call errors `multiple_projects_open`; the **pinned** call succeeds and returns only paths under the member's worktree — never a path in the primary checkout. 5. The worktree's `.mcp.json` is still `{"mcpServers": {}}` and still `--skip-worktree`. 6. Release the member; confirm the IDE project closed **and** the worktree is gone.
ltms added this to the 2.0 — one operation centre, many hosts milestone 2026-08-23 15:46:19 +02:00
Author
Owner

Design change (operator, 2026-08-23): deliver the IDE guidance as an on-disk overlay at spawn, not as a system-prompt charter.

Reason: a project usually ships its own CLAUDE.md, and we must not clobber it. An overlay file sits beside the project file and merges with it. Confirmed against OpenCode's own docs (opencode.ai/docs/rules): OpenCode reads AGENTS.md (primary) and CLAUDE.md (fallback), but not CLAUDE.local.md; its combine-don't-replace seam is the instructions field in opencode.json.

So the delivery is backend-specific:

  • Claude Code member → write CLAUDE.local.md into the worktree root at spawn. Claude reads it and merges with the project's own CLAUDE.md.
  • OpenCode member → write the same guidance to a file and add its path to the generated opencode.json instructions array.

The IDE mount (--mcp-config intellij server) is unchanged. Only the IDE guidance text moves out of --append-system-prompt-file and into the overlay.

Safety gate (the key point): a provisioned worktree's .git is a regular file (a gitdir pointer); the primary's main checkout's .git is a directory. Write the overlay only when <cwd>/.git is a regular file, so a non-worktree spawn can never pollute the real repo. Add the overlay filename to the worktree's .git/info/exclude so it never shows as untracked and a worker cannot stage it. Teardown is free — the file dies with the worktree.

This supersedes the "guidance as charter" step in the design above. Content scope for the first cut: the IDE guidance only; the canonical bridge-block delivery can reuse the same overlay later.

**Design change (operator, 2026-08-23): deliver the IDE guidance as an on-disk overlay at spawn, not as a system-prompt charter.** Reason: a project usually ships its own `CLAUDE.md`, and we must not clobber it. An overlay file sits *beside* the project file and merges with it. Confirmed against OpenCode's own docs (opencode.ai/docs/rules): OpenCode reads `AGENTS.md` (primary) and `CLAUDE.md` (fallback), but **not** `CLAUDE.local.md`; its combine-don't-replace seam is the `instructions` field in `opencode.json`. So the delivery is backend-specific: - **Claude Code member** → write `CLAUDE.local.md` into the worktree root at spawn. Claude reads it and merges with the project's own `CLAUDE.md`. - **OpenCode member** → write the same guidance to a file and add its path to the generated `opencode.json` `instructions` array. The IDE **mount** (`--mcp-config` `intellij` server) is unchanged. Only the IDE **guidance text** moves out of `--append-system-prompt-file` and into the overlay. **Safety gate (the key point):** a provisioned worktree's `.git` is a regular *file* (a gitdir pointer); the primary's main checkout's `.git` is a *directory*. Write the overlay only when `<cwd>/.git` is a regular file, so a non-worktree spawn can never pollute the real repo. Add the overlay filename to the worktree's `.git/info/exclude` so it never shows as untracked and a worker cannot stage it. Teardown is free — the file dies with the worktree. This supersedes the "guidance as charter" step in the design above. Content scope for the first cut: the IDE guidance only; the canonical bridge-block delivery can reuse the same overlay later.
Author
Owner

Implemented (Mac lead, 2026-08-24). Not yet merged to main.

The design here is built and deployed. It is live on branch cb-634-ide-mcp (tip 7655f1b), 918 tests green, and proven end to end on the fleet01 host with zero manual IDE steps. Please do not file a new issue for this — it is #162, and it is done pending merge.

What shipped, and where it went beyond the plan above:

  • Overlay delivery (per the operator comment) works on both backends. Claude Code → CLAUDE.local.md in the worktree root; opencode → an ide-rules.md file added to the generated config's instructions array. The .git-is-a-regular-file safety gate is in place.
  • opencode is also mounted (intellij server + guidance), so it is no longer a non-goal. The follow-on ticket the Non-goals section reserved is not needed.
  • Pin and auto-open target the module dir, not the worktree root. This repo's Maven pom is in bridged/, so opening the worktree root imports no module and ide_* resolves nothing. The overlay now pins project_path to <worktree>/bridged, and the IDE opens that same dir.
  • Auto-open is a configurable host command, not a hardcoded idea <worktree>. Two new opt-in per-Profile keys, read only when ideMcpUrl is set: ideProjectDir (the module subdir) and ideOpenCommand (e.g. env DISPLAY=:10.0 idea {dir}, run through /bin/sh -c, best-effort and non-fatal). Both are documented in fleetd.example.yaml.
  • Bug found and fixed: the overlay's info/exclude entry was written to the per-worktree gitdir, which git ignores for a worktree — git reads a worktree's excludes from the common dir. So CLAUDE.local.md showed as untracked and could be swept into a worker's PR. It now writes to the common dir; verified live that the overlay is not untracked.

Deferred (not shipped): the close half — §4's close-before-Worktrees.remove() ordering and §5's ide_release_project. So open IDE projects accumulate; nothing closes them yet. This is the main open piece.

Guard rails held: GitWorktrees.isolateToolSurface still neutralizes .mcp.json unconditionally; .mcp.json and .claude/settings.local.json are still absent from parityOverlay.

Live proof on fleet01: spawned a local member; the daemon logged the auto-open of <worktree>/bridged; ide_project_status listed that module as an open project; ide_find_class resolved the member's own ClaudeCodeLauncher against its worktree (stale:false).

Next: build the close half, then merge to main and add the wiki/11-Features.md entry.

**Implemented (Mac lead, 2026-08-24). Not yet merged to `main`.** The design here is built and deployed. It is live on branch `cb-634-ide-mcp` (tip `7655f1b`), 918 tests green, and proven end to end on the fleet01 host with zero manual IDE steps. Please do **not** file a new issue for this — it is #162, and it is done pending merge. **What shipped, and where it went beyond the plan above:** - **Overlay delivery** (per the operator comment) works on both backends. Claude Code → `CLAUDE.local.md` in the worktree root; opencode → an `ide-rules.md` file added to the generated config's `instructions` array. The `.git`-is-a-regular-file safety gate is in place. - **opencode is also mounted** (`intellij` server + guidance), so it is no longer a non-goal. The follow-on ticket the Non-goals section reserved is not needed. - **Pin and auto-open target the module dir, not the worktree root.** This repo's Maven pom is in `bridged/`, so opening the worktree root imports no module and `ide_*` resolves nothing. The overlay now pins `project_path` to `<worktree>/bridged`, and the IDE opens that same dir. - **Auto-open is a configurable host command, not a hardcoded `idea <worktree>`.** Two new opt-in per-`Profile` keys, read only when `ideMcpUrl` is set: `ideProjectDir` (the module subdir) and `ideOpenCommand` (e.g. `env DISPLAY=:10.0 idea {dir}`, run through `/bin/sh -c`, best-effort and non-fatal). Both are documented in `fleetd.example.yaml`. - **Bug found and fixed:** the overlay's `info/exclude` entry was written to the per-worktree gitdir, which git ignores for a worktree — git reads a worktree's excludes from the **common** dir. So `CLAUDE.local.md` showed as untracked and could be swept into a worker's PR. It now writes to the common dir; verified live that the overlay is not untracked. **Deferred (not shipped):** the close half — §4's close-before-`Worktrees.remove()` ordering and §5's `ide_release_project`. So open IDE projects accumulate; nothing closes them yet. This is the main open piece. **Guard rails held:** `GitWorktrees.isolateToolSurface` still neutralizes `.mcp.json` unconditionally; `.mcp.json` and `.claude/settings.local.json` are still absent from `parityOverlay`. **Live proof on fleet01:** spawned a `local` member; the daemon logged the auto-open of `<worktree>/bridged`; `ide_project_status` listed that module as an open project; `ide_find_class` resolved the member's own `ClaudeCodeLauncher` against its worktree (`stale:false`). Next: build the close half, then merge to `main` and add the `wiki/11-Features.md` entry.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#162