- planner: { ready }, coder: { done }, tester: { passed }, committer: { success }
- planner/coder: createCursorRole, tester: createHermesRole
- committer: direct spawn, output to .log file
- moderator: coder loop (max 5), committer fail → coder
- bundle 24kb → 8.7kb
Fixes #5
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import { writeFileSync, mkdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import type { Role, RoleResult } from "@uncaged/nerve-core";
|
|
import { isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils";
|
|
|
|
export type CommitterMeta = {
|
|
success: boolean;
|
|
};
|
|
|
|
export type BuildCommitterDeps = {
|
|
nerveRoot: string;
|
|
};
|
|
|
|
function logPath(nerveRoot: string): string {
|
|
return join(nerveRoot, "logs", `committer-${Date.now()}.log`);
|
|
}
|
|
|
|
function writeLog(path: string, content: string): void {
|
|
mkdirSync(join(path, ".."), { recursive: true });
|
|
writeFileSync(path, content, "utf-8");
|
|
}
|
|
|
|
export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<CommitterMeta> {
|
|
return async (start, _messages) => {
|
|
const dry = isDryRun(start);
|
|
const file = logPath(nerveRoot);
|
|
|
|
if (dry) {
|
|
writeLog(file, "[dry-run] committer skipped\n");
|
|
return {
|
|
content: `[dry-run] committer skipped — log: ${file}`,
|
|
meta: { success: true },
|
|
} satisfies RoleResult<CommitterMeta>;
|
|
}
|
|
|
|
const lines: string[] = [];
|
|
let success = true;
|
|
|
|
const run = async (cmd: string, args: string[]): Promise<boolean> => {
|
|
const r = await spawnSafe(cmd, args, { cwd: nerveRoot, env: null, timeoutMs: 60_000, dryRun: false });
|
|
if (r.ok) {
|
|
lines.push(`$ ${cmd} ${args.join(" ")}`);
|
|
if (r.value.stdout) lines.push(r.value.stdout);
|
|
if (r.value.stderr) lines.push(r.value.stderr);
|
|
lines.push("");
|
|
return true;
|
|
}
|
|
const e = r.error;
|
|
lines.push(`$ ${cmd} ${args.join(" ")} — FAILED`);
|
|
if (e.kind === "non_zero_exit") {
|
|
lines.push(`exit ${e.exitCode}`);
|
|
if (e.stdout) lines.push(e.stdout);
|
|
if (e.stderr) lines.push(e.stderr);
|
|
} else if (e.kind === "timeout") {
|
|
lines.push("timeout");
|
|
if (e.stdout) lines.push(e.stdout);
|
|
if (e.stderr) lines.push(e.stderr);
|
|
} else {
|
|
lines.push(e.message);
|
|
}
|
|
lines.push("");
|
|
return false;
|
|
};
|
|
|
|
await run("git", ["add", "-A"]);
|
|
const committed = await run("git", ["commit", "-m", "chore: add generated workflow"]);
|
|
if (!committed) {
|
|
success = false;
|
|
} else {
|
|
const pushed = await run("git", ["push"]);
|
|
if (!pushed) success = false;
|
|
}
|
|
|
|
const log = lines.join("\n");
|
|
writeLog(file, log);
|
|
|
|
const summary = success ? "committed and pushed" : "commit/push failed — see log";
|
|
return {
|
|
content: `committer: ${summary}\nLog: ${file}`,
|
|
meta: { success },
|
|
} satisfies RoleResult<CommitterMeta>;
|
|
};
|
|
}
|