小橘 🍊(NEKO Team) 56ce22fb1b Migrate workflows to WorkflowSpec-style roles (RFC-003)
Replace createCursorRole/createHermesRole with adapter + prompt + zod meta.

Add shared compileRoleSpec, cursor ask adapter, nerve.yaml extract defaults.

Refs #248

Made-with: Cursor
2026-04-29 09:23:55 +00:00

92 lines
2.7 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,
abortSignal: null,
});
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 if (e.kind === "spawn_failed") {
lines.push(e.message);
} else {
lines.push(e.kind === "aborted" ? "aborted" : "error");
}
lines.push("");
return false;
};
await run("git", ["add", "-A"]);
const committed = await run("git", ["commit", "-m", "chore(sense): auto-generated commit"]);
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>;
};
}