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/plan.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { GateError } from "./errors.js";
|
|
3
|
+
export async function loadPlanFile(file) {
|
|
4
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
5
|
+
return parsePlan(raw);
|
|
6
|
+
}
|
|
7
|
+
export function parsePlan(raw) {
|
|
8
|
+
if (!raw || typeof raw !== "object")
|
|
9
|
+
throw new GateError("plan must be an object");
|
|
10
|
+
const rec = raw;
|
|
11
|
+
const tasksRaw = rec.tasks ?? (Array.isArray(raw) ? raw : undefined);
|
|
12
|
+
if (tasksRaw && !Array.isArray(tasksRaw))
|
|
13
|
+
throw new GateError("plan.tasks must be an array");
|
|
14
|
+
const tasks = (tasksRaw ?? []).map((item, index) => parsePlanTask(item, index));
|
|
15
|
+
if (tasks.length === 0 && typeof rec.id === "string") {
|
|
16
|
+
return {
|
|
17
|
+
objective: asString(rec.title) ?? asString(rec.objective),
|
|
18
|
+
tasks: [parsePlanTask(rec, 0)],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
objective: asString(rec.objective),
|
|
23
|
+
planner: asString(rec.planner),
|
|
24
|
+
tasks,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parsePlanTask(item, index) {
|
|
28
|
+
if (!item || typeof item !== "object")
|
|
29
|
+
throw new GateError(`plan task ${index} must be an object`);
|
|
30
|
+
const rec = item;
|
|
31
|
+
const id = asString(rec.id) ?? slug(asString(rec.title) ?? `task-${index + 1}`, index);
|
|
32
|
+
const title = asString(rec.title) ?? id;
|
|
33
|
+
const scopeRec = rec.scope && typeof rec.scope === "object" ? rec.scope : undefined;
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
title,
|
|
37
|
+
agent: asString(rec.agent),
|
|
38
|
+
mode: asString(rec.mode),
|
|
39
|
+
deliverable: asString(rec.deliverable),
|
|
40
|
+
deps: Array.isArray(rec.deps) ? rec.deps.filter((d) => typeof d === "string") : undefined,
|
|
41
|
+
worktree: asString(rec.worktree),
|
|
42
|
+
scope: scopeRec
|
|
43
|
+
? {
|
|
44
|
+
paths: Array.isArray(scopeRec.paths)
|
|
45
|
+
? scopeRec.paths.filter((p) => typeof p === "string")
|
|
46
|
+
: undefined,
|
|
47
|
+
}
|
|
48
|
+
: undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function decomposeObjective(objective, planner) {
|
|
52
|
+
const lines = objective
|
|
53
|
+
.split(/\r?\n/)
|
|
54
|
+
.map((line) => line.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
const bullet = lines.filter((line) => /^(?:[-*]|\d+[.)])\s+/.test(line));
|
|
57
|
+
const titles = (bullet.length > 1 ? bullet : [objective.trim()]).map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s+/, "").trim());
|
|
58
|
+
const tasks = titles.filter(Boolean).map((title, index) => ({
|
|
59
|
+
id: slug(title, index),
|
|
60
|
+
title,
|
|
61
|
+
deliverable: title,
|
|
62
|
+
}));
|
|
63
|
+
return { objective, planner, tasks: tasks.length ? tasks : [{ id: "task-1", title: objective, deliverable: objective }] };
|
|
64
|
+
}
|
|
65
|
+
export function applyPlan(run, plan) {
|
|
66
|
+
run.plan = plan;
|
|
67
|
+
run.objective = plan.objective ?? run.objective;
|
|
68
|
+
run.planner = plan.planner ?? run.planner;
|
|
69
|
+
if (run.status !== "cleaned" && run.status !== "success" && run.status !== "failed") {
|
|
70
|
+
run.status = "planned";
|
|
71
|
+
}
|
|
72
|
+
const created = [];
|
|
73
|
+
for (const item of plan.tasks) {
|
|
74
|
+
const existing = run.tasks[item.id];
|
|
75
|
+
if (existing && (existing.status === "accepted" || existing.status === "integrated"))
|
|
76
|
+
continue;
|
|
77
|
+
const task = {
|
|
78
|
+
id: item.id,
|
|
79
|
+
status: "planned",
|
|
80
|
+
worktree: item.worktree ?? run.repo,
|
|
81
|
+
agent: item.agent,
|
|
82
|
+
workerOutcome: "pending",
|
|
83
|
+
attempts: existing?.attempts ?? [],
|
|
84
|
+
title: item.title,
|
|
85
|
+
deliverable: item.deliverable,
|
|
86
|
+
deps: item.deps,
|
|
87
|
+
};
|
|
88
|
+
run.tasks[item.id] = task;
|
|
89
|
+
created.push(task);
|
|
90
|
+
}
|
|
91
|
+
return created;
|
|
92
|
+
}
|
|
93
|
+
export function readyPlanTasks(run) {
|
|
94
|
+
return Object.values(run.tasks).filter((task) => {
|
|
95
|
+
if (task.status !== "planned")
|
|
96
|
+
return false;
|
|
97
|
+
const deps = task.deps ?? [];
|
|
98
|
+
return deps.every((dep) => {
|
|
99
|
+
const other = run.tasks[dep];
|
|
100
|
+
return other?.status === "accepted" || other?.status === "integrated";
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function asString(value) {
|
|
105
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
106
|
+
}
|
|
107
|
+
function slug(title, index) {
|
|
108
|
+
const base = title
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
111
|
+
.replace(/^-|-$/g, "")
|
|
112
|
+
.slice(0, 40);
|
|
113
|
+
return base || `task-${index + 1}`;
|
|
114
|
+
}
|
package/dist/repair.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type OrcaExec } from "./orca.js";
|
|
2
|
+
import type { RunState, TaskState } from "./types.js";
|
|
3
|
+
export declare function createRepairTask(run: RunState, fromIds: string[]): TaskState;
|
|
4
|
+
export declare function startIntegrationRepair(run: RunState, opts?: {
|
|
5
|
+
fromIds?: string[];
|
|
6
|
+
orca?: boolean;
|
|
7
|
+
orcaBin?: string;
|
|
8
|
+
orcaExec?: OrcaExec;
|
|
9
|
+
agent?: string;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
task: TaskState;
|
|
12
|
+
orca?: string;
|
|
13
|
+
}>;
|
package/dist/repair.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { GateError } from "./errors.js";
|
|
2
|
+
import { refreshRunStatus } from "./gate.js";
|
|
3
|
+
import { startOrcaRepair } from "./orca.js";
|
|
4
|
+
import { addResource } from "./resources.js";
|
|
5
|
+
export function createRepairTask(run, fromIds) {
|
|
6
|
+
if (fromIds.length === 0)
|
|
7
|
+
throw new GateError("repair requires at least one blocked task");
|
|
8
|
+
if (new Set(fromIds).size !== fromIds.length) {
|
|
9
|
+
throw new GateError("repair source task ids must be unique");
|
|
10
|
+
}
|
|
11
|
+
for (const taskId of fromIds) {
|
|
12
|
+
const source = run.tasks[taskId];
|
|
13
|
+
if (!source)
|
|
14
|
+
throw new GateError(`repair source task not found: ${taskId}`);
|
|
15
|
+
const failedIntegrationSource = source.status === "integrated" &&
|
|
16
|
+
run.integration?.status === "failed" &&
|
|
17
|
+
run.integration.mergedTaskIds.includes(taskId);
|
|
18
|
+
if (source.status !== "blocked" && !failedIntegrationSource) {
|
|
19
|
+
throw new GateError(`repair source task ${taskId} is ${source.status}; expected blocked`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let n = 1;
|
|
23
|
+
while (run.tasks[`integration_repair_${n}`])
|
|
24
|
+
n += 1;
|
|
25
|
+
const id = `integration_repair_${n}`;
|
|
26
|
+
const evidence = fromIds
|
|
27
|
+
.map((taskId) => {
|
|
28
|
+
const task = run.tasks[taskId];
|
|
29
|
+
return `${taskId}: ${task.lastError ??
|
|
30
|
+
(task.status === "integrated" ? run.integration?.error : undefined) ??
|
|
31
|
+
"blocked"}`;
|
|
32
|
+
})
|
|
33
|
+
.join("\n");
|
|
34
|
+
const task = {
|
|
35
|
+
id,
|
|
36
|
+
status: "planned",
|
|
37
|
+
worktree: run.integration?.into ?? run.repo,
|
|
38
|
+
workerOutcome: "pending",
|
|
39
|
+
attempts: [],
|
|
40
|
+
title: "integration repair",
|
|
41
|
+
deliverable: `resolve integration failure for ${fromIds.join(", ")}`,
|
|
42
|
+
escalatedFrom: fromIds,
|
|
43
|
+
lastError: evidence,
|
|
44
|
+
errorClass: "merge_conflict",
|
|
45
|
+
};
|
|
46
|
+
run.tasks[id] = task;
|
|
47
|
+
if (run.integration)
|
|
48
|
+
run.integration.status = "repairing";
|
|
49
|
+
refreshRunStatus(run);
|
|
50
|
+
return task;
|
|
51
|
+
}
|
|
52
|
+
export async function startIntegrationRepair(run, opts = {}) {
|
|
53
|
+
const fromIds = opts.fromIds ??
|
|
54
|
+
(() => {
|
|
55
|
+
const blocked = Object.values(run.tasks)
|
|
56
|
+
.filter((task) => task.status === "blocked")
|
|
57
|
+
.map((task) => task.id);
|
|
58
|
+
if (blocked.length > 0)
|
|
59
|
+
return blocked;
|
|
60
|
+
if (run.integration?.status === "failed") {
|
|
61
|
+
return run.integration.mergedTaskIds.filter((taskId) => run.tasks[taskId]?.status === "integrated");
|
|
62
|
+
}
|
|
63
|
+
return blocked;
|
|
64
|
+
})();
|
|
65
|
+
const previousIntegration = run.integration
|
|
66
|
+
? { status: run.integration.status, error: run.integration.error }
|
|
67
|
+
: undefined;
|
|
68
|
+
const task = createRepairTask(run, fromIds);
|
|
69
|
+
task.status = "repairing";
|
|
70
|
+
task.agent = opts.agent ?? task.agent;
|
|
71
|
+
if (opts.orca) {
|
|
72
|
+
try {
|
|
73
|
+
const orca = await startOrcaRepair(task, fromIds, {
|
|
74
|
+
orcaBin: opts.orcaBin,
|
|
75
|
+
exec: opts.orcaExec,
|
|
76
|
+
});
|
|
77
|
+
const dispatchId = task.dispatchId;
|
|
78
|
+
if (!dispatchId) {
|
|
79
|
+
throw new GateError(`repair task ${task.id} has no Orca dispatch id`);
|
|
80
|
+
}
|
|
81
|
+
addResource(run, {
|
|
82
|
+
id: `dispatch:${task.id}:${dispatchId}`,
|
|
83
|
+
kind: "dispatch",
|
|
84
|
+
selector: dispatchId,
|
|
85
|
+
taskId: task.id,
|
|
86
|
+
});
|
|
87
|
+
return { task, orca };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
task.status = "blocked";
|
|
91
|
+
task.workerOutcome = "failed";
|
|
92
|
+
task.lastError = `${task.lastError ?? ""}\nOrca repair dispatch failed: ${errorMessage(err)}`.trim();
|
|
93
|
+
task.errorClass = "unknown";
|
|
94
|
+
if (run.integration && previousIntegration) {
|
|
95
|
+
run.integration.status =
|
|
96
|
+
previousIntegration.status === "repairing" ? "failed" : previousIntegration.status;
|
|
97
|
+
run.integration.error =
|
|
98
|
+
previousIntegration.status === "repairing"
|
|
99
|
+
? previousIntegration.error ?? task.lastError
|
|
100
|
+
: previousIntegration.error;
|
|
101
|
+
}
|
|
102
|
+
refreshRunStatus(run);
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { task };
|
|
107
|
+
}
|
|
108
|
+
function errorMessage(err) {
|
|
109
|
+
return err instanceof Error ? err.message : String(err);
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResourceRecord, RunState } from "./types.js";
|
|
2
|
+
export declare function addResource(run: RunState, input: Omit<ResourceRecord, "createdBy" | "createdAt" | "lastHeartbeatAt" | "leaseExpiresAt" | "runId"> & {
|
|
3
|
+
leaseExpiresAt?: string;
|
|
4
|
+
}): ResourceRecord;
|
|
5
|
+
export declare function heartbeat(run: RunState): void;
|
|
6
|
+
export declare function expiredResources(run: RunState, now?: number): ResourceRecord[];
|
|
7
|
+
export declare function markCleaned(run: RunState, id: string): ResourceRecord | undefined;
|
|
8
|
+
export declare function liveResources(run: RunState, kind?: ResourceRecord["kind"]): ResourceRecord[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function addResource(run, input) {
|
|
2
|
+
const now = new Date();
|
|
3
|
+
const ttl = run.config.lease.ttlMs;
|
|
4
|
+
const record = {
|
|
5
|
+
id: input.id,
|
|
6
|
+
kind: input.kind,
|
|
7
|
+
selector: input.selector,
|
|
8
|
+
createdBy: "rpa",
|
|
9
|
+
createdAt: now.toISOString(),
|
|
10
|
+
leaseExpiresAt: input.leaseExpiresAt ?? new Date(now.getTime() + ttl).toISOString(),
|
|
11
|
+
lastHeartbeatAt: now.toISOString(),
|
|
12
|
+
runId: run.id,
|
|
13
|
+
taskId: input.taskId,
|
|
14
|
+
};
|
|
15
|
+
run.resources = run.resources ?? [];
|
|
16
|
+
run.resources.push(record);
|
|
17
|
+
return record;
|
|
18
|
+
}
|
|
19
|
+
export function heartbeat(run) {
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
run.lastHeartbeatAt = now;
|
|
22
|
+
for (const resource of run.resources ?? []) {
|
|
23
|
+
if (resource.cleanedAt)
|
|
24
|
+
continue;
|
|
25
|
+
resource.lastHeartbeatAt = now;
|
|
26
|
+
resource.leaseExpiresAt = new Date(Date.now() + run.config.lease.ttlMs).toISOString();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function expiredResources(run, now = Date.now()) {
|
|
30
|
+
return (run.resources ?? []).filter((resource) => !resource.cleanedAt && Date.parse(resource.leaseExpiresAt) <= now);
|
|
31
|
+
}
|
|
32
|
+
export function markCleaned(run, id) {
|
|
33
|
+
const resource = (run.resources ?? []).find((item) => item.id === id);
|
|
34
|
+
if (!resource)
|
|
35
|
+
return undefined;
|
|
36
|
+
resource.cleanedAt = new Date().toISOString();
|
|
37
|
+
return resource;
|
|
38
|
+
}
|
|
39
|
+
export function liveResources(run, kind) {
|
|
40
|
+
return (run.resources ?? []).filter((resource) => !resource.cleanedAt && resource.createdBy === "rpa" && (kind ? resource.kind === kind : true));
|
|
41
|
+
}
|
package/dist/resume.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type OrcaOpts } from "./orca.js";
|
|
2
|
+
import type { ExecFn, RunState, TaskState } from "./types.js";
|
|
3
|
+
export type ResumeOpts = {
|
|
4
|
+
storeDir: string;
|
|
5
|
+
run: RunState;
|
|
6
|
+
exec: ExecFn;
|
|
7
|
+
orca?: boolean;
|
|
8
|
+
orcaBin?: string;
|
|
9
|
+
orcaExec?: OrcaOpts["exec"];
|
|
10
|
+
};
|
|
11
|
+
export declare function resumeRun(opts: ResumeOpts): Promise<RunState>;
|
|
12
|
+
export declare function reconcileOrca(run: RunState, opts: Omit<ResumeOpts, "exec">): Promise<TaskState[]>;
|
package/dist/resume.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { GateError } from "./errors.js";
|
|
2
|
+
import { refreshRunStatus, verifyAll } from "./gate.js";
|
|
3
|
+
import { listOrcaWorkers } from "./orca.js";
|
|
4
|
+
import { persist } from "./store.js";
|
|
5
|
+
export async function resumeRun(opts) {
|
|
6
|
+
const { run } = opts;
|
|
7
|
+
await persist(opts.storeDir, run, { type: "RUN_RESUMED", payload: {} });
|
|
8
|
+
if (opts.orca) {
|
|
9
|
+
await reconcileOrca(run, opts);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
await failClosedIncompleteDispatches(run, opts.storeDir);
|
|
13
|
+
}
|
|
14
|
+
const verified = await verifyAll(run, { exec: opts.exec });
|
|
15
|
+
refreshRunStatus(run);
|
|
16
|
+
await persist(opts.storeDir, run, {
|
|
17
|
+
type: "HEARTBEAT",
|
|
18
|
+
payload: { verified: verified.map((task) => task.id) },
|
|
19
|
+
});
|
|
20
|
+
return run;
|
|
21
|
+
}
|
|
22
|
+
export async function reconcileOrca(run, opts) {
|
|
23
|
+
const touched = await failClosedIncompleteDispatches(run, opts.storeDir);
|
|
24
|
+
let ids;
|
|
25
|
+
try {
|
|
26
|
+
const workersRaw = await listOrcaWorkers({ orcaBin: opts.orcaBin, exec: opts.orcaExec });
|
|
27
|
+
ids = extractWorkerIds(workersRaw);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
const evidence = message.startsWith("resume reconcile failed:")
|
|
32
|
+
? message
|
|
33
|
+
: `resume reconcile failed: ${message}`;
|
|
34
|
+
for (const task of touched) {
|
|
35
|
+
if (task.status === "dispatched") {
|
|
36
|
+
blockDispatch(task, `${evidence}; worker presence is unknown, inspect Orca state and resolve manually`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
refreshRunStatus(run);
|
|
40
|
+
await persist(opts.storeDir, run, {
|
|
41
|
+
type: "HEARTBEAT",
|
|
42
|
+
payload: { reconcileError: evidence },
|
|
43
|
+
});
|
|
44
|
+
throw new GateError(evidence);
|
|
45
|
+
}
|
|
46
|
+
for (const task of touched) {
|
|
47
|
+
if (task.status === "dispatched") {
|
|
48
|
+
if (task.dispatchId && !ids.has(task.dispatchId)) {
|
|
49
|
+
blockDispatch(task, `orca worker ${task.dispatchId} missing during reconcile; inspect Orca state and resolve manually`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
refreshRunStatus(run);
|
|
54
|
+
await persist(opts.storeDir, run, {
|
|
55
|
+
type: "HEARTBEAT",
|
|
56
|
+
payload: { reconciled: touched.map((task) => task.id) },
|
|
57
|
+
});
|
|
58
|
+
return touched;
|
|
59
|
+
}
|
|
60
|
+
async function failClosedIncompleteDispatches(run, storeDir) {
|
|
61
|
+
const touched = [];
|
|
62
|
+
for (const task of Object.values(run.tasks)) {
|
|
63
|
+
if (task.status !== "dispatched" && task.status !== "claimed" && task.status !== "repairing")
|
|
64
|
+
continue;
|
|
65
|
+
if (task.status === "dispatched") {
|
|
66
|
+
if (task.dispatchPhase === "intent" || task.dispatchPhase === "task-created") {
|
|
67
|
+
const identity = task.dispatchPhase === "task-created"
|
|
68
|
+
? `; Orca task ${task.orcaTaskId ?? "identity missing"} may already exist`
|
|
69
|
+
: "";
|
|
70
|
+
blockDispatch(task, `orca dispatch stopped in ${task.dispatchPhase} phase${identity}; inspect Orca state and resolve manually to avoid duplicate dispatch`);
|
|
71
|
+
}
|
|
72
|
+
else if (!task.dispatchId) {
|
|
73
|
+
blockDispatch(task, `orca worker identity missing for ${task.dispatchPhase ?? "unknown"} dispatch phase; inspect Orca state and resolve manually`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
touched.push(task);
|
|
77
|
+
}
|
|
78
|
+
refreshRunStatus(run);
|
|
79
|
+
await persist(storeDir, run, {
|
|
80
|
+
type: "HEARTBEAT",
|
|
81
|
+
payload: { reconciledLocalDispatches: touched.map((task) => task.id) },
|
|
82
|
+
});
|
|
83
|
+
return touched;
|
|
84
|
+
}
|
|
85
|
+
function blockDispatch(task, evidence) {
|
|
86
|
+
task.status = "blocked";
|
|
87
|
+
task.dispatchPhase = "failed";
|
|
88
|
+
task.lastError = evidence;
|
|
89
|
+
task.errorClass = "environment";
|
|
90
|
+
}
|
|
91
|
+
function extractWorkerIds(raw) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw new GateError(`resume reconcile failed: orca worker-list returned malformed JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
100
|
+
throw new GateError("resume reconcile failed: orca worker-list response must be an object");
|
|
101
|
+
}
|
|
102
|
+
const response = parsed;
|
|
103
|
+
const result = "result" in response ? response.result : response;
|
|
104
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
105
|
+
throw new GateError("resume reconcile failed: orca worker-list result must be an object");
|
|
106
|
+
}
|
|
107
|
+
const workers = result.workers;
|
|
108
|
+
if (!Array.isArray(workers)) {
|
|
109
|
+
throw new GateError("resume reconcile failed: orca worker-list workers must be an array");
|
|
110
|
+
}
|
|
111
|
+
const ids = new Set();
|
|
112
|
+
for (const [index, worker] of workers.entries()) {
|
|
113
|
+
if (!worker || typeof worker !== "object" || Array.isArray(worker)) {
|
|
114
|
+
throw new GateError(`resume reconcile failed: orca worker-list workers[${index}] must be an object`);
|
|
115
|
+
}
|
|
116
|
+
const rec = worker;
|
|
117
|
+
let recognized = false;
|
|
118
|
+
for (const key of ["id", "dispatchId", "dispatch_id"]) {
|
|
119
|
+
if (typeof rec[key] === "string" && rec[key].trim()) {
|
|
120
|
+
ids.add(rec[key].trim());
|
|
121
|
+
recognized = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!recognized) {
|
|
125
|
+
throw new GateError(`resume reconcile failed: orca worker-list workers[${index}] must expose a non-empty id, dispatchId, or dispatch_id`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return ids;
|
|
129
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type OrcaOpts } from "./orca.js";
|
|
2
|
+
import type { ExecFn, RunState } from "./types.js";
|
|
3
|
+
export type RunOpts = {
|
|
4
|
+
storeDir: string;
|
|
5
|
+
run: RunState;
|
|
6
|
+
objective?: string;
|
|
7
|
+
planFile?: string;
|
|
8
|
+
planner?: string;
|
|
9
|
+
planOnly?: boolean;
|
|
10
|
+
dry?: boolean;
|
|
11
|
+
dispatch?: boolean;
|
|
12
|
+
orca?: boolean;
|
|
13
|
+
orcaBin?: string;
|
|
14
|
+
exec?: ExecFn;
|
|
15
|
+
orcaExec?: OrcaOpts["exec"];
|
|
16
|
+
};
|
|
17
|
+
export declare function prepareRun(opts: RunOpts): Promise<RunState>;
|
|
18
|
+
export declare function dispatchReadyTasks(run: RunState, opts: RunOpts): Promise<void>;
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execCommand } from "./exec.js";
|
|
2
|
+
import { GateError } from "./errors.js";
|
|
3
|
+
import { refreshRunStatus } from "./gate.js";
|
|
4
|
+
import { createOrcaTask, startOrcaWorker } from "./orca.js";
|
|
5
|
+
import { applyPlan, decomposeObjective, loadPlanFile, parsePlan, readyPlanTasks } from "./plan.js";
|
|
6
|
+
import { addResource } from "./resources.js";
|
|
7
|
+
import { persist, writeManifest, writePlanFile } from "./store.js";
|
|
8
|
+
export async function prepareRun(opts) {
|
|
9
|
+
const plan = await resolvePlan(opts);
|
|
10
|
+
applyPlan(opts.run, plan);
|
|
11
|
+
opts.run.objective = plan.objective ?? opts.objective ?? opts.run.objective;
|
|
12
|
+
opts.run.planner = plan.planner ?? opts.planner ?? opts.run.planner;
|
|
13
|
+
refreshRunStatus(opts.run);
|
|
14
|
+
await writePlanFile(opts.storeDir, opts.run);
|
|
15
|
+
await writeManifest(opts.storeDir, opts.run);
|
|
16
|
+
await persist(opts.storeDir, opts.run, {
|
|
17
|
+
type: "PLAN_CREATED",
|
|
18
|
+
payload: { tasks: plan.tasks.map((task) => task.id), planner: plan.planner ?? null },
|
|
19
|
+
});
|
|
20
|
+
if (opts.planOnly || opts.dry)
|
|
21
|
+
return opts.run;
|
|
22
|
+
if (opts.dispatch !== false) {
|
|
23
|
+
await dispatchReadyTasks(opts.run, opts);
|
|
24
|
+
}
|
|
25
|
+
return opts.run;
|
|
26
|
+
}
|
|
27
|
+
export async function dispatchReadyTasks(run, opts) {
|
|
28
|
+
for (const task of readyPlanTasks(run)) {
|
|
29
|
+
task.status = "dispatched";
|
|
30
|
+
task.workerOutcome = "pending";
|
|
31
|
+
task.dispatchPhase = "intent";
|
|
32
|
+
if (opts.orca && !task.agent)
|
|
33
|
+
task.agent = "codex";
|
|
34
|
+
await persist(opts.storeDir, run, {
|
|
35
|
+
type: "TASK_DISPATCHED",
|
|
36
|
+
taskId: task.id,
|
|
37
|
+
payload: {
|
|
38
|
+
phase: "intent",
|
|
39
|
+
dispatchId: null,
|
|
40
|
+
agent: task.agent ?? null,
|
|
41
|
+
orca: Boolean(opts.orca),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (opts.orca) {
|
|
45
|
+
const orcaOpts = { orcaBin: opts.orcaBin, exec: opts.orcaExec };
|
|
46
|
+
try {
|
|
47
|
+
await createOrcaTask(task, orcaOpts);
|
|
48
|
+
if (!task.orcaTaskId) {
|
|
49
|
+
throw new GateError(`orca task-create returned no task id for task ${task.id}`);
|
|
50
|
+
}
|
|
51
|
+
task.dispatchPhase = "task-created";
|
|
52
|
+
await persist(opts.storeDir, run, {
|
|
53
|
+
type: "TASK_DISPATCHED",
|
|
54
|
+
taskId: task.id,
|
|
55
|
+
payload: {
|
|
56
|
+
phase: "task-created",
|
|
57
|
+
orcaTaskId: task.orcaTaskId,
|
|
58
|
+
agent: task.agent,
|
|
59
|
+
orca: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
await startOrcaWorker(task, orcaOpts);
|
|
63
|
+
if (!task.dispatchId) {
|
|
64
|
+
throw new GateError(`orca worker-start returned no dispatch id for task ${task.id}`);
|
|
65
|
+
}
|
|
66
|
+
task.dispatchPhase = "worker-started";
|
|
67
|
+
await persist(opts.storeDir, run, {
|
|
68
|
+
type: "WORKER_STARTED",
|
|
69
|
+
taskId: task.id,
|
|
70
|
+
payload: {
|
|
71
|
+
phase: task.dispatchPhase,
|
|
72
|
+
orcaTaskId: task.orcaTaskId,
|
|
73
|
+
dispatchId: task.dispatchId,
|
|
74
|
+
agent: task.agent,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
task.status = "blocked";
|
|
80
|
+
task.dispatchPhase = "failed";
|
|
81
|
+
task.lastError = `orca dispatch failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
82
|
+
refreshRunStatus(run);
|
|
83
|
+
await persist(opts.storeDir, run, {
|
|
84
|
+
type: "TASK_DISPATCHED",
|
|
85
|
+
taskId: task.id,
|
|
86
|
+
payload: {
|
|
87
|
+
phase: "failed",
|
|
88
|
+
orcaTaskId: task.orcaTaskId ?? null,
|
|
89
|
+
dispatchId: task.dispatchId ?? null,
|
|
90
|
+
error: task.lastError,
|
|
91
|
+
orca: true,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
throw new GateError(task.lastError);
|
|
95
|
+
}
|
|
96
|
+
addResource(run, {
|
|
97
|
+
id: `dispatch:${task.id}:${task.dispatchId ?? Date.now()}`,
|
|
98
|
+
kind: "dispatch",
|
|
99
|
+
selector: task.dispatchId ?? task.id,
|
|
100
|
+
taskId: task.id,
|
|
101
|
+
});
|
|
102
|
+
addResource(run, {
|
|
103
|
+
id: `worktree:${task.id}`,
|
|
104
|
+
kind: "worktree",
|
|
105
|
+
selector: task.worktree,
|
|
106
|
+
taskId: task.id,
|
|
107
|
+
});
|
|
108
|
+
await persist(opts.storeDir, run);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
refreshRunStatus(run);
|
|
112
|
+
await persist(opts.storeDir, run);
|
|
113
|
+
}
|
|
114
|
+
async function resolvePlan(opts) {
|
|
115
|
+
if (opts.planFile) {
|
|
116
|
+
const plan = await loadPlanFile(opts.planFile);
|
|
117
|
+
plan.planner = plan.planner ?? opts.planner;
|
|
118
|
+
return plan;
|
|
119
|
+
}
|
|
120
|
+
const command = plannerCommand(opts);
|
|
121
|
+
if (command && opts.objective) {
|
|
122
|
+
const exec = opts.exec ?? execCommand;
|
|
123
|
+
const output = await exec(command, opts.run.repo, 120_000);
|
|
124
|
+
if (output.exitCode !== 0) {
|
|
125
|
+
throw new GateError(`planner failed (exit ${output.exitCode}): ${output.stderr || output.stdout}`);
|
|
126
|
+
}
|
|
127
|
+
const raw = JSON.parse(output.stdout || "{}");
|
|
128
|
+
const plan = parsePlan(raw);
|
|
129
|
+
plan.objective ??= opts.objective;
|
|
130
|
+
plan.planner = opts.planner ?? command;
|
|
131
|
+
return plan;
|
|
132
|
+
}
|
|
133
|
+
if (!opts.objective)
|
|
134
|
+
throw new GateError("rpa run needs an objective or --plan");
|
|
135
|
+
return decomposeObjective(opts.objective, opts.planner);
|
|
136
|
+
}
|
|
137
|
+
function plannerCommand(opts) {
|
|
138
|
+
if (opts.planner && opts.planner !== "none" && opts.planner !== "heuristic" && opts.planner.includes(" ")) {
|
|
139
|
+
return opts.planner;
|
|
140
|
+
}
|
|
141
|
+
return opts.run.config.planner.command;
|
|
142
|
+
}
|
package/dist/skill.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function packageRoot(): string;
|
|
2
|
+
export declare function skillPath(): string;
|
|
3
|
+
export declare function cliPath(): string;
|
|
4
|
+
export declare function readSkill(): Promise<string>;
|
|
5
|
+
export type SkillInstall = {
|
|
6
|
+
bin: string;
|
|
7
|
+
skills: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function defaultInstallTargets(home?: string): {
|
|
10
|
+
bin: string;
|
|
11
|
+
agentSkillDir: string;
|
|
12
|
+
claudeSkillDir: string;
|
|
13
|
+
codexSkillDir: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function installSkill(opts?: {
|
|
16
|
+
home?: string;
|
|
17
|
+
cli?: string;
|
|
18
|
+
}): Promise<SkillInstall>;
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rm, stat, symlink } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { GateError } from "./errors.js";
|
|
6
|
+
export function packageRoot() {
|
|
7
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
}
|
|
9
|
+
export function skillPath() {
|
|
10
|
+
return path.join(packageRoot(), "skills", "rpa", "SKILL.md");
|
|
11
|
+
}
|
|
12
|
+
export function cliPath() {
|
|
13
|
+
return path.join(packageRoot(), "dist", "cli.js");
|
|
14
|
+
}
|
|
15
|
+
export async function readSkill() {
|
|
16
|
+
try {
|
|
17
|
+
return await readFile(skillPath(), "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new GateError(`skill file missing: ${skillPath()}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function defaultInstallTargets(home = os.homedir()) {
|
|
24
|
+
return {
|
|
25
|
+
bin: path.join(home, ".local", "bin", "rpa"),
|
|
26
|
+
agentSkillDir: path.join(home, ".agents", "skills", "rpa"),
|
|
27
|
+
claudeSkillDir: path.join(home, ".claude", "skills", "rpa"),
|
|
28
|
+
codexSkillDir: path.join(home, ".codex", "skills", "rpa"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function pathExists(p) {
|
|
32
|
+
try {
|
|
33
|
+
await stat(p);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function replaceSymlink(dest, target) {
|
|
41
|
+
await rm(dest, { force: true, recursive: true });
|
|
42
|
+
await symlink(target, dest);
|
|
43
|
+
}
|
|
44
|
+
export async function installSkill(opts = {}) {
|
|
45
|
+
const cli = opts.cli ?? cliPath();
|
|
46
|
+
if (!(await pathExists(cli))) {
|
|
47
|
+
throw new GateError(`build first (missing ${cli})`);
|
|
48
|
+
}
|
|
49
|
+
const src = skillPath();
|
|
50
|
+
if (!(await pathExists(src))) {
|
|
51
|
+
throw new GateError(`skill file missing: ${src}`);
|
|
52
|
+
}
|
|
53
|
+
const { bin, agentSkillDir, claudeSkillDir, codexSkillDir } = defaultInstallTargets(opts.home);
|
|
54
|
+
await mkdir(path.dirname(bin), { recursive: true });
|
|
55
|
+
await replaceSymlink(bin, cli);
|
|
56
|
+
await chmod(cli, 0o755);
|
|
57
|
+
const skills = [];
|
|
58
|
+
await mkdir(agentSkillDir, { recursive: true });
|
|
59
|
+
const agentSkill = path.join(agentSkillDir, "SKILL.md");
|
|
60
|
+
await replaceSymlink(agentSkill, src);
|
|
61
|
+
skills.push(agentSkill);
|
|
62
|
+
await mkdir(path.dirname(claudeSkillDir), { recursive: true });
|
|
63
|
+
await replaceSymlink(claudeSkillDir, agentSkillDir);
|
|
64
|
+
skills.push(path.join(claudeSkillDir, "SKILL.md"));
|
|
65
|
+
await mkdir(codexSkillDir, { recursive: true });
|
|
66
|
+
const codexSkill = path.join(codexSkillDir, "SKILL.md");
|
|
67
|
+
await replaceSymlink(codexSkill, src);
|
|
68
|
+
skills.push(codexSkill);
|
|
69
|
+
return { bin, skills };
|
|
70
|
+
}
|