UNPKG

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
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.`); }