Show the selected bot profile and add OpenRouter fallback (#18)

Uses Codex first, then Claude, then OpenCode through OpenRouter when quota is exhausted.

Every generated comment and review now identifies the selected profile and model in a final quote. The dependent action interface remains one master key.

Verified formatting, lint, type checks, encrypted profile decryption, current OpenCode CLI flags, and 45 tests.

Reviewed-on: #18
Reviewed-by: bot <noreply@capsulizers.com>
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #18.
This commit is contained in:
Danny Kim
2026-09-24 01:39:49 +00:00
committed by temeddix
parent 4ecdf5adc0
commit ea20f6d629
13 changed files with 429 additions and 78 deletions
+56 -8
View File
@@ -17,7 +17,13 @@ import {
agentEnvironment,
runProfiles,
sanitizedEnvironment,
withProfile,
} from "./profiles.ts";
import {
collectOpenCodeOutput,
openCodeArgs,
parseOpenCodeEvents,
} from "./opencode.ts";
import {
readRuntimeManifest,
type RuntimeOutcome,
@@ -30,6 +36,7 @@ type AttemptResult = {
body: string;
reviewPath: string;
anchorsPath: string;
profile: Pick<RuntimeProfile, "type" | "model">;
};
const env = (name: string): string => {
@@ -109,7 +116,6 @@ async function runAgentCommand(
async function prepareProfile(
profile: RuntimeProfile,
token: string | undefined,
): Promise<void> {
await runAgentCommand(profile, "git", [
"config",
@@ -117,15 +123,15 @@ async function prepareProfile(
"--add",
"safe.directory",
WORKSPACE,
], token);
]);
await runAgentCommand(profile, "npx", [
"-y",
...SKILLS_INSTALL.split(" "),
"--global",
"--agent",
profile.type === "claude" ? "claude-code" : "codex",
profile.type === "claude" ? "claude-code" : profile.type,
"--yes",
], token);
]);
}
async function renderPrompt(
@@ -253,7 +259,7 @@ async function codexDeviceLogin(
]);
if (exited) throw new Error("AI bot login produced no instructions");
}
await postAuthorComment(shown);
await postAuthorComment(withProfile(shown, profile));
const [result] = await Promise.all([status, drained]);
if (!result.success) {
throw new Error(`codex login exited with ${result.code}`);
@@ -357,6 +363,41 @@ async function runCodex(
return outputPath;
}
async function runOpenCode(
profile: RuntimeProfile,
token: string,
prompt: string,
): Promise<string> {
const process = agentCommand(
profile,
"opencode",
openCodeArgs(profile.model, prompt),
{
cwd: WORKSPACE,
env: agentEnvironment(PROCESS_ENV, profile, token),
clearEnv: true,
stdout: "piped",
stderr: "piped",
},
).spawn();
const output = collectOpenCodeOutput(
process.stdout,
process.stderr,
async (chunk) => {
await Deno.stdout.write(chunk);
},
async (chunk) => {
await Deno.stderr.write(chunk);
},
);
const [status, streams] = await Promise.all([process.status, output]);
const error = streams.stderr.trim();
if (!status.success) {
throw new Error(error || `opencode exited with ${status.code}`);
}
return parseOpenCodeEvents(streams.stdout);
}
async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
const token = await readProfileToken(profile);
const authentication = profile.type === "codex"
@@ -376,14 +417,16 @@ async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
profile.gid,
);
}
await prepareProfile(profile, token);
await prepareProfile(profile);
const rendered = await renderPrompt(profile);
reviewPath = rendered.reviewPath;
anchorsPath = rendered.anchorsPath;
if (profile.type === "claude") {
body = await runClaude(profile, token ?? "", rendered.prompt);
} else {
} else if (profile.type === "codex") {
resultPath = await runCodex(profile, rendered.prompt, authentication!);
} else {
body = await runOpenCode(profile, token ?? "", rendered.prompt);
}
} finally {
await killProfileProcesses(profile.uid);
@@ -397,7 +440,12 @@ async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
) {
throw new Error("agent attempt produced no result");
}
return { body, reviewPath, anchorsPath };
return {
body,
reviewPath,
anchorsPath,
profile: { type: profile.type, model: profile.model },
};
}
async function writeOutcome(outcome: RuntimeOutcome): Promise<void> {