Skip to content
Open
16 changes: 9 additions & 7 deletions src/cli/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ async function sessionStart(args: readonly string[]): Promise<void> {

// Create runtime — prefer acpx, fall back to mock
const { AcpxRuntime } = await import("../../core/acpx-runtime.js");
const acpx = new AcpxRuntime();
const acpx = new AcpxRuntime({ logDir: join(groveDir, "agent-logs") });
const runtime = (await acpx.isAvailable()) ? acpx : new MockRuntime();
const eventBus = new LocalEventBus();

Expand Down Expand Up @@ -209,12 +209,7 @@ async function sessionStart(args: readonly string[]): Promise<void> {
});
});

// If orchestrator auto-stopped (all agents idle), mark completed immediately
if (status.stopped) {
await markDone(status.stopReason ?? "Orchestrator stopped");
db.close();
}

// Output initial status
outputJson({
sessionId: session.id,
goal,
Expand All @@ -226,6 +221,13 @@ async function sessionStart(args: readonly string[]): Promise<void> {
})),
message: `Session started with ${status.agents.length} agents`,
});

// Wait for session to complete — agents need time to work, submit, review, and call grove_done.
// Without this, the CLI exits immediately and the reviewer never gets routed events.
const SESSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const stopReason = await orchestrator.waitForCompletion(SESSION_TIMEOUT_MS);
await markDone(stopReason);
db.close();
}

// ---------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions src/cli/grove-md-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ function renderTopology(topology: AgentTopology | undefined, version: 2 | 3): st
for (const edge of role.edges) {
lines.push(` - target: ${edge.target}`);
lines.push(` edge_type: ${edge.edgeType}`);
if (edge.workspace) {
lines.push(` workspace: ${edge.workspace}`);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/presets/review-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const reviewLoopPreset: PresetConfig = {
name: "coder",
description: "Writes and iterates on code",
maxInstances: 1,
edges: [{ target: "reviewer", edgeType: "delegates" }],
edges: [{ target: "reviewer", edgeType: "delegates", workspace: "branch_from_source" }],
platform: "claude-code",
prompt:
"You are a software engineer. Your workflow:\n" +
Expand Down
2 changes: 2 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ export { WorkspaceStatus } from "./workspace.js";
export type {
ProvisionedWorkspace,
SessionWorkspaces,
WorkspaceIsolationPolicy,
WorkspaceMode,
WorkspaceProvisionError,
WorkspaceProvisionOptions,
} from "./workspace-provisioner.js";
Expand Down
57 changes: 57 additions & 0 deletions src/core/resolve-mcp-serve-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Resolves the path to the grove MCP server entry point.
*
* The MCP server lives in the grove installation directory (dist/mcp/serve.js
* for built installs, src/mcp/serve.ts for development). It does NOT live in
* the user's project directory.
*
* Resolution order:
* 1. process.argv[1] → climb 3 levels → dist/mcp/serve.js
* 2. process.argv[1] → climb 3 levels �� src/mcp/serve.ts
* 3. import.meta.url → climb 3 levels → dist/mcp/serve.js
* 4. import.meta.url → climb 3 levels → src/mcp/serve.ts
* 5. fallback: projectRoot/src/mcp/serve.ts (last resort)
*
* Used by both SpawnManager (TUI) and SessionOrchestrator (headless).
*/

import { existsSync } from "node:fs";
import { dirname, join } from "node:path";

/**
* Resolve the MCP serve entry point from the grove installation.
*
* @param projectRoot — fallback if no installation path can be derived
*/
export function resolveMcpServePath(projectRoot?: string): string {
const entryPoint = process.argv[1] ?? "";
// process.argv[1] = "<groveRoot>/dist/cli/main.js" or "<groveRoot>/src/cli/main.ts"
// Climb 3 levels: main.js → cli/ → dist/ or src/ → <groveRoot>
const groveRootFromEntry = dirname(dirname(dirname(entryPoint)));

// import.meta.url fallback — may point to a bundled chunk, but worth trying
const groveRootFromMeta = dirname(dirname(dirname(new URL(import.meta.url).pathname)));

// Try dist first (built install), then src (development)
const candidates = [
join(groveRootFromEntry, "dist", "mcp", "serve.js"),
join(groveRootFromEntry, "src", "mcp", "serve.ts"),
join(groveRootFromMeta, "dist", "mcp", "serve.js"),
join(groveRootFromMeta, "src", "mcp", "serve.ts"),
];

// Last resort: project root (only works when project IS the grove repo)
if (projectRoot) {
candidates.push(join(projectRoot, "src", "mcp", "serve.ts"));
}

for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
}

// Return best guess even if it doesn't exist — caller will get a clear
// "file not found" error rather than a confusing empty path
return candidates[0]!;
}
Loading
Loading