rigjs
Version:
A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.
100 lines (85 loc) • 5.76 kB
text/typescript
import print from '../print';
import { requireCrew, shortPath } from './config';
import { resolveEngine } from './engine';
import {
buildEngineInvocation, dispatchTask, runParallel, runCommand,
commitWorktree, isRepoClean, mergeTaskBranch, removeTaskWorktree, deleteBranch,
} from './runtime';
import { readPlanTasks, selectDispatchable, writeTaskStatus, stampTaskDone, buildTaskPrompt, PlanTask } from './planTask';
// om-loop (design §2.2 编排循环): read a project's docs/plan/tasks, select dispatchable
// (status ready + deps done), then per task: develop (engine writes in an isolated worktree)
// → commit → verify (Tester gate = task `verify` command) → auto-merge into the base branch
// on pass. Only MERGED tasks become `done` (so deps build on landed work). Autonomy = Ral's
// om-approval decision: develop in-worktree, mandatory verify, auto-merge on pass.
interface RunOpts { crew?: string; concurrency?: string; timeout?: string; dryRun?: boolean; }
interface Outcome { id: string; status: string; ok: boolean; detail: string; }
async function processTask(repoDir: string, task: PlanTask, engine: string, timeoutMs: number): Promise<Outcome> {
const blocked = (detail: string): Outcome => { writeTaskStatus(task.file, 'blocked'); return { id: task.id, status: 'blocked', ok: false, detail }; };
writeTaskStatus(task.file, 'in-progress');
// 1. develop — engine writes inside the isolated worktree
let worktree: { path: string; branch: string };
try {
const inv = buildEngineInvocation(engine as 'claude' | 'codex' | 'pi', buildTaskPrompt(task), { autonomy: 'develop' });
const { worktree: wt, result } = await dispatchTask(repoDir, task.id, inv, { timeoutMs });
worktree = wt;
if (result.timedOut) return blocked('develop timed out');
if (result.code !== 0) return blocked(`develop exit ${result.code}`);
} catch (e: any) {
return blocked(`develop error: ${e.message}`);
}
// 2. commit the worktree changes
let committed: boolean;
try { committed = await commitWorktree(worktree.path, `task ${task.id}: ${task.title}`); }
catch (e: any) { return blocked(`commit failed: ${e.message}`); }
if (!committed) return blocked('develop produced no changes');
// 3. verify — the mandatory Tester gate (deterministic command); no command → no merge
if (!task.verify) return blocked('verified-gate missing: add `verify:` command to enable auto-merge');
const v = await runCommand('sh', ['-c', task.verify], { cwd: worktree.path, timeoutMs });
if (v.code !== 0) return blocked(`verify failed (exit ${v.code}): ${task.verify}`);
// 4. auto-merge into the base branch (only if the repo is clean, to avoid clobbering user work)
if (!(await isRepoClean(repoDir))) return blocked(`verified ok but repo dirty — merge ${worktree.branch} manually`);
const m = await mergeTaskBranch(repoDir, worktree.branch);
if (!m.merged) return blocked(`merge conflict on ${worktree.branch}: ${m.detail}`);
try { await removeTaskWorktree(repoDir, worktree.path, { force: true }); await deleteBranch(repoDir, worktree.branch); } catch { /* best-effort cleanup */ }
stampTaskDone(task.file, new Date().toISOString().slice(0, 10));
return { id: task.id, status: 'done', ok: true, detail: `${engine} → verified + merged` };
}
export default async function crewRun(project: string, opts: RunOpts): Promise<void> {
const crew = requireCrew(opts.crew);
const proj = (crew.projects || []).find(p => p.name === project);
if (!proj) {
print.error(`unknown project: ${project}. Register with \`rig orchestrate project add\`.`);
process.exit(1);
}
const all = readPlanTasks(proj.path);
if (all.length === 0) { print.info(`no tasks under ${shortPath(proj.path)}/docs/plan/tasks/`); return; }
const ready = selectDispatchable(all);
if (ready.length === 0) { print.info(`no dispatchable tasks (need status: ready + deps done) among ${all.length}.`); return; }
const planned = ready.map(task => {
try {
const r = resolveEngine({ explicit: task.engine, project: proj, crew });
return { task, engine: r.engine, source: r.source, err: '' };
} catch (e: any) {
return { task, engine: null as string | null, source: '', err: e.message };
}
});
if (opts.dryRun) {
print.info(`would dispatch ${planned.length} task(s) to ${proj.name} (develop → verify → auto-merge):`);
for (const p of planned) {
// eslint-disable-next-line no-console
console.log(` ${p.task.id} [${p.task.role || '?'}] → ${p.engine || 'UNRESOLVED'}${p.source ? ` (${p.source})` : ''} verify: ${p.task.verify || '(none → will not merge)'}${p.err ? ` ${p.err}` : ''}`);
}
return;
}
const concurrency = opts.concurrency ? Number(opts.concurrency) : 4;
const timeoutMs = opts.timeout ? Number(opts.timeout) : 600000;
print.info(`dispatching ${planned.length} task(s) to ${proj.name} (concurrency ${concurrency}, develop→verify→merge) …`);
const outcomes = await runParallel<typeof planned[number], Outcome>(planned, async (p) => {
if (!p.engine) { writeTaskStatus(p.task.file, 'blocked'); return { id: p.task.id, status: 'blocked', ok: false, detail: p.err || 'engine unresolved' }; }
try { return await processTask(proj.path, p.task, p.engine, timeoutMs); }
catch (e: any) { writeTaskStatus(p.task.file, 'blocked'); return { id: p.task.id, status: 'blocked', ok: false, detail: e.message }; }
}, concurrency);
const done = outcomes.filter(o => o.ok).length;
for (const o of outcomes) (o.ok ? print.succeed : print.warn)(`${o.id} → ${o.status} (${o.detail})`);
print.info(`run complete: ${done}/${outcomes.length} merged. Blocked tasks keep their worktree (task/<id>) for inspection.`);
}