feat: add spawn prompt preview (#1077)
This commit is contained in:
parent
5d0b624fbe
commit
55535d480b
|
|
@ -521,10 +521,48 @@ describe("spawn command", () => {
|
|||
const help = spawnCommand?.helpInformation() ?? "";
|
||||
|
||||
expect(help).toContain("Usage: spawn [options] [issue]");
|
||||
expect(help).toContain("--preview-prompt");
|
||||
expect(help).not.toContain("[first]");
|
||||
expect(help).not.toContain("[second]");
|
||||
});
|
||||
|
||||
it("prints a composed prompt preview without spawning or requiring the daemon", async () => {
|
||||
const projects = (mockConfigRef.current as Record<string, unknown>).projects as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
projects["my-app"].agentRules = "Always write focused tests.";
|
||||
projects["my-app"].agentRulesFile = "AGENTS.md";
|
||||
writeFileSync(join(tmpDir, "main-repo", "AGENTS.md"), "Prefer small changes.");
|
||||
mockGetRunning.mockRejectedValue(new Error("daemon should not be checked"));
|
||||
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"spawn",
|
||||
"INT-42",
|
||||
"--preview-prompt",
|
||||
"--prompt",
|
||||
"Fix this\ncarefully",
|
||||
]);
|
||||
|
||||
const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(output).toContain("PROMPT PREVIEW");
|
||||
expect(output).toContain("Project: my-app");
|
||||
expect(output).toContain("Issue: INT-42");
|
||||
expect(output).toContain("tracker issue context is not fetched in preview mode");
|
||||
expect(output).toContain("--- System Prompt ---");
|
||||
expect(output).toContain("## Project Context");
|
||||
expect(output).toContain("Work on issue #INT-42");
|
||||
expect(output).toContain("Always write focused tests.");
|
||||
expect(output).toContain("Prefer small changes.");
|
||||
expect(output).toContain("--- Task Prompt ---");
|
||||
expect(output).toContain("Fix this carefully");
|
||||
expect(mockSessionManager.spawn).not.toHaveBeenCalled();
|
||||
expect(mockRegistryGet).not.toHaveBeenCalled();
|
||||
expect(mockGetRunning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects more than one positional arg with replacement usage", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import ora from "ora";
|
|||
import type { Command } from "commander";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
buildPrompt,
|
||||
getProjectSessionsDir,
|
||||
loadConfig,
|
||||
readMetadataRaw,
|
||||
recordActivityEvent,
|
||||
resolveSpawnTarget,
|
||||
TERMINAL_STATUSES,
|
||||
|
|
@ -100,6 +103,58 @@ interface SpawnClaimOptions {
|
|||
assignOnGithub?: boolean;
|
||||
}
|
||||
|
||||
function sanitizeSpawnPrompt(prompt: string | undefined): string | undefined {
|
||||
const sanitizedPrompt = prompt?.replace(/[\r\n]/g, " ").trim() || undefined;
|
||||
if (sanitizedPrompt && sanitizedPrompt.length > 4096) {
|
||||
throw new Error("Prompt must be at most 4096 characters");
|
||||
}
|
||||
return sanitizedPrompt;
|
||||
}
|
||||
|
||||
function previewSpawnPrompt(
|
||||
config: OrchestratorConfig,
|
||||
projectId: string,
|
||||
issueId?: string,
|
||||
prompt?: string,
|
||||
): void {
|
||||
const project = config.projects[projectId];
|
||||
if (!project) {
|
||||
throw new Error(`Unknown project: ${projectId}`);
|
||||
}
|
||||
|
||||
const sanitizedPrompt = sanitizeSpawnPrompt(prompt);
|
||||
const orchestratorSessionId = `${project.sessionPrefix}-orchestrator`;
|
||||
const orchestratorExists =
|
||||
readMetadataRaw(getProjectSessionsDir(projectId), orchestratorSessionId) !== null;
|
||||
const { systemPrompt, taskPrompt } = buildPrompt({
|
||||
project,
|
||||
projectId,
|
||||
issueId,
|
||||
userPrompt: sanitizedPrompt,
|
||||
...(orchestratorExists && { orchestratorSessionId }),
|
||||
});
|
||||
|
||||
console.log(banner("PROMPT PREVIEW"));
|
||||
console.log();
|
||||
console.log(`Project: ${chalk.bold(projectId)}`);
|
||||
if (issueId) {
|
||||
console.log(`Issue: ${chalk.bold(issueId)}`);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"Note: tracker issue context is not fetched in preview mode, so network-sourced issue details are omitted.",
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
console.log(chalk.bold("--- System Prompt ---"));
|
||||
console.log(systemPrompt);
|
||||
if (taskPrompt) {
|
||||
console.log();
|
||||
console.log(chalk.bold("--- Task Prompt ---"));
|
||||
console.log(taskPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle polling runs in-process inside the long-lived `ao start` process.
|
||||
* `ao spawn` is a one-shot CLI — it can't start polling in its own process
|
||||
|
|
@ -212,10 +267,7 @@ async function spawnSession(
|
|||
spinner.text = "Spawning session via core";
|
||||
|
||||
// Validate and sanitize prompt (strip newlines to prevent metadata injection)
|
||||
const sanitizedPrompt = prompt?.replace(/[\r\n]/g, " ").trim() || undefined;
|
||||
if (sanitizedPrompt && sanitizedPrompt.length > 4096) {
|
||||
throw new Error("Prompt must be at most 4096 characters");
|
||||
}
|
||||
const sanitizedPrompt = sanitizeSpawnPrompt(prompt);
|
||||
|
||||
recordActivityEvent({
|
||||
projectId,
|
||||
|
|
@ -304,6 +356,10 @@ export function registerSpawn(program: Command): void {
|
|||
.option("--agent <name>", "Override the agent plugin (e.g. codex, claude-code)")
|
||||
.option("--claim-pr <pr>", "Immediately claim an existing PR for the spawned session")
|
||||
.option("--assign-on-github", "Assign the claimed PR to the authenticated GitHub user")
|
||||
.option(
|
||||
"--preview-prompt",
|
||||
"Build and print the composed agent prompt without creating a session",
|
||||
)
|
||||
.option(
|
||||
"--prompt <text>",
|
||||
"Initial prompt/instructions for the agent (use instead of an issue)",
|
||||
|
|
@ -316,6 +372,7 @@ export function registerSpawn(program: Command): void {
|
|||
agent?: string;
|
||||
claimPr?: string;
|
||||
assignOnGithub?: boolean;
|
||||
previewPrompt?: boolean;
|
||||
prompt?: string;
|
||||
},
|
||||
command: Command,
|
||||
|
|
@ -346,6 +403,16 @@ export function registerSpawn(program: Command): void {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
if (opts.previewPrompt) {
|
||||
try {
|
||||
previewSpawnPrompt(config, projectId, issueId, opts.prompt);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const claimOptions: SpawnClaimOptions = {
|
||||
claimPr: opts.claimPr,
|
||||
assignOnGithub: opts.assignOnGithub,
|
||||
|
|
|
|||
Loading…
Reference in New Issue