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/config.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
|
+
export const DEFAULT_CONFIG = {
|
|
5
|
+
verify: { task: [], integration: [] },
|
|
6
|
+
retry: { max: 1 },
|
|
7
|
+
flaky: { retries: 0, requireConsecutivePasses: 1 },
|
|
8
|
+
planner: {},
|
|
9
|
+
providers: {},
|
|
10
|
+
lease: { ttlMs: 30 * 60_000 },
|
|
11
|
+
};
|
|
12
|
+
export function parseConfig(raw) {
|
|
13
|
+
const data = (YAML.parse(raw) ?? {});
|
|
14
|
+
const verify = data.verify && typeof data.verify === "object"
|
|
15
|
+
? data.verify
|
|
16
|
+
: {};
|
|
17
|
+
const retry = data.retry && typeof data.retry === "object"
|
|
18
|
+
? data.retry
|
|
19
|
+
: {};
|
|
20
|
+
const flaky = data.flaky && typeof data.flaky === "object"
|
|
21
|
+
? data.flaky
|
|
22
|
+
: {};
|
|
23
|
+
const planner = data.planner && typeof data.planner === "object"
|
|
24
|
+
? data.planner
|
|
25
|
+
: typeof data.planner === "string"
|
|
26
|
+
? { command: data.planner }
|
|
27
|
+
: {};
|
|
28
|
+
const providers = data.providers && typeof data.providers === "object"
|
|
29
|
+
? data.providers
|
|
30
|
+
: {};
|
|
31
|
+
const lease = data.lease && typeof data.lease === "object"
|
|
32
|
+
? data.lease
|
|
33
|
+
: {};
|
|
34
|
+
return {
|
|
35
|
+
verify: {
|
|
36
|
+
task: parseChecks(verify.task),
|
|
37
|
+
integration: parseChecks(verify.integration),
|
|
38
|
+
},
|
|
39
|
+
retry: {
|
|
40
|
+
max: parseMax(retry.max, DEFAULT_CONFIG.retry.max),
|
|
41
|
+
},
|
|
42
|
+
flaky: {
|
|
43
|
+
retries: parseMax(flaky.retries, DEFAULT_CONFIG.flaky.retries),
|
|
44
|
+
requireConsecutivePasses: Math.max(1, parseMax(flaky.require_consecutive_passes ?? flaky.requireConsecutivePasses, DEFAULT_CONFIG.flaky.requireConsecutivePasses)),
|
|
45
|
+
},
|
|
46
|
+
planner: parsePlanner(planner),
|
|
47
|
+
providers: parseProviders(providers),
|
|
48
|
+
lease: {
|
|
49
|
+
ttlMs: parseTimeout(lease.ttl ?? lease.ttlMs, DEFAULT_CONFIG.lease.ttlMs),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function loadConfig(path) {
|
|
54
|
+
return parseConfig(await readFile(path, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
function parseChecks(value) {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return [];
|
|
59
|
+
return value.map((item, index) => {
|
|
60
|
+
if (!item || typeof item !== "object") {
|
|
61
|
+
throw new Error(`verify check ${index} must be an object`);
|
|
62
|
+
}
|
|
63
|
+
const rec = item;
|
|
64
|
+
const provider = typeof rec.provider === "string" ? rec.provider.trim() : undefined;
|
|
65
|
+
const command = typeof rec.command === "string" && rec.command.trim()
|
|
66
|
+
? rec.command.trim()
|
|
67
|
+
: provider
|
|
68
|
+
? ""
|
|
69
|
+
: requiredString(rec.command, `verify check ${index} command`);
|
|
70
|
+
const name = typeof rec.name === "string" && rec.name.trim()
|
|
71
|
+
? rec.name.trim()
|
|
72
|
+
: command || provider || `check-${index}`;
|
|
73
|
+
const check = {
|
|
74
|
+
name,
|
|
75
|
+
command,
|
|
76
|
+
required: rec.required === undefined ? true : Boolean(rec.required),
|
|
77
|
+
timeoutMs: parseTimeout(rec.timeout ?? rec.timeoutMs, DEFAULT_TIMEOUT_MS),
|
|
78
|
+
};
|
|
79
|
+
if (provider)
|
|
80
|
+
check.provider = provider;
|
|
81
|
+
return check;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function parsePlanner(value) {
|
|
85
|
+
const command = typeof value.command === "string" ? value.command.trim() : undefined;
|
|
86
|
+
return command ? { command } : {};
|
|
87
|
+
}
|
|
88
|
+
function parseProviders(value) {
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const [name, item] of Object.entries(value)) {
|
|
91
|
+
if (typeof item === "string") {
|
|
92
|
+
out[name] = { command: item };
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (item && typeof item === "object" && typeof item.command === "string") {
|
|
96
|
+
out[name] = { command: item.command };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function parseMax(value, fallback) {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return fallback;
|
|
104
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
105
|
+
throw new Error("must be a non-negative integer");
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
export function parseTimeout(value, fallback) {
|
|
110
|
+
if (value === undefined || value === null)
|
|
111
|
+
return fallback;
|
|
112
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
113
|
+
return value > 0 && value < 1000 ? value * 1000 : value;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/i);
|
|
117
|
+
if (!match)
|
|
118
|
+
throw new Error(`invalid timeout: ${value}`);
|
|
119
|
+
const n = Number(match[1]);
|
|
120
|
+
const unit = (match[2] ?? "s").toLowerCase();
|
|
121
|
+
if (unit === "ms")
|
|
122
|
+
return n;
|
|
123
|
+
if (unit === "m")
|
|
124
|
+
return n * 60_000;
|
|
125
|
+
return n * 1000;
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`invalid timeout: ${String(value)}`);
|
|
128
|
+
}
|
|
129
|
+
function requiredString(value, label) {
|
|
130
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
131
|
+
throw new Error(`${label} is required`);
|
|
132
|
+
}
|
|
133
|
+
return value.trim();
|
|
134
|
+
}
|
|
135
|
+
export function commandAsCheck(command) {
|
|
136
|
+
return {
|
|
137
|
+
name: command,
|
|
138
|
+
command,
|
|
139
|
+
required: true,
|
|
140
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
141
|
+
};
|
|
142
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/exec.d.ts
ADDED
package/dist/exec.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const OUTPUT_CAP = 32_768;
|
|
3
|
+
export const execCommand = (command, cwd, timeoutMs) => {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const child = spawn(command, {
|
|
6
|
+
cwd,
|
|
7
|
+
shell: true,
|
|
8
|
+
env: process.env,
|
|
9
|
+
});
|
|
10
|
+
let stdout = "";
|
|
11
|
+
let stderr = "";
|
|
12
|
+
let timedOut = false;
|
|
13
|
+
let settled = false;
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
timedOut = true;
|
|
16
|
+
child.kill("SIGTERM");
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
if (!settled)
|
|
19
|
+
child.kill("SIGKILL");
|
|
20
|
+
}, 1000).unref();
|
|
21
|
+
}, timeoutMs);
|
|
22
|
+
child.stdout?.on("data", (chunk) => {
|
|
23
|
+
stdout = cap(`${stdout}${chunk.toString()}`);
|
|
24
|
+
});
|
|
25
|
+
child.stderr?.on("data", (chunk) => {
|
|
26
|
+
stderr = cap(`${stderr}${chunk.toString()}`);
|
|
27
|
+
});
|
|
28
|
+
child.on("error", (err) => {
|
|
29
|
+
if (settled)
|
|
30
|
+
return;
|
|
31
|
+
settled = true;
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve({
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
stdout,
|
|
36
|
+
stderr: cap(`${stderr}${err.message}`),
|
|
37
|
+
timedOut: false,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
child.on("close", (code) => {
|
|
41
|
+
if (settled)
|
|
42
|
+
return;
|
|
43
|
+
settled = true;
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
resolve({
|
|
46
|
+
exitCode: timedOut ? 124 : (code ?? 1),
|
|
47
|
+
stdout,
|
|
48
|
+
stderr,
|
|
49
|
+
timedOut,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
function cap(text) {
|
|
55
|
+
if (text.length <= OUTPUT_CAP)
|
|
56
|
+
return text;
|
|
57
|
+
return text.slice(text.length - OUTPUT_CAP);
|
|
58
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CheckResult, RunState, TaskState } from "./types.js";
|
|
2
|
+
export declare function formatRun(run: RunState): string;
|
|
3
|
+
export declare function formatCheck(check: CheckResult): string;
|
|
4
|
+
export declare function formatTask(task: TaskState): string;
|
|
5
|
+
export declare function toJson(value: unknown): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function formatRun(run) {
|
|
2
|
+
const lines = [`run ${run.id} ${run.status}`, ""];
|
|
3
|
+
if (run.objective)
|
|
4
|
+
lines.push(`objective ${run.objective}`, "");
|
|
5
|
+
const tasks = Object.values(run.tasks);
|
|
6
|
+
if (tasks.length === 0) {
|
|
7
|
+
lines.push("no tasks");
|
|
8
|
+
return lines.join("\n");
|
|
9
|
+
}
|
|
10
|
+
const idW = Math.max(4, ...tasks.map((task) => task.id.length));
|
|
11
|
+
const statusW = Math.max(6, ...tasks.map((task) => task.status.length));
|
|
12
|
+
lines.push(`${pad("TASK", idW)} ${pad("STATUS", statusW)} ATT WORKER VERIFY`);
|
|
13
|
+
lines.push("-".repeat(idW + statusW + 28));
|
|
14
|
+
for (const task of tasks) {
|
|
15
|
+
const last = task.attempts.at(-1);
|
|
16
|
+
const verify = last ? (last.verifyPass ? (last.flaky ? "flaky" : "pass") : "fail") : "—";
|
|
17
|
+
lines.push(`${pad(task.id, idW)} ${pad(task.status, statusW)} ${String(task.attempts.length).padStart(3, " ")} ${pad(task.workerOutcome, 9)} ${verify}`);
|
|
18
|
+
if (task.errorClass)
|
|
19
|
+
lines.push(` class ${task.errorClass}`);
|
|
20
|
+
if (task.lastError)
|
|
21
|
+
lines.push(` ${task.lastError}`);
|
|
22
|
+
if (last) {
|
|
23
|
+
for (const check of last.checks) {
|
|
24
|
+
lines.push(` ${formatCheck(check)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (run.integration) {
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push(`integration ${run.integration.status} into ${run.integration.into}`);
|
|
31
|
+
if (run.integration.mergedTaskIds.length > 0) {
|
|
32
|
+
lines.push(` merged: ${run.integration.mergedTaskIds.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (run.integration.error)
|
|
35
|
+
lines.push(` ${run.integration.error}`);
|
|
36
|
+
for (const check of run.integration.checks) {
|
|
37
|
+
lines.push(` ${formatCheck(check)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
export function formatCheck(check) {
|
|
43
|
+
const mark = check.passed ? "pass" : check.timedOut ? "timeout" : "fail";
|
|
44
|
+
const req = check.required ? "required" : "optional";
|
|
45
|
+
const provider = check.provider ? ` provider:${check.provider}` : "";
|
|
46
|
+
return `${check.name} ${mark} ${req} ${check.durationMs}ms${provider}`;
|
|
47
|
+
}
|
|
48
|
+
export function formatTask(task) {
|
|
49
|
+
const last = task.attempts.at(-1);
|
|
50
|
+
const lines = [
|
|
51
|
+
`task ${task.id} ${task.status} worker=${task.workerOutcome}`,
|
|
52
|
+
` worktree ${task.worktree}`,
|
|
53
|
+
];
|
|
54
|
+
if (task.headSha)
|
|
55
|
+
lines.push(` head ${task.headSha}${task.dirty ? " (dirty)" : ""}`);
|
|
56
|
+
if (task.errorClass)
|
|
57
|
+
lines.push(` class ${task.errorClass}`);
|
|
58
|
+
if (task.lastError)
|
|
59
|
+
lines.push(` ${task.lastError}`);
|
|
60
|
+
if (last) {
|
|
61
|
+
for (const check of last.checks)
|
|
62
|
+
lines.push(` ${formatCheck(check)}`);
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
export function toJson(value) {
|
|
67
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
68
|
+
}
|
|
69
|
+
function pad(value, width) {
|
|
70
|
+
return value.padEnd(width, " ");
|
|
71
|
+
}
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExecFn, RpaConfig, RunState, RunStatus, TaskState, VerifyCheck, WorkerOutcome } from "./types.js";
|
|
2
|
+
export type ClaimInput = {
|
|
3
|
+
taskId: string;
|
|
4
|
+
worktree: string;
|
|
5
|
+
outcome?: WorkerOutcome;
|
|
6
|
+
dispatchId?: string;
|
|
7
|
+
orcaTaskId?: string;
|
|
8
|
+
agent?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function claimTask(run: RunState, input: ClaimInput, cwd?: string): Promise<TaskState>;
|
|
11
|
+
export declare function verifyTask(run: RunState, taskId: string, opts: {
|
|
12
|
+
exec: ExecFn;
|
|
13
|
+
checks?: VerifyCheck[];
|
|
14
|
+
}): Promise<TaskState>;
|
|
15
|
+
export declare function verifyAll(run: RunState, opts: {
|
|
16
|
+
exec: ExecFn;
|
|
17
|
+
checks?: VerifyCheck[];
|
|
18
|
+
}): Promise<TaskState[]>;
|
|
19
|
+
export declare function retryTask(run: RunState, taskId: string): TaskState;
|
|
20
|
+
export declare function isMergeFailure(task: TaskState): boolean;
|
|
21
|
+
export declare function requireTask(run: RunState, taskId: string): TaskState;
|
|
22
|
+
export declare function refreshRunStatus(run: RunState): void;
|
|
23
|
+
export declare function deriveRunStatus(run: RunState): RunStatus;
|
|
24
|
+
export declare function withConfig(run: RunState, config: RpaConfig): void;
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { classifyFailure, isFlakyClass, shouldBlockWithoutRetry } from "./classify.js";
|
|
3
|
+
import { GateError } from "./errors.js";
|
|
4
|
+
import { snapshot } from "./git.js";
|
|
5
|
+
import { requiredFailed, runChecks, verifyPassed } from "./verify.js";
|
|
6
|
+
export async function claimTask(run, input, cwd = process.cwd()) {
|
|
7
|
+
const worktree = path.resolve(cwd, input.worktree);
|
|
8
|
+
const existing = run.tasks[input.taskId];
|
|
9
|
+
if (existing && (existing.status === "accepted" || existing.status === "integrated")) {
|
|
10
|
+
throw new GateError(`task ${input.taskId} is ${existing.status}; not overwriting a verified result`);
|
|
11
|
+
}
|
|
12
|
+
const git = await snapshot(worktree);
|
|
13
|
+
const outcome = input.outcome ?? "succeeded";
|
|
14
|
+
const task = {
|
|
15
|
+
id: input.taskId,
|
|
16
|
+
status: outcome === "failed" ? "blocked" : "claimed",
|
|
17
|
+
worktree,
|
|
18
|
+
dispatchId: input.dispatchId ?? existing?.dispatchId,
|
|
19
|
+
agent: input.agent ?? existing?.agent,
|
|
20
|
+
workerOutcome: outcome === "pending" ? "succeeded" : outcome,
|
|
21
|
+
headSha: git.headSha,
|
|
22
|
+
dirty: git.dirty,
|
|
23
|
+
attempts: existing?.attempts ?? [],
|
|
24
|
+
title: existing?.title,
|
|
25
|
+
deliverable: existing?.deliverable,
|
|
26
|
+
deps: existing?.deps,
|
|
27
|
+
escalatedFrom: existing?.escalatedFrom,
|
|
28
|
+
orcaTaskId: input.orcaTaskId ?? existing?.orcaTaskId,
|
|
29
|
+
lastError: outcome === "failed" ? "worker_done outcome=failed is a failed claim, not accepted" : undefined,
|
|
30
|
+
};
|
|
31
|
+
run.tasks[input.taskId] = task;
|
|
32
|
+
refreshRunStatus(run);
|
|
33
|
+
return task;
|
|
34
|
+
}
|
|
35
|
+
export async function verifyTask(run, taskId, opts) {
|
|
36
|
+
const task = requireTask(run, taskId);
|
|
37
|
+
if (task.status === "integrated") {
|
|
38
|
+
throw new GateError(`task ${taskId} is already integrated`);
|
|
39
|
+
}
|
|
40
|
+
if (task.workerOutcome === "failed") {
|
|
41
|
+
task.status = "blocked";
|
|
42
|
+
task.lastError = "worker_done outcome=failed; skipping verify";
|
|
43
|
+
refreshRunStatus(run);
|
|
44
|
+
return task;
|
|
45
|
+
}
|
|
46
|
+
if (task.status === "accepted")
|
|
47
|
+
return task;
|
|
48
|
+
if (task.status !== "claimed" && task.status !== "retrying") {
|
|
49
|
+
throw new GateError(`task ${taskId} is ${task.status}; verification requires a claimed or retrying task`);
|
|
50
|
+
}
|
|
51
|
+
const checks = opts.checks ?? run.config.verify.task;
|
|
52
|
+
task.status = "verifying";
|
|
53
|
+
const startedAt = new Date().toISOString();
|
|
54
|
+
let before;
|
|
55
|
+
try {
|
|
56
|
+
before = await snapshot(task.worktree);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return finalizeFailedVerification(run, task, startedAt, [snapshotFailure(`pre-check snapshot failed: ${errorMessage(err)}`)]);
|
|
60
|
+
}
|
|
61
|
+
task.headSha = before.headSha;
|
|
62
|
+
task.dirty = before.dirty;
|
|
63
|
+
const extra = {
|
|
64
|
+
cwd: task.worktree,
|
|
65
|
+
base: run.baseSha ?? "",
|
|
66
|
+
head: before.headSha ?? "",
|
|
67
|
+
};
|
|
68
|
+
let results;
|
|
69
|
+
if (!before.headSha || before.dirty) {
|
|
70
|
+
results = [
|
|
71
|
+
snapshotFailure(!before.headSha
|
|
72
|
+
? "verification refused: pre-check snapshot has no HEAD commit"
|
|
73
|
+
: `verification refused: pre-check worktree is dirty at ${before.headSha}`),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
try {
|
|
78
|
+
results = await runChecks(checks, task.worktree, opts.exec, {
|
|
79
|
+
config: run.config,
|
|
80
|
+
extra,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
results = [executionFailure(`verification execution failed: ${errorMessage(err)}`)];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let passed = verifyPassed(results);
|
|
88
|
+
let flaky = false;
|
|
89
|
+
let errorClass = passed ? undefined : classifyFailure(results);
|
|
90
|
+
if (before.headSha &&
|
|
91
|
+
!before.dirty &&
|
|
92
|
+
!passed &&
|
|
93
|
+
errorClass &&
|
|
94
|
+
isFlakyClass(errorClass) &&
|
|
95
|
+
run.config.flaky.retries > 0) {
|
|
96
|
+
let consecutive = 0;
|
|
97
|
+
const need = run.config.flaky.requireConsecutivePasses;
|
|
98
|
+
for (let i = 0; i < run.config.flaky.retries; i += 1) {
|
|
99
|
+
let extraResults;
|
|
100
|
+
try {
|
|
101
|
+
extraResults = await runChecks(checks, task.worktree, opts.exec, {
|
|
102
|
+
config: run.config,
|
|
103
|
+
extra,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
extraResults = [
|
|
108
|
+
executionFailure(`verification execution failed: ${errorMessage(err)}`),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
if (verifyPassed(extraResults))
|
|
112
|
+
consecutive += 1;
|
|
113
|
+
else
|
|
114
|
+
consecutive = 0;
|
|
115
|
+
results = extraResults;
|
|
116
|
+
if (consecutive >= need) {
|
|
117
|
+
passed = true;
|
|
118
|
+
flaky = true;
|
|
119
|
+
errorClass = "flaky";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!passed)
|
|
124
|
+
errorClass = classifyFailure(results);
|
|
125
|
+
}
|
|
126
|
+
let git;
|
|
127
|
+
try {
|
|
128
|
+
git = await snapshot(task.worktree);
|
|
129
|
+
task.headSha = git.headSha;
|
|
130
|
+
task.dirty = git.dirty;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
results.push(snapshotFailure(`post-check snapshot failed: ${errorMessage(err)}`));
|
|
134
|
+
passed = false;
|
|
135
|
+
flaky = false;
|
|
136
|
+
errorClass = classifyFailure(results);
|
|
137
|
+
}
|
|
138
|
+
if (git &&
|
|
139
|
+
before.headSha &&
|
|
140
|
+
!before.dirty &&
|
|
141
|
+
(git.headSha !== before.headSha || git.dirty)) {
|
|
142
|
+
results.push(snapshotFailure(`verification snapshot changed during checks: expected HEAD ${before.headSha} with a clean worktree; observed HEAD ${git.headSha ?? "<none>"}, dirty=${git.dirty}`));
|
|
143
|
+
passed = false;
|
|
144
|
+
flaky = false;
|
|
145
|
+
errorClass = classifyFailure(results);
|
|
146
|
+
}
|
|
147
|
+
const attemptNo = (task.attempts.at(-1)?.attemptNo ?? 0) + 1;
|
|
148
|
+
task.attempts.push({
|
|
149
|
+
attemptNo,
|
|
150
|
+
startedAt,
|
|
151
|
+
finishedAt: new Date().toISOString(),
|
|
152
|
+
verifyPass: passed,
|
|
153
|
+
checks: results,
|
|
154
|
+
flaky: flaky || undefined,
|
|
155
|
+
errorClass,
|
|
156
|
+
escalatedFrom: task.escalatedFrom?.[0],
|
|
157
|
+
});
|
|
158
|
+
if (passed) {
|
|
159
|
+
task.status = "accepted";
|
|
160
|
+
task.lastError = undefined;
|
|
161
|
+
task.errorClass = flaky ? "flaky" : undefined;
|
|
162
|
+
refreshRunStatus(run);
|
|
163
|
+
return task;
|
|
164
|
+
}
|
|
165
|
+
const failed = requiredFailed(results);
|
|
166
|
+
const reason = failed
|
|
167
|
+
? failed.name === "verification-snapshot" || failed.name === "verification-execution"
|
|
168
|
+
? failed.stderr
|
|
169
|
+
: failed.timedOut
|
|
170
|
+
? `${failed.name} timed out`
|
|
171
|
+
: `${failed.name} failed (exit ${failed.exitCode})`
|
|
172
|
+
: "verify failed";
|
|
173
|
+
task.lastError = reason;
|
|
174
|
+
task.errorClass = errorClass ?? classifyFailure(results, reason);
|
|
175
|
+
const maxAttempts = 1 + run.config.retry.max;
|
|
176
|
+
if (shouldBlockWithoutRetry(task.errorClass) || attemptNo >= maxAttempts) {
|
|
177
|
+
task.status = "blocked";
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
task.status = "retrying";
|
|
181
|
+
}
|
|
182
|
+
refreshRunStatus(run);
|
|
183
|
+
return task;
|
|
184
|
+
}
|
|
185
|
+
export async function verifyAll(run, opts) {
|
|
186
|
+
const ids = Object.values(run.tasks)
|
|
187
|
+
.filter((task) => task.status === "claimed" || task.status === "retrying")
|
|
188
|
+
.map((task) => task.id);
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const id of ids) {
|
|
191
|
+
out.push(await verifyTask(run, id, opts));
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
export function retryTask(run, taskId) {
|
|
196
|
+
const task = requireTask(run, taskId);
|
|
197
|
+
if (task.workerOutcome === "failed") {
|
|
198
|
+
throw new GateError(`task ${taskId} has worker_done outcome=failed; require a new successful claim`);
|
|
199
|
+
}
|
|
200
|
+
if (task.status === "accepted" || task.status === "integrated") {
|
|
201
|
+
throw new GateError(`task ${taskId} is ${task.status}; nothing to retry`);
|
|
202
|
+
}
|
|
203
|
+
if (task.status !== "retrying" && task.status !== "blocked" && task.status !== "repairing") {
|
|
204
|
+
throw new GateError(`task ${taskId} is ${task.status}; claim/verify first`);
|
|
205
|
+
}
|
|
206
|
+
if (task.status === "blocked" && isMergeFailure(task)) {
|
|
207
|
+
task.status = "retrying";
|
|
208
|
+
if (run.integration?.status === "conflict" || run.integration?.status === "failed" || run.integration?.status === "repairing") {
|
|
209
|
+
run.integration.status = "pending";
|
|
210
|
+
}
|
|
211
|
+
refreshRunStatus(run);
|
|
212
|
+
return task;
|
|
213
|
+
}
|
|
214
|
+
if (task.errorClass && shouldBlockWithoutRetry(task.errorClass)) {
|
|
215
|
+
throw new GateError(`task ${taskId} is blocked as ${task.errorClass}; not retrying`);
|
|
216
|
+
}
|
|
217
|
+
const maxAttempts = 1 + run.config.retry.max;
|
|
218
|
+
if (task.attempts.length >= maxAttempts && task.status === "blocked") {
|
|
219
|
+
throw new GateError(`task ${taskId} is blocked after ${task.attempts.length} verify attempt(s)`);
|
|
220
|
+
}
|
|
221
|
+
task.status = "retrying";
|
|
222
|
+
refreshRunStatus(run);
|
|
223
|
+
return task;
|
|
224
|
+
}
|
|
225
|
+
export function isMergeFailure(task) {
|
|
226
|
+
const err = task.lastError ?? "";
|
|
227
|
+
return (task.errorClass === "merge_conflict" ||
|
|
228
|
+
err.startsWith("merge conflict") ||
|
|
229
|
+
err.startsWith("merge failed:"));
|
|
230
|
+
}
|
|
231
|
+
export function requireTask(run, taskId) {
|
|
232
|
+
const task = run.tasks[taskId];
|
|
233
|
+
if (!task)
|
|
234
|
+
throw new GateError(`task not found: ${taskId}`);
|
|
235
|
+
return task;
|
|
236
|
+
}
|
|
237
|
+
export function refreshRunStatus(run) {
|
|
238
|
+
run.status = deriveRunStatus(run);
|
|
239
|
+
}
|
|
240
|
+
export function deriveRunStatus(run) {
|
|
241
|
+
const tasks = Object.values(run.tasks);
|
|
242
|
+
if (run.status === "cleaned")
|
|
243
|
+
return "cleaned";
|
|
244
|
+
if (run.status === "cleaning")
|
|
245
|
+
return "cleaning";
|
|
246
|
+
if (tasks.length === 0)
|
|
247
|
+
return run.plan ? "planned" : "running";
|
|
248
|
+
if (tasks.every((task) => task.status === "planned"))
|
|
249
|
+
return "planned";
|
|
250
|
+
const integrationFailed = run.integration?.status === "failed" || run.integration?.status === "conflict";
|
|
251
|
+
if (integrationFailed)
|
|
252
|
+
return "failed";
|
|
253
|
+
if (run.integration?.status === "passed") {
|
|
254
|
+
const allIntegrated = tasks.every((task) => task.status === "integrated" || task.status === "blocked");
|
|
255
|
+
if (allIntegrated && tasks.some((task) => task.status === "integrated")) {
|
|
256
|
+
return tasks.some((task) => task.status === "blocked") ? "partial" : "success";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const allBlocked = tasks.every((task) => task.status === "blocked");
|
|
260
|
+
if (allBlocked)
|
|
261
|
+
return "failed";
|
|
262
|
+
const someBlocked = tasks.some((task) => task.status === "blocked");
|
|
263
|
+
const pending = tasks.some((task) => task.status === "planned" ||
|
|
264
|
+
task.status === "dispatched" ||
|
|
265
|
+
task.status === "claimed" ||
|
|
266
|
+
task.status === "verifying" ||
|
|
267
|
+
task.status === "retrying" ||
|
|
268
|
+
task.status === "repairing");
|
|
269
|
+
if (pending)
|
|
270
|
+
return someBlocked ? "partial" : "running";
|
|
271
|
+
const allAccepted = tasks.every((task) => task.status === "accepted" || task.status === "integrated");
|
|
272
|
+
if (allAccepted)
|
|
273
|
+
return "running";
|
|
274
|
+
return someBlocked ? "partial" : "running";
|
|
275
|
+
}
|
|
276
|
+
export function withConfig(run, config) {
|
|
277
|
+
run.config = config;
|
|
278
|
+
}
|
|
279
|
+
function snapshotFailure(message) {
|
|
280
|
+
return {
|
|
281
|
+
name: "verification-snapshot",
|
|
282
|
+
command: "git snapshot",
|
|
283
|
+
required: true,
|
|
284
|
+
exitCode: 1,
|
|
285
|
+
passed: false,
|
|
286
|
+
timedOut: false,
|
|
287
|
+
durationMs: 0,
|
|
288
|
+
stdout: "",
|
|
289
|
+
stderr: message,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function executionFailure(message) {
|
|
293
|
+
return {
|
|
294
|
+
name: "verification-execution",
|
|
295
|
+
command: "task verification",
|
|
296
|
+
required: true,
|
|
297
|
+
exitCode: 1,
|
|
298
|
+
passed: false,
|
|
299
|
+
timedOut: false,
|
|
300
|
+
durationMs: 0,
|
|
301
|
+
stdout: "",
|
|
302
|
+
stderr: message,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function finalizeFailedVerification(run, task, startedAt, results) {
|
|
306
|
+
const attemptNo = (task.attempts.at(-1)?.attemptNo ?? 0) + 1;
|
|
307
|
+
const reason = requiredFailed(results)?.stderr || "verify failed";
|
|
308
|
+
task.attempts.push({
|
|
309
|
+
attemptNo,
|
|
310
|
+
startedAt,
|
|
311
|
+
finishedAt: new Date().toISOString(),
|
|
312
|
+
verifyPass: false,
|
|
313
|
+
checks: results,
|
|
314
|
+
errorClass: classifyFailure(results, reason),
|
|
315
|
+
escalatedFrom: task.escalatedFrom?.[0],
|
|
316
|
+
});
|
|
317
|
+
task.lastError = reason;
|
|
318
|
+
task.errorClass = classifyFailure(results, reason);
|
|
319
|
+
const maxAttempts = 1 + run.config.retry.max;
|
|
320
|
+
task.status =
|
|
321
|
+
shouldBlockWithoutRetry(task.errorClass) || attemptNo >= maxAttempts
|
|
322
|
+
? "blocked"
|
|
323
|
+
: "retrying";
|
|
324
|
+
refreshRunStatus(run);
|
|
325
|
+
return task;
|
|
326
|
+
}
|
|
327
|
+
function errorMessage(err) {
|
|
328
|
+
return err instanceof Error ? err.message : String(err);
|
|
329
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type GitSnapshot = {
|
|
2
|
+
headSha?: string;
|
|
3
|
+
dirty: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function snapshot(cwd: string): Promise<GitSnapshot>;
|
|
6
|
+
export declare function currentSha(cwd: string): Promise<string>;
|
|
7
|
+
export declare function fetchSha(into: string, from: string, sha: string): Promise<{
|
|
8
|
+
ok: true;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function mergeCommit(into: string, sha: string, message: string): Promise<{
|
|
14
|
+
ok: true;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
conflict: boolean;
|
|
18
|
+
error: string;
|
|
19
|
+
}>;
|