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/store.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { GateError } from "./errors.js";
|
|
2
|
+
import type { EventType, RpaConfig, RunState } from "./types.js";
|
|
3
|
+
export declare const CURRENT_FILE = "current";
|
|
4
|
+
export declare const RPA_VERSION = "0.1.0";
|
|
5
|
+
export type StoreErrorKind = "no-current" | "missing-state" | "unreadable-state" | "malformed-state" | "invalid-state";
|
|
6
|
+
export declare class StoreError extends GateError {
|
|
7
|
+
readonly kind: StoreErrorKind;
|
|
8
|
+
constructor(kind: StoreErrorKind, message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveStoreDir(store?: string, cwd?: string): string;
|
|
11
|
+
export declare function runDir(storeDir: string, runId: string): string;
|
|
12
|
+
export declare function createRun(storeDir: string, input: {
|
|
13
|
+
repo: string;
|
|
14
|
+
config: RpaConfig;
|
|
15
|
+
id?: string;
|
|
16
|
+
objective?: string;
|
|
17
|
+
planner?: string;
|
|
18
|
+
}): Promise<RunState>;
|
|
19
|
+
export declare function persist(storeDir: string, run: RunState, event?: {
|
|
20
|
+
type: EventType;
|
|
21
|
+
taskId?: string;
|
|
22
|
+
attemptId?: string;
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
export declare function saveRun(storeDir: string, run: RunState): Promise<void>;
|
|
26
|
+
export declare function loadRun(storeDir: string, runId?: string): Promise<RunState>;
|
|
27
|
+
export declare function writeManifest(storeDir: string, run: RunState): Promise<void>;
|
|
28
|
+
export declare function writePlanFile(storeDir: string, run: RunState): Promise<void>;
|
|
29
|
+
export declare function readCurrent(storeDir: string): Promise<string>;
|
|
30
|
+
export declare function listRunDirs(storeDir: string): Promise<string[]>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { GateError } from "./errors.js";
|
|
4
|
+
import { appendEvent } from "./journal.js";
|
|
5
|
+
import { heartbeat } from "./resources.js";
|
|
6
|
+
export const CURRENT_FILE = "current";
|
|
7
|
+
export const RPA_VERSION = "0.1.0";
|
|
8
|
+
export class StoreError extends GateError {
|
|
9
|
+
kind;
|
|
10
|
+
constructor(kind, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.kind = kind;
|
|
13
|
+
this.name = "StoreError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function resolveStoreDir(store, cwd = process.cwd()) {
|
|
17
|
+
if (store)
|
|
18
|
+
return path.resolve(cwd, store);
|
|
19
|
+
if (process.env.RPA_STORE)
|
|
20
|
+
return path.resolve(process.env.RPA_STORE);
|
|
21
|
+
return path.resolve(cwd, ".rpa");
|
|
22
|
+
}
|
|
23
|
+
export function runDir(storeDir, runId) {
|
|
24
|
+
return path.join(storeDir, "runs", runId);
|
|
25
|
+
}
|
|
26
|
+
export async function createRun(storeDir, input) {
|
|
27
|
+
const id = input.id ?? newRunId();
|
|
28
|
+
const repo = path.resolve(input.repo);
|
|
29
|
+
const run = {
|
|
30
|
+
id,
|
|
31
|
+
repo,
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
status: "running",
|
|
34
|
+
config: input.config,
|
|
35
|
+
tasks: {},
|
|
36
|
+
objective: input.objective,
|
|
37
|
+
planner: input.planner,
|
|
38
|
+
resources: [],
|
|
39
|
+
};
|
|
40
|
+
heartbeat(run);
|
|
41
|
+
await saveRun(storeDir, run);
|
|
42
|
+
await writeManifest(storeDir, run);
|
|
43
|
+
await writeFile(path.join(storeDir, CURRENT_FILE), `${id}\n`, "utf8");
|
|
44
|
+
await appendEvent(storeDir, {
|
|
45
|
+
runId: id,
|
|
46
|
+
type: "RUN_CREATED",
|
|
47
|
+
payload: { repo, objective: input.objective ?? null },
|
|
48
|
+
});
|
|
49
|
+
return run;
|
|
50
|
+
}
|
|
51
|
+
export async function persist(storeDir, run, event) {
|
|
52
|
+
heartbeat(run);
|
|
53
|
+
await saveRun(storeDir, run);
|
|
54
|
+
if (event) {
|
|
55
|
+
await appendEvent(storeDir, { runId: run.id, ...event });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function saveRun(storeDir, run) {
|
|
59
|
+
run.resources ??= [];
|
|
60
|
+
const dir = runDir(storeDir, run.id);
|
|
61
|
+
await mkdir(dir, { recursive: true });
|
|
62
|
+
const file = path.join(dir, "state.json");
|
|
63
|
+
const tmp = `${file}.tmp`;
|
|
64
|
+
await writeFile(tmp, `${JSON.stringify(run, null, 2)}\n`, "utf8");
|
|
65
|
+
await rename(tmp, file);
|
|
66
|
+
}
|
|
67
|
+
export async function loadRun(storeDir, runId) {
|
|
68
|
+
const id = runId ?? (await readCurrent(storeDir));
|
|
69
|
+
const file = path.join(runDir(storeDir, id), "state.json");
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = await readFile(file, "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (isNotFound(err)) {
|
|
76
|
+
throw new StoreError("missing-state", `run state is missing for ${id}: ${file}; restore the state file or select a valid run with --run`);
|
|
77
|
+
}
|
|
78
|
+
throw new StoreError("unreadable-state", `run state is unreadable for ${id}: ${file}: ${errorMessage(err)}`);
|
|
79
|
+
}
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(raw);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new StoreError("malformed-state", `run state contains malformed JSON for ${id}: ${file}: ${errorMessage(err)}`);
|
|
86
|
+
}
|
|
87
|
+
if (!isRunState(parsed, id)) {
|
|
88
|
+
throw new StoreError("invalid-state", `run state is corrupt for ${id}: ${file}; required run, config, task, attempt, or resource fields are structurally invalid`);
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
export async function writeManifest(storeDir, run) {
|
|
93
|
+
const manifest = {
|
|
94
|
+
runId: run.id,
|
|
95
|
+
repo: run.repo,
|
|
96
|
+
baseSha: run.baseSha,
|
|
97
|
+
planner: run.planner,
|
|
98
|
+
rpaVersion: RPA_VERSION,
|
|
99
|
+
createdAt: run.createdAt,
|
|
100
|
+
objective: run.objective,
|
|
101
|
+
};
|
|
102
|
+
await mkdir(runDir(storeDir, run.id), { recursive: true });
|
|
103
|
+
await writeFile(path.join(runDir(storeDir, run.id), "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
104
|
+
}
|
|
105
|
+
export async function writePlanFile(storeDir, run) {
|
|
106
|
+
if (!run.plan)
|
|
107
|
+
return;
|
|
108
|
+
await mkdir(runDir(storeDir, run.id), { recursive: true });
|
|
109
|
+
await writeFile(path.join(runDir(storeDir, run.id), "plan.json"), `${JSON.stringify(run.plan, null, 2)}\n`, "utf8");
|
|
110
|
+
}
|
|
111
|
+
export async function readCurrent(storeDir) {
|
|
112
|
+
const file = path.join(storeDir, CURRENT_FILE);
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = await readFile(file, "utf8");
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (isNotFound(err) && !(await pathExists(file))) {
|
|
119
|
+
throw new StoreError("no-current", "no current run; pass --run or run rpa init");
|
|
120
|
+
}
|
|
121
|
+
throw new StoreError("unreadable-state", `current run pointer is unreadable: ${file}: ${errorMessage(err)}`);
|
|
122
|
+
}
|
|
123
|
+
const id = raw.trim();
|
|
124
|
+
if (!id) {
|
|
125
|
+
throw new StoreError("unreadable-state", `current run pointer is empty: ${file}; restore it or pass --run`);
|
|
126
|
+
}
|
|
127
|
+
return id;
|
|
128
|
+
}
|
|
129
|
+
export async function listRunDirs(storeDir) {
|
|
130
|
+
try {
|
|
131
|
+
const entries = await readdir(path.join(storeDir, "runs"), { withFileTypes: true });
|
|
132
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function newRunId() {
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
141
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
142
|
+
return `run-${stamp}-${rand}`;
|
|
143
|
+
}
|
|
144
|
+
function isNotFound(err) {
|
|
145
|
+
return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
146
|
+
}
|
|
147
|
+
function errorMessage(err) {
|
|
148
|
+
return err instanceof Error ? err.message : String(err);
|
|
149
|
+
}
|
|
150
|
+
async function pathExists(file) {
|
|
151
|
+
try {
|
|
152
|
+
await lstat(file);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const RUN_STATUSES = new Set([
|
|
160
|
+
"planned",
|
|
161
|
+
"running",
|
|
162
|
+
"partial",
|
|
163
|
+
"blocked",
|
|
164
|
+
"failed",
|
|
165
|
+
"success",
|
|
166
|
+
"cleaning",
|
|
167
|
+
"cleaned",
|
|
168
|
+
]);
|
|
169
|
+
const TASK_STATUSES = new Set([
|
|
170
|
+
"planned",
|
|
171
|
+
"dispatched",
|
|
172
|
+
"claimed",
|
|
173
|
+
"verifying",
|
|
174
|
+
"accepted",
|
|
175
|
+
"retrying",
|
|
176
|
+
"blocked",
|
|
177
|
+
"integrated",
|
|
178
|
+
"repairing",
|
|
179
|
+
]);
|
|
180
|
+
const WORKER_OUTCOMES = new Set(["pending", "succeeded", "failed"]);
|
|
181
|
+
const DISPATCH_PHASES = new Set(["intent", "task-created", "worker-started", "failed"]);
|
|
182
|
+
const RESOURCE_KINDS = new Set(["worktree", "terminal", "dispatch", "run"]);
|
|
183
|
+
const INTEGRATION_STATUSES = new Set([
|
|
184
|
+
"pending",
|
|
185
|
+
"merging",
|
|
186
|
+
"verifying",
|
|
187
|
+
"passed",
|
|
188
|
+
"failed",
|
|
189
|
+
"conflict",
|
|
190
|
+
"repairing",
|
|
191
|
+
]);
|
|
192
|
+
const ERROR_CLASSES = new Set([
|
|
193
|
+
"assertion",
|
|
194
|
+
"typecheck",
|
|
195
|
+
"lint",
|
|
196
|
+
"timeout",
|
|
197
|
+
"crash",
|
|
198
|
+
"merge_conflict",
|
|
199
|
+
"ambiguous",
|
|
200
|
+
"environment",
|
|
201
|
+
"permission",
|
|
202
|
+
"credential",
|
|
203
|
+
"flaky",
|
|
204
|
+
"unknown",
|
|
205
|
+
]);
|
|
206
|
+
function isRunState(value, id) {
|
|
207
|
+
if (!isRecord(value))
|
|
208
|
+
return false;
|
|
209
|
+
if (value.id !== id ||
|
|
210
|
+
!isNonEmptyString(value.repo) ||
|
|
211
|
+
!isNonEmptyString(value.createdAt) ||
|
|
212
|
+
!isMember(value.status, RUN_STATUSES) ||
|
|
213
|
+
!isConfig(value.config) ||
|
|
214
|
+
!isRecord(value.tasks) ||
|
|
215
|
+
!Array.isArray(value.resources) ||
|
|
216
|
+
(value.integration !== undefined && !isIntegration(value.integration)) ||
|
|
217
|
+
(value.objective !== undefined && typeof value.objective !== "string") ||
|
|
218
|
+
(value.planner !== undefined && typeof value.planner !== "string") ||
|
|
219
|
+
(value.baseSha !== undefined && !isSha(value.baseSha)) ||
|
|
220
|
+
(value.lastHeartbeatAt !== undefined && !isNonEmptyString(value.lastHeartbeatAt))) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
for (const [taskId, task] of Object.entries(value.tasks)) {
|
|
224
|
+
if (!isTask(task, taskId))
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return value.resources.every((resource) => isResource(resource, id));
|
|
228
|
+
}
|
|
229
|
+
function isTask(value, taskId) {
|
|
230
|
+
if (!isRecord(value))
|
|
231
|
+
return false;
|
|
232
|
+
if (value.id !== taskId ||
|
|
233
|
+
!isMember(value.status, TASK_STATUSES) ||
|
|
234
|
+
!isNonEmptyString(value.worktree) ||
|
|
235
|
+
!isMember(value.workerOutcome, WORKER_OUTCOMES) ||
|
|
236
|
+
!Array.isArray(value.attempts) ||
|
|
237
|
+
!value.attempts.every(isAttempt)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (value.dispatchPhase !== undefined && !isMember(value.dispatchPhase, DISPATCH_PHASES)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (value.status === "dispatched" && value.dispatchPhase === undefined)
|
|
244
|
+
return false;
|
|
245
|
+
if (value.dispatchPhase === "task-created" && !isNonEmptyString(value.orcaTaskId))
|
|
246
|
+
return false;
|
|
247
|
+
if (value.dispatchPhase === "worker-started" &&
|
|
248
|
+
(!isNonEmptyString(value.dispatchId) || !isNonEmptyString(value.orcaTaskId))) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return ((value.dispatchId === undefined || isNonEmptyString(value.dispatchId)) &&
|
|
252
|
+
(value.agent === undefined || isNonEmptyString(value.agent)) &&
|
|
253
|
+
(value.headSha === undefined || isSha(value.headSha)) &&
|
|
254
|
+
(value.dirty === undefined || typeof value.dirty === "boolean") &&
|
|
255
|
+
(value.lastError === undefined || isNonEmptyString(value.lastError)) &&
|
|
256
|
+
(value.title === undefined || typeof value.title === "string") &&
|
|
257
|
+
(value.deliverable === undefined || typeof value.deliverable === "string") &&
|
|
258
|
+
(value.deps === undefined ||
|
|
259
|
+
(Array.isArray(value.deps) && value.deps.every(isNonEmptyString))) &&
|
|
260
|
+
(value.errorClass === undefined || isMember(value.errorClass, ERROR_CLASSES)) &&
|
|
261
|
+
(value.escalatedFrom === undefined ||
|
|
262
|
+
(Array.isArray(value.escalatedFrom) && value.escalatedFrom.every(isNonEmptyString))) &&
|
|
263
|
+
(value.orcaTaskId === undefined || isNonEmptyString(value.orcaTaskId)));
|
|
264
|
+
}
|
|
265
|
+
function isAttempt(value) {
|
|
266
|
+
if (!isRecord(value))
|
|
267
|
+
return false;
|
|
268
|
+
return (Number.isInteger(value.attemptNo) &&
|
|
269
|
+
value.attemptNo > 0 &&
|
|
270
|
+
isNonEmptyString(value.startedAt) &&
|
|
271
|
+
isNonEmptyString(value.finishedAt) &&
|
|
272
|
+
typeof value.verifyPass === "boolean" &&
|
|
273
|
+
Array.isArray(value.checks) &&
|
|
274
|
+
value.checks.every(isCheck) &&
|
|
275
|
+
(value.flaky === undefined || typeof value.flaky === "boolean") &&
|
|
276
|
+
(value.errorClass === undefined || isMember(value.errorClass, ERROR_CLASSES)) &&
|
|
277
|
+
(value.escalatedFrom === undefined || isNonEmptyString(value.escalatedFrom)));
|
|
278
|
+
}
|
|
279
|
+
function isCheck(value) {
|
|
280
|
+
if (!isRecord(value))
|
|
281
|
+
return false;
|
|
282
|
+
return (isNonEmptyString(value.name) &&
|
|
283
|
+
typeof value.command === "string" &&
|
|
284
|
+
typeof value.required === "boolean" &&
|
|
285
|
+
Number.isInteger(value.exitCode) &&
|
|
286
|
+
typeof value.passed === "boolean" &&
|
|
287
|
+
typeof value.timedOut === "boolean" &&
|
|
288
|
+
typeof value.durationMs === "number" &&
|
|
289
|
+
value.durationMs >= 0 &&
|
|
290
|
+
typeof value.stdout === "string" &&
|
|
291
|
+
typeof value.stderr === "string");
|
|
292
|
+
}
|
|
293
|
+
function isResource(value, runId) {
|
|
294
|
+
if (!isRecord(value))
|
|
295
|
+
return false;
|
|
296
|
+
return (isNonEmptyString(value.id) &&
|
|
297
|
+
isMember(value.kind, RESOURCE_KINDS) &&
|
|
298
|
+
isNonEmptyString(value.selector) &&
|
|
299
|
+
value.createdBy === "rpa" &&
|
|
300
|
+
isNonEmptyString(value.createdAt) &&
|
|
301
|
+
isNonEmptyString(value.leaseExpiresAt) &&
|
|
302
|
+
isNonEmptyString(value.lastHeartbeatAt) &&
|
|
303
|
+
value.runId === runId &&
|
|
304
|
+
(value.taskId === undefined || isNonEmptyString(value.taskId)) &&
|
|
305
|
+
(value.cleanedAt === undefined || isNonEmptyString(value.cleanedAt)));
|
|
306
|
+
}
|
|
307
|
+
function isIntegration(value) {
|
|
308
|
+
if (!isRecord(value))
|
|
309
|
+
return false;
|
|
310
|
+
return (isNonEmptyString(value.into) &&
|
|
311
|
+
isMember(value.status, INTEGRATION_STATUSES) &&
|
|
312
|
+
(value.baseSha === undefined || isSha(value.baseSha)) &&
|
|
313
|
+
(value.headSha === undefined || isSha(value.headSha)) &&
|
|
314
|
+
Array.isArray(value.mergedTaskIds) &&
|
|
315
|
+
value.mergedTaskIds.every(isNonEmptyString) &&
|
|
316
|
+
Array.isArray(value.checks) &&
|
|
317
|
+
value.checks.every(isCheck) &&
|
|
318
|
+
(value.error === undefined || isNonEmptyString(value.error)));
|
|
319
|
+
}
|
|
320
|
+
function isConfig(value) {
|
|
321
|
+
if (!isRecord(value))
|
|
322
|
+
return false;
|
|
323
|
+
if (!isRecord(value.verify) ||
|
|
324
|
+
!Array.isArray(value.verify.task) ||
|
|
325
|
+
!value.verify.task.every(isVerifyCheck) ||
|
|
326
|
+
!Array.isArray(value.verify.integration) ||
|
|
327
|
+
!value.verify.integration.every(isVerifyCheck) ||
|
|
328
|
+
!isRecord(value.retry) ||
|
|
329
|
+
!isNonNegativeInteger(value.retry.max) ||
|
|
330
|
+
!isRecord(value.flaky) ||
|
|
331
|
+
!isNonNegativeInteger(value.flaky.retries) ||
|
|
332
|
+
!Number.isInteger(value.flaky.requireConsecutivePasses) ||
|
|
333
|
+
value.flaky.requireConsecutivePasses < 1 ||
|
|
334
|
+
!isRecord(value.planner) ||
|
|
335
|
+
(value.planner.command !== undefined && typeof value.planner.command !== "string") ||
|
|
336
|
+
!isRecord(value.providers) ||
|
|
337
|
+
!isRecord(value.lease) ||
|
|
338
|
+
typeof value.lease.ttlMs !== "number" ||
|
|
339
|
+
value.lease.ttlMs <= 0) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
return Object.values(value.providers).every((provider) => isRecord(provider) && isNonEmptyString(provider.command));
|
|
343
|
+
}
|
|
344
|
+
function isVerifyCheck(value) {
|
|
345
|
+
if (!isRecord(value))
|
|
346
|
+
return false;
|
|
347
|
+
return (isNonEmptyString(value.name) &&
|
|
348
|
+
typeof value.command === "string" &&
|
|
349
|
+
typeof value.required === "boolean" &&
|
|
350
|
+
typeof value.timeoutMs === "number" &&
|
|
351
|
+
value.timeoutMs > 0 &&
|
|
352
|
+
(value.provider === undefined || isNonEmptyString(value.provider)));
|
|
353
|
+
}
|
|
354
|
+
function isRecord(value) {
|
|
355
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
356
|
+
}
|
|
357
|
+
function isNonEmptyString(value) {
|
|
358
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
359
|
+
}
|
|
360
|
+
function isSha(value) {
|
|
361
|
+
return typeof value === "string" && /^[0-9a-f]{40}$/.test(value);
|
|
362
|
+
}
|
|
363
|
+
function isMember(value, values) {
|
|
364
|
+
return typeof value === "string" && values.has(value);
|
|
365
|
+
}
|
|
366
|
+
function isNonNegativeInteger(value) {
|
|
367
|
+
return Number.isInteger(value) && value >= 0;
|
|
368
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export type WorkerOutcome = "pending" | "succeeded" | "failed";
|
|
2
|
+
export type DispatchPhase = "intent" | "task-created" | "worker-started" | "failed";
|
|
3
|
+
export type TaskStatus = "planned" | "dispatched" | "claimed" | "verifying" | "accepted" | "retrying" | "blocked" | "integrated" | "repairing";
|
|
4
|
+
export type RunStatus = "planned" | "running" | "partial" | "blocked" | "failed" | "success" | "cleaning" | "cleaned";
|
|
5
|
+
export type IntegrationStatus = "pending" | "merging" | "verifying" | "passed" | "failed" | "conflict" | "repairing";
|
|
6
|
+
export type ErrorClass = "assertion" | "typecheck" | "lint" | "timeout" | "crash" | "merge_conflict" | "ambiguous" | "environment" | "permission" | "credential" | "flaky" | "unknown";
|
|
7
|
+
export type EventType = "RUN_CREATED" | "PLAN_CREATED" | "TASK_DISPATCHED" | "WORKER_STARTED" | "WORKER_DONE" | "TASK_CLAIMED" | "VERIFY_FAILED" | "TASK_ACCEPTED" | "RETRY_REQUESTED" | "INTEGRATION_STARTED" | "INTEGRATION_CONFLICT" | "INTEGRATION_FAILED" | "REPAIR_STARTED" | "RUN_FAILED" | "RUN_SUCCEEDED" | "RUN_RESUMED" | "RESOURCE_CLEANED" | "HEARTBEAT";
|
|
8
|
+
export type VerifyCheck = {
|
|
9
|
+
name: string;
|
|
10
|
+
command: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
provider?: string;
|
|
14
|
+
};
|
|
15
|
+
export type FlakyConfig = {
|
|
16
|
+
retries: number;
|
|
17
|
+
requireConsecutivePasses: number;
|
|
18
|
+
};
|
|
19
|
+
export type PlannerConfig = {
|
|
20
|
+
command?: string;
|
|
21
|
+
};
|
|
22
|
+
export type ProviderConfig = {
|
|
23
|
+
command: string;
|
|
24
|
+
};
|
|
25
|
+
export type LeaseConfig = {
|
|
26
|
+
ttlMs: number;
|
|
27
|
+
};
|
|
28
|
+
export type RpaConfig = {
|
|
29
|
+
verify: {
|
|
30
|
+
task: VerifyCheck[];
|
|
31
|
+
integration: VerifyCheck[];
|
|
32
|
+
};
|
|
33
|
+
retry: {
|
|
34
|
+
max: number;
|
|
35
|
+
};
|
|
36
|
+
flaky: FlakyConfig;
|
|
37
|
+
planner: PlannerConfig;
|
|
38
|
+
providers: Record<string, ProviderConfig>;
|
|
39
|
+
lease: LeaseConfig;
|
|
40
|
+
};
|
|
41
|
+
export type CheckResult = {
|
|
42
|
+
name: string;
|
|
43
|
+
command: string;
|
|
44
|
+
required: boolean;
|
|
45
|
+
exitCode: number;
|
|
46
|
+
passed: boolean;
|
|
47
|
+
timedOut: boolean;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
stdout: string;
|
|
50
|
+
stderr: string;
|
|
51
|
+
provider?: string;
|
|
52
|
+
};
|
|
53
|
+
export type Attempt = {
|
|
54
|
+
attemptNo: number;
|
|
55
|
+
startedAt: string;
|
|
56
|
+
finishedAt: string;
|
|
57
|
+
verifyPass: boolean;
|
|
58
|
+
checks: CheckResult[];
|
|
59
|
+
flaky?: boolean;
|
|
60
|
+
errorClass?: ErrorClass;
|
|
61
|
+
escalatedFrom?: string;
|
|
62
|
+
};
|
|
63
|
+
export type TaskState = {
|
|
64
|
+
id: string;
|
|
65
|
+
status: TaskStatus;
|
|
66
|
+
worktree: string;
|
|
67
|
+
dispatchPhase?: DispatchPhase;
|
|
68
|
+
dispatchId?: string;
|
|
69
|
+
agent?: string;
|
|
70
|
+
workerOutcome: WorkerOutcome;
|
|
71
|
+
headSha?: string;
|
|
72
|
+
dirty?: boolean;
|
|
73
|
+
attempts: Attempt[];
|
|
74
|
+
lastError?: string;
|
|
75
|
+
title?: string;
|
|
76
|
+
deliverable?: string;
|
|
77
|
+
deps?: string[];
|
|
78
|
+
errorClass?: ErrorClass;
|
|
79
|
+
escalatedFrom?: string[];
|
|
80
|
+
orcaTaskId?: string;
|
|
81
|
+
};
|
|
82
|
+
export type IntegrationState = {
|
|
83
|
+
into: string;
|
|
84
|
+
status: IntegrationStatus;
|
|
85
|
+
baseSha?: string;
|
|
86
|
+
headSha?: string;
|
|
87
|
+
mergedTaskIds: string[];
|
|
88
|
+
checks: CheckResult[];
|
|
89
|
+
error?: string;
|
|
90
|
+
};
|
|
91
|
+
export type ResourceRecord = {
|
|
92
|
+
id: string;
|
|
93
|
+
kind: "worktree" | "terminal" | "dispatch" | "run";
|
|
94
|
+
selector: string;
|
|
95
|
+
createdBy: "rpa";
|
|
96
|
+
createdAt: string;
|
|
97
|
+
leaseExpiresAt: string;
|
|
98
|
+
lastHeartbeatAt: string;
|
|
99
|
+
runId: string;
|
|
100
|
+
taskId?: string;
|
|
101
|
+
cleanedAt?: string;
|
|
102
|
+
};
|
|
103
|
+
export type PlanTask = {
|
|
104
|
+
id: string;
|
|
105
|
+
title: string;
|
|
106
|
+
agent?: string;
|
|
107
|
+
mode?: string;
|
|
108
|
+
scope?: {
|
|
109
|
+
paths?: string[];
|
|
110
|
+
};
|
|
111
|
+
deliverable?: string;
|
|
112
|
+
verify?: VerifyCheck[];
|
|
113
|
+
deps?: string[];
|
|
114
|
+
worktree?: string;
|
|
115
|
+
};
|
|
116
|
+
export type Plan = {
|
|
117
|
+
objective?: string;
|
|
118
|
+
planner?: string;
|
|
119
|
+
tasks: PlanTask[];
|
|
120
|
+
};
|
|
121
|
+
export type JournalEvent = {
|
|
122
|
+
id: number;
|
|
123
|
+
runId: string;
|
|
124
|
+
taskId?: string;
|
|
125
|
+
attemptId?: string;
|
|
126
|
+
type: EventType;
|
|
127
|
+
payload: Record<string, unknown>;
|
|
128
|
+
createdAt: string;
|
|
129
|
+
};
|
|
130
|
+
export type RunManifest = {
|
|
131
|
+
runId: string;
|
|
132
|
+
repo: string;
|
|
133
|
+
baseSha?: string;
|
|
134
|
+
planner?: string;
|
|
135
|
+
rpaVersion: string;
|
|
136
|
+
createdAt: string;
|
|
137
|
+
objective?: string;
|
|
138
|
+
};
|
|
139
|
+
export type RunState = {
|
|
140
|
+
id: string;
|
|
141
|
+
repo: string;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
status: RunStatus;
|
|
144
|
+
config: RpaConfig;
|
|
145
|
+
tasks: Record<string, TaskState>;
|
|
146
|
+
integration?: IntegrationState;
|
|
147
|
+
objective?: string;
|
|
148
|
+
planner?: string;
|
|
149
|
+
plan?: Plan;
|
|
150
|
+
baseSha?: string;
|
|
151
|
+
resources: ResourceRecord[];
|
|
152
|
+
lastHeartbeatAt?: string;
|
|
153
|
+
};
|
|
154
|
+
export type ExecResult = {
|
|
155
|
+
exitCode: number;
|
|
156
|
+
stdout: string;
|
|
157
|
+
stderr: string;
|
|
158
|
+
timedOut: boolean;
|
|
159
|
+
};
|
|
160
|
+
export type ExecFn = (command: string, cwd: string, timeoutMs: number) => Promise<ExecResult>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CheckResult, ExecFn, RpaConfig, VerifyCheck } from "./types.js";
|
|
2
|
+
export declare function runChecks(checks: VerifyCheck[], cwd: string, exec: ExecFn, opts?: {
|
|
3
|
+
config?: RpaConfig;
|
|
4
|
+
extra?: Record<string, string>;
|
|
5
|
+
}): Promise<CheckResult[]>;
|
|
6
|
+
export declare function resolveCheckCommand(check: VerifyCheck, config?: RpaConfig, extra?: Record<string, string>): string;
|
|
7
|
+
export declare function interpolate(template: string, vars: Record<string, string>): string;
|
|
8
|
+
export declare function requiredFailed(checks: CheckResult[]): CheckResult | undefined;
|
|
9
|
+
export declare function verifyPassed(checks: CheckResult[]): boolean;
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export async function runChecks(checks, cwd, exec, opts = {}) {
|
|
2
|
+
const results = [];
|
|
3
|
+
for (const check of checks) {
|
|
4
|
+
const started = Date.now();
|
|
5
|
+
const command = resolveCheckCommand(check, opts.config, opts.extra);
|
|
6
|
+
const output = await exec(command, cwd, check.timeoutMs);
|
|
7
|
+
results.push({
|
|
8
|
+
name: check.name,
|
|
9
|
+
command,
|
|
10
|
+
required: check.required,
|
|
11
|
+
exitCode: output.exitCode,
|
|
12
|
+
passed: output.exitCode === 0 && !output.timedOut,
|
|
13
|
+
timedOut: output.timedOut,
|
|
14
|
+
durationMs: Date.now() - started,
|
|
15
|
+
stdout: output.stdout,
|
|
16
|
+
stderr: output.stderr,
|
|
17
|
+
provider: check.provider,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
export function resolveCheckCommand(check, config, extra = {}) {
|
|
23
|
+
const fromProvider = check.provider && config?.providers[check.provider]?.command
|
|
24
|
+
? config.providers[check.provider].command
|
|
25
|
+
: undefined;
|
|
26
|
+
const command = check.command || fromProvider;
|
|
27
|
+
if (!command) {
|
|
28
|
+
throw new Error(check.provider
|
|
29
|
+
? `provider ${check.provider} has no command; set providers.${check.provider}.command`
|
|
30
|
+
: `check ${check.name} has no command`);
|
|
31
|
+
}
|
|
32
|
+
return interpolate(command, { cwd: extra.cwd ?? "", base: extra.base ?? "", head: extra.head ?? "", ...extra });
|
|
33
|
+
}
|
|
34
|
+
export function interpolate(template, vars) {
|
|
35
|
+
return template.replace(/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, key) => vars[key] ?? "");
|
|
36
|
+
}
|
|
37
|
+
export function requiredFailed(checks) {
|
|
38
|
+
return checks.find((check) => check.required && !check.passed);
|
|
39
|
+
}
|
|
40
|
+
export function verifyPassed(checks) {
|
|
41
|
+
return requiredFailed(checks) === undefined;
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rich-parallel-agents",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Verification gate for orca workers. worker_done is a claim until verify passes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"orchestration",
|
|
8
|
+
"verification",
|
|
9
|
+
"orca"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/agent-infra-for-justn/rich-parallel-agents#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/agent-infra-for-justn/rich-parallel-agents/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/agent-infra-for-justn/rich-parallel-agents.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"rpa": "dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": ["dist", "skills"],
|
|
30
|
+
"engines": { "node": ">=20" },
|
|
31
|
+
"packageManager": "pnpm@10.33.2",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc && node --eval \"import('node:fs').then((fs) => fs.chmodSync('dist/cli.js', 0o755))\"",
|
|
37
|
+
"prepack": "pnpm build",
|
|
38
|
+
"rpa": "tsx src/cli.ts",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^14.0.0",
|
|
44
|
+
"yaml": "^2.8.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^24.3.0",
|
|
48
|
+
"tsx": "^4.20.5",
|
|
49
|
+
"typescript": "^5.9.2",
|
|
50
|
+
"vitest": "^3.2.4"
|
|
51
|
+
}
|
|
52
|
+
}
|