diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index bc60147f3..46158c2b3 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -207,105 +207,111 @@ export function registerStart(program: Command): void { if (exists) { console.log(chalk.yellow(`Orchestrator session "${sessionId}" is already running (skipping creation)`)); } else { - // Ensure CLAUDE.orchestrator.md exists - spinner.start("Generating orchestrator prompt"); - ensureOrchestratorPrompt( - project.path, - config, - projectId, - project, - opts?.regenerate ?? false, - ); - spinner.succeed("Orchestrator prompt ready"); - - // Ensure CLAUDE.local.md imports CLAUDE.orchestrator.md - spinner.start("Configuring CLAUDE.local.md"); try { + // Ensure CLAUDE.orchestrator.md exists + spinner.start("Generating orchestrator prompt"); + ensureOrchestratorPrompt( + project.path, + config, + projectId, + project, + opts?.regenerate ?? false, + ); + spinner.succeed("Orchestrator prompt ready"); + + // Ensure CLAUDE.local.md imports CLAUDE.orchestrator.md + spinner.start("Configuring CLAUDE.local.md"); ensureOrchestratorImport(project.path); spinner.succeed("CLAUDE.local.md configured"); - } catch (err) { - spinner.fail("Could not write CLAUDE.local.md"); - throw new Error( - `Failed to update CLAUDE.local.md: ${err instanceof Error ? err.message : String(err)}`, - { cause: err }, - ); - } - // Get agent instance (used for hooks and launch) - const agent = getAgent(config, projectId); + // Get agent instance (used for hooks and launch) + const agent = getAgent(config, projectId); - // Setup agent hooks for automatic metadata updates - spinner.start("Configuring agent hooks"); - try { + // Setup agent hooks for automatic metadata updates + spinner.start("Configuring agent hooks"); if (agent.setupWorkspaceHooks) { await agent.setupWorkspaceHooks(project.path, { dataDir: config.dataDir }); } spinner.succeed("Agent hooks configured"); + + spinner.start("Creating orchestrator session"); + + // Get agent launch command + const launchCmd = agent.getLaunchCommand({ + sessionId, + projectConfig: project, + permissions: project.agentConfig?.permissions ?? "default", + model: project.agentConfig?.model, + }); + + // Determine environment variables + const envVarName = `${project.sessionPrefix.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}_SESSION`; + const environment: Record = { + [envVarName]: sessionId, + AO_SESSION: sessionId, + AO_DATA_DIR: config.dataDir, + DIRENV_LOG_FORMAT: "", + }; + + // Merge agent-specific environment + const agentEnv = agent.getEnvironment({ + sessionId, + projectConfig: project, + permissions: project.agentConfig?.permissions ?? "default", + model: project.agentConfig?.model, + }); + Object.assign(environment, agentEnv); + + // NOTE: AO_PROJECT_ID is intentionally not set for orchestrator (uses flat metadata path) + + // Create tmux session + await newTmuxSession({ + name: sessionId, + cwd: project.path, + environment, + }); + + try { + // Launch agent + await tmuxSendKeys(sessionId, launchCmd, true); + + spinner.succeed("Orchestrator session created"); + + // Write metadata + const runtimeHandle = JSON.stringify({ + id: sessionId, + runtimeName: "tmux", + data: {}, + }); + + writeMetadata(config.dataDir, sessionId, { + worktree: project.path, + branch: project.defaultBranch, + status: "working", + project: projectId, + createdAt: new Date().toISOString(), + runtimeHandle, + }); + } catch (err) { + // Cleanup tmux session if metadata write or agent launch fails + try { + await exec("tmux", ["kill-session", "-t", sessionId]); + } catch { + // Best effort cleanup - session may not exist + } + throw err; + } } catch (err) { - spinner.fail("Could not setup agent hooks"); + spinner.fail("Orchestrator setup failed"); + // Cleanup dashboard if orchestrator setup fails + if (dashboardProcess) { + dashboardProcess.kill(); + } throw new Error( - `Failed to setup agent hooks: ${err instanceof Error ? err.message : String(err)}`, + `Failed to setup orchestrator: ${err instanceof Error ? err.message : String(err)}`, { cause: err }, ); } - - spinner.start("Creating orchestrator session"); - - // Get agent launch command - const launchCmd = agent.getLaunchCommand({ - sessionId, - projectConfig: project, - permissions: project.agentConfig?.permissions ?? "default", - model: project.agentConfig?.model, - }); - - // Determine environment variables - const envVarName = `${project.sessionPrefix.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}_SESSION`; - const environment: Record = { - [envVarName]: sessionId, - AO_SESSION: sessionId, - AO_DATA_DIR: config.dataDir, - DIRENV_LOG_FORMAT: "", - }; - - // Merge agent-specific environment - const agentEnv = agent.getEnvironment({ - sessionId, - projectConfig: project, - permissions: project.agentConfig?.permissions ?? "default", - model: project.agentConfig?.model, - }); - Object.assign(environment, agentEnv); - - // NOTE: AO_PROJECT_ID is intentionally not set for orchestrator (uses flat metadata path) - - // Create tmux session - await newTmuxSession({ - name: sessionId, - cwd: project.path, - environment, - }); - - // Launch agent - await tmuxSendKeys(sessionId, launchCmd, true); - - spinner.succeed("Orchestrator session created"); - - // Write metadata - const runtimeHandle = JSON.stringify({ - id: sessionId, - runtimeName: "tmux", - data: {}, - }); - - writeMetadata(config.dataDir, sessionId, { - worktree: project.path, - branch: project.defaultBranch, - status: "working", - project: projectId, - createdAt: new Date().toISOString(), - runtimeHandle, - }); } } diff --git a/packages/plugins/agent-claude-code/src/index.ts b/packages/plugins/agent-claude-code/src/index.ts index 664e1a9d8..3dd6e9daf 100644 --- a/packages/plugins/agent-claude-code/src/index.ts +++ b/packages/plugins/agent-claude-code/src/index.ts @@ -710,9 +710,9 @@ function createClaudeCodeAgent(): Agent { }, async setupWorkspaceHooks(workspacePath: string, _config: WorkspaceHooksConfig): Promise { - // Use $CLAUDE_PROJECT_DIR variable for hook command - // (works in both main checkout and worktrees when .claude is symlinked) - await setupHookInWorkspace(workspacePath, '$CLAUDE_PROJECT_DIR/.claude/metadata-updater.sh'); + // Use absolute path for hook command (specific to this workspace) + const hookScriptPath = join(workspacePath, ".claude", "metadata-updater.sh"); + await setupHookInWorkspace(workspacePath, hookScriptPath); }, async postLaunchSetup(session: Session): Promise {