@garuda-agent/garuda 0.0.1 → 0.15.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/CHANGELOG.md +77 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +1097 -0
- package/SECURITY.md +32 -0
- package/dist/acpCommand-FZ2P5JFO.js +153 -0
- package/dist/anthropic-NQDBCHJU.js +327 -0
- package/dist/chunk-23FXBNBB.js +46 -0
- package/dist/chunk-2S62ZNTC.js +320 -0
- package/dist/chunk-2TAXSFWN.js +49 -0
- package/dist/chunk-36GQIVGT.js +526 -0
- package/dist/chunk-3CEPHS4C.js +231 -0
- package/dist/chunk-3NJNVZTK.js +123 -0
- package/dist/chunk-42DSA4EA.js +116 -0
- package/dist/chunk-4JSUUDFQ.js +118 -0
- package/dist/chunk-4P26OKTP.js +100 -0
- package/dist/chunk-5DEWP3YX.js +139 -0
- package/dist/chunk-A2QAIRQL.js +45 -0
- package/dist/chunk-ATW7BNIG.js +31 -0
- package/dist/chunk-B25VL4RU.js +242 -0
- package/dist/chunk-EPKWDCV4.js +9 -0
- package/dist/chunk-EU7PA45O.js +324 -0
- package/dist/chunk-FARS4R67.js +206 -0
- package/dist/chunk-G2AX7MGS.js +36 -0
- package/dist/chunk-HRGX53VC.js +173 -0
- package/dist/chunk-HUIK3WXO.js +234 -0
- package/dist/chunk-INRZR772.js +73 -0
- package/dist/chunk-K3SIUOAG.js +138 -0
- package/dist/chunk-K7KK7U44.js +9358 -0
- package/dist/chunk-KKB5W47S.js +231 -0
- package/dist/chunk-N4I6YS3E.js +73 -0
- package/dist/chunk-NXJ4XLB5.js +19 -0
- package/dist/chunk-OUCV3FCT.js +205 -0
- package/dist/chunk-PQNT34LY.js +1247 -0
- package/dist/chunk-PZS3V6P3.js +46 -0
- package/dist/chunk-S77XDSD4.js +157 -0
- package/dist/chunk-SI2QJYCR.js +1341 -0
- package/dist/chunk-WMOLXTBU.js +61 -0
- package/dist/chunk-YFQJXRIJ.js +193 -0
- package/dist/chunk-YQTGMCU6.js +111 -0
- package/dist/chunk-ZAE25JM2.js +158 -0
- package/dist/cli/index.js +1203 -0
- package/dist/create-N4I7TIZE.js +223 -0
- package/dist/deadline-F5RJJ2QW.js +86 -0
- package/dist/detect-LARDSLLZ.js +10 -0
- package/dist/evalCommand-3SSFMZQD.js +2798 -0
- package/dist/export-HN3CT7AV.js +158 -0
- package/dist/inkChat-34BTFG2K.js +1193 -0
- package/dist/install-VRPBZY5H.js +17 -0
- package/dist/job-3NYDMP2M.js +31 -0
- package/dist/jobCommand-XJ5GULS5.js +30 -0
- package/dist/launchd-QVHQ3VNN.js +40 -0
- package/dist/lspCommand-XNTBHXLA.js +99 -0
- package/dist/manager-4PMSXT47.js +14 -0
- package/dist/manager-CSI76AWF.js +847 -0
- package/dist/nightCommand-CR6ZHYGP.js +247 -0
- package/dist/oauth-W6OK6XXO.js +23 -0
- package/dist/openaiCompatible-CJWVJP5M.js +501 -0
- package/dist/policy-EJPLEEXP.js +31 -0
- package/dist/proxy-XJAHCZB2.js +192 -0
- package/dist/remove-LE7Q7K46.js +59 -0
- package/dist/run-ML6GSPN4.js +888 -0
- package/dist/server-R3GNIC33.js +676 -0
- package/dist/servers-6YDGWOZ6.js +24 -0
- package/dist/text-J2GD647J.js +11 -0
- package/package.json +84 -3
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseRule
|
|
4
|
+
} from "./chunk-3NJNVZTK.js";
|
|
5
|
+
|
|
6
|
+
// src/jobs/job.ts
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { mkdirSync, renameSync, writeFileSync } from "fs";
|
|
9
|
+
import { mkdir, readdir, readFile, rename, writeFile } from "fs/promises";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
var JOBS_DIR = join(".garuda", "jobs");
|
|
13
|
+
var DEFAULT_JOB_MAX_STEPS = 100;
|
|
14
|
+
var JOB_STATUSES = ["scheduled", "running", "done", "stopped", "failed"];
|
|
15
|
+
var fileChange = z.object({
|
|
16
|
+
status: z.enum(["added", "modified", "deleted"]),
|
|
17
|
+
path: z.string(),
|
|
18
|
+
added: z.number().optional(),
|
|
19
|
+
removed: z.number().optional()
|
|
20
|
+
});
|
|
21
|
+
var testRun = z.object({
|
|
22
|
+
exitCode: z.number().nullable(),
|
|
23
|
+
timedOut: z.boolean(),
|
|
24
|
+
durationMs: z.number(),
|
|
25
|
+
tail: z.string()
|
|
26
|
+
});
|
|
27
|
+
var resultSchema = z.object({
|
|
28
|
+
stopReason: z.string(),
|
|
29
|
+
steps: z.number(),
|
|
30
|
+
tokens: z.number(),
|
|
31
|
+
costUsd: z.number().optional(),
|
|
32
|
+
durationMs: z.number(),
|
|
33
|
+
sessionId: z.string().optional(),
|
|
34
|
+
commit: z.string().optional(),
|
|
35
|
+
files: z.array(fileChange),
|
|
36
|
+
denied: z.array(z.object({ tool: z.string(), target: z.string() })),
|
|
37
|
+
answer: z.string(),
|
|
38
|
+
error: z.string().optional(),
|
|
39
|
+
/** With the Batch API: requests per API, and when the job switched to the normal API. */
|
|
40
|
+
modelCalls: z.object({
|
|
41
|
+
batch: z.number(),
|
|
42
|
+
normal: z.number(),
|
|
43
|
+
/** Steps that waited past the step limit and ran on the normal API. */
|
|
44
|
+
slow: z.number().optional(),
|
|
45
|
+
switchedAt: z.string().optional()
|
|
46
|
+
}).optional(),
|
|
47
|
+
/** Proof of work (0.11): tests before and after, risk flags, the review and the verdict. */
|
|
48
|
+
proof: z.object({
|
|
49
|
+
verdict: z.enum(["ready", "needs-look"]),
|
|
50
|
+
before: testRun.optional(),
|
|
51
|
+
after: testRun.optional(),
|
|
52
|
+
flags: z.array(z.object({ level: z.enum(["stop", "look"]), text: z.string() })),
|
|
53
|
+
review: z.object({
|
|
54
|
+
verdict: z.enum(["ready", "needs-look"]),
|
|
55
|
+
text: z.string(),
|
|
56
|
+
costUsd: z.number().optional()
|
|
57
|
+
}).optional(),
|
|
58
|
+
reviewError: z.string().optional()
|
|
59
|
+
}).optional()
|
|
60
|
+
});
|
|
61
|
+
var jobSchema = z.object({
|
|
62
|
+
version: z.literal(1),
|
|
63
|
+
id: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/),
|
|
64
|
+
title: z.string(),
|
|
65
|
+
createdAt: z.string(),
|
|
66
|
+
/** The project folder the job belongs to (the user's checkout). */
|
|
67
|
+
root: z.string(),
|
|
68
|
+
/** The commit the job starts from: HEAD, or a snapshot of the uncommitted changes. */
|
|
69
|
+
base: z.string().regex(/^[0-9a-f]{40,64}$/),
|
|
70
|
+
branch: z.string().regex(/^garuda\/job-[A-Za-z0-9_-]+$/),
|
|
71
|
+
worktree: z.string(),
|
|
72
|
+
/** The task: the plan and how to carry it out. */
|
|
73
|
+
prompt: z.string().min(1),
|
|
74
|
+
/** The session the plan came from. */
|
|
75
|
+
planSession: z.string().optional(),
|
|
76
|
+
model: z.string().optional(),
|
|
77
|
+
/** Permission rules for this job only, in the settings format ("edit_file(src/**)"). */
|
|
78
|
+
allow: z.array(z.string()),
|
|
79
|
+
onUnapproved: z.literal("deny-and-continue"),
|
|
80
|
+
/** "01:00" (local time) for `garuda run --at`, when the user gave one. */
|
|
81
|
+
at: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).optional(),
|
|
82
|
+
maxSteps: z.number().int().min(1).max(1e3),
|
|
83
|
+
/** The Batch API (0.7): half price, minutes per step; the normal API from 15 min before finishBy. */
|
|
84
|
+
batch: z.boolean().optional(),
|
|
85
|
+
finishBy: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).optional(),
|
|
86
|
+
/** A batch step that waits longer goes to the normal API (only that step). Default 20. */
|
|
87
|
+
stepLimitMinutes: z.number().int().min(1).max(24 * 60).optional(),
|
|
88
|
+
/**
|
|
89
|
+
* Ignored folders of the checkout (node_modules, .venv) that the worktree links to. Each is one
|
|
90
|
+
* folder name in the root (0.14.1, review: ".." made the parent folder writable for the job).
|
|
91
|
+
*/
|
|
92
|
+
links: z.array(
|
|
93
|
+
z.string().regex(/^[^/\\]+$/, "a link is one folder name in the project").refine((name) => name !== "." && name !== "..", "a link is one folder name in the project")
|
|
94
|
+
),
|
|
95
|
+
/**
|
|
96
|
+
* Proof of work (0.11): the test command, run in the sandbox before and after the job (absent:
|
|
97
|
+
* no tests), and the review of the diff (default true).
|
|
98
|
+
*/
|
|
99
|
+
test: z.string().min(1).optional(),
|
|
100
|
+
review: z.boolean().optional(),
|
|
101
|
+
/**
|
|
102
|
+
* The network allowlist (0.13): presets and hosts that the job's sandboxed commands may reach,
|
|
103
|
+
* approved with the job. Other hosts are denied and reported. Absent: no network.
|
|
104
|
+
*/
|
|
105
|
+
network: z.array(z.string()).optional(),
|
|
106
|
+
/** In the project's night queue (0.11): `garuda night` runs it. */
|
|
107
|
+
queue: z.boolean().optional(),
|
|
108
|
+
/** The launchd agent that starts the job (macOS), while it is installed. */
|
|
109
|
+
launchd: z.object({ label: z.string(), plist: z.string(), when: z.string() }).optional(),
|
|
110
|
+
status: z.enum(JOB_STATUSES),
|
|
111
|
+
/** Why a job failed before its turn ended (0.14.1, review). */
|
|
112
|
+
error: z.string().max(2e3).optional(),
|
|
113
|
+
startedAt: z.string().optional(),
|
|
114
|
+
endedAt: z.string().optional(),
|
|
115
|
+
result: resultSchema.optional()
|
|
116
|
+
});
|
|
117
|
+
function newJobId(now = /* @__PURE__ */ new Date()) {
|
|
118
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
119
|
+
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
120
|
+
return `${date}-${pad(now.getHours())}${pad(now.getMinutes())}-${randomBytes(2).toString("hex")}`;
|
|
121
|
+
}
|
|
122
|
+
function jobPath(root, id) {
|
|
123
|
+
return join(root, JOBS_DIR, `${id}.json`);
|
|
124
|
+
}
|
|
125
|
+
function failJobSync(job, error) {
|
|
126
|
+
job.status = "failed";
|
|
127
|
+
job.error = error.slice(0, 2e3);
|
|
128
|
+
job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
129
|
+
const dir = join(job.root, JOBS_DIR);
|
|
130
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
131
|
+
const file = jobPath(job.root, job.id);
|
|
132
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
133
|
+
writeFileSync(temp, `${JSON.stringify(job, null, 2)}
|
|
134
|
+
`, { mode: 384 });
|
|
135
|
+
renameSync(temp, file);
|
|
136
|
+
}
|
|
137
|
+
async function settleJob(root, id, exitCode) {
|
|
138
|
+
const job = await loadJob(root, id).catch(() => void 0);
|
|
139
|
+
if (job?.status !== "running") return;
|
|
140
|
+
failJobSync(
|
|
141
|
+
job,
|
|
142
|
+
`The job process ended (exit code ${exitCode ?? "none"}) before the job finished. See ${JOBS_DIR}/${id}.log.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function saveJob(job) {
|
|
146
|
+
const dir = join(job.root, JOBS_DIR);
|
|
147
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
148
|
+
const file = jobPath(job.root, job.id);
|
|
149
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
150
|
+
await writeFile(temp, `${JSON.stringify(job, null, 2)}
|
|
151
|
+
`, { mode: 384 });
|
|
152
|
+
await rename(temp, file);
|
|
153
|
+
}
|
|
154
|
+
async function loadJob(root, id) {
|
|
155
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(id)) throw new Error(`"${id}" is not a job id.`);
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = await readFile(jobPath(root, id), "utf8");
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code === "ENOENT") {
|
|
161
|
+
throw new Error(`There is no job ${id} in this project (${JOBS_DIR}).`);
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
const result = jobSchema.safeParse(JSON.parse(text));
|
|
168
|
+
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
169
|
+
parsed = result.data;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(`${JOBS_DIR}/${id}.json: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
if (parsed.root !== root) {
|
|
174
|
+
throw new Error(`Job ${id} belongs to ${parsed.root}, not to ${root}.`);
|
|
175
|
+
}
|
|
176
|
+
for (const rule of parsed.allow) parseRule(rule);
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
async function listJobs(root) {
|
|
180
|
+
let names;
|
|
181
|
+
try {
|
|
182
|
+
names = (await readdir(join(root, JOBS_DIR))).filter((n) => n.endsWith(".json"));
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const jobs = [];
|
|
187
|
+
for (const name of names.sort().reverse()) {
|
|
188
|
+
try {
|
|
189
|
+
jobs.push(await loadJob(root, name.slice(0, -".json".length)));
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return jobs;
|
|
194
|
+
}
|
|
195
|
+
function planPermissions(plan) {
|
|
196
|
+
const block = /```permissions[^\n]*\n([\s\S]*?)```/.exec(plan)?.[1];
|
|
197
|
+
const rules = [];
|
|
198
|
+
const problems = [];
|
|
199
|
+
for (const raw of (block ?? "").split("\n")) {
|
|
200
|
+
const line = raw.replace(/^\s*[-*]\s+/, "").trim();
|
|
201
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
202
|
+
try {
|
|
203
|
+
parseRule(line);
|
|
204
|
+
if (!rules.includes(line)) rules.push(line);
|
|
205
|
+
} catch {
|
|
206
|
+
problems.push(line);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { rules, problems };
|
|
210
|
+
}
|
|
211
|
+
function planTitle(prompt, plan) {
|
|
212
|
+
const line = prompt.split("\n").map((l) => l.trim()).find((l) => l !== "") ?? plan.split("\n").find((l) => l.trim() !== "") ?? "job";
|
|
213
|
+
const clean = line.replace(/^#+\s*/, "");
|
|
214
|
+
return clean.length <= 70 ? clean : `${clean.slice(0, 69)}\u2026`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
JOBS_DIR,
|
|
219
|
+
DEFAULT_JOB_MAX_STEPS,
|
|
220
|
+
JOB_STATUSES,
|
|
221
|
+
newJobId,
|
|
222
|
+
jobPath,
|
|
223
|
+
failJobSync,
|
|
224
|
+
settleJob,
|
|
225
|
+
saveJob,
|
|
226
|
+
loadJob,
|
|
227
|
+
listJobs,
|
|
228
|
+
planPermissions,
|
|
229
|
+
planTitle
|
|
230
|
+
};
|
|
231
|
+
//# sourceMappingURL=chunk-KKB5W47S.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/notify.ts
|
|
4
|
+
var DEFAULT_AFTER_SECONDS = 10;
|
|
5
|
+
var MAX_TEXT = 120;
|
|
6
|
+
var OSC9_TERMINALS = /* @__PURE__ */ new Set(["iTerm.app", "ghostty", "WezTerm"]);
|
|
7
|
+
function pickChannel(choice, env = process.env) {
|
|
8
|
+
const fromEnv = env.GARUDA_NOTIFY;
|
|
9
|
+
const wanted = fromEnv !== void 0 && fromEnv !== "" ? fromEnv : choice ?? "auto";
|
|
10
|
+
if (wanted === "off" || wanted === "bell" || wanted === "osc9") return wanted;
|
|
11
|
+
const multiplexer = env.TMUX !== void 0 || (env.TERM ?? "").startsWith("screen");
|
|
12
|
+
return !multiplexer && OSC9_TERMINALS.has(env.TERM_PROGRAM ?? "") ? "osc9" : "bell";
|
|
13
|
+
}
|
|
14
|
+
function notificationBytes(channel, text) {
|
|
15
|
+
if (channel === "off") return "";
|
|
16
|
+
if (channel === "bell") return "\x07";
|
|
17
|
+
const clean = Array.from(text, (ch) => {
|
|
18
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
19
|
+
return code < 32 || code >= 127 && code <= 159 ? " " : ch;
|
|
20
|
+
}).join("").replace(/ {2,}/g, " ").trim();
|
|
21
|
+
const cut = clean.length <= MAX_TEXT ? clean : `${clean.slice(0, MAX_TEXT - 1)}\u2026`;
|
|
22
|
+
return `\x1B]9;${cut}\x07`;
|
|
23
|
+
}
|
|
24
|
+
var Notifier = class {
|
|
25
|
+
constructor(channel, write, afterSeconds = DEFAULT_AFTER_SECONDS, isFocused = () => void 0) {
|
|
26
|
+
this.channel = channel;
|
|
27
|
+
this.write = write;
|
|
28
|
+
this.afterSeconds = afterSeconds;
|
|
29
|
+
this.isFocused = isFocused;
|
|
30
|
+
}
|
|
31
|
+
channel;
|
|
32
|
+
write;
|
|
33
|
+
afterSeconds;
|
|
34
|
+
isFocused;
|
|
35
|
+
/** Questions notify only during a turn: after a command the user typed (/undo), the user is here. */
|
|
36
|
+
running = false;
|
|
37
|
+
/** An approval or a consent question waits for the user. */
|
|
38
|
+
approval(request) {
|
|
39
|
+
if (!this.running) return;
|
|
40
|
+
const target = request.target;
|
|
41
|
+
const what = request.title ?? (target.kind === "command" ? `${request.tool}: ${target.command.split("\n")[0] ?? ""}` : target.kind === "path" ? `${request.tool} ${target.path}` : request.tool);
|
|
42
|
+
this.send(`Garuda needs your approval: ${what}`);
|
|
43
|
+
}
|
|
44
|
+
turnStarted() {
|
|
45
|
+
this.running = true;
|
|
46
|
+
}
|
|
47
|
+
/** A turn ended. Short turns and turns the user stopped stay quiet. */
|
|
48
|
+
turnEnded(outcome, ms) {
|
|
49
|
+
this.running = false;
|
|
50
|
+
if (outcome === void 0 || outcome.kind === "interrupted") return;
|
|
51
|
+
if (ms < this.afterSeconds * 1e3) return;
|
|
52
|
+
const time = formatSeconds(ms);
|
|
53
|
+
if (outcome.kind === "error") this.send(`Garuda: the task failed after ${time}.`);
|
|
54
|
+
else if (outcome.result.stopReason === "done") this.send(`Garuda: the task is done (${time}).`);
|
|
55
|
+
else this.send(`Garuda: the task stopped (${outcome.result.stopReason}) after ${time}.`);
|
|
56
|
+
}
|
|
57
|
+
send(text) {
|
|
58
|
+
if (this.isFocused() === true) return;
|
|
59
|
+
const bytes = notificationBytes(this.channel, text);
|
|
60
|
+
if (bytes !== "") this.write(bytes);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function formatSeconds(ms) {
|
|
64
|
+
const s = Math.round(ms / 1e3);
|
|
65
|
+
return s < 60 ? `${s} s` : `${Math.floor(s / 60)} min ${s % 60} s`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
pickChannel,
|
|
70
|
+
notificationBytes,
|
|
71
|
+
Notifier
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=chunk-N4I6YS3E.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/permissions/autoApprover.ts
|
|
4
|
+
var AutoApprover = class {
|
|
5
|
+
requests = [];
|
|
6
|
+
answer;
|
|
7
|
+
constructor(answer = "once") {
|
|
8
|
+
this.answer = typeof answer === "function" ? answer : () => answer;
|
|
9
|
+
}
|
|
10
|
+
async ask(request) {
|
|
11
|
+
this.requests.push(request);
|
|
12
|
+
return this.answer(request);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
AutoApprover
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=chunk-NXJ4XLB5.js.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/jobs/proof.ts
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
5
|
+
import { extname, join } from "path";
|
|
6
|
+
var TEST_TAIL_LINES = 30;
|
|
7
|
+
var REVIEW_DIFF_CHARS = 6e4;
|
|
8
|
+
function detectTestCommand(root, profiles) {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
11
|
+
const test = pkg.scripts?.test;
|
|
12
|
+
if (typeof test === "string" && test.trim() !== "" && !test.includes("no test specified")) {
|
|
13
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm test";
|
|
14
|
+
if (existsSync(join(root, "yarn.lock"))) return "yarn test";
|
|
15
|
+
return "npm test";
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
if (profiles[0] !== void 0) return profiles[0].test;
|
|
20
|
+
return hasNodeTests(root) ? "node --test" : void 0;
|
|
21
|
+
}
|
|
22
|
+
function hasNodeTests(root) {
|
|
23
|
+
const isTest = (name) => /\.test\.[cm]?js$/.test(name);
|
|
24
|
+
for (const dir of [root, join(root, "test")]) {
|
|
25
|
+
try {
|
|
26
|
+
if (readdirSync(dir).some(isTest)) return true;
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function testSummary(run) {
|
|
33
|
+
if (run === void 0) return "not run";
|
|
34
|
+
if (run.timedOut) return `stopped after ${Math.round(run.durationMs / 1e3)} s (too long)`;
|
|
35
|
+
return `${run.exitCode === 0 ? "pass" : `fail (exit code ${run.exitCode})`} in ${Math.round(run.durationMs / 1e3)} s`;
|
|
36
|
+
}
|
|
37
|
+
var passed = (run) => run !== void 0 && !run.timedOut && run.exitCode === 0;
|
|
38
|
+
var TEST_PATH = /(^|\/)(tests?|__tests__|spec)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$|(^|\/)src\/test\//;
|
|
39
|
+
var MANIFEST = /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|pyproject\.toml|requirements[^/]*\.txt|poetry\.lock|uv\.lock|pom\.xml|build\.gradle(\.kts)?|go\.mod|go\.sum|Cargo\.toml|Cargo\.lock|Gemfile(\.lock)?)$/;
|
|
40
|
+
var DELIVERY = /(^|\/)(\.github\/workflows\/|\.gitlab-ci\.yml$|Dockerfile$|docker-compose[^/]*\.ya?ml$|\.env)/;
|
|
41
|
+
function riskFlags(input) {
|
|
42
|
+
const { result, test, before, after } = input;
|
|
43
|
+
const flags = [];
|
|
44
|
+
if (result.stopReason !== "done") {
|
|
45
|
+
flags.push({
|
|
46
|
+
level: "stop",
|
|
47
|
+
text: `The job did not finish: it stopped with "${result.stopReason}".`
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (result.error !== void 0) flags.push({ level: "stop", text: `Error: ${result.error}` });
|
|
51
|
+
if (test === void 0) {
|
|
52
|
+
flags.push({ level: "look", text: "No test command: nothing proves the change works." });
|
|
53
|
+
} else if (after !== void 0 && !passed(after)) {
|
|
54
|
+
flags.push({
|
|
55
|
+
level: "stop",
|
|
56
|
+
text: passed(before) ? "The tests passed before the job and fail after it." : "The tests fail after the job (they failed before it too)."
|
|
57
|
+
});
|
|
58
|
+
} else if (after !== void 0 && before !== void 0 && !passed(before)) {
|
|
59
|
+
flags.push({
|
|
60
|
+
level: "look",
|
|
61
|
+
text: "The tests failed before the job and pass after it: check that the change is why."
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const tests = result.files.filter((f) => TEST_PATH.test(f.path));
|
|
65
|
+
const deletedTests = tests.filter((f) => f.status === "deleted");
|
|
66
|
+
if (deletedTests.length > 0) {
|
|
67
|
+
flags.push({
|
|
68
|
+
level: "look",
|
|
69
|
+
text: `Test files deleted: ${deletedTests.map((f) => f.path).join(", ")}.`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const changedTests = tests.filter((f) => f.status === "modified");
|
|
73
|
+
if (changedTests.length > 0) {
|
|
74
|
+
flags.push({
|
|
75
|
+
level: "look",
|
|
76
|
+
text: `Existing tests changed: ${changedTests.map((f) => f.path).join(", ")}. Check that they still test what they did.`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const manifests = result.files.filter((f) => MANIFEST.test(f.path));
|
|
80
|
+
if (manifests.length > 0) {
|
|
81
|
+
flags.push({
|
|
82
|
+
level: "look",
|
|
83
|
+
text: `Dependencies or build files changed: ${manifests.map((f) => f.path).join(", ")}.`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const delivery = result.files.filter((f) => DELIVERY.test(f.path));
|
|
87
|
+
if (delivery.length > 0) {
|
|
88
|
+
flags.push({
|
|
89
|
+
level: "look",
|
|
90
|
+
text: `CI, container or environment files changed: ${delivery.map((f) => f.path).join(", ")}.`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const lines = result.files.reduce((n, f) => n + (f.added ?? 0) + (f.removed ?? 0), 0);
|
|
94
|
+
if (result.files.length > 20 || lines > 500) {
|
|
95
|
+
flags.push({
|
|
96
|
+
level: "look",
|
|
97
|
+
text: `A large change: ${result.files.length} files, ${lines} lines.`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (result.denied.length > 0) {
|
|
101
|
+
flags.push({
|
|
102
|
+
level: "look",
|
|
103
|
+
text: `${result.denied.length} call(s) were denied: the job may be incomplete.`
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return flags;
|
|
107
|
+
}
|
|
108
|
+
var LANGUAGES = {
|
|
109
|
+
ts: "TypeScript",
|
|
110
|
+
tsx: "TypeScript",
|
|
111
|
+
mts: "TypeScript",
|
|
112
|
+
cts: "TypeScript",
|
|
113
|
+
js: "JavaScript",
|
|
114
|
+
jsx: "JavaScript",
|
|
115
|
+
mjs: "JavaScript",
|
|
116
|
+
cjs: "JavaScript",
|
|
117
|
+
py: "Python",
|
|
118
|
+
java: "Java",
|
|
119
|
+
kt: "Kotlin",
|
|
120
|
+
go: "Go",
|
|
121
|
+
rs: "Rust",
|
|
122
|
+
rb: "Ruby",
|
|
123
|
+
cs: "C#",
|
|
124
|
+
swift: "Swift",
|
|
125
|
+
php: "PHP",
|
|
126
|
+
scala: "Scala",
|
|
127
|
+
sql: "SQL"
|
|
128
|
+
};
|
|
129
|
+
function stackOf(files, profiles) {
|
|
130
|
+
const out = /* @__PURE__ */ new Set();
|
|
131
|
+
for (const f of files) {
|
|
132
|
+
const language = LANGUAGES[extname(f.path).slice(1).toLowerCase()];
|
|
133
|
+
if (language !== void 0) out.add(language);
|
|
134
|
+
}
|
|
135
|
+
for (const p of profiles) out.add(p.label);
|
|
136
|
+
return [...out];
|
|
137
|
+
}
|
|
138
|
+
function reviewerSystem(stack) {
|
|
139
|
+
const expert = stack.length === 0 ? "this codebase" : `${stack.join(", ")} and this codebase`;
|
|
140
|
+
return [
|
|
141
|
+
`You are a principal engineer and a domain expert in ${expert}. A staff engineer made the change below`,
|
|
142
|
+
"as an unattended job, from a plan that the user approved. You review it before the user merges it.",
|
|
143
|
+
"Check, in this order: correctness and edge cases; that the change does what the plan says, no more and",
|
|
144
|
+
"no less; that tests prove it (new behaviour tested, no test weakened); security and data risks; the",
|
|
145
|
+
"project's conventions. Judge only what the diff and the facts show. Do not invent problems; say what",
|
|
146
|
+
"you cannot see.",
|
|
147
|
+
"",
|
|
148
|
+
"Answer in exactly this form:",
|
|
149
|
+
"VERDICT: ready | needs a look",
|
|
150
|
+
"SUMMARY: one sentence",
|
|
151
|
+
"FINDINGS:",
|
|
152
|
+
"- [high|medium|low] path:line: the problem and the fix",
|
|
153
|
+
'(or "- none")'
|
|
154
|
+
].join("\n");
|
|
155
|
+
}
|
|
156
|
+
function reviewPrompt(input) {
|
|
157
|
+
const diff = input.diff.length <= REVIEW_DIFF_CHARS ? input.diff : `${input.diff.slice(0, REVIEW_DIFF_CHARS)}
|
|
158
|
+
[\u2026 the diff is cut here; ${input.diff.length - REVIEW_DIFF_CHARS} more characters]`;
|
|
159
|
+
return [
|
|
160
|
+
"<job>",
|
|
161
|
+
input.prompt.trim(),
|
|
162
|
+
"</job>",
|
|
163
|
+
"",
|
|
164
|
+
"<facts>",
|
|
165
|
+
`Files: ${input.files.map((f) => `${f.status} ${f.path} (+${f.added ?? "?"} \u2212${f.removed ?? "?"})`).join("; ") || "none"}`,
|
|
166
|
+
`Tests (${input.test ?? "no test command"}): before the job ${testSummary(input.before)}; after it ${testSummary(input.after)}`,
|
|
167
|
+
...input.after !== void 0 && !passed(input.after) ? [`Test output after the job:
|
|
168
|
+
${input.after.tail}`] : [],
|
|
169
|
+
`Risk flags: ${input.flags.length === 0 ? "none" : input.flags.map((f) => f.text).join(" ")}`,
|
|
170
|
+
"</facts>",
|
|
171
|
+
"",
|
|
172
|
+
"<diff>",
|
|
173
|
+
diff,
|
|
174
|
+
"</diff>"
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
function parseReview(text) {
|
|
178
|
+
const m = /^\s*VERDICT:\s*(ready|needs a look)\b/im.exec(text);
|
|
179
|
+
return { verdict: m?.[1]?.toLowerCase() === "ready" ? "ready" : "needs-look", text: text.trim() };
|
|
180
|
+
}
|
|
181
|
+
function jobVerdict(flags, review) {
|
|
182
|
+
if (flags.some((f) => f.level === "stop")) return "needs-look";
|
|
183
|
+
return review ?? (flags.length === 0 ? "ready" : "needs-look");
|
|
184
|
+
}
|
|
185
|
+
function tail(text, lines = TEST_TAIL_LINES) {
|
|
186
|
+
const all = text.trimEnd().split("\n");
|
|
187
|
+
return all.length <= lines ? all.join("\n") : `[\u2026 ${all.length - lines} lines]
|
|
188
|
+
${all.slice(-lines).join("\n")}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
detectTestCommand,
|
|
193
|
+
testSummary,
|
|
194
|
+
passed,
|
|
195
|
+
TEST_PATH,
|
|
196
|
+
MANIFEST,
|
|
197
|
+
riskFlags,
|
|
198
|
+
stackOf,
|
|
199
|
+
reviewerSystem,
|
|
200
|
+
reviewPrompt,
|
|
201
|
+
parseReview,
|
|
202
|
+
jobVerdict,
|
|
203
|
+
tail
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=chunk-OUCV3FCT.js.map
|