diff --git a/packages/cli/src/commands/spawn.ts b/packages/cli/src/commands/spawn.ts index 52fdfed0d..5035ab780 100644 --- a/packages/cli/src/commands/spawn.ts +++ b/packages/cli/src/commands/spawn.ts @@ -11,6 +11,8 @@ async function spawnSession( projectId: string, issueId?: string, openTab?: boolean, + branch?: string, + prompt?: string, ): Promise { const spinner = ora("Creating session").start(); @@ -21,6 +23,8 @@ async function spawnSession( const session = await sm.spawn({ projectId, issueId, + branch, + prompt, }); spinner.succeed(`Session ${chalk.green(session.id)} created`); @@ -58,7 +62,9 @@ export function registerSpawn(program: Command): void { .argument("", "Project ID from config") .argument("[issue]", "Issue identifier (e.g. INT-1234, #42) - must exist in tracker") .option("--open", "Open session in terminal tab") - .action(async (projectId: string, issueId: string | undefined, opts: { open?: boolean }) => { + .option("--branch ", "Use existing branch instead of creating from issue") + .option("-p, --prompt ", "Initial prompt to send to the agent") + .action(async (projectId: string, issueId: string | undefined, opts: { open?: boolean; branch?: string; prompt?: string }) => { const config = loadConfig(); if (!config.projects[projectId]) { console.error( @@ -70,7 +76,7 @@ export function registerSpawn(program: Command): void { } try { - await spawnSession(config, projectId, issueId, opts.open); + await spawnSession(config, projectId, issueId, opts.open, opts.branch, opts.prompt); } catch (err) { console.error(chalk.red(`✗ ${err}`)); process.exit(1); diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index c58a286cd..5eea8bdab 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -112,6 +112,24 @@ function validateStatus(raw: string | undefined): SessionStatus { return "spawning"; } +/** Parse a PR URL (typically GitHub) into a PRInfo-like object, or return null. */ +function parsePrUrl(prUrl: string | undefined, branch?: string): Session["pr"] { + if (!prUrl) return null; + const ghMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/); + return { + number: ghMatch + ? parseInt(ghMatch[3], 10) + : parseInt(prUrl.match(/\/(\d+)$/)?.[1] ?? "0", 10), + url: prUrl, + title: "", + owner: ghMatch?.[1] ?? "", + repo: ghMatch?.[2] ?? "", + branch: branch ?? "", + baseBranch: "", + isDraft: false, + }; +} + /** Reconstruct a Session object from raw metadata key=value pairs. */ function metadataToSession( sessionId: SessionId, @@ -126,25 +144,7 @@ function metadataToSession( activity: null, branch: meta["branch"] || null, issueId: meta["issue"] || null, - pr: meta["pr"] - ? (() => { - // Parse owner/repo from GitHub PR URL: https://github.com/owner/repo/pull/123 - const prUrl = meta["pr"]; - const ghMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/); - return { - number: ghMatch - ? parseInt(ghMatch[3], 10) - : parseInt(prUrl.match(/\/(\d+)$/)?.[1] ?? "0", 10), - url: prUrl, - title: "", - owner: ghMatch?.[1] ?? "", - repo: ghMatch?.[2] ?? "", - branch: meta["branch"] ?? "", - baseBranch: "", - isDraft: false, - }; - })() - : null, + pr: parsePrUrl(meta["pr"], meta["branch"]), workspacePath: meta["worktree"] || null, runtimeHandle: meta["runtimeHandle"] ? safeJsonParse(meta["runtimeHandle"]) @@ -446,12 +446,25 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager { userPrompt: spawnConfig.prompt, }); + // Write long prompts to a file to avoid shell/tmux truncation + const finalPrompt = composedPrompt ?? spawnConfig.prompt; + let promptFile: string | undefined; + if (finalPrompt) { + const baseDir = config.configPath + ? getProjectBaseDir(config.configPath, project.path) + : sessionsDir; + mkdirSync(baseDir, { recursive: true }); + promptFile = join(baseDir, `${sessionId}-prompt.md`); + writeFileSync(promptFile, finalPrompt, "utf-8"); + } + // Get agent launch config and create runtime — clean up workspace on failure const agentLaunchConfig = { sessionId, projectConfig: project, issueId: spawnConfig.issueId, - prompt: composedPrompt ?? spawnConfig.prompt, + prompt: finalPrompt, + promptFile, permissions: project.agentConfig?.permissions, model: project.agentConfig?.model, }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6de795c8f..d6d94bc1e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -341,6 +341,15 @@ export interface AgentLaunchConfig { * - Codex/Aider: similar shell substitution */ systemPromptFile?: string; + /** + * Path to a file containing the initial prompt (-p). + * Preferred over inline `prompt` for long prompts to avoid shell/tmux truncation. + * + * When set, takes precedence over prompt. + * - Claude Code: -p "$(cat /path/to/file)" + * - Codex/Aider: similar shell substitution + */ + promptFile?: string; } export interface WorkspaceHooksConfig { diff --git a/packages/plugins/agent-aider/src/index.ts b/packages/plugins/agent-aider/src/index.ts index b103248ca..f38359a17 100644 --- a/packages/plugins/agent-aider/src/index.ts +++ b/packages/plugins/agent-aider/src/index.ts @@ -89,7 +89,9 @@ function createAiderAgent(): Agent { parts.push("--system-prompt", shellEscape(config.systemPrompt)); } - if (config.prompt) { + if (config.promptFile) { + parts.push("--message", `"$(cat ${shellEscape(config.promptFile)})"`); + } else if (config.prompt) { parts.push("--message", shellEscape(config.prompt)); } diff --git a/packages/plugins/agent-claude-code/src/index.ts b/packages/plugins/agent-claude-code/src/index.ts index cb2abbc26..ffc704783 100644 --- a/packages/plugins/agent-claude-code/src/index.ts +++ b/packages/plugins/agent-claude-code/src/index.ts @@ -605,7 +605,9 @@ function createClaudeCodeAgent(): Agent { parts.push("--append-system-prompt", shellEscape(config.systemPrompt)); } - if (config.prompt) { + if (config.promptFile) { + parts.push("-p", `"$(cat ${shellEscape(config.promptFile)})"`); + } else if (config.prompt) { parts.push("-p", shellEscape(config.prompt)); } diff --git a/packages/plugins/agent-codex/src/index.ts b/packages/plugins/agent-codex/src/index.ts index 0c6952052..362bedae3 100644 --- a/packages/plugins/agent-codex/src/index.ts +++ b/packages/plugins/agent-codex/src/index.ts @@ -51,7 +51,9 @@ function createCodexAgent(): Agent { parts.push("--system-prompt", shellEscape(config.systemPrompt)); } - if (config.prompt) { + if (config.promptFile) { + parts.push("--", `"$(cat ${shellEscape(config.promptFile)})"`); + } else if (config.prompt) { // Use `--` to end option parsing so prompts starting with `-` aren't // misinterpreted as flags. parts.push("--", shellEscape(config.prompt)); diff --git a/packages/plugins/agent-opencode/src/index.ts b/packages/plugins/agent-opencode/src/index.ts index 5b4f2ffd9..9449c23b1 100644 --- a/packages/plugins/agent-opencode/src/index.ts +++ b/packages/plugins/agent-opencode/src/index.ts @@ -37,7 +37,9 @@ function createOpenCodeAgent(): Agent { getLaunchCommand(config: AgentLaunchConfig): string { const parts: string[] = ["opencode"]; - if (config.prompt) { + if (config.promptFile) { + parts.push("run", `"$(cat ${shellEscape(config.promptFile)})"`); + } else if (config.prompt) { parts.push("run", shellEscape(config.prompt)); } diff --git a/packages/web/src/components/CIBadge.tsx b/packages/web/src/components/CIBadge.tsx index d5aef8156..7bb1cabd3 100644 --- a/packages/web/src/components/CIBadge.tsx +++ b/packages/web/src/components/CIBadge.tsx @@ -86,7 +86,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) { if (layout === "inline") { return (
- {sorted.map((check) => { + {sorted.map((check, i) => { const { icon, color } = checkStatusIcon[check.status]; const inner = ( @@ -96,7 +96,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) { ); return check.url ? ( ) : ( - {inner} + {inner} ); })}
@@ -115,7 +115,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) { if (layout === "expanded") { return (