blob: 85ced98e72a6a3b3307cc91e737ad95a0e91e41b [file]
import { execFile } from 'node:child_process';
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { readFixedPromptWal } from './fixed-prompt-controller.js';
import {
reconcilePromptRepoWithReplayState,
derivePromptOptimizationReplayState,
replayStateHasRecoverablePendingCandidateEvidence,
} from './prompt-optimization-replay.js';
const execFileAsync = promisify(execFile);
export interface EnsurePromptOptimizationPromptRepoInput {
promptRepoDir: string;
program: string;
systemPrompt: string;
}
export interface PromptOptimizationPromptRepoPaths {
agentCwdPath: string;
programPath: string;
systemPromptPath: string;
}
export async function ensurePromptOptimizationPromptRepo(
input: EnsurePromptOptimizationPromptRepoInput,
): Promise<PromptOptimizationPromptRepoPaths> {
const agentCwdPath = join(input.promptRepoDir, 'agent-cwd');
const programPath = join(input.promptRepoDir, 'program.md');
const systemPromptPath = join(input.promptRepoDir, 'system_prompt.md');
await mkdir(agentCwdPath, { recursive: true });
if (await pathExists(join(input.promptRepoDir, '.git'))) {
if (!(await hasHead(input.promptRepoDir))) {
await ensureSeedFiles(input);
await commitSeed(input.promptRepoDir);
return { agentCwdPath, programPath, systemPromptPath };
}
const seedCommitSha = await gitOutput(
input.promptRepoDir,
'rev-list',
'--max-parents=0',
'HEAD',
);
await assertSeedCommitFilesMatchInput(input, seedCommitSha);
return { agentCwdPath, programPath, systemPromptPath };
}
await ensureSeedFiles(input);
await git(input.promptRepoDir, 'init', '-q');
await commitSeed(input.promptRepoDir);
return { agentCwdPath, programPath, systemPromptPath };
}
export async function preparePromptOptimizationResume(input: {
promptRepoDir: string;
resultsJsonlPath: string;
}): Promise<void> {
const events = await readFixedPromptWal(input.resultsJsonlPath);
const replayState = await derivePromptOptimizationReplayState({
events,
promptRepoDir: input.promptRepoDir,
});
await reconcilePromptRepoWithReplayState({
gitRootPath: input.promptRepoDir,
expectedHead: replayState.expectedPromptRepoHead,
programPath: join(input.promptRepoDir, 'program.md'),
systemPromptGitPath: 'system_prompt.md',
...(replayStateHasRecoverablePendingCandidateEvidence({ events, state: replayState })
? { recoverExpectedHeadFromParent: true }
: {}),
});
}
async function assertExistingSeedFile(path: string, expected: string): Promise<void> {
const actual = await readFile(path, 'utf8');
if (actual !== expected) {
throw new Error(`existing prompt repo seed files do not match this run: ${path}`);
}
}
async function assertSeedCommitFilesMatchInput(
input: EnsurePromptOptimizationPromptRepoInput,
seedCommitSha: string,
): Promise<void> {
const [program, systemPrompt] = await Promise.all([
gitBlob(input.promptRepoDir, `${seedCommitSha}:program.md`),
gitBlob(input.promptRepoDir, `${seedCommitSha}:system_prompt.md`),
]);
if (program !== input.program) {
throw new Error('existing prompt repo seed files do not match this run: program.md');
}
if (systemPrompt !== input.systemPrompt) {
throw new Error('existing prompt repo seed files do not match this run: system_prompt.md');
}
}
async function ensureSeedFiles(input: EnsurePromptOptimizationPromptRepoInput): Promise<void> {
const programPath = join(input.promptRepoDir, 'program.md');
const systemPromptPath = join(input.promptRepoDir, 'system_prompt.md');
await mkdir(input.promptRepoDir, { recursive: true });
await ensureSeedFile(programPath, input.program);
await ensureSeedFile(systemPromptPath, input.systemPrompt);
}
async function ensureSeedFile(path: string, expected: string): Promise<void> {
try {
await assertExistingSeedFile(path, expected);
} catch (error) {
if (isNotFound(error)) {
await writeFile(path, expected, 'utf8');
return;
}
throw error;
}
}
async function commitSeed(promptRepoDir: string): Promise<void> {
await git(promptRepoDir, 'config', 'user.email', 'rsi@maka.local');
await git(promptRepoDir, 'config', 'user.name', 'RSI Loop');
await git(promptRepoDir, 'add', 'program.md', 'system_prompt.md');
await git(promptRepoDir, 'commit', '-q', '-m', 'seed prompt');
}
async function hasHead(promptRepoDir: string): Promise<boolean> {
try {
await gitOutput(promptRepoDir, 'rev-parse', '--verify', 'HEAD');
return true;
} catch {
return false;
}
}
async function git(cwd: string, ...args: string[]): Promise<void> {
await execFileAsync('git', args, { cwd });
}
async function gitOutput(cwd: string, ...args: string[]): Promise<string> {
const { stdout } = await execFileAsync('git', args, { cwd, encoding: 'utf8' });
return stdout.trim();
}
async function gitBlob(cwd: string, refPath: string): Promise<string> {
const { stdout } = await execFileAsync('git', ['show', refPath], { cwd, encoding: 'utf8' });
return stdout;
}
async function pathExists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch (error) {
if (isNotFound(error)) {
return false;
}
throw error;
}
}
function isNotFound(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
(error as { code?: unknown }).code === 'ENOENT'
);
}