feat: add --branch/--prompt to spawn CLI + file-based prompt delivery
- Add --branch and --prompt options to `ao spawn` CLI command - Write long prompts to file and use $(cat ...) to avoid tmux truncation - Add promptFile field to AgentLaunchConfig, handled by all 4 agent plugins - Extract shared parsePrUrl() helper to deduplicate PR URL parsing - Fix duplicate React keys in CIBadge when CI checks share names Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40c1906d41
commit
6d1f05f5a1
|
|
@ -11,6 +11,8 @@ async function spawnSession(
|
|||
projectId: string,
|
||||
issueId?: string,
|
||||
openTab?: boolean,
|
||||
branch?: string,
|
||||
prompt?: string,
|
||||
): Promise<string> {
|
||||
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>", "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 <branch>", "Use existing branch instead of creating from issue")
|
||||
.option("-p, --prompt <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);
|
||||
|
|
|
|||
|
|
@ -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<RuntimeHandle>(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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) {
|
|||
if (layout === "inline") {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
{sorted.map((check) => {
|
||||
{sorted.map((check, i) => {
|
||||
const { icon, color } = checkStatusIcon[check.status];
|
||||
const inner = (
|
||||
<span className="inline-flex items-center gap-1 text-xs">
|
||||
|
|
@ -96,7 +96,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) {
|
|||
);
|
||||
return check.url ? (
|
||||
<a
|
||||
key={check.name}
|
||||
key={`${check.name}-${i}`}
|
||||
href={check.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
|
@ -105,7 +105,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) {
|
|||
{inner}
|
||||
</a>
|
||||
) : (
|
||||
<span key={check.name}>{inner}</span>
|
||||
<span key={`${check.name}-${i}`}>{inner}</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -115,7 +115,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) {
|
|||
if (layout === "expanded") {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{sorted.map((check) => {
|
||||
{sorted.map((check, i) => {
|
||||
const { icon, color } = checkStatusIcon[check.status];
|
||||
const inner = (
|
||||
<span className="inline-flex items-center gap-1 text-xs">
|
||||
|
|
@ -124,7 +124,7 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) {
|
|||
</span>
|
||||
);
|
||||
return (
|
||||
<div key={check.name} className="flex items-center gap-2">
|
||||
<div key={`${check.name}-${i}`} className="flex items-center gap-2">
|
||||
{check.url ? (
|
||||
<a
|
||||
href={check.url}
|
||||
|
|
@ -156,10 +156,10 @@ export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) {
|
|||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{sorted.map((check) => {
|
||||
{sorted.map((check, i) => {
|
||||
const { icon, color } = checkStatusIcon[check.status];
|
||||
return (
|
||||
<div key={check.name} className="flex items-center gap-2 text-xs">
|
||||
<div key={`${check.name}-${i}`} className="flex items-center gap-2 text-xs">
|
||||
<span style={{ color }} className="w-3.5 shrink-0 text-center">
|
||||
{icon}
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Reference in New Issue