rich-parallel-agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classify.d.ts +4 -0
- package/dist/classify.js +46 -0
- package/dist/clean.d.ts +25 -0
- package/dist/clean.js +64 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +449 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +142 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +8 -0
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +58 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +71 -0
- package/dist/gate.d.ts +24 -0
- package/dist/gate.js +329 -0
- package/dist/git.d.ts +19 -0
- package/dist/git.js +64 -0
- package/dist/history.d.ts +27 -0
- package/dist/history.js +106 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +18 -0
- package/dist/integrate.d.ts +6 -0
- package/dist/integrate.js +185 -0
- package/dist/journal.d.ts +10 -0
- package/dist/journal.js +34 -0
- package/dist/orca.d.ts +16 -0
- package/dist/orca.js +168 -0
- package/dist/plan.d.ts +6 -0
- package/dist/plan.js +114 -0
- package/dist/repair.d.ts +13 -0
- package/dist/repair.js +110 -0
- package/dist/resources.d.ts +8 -0
- package/dist/resources.js +41 -0
- package/dist/resume.d.ts +12 -0
- package/dist/resume.js +129 -0
- package/dist/run.d.ts +18 -0
- package/dist/run.js +142 -0
- package/dist/skill.d.ts +18 -0
- package/dist/skill.js +70 -0
- package/dist/store.d.ts +30 -0
- package/dist/store.js +368 -0
- package/dist/types.d.ts +160 -0
- package/dist/types.js +1 -0
- package/dist/verify.d.ts +9 -0
- package/dist/verify.js +42 -0
- package/package.json +52 -0
- package/skills/rpa/SKILL.md +106 -0
package/dist/git.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { GateError } from "./errors.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export async function snapshot(cwd) {
|
|
6
|
+
let inside = "";
|
|
7
|
+
try {
|
|
8
|
+
inside = (await git(cwd, ["rev-parse", "--is-inside-work-tree"])).trim();
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new GateError(`not a git worktree: ${cwd}`);
|
|
12
|
+
}
|
|
13
|
+
if (inside !== "true") {
|
|
14
|
+
throw new GateError(`not a git worktree: ${cwd}`);
|
|
15
|
+
}
|
|
16
|
+
let headSha;
|
|
17
|
+
try {
|
|
18
|
+
headSha = (await git(cwd, ["rev-parse", "HEAD"])).trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
headSha = undefined;
|
|
22
|
+
}
|
|
23
|
+
const porcelain = (await git(cwd, ["status", "--porcelain"])).trim();
|
|
24
|
+
return { headSha, dirty: porcelain.length > 0 };
|
|
25
|
+
}
|
|
26
|
+
export async function currentSha(cwd) {
|
|
27
|
+
return (await git(cwd, ["rev-parse", "HEAD"])).trim();
|
|
28
|
+
}
|
|
29
|
+
export async function fetchSha(into, from, sha) {
|
|
30
|
+
try {
|
|
31
|
+
await git(into, ["fetch", "--no-tags", from, sha]);
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return { ok: false, error: gitError(err) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function mergeCommit(into, sha, message) {
|
|
39
|
+
try {
|
|
40
|
+
await git(into, ["merge", "--no-edit", "-m", message, sha]);
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const text = gitError(err);
|
|
45
|
+
const conflict = /CONFLICT|Automatic merge failed|Merge conflict|fix conflicts/i.test(text);
|
|
46
|
+
await git(into, ["merge", "--abort"]).catch(() => undefined);
|
|
47
|
+
return { ok: false, conflict, error: text };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function git(cwd, args) {
|
|
51
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
54
|
+
});
|
|
55
|
+
return stdout;
|
|
56
|
+
}
|
|
57
|
+
function gitError(err) {
|
|
58
|
+
if (err && typeof err === "object") {
|
|
59
|
+
const rec = err;
|
|
60
|
+
const combined = [rec.stderr, rec.stdout].filter(Boolean).join("\n").trim();
|
|
61
|
+
return combined || (rec.message || String(err)).trim();
|
|
62
|
+
}
|
|
63
|
+
return String(err);
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ErrorClass, JournalEvent, RunState } from "./types.js";
|
|
2
|
+
export type HistoryRow = {
|
|
3
|
+
runId: string;
|
|
4
|
+
status: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
tasks: number;
|
|
7
|
+
attempts: number;
|
|
8
|
+
retries: number;
|
|
9
|
+
errorClass?: ErrorClass;
|
|
10
|
+
};
|
|
11
|
+
export type HistoryStats = {
|
|
12
|
+
runs: number;
|
|
13
|
+
success: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
attempts: number;
|
|
16
|
+
retries: number;
|
|
17
|
+
byErrorClass: Record<string, number>;
|
|
18
|
+
byStatus: Record<string, number>;
|
|
19
|
+
};
|
|
20
|
+
export declare function listRunIds(storeDir: string): Promise<string[]>;
|
|
21
|
+
export declare function loadHistory(storeDir: string): Promise<{
|
|
22
|
+
rows: HistoryRow[];
|
|
23
|
+
events: JournalEvent[];
|
|
24
|
+
}>;
|
|
25
|
+
export declare function summarizeHistory(rows: HistoryRow[]): HistoryStats;
|
|
26
|
+
export declare function formatHistory(rows: HistoryRow[], stats?: boolean, byType?: boolean): string;
|
|
27
|
+
export declare function formatReport(run: RunState): string;
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readEvents } from "./journal.js";
|
|
4
|
+
import { loadRun } from "./store.js";
|
|
5
|
+
export async function listRunIds(storeDir) {
|
|
6
|
+
try {
|
|
7
|
+
const dir = path.join(storeDir, "runs");
|
|
8
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
9
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function loadHistory(storeDir) {
|
|
16
|
+
const ids = await listRunIds(storeDir);
|
|
17
|
+
const rows = [];
|
|
18
|
+
const events = [];
|
|
19
|
+
for (const id of ids) {
|
|
20
|
+
let run;
|
|
21
|
+
try {
|
|
22
|
+
run = await loadRun(storeDir, id);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const runEvents = await readEvents(storeDir, id);
|
|
28
|
+
events.push(...runEvents);
|
|
29
|
+
const attempts = Object.values(run.tasks).reduce((sum, task) => sum + task.attempts.length, 0);
|
|
30
|
+
const retries = Math.max(0, attempts - Object.values(run.tasks).length);
|
|
31
|
+
const errorClass = Object.values(run.tasks)
|
|
32
|
+
.map((task) => task.errorClass)
|
|
33
|
+
.find((value) => value);
|
|
34
|
+
rows.push({
|
|
35
|
+
runId: run.id,
|
|
36
|
+
status: run.status,
|
|
37
|
+
createdAt: run.createdAt,
|
|
38
|
+
tasks: Object.keys(run.tasks).length,
|
|
39
|
+
attempts,
|
|
40
|
+
retries,
|
|
41
|
+
errorClass,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { rows, events };
|
|
45
|
+
}
|
|
46
|
+
export function summarizeHistory(rows) {
|
|
47
|
+
const byErrorClass = {};
|
|
48
|
+
const byStatus = {};
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
byStatus[row.status] = (byStatus[row.status] ?? 0) + 1;
|
|
51
|
+
if (row.errorClass)
|
|
52
|
+
byErrorClass[row.errorClass] = (byErrorClass[row.errorClass] ?? 0) + 1;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
runs: rows.length,
|
|
56
|
+
success: rows.filter((row) => row.status === "success").length,
|
|
57
|
+
failed: rows.filter((row) => row.status === "failed").length,
|
|
58
|
+
attempts: rows.reduce((sum, row) => sum + row.attempts, 0),
|
|
59
|
+
retries: rows.reduce((sum, row) => sum + row.retries, 0),
|
|
60
|
+
byErrorClass,
|
|
61
|
+
byStatus,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function formatHistory(rows, stats = false, byType = false) {
|
|
65
|
+
if (rows.length === 0)
|
|
66
|
+
return "no runs";
|
|
67
|
+
if (stats || byType) {
|
|
68
|
+
const summary = summarizeHistory(rows);
|
|
69
|
+
const lines = [
|
|
70
|
+
`runs ${summary.runs} success ${summary.success} failed ${summary.failed}`,
|
|
71
|
+
`attempts ${summary.attempts} retries ${summary.retries}`,
|
|
72
|
+
];
|
|
73
|
+
const classes = Object.entries(summary.byErrorClass);
|
|
74
|
+
if (classes.length) {
|
|
75
|
+
lines.push("by error class:");
|
|
76
|
+
for (const [name, count] of classes)
|
|
77
|
+
lines.push(` ${name} ${count}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push("by status:");
|
|
80
|
+
for (const [name, count] of Object.entries(summary.byStatus)) {
|
|
81
|
+
lines.push(` ${name} ${count}`);
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
return rows
|
|
86
|
+
.map((row) => `${row.runId} ${row.status} tasks=${row.tasks} attempts=${row.attempts} retries=${row.retries}${row.errorClass ? ` ${row.errorClass}` : ""}`)
|
|
87
|
+
.join("\n");
|
|
88
|
+
}
|
|
89
|
+
export function formatReport(run) {
|
|
90
|
+
const tasks = Object.values(run.tasks);
|
|
91
|
+
const accepted = tasks.filter((task) => task.status === "accepted" || task.status === "integrated").length;
|
|
92
|
+
const blocked = tasks.filter((task) => task.status === "blocked").length;
|
|
93
|
+
const lines = [
|
|
94
|
+
`run ${run.id} ${run.status}`,
|
|
95
|
+
`repo ${run.repo}`,
|
|
96
|
+
`tasks ${tasks.length} accepted/integrated ${accepted} blocked ${blocked}`,
|
|
97
|
+
];
|
|
98
|
+
if (run.objective)
|
|
99
|
+
lines.push(`objective ${run.objective}`);
|
|
100
|
+
if (run.integration)
|
|
101
|
+
lines.push(`integration ${run.integration.status}`);
|
|
102
|
+
for (const task of tasks) {
|
|
103
|
+
lines.push(` ${task.id} ${task.status}${task.errorClass ? ` ${task.errorClass}` : ""}${task.lastError ? ` ${task.lastError}` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { parseConfig, loadConfig, DEFAULT_CONFIG, commandAsCheck } from "./config.js";
|
|
2
|
+
export { GateError } from "./errors.js";
|
|
3
|
+
export { execCommand } from "./exec.js";
|
|
4
|
+
export { formatRun, formatTask, formatCheck, toJson } from "./format.js";
|
|
5
|
+
export { claimTask, verifyTask, verifyAll, retryTask, deriveRunStatus, refreshRunStatus, isMergeFailure, } from "./gate.js";
|
|
6
|
+
export { integrateAccepted } from "./integrate.js";
|
|
7
|
+
export { startOrcaRetry, startOrcaWorker, startOrcaRepair, worktreeSelector, } from "./orca.js";
|
|
8
|
+
export { createRun, loadRun, saveRun, persist, resolveStoreDir } from "./store.js";
|
|
9
|
+
export { runChecks, verifyPassed, requiredFailed } from "./verify.js";
|
|
10
|
+
export { classifyFailure, shouldBlockWithoutRetry } from "./classify.js";
|
|
11
|
+
export { decomposeObjective, applyPlan, loadPlanFile } from "./plan.js";
|
|
12
|
+
export { prepareRun } from "./run.js";
|
|
13
|
+
export { resumeRun } from "./resume.js";
|
|
14
|
+
export { cleanRun } from "./clean.js";
|
|
15
|
+
export { startIntegrationRepair, createRepairTask } from "./repair.js";
|
|
16
|
+
export { loadHistory, formatHistory, formatReport } from "./history.js";
|
|
17
|
+
export { appendEvent, readEvents } from "./journal.js";
|
|
18
|
+
export { readSkill, installSkill } from "./skill.js";
|
|
19
|
+
export type { Attempt, CheckResult, ExecFn, IntegrationState, RpaConfig, RunState, TaskState, VerifyCheck, WorkerOutcome, ErrorClass, Plan, JournalEvent, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { parseConfig, loadConfig, DEFAULT_CONFIG, commandAsCheck } from "./config.js";
|
|
2
|
+
export { GateError } from "./errors.js";
|
|
3
|
+
export { execCommand } from "./exec.js";
|
|
4
|
+
export { formatRun, formatTask, formatCheck, toJson } from "./format.js";
|
|
5
|
+
export { claimTask, verifyTask, verifyAll, retryTask, deriveRunStatus, refreshRunStatus, isMergeFailure, } from "./gate.js";
|
|
6
|
+
export { integrateAccepted } from "./integrate.js";
|
|
7
|
+
export { startOrcaRetry, startOrcaWorker, startOrcaRepair, worktreeSelector, } from "./orca.js";
|
|
8
|
+
export { createRun, loadRun, saveRun, persist, resolveStoreDir } from "./store.js";
|
|
9
|
+
export { runChecks, verifyPassed, requiredFailed } from "./verify.js";
|
|
10
|
+
export { classifyFailure, shouldBlockWithoutRetry } from "./classify.js";
|
|
11
|
+
export { decomposeObjective, applyPlan, loadPlanFile } from "./plan.js";
|
|
12
|
+
export { prepareRun } from "./run.js";
|
|
13
|
+
export { resumeRun } from "./resume.js";
|
|
14
|
+
export { cleanRun } from "./clean.js";
|
|
15
|
+
export { startIntegrationRepair, createRepairTask } from "./repair.js";
|
|
16
|
+
export { loadHistory, formatHistory, formatReport } from "./history.js";
|
|
17
|
+
export { appendEvent, readEvents } from "./journal.js";
|
|
18
|
+
export { readSkill, installSkill } from "./skill.js";
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { GateError } from "./errors.js";
|
|
3
|
+
import { isMergeFailure, refreshRunStatus, requireTask } from "./gate.js";
|
|
4
|
+
import { fetchSha, mergeCommit, snapshot } from "./git.js";
|
|
5
|
+
import { runChecks, verifyPassed } from "./verify.js";
|
|
6
|
+
export async function integrateAccepted(run, opts) {
|
|
7
|
+
const into = path.resolve(opts.cwd ?? process.cwd(), opts.into);
|
|
8
|
+
const accepted = Object.values(run.tasks).filter((task) => task.status === "accepted");
|
|
9
|
+
if (accepted.length === 0) {
|
|
10
|
+
if (run.integration?.status === "failed" && run.integration.headSha) {
|
|
11
|
+
if (run.integration.into !== into) {
|
|
12
|
+
throw new GateError(`integration verification rerun target changed: expected ${run.integration.into}, got ${into}`);
|
|
13
|
+
}
|
|
14
|
+
const target = await requireCleanTarget(into);
|
|
15
|
+
if (target.headSha !== run.integration.headSha) {
|
|
16
|
+
throw new GateError(`integration verification rerun HEAD changed: expected ${run.integration.headSha}, got ${target.headSha ?? "no HEAD"}`);
|
|
17
|
+
}
|
|
18
|
+
return verifyIntegration(run, into, opts.exec);
|
|
19
|
+
}
|
|
20
|
+
throw new GateError("no accepted tasks to integrate");
|
|
21
|
+
}
|
|
22
|
+
const dirty = accepted.filter((task) => task.dirty);
|
|
23
|
+
if (dirty.length > 0) {
|
|
24
|
+
throw new GateError(`accepted task(s) have uncommitted changes; commit before integrate: ${dirty
|
|
25
|
+
.map((task) => task.id)
|
|
26
|
+
.join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
const missingSha = accepted.filter((task) => !task.headSha);
|
|
29
|
+
if (missingSha.length > 0) {
|
|
30
|
+
throw new GateError(`accepted task(s) have no git HEAD to merge: ${missingSha.map((task) => task.id).join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
const initial = await requireCleanTarget(into);
|
|
33
|
+
const baseSha = initial.headSha;
|
|
34
|
+
run.integration = {
|
|
35
|
+
into,
|
|
36
|
+
status: "merging",
|
|
37
|
+
baseSha,
|
|
38
|
+
mergedTaskIds: run.integration?.mergedTaskIds ?? [],
|
|
39
|
+
checks: [],
|
|
40
|
+
};
|
|
41
|
+
for (const task of accepted) {
|
|
42
|
+
try {
|
|
43
|
+
await requireCleanTarget(into);
|
|
44
|
+
await mergeTask(run, task, into);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
run.integration.status = "failed";
|
|
48
|
+
run.integration.error = `integration execution failed for ${task.id}: ${errorMessage(err)}`;
|
|
49
|
+
refreshRunStatus(run);
|
|
50
|
+
return run;
|
|
51
|
+
}
|
|
52
|
+
if (run.integration.status === "conflict") {
|
|
53
|
+
refreshRunStatus(run);
|
|
54
|
+
return run;
|
|
55
|
+
}
|
|
56
|
+
if (run.integration.status === "failed") {
|
|
57
|
+
refreshRunStatus(run);
|
|
58
|
+
return run;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const merged = Object.values(run.tasks).filter((task) => task.status === "integrated");
|
|
62
|
+
const mergeBlocked = Object.values(run.tasks).filter((task) => task.status === "blocked" && isMergeFailure(task));
|
|
63
|
+
if (mergeBlocked.some((task) => (task.lastError ?? "").startsWith("merge conflict"))) {
|
|
64
|
+
run.integration.status = "conflict";
|
|
65
|
+
run.integration.error ??= mergeBlocked[0]?.lastError ?? "merge conflict";
|
|
66
|
+
refreshRunStatus(run);
|
|
67
|
+
return run;
|
|
68
|
+
}
|
|
69
|
+
if (mergeBlocked.length > 0 || merged.length === 0) {
|
|
70
|
+
run.integration.status = "failed";
|
|
71
|
+
run.integration.error ??= mergeBlocked[0]?.lastError ?? "no accepted tasks could be merged";
|
|
72
|
+
refreshRunStatus(run);
|
|
73
|
+
return run;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const mergedTarget = await snapshot(into);
|
|
77
|
+
run.integration.headSha = mergedTarget.headSha;
|
|
78
|
+
if (mergedTarget.dirty) {
|
|
79
|
+
throw new GateError(`integration worktree has uncommitted changes: ${into}`);
|
|
80
|
+
}
|
|
81
|
+
if (!mergedTarget.headSha) {
|
|
82
|
+
throw new GateError(`integration worktree has no HEAD after merge: ${into}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
run.integration.status = "failed";
|
|
87
|
+
run.integration.error = `could not record clean integration HEAD: ${errorMessage(err)}`;
|
|
88
|
+
refreshRunStatus(run);
|
|
89
|
+
return run;
|
|
90
|
+
}
|
|
91
|
+
return verifyIntegration(run, into, opts.exec);
|
|
92
|
+
}
|
|
93
|
+
async function verifyIntegration(run, into, exec) {
|
|
94
|
+
const integration = run.integration;
|
|
95
|
+
if (!integration?.headSha) {
|
|
96
|
+
throw new GateError("integration has no recorded HEAD to verify");
|
|
97
|
+
}
|
|
98
|
+
const expectedHead = integration.headSha;
|
|
99
|
+
const checks = run.config.verify.integration;
|
|
100
|
+
integration.status = "verifying";
|
|
101
|
+
integration.checks = [];
|
|
102
|
+
try {
|
|
103
|
+
const results = await runChecks(checks, into, exec, {
|
|
104
|
+
config: run.config,
|
|
105
|
+
extra: { cwd: into, base: integration.baseSha ?? "", head: expectedHead },
|
|
106
|
+
});
|
|
107
|
+
integration.checks = results;
|
|
108
|
+
const after = await snapshot(into);
|
|
109
|
+
if (after.dirty || after.headSha !== expectedHead) {
|
|
110
|
+
integration.status = "failed";
|
|
111
|
+
integration.error = after.dirty
|
|
112
|
+
? `integration target became dirty during verification: ${into}`
|
|
113
|
+
: `integration HEAD changed during verification: expected ${expectedHead}, got ${after.headSha ?? "no HEAD"}`;
|
|
114
|
+
}
|
|
115
|
+
else if (verifyPassed(results)) {
|
|
116
|
+
integration.status = "passed";
|
|
117
|
+
integration.error = undefined;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const failed = results.find((check) => check.required && !check.passed);
|
|
121
|
+
integration.status = "failed";
|
|
122
|
+
integration.error = failed
|
|
123
|
+
? failed.timedOut
|
|
124
|
+
? `${failed.name} timed out`
|
|
125
|
+
: `${failed.name} failed (exit ${failed.exitCode})`
|
|
126
|
+
: "integration verify failed";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
integration.status = "failed";
|
|
131
|
+
integration.error = `integration verification execution failed: ${errorMessage(err)}`;
|
|
132
|
+
}
|
|
133
|
+
refreshRunStatus(run);
|
|
134
|
+
return run;
|
|
135
|
+
}
|
|
136
|
+
async function mergeTask(run, task, into) {
|
|
137
|
+
requireTask(run, task.id);
|
|
138
|
+
const fetched = await fetchSha(into, task.worktree, task.headSha);
|
|
139
|
+
if (!fetched.ok) {
|
|
140
|
+
task.status = "blocked";
|
|
141
|
+
task.errorClass = "unknown";
|
|
142
|
+
task.lastError = `merge failed: fetch failed for ${task.id} from ${task.worktree}: ${fetched.error}`;
|
|
143
|
+
if (run.integration) {
|
|
144
|
+
run.integration.status = "failed";
|
|
145
|
+
run.integration.error = task.lastError;
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await requireCleanTarget(into);
|
|
150
|
+
const result = await mergeCommit(into, task.headSha, `rpa: integrate ${task.id}`);
|
|
151
|
+
if (!result.ok) {
|
|
152
|
+
task.status = "blocked";
|
|
153
|
+
task.errorClass = result.conflict ? "merge_conflict" : "unknown";
|
|
154
|
+
task.lastError = result.conflict
|
|
155
|
+
? `merge conflict into ${into}`
|
|
156
|
+
: `merge failed: ${result.error}`;
|
|
157
|
+
if (run.integration) {
|
|
158
|
+
run.integration.error = task.lastError;
|
|
159
|
+
if (result.conflict)
|
|
160
|
+
run.integration.status = "conflict";
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
task.status = "integrated";
|
|
165
|
+
task.lastError = undefined;
|
|
166
|
+
if (run.integration && !run.integration.mergedTaskIds.includes(task.id)) {
|
|
167
|
+
run.integration.mergedTaskIds.push(task.id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function requireCleanTarget(into) {
|
|
171
|
+
let target;
|
|
172
|
+
try {
|
|
173
|
+
target = await snapshot(into);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
throw new GateError(`integration worktree is not a git repo: ${into} (${errorMessage(err)})`);
|
|
177
|
+
}
|
|
178
|
+
if (target.dirty) {
|
|
179
|
+
throw new GateError(`integration worktree has uncommitted changes: ${into}`);
|
|
180
|
+
}
|
|
181
|
+
return target;
|
|
182
|
+
}
|
|
183
|
+
function errorMessage(err) {
|
|
184
|
+
return err instanceof Error ? err.message : String(err);
|
|
185
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EventType, JournalEvent } from "./types.js";
|
|
2
|
+
export declare function eventsPath(storeDir: string, runId: string): string;
|
|
3
|
+
export declare function appendEvent(storeDir: string, input: {
|
|
4
|
+
runId: string;
|
|
5
|
+
type: EventType;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
attemptId?: string;
|
|
8
|
+
payload?: Record<string, unknown>;
|
|
9
|
+
}): Promise<JournalEvent>;
|
|
10
|
+
export declare function readEvents(storeDir: string, runId: string): Promise<JournalEvent[]>;
|
package/dist/journal.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function eventsPath(storeDir, runId) {
|
|
4
|
+
return path.join(storeDir, "runs", runId, "events.jsonl");
|
|
5
|
+
}
|
|
6
|
+
export async function appendEvent(storeDir, input) {
|
|
7
|
+
const existing = await readEvents(storeDir, input.runId);
|
|
8
|
+
const event = {
|
|
9
|
+
id: existing.length + 1,
|
|
10
|
+
runId: input.runId,
|
|
11
|
+
taskId: input.taskId,
|
|
12
|
+
attemptId: input.attemptId,
|
|
13
|
+
type: input.type,
|
|
14
|
+
payload: input.payload ?? {},
|
|
15
|
+
createdAt: new Date().toISOString(),
|
|
16
|
+
};
|
|
17
|
+
const dir = path.dirname(eventsPath(storeDir, input.runId));
|
|
18
|
+
await mkdir(dir, { recursive: true });
|
|
19
|
+
await appendFile(eventsPath(storeDir, input.runId), `${JSON.stringify(event)}\n`, "utf8");
|
|
20
|
+
return event;
|
|
21
|
+
}
|
|
22
|
+
export async function readEvents(storeDir, runId) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = await readFile(eventsPath(storeDir, runId), "utf8");
|
|
25
|
+
return raw
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.map((line) => JSON.parse(line));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/orca.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TaskState } from "./types.js";
|
|
2
|
+
export type OrcaExec = (args: string[]) => Promise<string>;
|
|
3
|
+
export type OrcaOpts = {
|
|
4
|
+
orcaBin?: string;
|
|
5
|
+
exec?: OrcaExec;
|
|
6
|
+
};
|
|
7
|
+
export declare function worktreeSelector(worktree: string): string;
|
|
8
|
+
export declare function startOrcaRetry(task: TaskState, opts?: OrcaOpts): Promise<string>;
|
|
9
|
+
export declare function startOrcaWorker(task: TaskState, opts?: OrcaOpts): Promise<string>;
|
|
10
|
+
export declare function createOrcaTask(task: TaskState, opts?: OrcaOpts): Promise<string>;
|
|
11
|
+
export declare function startOrcaRepair(task: TaskState, fromIds: string[], opts?: OrcaOpts): Promise<string>;
|
|
12
|
+
export declare function createOrcaRun(objective: string, opts?: OrcaOpts): Promise<string>;
|
|
13
|
+
export declare function listOrcaWorkers(opts?: OrcaOpts): Promise<string>;
|
|
14
|
+
export declare function showOrcaWorker(dispatchId: string, opts?: OrcaOpts): Promise<string>;
|
|
15
|
+
export declare function removeOrcaWorktree(selector: string, opts?: OrcaOpts): Promise<string>;
|
|
16
|
+
export declare function orca(args: string[], opts?: OrcaOpts): Promise<string>;
|
package/dist/orca.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { GateError } from "./errors.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export function worktreeSelector(worktree) {
|
|
6
|
+
if (worktree === "current" ||
|
|
7
|
+
worktree === "active" ||
|
|
8
|
+
worktree.startsWith("id:") ||
|
|
9
|
+
worktree.startsWith("name:") ||
|
|
10
|
+
worktree.startsWith("branch:") ||
|
|
11
|
+
worktree.startsWith("path:") ||
|
|
12
|
+
worktree.startsWith("issue:")) {
|
|
13
|
+
return worktree;
|
|
14
|
+
}
|
|
15
|
+
return `path:${worktree}`;
|
|
16
|
+
}
|
|
17
|
+
export async function startOrcaRetry(task, opts = {}) {
|
|
18
|
+
if (!task.dispatchId) {
|
|
19
|
+
throw new GateError(`task ${task.id} has no dispatch id; pass --dispatch on claim`);
|
|
20
|
+
}
|
|
21
|
+
if (!task.orcaTaskId) {
|
|
22
|
+
throw new GateError(`task ${task.id} has no Orca task id; create the Orca task before retrying`);
|
|
23
|
+
}
|
|
24
|
+
if (!task.agent) {
|
|
25
|
+
throw new GateError(`task ${task.id} has no agent; pass --agent on claim`);
|
|
26
|
+
}
|
|
27
|
+
const predecessorDispatchId = task.dispatchId;
|
|
28
|
+
const out = await orca([
|
|
29
|
+
"orchestration",
|
|
30
|
+
"worker-start",
|
|
31
|
+
"--task",
|
|
32
|
+
task.orcaTaskId,
|
|
33
|
+
"--retry-of",
|
|
34
|
+
predecessorDispatchId,
|
|
35
|
+
"--worktree",
|
|
36
|
+
worktreeSelector(task.worktree),
|
|
37
|
+
"--agent",
|
|
38
|
+
task.agent,
|
|
39
|
+
"--json",
|
|
40
|
+
], opts);
|
|
41
|
+
task.dispatchId = requireDispatchId(out, task.id);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
export async function startOrcaWorker(task, opts = {}) {
|
|
45
|
+
if (!task.agent) {
|
|
46
|
+
throw new GateError(`task ${task.id} has no agent; pass --agent to dispatch`);
|
|
47
|
+
}
|
|
48
|
+
if (!task.orcaTaskId) {
|
|
49
|
+
throw new GateError(`task ${task.id} has no Orca task id; create the Orca task before starting a worker`);
|
|
50
|
+
}
|
|
51
|
+
const out = await orca([
|
|
52
|
+
"orchestration",
|
|
53
|
+
"worker-start",
|
|
54
|
+
"--task",
|
|
55
|
+
task.orcaTaskId,
|
|
56
|
+
"--worktree",
|
|
57
|
+
worktreeSelector(task.worktree),
|
|
58
|
+
"--agent",
|
|
59
|
+
task.agent,
|
|
60
|
+
"--json",
|
|
61
|
+
], opts);
|
|
62
|
+
task.dispatchId = requireDispatchId(out, task.id);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export async function createOrcaTask(task, opts = {}) {
|
|
66
|
+
const spec = task.deliverable ?? task.title ?? task.id;
|
|
67
|
+
return createOrcaTaskWithSpec(task, spec, opts);
|
|
68
|
+
}
|
|
69
|
+
async function createOrcaTaskWithSpec(task, spec, opts) {
|
|
70
|
+
const out = await orca([
|
|
71
|
+
"orchestration",
|
|
72
|
+
"task-create",
|
|
73
|
+
"--spec",
|
|
74
|
+
spec,
|
|
75
|
+
"--task-title",
|
|
76
|
+
task.title ?? task.id,
|
|
77
|
+
"--json",
|
|
78
|
+
], opts);
|
|
79
|
+
const id = extractTaskId(out);
|
|
80
|
+
if (!id) {
|
|
81
|
+
throw new GateError(`orca task-create for ${task.id} returned no task id`);
|
|
82
|
+
}
|
|
83
|
+
task.orcaTaskId = id;
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
export async function startOrcaRepair(task, fromIds, opts = {}) {
|
|
87
|
+
const spec = [
|
|
88
|
+
task.deliverable ?? "integration repair",
|
|
89
|
+
`escalated_from: ${fromIds.join(", ")}`,
|
|
90
|
+
task.lastError ?? "",
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join("\n");
|
|
94
|
+
await createOrcaTaskWithSpec(task, spec, opts);
|
|
95
|
+
if (!task.agent)
|
|
96
|
+
task.agent = "codex";
|
|
97
|
+
return startOrcaWorker(task, opts);
|
|
98
|
+
}
|
|
99
|
+
export async function createOrcaRun(objective, opts = {}) {
|
|
100
|
+
return orca(["orchestration", "run-create", "--objective", objective, "--json"], opts);
|
|
101
|
+
}
|
|
102
|
+
export async function listOrcaWorkers(opts = {}) {
|
|
103
|
+
return orca(["orchestration", "worker-list", "--json"], opts);
|
|
104
|
+
}
|
|
105
|
+
export async function showOrcaWorker(dispatchId, opts = {}) {
|
|
106
|
+
return orca(["orchestration", "worker-show", "--dispatch", dispatchId, "--json"], opts);
|
|
107
|
+
}
|
|
108
|
+
export async function removeOrcaWorktree(selector, opts = {}) {
|
|
109
|
+
return orca(["worktree", "rm", "--worktree", worktreeSelector(selector), "--json"], opts);
|
|
110
|
+
}
|
|
111
|
+
export async function orca(args, opts = {}) {
|
|
112
|
+
if (opts.exec)
|
|
113
|
+
return opts.exec(args);
|
|
114
|
+
const bin = opts.orcaBin ?? "orca";
|
|
115
|
+
try {
|
|
116
|
+
const { stdout } = await execFileAsync(bin, args, {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
119
|
+
});
|
|
120
|
+
return stdout.trim();
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const rec = err;
|
|
124
|
+
throw new GateError(`orca failed: ${(rec.stderr || rec.message || String(err)).trim()}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function requireDispatchId(stdout, taskId) {
|
|
128
|
+
const id = extractExplicitId(stdout, ["dispatchId", "dispatch_id"]);
|
|
129
|
+
if (!id) {
|
|
130
|
+
throw new GateError(`orca worker-start for ${taskId} returned no dispatch id`);
|
|
131
|
+
}
|
|
132
|
+
return id;
|
|
133
|
+
}
|
|
134
|
+
function extractExplicitId(stdout, keys) {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(stdout);
|
|
137
|
+
const result = parsed.result && typeof parsed.result === "object"
|
|
138
|
+
? parsed.result
|
|
139
|
+
: parsed;
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
const value = result[key] ?? parsed[key];
|
|
142
|
+
if (typeof value === "string" && value.trim())
|
|
143
|
+
return value.trim();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
function extractTaskId(stdout) {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(stdout);
|
|
154
|
+
const result = parsed.result && typeof parsed.result === "object"
|
|
155
|
+
? parsed.result
|
|
156
|
+
: parsed;
|
|
157
|
+
const task = result.task;
|
|
158
|
+
if (task && typeof task === "object") {
|
|
159
|
+
const id = task.id;
|
|
160
|
+
if (typeof id === "string" && id.trim())
|
|
161
|
+
return id.trim();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return extractExplicitId(stdout, ["taskId", "task_id"]);
|
|
168
|
+
}
|
package/dist/plan.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Plan, RunState, TaskState } from "./types.js";
|
|
2
|
+
export declare function loadPlanFile(file: string): Promise<Plan>;
|
|
3
|
+
export declare function parsePlan(raw: unknown): Plan;
|
|
4
|
+
export declare function decomposeObjective(objective: string, planner?: string): Plan;
|
|
5
|
+
export declare function applyPlan(run: RunState, plan: Plan): TaskState[];
|
|
6
|
+
export declare function readyPlanTasks(run: RunState): TaskState[];
|