7655f1b51a
The overlay pinned project_path to the worktree root. For a repo whose Maven
module is a subdir (this repo's pom is in `bridged/`, not at the root), opening
the root imports no module and every ide_* call resolves nothing. Pin and open
the module dir instead.
Two new opt-in per-Profile keys, both read only when ideMcpUrl is set:
- ideProjectDir: repo-relative module dir the IDE opens and the overlay pins;
blank keeps the old worktree-root behaviour.
- ideOpenCommand: host command that opens that dir in the IDE at spawn, with
{dir} substituted and run through /bin/sh -c so env (e.g. DISPLAY) can be set
inline. Best-effort and non-fatal — a failure never fails the spawn. Blank
keeps the manual-open behaviour. No close half yet (deferred).
Shared helpers PeerLauncher.ideProjectPath / openInIde back both launchers.
The two Profile fields ride a back-compat constructor, so every existing call
site and YAML compiles and behaves unchanged.
Tests: overlay content pins the module dir when ideProjectDir is set;
ideProjectPath resolution; openInIde no-op on a blank command. 918 tests green.
196 lines
9.6 KiB
Java
196 lines
9.6 KiB
Java
package dev.ltms.fleet.peer;
|
|
|
|
import java.nio.file.Path;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
/**
|
|
* SPI for materializing a connected peer — the only way the bridge core creates or tears down
|
|
* a peer process. Every launcher is a first-party, in-tree adapter selected by (future) profile
|
|
* config; today's single adapter is the {@code ClaudeCodeLauncher} / Claude Code over herdr.
|
|
*
|
|
* <p>The core delegates spawn and teardown to this interface without knowing how the peer is set
|
|
* up. Environment variables, CLI flags, subscription guards, transport (herdr tab/pane) layout,
|
|
* and naming conventions are all adapter-private — the core sees only the returned
|
|
* {@link PeerHandle} whose {@code id()} is the registry/routing key.
|
|
*
|
|
* <p>The interface is a superset of what {@code SessionManager} and {@code Fleetd.main} call
|
|
* on the concrete launcher today.
|
|
*/
|
|
public interface PeerLauncher {
|
|
|
|
/**
|
|
* The name every launcher gives the bridge's MCP server in the config it writes for its peer.
|
|
* The peer's tools are addressed as {@code mcp__<this>__fleet_*}, and {@code CLAUDE.md}'s
|
|
* role-detection ladder names that prefix, so the two must agree.
|
|
*
|
|
* <p>It is a constant because three launchers write it — {@code ClaudeCodeLauncher} and
|
|
* {@code LeadLauncher} into a {@code --mcp-config} literal, {@code OpenCodeLauncher} into an
|
|
* {@code opencode.json} node. Three hand-written copies of one name is how a rename lands in
|
|
* two of them (CB-632).
|
|
*/
|
|
String MCP_MOUNT_NAME = "fleet";
|
|
|
|
/**
|
|
* Shared IDE-guidance text (CB-634), delivered per-backend as an on-disk overlay rather than
|
|
* any one adapter's system-prompt charter, so a project's own {@code CLAUDE.md} is never
|
|
* clobbered. It pins every {@code ide_*} call to the member's own worktree, which is the whole
|
|
* point of the mechanism. Both launchers render their own overlay from this single source.
|
|
*
|
|
* @param projectPath the path the member must pin every {@code ide_*} call to — the module dir
|
|
* IntelliJ opened as the project, which is {@link #ideProjectPath} of the
|
|
* member's own worktree (the worktree root when no module subdir is set)
|
|
*/
|
|
static String ideOverlayText(String projectPath) {
|
|
return "## IDE code intelligence — your worktree only\n"
|
|
+ "An IntelliJ IDE Index MCP server is mounted as `mcp__intellij__ide_*`. Prefer it "
|
|
+ "over `grep`/`find` for symbol lookups, references, call and type hierarchy, and "
|
|
+ "diagnostics — it resolves the real AST, text search does not.\n\n"
|
|
+ "Every `ide_*` call MUST pass `project_path: \"" + projectPath + "\"` — your own "
|
|
+ "worktree — and never any other path. A call without it errors "
|
|
+ "`multiple_projects_open`; a call with a different path reads another checkout, "
|
|
+ "not your changes. This is not the primary's IDE: it is your worktree, pinned to "
|
|
+ "you.";
|
|
}
|
|
|
|
/**
|
|
* The absolute path IntelliJ must open as the project, and the {@code project_path} the overlay
|
|
* pins (CB-634). It is {@code cwd} resolved against {@code ideProjectDir}. The distinction
|
|
* matters because this repo (like {@code fleet/fleetd}) keeps its Maven module in a subdir
|
|
* ({@code bridged/}), not at the worktree root: opening the root imports no module and
|
|
* {@code ide_*} resolves nothing, so the module dir is the correct pin and open target.
|
|
*
|
|
* @param cwd the member's worktree root
|
|
* @param ideProjectDir repo-relative module subdir, or {@code null}/blank for the worktree root
|
|
* @return the absolute, normalized module dir as a string
|
|
*/
|
|
static String ideProjectPath(String cwd, String ideProjectDir) {
|
|
Path base = Path.of(cwd);
|
|
if (ideProjectDir == null || ideProjectDir.isBlank()) {
|
|
return base.toString();
|
|
}
|
|
return base.resolve(ideProjectDir).normalize().toString();
|
|
}
|
|
|
|
/**
|
|
* Best-effort: open {@code projectPath} in the host IDE by running {@code openCommand} with
|
|
* every {@code {dir}} replaced by {@code projectPath} (CB-634 auto-open). The command runs
|
|
* through {@code /bin/sh -c} so an operator can set env inline — e.g.
|
|
* {@code "env DISPLAY=:10.0 idea {dir}"} — because the daemon's own env may lack {@code DISPLAY}.
|
|
*
|
|
* <p>A blank command is a no-op: the profile opted into IDE MCP but not auto-open, so the
|
|
* operator opens the module by hand. The child process is detached and its exit is not awaited;
|
|
* any failure is logged and swallowed, because a member must spawn whether or not an IDE is
|
|
* running. There is no close half yet (CB-634 defers it): an opened module stays open until the
|
|
* operator closes it, and opening the same module again just refocuses it.
|
|
*
|
|
* @param projectPath the module dir to open (typically {@link #ideProjectPath})
|
|
* @param openCommand the host command template, with {@code {dir}} substituted; null/blank ⇒ no-op
|
|
* @param log the calling launcher's logger, for the best-effort WARN
|
|
*/
|
|
static void openInIde(String projectPath, String openCommand, Logger log) {
|
|
if (openCommand == null || openCommand.isBlank()) {
|
|
return;
|
|
}
|
|
String cmd = openCommand.replace("{dir}", projectPath);
|
|
try {
|
|
new ProcessBuilder("/bin/sh", "-c", cmd)
|
|
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
|
|
.redirectError(ProcessBuilder.Redirect.DISCARD)
|
|
.start();
|
|
log.info("CB-634 auto-open: launched IDE open for {}", projectPath);
|
|
} catch (Exception e) {
|
|
log.warn("CB-634 auto-open of '{}' failed (member still spawns): {}", projectPath, e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The set of {@link Capability capabilities} this launcher declares. A peer whose profile
|
|
* opts into a git-forge token should include {@link Capability#SELF_PR}; the base set for
|
|
* the Claude Code herdr adapter is always {@code MID_TURN_ASK, WORKTREE, ORPHAN_REAP}.
|
|
*/
|
|
Set<Capability> capabilities();
|
|
|
|
/**
|
|
* The capabilities of the adapter that {@code profileName} resolves to (null/blank → the
|
|
* default profile, the same resolution {@link #spawn} uses). Distinct from {@link
|
|
* #capabilities()}, which unions every configured adapter: a caller that must know whether
|
|
* <em>this</em> profile's backend supports a capability — e.g. {@link Capability#SESSION_RESUME}
|
|
* before honoring {@link SpawnRequest#resumeSessionId()} — needs the per-profile answer, not
|
|
* the fleet-wide union, or a mixed fleet could OK a resume that lands on a non-supporting
|
|
* adapter (CB-584).
|
|
*
|
|
* @throws IllegalArgumentException if the profile is unknown and no default is configured
|
|
*/
|
|
Set<Capability> capabilitiesFor(String profileName);
|
|
|
|
/**
|
|
* {@code profileName}/requestedCwd null/blank → default resolution. Returns after the peer
|
|
* process is live (env + argv + placement complete). Never returns {@code null}.
|
|
*
|
|
* @param req the spawn parameters (profile, requested cwd, caller cwd)
|
|
* @return a handle whose {@link PeerHandle#id()} is the registry/routing key
|
|
* @throws IllegalArgumentException if the profile is unknown and no default is configured
|
|
*/
|
|
PeerHandle spawn(SpawnRequest req);
|
|
|
|
/**
|
|
* The configured worker profile names — the set of names {@code spawn(profileName)} accepts.
|
|
*/
|
|
Set<String> profiles();
|
|
|
|
/**
|
|
* The profile a no-argument {@link #spawn(SpawnRequest)} uses, or {@code null} if none is configured.
|
|
*/
|
|
String defaultProfile();
|
|
|
|
/**
|
|
* Resolve the effective working directory for a spawn {@code req} without actually spawning.
|
|
* Resolution order: requestedCwd → profile cwd → callerCwd → daemon cwd.
|
|
*
|
|
* @return the resolved absolute path, never null/blank
|
|
*/
|
|
String effectiveCwd(SpawnRequest req);
|
|
|
|
/**
|
|
* The parity-overlay file list for {@code profileName} (default list when unset). Used by
|
|
* worktree provisioning to copy config files into the isolated checkout before spawning.
|
|
*/
|
|
List<String> parityOverlay(String profileName);
|
|
|
|
/**
|
|
* The set of all agents this launcher currently tracks, transport-specific. Each element
|
|
* exposes at minimum a pane-like {@code id()} matching this launcher's {@link PeerHandle}
|
|
* scheme, plus transport-level status. Callers merge this set with the session registry to
|
|
* build a live roster view.
|
|
*/
|
|
List<?> list();
|
|
|
|
/**
|
|
* Reap orphaned peers left behind by a prior daemon process. Only peers whose naming scheme
|
|
* matches this launcher's and whose nonce differs from the current process are eligible.
|
|
* Best-effort: a failure to list or to stop any one peer is logged and never aborts startup.
|
|
*
|
|
* @return the number of orphaned peers reaped
|
|
*/
|
|
int reapOrphanWorkers();
|
|
|
|
/**
|
|
* Tear a peer down by its registry/routing key ({@link PeerHandle#id()}). Tolerates an
|
|
* already-gone peer. Also cleans up launcher-private resources (e.g. empty dedicated tabs)
|
|
* when safe to do so.
|
|
*/
|
|
void stop(String id);
|
|
|
|
/**
|
|
* Discard the context of the peer identified by {@code id}. Implementations must bypass normal
|
|
* bridge delivery/turn accounting. Unsupported peer kinds return {@code false} without sending
|
|
* a guessed command.
|
|
*
|
|
* @return {@code true} when a reset was sent and its status transition must settle before reuse
|
|
*/
|
|
boolean clearContext(String id);
|
|
}
|