// Runs one ordered agent profile. Credential decryption and reviewer posting // live in separate processes so neither privileged secret reaches this runner. import { chmod, readFile, writeFile } from "node:fs/promises"; import { codexAuthentication, persistCodexAuth, stageCodexAuth } from "./auth.ts"; import { runAgent } from "./agents.ts"; import type { AgentAnswer } from "./agents.ts"; import { output } from "./command.ts"; import { killProfileProcesses, readProfileFile } from "./isolation.ts"; import { isObject } from "./json.ts"; import { runProfiles } from "./profiles.ts"; import type { ProfileFailure } from "./profiles.ts"; import { API, env, EVENT, gitea, INDEX, makeProfileFile, MANIFEST, PROCESS_ENV, REPO, runAgentCommand, WORKSPACE, } from "./run-context.ts"; import type { RuntimeOutcome, RuntimeProfile } from "./runtime.ts"; interface AttemptResult { body: string; reviewPath: string; anchorsPath: string; profile: Pick; } interface RenderedPrompt { prompt: string; reviewPath: string; anchorsPath: string; } const RULES_PATH = "repos/commons/code-rules/raw/README.md"; const CODEX_AUTH = "/root/.codex/auth.json"; const SKILLS_INSTALL = "skills add obra/superpowers"; async function runRoot(command: string, args: string[]): Promise { const { success, code } = await output(command, { args, env: PROCESS_ENV, clearEnv: true }); if (!success) { throw new Error(`${command} ${args[0]} exited with ${code}`); } } async function configureGitAuthor(): Promise { const user: unknown = await (await gitea("user")).json(); if (!isObject(user) || typeof user.login !== "string" || typeof user.email !== "string") { throw new Error("user: unexpected Gitea response"); } await runRoot("git", ["config", "user.name", user.login]); await runRoot("git", ["config", "user.email", user.email]); } async function readProfileToken(profile: RuntimeProfile): Promise { const token = profile.tokenFile === null ? undefined : await readFile(profile.tokenFile, "utf8"); return token; } async function prepareProfile(profile: RuntimeProfile): Promise { await runAgentCommand(profile, "git", [ "config", "--global", "--add", "safe.directory", WORKSPACE, ]); await runAgentCommand(profile, "npx", [ "-y", ...SKILLS_INSTALL.split(" "), "--global", "--agent", profile.type === "claude" ? "claude-code" : profile.type, "--yes", ]); } // One issue comment as the prompt quotes it. function formatComment(comment: unknown): string { const { user, created_at: createdAt, body } = isObject(comment) ? comment : {}; const login = isObject(user) ? user.login : undefined; return `## ${String(login)} at ${String(createdAt)}\n\n${String(body)}\n`; } async function promptValues( reviewPath: string, anchorsPath: string, ): Promise> { const comments: unknown = await ( await gitea(`repos/${REPO}/issues/${INDEX}/comments?limit=100`) ).json(); if (!Array.isArray(comments)) { throw new TypeError("comments: unexpected Gitea response"); } return { COMMENT: env("COMMENT"), ISSUE_COMMENTS: comments.map((comment: unknown) => formatComment(comment)).join("\n"), CODE_RULES: await (await gitea(RULES_PATH)).text(), EVENT_NAME: EVENT, GITEA_API_URL: API, GITEA_REPOSITORY: REPO, ISSUE_INDEX: INDEX, REVIEW_PATH: reviewPath, ANCHORS_PATH: anchorsPath, }; } async function renderPrompt(profile: RuntimeProfile): Promise { const reviewPath = await makeProfileFile(profile, "review.md"); const anchorsPath = await makeProfileFile(profile, "anchors.json"); const values = await promptValues(reviewPath, anchorsPath); const template = await readFile(new URL("../prompt.md", import.meta.url), "utf8"); return { prompt: template.replaceAll( /\$\{(\w+)\}/gu, (match: string, name: string) => values[name] ?? match, ), reviewPath, anchorsPath, }; } async function runProfile(profile: RuntimeProfile): Promise { const token = await readProfileToken(profile); const authentication = profile.type === "codex" ? codexAuthentication(token) : null; let stagedCodexAuth: string | null = null; let answer: AgentAnswer = {}; let rendered: RenderedPrompt | undefined; try { if (authentication?.kind === "persistent") { stagedCodexAuth = await stageCodexAuth(CODEX_AUTH, profile.home, profile.uid, profile.gid); } await prepareProfile(profile); rendered = await renderPrompt(profile); answer = await runAgent(profile, token, rendered.prompt, authentication); } finally { await killProfileProcesses(profile.uid); if (stagedCodexAuth !== null) { await persistCodexAuth(stagedCodexAuth, CODEX_AUTH); } } const body = answer.resultPath === undefined ? answer.body : await readProfileFile(profile, answer.resultPath); if (body === undefined || rendered === undefined) { throw new Error("agent attempt produced no result"); } const { reviewPath, anchorsPath } = rendered; return { body, reviewPath, anchorsPath, profile: { type: profile.type, model: profile.model } }; } async function writeOutcome(outcome: RuntimeOutcome): Promise { await writeFile(MANIFEST.outcomeFile, JSON.stringify(outcome), { mode: 0o600 }); await chmod(MANIFEST.outcomeFile, 0o600); } const failures: ProfileFailure[] = []; try { await configureGitAuthor(); await writeOutcome({ ok: true, ...(await runProfiles(MANIFEST.profiles, runProfile, failures)) }); } catch (error) { console.error(error); await writeOutcome({ ok: false, failures }); throw error; }