Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114/** * `aite author JOURNEY --out scripts/x.aite` (P7-2): a model writes a script from a journey, the script is run, and what the run saw goes back to the model, until the * script is green or the attempts run out. The model only ever returns the text of one script; this loop is what writes the file, runs it, and refuses what * PLAN.md section 7 forbids (runner/authorGuard.ts): a weaker verify, an invented expectation, a check that is not on the allowlist. It stops and asks a person instead of * accepting any of them. A green script is not done: a person reads it and the lock file it made, and commits them (that is the approval). */import { isAbsolute, relative, resolve, sep } from "node:path";import { buildAuthorPrompt, failureReport, untrustedBlock } from "./authorPrompt";import { checkProposal, extractScript } from "./authorGuard";import type { RunOptions, RunOutcome } from "./run";import type { RunJson } from "./runJson";
export interface AuthorModel { /** Gives the model the prompt and returns what it answered. */ propose(prompt: string): Promise<string>;}
export interface AuthorOptions { journey: string; projectRoot: string; out: string; configPath: string; maxAttempts: number; headless: boolean; noSandbox: boolean; display?: string; profile?: string;}
export interface AuthorDeps { model: AuthorModel; runScript(options: RunOptions): Promise<RunOutcome>; readFile(path: string): string; /** `exclusive`: fail if the file is there (the first write of the script). */ writeFile(path: string, text: string, exclusive?: boolean): void; exists(path: string): boolean; isLink(path: string): boolean; checkAllowed(checksDir: string, name: string): string | undefined; log(line: string): void;}
export type AuthorStatus = "green" | "gave-up" | "needs-human" | "cannot-run" | "model-failed" | "usage";export interface AuthorResult { status: AuthorStatus; exitCode: 0 | 1 | 2 | 3; attempts: number; output: string;}
const messageOf = (e: unknown) => (e instanceof Error ? e.message : String(e));
export async function runAuthor(options: AuthorOptions, deps: AuthorDeps): Promise<AuthorResult> { const usage = (output: string): AuthorResult => ({ status: "usage", exitCode: 2, attempts: 0, output }); if (options.journey.trim() === "") return usage("the journey is empty: write what the person does and what they should see, in words"); if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) return usage("the number of attempts must be a whole number of 1 or more"); const scriptsDir = resolve(options.projectRoot, "scripts"); const out = resolve(options.out); const rel = relative(scriptsDir, out); if (rel === "" || rel.startsWith("..") || isAbsolute(rel) || rel.startsWith(`..${sep}`)) return usage(`${options.out} is not inside the project's scripts folder (${scriptsDir}): the author writes nothing anywhere else`); if (!out.endsWith(".aite")) return usage(`${options.out} must end in .aite`); if (deps.isLink(out)) return usage(`${options.out} is a symbolic link; the author does not write through links`); if (deps.exists(out)) return usage(`${options.out} already exists: the author does not overwrite a file (choose another --out)`);
let previousRun: string | undefined; // the text of the last script that was run let problem: string | undefined; let written = false; for (let attempt = 1; attempt <= options.maxAttempts; attempt++) { const prompt = buildAuthorPrompt({ journey: options.journey, attempt, maxAttempts: options.maxAttempts, ...(previousRun !== undefined && problem !== undefined ? { previous: { script: previousRun, problem } } : {}) }); let answer: string; try { answer = await deps.model.propose(prompt); } catch (err) { return { status: "model-failed", exitCode: 3, attempts: attempt - 1, output: `the model could not be asked: ${messageOf(err)}` }; } const text = extractScript(answer); const proposal = checkProposal(text, { journey: options.journey, ...(previousRun !== undefined ? { previous: previousRun } : {}), checkAllowed: deps.checkAllowed }); if (!proposal.ok) { if (proposal.action === "ask") { return { status: "needs-human", exitCode: 1, attempts: attempt, output: `the author stopped for a person (${proposal.kind}); the script was not changed:\n${proposal.messages.map((m) => ` - ${m}`).join("\n")}${written ? `\nthe last script that was run is still ${options.out}` : ""}` }; } deps.log(`attempt ${attempt}: refused (${proposal.kind}); asking again`); problem = `Your script was refused before it ran (${proposal.kind}):\n${proposal.messages.map((m) => `- ${m}`).join("\n")}`; previousRun = previousRun ?? text; continue; } try { deps.writeFile(out, text, !written); written = true; } catch (err) { return { status: "usage", exitCode: 2, attempts: attempt, output: `could not write ${options.out}: ${messageOf(err)}` }; } const outcome = await deps.runScript({ scriptPath: out, configPath: options.configPath, headless: options.headless, noSandbox: options.noSandbox, ...(options.display !== undefined ? { display: options.display } : {}), ...(options.profile !== undefined ? { profile: options.profile } : {}), }); previousRun = text; if (outcome.exitCode === 0) { return { status: "green", exitCode: 0, attempts: attempt, output: `green after ${attempt} attempt${attempt === 1 ? "" : "s"}: ${options.out}\nA person must review the script and the lock file it made (next to it), then commit both: that is the approval.` }; } if (outcome.exitCode >= 2) return { status: "cannot-run", exitCode: 3, attempts: attempt, output: `the script could not be run, so the author stopped: ${outcome.output.split("\n")[0] ?? ""}` }; const dir = [...outcome.output.matchAll(/^results: (.+)$/gm)].pop()?.[1]; problem = untrustedBlock(outcome.output, 1500); if (dir !== undefined) { try { problem = failureReport(JSON.parse(deps.readFile(`${dir}/run.json`)) as RunJson); } catch { /* the run's own output, marked as untrusted, is all there is */ } } deps.log(`attempt ${attempt}: the script ran and failed`); } return { status: "gave-up", exitCode: 1, attempts: options.maxAttempts, output: `not green after ${options.maxAttempts} attempts: ${options.out} is a failing draft, not a test. Read what the last run said, and fix the script or the journey.` };}