bugpatrol 0.2.0 → 0.3.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/README.md +1 -1
- package/dist/bin.js +741 -365
- package/package.json +1 -1
- package/src/ui/app.js +147 -30
- package/src/ui/style.css +17 -0
package/dist/bin.js
CHANGED
|
@@ -948,6 +948,7 @@ var paths = {
|
|
|
948
948
|
issue: (root2, id2) => data(root2, "issues", `${recordId(id2)}.json`),
|
|
949
949
|
fixes: (root2) => data(root2, "fixes"),
|
|
950
950
|
fix: (root2, id2) => data(root2, "fixes", `${recordId(id2)}.json`),
|
|
951
|
+
fixAttemptDiff: (root2, id2, n) => data(root2, "fixes", recordId(id2), `attempt-${n}.diff`),
|
|
951
952
|
publish: (root2) => data(root2, "publish"),
|
|
952
953
|
review: (root2, pr) => data(root2, "reviews", `pr-${pr}.json`),
|
|
953
954
|
/** What one review keeps besides its record: the claim routines and their replays. Nothing else reads it. */
|
|
@@ -973,9 +974,8 @@ function findProjectRoot(start) {
|
|
|
973
974
|
}
|
|
974
975
|
function instructionsPath(root2, configured) {
|
|
975
976
|
if (configured)
|
|
976
|
-
return resolve(root2, configured);
|
|
977
|
-
|
|
978
|
-
return existsSync(fallback) ? fallback : void 0;
|
|
977
|
+
return { path: resolve(root2, configured), required: true };
|
|
978
|
+
return { path: join(dir(root2), "instructions.md"), required: false };
|
|
979
979
|
}
|
|
980
980
|
function legacyLayout(root2) {
|
|
981
981
|
if (!existsSync(join(root2, LEGACY_CONFIG_FILENAME)))
|
|
@@ -1178,7 +1178,6 @@ var InfrastructureError = class extends BugpatrolError {
|
|
|
1178
1178
|
|
|
1179
1179
|
// ../core/dist/config/schema.js
|
|
1180
1180
|
import { z } from "zod";
|
|
1181
|
-
var secretRefString = z.string().describe("A ${ENV_VAR} reference. Never a literal credential (spec 11.4).");
|
|
1182
1181
|
var viewportSchema = z.object({
|
|
1183
1182
|
name: z.string(),
|
|
1184
1183
|
width: z.number().int().positive(),
|
|
@@ -1196,27 +1195,6 @@ var runSchema = z.object({
|
|
|
1196
1195
|
}).default({}),
|
|
1197
1196
|
seeds: z.array(z.string()).default([])
|
|
1198
1197
|
});
|
|
1199
|
-
var authSchema = z.discriminatedUnion("kind", [
|
|
1200
|
-
z.object({ kind: z.literal("none") }),
|
|
1201
|
-
z.object({
|
|
1202
|
-
kind: z.literal("form"),
|
|
1203
|
-
loginUrl: z.string(),
|
|
1204
|
-
credentials: secretRefString.optional(),
|
|
1205
|
-
steps: z.array(actionSchema).default([])
|
|
1206
|
-
}),
|
|
1207
|
-
z.object({ kind: z.literal("storageState"), path: z.string(), expiresAt: z.string().optional() }),
|
|
1208
|
-
z.object({
|
|
1209
|
-
kind: z.literal("seededUser"),
|
|
1210
|
-
seedCommand: z.string(),
|
|
1211
|
-
credentials: secretRefString.optional()
|
|
1212
|
-
}),
|
|
1213
|
-
z.object({
|
|
1214
|
-
kind: z.literal("ssoBypass"),
|
|
1215
|
-
header: z.string().optional(),
|
|
1216
|
-
token: secretRefString.optional()
|
|
1217
|
-
}),
|
|
1218
|
-
z.object({ kind: z.literal("manual"), path: z.string() })
|
|
1219
|
-
]);
|
|
1220
1198
|
var maskSchema = z.object({
|
|
1221
1199
|
selector: z.string(),
|
|
1222
1200
|
screen: z.string().optional(),
|
|
@@ -1445,12 +1423,18 @@ var agentsSchema = z.object({
|
|
|
1445
1423
|
enabled: z.boolean().default(false),
|
|
1446
1424
|
/** Run after the change; a non-zero exit marks the fix failed. */
|
|
1447
1425
|
verify: z.string().optional(),
|
|
1426
|
+
/**
|
|
1427
|
+
* Fix attempts on one issue before the fixer gives up on it: first
|
|
1428
|
+
* fixes, reruns after a failure, and refixes. CI fixes have their own
|
|
1429
|
+
* limit. `bugpatrol fix --issue` still runs an issue at the limit.
|
|
1430
|
+
*/
|
|
1431
|
+
attempts: z.number().int().positive().default(3),
|
|
1448
1432
|
/** After a fix, start the app from the fix worktree and repeat the issue's flow. */
|
|
1449
1433
|
retest: z.object({
|
|
1450
1434
|
enabled: z.boolean().default(true),
|
|
1451
1435
|
/** Shell command run in the worktree before the app starts, e.g. `bun install`. */
|
|
1452
1436
|
prepare: z.string().optional(),
|
|
1453
|
-
/**
|
|
1437
|
+
/** Retest verdicts in total; a not-fixed verdict gets a refix with the verdict as feedback. */
|
|
1454
1438
|
attempts: z.number().int().positive().default(2),
|
|
1455
1439
|
maxSteps: z.number().int().positive().default(30),
|
|
1456
1440
|
budgetUsd: z.number().nonnegative().optional()
|
|
@@ -1484,6 +1468,12 @@ var agentsSchema = z.object({
|
|
|
1484
1468
|
}).default({}),
|
|
1485
1469
|
/** `bugpatrol review <pr>`. */
|
|
1486
1470
|
review: z.object({
|
|
1471
|
+
/**
|
|
1472
|
+
* Tells this review apart from another Bugpatrol review of the same pull
|
|
1473
|
+
* request, for example one per app of a monorepo. Each name keeps its own
|
|
1474
|
+
* review and its own check run, and never replaces the other's.
|
|
1475
|
+
*/
|
|
1476
|
+
name: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Use lowercase letters, digits and dashes").optional(),
|
|
1487
1477
|
/** The claim check: test what the pull request says it does. Off until a team opts in. */
|
|
1488
1478
|
claims: z.boolean().default(false),
|
|
1489
1479
|
/**
|
|
@@ -1538,7 +1528,14 @@ var bugpatrolConfigSchema = z.object({
|
|
|
1538
1528
|
run: runSchema.optional(),
|
|
1539
1529
|
app: appSchema,
|
|
1540
1530
|
agents: agentsSchema,
|
|
1541
|
-
|
|
1531
|
+
/** Never read. Accept the `kind: none` older `init` wrote; reject a login flow instead of ignoring it. */
|
|
1532
|
+
auth: z.object({
|
|
1533
|
+
kind: z.literal("none", {
|
|
1534
|
+
errorMap: () => ({
|
|
1535
|
+
message: "Bugpatrol does not read `auth`. Sign in through `app.setup` and `app.instructions`."
|
|
1536
|
+
})
|
|
1537
|
+
})
|
|
1538
|
+
}).optional(),
|
|
1542
1539
|
viewports: z.array(viewportSchema).min(1).default([
|
|
1543
1540
|
{ name: "desktop", width: 1440, height: 900, deviceScaleFactor: 1 },
|
|
1544
1541
|
{ name: "mobile", width: 390, height: 844, deviceScaleFactor: 1 }
|
|
@@ -1606,9 +1603,28 @@ function requireRun(config) {
|
|
|
1606
1603
|
}
|
|
1607
1604
|
|
|
1608
1605
|
// ../core/dist/types/agents.js
|
|
1606
|
+
var LESSON_MAX_LENGTH = 200;
|
|
1607
|
+
function fitLesson(text3) {
|
|
1608
|
+
const trimmed = text3.trim();
|
|
1609
|
+
if (trimmed.length <= LESSON_MAX_LENGTH)
|
|
1610
|
+
return trimmed;
|
|
1611
|
+
const space = trimmed.lastIndexOf(" ", LESSON_MAX_LENGTH - 1);
|
|
1612
|
+
return `${trimmed.slice(0, space > 0 ? space : LESSON_MAX_LENGTH - 1).trimEnd()}\u2026`;
|
|
1613
|
+
}
|
|
1614
|
+
function lessonTooLong(text3) {
|
|
1615
|
+
const length = text3.trim().length;
|
|
1616
|
+
if (length > LESSON_MAX_LENGTH)
|
|
1617
|
+
return `The lesson has ${length} characters, and the limit is ${LESSON_MAX_LENGTH}. Nothing was saved. Write it shorter.`;
|
|
1618
|
+
}
|
|
1609
1619
|
function judgedRetests(retests = []) {
|
|
1610
1620
|
return retests.filter((retest) => retest.outcome !== "error" && retest.outcome !== "skipped");
|
|
1611
1621
|
}
|
|
1622
|
+
function fixerAttempts(attempts = []) {
|
|
1623
|
+
return attempts.filter((attempt) => attempt.kind !== "ci");
|
|
1624
|
+
}
|
|
1625
|
+
function ciAttempts(attempts = []) {
|
|
1626
|
+
return attempts.filter((attempt) => attempt.kind === "ci");
|
|
1627
|
+
}
|
|
1612
1628
|
|
|
1613
1629
|
// ../core/dist/types/ids.js
|
|
1614
1630
|
var id = {
|
|
@@ -2345,6 +2361,18 @@ function firstLine(text3) {
|
|
|
2345
2361
|
const line = text3.split("\n").find((item) => item.trim()) ?? "";
|
|
2346
2362
|
return line.length > 160 ? `${line.slice(0, 157)}...` : line;
|
|
2347
2363
|
}
|
|
2364
|
+
function tell(messages, text3) {
|
|
2365
|
+
const last = messages.at(-1);
|
|
2366
|
+
if (last?.role === "user" && typeof last.content === "string") {
|
|
2367
|
+
last.content = `${last.content}
|
|
2368
|
+
|
|
2369
|
+
${text3}`;
|
|
2370
|
+
} else if (last?.role === "user" && Array.isArray(last.content)) {
|
|
2371
|
+
last.content.push({ type: "text", text: text3 });
|
|
2372
|
+
} else {
|
|
2373
|
+
messages.push({ role: "user", content: text3 });
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2348
2376
|
function textOf(result3) {
|
|
2349
2377
|
return result3.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
2350
2378
|
}
|
|
@@ -2373,14 +2401,17 @@ function prune(messages) {
|
|
|
2373
2401
|
}
|
|
2374
2402
|
}
|
|
2375
2403
|
}
|
|
2404
|
+
var REQUEST_TIMEOUT_MS = 4 * 60 * 1e3;
|
|
2376
2405
|
var ModelRuntime = class {
|
|
2377
2406
|
use;
|
|
2378
2407
|
label;
|
|
2379
2408
|
request;
|
|
2409
|
+
requestTimeoutMs;
|
|
2380
2410
|
constructor(use, options = {}) {
|
|
2381
2411
|
this.use = use;
|
|
2382
2412
|
this.label = `model:${use.via}/${use.model}`;
|
|
2383
2413
|
this.request = options.fetch ?? fetch;
|
|
2414
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
2384
2415
|
}
|
|
2385
2416
|
async run(task, emit) {
|
|
2386
2417
|
const keyName = MODEL_KEYS[this.use.via];
|
|
@@ -2403,6 +2434,7 @@ var ModelRuntime = class {
|
|
|
2403
2434
|
let costUsd = 0;
|
|
2404
2435
|
let lastText = "";
|
|
2405
2436
|
let textOnly = 0;
|
|
2437
|
+
let handBack;
|
|
2406
2438
|
const outcome = (stop, summary, error) => ({
|
|
2407
2439
|
stop,
|
|
2408
2440
|
steps,
|
|
@@ -2410,12 +2442,21 @@ var ModelRuntime = class {
|
|
|
2410
2442
|
summary,
|
|
2411
2443
|
error
|
|
2412
2444
|
});
|
|
2413
|
-
|
|
2445
|
+
for (; ; ) {
|
|
2446
|
+
if (steps >= task.maxSteps) {
|
|
2447
|
+
const finish = task.tools.filter((tool) => tool.name.startsWith("finish"));
|
|
2448
|
+
if (handBack || !finish.length) {
|
|
2449
|
+
return outcome("max-steps", lastText);
|
|
2450
|
+
}
|
|
2451
|
+
handBack = { ...task, tools: finish };
|
|
2452
|
+
tell(messages, "Your steps are used. Call finish now with what you did and what you did not reach.");
|
|
2453
|
+
}
|
|
2454
|
+
const current = handBack ?? task;
|
|
2414
2455
|
if (Date.now() - started >= task.timeoutMs) {
|
|
2415
2456
|
return outcome("timeout", lastText);
|
|
2416
2457
|
}
|
|
2417
2458
|
prune(messages);
|
|
2418
|
-
const body2 = requestBody(this.use,
|
|
2459
|
+
const body2 = requestBody(this.use, current, messages, anthropic);
|
|
2419
2460
|
let payload;
|
|
2420
2461
|
try {
|
|
2421
2462
|
payload = await this.call(endpoint, apiKey, anthropic, body2, deadline);
|
|
@@ -2442,22 +2483,21 @@ var ModelRuntime = class {
|
|
|
2442
2483
|
if (textOnly >= 3) {
|
|
2443
2484
|
return outcome("done", lastText);
|
|
2444
2485
|
}
|
|
2445
|
-
messages
|
|
2486
|
+
tell(messages, "Call one of the tools. Call finish when you are done.");
|
|
2446
2487
|
} else {
|
|
2447
2488
|
textOnly = 0;
|
|
2448
|
-
const calls = await this.runCalls(
|
|
2489
|
+
const calls = await this.runCalls(current, parsed.calls, emit, messages, anthropic, deadline, stepCost, step);
|
|
2449
2490
|
if (calls.timeout) {
|
|
2450
2491
|
return outcome("timeout", lastText);
|
|
2451
2492
|
}
|
|
2452
2493
|
if (calls.done) {
|
|
2453
|
-
return outcome("done", calls.output);
|
|
2494
|
+
return { ...outcome(handBack ? "max-steps" : "done", calls.output), finished: true };
|
|
2454
2495
|
}
|
|
2455
2496
|
}
|
|
2456
2497
|
if (task.budgetUsd !== void 0 && costUsd >= task.budgetUsd) {
|
|
2457
2498
|
return outcome("budget", lastText);
|
|
2458
2499
|
}
|
|
2459
2500
|
}
|
|
2460
|
-
return outcome("max-steps", lastText);
|
|
2461
2501
|
}
|
|
2462
2502
|
async runCalls(task, calls, emit, messages, anthropic, deadline, stepCost, step) {
|
|
2463
2503
|
const anthroResults = [];
|
|
@@ -2541,7 +2581,12 @@ var ModelRuntime = class {
|
|
|
2541
2581
|
async call(endpoint, apiKey, anthropic, body2, deadline) {
|
|
2542
2582
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2543
2583
|
const controller = new AbortController();
|
|
2544
|
-
const
|
|
2584
|
+
const left = Math.max(1, deadline - Date.now());
|
|
2585
|
+
let stalled = false;
|
|
2586
|
+
const timer = setTimeout(() => {
|
|
2587
|
+
stalled = this.requestTimeoutMs < left;
|
|
2588
|
+
controller.abort();
|
|
2589
|
+
}, Math.min(left, this.requestTimeoutMs));
|
|
2545
2590
|
try {
|
|
2546
2591
|
const response7 = await this.request(endpoint, {
|
|
2547
2592
|
method: "POST",
|
|
@@ -2559,7 +2604,7 @@ var ModelRuntime = class {
|
|
|
2559
2604
|
throw new Error(`Model returned ${response7.status}`);
|
|
2560
2605
|
}
|
|
2561
2606
|
} catch (error) {
|
|
2562
|
-
if (attempt === 2 || error instanceof Error && (/^Model returned 4(?!29)/.test(error.message) || error.name === "AbortError")) {
|
|
2607
|
+
if (attempt === 2 || error instanceof Error && (/^Model returned 4(?!29)/.test(error.message) || error.name === "AbortError" && !stalled)) {
|
|
2563
2608
|
throw error;
|
|
2564
2609
|
}
|
|
2565
2610
|
} finally {
|
|
@@ -2634,6 +2679,7 @@ var CliRuntime = class {
|
|
|
2634
2679
|
async run(task, emit) {
|
|
2635
2680
|
const temp = await mkdtemp(join3(tmpdir(), "bugpatrol-agent-"));
|
|
2636
2681
|
let summary = "";
|
|
2682
|
+
let finished = false;
|
|
2637
2683
|
let steps = 0;
|
|
2638
2684
|
let stderr = "";
|
|
2639
2685
|
let stdout = "";
|
|
@@ -2677,6 +2723,7 @@ var CliRuntime = class {
|
|
|
2677
2723
|
});
|
|
2678
2724
|
if (result3.done) {
|
|
2679
2725
|
summary = output2;
|
|
2726
|
+
finished = true;
|
|
2680
2727
|
}
|
|
2681
2728
|
}
|
|
2682
2729
|
});
|
|
@@ -2761,7 +2808,8 @@ ${task.prompt}`;
|
|
|
2761
2808
|
stop: "timeout",
|
|
2762
2809
|
steps,
|
|
2763
2810
|
costUsd: 0,
|
|
2764
|
-
summary: summary || text3
|
|
2811
|
+
summary: summary || text3,
|
|
2812
|
+
finished
|
|
2765
2813
|
};
|
|
2766
2814
|
}
|
|
2767
2815
|
if (code !== 0 || inputError) {
|
|
@@ -2776,7 +2824,8 @@ ${task.prompt}`;
|
|
|
2776
2824
|
stop: "done",
|
|
2777
2825
|
steps,
|
|
2778
2826
|
costUsd: 0,
|
|
2779
|
-
summary: summary || text3
|
|
2827
|
+
summary: summary || text3,
|
|
2828
|
+
finished
|
|
2780
2829
|
};
|
|
2781
2830
|
} finally {
|
|
2782
2831
|
await mcp.close();
|
|
@@ -2922,17 +2971,17 @@ function boundingRegions(pixels, gridPx = 32) {
|
|
|
2922
2971
|
const cells = /* @__PURE__ */ new Map();
|
|
2923
2972
|
for (const p of pixels) {
|
|
2924
2973
|
const key = `${Math.floor(p.x / gridPx)},${Math.floor(p.y / gridPx)}`;
|
|
2925
|
-
const
|
|
2926
|
-
if (!
|
|
2974
|
+
const cell2 = cells.get(key);
|
|
2975
|
+
if (!cell2) {
|
|
2927
2976
|
cells.set(key, { x: p.x, y: p.y, width: 1, height: 1 });
|
|
2928
2977
|
continue;
|
|
2929
2978
|
}
|
|
2930
|
-
const right = Math.max(
|
|
2931
|
-
const bottom = Math.max(
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2979
|
+
const right = Math.max(cell2.x + cell2.width, p.x + 1);
|
|
2980
|
+
const bottom = Math.max(cell2.y + cell2.height, p.y + 1);
|
|
2981
|
+
cell2.x = Math.min(cell2.x, p.x);
|
|
2982
|
+
cell2.y = Math.min(cell2.y, p.y);
|
|
2983
|
+
cell2.width = right - cell2.x;
|
|
2984
|
+
cell2.height = bottom - cell2.y;
|
|
2936
2985
|
}
|
|
2937
2986
|
return [...cells.values()].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
2938
2987
|
}
|
|
@@ -3826,14 +3875,14 @@ function severityRank(s) {
|
|
|
3826
3875
|
// ../agents/dist/github.js
|
|
3827
3876
|
import { execFile as execFile3 } from "node:child_process";
|
|
3828
3877
|
import { createHash as createHash2 } from "node:crypto";
|
|
3829
|
-
import { mkdtemp as mkdtemp2, readFile as readFile3, rm as
|
|
3878
|
+
import { mkdtemp as mkdtemp2, readFile as readFile3, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3830
3879
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3831
3880
|
import { basename as basename3, join as join7, resolve as resolve3 } from "node:path";
|
|
3832
3881
|
import { promisify as promisify3 } from "node:util";
|
|
3833
3882
|
|
|
3834
3883
|
// ../agents/dist/roles/fixer.js
|
|
3835
3884
|
import { execFile as execFile2 } from "node:child_process";
|
|
3836
|
-
import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, realpath, symlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
3885
|
+
import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, realpath, rm as rm2, symlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
3837
3886
|
import { basename as basename2, dirname as dirname2, join as join6, relative as relative3, resolve as resolve2, sep } from "node:path";
|
|
3838
3887
|
import { promisify as promisify2 } from "node:util";
|
|
3839
3888
|
|
|
@@ -3961,7 +4010,9 @@ Find the problems that this pull request causes, before it merges.
|
|
|
3961
4010
|
|
|
3962
4011
|
YOUR JOB, IN ORDER
|
|
3963
4012
|
1. Read the pull request and its diff in the prompt. Work out which screens and flows the change can affect:
|
|
3964
|
-
the screens that the changed files render, and the flows that use the changed logic.
|
|
4013
|
+
the screens that the changed files render, and the flows that use the changed logic. If the app does not
|
|
4014
|
+
render or run any changed file (a change to tests, CI, docs, or code outside the app), call finish at once
|
|
4015
|
+
and say so in untested. Do not look for a screen that the change cannot reach.
|
|
3965
4016
|
2. Enter the app. If the routine "enter-app" is known, use run_routine for it. To reach a known screen, use
|
|
3966
4017
|
run_routine with its routine. A flow that starts from a routine is a flow that Bugpatrol can repeat.
|
|
3967
4018
|
3. Test each affected screen and flow like a careful user. Use each control that the change touches. Try an
|
|
@@ -3970,7 +4021,9 @@ YOUR JOB, IN ORDER
|
|
|
3970
4021
|
after a changed flow.
|
|
3971
4022
|
5. Report each problem with report_bug as soon as you see it, with the problem on screen. Then continue.
|
|
3972
4023
|
6. Do not test the rest of the app. Call finish when you tested what the change can affect, or when your steps
|
|
3973
|
-
are almost gone.
|
|
4024
|
+
are almost gone. In tested, name each screen or flow that you tested. In untested, name each one that the
|
|
4025
|
+
change can affect and that you did not reach, with why. A developer scans these lines: one short line each,
|
|
4026
|
+
such as "Rename a project from settings", not a paragraph.
|
|
3974
4027
|
|
|
3975
4028
|
Report each problem that you see, also when it looks older than this pull request. Bugpatrol repeats the flow of
|
|
3976
4029
|
each report on the base build, and the QA lead compares the two builds.
|
|
@@ -4340,7 +4393,7 @@ function lessonTools(session, role) {
|
|
|
4340
4393
|
return [
|
|
4341
4394
|
{
|
|
4342
4395
|
name: "save_lesson",
|
|
4343
|
-
description:
|
|
4396
|
+
description: `Save one lasting fact about this app or its code for later sessions: one short, specific sentence of at most ${LESSON_MAX_LENGTH} characters. Use "for" to save it for another role (explorer, judge, or fixer). When a similar lesson exists, the tool shows it and saves nothing.`,
|
|
4344
4397
|
inputSchema: {
|
|
4345
4398
|
type: "object",
|
|
4346
4399
|
properties: {
|
|
@@ -4360,7 +4413,7 @@ function lessonTools(session, role) {
|
|
|
4360
4413
|
if (saved >= memory.maxPerSession) {
|
|
4361
4414
|
return { content: [{ type: "text", text: "The lesson limit for this session is reached." }] };
|
|
4362
4415
|
}
|
|
4363
|
-
const text3 = session.vars.redact(String(input.text ?? "")).trim()
|
|
4416
|
+
const text3 = session.vars.redact(String(input.text ?? "")).trim();
|
|
4364
4417
|
if (!text3)
|
|
4365
4418
|
return { content: [{ type: "text", text: "The lesson needs a text." }], isError: true };
|
|
4366
4419
|
const target = roles.includes(input.for) ? input.for : role;
|
|
@@ -4372,6 +4425,9 @@ function lessonTools(session, role) {
|
|
|
4372
4425
|
]);
|
|
4373
4426
|
return { content: [{ type: "text", text: `Confirmed ${confirmed.id}. It has one more hit now.` }] };
|
|
4374
4427
|
}
|
|
4428
|
+
const tooLong = lessonTooLong(text3);
|
|
4429
|
+
if (tooLong)
|
|
4430
|
+
return { content: [{ type: "text", text: tooLong }], isError: true };
|
|
4375
4431
|
const similar = input.different === true ? void 0 : similarLesson(lessons, target, text3);
|
|
4376
4432
|
if (similar && similar.text.toLowerCase() !== text3.toLowerCase()) {
|
|
4377
4433
|
return {
|
|
@@ -4448,7 +4504,7 @@ var Workspace = class {
|
|
|
4448
4504
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4449
4505
|
const saved = [];
|
|
4450
4506
|
for (const lesson of lessons) {
|
|
4451
|
-
const text3 = lesson.text.trim()
|
|
4507
|
+
const text3 = lesson.text.trim();
|
|
4452
4508
|
if (!text3)
|
|
4453
4509
|
continue;
|
|
4454
4510
|
const id2 = `les_${shortHash(`${lesson.role}:${text3.toLowerCase().replace(/\s+/g, " ")}`)}`;
|
|
@@ -4583,6 +4639,20 @@ var Workspace = class {
|
|
|
4583
4639
|
saveFix(value) {
|
|
4584
4640
|
return atomic(paths.fix(this.root, value.id), value);
|
|
4585
4641
|
}
|
|
4642
|
+
async readFixAttemptDiff(id2, n) {
|
|
4643
|
+
try {
|
|
4644
|
+
return await readFile(paths.fixAttemptDiff(this.root, id2, n), "utf8");
|
|
4645
|
+
} catch (error) {
|
|
4646
|
+
if (error.code === "ENOENT")
|
|
4647
|
+
return void 0;
|
|
4648
|
+
throw error;
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
async saveFixAttemptDiff(id2, n, diff2) {
|
|
4652
|
+
const file = paths.fixAttemptDiff(this.root, id2, n);
|
|
4653
|
+
await mkdir(join5(file, ".."), { recursive: true });
|
|
4654
|
+
await writeFile2(file, diff2);
|
|
4655
|
+
}
|
|
4586
4656
|
readReview(pr) {
|
|
4587
4657
|
return readJson(paths.review(this.root, pr));
|
|
4588
4658
|
}
|
|
@@ -4947,6 +5017,27 @@ async function linkEnvFiles(source, worktree) {
|
|
|
4947
5017
|
}
|
|
4948
5018
|
return linked;
|
|
4949
5019
|
}
|
|
5020
|
+
async function change(worktree, source) {
|
|
5021
|
+
await git(worktree, "add", "-A");
|
|
5022
|
+
const base = await git(worktree, "merge-base", "HEAD", await git(source, "rev-parse", "HEAD"));
|
|
5023
|
+
return {
|
|
5024
|
+
diffStat: await git(worktree, "diff", "--cached", "--stat", base),
|
|
5025
|
+
diff: await git(worktree, "diff", "--cached", base)
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
async function closeAttempt(workspace, fixId, attempt, worktree, source) {
|
|
5029
|
+
attempt.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5030
|
+
try {
|
|
5031
|
+
await rm2(join6(worktree, LAST_DIFF), { force: true });
|
|
5032
|
+
const { diffStat, diff: diff2 } = await change(worktree, source);
|
|
5033
|
+
attempt.diffStat = diffStat || void 0;
|
|
5034
|
+
if (diff2)
|
|
5035
|
+
await workspace.saveFixAttemptDiff(fixId, attempt.n, `${diff2}
|
|
5036
|
+
`);
|
|
5037
|
+
} catch (error) {
|
|
5038
|
+
attempt.reason = `${attempt.reason ? `${attempt.reason} ` : ""}(The change could not be read: ${String(error).slice(0, 200)})`;
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
4950
5041
|
async function updateIssue(session, id2, patch) {
|
|
4951
5042
|
const current = await session.workspace.readIssue(id2);
|
|
4952
5043
|
if (!current || current.status === "dismissed")
|
|
@@ -4968,6 +5059,8 @@ async function runFixer(session, runtime, opts = {}) {
|
|
|
4968
5059
|
return false;
|
|
4969
5060
|
if (!opts.issueIds && (issue.fixRejected || fixes.some((fix) => fix.issueId === issue.id && fix.status === "rejected")))
|
|
4970
5061
|
return false;
|
|
5062
|
+
if (!opts.issueIds && fixerAttempts(fixes.find((fix) => fix.issueId === issue.id)?.attempts).length >= config.attempts)
|
|
5063
|
+
return false;
|
|
4971
5064
|
const pending = ["new", "filed"].includes(issue.status) || failed2.has(issue.id) || retry.has(issue.id);
|
|
4972
5065
|
return pending && (!issue.fixId || failed2.has(issue.id) || retry.has(issue.id) || fixes.some((fix) => fix.issueId === issue.id && fix.worktreeRemovedAt) || opts.issueIds && fixes.some((fix) => fix.issueId === issue.id && fix.status === "rejected")) && ranks[issue.severity] >= ranks[config.minSeverity];
|
|
4973
5066
|
});
|
|
@@ -4978,6 +5071,7 @@ async function runFixer(session, runtime, opts = {}) {
|
|
|
4978
5071
|
if (session.cancelled)
|
|
4979
5072
|
break;
|
|
4980
5073
|
const oldFix = fixes.find((fix) => fix.issueId === issue.id);
|
|
5074
|
+
const fixId = `fix_${issue.id}`;
|
|
4981
5075
|
const baseBranch = `bugpatrol/fix-${issue.id}`;
|
|
4982
5076
|
let branch = baseBranch;
|
|
4983
5077
|
if (oldFix?.worktreeRemovedAt && (await branchExists(source, oldFix.branch) || await branchExists(source, baseBranch))) {
|
|
@@ -4995,7 +5089,18 @@ async function runFixer(session, runtime, opts = {}) {
|
|
|
4995
5089
|
} catch {
|
|
4996
5090
|
await git(source, "worktree", "add", "-b", branch, worktree, "HEAD");
|
|
4997
5091
|
}
|
|
4998
|
-
|
|
5092
|
+
const earlier = (oldFix?.attempts ?? []).map((item) => ({ ...item }));
|
|
5093
|
+
const unfinished2 = earlier.at(-1);
|
|
5094
|
+
if (unfinished2 && !unfinished2.outcome) {
|
|
5095
|
+
unfinished2.outcome = "abandoned";
|
|
5096
|
+
unfinished2.reason = "The fixer was stopped before it finished.";
|
|
5097
|
+
if (existing)
|
|
5098
|
+
await closeAttempt(session.workspace, fixId, unfinished2, worktree, source);
|
|
5099
|
+
else
|
|
5100
|
+
unfinished2.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5101
|
+
}
|
|
5102
|
+
const start = !existing ? "clean" : !failed2.has(issue.id) ? "continue" : earlier.at(-1)?.outcome === "verify-failed" ? "repair" : "clean";
|
|
5103
|
+
if (existing && start === "clean") {
|
|
4999
5104
|
const base = await git(worktree, "merge-base", "HEAD", await git(source, "rev-parse", "HEAD"));
|
|
5000
5105
|
await git(worktree, "reset", "--hard", base);
|
|
5001
5106
|
await git(worktree, "clean", "-fd");
|
|
@@ -5017,10 +5122,16 @@ async function runFixer(session, runtime, opts = {}) {
|
|
|
5017
5122
|
}
|
|
5018
5123
|
}
|
|
5019
5124
|
const previous = retry.get(issue.id);
|
|
5125
|
+
const attempt = {
|
|
5126
|
+
n: (earlier.at(-1)?.n ?? 0) + 1,
|
|
5127
|
+
kind: previous ? "refix" : earlier.length || failed2.has(issue.id) ? "rerun" : "first",
|
|
5128
|
+
sessionId: session.sessionId,
|
|
5129
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5130
|
+
};
|
|
5020
5131
|
const proposal = {
|
|
5021
5132
|
...previous,
|
|
5022
5133
|
version: 1,
|
|
5023
|
-
id:
|
|
5134
|
+
id: fixId,
|
|
5024
5135
|
issueId: issue.id,
|
|
5025
5136
|
status: "running",
|
|
5026
5137
|
runtime: runtime.label,
|
|
@@ -5030,96 +5141,102 @@ async function runFixer(session, runtime, opts = {}) {
|
|
|
5030
5141
|
worktreeRemovedAt: void 0,
|
|
5031
5142
|
...oldFix?.worktreeRemovedAt ? { pr: void 0, commit: void 0 } : {},
|
|
5032
5143
|
startedAt: previous?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5033
|
-
error: void 0
|
|
5144
|
+
error: void 0,
|
|
5145
|
+
attempts: [...earlier, attempt]
|
|
5034
5146
|
};
|
|
5035
5147
|
await session.workspace.saveFix(proposal);
|
|
5036
5148
|
await session.workspace.saveIssue({ ...issue, status: "fixing" });
|
|
5037
5149
|
session.emit({ kind: "fix", summary: `Fixing ${issue.title}` });
|
|
5038
5150
|
try {
|
|
5039
|
-
const outcome = await runOne(session, runtime, issue, worktree, previous?.retests?.at(-1));
|
|
5151
|
+
const outcome = await runOne(session, runtime, issue, worktree, previous?.retests?.at(-1), { earlier, start });
|
|
5152
|
+
attempt.costUsd = outcome.costUsd;
|
|
5040
5153
|
if (outcome.stop !== "done") {
|
|
5154
|
+
attempt.outcome = outcome.stop === "timeout" ? "timeout" : "error";
|
|
5041
5155
|
throw new Error(outcome.error ?? `Fixer stopped: ${outcome.stop}`);
|
|
5042
5156
|
}
|
|
5043
5157
|
proposal.costUsd = (previous?.costUsd ?? 0) + outcome.costUsd;
|
|
5044
5158
|
proposal.summary = outcome.summary;
|
|
5159
|
+
attempt.reason = outcome.summary;
|
|
5045
5160
|
await git(worktree, "add", "-A");
|
|
5046
5161
|
const newDiff = await git(worktree, "diff", "--cached");
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
proposal.diffStat = await git(worktree, "diff", "--cached", "--stat", base);
|
|
5050
|
-
proposal.diff = Buffer.from(await git(worktree, "diff", "--cached", base)).subarray(0, 2e5).toString("utf8");
|
|
5162
|
+
Object.assign(proposal, await change(worktree, source));
|
|
5163
|
+
proposal.diff = Buffer.from(proposal.diff ?? "").subarray(0, 2e5).toString("utf8");
|
|
5051
5164
|
if (!newDiff && previous) {
|
|
5165
|
+
attempt.outcome = "no-change";
|
|
5052
5166
|
proposal.status = "proposed";
|
|
5053
|
-
proposal.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5054
|
-
await session.workspace.saveFix(proposal);
|
|
5055
5167
|
await updateIssue(session, issue.id, { status: "fix-proposed", fixId: proposal.id });
|
|
5056
|
-
|
|
5057
|
-
continue;
|
|
5058
|
-
}
|
|
5059
|
-
if (!newDiff) {
|
|
5168
|
+
} else if (!newDiff) {
|
|
5060
5169
|
proposal.status = outcome.summary ? "declined" : "failed";
|
|
5170
|
+
attempt.outcome = outcome.summary ? "declined" : "no-change";
|
|
5061
5171
|
if (proposal.status === "declined")
|
|
5062
5172
|
await session.workspace.upsertLessons([
|
|
5063
5173
|
{
|
|
5064
5174
|
role: "judge",
|
|
5065
5175
|
source: "fixer-decline",
|
|
5066
5176
|
scope: issue.screenId,
|
|
5067
|
-
text: `Likely by design: ${issue.title} \u2014 ${outcome.summary?.split(/(?<=[.!?])\s/)[0] ?? ""}
|
|
5177
|
+
text: fitLesson(`Likely by design: ${issue.title} \u2014 ${outcome.summary?.split(/(?<=[.!?])\s/)[0] ?? ""}`)
|
|
5068
5178
|
}
|
|
5069
5179
|
]);
|
|
5070
5180
|
if (!outcome.summary)
|
|
5071
5181
|
proposal.error = "The fixer made no change and gave no reason.";
|
|
5072
5182
|
await updateIssue(session, issue.id, { status: issue.status });
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5183
|
+
} else {
|
|
5184
|
+
if (config.verify) {
|
|
5185
|
+
try {
|
|
5186
|
+
await exec2("/bin/sh", ["-c", config.verify], {
|
|
5187
|
+
cwd: worktree,
|
|
5188
|
+
timeout: 3e5,
|
|
5189
|
+
maxBuffer: 4 * 1024 * 1024
|
|
5190
|
+
});
|
|
5191
|
+
} catch (error) {
|
|
5192
|
+
const output2 = error;
|
|
5193
|
+
const text3 = `${output2.stdout ?? ""}${output2.stderr ?? ""}`.trim() || output2.message;
|
|
5194
|
+
const last = text3.split("\n").at(-1) ?? "unknown error";
|
|
5195
|
+
await session.workspace.upsertLessons([
|
|
5196
|
+
{
|
|
5197
|
+
role: "fixer",
|
|
5198
|
+
source: "verify",
|
|
5199
|
+
text: fitLesson(`The verify command failed with: ${last}. Run it before you finish.`)
|
|
5200
|
+
}
|
|
5201
|
+
]);
|
|
5202
|
+
attempt.outcome = "verify-failed";
|
|
5203
|
+
attempt.reason = `The verify command failed: ${last}`.slice(0, 500);
|
|
5204
|
+
attempt.verifyOutput = text3.slice(-4e3);
|
|
5205
|
+
throw new Error(`Verification failed: ${attempt.verifyOutput}`, {
|
|
5206
|
+
cause: error
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
attempt.outcome = "proposed";
|
|
5211
|
+
proposal.checks = config.verify;
|
|
5212
|
+
proposal.status = "proposed";
|
|
5213
|
+
const committed = await commitFix(worktree, issue.title, config.commitMessage);
|
|
5214
|
+
if (!committed.ok) {
|
|
5088
5215
|
await session.workspace.upsertLessons([
|
|
5089
5216
|
{
|
|
5090
5217
|
role: "fixer",
|
|
5091
|
-
source: "
|
|
5092
|
-
text: `The
|
|
5218
|
+
source: "commit-hook",
|
|
5219
|
+
text: fitLesson(`The commit hook rejected a commit: ${committed.reason}. Make the change pass it.`)
|
|
5093
5220
|
}
|
|
5094
5221
|
]);
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5222
|
+
proposal.error = `Left uncommitted in the worktree: ${committed.reason}`;
|
|
5223
|
+
} else {
|
|
5224
|
+
proposal.commit = await git(worktree, "rev-parse", "HEAD");
|
|
5098
5225
|
}
|
|
5226
|
+
if (committed.ok && config.retest.enabled && session.config.agents.explorer.enabled && session.config.agents.judge.enabled && (issue.evidence.routineId || issue.evidence.steps?.length || issue.candidateIds.length))
|
|
5227
|
+
proposal.status = "retesting";
|
|
5228
|
+
await updateIssue(session, issue.id, { status: "fix-proposed", fixId: proposal.id });
|
|
5099
5229
|
}
|
|
5100
|
-
proposal.checks = config.verify;
|
|
5101
|
-
proposal.status = "proposed";
|
|
5102
|
-
const committed = await commitFix(worktree, issue.title, config.commitMessage);
|
|
5103
|
-
if (!committed.ok) {
|
|
5104
|
-
await session.workspace.upsertLessons([
|
|
5105
|
-
{
|
|
5106
|
-
role: "fixer",
|
|
5107
|
-
source: "commit-hook",
|
|
5108
|
-
text: `The commit hook rejected a commit: ${committed.reason}. Make the change pass it.`.slice(0, 200)
|
|
5109
|
-
}
|
|
5110
|
-
]);
|
|
5111
|
-
proposal.error = `Left uncommitted in the worktree: ${committed.reason}`;
|
|
5112
|
-
} else {
|
|
5113
|
-
proposal.commit = await git(worktree, "rev-parse", "HEAD");
|
|
5114
|
-
}
|
|
5115
|
-
if (committed.ok && config.retest.enabled && session.config.agents.explorer.enabled && session.config.agents.judge.enabled && (issue.evidence.routineId || issue.evidence.steps?.length || issue.candidateIds.length))
|
|
5116
|
-
proposal.status = "retesting";
|
|
5117
|
-
await updateIssue(session, issue.id, { status: "fix-proposed", fixId: proposal.id });
|
|
5118
5230
|
} catch (error) {
|
|
5119
5231
|
proposal.status = "failed";
|
|
5120
5232
|
proposal.error = String(error);
|
|
5233
|
+
attempt.outcome ??= "error";
|
|
5234
|
+
attempt.reason ??= String(error).slice(0, 500);
|
|
5121
5235
|
await updateIssue(session, issue.id, { status: issue.status });
|
|
5122
5236
|
}
|
|
5237
|
+
await closeAttempt(session.workspace, proposal.id, attempt, worktree, source);
|
|
5238
|
+
if (proposal.status === "failed" && fixerAttempts(proposal.attempts).length >= config.attempts)
|
|
5239
|
+
proposal.error = `Gave up after ${config.attempts} fix attempts. The last one: ${proposal.error}`;
|
|
5123
5240
|
proposal.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5124
5241
|
await session.workspace.saveFix(proposal);
|
|
5125
5242
|
proposals.push(proposal);
|
|
@@ -5131,7 +5248,7 @@ async function runFixer(session, runtime, opts = {}) {
|
|
|
5131
5248
|
await session.idle(proposals.reduce((sum, item) => sum + (item.costUsd ?? 0), 0));
|
|
5132
5249
|
return proposals;
|
|
5133
5250
|
}
|
|
5134
|
-
async function runOne(session, runtime, issue, worktree, last) {
|
|
5251
|
+
async function runOne(session, runtime, issue, worktree, last, history) {
|
|
5135
5252
|
const config = session.config.agents.fixer;
|
|
5136
5253
|
const evidencePaths = Object.entries(issue.evidence).filter(([key]) => ["screenshot", "baseline", "diff"].includes(key)).map(([key, value]) => `${key}: ${resolve2(session.root, String(value))}`);
|
|
5137
5254
|
const evidence = evidencePaths.join("\n");
|
|
@@ -5139,29 +5256,67 @@ async function runOne(session, runtime, issue, worktree, last) {
|
|
|
5139
5256
|
const repro = `Run the routine ${issue.evidence.routineId ?? "(none)"}, then:
|
|
5140
5257
|
${steps.join("\n")}`;
|
|
5141
5258
|
const after = last?.shots?.length ? last.shots.map((shot) => `${shot.screenId ?? "(unknown screen)"}: ${shot.after ? resolve2(session.root, shot.after) : "(unavailable)"}`).join("\n") : last?.after ? resolve2(session.root, last.after) : "(unavailable)";
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5259
|
+
const earlierAttempts = await attemptsPrompt(session, `fix_${issue.id}`, worktree, history);
|
|
5260
|
+
try {
|
|
5261
|
+
return await runtime.run({
|
|
5262
|
+
role: "fixer",
|
|
5263
|
+
sessionId: session.sessionId,
|
|
5264
|
+
workdir: worktree,
|
|
5265
|
+
system: fixerSystem(lessonsFor(await session.workspace.readMemory(), "fixer")),
|
|
5266
|
+
prompt: `Issue: ${issue.title}
|
|
5148
5267
|
Severity: ${issue.severity}
|
|
5149
5268
|
${issue.body}
|
|
5150
5269
|
Evidence:
|
|
5151
5270
|
${evidence}
|
|
5152
|
-
Reproduction: ${repro}` + (last ? `
|
|
5271
|
+
Reproduction: ${repro}` + earlierAttempts + (last ? `
|
|
5153
5272
|
Your last change did not fix the issue in the running app. The QA lead said: ${last.reason}. After screenshots:
|
|
5154
5273
|
${after}
|
|
5155
5274
|
First find why your last change had no effect in the running app, for example a different file that the app loads on this platform. Do not trust a guess from the QA lead: check the code. When you find the cause, call save_lesson with it, so that later fixes avoid it. Then fix the issue.` : ""),
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5275
|
+
tools: [
|
|
5276
|
+
...runtime.label.startsWith("cli:") ? [] : modelTools(worktree),
|
|
5277
|
+
...lessonTools(session, "fixer"),
|
|
5278
|
+
finishTool()
|
|
5279
|
+
],
|
|
5280
|
+
maxSteps: config.maxSteps,
|
|
5281
|
+
budgetUsd: config.budgetUsd,
|
|
5282
|
+
timeoutMs: config.timeoutMs
|
|
5283
|
+
}, session.emit);
|
|
5284
|
+
} finally {
|
|
5285
|
+
await rm2(join6(worktree, LAST_DIFF), { force: true });
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
var LAST_DIFF = ".bugpatrol-last-attempt.diff";
|
|
5289
|
+
async function attemptsPrompt(session, fixId, worktree, { earlier, start }) {
|
|
5290
|
+
const last = earlier.at(-1);
|
|
5291
|
+
if (!last)
|
|
5292
|
+
return "";
|
|
5293
|
+
const lines = earlier.map((attempt) => {
|
|
5294
|
+
const reason2 = attempt.reason?.split("\n")[0]?.slice(0, 200);
|
|
5295
|
+
const files2 = attempt.diffStat?.split("\n").slice(0, -1).map((line) => line.trim()).join(", ").slice(0, 300);
|
|
5296
|
+
return `${attempt.n}. ${attempt.kind}, ${attempt.outcome ?? "unfinished"}${reason2 ? `: ${reason2}` : ""}${files2 ? ` (${files2})` : ""}`;
|
|
5297
|
+
});
|
|
5298
|
+
const parts = [`
|
|
5299
|
+
|
|
5300
|
+
Earlier fix attempts on this issue:
|
|
5301
|
+
${lines.join("\n")}`];
|
|
5302
|
+
if (last.verifyOutput)
|
|
5303
|
+
parts.push(`The last attempt failed the verify command \`${session.config.agents.fixer.verify ?? ""}\` with:
|
|
5304
|
+
${last.verifyOutput}`);
|
|
5305
|
+
if (start === "repair")
|
|
5306
|
+
parts.push("Your last change is still in the worktree. Repair that change. Do not start over.");
|
|
5307
|
+
else if (start === "clean") {
|
|
5308
|
+
const diff2 = await session.workspace.readFixAttemptDiff(fixId, last.n);
|
|
5309
|
+
if (diff2 && diff2.length <= 2e4)
|
|
5310
|
+
parts.push(`The worktree starts clean. The last attempt's change was:
|
|
5311
|
+
\`\`\`diff
|
|
5312
|
+
${diff2}\`\`\``);
|
|
5313
|
+
else if (diff2) {
|
|
5314
|
+
await writeFile3(join6(worktree, LAST_DIFF), diff2);
|
|
5315
|
+
parts.push(`The worktree starts clean. The last attempt's change is in ${LAST_DIFF}; that file goes away after you finish.`);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
return `${parts.join("\n\n")}
|
|
5319
|
+
`;
|
|
5165
5320
|
}
|
|
5166
5321
|
function stepWords(step) {
|
|
5167
5322
|
if (step.kind === "tap") {
|
|
@@ -5304,7 +5459,7 @@ async function syncGitHub(root2, config, deps = {}) {
|
|
|
5304
5459
|
role,
|
|
5305
5460
|
source: "rejected-pr",
|
|
5306
5461
|
scope: issue.screenId,
|
|
5307
|
-
text: lesson
|
|
5462
|
+
text: fitLesson(lesson)
|
|
5308
5463
|
})));
|
|
5309
5464
|
const record = await workspace.startSession("judge");
|
|
5310
5465
|
await workspace.appendEvent(record.id, {
|
|
@@ -5351,7 +5506,7 @@ async function syncGitHub(root2, config, deps = {}) {
|
|
|
5351
5506
|
role: "judge",
|
|
5352
5507
|
source: "human",
|
|
5353
5508
|
scope: issue.screenId,
|
|
5354
|
-
text: `Not a bug: ${issue.title} \u2014 ${reason2}
|
|
5509
|
+
text: fitLesson(`Not a bug: ${issue.title} \u2014 ${reason2}`)
|
|
5355
5510
|
}
|
|
5356
5511
|
]);
|
|
5357
5512
|
} else if (state === "closed" && stateReason === "completed") {
|
|
@@ -5423,7 +5578,7 @@ async function withBody(body2, run3) {
|
|
|
5423
5578
|
await writeFile4(file, limitBody(body2));
|
|
5424
5579
|
return await run3(file);
|
|
5425
5580
|
} finally {
|
|
5426
|
-
await
|
|
5581
|
+
await rm3(dir2, { recursive: true, force: true });
|
|
5427
5582
|
}
|
|
5428
5583
|
}
|
|
5429
5584
|
var fromUrl = (url) => ({ url: url.trim(), number: Number(url.trim().split("/").at(-1)) });
|
|
@@ -5438,7 +5593,7 @@ async function createPr(gh, input) {
|
|
|
5438
5593
|
{
|
|
5439
5594
|
role: "fixer",
|
|
5440
5595
|
source: "commit-hook",
|
|
5441
|
-
text: `The commit hook rejected a commit: ${committed.reason}. Make the change pass it
|
|
5596
|
+
text: fitLesson(`The commit hook rejected a commit: ${committed.reason}. Make the change pass it.`)
|
|
5442
5597
|
}
|
|
5443
5598
|
]);
|
|
5444
5599
|
throw new Error(committed.reason);
|
|
@@ -6278,7 +6433,7 @@ async function captureSessionFlow(options) {
|
|
|
6278
6433
|
|
|
6279
6434
|
// ../agents/dist/patrol.js
|
|
6280
6435
|
import { execFile as execFile9 } from "node:child_process";
|
|
6281
|
-
import { relative as
|
|
6436
|
+
import { relative as relative6, resolve as resolve13 } from "node:path";
|
|
6282
6437
|
import { promisify as promisify9 } from "node:util";
|
|
6283
6438
|
|
|
6284
6439
|
// ../drivers/dist/api.js
|
|
@@ -10362,7 +10517,7 @@ import { PNG as PNG3 } from "pngjs";
|
|
|
10362
10517
|
// ../drivers/dist/video.js
|
|
10363
10518
|
import { execFile as execFile4 } from "node:child_process";
|
|
10364
10519
|
import { accessSync as accessSync2, constants as constants2 } from "node:fs";
|
|
10365
|
-
import { mkdtemp as mkdtemp3, rm as
|
|
10520
|
+
import { mkdtemp as mkdtemp3, rm as rm4, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
10366
10521
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
10367
10522
|
import { delimiter as delimiter2, join as join9 } from "node:path";
|
|
10368
10523
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -10405,7 +10560,7 @@ async function encodeFrames(frames, file, endAt) {
|
|
|
10405
10560
|
...["-c:v", "libx264", "-movflags", "+faststart", "-an", file]
|
|
10406
10561
|
]);
|
|
10407
10562
|
} finally {
|
|
10408
|
-
await
|
|
10563
|
+
await rm4(dir2, { recursive: true, force: true });
|
|
10409
10564
|
}
|
|
10410
10565
|
}
|
|
10411
10566
|
var GIF_TRIES = [
|
|
@@ -10428,7 +10583,7 @@ async function inlineGif(video, gif) {
|
|
|
10428
10583
|
if ((await stat2(gif)).size <= GIF_MAX_BYTES)
|
|
10429
10584
|
return true;
|
|
10430
10585
|
}
|
|
10431
|
-
await
|
|
10586
|
+
await rm4(gif, { force: true });
|
|
10432
10587
|
return false;
|
|
10433
10588
|
}
|
|
10434
10589
|
|
|
@@ -10532,7 +10687,7 @@ var Terminal = class {
|
|
|
10532
10687
|
}
|
|
10533
10688
|
/** The lines that scrolled off, then the screen, with no trailing spaces or empty lines. */
|
|
10534
10689
|
text() {
|
|
10535
|
-
const lines = [...this.history, ...this.grid.map((row) => row.map((
|
|
10690
|
+
const lines = [...this.history, ...this.grid.map((row) => row.map((cell2) => cell2.char).join(""))].map((line) => line.trimEnd());
|
|
10536
10691
|
while (lines.length && !lines.at(-1))
|
|
10537
10692
|
lines.pop();
|
|
10538
10693
|
return lines.join("\n");
|
|
@@ -10543,9 +10698,9 @@ var Terminal = class {
|
|
|
10543
10698
|
const out = new Uint8Array(width * this.rows * GLYPH.height);
|
|
10544
10699
|
for (let row = 0; row < this.rows; row++) {
|
|
10545
10700
|
for (let col = 0; col < this.cols; col++) {
|
|
10546
|
-
const
|
|
10547
|
-
const [fg, bg] =
|
|
10548
|
-
const bits = glyph(
|
|
10701
|
+
const cell2 = this.grid[row][col];
|
|
10702
|
+
const [fg, bg] = cell2.inverse ? [cell2.bg, cell2.fg] : [cell2.fg, cell2.bg];
|
|
10703
|
+
const bits = glyph(cell2.char, cell2.bold);
|
|
10549
10704
|
for (let line = 0; line < GLYPH.height; line++) {
|
|
10550
10705
|
const start = (row * GLYPH.height + line) * width + col * GLYPH.width;
|
|
10551
10706
|
for (let bit = 0; bit < GLYPH.width; bit++)
|
|
@@ -10573,7 +10728,7 @@ var Terminal = class {
|
|
|
10573
10728
|
this.y++;
|
|
10574
10729
|
return;
|
|
10575
10730
|
}
|
|
10576
|
-
this.history.push(this.grid.shift().map((
|
|
10731
|
+
this.history.push(this.grid.shift().map((cell2) => cell2.char).join(""));
|
|
10577
10732
|
if (this.history.length > 1e4)
|
|
10578
10733
|
this.history.shift();
|
|
10579
10734
|
this.grid.push(this.blank());
|
|
@@ -11637,7 +11792,7 @@ import { spawn as spawn5 } from "node:child_process";
|
|
|
11637
11792
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
11638
11793
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
11639
11794
|
import { once } from "node:events";
|
|
11640
|
-
import { copyFile as copyFile2, mkdtemp as mkdtemp4, readFile as readFile6, rm as
|
|
11795
|
+
import { copyFile as copyFile2, mkdtemp as mkdtemp4, readFile as readFile6, rm as rm5 } from "node:fs/promises";
|
|
11641
11796
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
11642
11797
|
import { dirname as dirname3, join as join10 } from "node:path";
|
|
11643
11798
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -11704,13 +11859,35 @@ function nativeRole(leaf, hint, val, name, a11y, rid, txt) {
|
|
|
11704
11859
|
}
|
|
11705
11860
|
return "group";
|
|
11706
11861
|
}
|
|
11862
|
+
function rootBounds(roots, read) {
|
|
11863
|
+
const first = bounds(read(roots[0] ?? {}, "bounds"));
|
|
11864
|
+
if (first) {
|
|
11865
|
+
return first;
|
|
11866
|
+
}
|
|
11867
|
+
let level = roots;
|
|
11868
|
+
while (level.length > 0) {
|
|
11869
|
+
const boxes = level.map((node) => bounds(read(node, "bounds"))).filter((box) => box !== void 0);
|
|
11870
|
+
if (boxes.length > 0) {
|
|
11871
|
+
const x1 = Math.min(...boxes.map((box) => box.x));
|
|
11872
|
+
const y1 = Math.min(...boxes.map((box) => box.y));
|
|
11873
|
+
const x2 = Math.max(...boxes.map((box) => box.x + box.width));
|
|
11874
|
+
const y2 = Math.max(...boxes.map((box) => box.y + box.height));
|
|
11875
|
+
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
|
11876
|
+
}
|
|
11877
|
+
level = level.flatMap((node) => {
|
|
11878
|
+
const children = read(node, "children");
|
|
11879
|
+
return Array.isArray(children) ? children : [];
|
|
11880
|
+
});
|
|
11881
|
+
}
|
|
11882
|
+
return void 0;
|
|
11883
|
+
}
|
|
11707
11884
|
function flattenHierarchy(schema10) {
|
|
11708
11885
|
const aliases = schema10.ui_schema?.abbreviations ?? {};
|
|
11709
11886
|
const defaults = schema10.ui_schema?.defaults ?? {};
|
|
11710
11887
|
const key = (name) => Object.entries(aliases).find(([, expanded]) => expanded === name)?.[0] ?? name;
|
|
11711
11888
|
const read = (node, name) => node[key(name)] ?? node[name] ?? defaults[key(name)] ?? defaults[name];
|
|
11712
11889
|
const roots = schema10.elements;
|
|
11713
|
-
const root2 =
|
|
11890
|
+
const root2 = rootBounds(roots, read);
|
|
11714
11891
|
if (!root2) {
|
|
11715
11892
|
throw new Error("Maestro hierarchy has no root bounds");
|
|
11716
11893
|
}
|
|
@@ -11948,7 +12125,7 @@ var MaestroDriver = class {
|
|
|
11948
12125
|
await exec3("xcrun", ["simctl", "io", this.deviceId, "screenshot", file]);
|
|
11949
12126
|
return await readFile6(file);
|
|
11950
12127
|
} finally {
|
|
11951
|
-
await
|
|
12128
|
+
await rm5(dir2, { recursive: true, force: true });
|
|
11952
12129
|
}
|
|
11953
12130
|
}
|
|
11954
12131
|
async observe() {
|
|
@@ -12149,7 +12326,7 @@ var MaestroDriver = class {
|
|
|
12149
12326
|
}
|
|
12150
12327
|
} catch (error) {
|
|
12151
12328
|
child.kill("SIGKILL");
|
|
12152
|
-
await
|
|
12329
|
+
await rm5(dir2, { recursive: true, force: true });
|
|
12153
12330
|
throw error;
|
|
12154
12331
|
}
|
|
12155
12332
|
return { child, exited, file };
|
|
@@ -12201,7 +12378,7 @@ var MaestroDriver = class {
|
|
|
12201
12378
|
await within(recording.exited, 3e4, "Stopping the recording");
|
|
12202
12379
|
await copyFile2(recording.file, file);
|
|
12203
12380
|
} finally {
|
|
12204
|
-
await
|
|
12381
|
+
await rm5(dirname3(recording.file), { recursive: true, force: true });
|
|
12205
12382
|
}
|
|
12206
12383
|
return file;
|
|
12207
12384
|
}
|
|
@@ -12224,7 +12401,7 @@ var MaestroDriver = class {
|
|
|
12224
12401
|
});
|
|
12225
12402
|
} else if (recording) {
|
|
12226
12403
|
recording.child.kill("SIGKILL");
|
|
12227
|
-
await
|
|
12404
|
+
await rm5(dirname3(recording.file), { recursive: true, force: true });
|
|
12228
12405
|
}
|
|
12229
12406
|
await this.client?.close();
|
|
12230
12407
|
await this.transport?.close();
|
|
@@ -12475,7 +12652,7 @@ var Control = class {
|
|
|
12475
12652
|
|
|
12476
12653
|
// ../drivers/dist/cua/isolation.js
|
|
12477
12654
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
12478
|
-
import { access, mkdir as mkdir4, mkdtemp as mkdtemp5, rm as
|
|
12655
|
+
import { access, mkdir as mkdir4, mkdtemp as mkdtemp5, rm as rm6 } from "node:fs/promises";
|
|
12479
12656
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
12480
12657
|
import { join as join11 } from "node:path";
|
|
12481
12658
|
import { getDefaultEnvironment, StdioClientTransport as StdioClientTransport2 } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -12541,9 +12718,9 @@ async function isolatedTransport(command, windowManager, width, height) {
|
|
|
12541
12718
|
env,
|
|
12542
12719
|
stderr: "pipe"
|
|
12543
12720
|
});
|
|
12544
|
-
return { transport, directory, remove: () =>
|
|
12721
|
+
return { transport, directory, remove: () => rm6(directory, { recursive: true, force: true }) };
|
|
12545
12722
|
} catch (error) {
|
|
12546
|
-
await
|
|
12723
|
+
await rm6(directory, { recursive: true, force: true });
|
|
12547
12724
|
throw error;
|
|
12548
12725
|
}
|
|
12549
12726
|
}
|
|
@@ -13433,7 +13610,6 @@ async function watchCi(root2, config, deps = {}) {
|
|
|
13433
13610
|
state,
|
|
13434
13611
|
head: fix.commit,
|
|
13435
13612
|
failing: failed2.map((check) => check.name),
|
|
13436
|
-
attempts: fix.ci?.attempts ?? 0,
|
|
13437
13613
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13438
13614
|
};
|
|
13439
13615
|
await workspace.saveFix(fix);
|
|
@@ -13456,7 +13632,7 @@ async function watchCi(root2, config, deps = {}) {
|
|
|
13456
13632
|
deps.onLog?.(`CI: all ${checks.length} check(s) passed on PR #${pr}.`);
|
|
13457
13633
|
break;
|
|
13458
13634
|
}
|
|
13459
|
-
if ((fix.
|
|
13635
|
+
if (ciAttempts(fix.attempts).length >= settings.attempts) {
|
|
13460
13636
|
await save("gave-up");
|
|
13461
13637
|
const problem = `CI: PR #${pr} still fails ${failed2.map((check) => check.name).join(", ")} after ${settings.attempts} fixer attempt(s). A person must look at it.`;
|
|
13462
13638
|
problems.push(problem);
|
|
@@ -13468,12 +13644,21 @@ async function watchCi(root2, config, deps = {}) {
|
|
|
13468
13644
|
deps.onLog?.(`CI: PR #${pr} failed ${failed2.map((check) => check.name).join(", ")}. The fixer is off, so a person must fix it.`);
|
|
13469
13645
|
break;
|
|
13470
13646
|
}
|
|
13647
|
+
try {
|
|
13648
|
+
await access2(fix.worktree);
|
|
13649
|
+
} catch {
|
|
13650
|
+
fix.ci = { ...fix.ci, state: "gave-up" };
|
|
13651
|
+
await workspace.saveFix(fix);
|
|
13652
|
+
const problem = `CI: the worktree of ${fix.issueId} is gone, so the fixer cannot change PR #${pr}. A person must look at it.`;
|
|
13653
|
+
problems.push(problem);
|
|
13654
|
+
deps.onLog?.(problem);
|
|
13655
|
+
break;
|
|
13656
|
+
}
|
|
13471
13657
|
deps.onLog?.(`CI: PR #${pr} failed ${failed2.map((check) => check.name).join(", ")}. The fixer tries to fix it.`);
|
|
13472
13658
|
const pushed = await fixCi(root2, config, fix, failed2, await failureLog(gh, repo, failed2), deps);
|
|
13473
|
-
fix.ci = { ...fix.ci, attempts: (fix.ci?.attempts ?? 0) + 1 };
|
|
13474
13659
|
await workspace.saveFix(fix);
|
|
13475
13660
|
if (!pushed) {
|
|
13476
|
-
if (fix.
|
|
13661
|
+
if (ciAttempts(fix.attempts).length >= settings.attempts) {
|
|
13477
13662
|
const problem = `CI: the fixer could not fix PR #${pr}. A person must look at it.`;
|
|
13478
13663
|
fix.ci = { ...fix.ci, state: "gave-up" };
|
|
13479
13664
|
await workspace.saveFix(fix);
|
|
@@ -13494,12 +13679,6 @@ async function watchCi(root2, config, deps = {}) {
|
|
|
13494
13679
|
return { problems };
|
|
13495
13680
|
}
|
|
13496
13681
|
async function fixCi(root2, config, fix, failed2, log, deps) {
|
|
13497
|
-
try {
|
|
13498
|
-
await access2(fix.worktree);
|
|
13499
|
-
} catch {
|
|
13500
|
-
deps.onLog?.(`CI: the worktree of ${fix.issueId} is gone, so the fixer cannot change PR #${fix.pr.number}.`);
|
|
13501
|
-
return false;
|
|
13502
|
-
}
|
|
13503
13682
|
const workspace = new Workspace(root2);
|
|
13504
13683
|
const record = await workspace.startSession("fixer");
|
|
13505
13684
|
const session = new AgentSession(root2, config, new Vars(config.app.secrets), record.id, "fixer", void 0, deps.onLog);
|
|
@@ -13511,6 +13690,20 @@ async function fixCi(root2, config, fix, failed2, log, deps) {
|
|
|
13511
13690
|
});
|
|
13512
13691
|
let summary = "";
|
|
13513
13692
|
let status = "failed";
|
|
13693
|
+
const unfinished2 = fix.attempts?.at(-1);
|
|
13694
|
+
if (unfinished2 && !unfinished2.outcome) {
|
|
13695
|
+
unfinished2.outcome = "abandoned";
|
|
13696
|
+
unfinished2.reason = "The fixer was stopped before it finished.";
|
|
13697
|
+
unfinished2.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13698
|
+
}
|
|
13699
|
+
const attempt = {
|
|
13700
|
+
n: (fix.attempts?.at(-1)?.n ?? 0) + 1,
|
|
13701
|
+
kind: "ci",
|
|
13702
|
+
sessionId: session.sessionId,
|
|
13703
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13704
|
+
};
|
|
13705
|
+
fix.attempts = [...fix.attempts ?? [], attempt];
|
|
13706
|
+
await workspace.saveFix(fix);
|
|
13514
13707
|
try {
|
|
13515
13708
|
const outcome = await runtime.run({
|
|
13516
13709
|
role: "fixer",
|
|
@@ -13528,20 +13721,31 @@ async function fixCi(root2, config, fix, failed2, log, deps) {
|
|
|
13528
13721
|
timeoutMs: fixer.timeoutMs
|
|
13529
13722
|
}, session.emit);
|
|
13530
13723
|
summary = outcome.summary ?? "";
|
|
13531
|
-
|
|
13724
|
+
attempt.costUsd = outcome.costUsd;
|
|
13725
|
+
if (outcome.stop !== "done") {
|
|
13726
|
+
attempt.outcome = outcome.stop === "timeout" ? "timeout" : "error";
|
|
13532
13727
|
throw new Error(outcome.error ?? `Fixer stopped: ${outcome.stop}`);
|
|
13728
|
+
}
|
|
13533
13729
|
if (!await git3(fix.worktree, "status", "--porcelain")) {
|
|
13534
13730
|
summary = summary || "The fixer made no change.";
|
|
13731
|
+
attempt.outcome = "no-change";
|
|
13535
13732
|
session.emit({ kind: "fix", summary: `No change for CI on PR #${fix.pr.number}: ${summary.split("\n")[0]}` });
|
|
13536
13733
|
status = "finished";
|
|
13537
13734
|
return false;
|
|
13538
13735
|
}
|
|
13539
13736
|
if (fixer.verify) {
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
13737
|
+
try {
|
|
13738
|
+
await promisify6(execFile6)("/bin/sh", ["-c", fixer.verify], {
|
|
13739
|
+
cwd: fix.worktree,
|
|
13740
|
+
timeout: 3e5,
|
|
13741
|
+
maxBuffer: 4 * 1024 * 1024
|
|
13742
|
+
});
|
|
13743
|
+
} catch (error) {
|
|
13744
|
+
const output2 = error;
|
|
13745
|
+
attempt.outcome = "verify-failed";
|
|
13746
|
+
attempt.verifyOutput = (`${output2.stdout ?? ""}${output2.stderr ?? ""}`.trim() || output2.message).slice(-4e3);
|
|
13747
|
+
throw error;
|
|
13748
|
+
}
|
|
13545
13749
|
}
|
|
13546
13750
|
await git3(fix.worktree, "add", "-A");
|
|
13547
13751
|
const committed = await commitFix(fix.worktree, "pass the CI checks", fixer.commitMessage);
|
|
@@ -13550,20 +13754,31 @@ async function fixCi(root2, config, fix, failed2, log, deps) {
|
|
|
13550
13754
|
fix.commit = await git3(fix.worktree, "rev-parse", "HEAD");
|
|
13551
13755
|
await git3(fix.worktree, "push", "origin", fix.branch);
|
|
13552
13756
|
session.emit({ kind: "fix", summary: `Pushed a CI fix to PR #${fix.pr.number} (${fix.commit.slice(0, 7)})` });
|
|
13757
|
+
attempt.outcome = "proposed";
|
|
13553
13758
|
status = "finished";
|
|
13554
13759
|
return true;
|
|
13555
13760
|
} catch (error) {
|
|
13556
13761
|
summary = String(error).slice(0, 500);
|
|
13762
|
+
attempt.outcome ??= "error";
|
|
13557
13763
|
session.emit({ kind: "error", summary: `CI fix failed on PR #${fix.pr.number}: ${summary}` });
|
|
13558
13764
|
return false;
|
|
13559
13765
|
} finally {
|
|
13766
|
+
attempt.reason = summary || void 0;
|
|
13767
|
+
await closeAttempt(workspace, fix.id, attempt, fix.worktree, fix.repo);
|
|
13560
13768
|
await workspace.endSession(session.sessionId, { status, summary });
|
|
13561
13769
|
await session.idle();
|
|
13562
13770
|
}
|
|
13563
13771
|
}
|
|
13564
13772
|
|
|
13565
|
-
// ../agents/dist/
|
|
13566
|
-
import {
|
|
13773
|
+
// ../agents/dist/guide.js
|
|
13774
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
13775
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
13776
|
+
import { relative as relative4 } from "node:path";
|
|
13777
|
+
async function readGuide(root2, config) {
|
|
13778
|
+
const { path, required } = instructionsPath(root2, config.app.instructions);
|
|
13779
|
+
return required || existsSync5(path) ? { path, text: await readFile7(path, "utf8") } : { path };
|
|
13780
|
+
}
|
|
13781
|
+
var noGuide = (root2, guide) => `No app guide at ${relative4(root2, guide.path)}, so the explorer runs without one.`;
|
|
13567
13782
|
|
|
13568
13783
|
// ../agents/dist/tools/explorer.js
|
|
13569
13784
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -14442,7 +14657,6 @@ Expected: ${arg(input, "expected")}`),
|
|
|
14442
14657
|
}
|
|
14443
14658
|
|
|
14444
14659
|
// ../agents/dist/roles/reflect.js
|
|
14445
|
-
import { readFile as readFile7 } from "node:fs/promises";
|
|
14446
14660
|
import { stripVTControlCharacters } from "node:util";
|
|
14447
14661
|
var schema2 = (properties = {}, required = []) => ({
|
|
14448
14662
|
type: "object",
|
|
@@ -14505,8 +14719,7 @@ async function reflectOnSession(root2, config, sessionId, deps = {}) {
|
|
|
14505
14719
|
const existing = lessonsFor(memory, "explorer");
|
|
14506
14720
|
const ids = new Set(existing.map((item) => item.id));
|
|
14507
14721
|
const vars = deps.vars ?? new Vars(config.app.secrets);
|
|
14508
|
-
const
|
|
14509
|
-
const guide = file ? await readFile7(file, "utf8") : "";
|
|
14722
|
+
const guide = (await readGuide(root2, config)).text ?? "";
|
|
14510
14723
|
const record = await workspace.startSession("explorer");
|
|
14511
14724
|
const emit = workspace.recordEvent(record.id, "explorer", vars);
|
|
14512
14725
|
let added = 0;
|
|
@@ -14515,14 +14728,17 @@ async function reflectOnSession(root2, config, sessionId, deps = {}) {
|
|
|
14515
14728
|
const tools = [
|
|
14516
14729
|
{
|
|
14517
14730
|
name: "add_lesson",
|
|
14518
|
-
description:
|
|
14731
|
+
description: `Save one specific imperative lesson of at most ${LESSON_MAX_LENGTH} characters.`,
|
|
14519
14732
|
inputSchema: schema2({ text: string2, scope: string2 }, ["text"]),
|
|
14520
14733
|
async run(input) {
|
|
14521
14734
|
if (added >= config.agents.memory.maxPerSession)
|
|
14522
14735
|
return reply("Lesson limit reached.");
|
|
14523
|
-
const text3 = vars.redact(String(input.text ?? "")).trim()
|
|
14736
|
+
const text3 = vars.redact(String(input.text ?? "")).trim();
|
|
14524
14737
|
if (!text3)
|
|
14525
14738
|
return reply("Lesson text is required.");
|
|
14739
|
+
const tooLong = lessonTooLong(text3);
|
|
14740
|
+
if (tooLong)
|
|
14741
|
+
return { ...reply(tooLong), isError: true };
|
|
14526
14742
|
const [lesson] = await workspace.upsertLessons([
|
|
14527
14743
|
{ role: "explorer", text: text3, scope: input.scope ? String(input.scope) : void 0, source: "reflection" }
|
|
14528
14744
|
]);
|
|
@@ -14568,7 +14784,7 @@ async function reflectOnSession(root2, config, sessionId, deps = {}) {
|
|
|
14568
14784
|
outcome = await (deps.createRuntime ?? createRuntime)(config.agents.explorer.use).run({
|
|
14569
14785
|
role: "explorer",
|
|
14570
14786
|
sessionId: record.id,
|
|
14571
|
-
system: `You review a QA explorer's run of this app. Write lessons that would have saved steps or avoided a mistake. Each lesson is ONE imperative sentence a future explorer can follow: what to do and where. Be specific to this app: name the screen, field, or control. Never include a secret value; placeholders such as {{E2E_RUN_ID}} are fine. Do not restate the app guide. Add at most ${config.agents.memory.maxPerSession} lessons. If an existing lesson already says it, call keep_lesson with its id. If one was followed and still failed, or is wrong, call retire_lesson with a reason. Then call finish.`,
|
|
14787
|
+
system: `You review a QA explorer's run of this app. Write lessons that would have saved steps or avoided a mistake. Each lesson is ONE imperative sentence of at most ${LESSON_MAX_LENGTH} characters that a future explorer can follow: what to do and where. Be specific to this app: name the screen, field, or control. Never include a secret value; placeholders such as {{E2E_RUN_ID}} are fine. Do not restate the app guide. Add at most ${config.agents.memory.maxPerSession} lessons. If an existing lesson already says it, call keep_lesson with its id. If one was followed and still failed, or is wrong, call retire_lesson with a reason. Then call finish.`,
|
|
14572
14788
|
prompt: `TROUBLE LOG
|
|
14573
14789
|
${lines.join("\n")}
|
|
14574
14790
|
|
|
@@ -14623,12 +14839,13 @@ async function runExplorer(session, runtime, opts = {}) {
|
|
|
14623
14839
|
const config = session.config.agents.explorer;
|
|
14624
14840
|
const map = await session.workspace.readAppMap();
|
|
14625
14841
|
const routines = await session.workspace.listRoutines();
|
|
14626
|
-
const guide =
|
|
14627
|
-
|
|
14842
|
+
const guide = await readGuide(session.root, session.config);
|
|
14843
|
+
if (guide.text === void 0)
|
|
14844
|
+
session.emit({ kind: "setup", summary: noGuide(session.root, guide) });
|
|
14628
14845
|
const task = {
|
|
14629
14846
|
role: "explorer",
|
|
14630
14847
|
sessionId: session.sessionId,
|
|
14631
|
-
system: explorerSystem(session.config.app.platform,
|
|
14848
|
+
system: explorerSystem(session.config.app.platform, guide.text ?? "", lessonsFor(await session.workspace.readMemory(), "explorer"), session.config.agents.checks),
|
|
14632
14849
|
prompt: explorerPrompt({
|
|
14633
14850
|
goal: opts.goal,
|
|
14634
14851
|
screens: map?.screens ?? [],
|
|
@@ -14673,7 +14890,7 @@ async function runExplorer(session, runtime, opts = {}) {
|
|
|
14673
14890
|
}
|
|
14674
14891
|
|
|
14675
14892
|
// ../agents/dist/tools/judge.js
|
|
14676
|
-
import { appendFile as appendFile2, copyFile as copyFile3, readFile as
|
|
14893
|
+
import { appendFile as appendFile2, copyFile as copyFile3, readFile as readFile8 } from "node:fs/promises";
|
|
14677
14894
|
import { join as join12, resolve as resolve8 } from "node:path";
|
|
14678
14895
|
var response = (value) => ({ content: [{ type: "text", text: value }] });
|
|
14679
14896
|
var schema3 = (properties, required = []) => ({
|
|
@@ -14698,7 +14915,7 @@ function issueTitle(value) {
|
|
|
14698
14915
|
}
|
|
14699
14916
|
async function decisions(session, sessionId) {
|
|
14700
14917
|
try {
|
|
14701
|
-
const lines = await
|
|
14918
|
+
const lines = await readFile8(join12(paths.session(session.root, sessionId), "decisions.jsonl"), "utf8");
|
|
14702
14919
|
return new Set(lines.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line).candidateId));
|
|
14703
14920
|
} catch (error) {
|
|
14704
14921
|
if (error.code === "ENOENT")
|
|
@@ -14743,7 +14960,7 @@ async function image(session, file) {
|
|
|
14743
14960
|
if (!file)
|
|
14744
14961
|
return [];
|
|
14745
14962
|
try {
|
|
14746
|
-
return [{ type: "image", png: await
|
|
14963
|
+
return [{ type: "image", png: await readFile8(resolve8(session.root, file)) }];
|
|
14747
14964
|
} catch {
|
|
14748
14965
|
return [];
|
|
14749
14966
|
}
|
|
@@ -14929,7 +15146,7 @@ function judgeTools(session, sessionIds, runtimeLabel) {
|
|
|
14929
15146
|
role: "explorer",
|
|
14930
15147
|
source: "dismissal",
|
|
14931
15148
|
scope: candidate.screenId,
|
|
14932
|
-
text: `Do not report: ${candidate.summary} \u2014 ${String(input.reason)}
|
|
15149
|
+
text: fitLesson(`Do not report: ${candidate.summary} \u2014 ${String(input.reason)}`)
|
|
14933
15150
|
}
|
|
14934
15151
|
]);
|
|
14935
15152
|
}
|
|
@@ -14993,7 +15210,7 @@ async function runJudge(session, runtime, opts) {
|
|
|
14993
15210
|
}
|
|
14994
15211
|
|
|
14995
15212
|
// ../agents/dist/roles/publish.js
|
|
14996
|
-
import { mkdir as mkdir5, readFile as
|
|
15213
|
+
import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
|
|
14997
15214
|
import { join as join13, resolve as resolve9 } from "node:path";
|
|
14998
15215
|
|
|
14999
15216
|
// ../agents/dist/pr-title.js
|
|
@@ -15127,7 +15344,7 @@ async function image2(root2, path) {
|
|
|
15127
15344
|
if (!path)
|
|
15128
15345
|
return [];
|
|
15129
15346
|
try {
|
|
15130
|
-
return [{ type: "image", png: await
|
|
15347
|
+
return [{ type: "image", png: await readFile9(resolve9(root2, path)) }];
|
|
15131
15348
|
} catch {
|
|
15132
15349
|
return [];
|
|
15133
15350
|
}
|
|
@@ -15461,12 +15678,11 @@ ${items.map((item) => `${item.issue.id}: ${item.issue.title}`).join("\n")}`),
|
|
|
15461
15678
|
|
|
15462
15679
|
// ../agents/dist/roles/retest.js
|
|
15463
15680
|
import { execFile as execFile7 } from "node:child_process";
|
|
15464
|
-
import { readFile as readFile13 } from "node:fs/promises";
|
|
15465
15681
|
import { resolve as resolve11 } from "node:path";
|
|
15466
15682
|
import { promisify as promisify7 } from "node:util";
|
|
15467
15683
|
|
|
15468
15684
|
// ../agents/dist/roles/capture.js
|
|
15469
|
-
import { readFile as
|
|
15685
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
15470
15686
|
import { resolve as resolve10 } from "node:path";
|
|
15471
15687
|
var schema5 = (properties = {}, required = []) => ({
|
|
15472
15688
|
type: "object",
|
|
@@ -15480,7 +15696,7 @@ async function image3(root2, file) {
|
|
|
15480
15696
|
if (!file)
|
|
15481
15697
|
return [];
|
|
15482
15698
|
try {
|
|
15483
|
-
return [{ type: "image", png: await
|
|
15699
|
+
return [{ type: "image", png: await readFile10(resolve10(root2, file)) }];
|
|
15484
15700
|
} catch {
|
|
15485
15701
|
return [];
|
|
15486
15702
|
}
|
|
@@ -15619,8 +15835,8 @@ async function captureTargets(session, runtime, targets, task) {
|
|
|
15619
15835
|
}
|
|
15620
15836
|
|
|
15621
15837
|
// ../agents/dist/roles/overlay.js
|
|
15622
|
-
import { mkdir as mkdir6, readdir as readdir3, readFile as
|
|
15623
|
-
import { dirname as dirname4, join as join14, relative as
|
|
15838
|
+
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile11, rm as rm7, writeFile as writeFile9 } from "node:fs/promises";
|
|
15839
|
+
import { dirname as dirname4, join as join14, relative as relative5, sep as sep2 } from "node:path";
|
|
15624
15840
|
async function files(dir2, skip) {
|
|
15625
15841
|
const out = [];
|
|
15626
15842
|
let entries;
|
|
@@ -15641,7 +15857,7 @@ async function files(dir2, skip) {
|
|
|
15641
15857
|
return out;
|
|
15642
15858
|
}
|
|
15643
15859
|
async function overlayBugpatrol(root2, source, worktree) {
|
|
15644
|
-
const where =
|
|
15860
|
+
const where = relative5(source, root2);
|
|
15645
15861
|
if (where.startsWith("..") || where.split(sep2)[0] === "..")
|
|
15646
15862
|
return async () => {
|
|
15647
15863
|
};
|
|
@@ -15649,11 +15865,11 @@ async function overlayBugpatrol(root2, source, worktree) {
|
|
|
15649
15865
|
const saved = [];
|
|
15650
15866
|
const skip = [join14(from, DATA_DIR), paths.routines(root2), paths.appMap(root2)];
|
|
15651
15867
|
for (const file of await files(from, skip)) {
|
|
15652
|
-
const target = join14(worktree, where,
|
|
15653
|
-
const content = await
|
|
15868
|
+
const target = join14(worktree, where, relative5(root2, file));
|
|
15869
|
+
const content = await readFile11(file);
|
|
15654
15870
|
let before;
|
|
15655
15871
|
try {
|
|
15656
|
-
before = await
|
|
15872
|
+
before = await readFile11(target);
|
|
15657
15873
|
} catch {
|
|
15658
15874
|
before = void 0;
|
|
15659
15875
|
}
|
|
@@ -15668,7 +15884,7 @@ async function overlayBugpatrol(root2, source, worktree) {
|
|
|
15668
15884
|
if (item.content)
|
|
15669
15885
|
await writeFile9(item.path, item.content);
|
|
15670
15886
|
else
|
|
15671
|
-
await
|
|
15887
|
+
await rm7(item.path, { force: true });
|
|
15672
15888
|
}
|
|
15673
15889
|
};
|
|
15674
15890
|
}
|
|
@@ -15726,6 +15942,7 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
15726
15942
|
const targets = await loadRetestTargets(workspace, issue);
|
|
15727
15943
|
const retest = {
|
|
15728
15944
|
attempt,
|
|
15945
|
+
fixAttempt: [...fix.attempts ?? []].reverse().find((item) => item.outcome === "proposed")?.n,
|
|
15729
15946
|
outcome: "unclear",
|
|
15730
15947
|
reason: "",
|
|
15731
15948
|
shots: targets.map((target) => target.shot),
|
|
@@ -15775,8 +15992,7 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
15775
15992
|
session2.emit({ kind: "session-start", summary: `Retesting the fix for: ${issue.title}` });
|
|
15776
15993
|
let outcome;
|
|
15777
15994
|
try {
|
|
15778
|
-
const
|
|
15779
|
-
const instructions = guide ? await readFile13(resolve11(root2, guide), "utf8") : "";
|
|
15995
|
+
const instructions = (await readGuide(root2, config)).text ?? "";
|
|
15780
15996
|
outcome = await captureTargets(session2, explorerRuntime, targets, {
|
|
15781
15997
|
replay: { save: false, onFixBuild: !options.build },
|
|
15782
15998
|
system: explorerRetestSystem(config.app.platform, instructions, lessonsFor(await workspace.readMemory(), "explorer")).replace("a build with a proposed fix", options.build === "main" ? "the main build after a merged fix" : "a build with a proposed fix"),
|
|
@@ -15911,12 +16127,15 @@ function needsRetest(fix) {
|
|
|
15911
16127
|
}
|
|
15912
16128
|
function applyRetest(config, fix, result3) {
|
|
15913
16129
|
fix.retests = [...fix.retests ?? [], result3];
|
|
15914
|
-
const
|
|
16130
|
+
const retestsLeft = judgedRetests(fix.retests).length < config.agents.fixer.retest.attempts;
|
|
16131
|
+
const refixLeft = fixerAttempts(fix.attempts).length < config.agents.fixer.attempts;
|
|
15915
16132
|
if (result3.outcome === "fixed")
|
|
15916
16133
|
fix.status = "verified";
|
|
15917
16134
|
else if (result3.outcome === "error")
|
|
15918
16135
|
fix.status = "retesting";
|
|
15919
|
-
else if (
|
|
16136
|
+
else if (result3.outcome === "not-fixed" && retestsLeft && refixLeft)
|
|
16137
|
+
fix.status = "retesting";
|
|
16138
|
+
else if (result3.outcome === "unclear" && retestsLeft)
|
|
15920
16139
|
fix.status = "retesting";
|
|
15921
16140
|
else
|
|
15922
16141
|
fix.status = "proposed";
|
|
@@ -16165,7 +16384,7 @@ async function git5(cwd, ...args) {
|
|
|
16165
16384
|
return (await exec6("git", args, { cwd })).stdout.trim();
|
|
16166
16385
|
}
|
|
16167
16386
|
async function dirty(root2, source) {
|
|
16168
|
-
const own = [paths.routines(root2), paths.appMap(root2)].map((path) =>
|
|
16387
|
+
const own = [paths.routines(root2), paths.appMap(root2)].map((path) => relative6(source, path)).filter((path) => !path.startsWith(".."));
|
|
16169
16388
|
const exclude = own.map((path) => `:(exclude)${path}`);
|
|
16170
16389
|
return Boolean(await git5(source, "status", "--porcelain", "--untracked-files=no", "--", ".", ...exclude));
|
|
16171
16390
|
}
|
|
@@ -16390,22 +16609,26 @@ async function runPatrol(options) {
|
|
|
16390
16609
|
}
|
|
16391
16610
|
|
|
16392
16611
|
// ../agents/dist/roles/promote.js
|
|
16393
|
-
import { readFile as
|
|
16612
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
16394
16613
|
import { resolve as resolve14 } from "node:path";
|
|
16395
16614
|
|
|
16396
16615
|
// ../agents/dist/review-comment.js
|
|
16397
|
-
|
|
16616
|
+
function reviewMarker(name) {
|
|
16617
|
+
return name ? `<!-- bugpatrol:review:${name} -->` : "<!-- bugpatrol:review -->";
|
|
16618
|
+
}
|
|
16619
|
+
var REVIEW_MARKER = reviewMarker();
|
|
16398
16620
|
var SUPERSEDED_MARKER = "<!-- bugpatrol:superseded -->";
|
|
16399
16621
|
var rank2 = { cosmetic: 0, minor: 1, major: 2, critical: 3 };
|
|
16400
16622
|
var short = (commit) => commit.slice(0, 7);
|
|
16401
16623
|
var stoppedAt = (replay) => `stopped at step ${(replay?.failedStep ?? 0) + 1}`;
|
|
16402
|
-
function details(summary, lines) {
|
|
16403
|
-
return lines.length ? [`<details><summary>${summary}</summary>
|
|
16624
|
+
function details(summary, lines, open3 = false) {
|
|
16625
|
+
return lines.length ? [`<details${open3 ? " open" : ""}><summary>${summary}</summary>
|
|
16404
16626
|
|
|
16405
16627
|
${lines.join("\n")}
|
|
16406
16628
|
|
|
16407
16629
|
</details>`] : [];
|
|
16408
16630
|
}
|
|
16631
|
+
var cell = (text3) => text3.replaceAll("|", "\\|").replace(/\s*\n\s*/g, " ");
|
|
16409
16632
|
function numberDiff(diff2) {
|
|
16410
16633
|
const lines = /* @__PURE__ */ new Map();
|
|
16411
16634
|
const text3 = [];
|
|
@@ -16484,9 +16707,7 @@ function claimLines(review, image4, url) {
|
|
|
16484
16707
|
return [];
|
|
16485
16708
|
const head = `\`${short(review.head)}\``;
|
|
16486
16709
|
const base = `\`${short(review.base)}\``;
|
|
16487
|
-
const
|
|
16488
|
-
const untested = claims.filter((finding) => finding.verdict === "untested");
|
|
16489
|
-
const cell = (path, fallback) => image4(path) ?? `_${fallback}_`;
|
|
16710
|
+
const shot = (path, fallback) => image4(path) ?? `_${fallback}_`;
|
|
16490
16711
|
const stopped = (finding, build) => {
|
|
16491
16712
|
const replay = finding[build];
|
|
16492
16713
|
return replay && !replay.ok ? `The replay ${stoppedAt(replay)}.` : "No screenshot.";
|
|
@@ -16495,18 +16716,18 @@ function claimLines(review, image4, url) {
|
|
|
16495
16716
|
const recording = finding[build]?.recording;
|
|
16496
16717
|
const video = recording && url(recording.file);
|
|
16497
16718
|
const label = recording?.file.endsWith(".cast") ? "Terminal recording" : "Full video";
|
|
16498
|
-
return
|
|
16719
|
+
return shot(recording?.gif ?? finding[build]?.shots.at(-1), stopped(finding, build)) + (video ? `<br><a href="${video}">${label}</a>` : recording ? `<br>${label}: \`${recording.file}\` in the run, and in the dashboard` : "");
|
|
16499
16720
|
};
|
|
16500
16721
|
const checks = (finding) => {
|
|
16501
16722
|
const results = finding.head?.assertions;
|
|
16502
16723
|
if (!results?.length)
|
|
16503
16724
|
return [];
|
|
16504
16725
|
const cellText = (text3) => text3.replaceAll("|", "\\|");
|
|
16505
|
-
const result3 = (
|
|
16726
|
+
const result3 = (item) => !item ? "-" : item.ok ? "Passed" : item.actual !== void 0 ? `Failed, ${assertionWords(item, true)}` : "Failed";
|
|
16506
16727
|
return [
|
|
16507
16728
|
`| Check | Base ${base} | This pull request ${head} |
|
|
16508
16729
|
| --- | --- | --- |
|
|
16509
|
-
` + results.map((
|
|
16730
|
+
` + results.map((item, index) => `| ${cellText(assertionWords(item))} | ${cellText(result3(finding.base?.assertions?.[index]))} | ${cellText(result3(item))} |`).join("\n")
|
|
16510
16731
|
];
|
|
16511
16732
|
};
|
|
16512
16733
|
const compared = (finding) => {
|
|
@@ -16529,8 +16750,7 @@ ${fence}`;
|
|
|
16529
16750
|
const replayed2 = Boolean(finding.head);
|
|
16530
16751
|
const steps = finding.steps ?? [];
|
|
16531
16752
|
return [
|
|
16532
|
-
|
|
16533
|
-
`\`${finding.verdict}\` \xB7 From ${sourceWords(finding.claim.source)}.` + (finding.evidence ? ` Evidence: ${finding.evidence === "explored" && replayed2 ? judgedReplay : evidenceWords[finding.evidence]}.` : ""),
|
|
16753
|
+
`From ${sourceWords(finding.claim.source)}.` + (finding.evidence ? ` Evidence: ${finding.evidence === "explored" && replayed2 ? judgedReplay : evidenceWords[finding.evidence]}.` : ""),
|
|
16534
16754
|
finding.reason,
|
|
16535
16755
|
// With a replay, the screenshots show the rest.
|
|
16536
16756
|
...finding.saw && (finding.verdict === "not-proven" || !replayed2) ? [`Bugpatrol saw: ${finding.saw}`] : [],
|
|
@@ -16547,21 +16767,36 @@ ${fence}`;
|
|
|
16547
16767
|
...details("Each step", recorded(finding) ? [] : [
|
|
16548
16768
|
`| Step | Base ${base} | This pull request ${head} |
|
|
16549
16769
|
| --- | --- | --- |`,
|
|
16550
|
-
...["Start", ...steps].map((step, index) => `| ${index ? `${index}. ${step}` : step} | ${
|
|
16770
|
+
...["Start", ...steps].map((step, index) => `| ${index ? `${index}. ${step}` : step} | ${shot(finding.base?.shots[index], "-")} | ${shot(finding.head?.shots[index], "-")} |`)
|
|
16551
16771
|
])
|
|
16552
16772
|
] : []
|
|
16553
16773
|
].join("\n\n");
|
|
16554
16774
|
};
|
|
16555
|
-
|
|
16775
|
+
if (!claims.length)
|
|
16776
|
+
return ["#### Claims (0)", "Bugpatrol found no claim in the pull request."];
|
|
16777
|
+
const order = ["not-proven", "partly-proven", "proven", "untested"];
|
|
16778
|
+
const sorted = [...claims].sort((a, b) => order.indexOf(a.verdict) - order.indexOf(b.verdict));
|
|
16779
|
+
const counts = ["proven", "partly-proven", "not-proven", "untested"].map((verdict) => [verdict, claims.filter((finding) => finding.verdict === verdict).length]).filter(([, count]) => count).map(([verdict, count]) => `${count} ${verdictWords[verdict].toLowerCase()}`);
|
|
16780
|
+
const evidence = (finding) => finding.evidence === "explored" && finding.head ? "replay, judged" : finding.evidence ? evidenceLabel[finding.evidence] : "";
|
|
16556
16781
|
return [
|
|
16557
|
-
`####
|
|
16558
|
-
|
|
16559
|
-
...
|
|
16560
|
-
...untested.length ? [`#### Claims that Bugpatrol could not test (${untested.length})`, untested.map(item).join("\n")] : [],
|
|
16561
|
-
"#### Problems that this pull request introduces"
|
|
16782
|
+
`#### Claims (${claims.length}): ${counts.join(" \xB7 ")}`,
|
|
16783
|
+
"| Claim | Verdict | Evidence |\n| --- | --- | --- |\n" + sorted.map((finding) => finding.verdict === "untested" ? `| ${cell(finding.claim.text)} | Untested: ${cell(finding.reason)} | |` : `| ${cell(finding.claim.text)} | ${verdictWords[finding.verdict]} | ${evidence(finding)} |`).join("\n"),
|
|
16784
|
+
...sorted.filter((finding) => finding.verdict !== "untested").flatMap((finding) => details(`${verdictWords[finding.verdict]}: ${finding.claim.text}`, [section(finding)], finding.verdict !== "proven"))
|
|
16562
16785
|
];
|
|
16563
16786
|
}
|
|
16564
|
-
|
|
16787
|
+
var verdictWords = {
|
|
16788
|
+
proven: "Proven",
|
|
16789
|
+
"partly-proven": "Partly proven",
|
|
16790
|
+
"not-proven": "Not proven",
|
|
16791
|
+
untested: "Untested"
|
|
16792
|
+
};
|
|
16793
|
+
var evidenceLabel = {
|
|
16794
|
+
replay: "replay",
|
|
16795
|
+
assertion: "exact check",
|
|
16796
|
+
explored: "explored",
|
|
16797
|
+
bench: "benchmark"
|
|
16798
|
+
};
|
|
16799
|
+
function renderReview(review, imageUrl, inDiff, name) {
|
|
16565
16800
|
const of = (verdict) => review.findings.filter((finding) => finding.verdict === verdict).sort((a, b) => rank2[b.severity] - rank2[a.severity]);
|
|
16566
16801
|
const introduced = of("introduced");
|
|
16567
16802
|
const onLine = (finding) => Boolean(finding.file && finding.line && inDiff(finding.file, finding.line));
|
|
@@ -16576,32 +16811,59 @@ function renderReview(review, imageUrl, inDiff) {
|
|
|
16576
16811
|
heading,
|
|
16577
16812
|
`**${finding.severity}**${finding.screenId ? ` \xB7 screen \`${finding.screenId}\`` : ""}`,
|
|
16578
16813
|
finding.reason,
|
|
16814
|
+
...shots(finding)
|
|
16815
|
+
].join("\n\n");
|
|
16816
|
+
const shots = (finding) => [
|
|
16579
16817
|
`| Base ${base} | This pull request ${head} |
|
|
16580
16818
|
| --- | --- |
|
|
16581
16819
|
| ${image4(finding.base) ?? `_${finding.baseNote ?? "No screenshot."}_`} | ${image4(finding.head) ?? "_No screenshot._"} |`,
|
|
16582
16820
|
...details("Steps", finding.steps.map((step, number) => `${number + 1}. ${step}`))
|
|
16583
|
-
].join("\n\n");
|
|
16584
|
-
const line = (finding) => `- **${finding.title}** (${finding.severity}): ${finding.reason}`;
|
|
16585
|
-
const where = located.length === 0 ? [] : located.length === introduced.length ? ["Each one is a comment on the changed line that causes it."] : [
|
|
16586
|
-
`${located.length} of them ${located.length === 1 ? "is a comment on the changed line that causes it" : "are comments on the changed lines that cause them"}. The others are below.`
|
|
16587
16821
|
];
|
|
16822
|
+
const problem = (finding, index) => {
|
|
16823
|
+
const where = onLine(finding) ? `on \`${finding.file}:${finding.line}\` (line comment)` : finding.file && finding.line ? `\`${finding.file}:${finding.line}\`` : finding.screenId ? `screen \`${finding.screenId}\`` : "";
|
|
16824
|
+
const headline = [`**${index + 1}. ${finding.title}**`, finding.severity, where].filter(Boolean).join(" \xB7 ");
|
|
16825
|
+
if (onLine(finding))
|
|
16826
|
+
return headline;
|
|
16827
|
+
return [
|
|
16828
|
+
headline,
|
|
16829
|
+
finding.reason,
|
|
16830
|
+
...details("Screenshots and steps", [
|
|
16831
|
+
shots(finding)[0],
|
|
16832
|
+
...finding.steps.map((step, number) => `${number + 1}. ${step}`)
|
|
16833
|
+
])
|
|
16834
|
+
].join("\n\n");
|
|
16835
|
+
};
|
|
16836
|
+
const others = [
|
|
16837
|
+
...of("pre-existing").map((finding) => [finding, `Already on \`${review.baseRef}\``]),
|
|
16838
|
+
...of("unclear").map((finding) => [finding, "Could not compare"]),
|
|
16839
|
+
...of("not-a-bug").map((finding) => [finding, "Not a bug"])
|
|
16840
|
+
];
|
|
16841
|
+
const otherCounts = [
|
|
16842
|
+
[of("pre-existing").length, `already on \`${review.baseRef}\``],
|
|
16843
|
+
[of("unclear").length, "could not compare"],
|
|
16844
|
+
[of("not-a-bug").length, "not a bug"]
|
|
16845
|
+
].filter(([count]) => count).map(([count, words2]) => `${count} ${words2}`);
|
|
16846
|
+
const coverage = review.coverage;
|
|
16847
|
+
const hint = nextRunHint(review);
|
|
16588
16848
|
const body2 = [
|
|
16589
|
-
|
|
16590
|
-
"### Bugpatrol review",
|
|
16849
|
+
reviewMarker(name),
|
|
16850
|
+
name ? `### Bugpatrol review: ${name}` : "### Bugpatrol review",
|
|
16851
|
+
`**${introduced.length ? `${introduced.length} ${introduced.length === 1 ? "problem" : "problems"}` : "No problem"} introduced** \xB7 tested ${head} against \`${review.baseRef}\` ${base}`,
|
|
16852
|
+
...hint ? [`> **Next run:** ${hint}`] : [],
|
|
16853
|
+
...introduced.length ? [`#### Problems this pull request introduces (${introduced.length})`, ...introduced.map(problem)] : [],
|
|
16591
16854
|
...claimLines(review, image4, imageUrl),
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
]
|
|
16599
|
-
...details(`
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
`<sub>Bugpatrol tested only what the diff can affect, not the whole app. This review does not block the merge.</sub>`
|
|
16855
|
+
...coverage ? [
|
|
16856
|
+
...coverage.tested.length ? ["#### Tested", coverage.tested.map((line) => `- ${line}`).join("\n")] : [],
|
|
16857
|
+
...coverage.untested.length ? [
|
|
16858
|
+
"#### Not tested",
|
|
16859
|
+
coverage.untested.map((item) => `- ${item.what}${item.why ? `: ${item.why}` : ""}`).join("\n")
|
|
16860
|
+
] : []
|
|
16861
|
+
] : review.tested ? ["#### Tested", review.tested] : [],
|
|
16862
|
+
...others.length ? details(`Other findings (${others.length}): ${otherCounts.join(" \xB7 ")}`, [
|
|
16863
|
+
"| Finding | Severity | Verdict | Why |\n| --- | --- | --- | --- |",
|
|
16864
|
+
...others.map(([finding, verdict]) => `| ${cell(finding.title)} | ${finding.severity} | ${verdict} | ${cell(`${finding.reason}${finding.verdict === "unclear" && finding.baseNote ? ` Base build: ${finding.baseNote}` : ""}`)} |`)
|
|
16865
|
+
]) : [],
|
|
16866
|
+
`<sub>Updated on each push. Tests only what the diff can affect.${review.check ? "" : " Does not block the merge."}</sub>`
|
|
16605
16867
|
].join("\n\n");
|
|
16606
16868
|
return {
|
|
16607
16869
|
body: body2,
|
|
@@ -16612,8 +16874,25 @@ function renderReview(review, imageUrl, inDiff) {
|
|
|
16612
16874
|
}))
|
|
16613
16875
|
};
|
|
16614
16876
|
}
|
|
16615
|
-
function
|
|
16616
|
-
return
|
|
16877
|
+
function lineReviewBody(count, comment, name) {
|
|
16878
|
+
return [
|
|
16879
|
+
reviewMarker(name),
|
|
16880
|
+
`**Bugpatrol review${name ? `: ${name}` : ""}**: ${count} ${count === 1 ? "problem" : "problems"} on the changed lines. [The full review](${comment}) is updated on each push.`
|
|
16881
|
+
].join("\n\n");
|
|
16882
|
+
}
|
|
16883
|
+
function nextRunHint(review) {
|
|
16884
|
+
if (review.cutShort?.by === "max-steps")
|
|
16885
|
+
return `The explorer used all ${review.cutShort.limit} steps. Raise \`agents.explorer.maxSteps\`, or the Action's \`steps\` input.`;
|
|
16886
|
+
if (review.cutShort?.by === "timeout")
|
|
16887
|
+
return `The explorer used its ${Math.round(review.cutShort.limit / 6e4)} minutes. Raise \`agents.explorer.timeoutMs\`.`;
|
|
16888
|
+
if (review.cutShort?.by === "budget")
|
|
16889
|
+
return `The explorer spent its $${review.cutShort.limit.toFixed(2)} budget. Raise \`agents.explorer.budgetUsd\`.`;
|
|
16890
|
+
if (review.files && !review.files.guide)
|
|
16891
|
+
return "Add `.bugpatrol/instructions.md`, an app guide that tells the explorer how to use the app.";
|
|
16892
|
+
return void 0;
|
|
16893
|
+
}
|
|
16894
|
+
function supersededBody(head, name) {
|
|
16895
|
+
return `${reviewMarker(name)}
|
|
16617
16896
|
${SUPERSEDED_MARKER}
|
|
16618
16897
|
_A Bugpatrol review of \`${short(head)}\` replaced this one._`;
|
|
16619
16898
|
}
|
|
@@ -16647,7 +16926,7 @@ async function promoteClaims(root2, config, number, claimIds, opts = {}) {
|
|
|
16647
16926
|
for (const finding of chosen) {
|
|
16648
16927
|
let saved;
|
|
16649
16928
|
try {
|
|
16650
|
-
saved = JSON.parse(await
|
|
16929
|
+
saved = JSON.parse(await readFile12(resolve14(root2, finding.routine), "utf8"));
|
|
16651
16930
|
} catch {
|
|
16652
16931
|
throw new ConfigError(`The routine of ${finding.claim.id} is gone from ${finding.routine}. Run \`bugpatrol review ${number} --claims --force\` again.`);
|
|
16653
16932
|
}
|
|
@@ -16672,8 +16951,8 @@ async function promoteClaims(root2, config, number, claimIds, opts = {}) {
|
|
|
16672
16951
|
|
|
16673
16952
|
// ../agents/dist/roles/review.js
|
|
16674
16953
|
import { execFile as execFile11 } from "node:child_process";
|
|
16675
|
-
import { appendFile as appendFile3, lstat as lstat3, mkdir as mkdir8,
|
|
16676
|
-
import { dirname as dirname5, join as join16, resolve as resolve15 } from "node:path";
|
|
16954
|
+
import { appendFile as appendFile3, lstat as lstat3, mkdir as mkdir8, rm as rm8, stat as stat3, writeFile as writeFile11 } from "node:fs/promises";
|
|
16955
|
+
import { dirname as dirname5, join as join16, relative as relative8, resolve as resolve15 } from "node:path";
|
|
16677
16956
|
import { promisify as promisify11 } from "node:util";
|
|
16678
16957
|
|
|
16679
16958
|
// ../agents/dist/roles/benches.js
|
|
@@ -16909,7 +17188,7 @@ function benchWords(bench) {
|
|
|
16909
17188
|
|
|
16910
17189
|
// ../agents/dist/roles/claims.js
|
|
16911
17190
|
import { mkdir as mkdir7, writeFile as writeFile10 } from "node:fs/promises";
|
|
16912
|
-
import { join as join15, relative as
|
|
17191
|
+
import { join as join15, relative as relative7 } from "node:path";
|
|
16913
17192
|
|
|
16914
17193
|
// ../agents/dist/outputs.js
|
|
16915
17194
|
var RULES2 = [
|
|
@@ -17066,7 +17345,7 @@ async function reproClaims(ctx, first) {
|
|
|
17066
17345
|
did: `Replayed the repro routine of #${issue.number}, and checked whether the last screen ${checkWords(routine.bug)}.`,
|
|
17067
17346
|
routine: { ...routine, steps, requires: void 0 },
|
|
17068
17347
|
check: routine.bug,
|
|
17069
|
-
path:
|
|
17348
|
+
path: relative7(ctx.root, paths.routine(ctx.root, id2))
|
|
17070
17349
|
});
|
|
17071
17350
|
}
|
|
17072
17351
|
return { claims, flows };
|
|
@@ -17197,7 +17476,7 @@ function claimTools(session, ctx, claims, flows) {
|
|
|
17197
17476
|
did: routine.description,
|
|
17198
17477
|
saw: redact(input.saw),
|
|
17199
17478
|
routine,
|
|
17200
|
-
path:
|
|
17479
|
+
path: relative7(ctx.root, file)
|
|
17201
17480
|
});
|
|
17202
17481
|
end();
|
|
17203
17482
|
if (same)
|
|
@@ -17293,13 +17572,13 @@ async function saveRecording(ctx, driver, name, what) {
|
|
|
17293
17572
|
ctx.log(`Could not record ${what}: ${String(error).split("\n")[0]}`);
|
|
17294
17573
|
return void 0;
|
|
17295
17574
|
}
|
|
17296
|
-
const recording = { file:
|
|
17575
|
+
const recording = { file: relative7(ctx.root, file) };
|
|
17297
17576
|
const gif = file.endsWith(".cast") ? castGif : /\.(mp4|mov|webm)$/.test(file) ? inlineGif : void 0;
|
|
17298
17577
|
if (!gif)
|
|
17299
17578
|
return recording;
|
|
17300
17579
|
try {
|
|
17301
17580
|
if (await gif(file, `${name}.gif`))
|
|
17302
|
-
return { ...recording, gif:
|
|
17581
|
+
return { ...recording, gif: relative7(ctx.root, `${name}.gif`) };
|
|
17303
17582
|
ctx.log(`The GIF of ${what} is over the size budget. The review shows its screenshots.`);
|
|
17304
17583
|
} catch (error) {
|
|
17305
17584
|
ctx.log(`Could not make a GIF of ${what}: ${String(error).split("\n")[0]}`);
|
|
@@ -17334,7 +17613,7 @@ async function replayClaims(ctx, review, build, routines, driver, vars, fresh) {
|
|
|
17334
17613
|
last = await current.observe();
|
|
17335
17614
|
errors.push(...last.consoleErrors, ...last.networkErrors ?? []);
|
|
17336
17615
|
await writeFile10(file, last.screenshot);
|
|
17337
|
-
shots.push(
|
|
17616
|
+
shots.push(relative7(root2, file));
|
|
17338
17617
|
};
|
|
17339
17618
|
const what = `${claim} on ${where}`;
|
|
17340
17619
|
let recording = false;
|
|
@@ -17740,7 +18019,7 @@ var schema9 = (properties = {}, required = []) => ({
|
|
|
17740
18019
|
});
|
|
17741
18020
|
var string9 = { type: "string" };
|
|
17742
18021
|
var response6 = (value) => ({ content: [{ type: "text", text: value }] });
|
|
17743
|
-
var
|
|
18022
|
+
var checkName = (name) => name ? `Bugpatrol claim check (${name})` : "Bugpatrol claim check";
|
|
17744
18023
|
var DIFF_PATHS = [".", ":(exclude,glob)**/*.lock", ":(exclude,glob)**/*-lock.*", ":(exclude,glob)**/*.lockb"];
|
|
17745
18024
|
var DIFF_LIMIT = 4e4;
|
|
17746
18025
|
var ISSUE_LIMIT = 4e3;
|
|
@@ -17762,9 +18041,44 @@ var REVIEW_TOOLS = /* @__PURE__ */ new Set([
|
|
|
17762
18041
|
"run_routine",
|
|
17763
18042
|
"switch_window",
|
|
17764
18043
|
"report_bug",
|
|
17765
|
-
"list_screens"
|
|
17766
|
-
"finish"
|
|
18044
|
+
"list_screens"
|
|
17767
18045
|
]);
|
|
18046
|
+
var COVERAGE_LINE = 140;
|
|
18047
|
+
function coverageFinish(keep) {
|
|
18048
|
+
const refuse = (message2) => ({ ...response6(message2), isError: true });
|
|
18049
|
+
return {
|
|
18050
|
+
name: "finish",
|
|
18051
|
+
description: `Finish with each screen or flow that you tested, and each one the change can affect that you did not reach, with why. One short line each, under ${COVERAGE_LINE} characters.`,
|
|
18052
|
+
inputSchema: schema9({
|
|
18053
|
+
tested: { type: "array", items: string9 },
|
|
18054
|
+
untested: { type: "array", items: schema9({ what: string9, why: string9 }, ["what", "why"]) }
|
|
18055
|
+
}, ["tested", "untested"]),
|
|
18056
|
+
async run(input) {
|
|
18057
|
+
const tested = (Array.isArray(input.tested) ? input.tested : []).map((line) => String(line).trim());
|
|
18058
|
+
const untested = (Array.isArray(input.untested) ? input.untested : []).map((item) => {
|
|
18059
|
+
if (typeof item === "string") {
|
|
18060
|
+
const [what2 = "", ...why2] = item.split(": ");
|
|
18061
|
+
return { what: what2.trim(), why: why2.join(": ").trim() };
|
|
18062
|
+
}
|
|
18063
|
+
const { what, why } = item ?? {};
|
|
18064
|
+
return { what: String(what ?? "").trim(), why: String(why ?? "").trim() };
|
|
18065
|
+
});
|
|
18066
|
+
if (tested.some((line) => !line) || untested.some((item) => !item.what))
|
|
18067
|
+
return refuse("Each line needs text: a screen or flow in tested, and what (with why) in untested.");
|
|
18068
|
+
if (!tested.length && !untested.length)
|
|
18069
|
+
return refuse("Name what you tested in tested, and what you did not reach in untested.");
|
|
18070
|
+
const long = [...tested, ...untested.flatMap((item) => [item.what, item.why])].find((line) => line.length > COVERAGE_LINE);
|
|
18071
|
+
if (long)
|
|
18072
|
+
return refuse(`Keep each line under ${COVERAGE_LINE} characters. Too long: ${long}`);
|
|
18073
|
+
keep({ tested, untested });
|
|
18074
|
+
const text3 = [
|
|
18075
|
+
tested.length ? `Tested: ${tested.join("; ")}.` : "",
|
|
18076
|
+
untested.length ? `Not reached: ${untested.map((item) => item.why ? `${item.what} (${item.why})` : item.what).join("; ")}.` : ""
|
|
18077
|
+
];
|
|
18078
|
+
return { ...response6(text3.filter(Boolean).join(" ")), done: true };
|
|
18079
|
+
}
|
|
18080
|
+
};
|
|
18081
|
+
}
|
|
17768
18082
|
var claimCheck = (ctx) => Boolean(ctx.opts.claims ?? ctx.config.agents.review.claims);
|
|
17769
18083
|
var blocking = (ctx) => ctx.config.agents.review.block;
|
|
17770
18084
|
var headRefOf = (number) => `refs/bugpatrol/pr-${number}`;
|
|
@@ -17864,7 +18178,7 @@ async function withBuild(ctx, name, commit, run3) {
|
|
|
17864
18178
|
}
|
|
17865
18179
|
});
|
|
17866
18180
|
}
|
|
17867
|
-
async function exploreHead(ctx, review, claims, flows) {
|
|
18181
|
+
async function exploreHead(ctx, review, guide, claims, flows) {
|
|
17868
18182
|
const { root: root2, config, workspace, pr, opts } = ctx;
|
|
17869
18183
|
return withBuild(ctx, "head", pr.head, async (driver, vars) => {
|
|
17870
18184
|
const record = await workspace.startSession("explorer");
|
|
@@ -17874,14 +18188,13 @@ async function exploreHead(ctx, review, claims, flows) {
|
|
|
17874
18188
|
const runtime = (opts.createRuntime ?? createRuntime)(config.agents.explorer.use);
|
|
17875
18189
|
const claimWork = claims.length ? claimTools(session, ctx, claims, flows) : void 0;
|
|
17876
18190
|
const maxSteps = opts.maxSteps ?? config.agents.explorer.maxSteps;
|
|
17877
|
-
const guide = instructionsPath(root2, config.app.instructions);
|
|
17878
18191
|
await session.activity(`Reviewing PR #${pr.number}`, 0, runtime.label);
|
|
17879
18192
|
session.emit({ kind: "session-start", summary: `Reviewing PR #${pr.number}: ${pr.title}` });
|
|
17880
18193
|
try {
|
|
17881
18194
|
const outcome = await withSessionLogs({ config, root: root2, vars, onLog: ctx.log, workspace, session: record }, async () => runtime.run({
|
|
17882
18195
|
role: "explorer",
|
|
17883
18196
|
sessionId: record.id,
|
|
17884
|
-
system: explorerReviewSystem(config.app.platform, guide
|
|
18197
|
+
system: explorerReviewSystem(config.app.platform, guide.text ?? "", lessonsFor(await workspace.readMemory(), "explorer")),
|
|
17885
18198
|
prompt: [
|
|
17886
18199
|
explorerReviewPrompt({
|
|
17887
18200
|
pr,
|
|
@@ -17894,18 +18207,31 @@ async function exploreHead(ctx, review, claims, flows) {
|
|
|
17894
18207
|
].join("\n\n"),
|
|
17895
18208
|
tools: [
|
|
17896
18209
|
...explorerTools(session, { replay: { save: false } }).filter((tool) => REVIEW_TOOLS.has(tool.name)),
|
|
18210
|
+
coverageFinish((coverage) => {
|
|
18211
|
+
review.coverage = vars.redact(coverage);
|
|
18212
|
+
}),
|
|
17897
18213
|
...claimWork?.tools ?? []
|
|
17898
18214
|
].map((tool) => stopOnCancellation(session, claimWork ? claimWork.meter(tool) : tool)),
|
|
17899
18215
|
maxSteps,
|
|
17900
18216
|
budgetUsd: config.agents.explorer.budgetUsd,
|
|
17901
18217
|
timeoutMs: config.agents.explorer.timeoutMs
|
|
17902
18218
|
}, session.emit));
|
|
17903
|
-
if (outcome.stop === "error" || outcome.stop === "timeout")
|
|
18219
|
+
if (outcome.stop === "error" || outcome.stop === "timeout" && !outcome.steps)
|
|
17904
18220
|
throw new InfrastructureError(`The explorer did not finish (${outcome.stop}): ${outcome.error ?? outcome.summary ?? "no result"}`);
|
|
17905
18221
|
claimWork?.charge(outcome);
|
|
17906
18222
|
const candidates = await workspace.readCandidates(record.id);
|
|
17907
18223
|
review.costUsd += outcome.costUsd;
|
|
17908
|
-
|
|
18224
|
+
const summary = outcome.summary?.trim();
|
|
18225
|
+
const cutShort = `The explorer stopped before it finished (${outcome.stop}), so it tested a part of the change only.`;
|
|
18226
|
+
if (outcome.stop === "max-steps")
|
|
18227
|
+
review.cutShort = { by: "max-steps", limit: maxSteps };
|
|
18228
|
+
if (outcome.stop === "timeout")
|
|
18229
|
+
review.cutShort = { by: "timeout", limit: config.agents.explorer.timeoutMs };
|
|
18230
|
+
if (outcome.stop === "budget" && config.agents.explorer.budgetUsd !== void 0)
|
|
18231
|
+
review.cutShort = { by: "budget", limit: config.agents.explorer.budgetUsd };
|
|
18232
|
+
review.tested = summary && (outcome.stop === "done" || outcome.finished) ? vars.redact(outcome.stop === "done" ? summary : `${cutShort}
|
|
18233
|
+
|
|
18234
|
+
${summary}`) : cutShort;
|
|
17909
18235
|
await workspace.endSession(record.id, {
|
|
17910
18236
|
steps: outcome.steps,
|
|
17911
18237
|
costUsd: outcome.costUsd,
|
|
@@ -17924,7 +18250,7 @@ async function exploreHead(ctx, review, claims, flows) {
|
|
|
17924
18250
|
}
|
|
17925
18251
|
});
|
|
17926
18252
|
}
|
|
17927
|
-
async function captureBase(ctx, review, candidates) {
|
|
18253
|
+
async function captureBase(ctx, review, guide, candidates) {
|
|
17928
18254
|
const { root: root2, config, workspace, pr, opts } = ctx;
|
|
17929
18255
|
const targets = candidates.map((candidate) => ({
|
|
17930
18256
|
shot: {
|
|
@@ -17941,7 +18267,6 @@ async function captureBase(ctx, review, candidates) {
|
|
|
17941
18267
|
const session = new AgentSession(root2, config, vars, record.id, "explorer", driver, ctx.log);
|
|
17942
18268
|
opts.onSession?.(session);
|
|
17943
18269
|
const runtime = (opts.createRuntime ?? createRuntime)(config.agents.explorer.use);
|
|
17944
|
-
const guide = instructionsPath(root2, config.app.instructions);
|
|
17945
18270
|
const summary = `Repeating ${targets.length} flow(s) on the base of PR #${pr.number}`;
|
|
17946
18271
|
await session.activity(summary, 0, runtime.label);
|
|
17947
18272
|
session.emit({ kind: "session-start", summary });
|
|
@@ -17950,7 +18275,7 @@ async function captureBase(ctx, review, candidates) {
|
|
|
17950
18275
|
try {
|
|
17951
18276
|
const outcome = await captureTargets(session, runtime, targets, {
|
|
17952
18277
|
replay: { save: false },
|
|
17953
|
-
system: explorerBaseSystem(config.app.platform, guide
|
|
18278
|
+
system: explorerBaseSystem(config.app.platform, guide.text ?? "", lessonsFor(await workspace.readMemory(), "explorer")),
|
|
17954
18279
|
prompt: `Pull request #${pr.number}: ${pr.title}
|
|
17955
18280
|
Findings:
|
|
17956
18281
|
${candidates.map((candidate, index) => `${index + 1}. ${candidate.summary}`).join("\n")}
|
|
@@ -18307,7 +18632,11 @@ ${pr.diff}`
|
|
|
18307
18632
|
return claims;
|
|
18308
18633
|
}
|
|
18309
18634
|
async function runReview(ctx) {
|
|
18310
|
-
const { workspace, pr } = ctx;
|
|
18635
|
+
const { root: root2, workspace, pr } = ctx;
|
|
18636
|
+
const guide = await readGuide(root2, ctx.config);
|
|
18637
|
+
const configFile = relative8(root2, paths.config(root2, ctx.opts.configFile));
|
|
18638
|
+
const guideFile = guide.text === void 0 ? void 0 : relative8(root2, guide.path);
|
|
18639
|
+
ctx.log(`Config: ${configFile}. ${guideFile ? `App guide: ${guideFile}.` : noGuide(root2, guide)}`);
|
|
18311
18640
|
const review = {
|
|
18312
18641
|
version: 1,
|
|
18313
18642
|
pr: { number: pr.number, url: pr.url, title: pr.title },
|
|
@@ -18317,6 +18646,7 @@ async function runReview(ctx) {
|
|
|
18317
18646
|
status: "running",
|
|
18318
18647
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18319
18648
|
sessions: {},
|
|
18649
|
+
files: { config: configFile, ...guideFile && { guide: guideFile } },
|
|
18320
18650
|
findings: [],
|
|
18321
18651
|
costUsd: 0
|
|
18322
18652
|
};
|
|
@@ -18333,7 +18663,7 @@ async function runReview(ctx) {
|
|
|
18333
18663
|
});
|
|
18334
18664
|
if (claims) {
|
|
18335
18665
|
review.claims = claims.map((claim) => notYet(claim, "The review stopped before it tested this claim."));
|
|
18336
|
-
await
|
|
18666
|
+
await rm8(paths.reviewDir(ctx.root, pr.number), { recursive: true, force: true });
|
|
18337
18667
|
}
|
|
18338
18668
|
const platform = ctx.config.app.platform;
|
|
18339
18669
|
const testable = (claims ?? []).filter((claim) => claim.testable);
|
|
@@ -18353,7 +18683,7 @@ async function runReview(ctx) {
|
|
|
18353
18683
|
const flows = new Map(repros.flows);
|
|
18354
18684
|
const picks = testable.some(explorable) && ctx.config.agents.review.benches.length ? await pickBenches(ctx, review, testable.filter(explorable)) : /* @__PURE__ */ new Map();
|
|
18355
18685
|
const toExplore = toTest.filter((claim) => explorable(claim) && !picks.has(claim.id));
|
|
18356
|
-
const candidates = await exploreHead(ctx, review, toExplore, flows);
|
|
18686
|
+
const candidates = await exploreHead(ctx, review, guide, toExplore, flows);
|
|
18357
18687
|
if (claims) {
|
|
18358
18688
|
const measured = await ctx.budget.timed(() => runBenches(ctx, picks, (run3) => withWorktree(ctx, "base", pr.base, (base) => withWorktree(ctx, "head", pr.head, (head) => run3({ head, base })))));
|
|
18359
18689
|
const build = (name, commit, run3) => withBuild(ctx, name, commit, run3);
|
|
@@ -18367,7 +18697,7 @@ async function runReview(ctx) {
|
|
|
18367
18697
|
review.claims = claims.map((claim) => tested.find((finding) => finding.claim.id === claim.id) ?? untestedHere(claim));
|
|
18368
18698
|
}
|
|
18369
18699
|
if (candidates.length) {
|
|
18370
|
-
const targets = await captureBase(ctx, review, candidates);
|
|
18700
|
+
const targets = await captureBase(ctx, review, guide, candidates);
|
|
18371
18701
|
review.findings = await judgeFindings(ctx, review, candidates, targets);
|
|
18372
18702
|
await settleCandidates(ctx, review.sessions.explorer, review.findings);
|
|
18373
18703
|
}
|
|
@@ -18385,19 +18715,30 @@ async function runReview(ctx) {
|
|
|
18385
18715
|
}
|
|
18386
18716
|
return review;
|
|
18387
18717
|
}
|
|
18388
|
-
async function openReviews(gh, reviews) {
|
|
18389
|
-
const select = `.[] | select(.body | contains("${
|
|
18390
|
-
return (await gh(["api", "--paginate", reviews, "--jq", `${select} | "\\(.id) \\(.commit_id)"`])).split("\n").filter(Boolean).map((row) => {
|
|
18391
|
-
const [id2, commit] = row.split(" ");
|
|
18392
|
-
return { id: id2, commit };
|
|
18718
|
+
async function openReviews(gh, reviews, name) {
|
|
18719
|
+
const select = `.[] | select(.body | contains("${reviewMarker(name)}")) | select(.body | contains("${SUPERSEDED_MARKER}") | not)`;
|
|
18720
|
+
return (await gh(["api", "--paginate", reviews, "--jq", `${select} | "\\(.id) \\(.commit_id) \\(.node_id)"`])).split("\n").filter(Boolean).map((row) => {
|
|
18721
|
+
const [id2, commit, node] = row.split(" ");
|
|
18722
|
+
return { id: id2, commit, node };
|
|
18393
18723
|
});
|
|
18394
18724
|
}
|
|
18725
|
+
async function saveSticky(gh, repo, pr, marker, body2) {
|
|
18726
|
+
const comments = `repos/${repo}/issues/${pr}/comments`;
|
|
18727
|
+
const [id2] = (await gh(["api", "--paginate", comments, "--jq", `.[] | select(.body | contains("${marker}")) | .id`])).split("\n").filter(Boolean);
|
|
18728
|
+
const input = JSON.stringify({ body: body2 });
|
|
18729
|
+
if (id2) {
|
|
18730
|
+
const edited = JSON.parse(await gh(["api", "-X", "PATCH", `repos/${repo}/issues/comments/${id2}`, "--input", "-"], { input }));
|
|
18731
|
+
return { html_url: edited.html_url, created: false };
|
|
18732
|
+
}
|
|
18733
|
+
const made = JSON.parse(await gh(["api", "-X", "POST", comments, "--input", "-"], { input }));
|
|
18734
|
+
return { html_url: made.html_url, created: true };
|
|
18735
|
+
}
|
|
18395
18736
|
async function supersede(ctx, gh, old, head) {
|
|
18396
18737
|
const pulls = `repos/${ctx.repo}/pulls`;
|
|
18397
18738
|
const ids = new Set(old.map((review) => review.id));
|
|
18398
18739
|
for (const id2 of ids)
|
|
18399
18740
|
await gh(["api", "-X", "PUT", `${pulls}/${ctx.pr.number}/reviews/${id2}`, "--input", "-"], {
|
|
18400
|
-
input: JSON.stringify({ body: supersededBody(head) })
|
|
18741
|
+
input: JSON.stringify({ body: supersededBody(head, ctx.config.agents.review.name) })
|
|
18401
18742
|
});
|
|
18402
18743
|
const rows = (await gh([
|
|
18403
18744
|
"api",
|
|
@@ -18410,6 +18751,15 @@ async function supersede(ctx, gh, old, head) {
|
|
|
18410
18751
|
for (const [id2, review, parent] of rows)
|
|
18411
18752
|
if (ids.has(review) && parent === "null" && !answered.has(id2))
|
|
18412
18753
|
await gh(["api", "-X", "DELETE", `${pulls}/comments/${id2}`]);
|
|
18754
|
+
for (const { node } of old)
|
|
18755
|
+
await gh([
|
|
18756
|
+
"api",
|
|
18757
|
+
"graphql",
|
|
18758
|
+
"-f",
|
|
18759
|
+
"query=mutation($id: ID!) { minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { clientMutationId } }",
|
|
18760
|
+
"-f",
|
|
18761
|
+
`id=${node}`
|
|
18762
|
+
]);
|
|
18413
18763
|
}
|
|
18414
18764
|
async function publishReview(ctx, gh, review, tested) {
|
|
18415
18765
|
const { root: root2, config, repo, pr } = ctx;
|
|
@@ -18417,7 +18767,7 @@ async function publishReview(ctx, gh, review, tested) {
|
|
|
18417
18767
|
const inDiff = (file, line) => pr.lines.get(file)?.has(line) ?? false;
|
|
18418
18768
|
if (ctx.opts.dryRun) {
|
|
18419
18769
|
const file = paths.review(root2, pr.number).replace(/\.json$/, ".md");
|
|
18420
|
-
const rendered2 = renderReview(review, (path) => resolve15(root2, path), inDiff);
|
|
18770
|
+
const rendered2 = renderReview(review, (path) => resolve15(root2, path), inDiff, config.agents.review.name);
|
|
18421
18771
|
await mkdir8(dirname5(file), { recursive: true });
|
|
18422
18772
|
await writeFile11(file, redact([
|
|
18423
18773
|
rendered2.body,
|
|
@@ -18429,7 +18779,7 @@ ${comment.body}`),
|
|
|
18429
18779
|
...review.check ? [
|
|
18430
18780
|
`---
|
|
18431
18781
|
|
|
18432
|
-
Check run ${
|
|
18782
|
+
Check run ${checkName(config.agents.review.name)}: \`${review.check.conclusion}\`, ${review.check.title}
|
|
18433
18783
|
|
|
18434
18784
|
${review.check.summary}`
|
|
18435
18785
|
] : []
|
|
@@ -18457,37 +18807,36 @@ ${review.check.summary}`
|
|
|
18457
18807
|
ctx.log(`Could not upload ${path}: ${String(error).split("\n")[0]}`);
|
|
18458
18808
|
}
|
|
18459
18809
|
}
|
|
18460
|
-
const rendered = renderReview(review, (path) => urls.get(path), inDiff);
|
|
18810
|
+
const rendered = renderReview(review, (path) => urls.get(path), inDiff, config.agents.review.name);
|
|
18461
18811
|
const body2 = limitBody(redact(rendered.body));
|
|
18462
|
-
const
|
|
18463
|
-
const
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18812
|
+
const name = config.agents.review.name;
|
|
18813
|
+
const posted = await saveSticky(gh, repo, pr.number, reviewMarker(name), body2);
|
|
18814
|
+
review.posted = { url: posted.html_url, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
18815
|
+
if (tested) {
|
|
18816
|
+
const reviews = `repos/${repo}/pulls/${pr.number}/reviews`;
|
|
18817
|
+
const open3 = await openReviews(gh, reviews, name);
|
|
18818
|
+
if (rendered.comments.length)
|
|
18819
|
+
await gh(["api", "-X", "POST", reviews, "--input", "-"], {
|
|
18820
|
+
input: JSON.stringify({
|
|
18821
|
+
commit_id: review.head,
|
|
18822
|
+
event: "COMMENT",
|
|
18823
|
+
body: lineReviewBody(rendered.comments.length, posted.html_url, name),
|
|
18824
|
+
comments: rendered.comments.map((comment) => ({
|
|
18825
|
+
path: comment.path,
|
|
18826
|
+
line: comment.line,
|
|
18827
|
+
side: "RIGHT",
|
|
18828
|
+
body: limitBody(redact(comment.body))
|
|
18829
|
+
}))
|
|
18830
|
+
})
|
|
18831
|
+
});
|
|
18482
18832
|
try {
|
|
18483
18833
|
await supersede(ctx, gh, open3, review.head);
|
|
18484
18834
|
} catch (error) {
|
|
18485
18835
|
ctx.log(`Could not replace the older review(s) of PR #${pr.number}: ${String(error).split("\n")[0]}`);
|
|
18486
18836
|
}
|
|
18487
18837
|
}
|
|
18488
|
-
review.posted = { url: posted.html_url, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
18489
18838
|
await ctx.workspace.saveReview(review);
|
|
18490
|
-
ctx.log(`${
|
|
18839
|
+
ctx.log(`${posted.created ? "Posted" : "Updated"} the review of PR #${pr.number}: ${posted.html_url}`);
|
|
18491
18840
|
if (review.check)
|
|
18492
18841
|
await setCheckRun(ctx, gh, review, review.check);
|
|
18493
18842
|
}
|
|
@@ -18496,7 +18845,7 @@ async function setCheckRun(ctx, gh, review, check) {
|
|
|
18496
18845
|
try {
|
|
18497
18846
|
const run3 = JSON.parse(await gh(["api", "-X", "POST", `repos/${ctx.repo}/check-runs`, "--input", "-"], {
|
|
18498
18847
|
input: JSON.stringify({
|
|
18499
|
-
name:
|
|
18848
|
+
name: checkName(ctx.config.agents.review.name),
|
|
18500
18849
|
head_sha: review.head,
|
|
18501
18850
|
status: "completed",
|
|
18502
18851
|
conclusion: check.conclusion,
|
|
@@ -18809,7 +19158,7 @@ function hostOf(href) {
|
|
|
18809
19158
|
}
|
|
18810
19159
|
|
|
18811
19160
|
// ../dashboard/dist/project.js
|
|
18812
|
-
import { existsSync as
|
|
19161
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
18813
19162
|
import { join as join17 } from "node:path";
|
|
18814
19163
|
var ProjectReader = class {
|
|
18815
19164
|
root;
|
|
@@ -18822,24 +19171,24 @@ var ProjectReader = class {
|
|
|
18822
19171
|
return paths.dir(this.root);
|
|
18823
19172
|
}
|
|
18824
19173
|
hasProject() {
|
|
18825
|
-
return
|
|
19174
|
+
return existsSync6(paths.config(this.root)) || existsSync6(this.bugpatrolDir);
|
|
18826
19175
|
}
|
|
18827
19176
|
/** Newest first. `latest/` holds loose capture artifacts, not a run. */
|
|
18828
19177
|
listRunIds() {
|
|
18829
19178
|
const dir2 = paths.runs(this.root);
|
|
18830
|
-
if (!
|
|
19179
|
+
if (!existsSync6(dir2))
|
|
18831
19180
|
return [];
|
|
18832
19181
|
return readdirSync2(dir2, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== "latest").map((e) => ({ name: e.name, mtime: safeMtime(join17(dir2, e.name)) })).sort((a, b) => b.mtime - a.mtime).map((e) => e.name);
|
|
18833
19182
|
}
|
|
18834
19183
|
readRun(runId) {
|
|
18835
19184
|
const dir2 = join17(paths.runs(this.root), runId);
|
|
18836
19185
|
const runFile = join17(dir2, "run.json");
|
|
18837
|
-
if (!
|
|
19186
|
+
if (!existsSync6(runFile))
|
|
18838
19187
|
return void 0;
|
|
18839
19188
|
try {
|
|
18840
19189
|
const parsed = JSON.parse(readFileSync4(runFile, "utf8"));
|
|
18841
19190
|
const traceFile = join17(dir2, "trace.json");
|
|
18842
|
-
const trace =
|
|
19191
|
+
const trace = existsSync6(traceFile) ? JSON.parse(readFileSync4(traceFile, "utf8")) : void 0;
|
|
18843
19192
|
return { id: runId, dir: dir2, run: parsed.run, findings: parsed.findings, trace, mtimeMs: safeMtime(dir2) };
|
|
18844
19193
|
} catch {
|
|
18845
19194
|
return void 0;
|
|
@@ -18858,7 +19207,7 @@ var ProjectReader = class {
|
|
|
18858
19207
|
}
|
|
18859
19208
|
readAppModel() {
|
|
18860
19209
|
const file = paths.appModel(this.root);
|
|
18861
|
-
if (!
|
|
19210
|
+
if (!existsSync6(file))
|
|
18862
19211
|
return void 0;
|
|
18863
19212
|
try {
|
|
18864
19213
|
return JSON.parse(readFileSync4(file, "utf8"));
|
|
@@ -18868,7 +19217,7 @@ var ProjectReader = class {
|
|
|
18868
19217
|
}
|
|
18869
19218
|
readIntents() {
|
|
18870
19219
|
const file = paths.intents(this.root);
|
|
18871
|
-
if (!
|
|
19220
|
+
if (!existsSync6(file))
|
|
18872
19221
|
return [];
|
|
18873
19222
|
try {
|
|
18874
19223
|
return JSON.parse(readFileSync4(file, "utf8")).intents ?? [];
|
|
@@ -18879,7 +19228,7 @@ var ProjectReader = class {
|
|
|
18879
19228
|
/** Progress for a run currently in flight, if one is. */
|
|
18880
19229
|
readLive() {
|
|
18881
19230
|
const file = paths.live(this.root);
|
|
18882
|
-
if (!
|
|
19231
|
+
if (!existsSync6(file))
|
|
18883
19232
|
return void 0;
|
|
18884
19233
|
try {
|
|
18885
19234
|
return JSON.parse(readFileSync4(file, "utf8"));
|
|
@@ -18889,7 +19238,7 @@ var ProjectReader = class {
|
|
|
18889
19238
|
}
|
|
18890
19239
|
readConfigRaw() {
|
|
18891
19240
|
const file = paths.config(this.root, this.configFile);
|
|
18892
|
-
return
|
|
19241
|
+
return existsSync6(file) ? readFileSync4(file, "utf8") : void 0;
|
|
18893
19242
|
}
|
|
18894
19243
|
};
|
|
18895
19244
|
function summarize(record) {
|
|
@@ -18923,14 +19272,14 @@ function safeMtime(path) {
|
|
|
18923
19272
|
}
|
|
18924
19273
|
|
|
18925
19274
|
// ../dashboard/dist/server.js
|
|
18926
|
-
import { createReadStream, existsSync as
|
|
19275
|
+
import { createReadStream, existsSync as existsSync8, realpathSync, statSync as statSync3, watch } from "node:fs";
|
|
18927
19276
|
import { createServer as createServer4 } from "node:http";
|
|
18928
|
-
import { extname as extname2, join as join19, relative as
|
|
19277
|
+
import { extname as extname2, join as join19, relative as relative10, resolve as resolve16, sep as sep5 } from "node:path";
|
|
18929
19278
|
import { fileURLToPath } from "node:url";
|
|
18930
19279
|
|
|
18931
19280
|
// ../dashboard/dist/agents.js
|
|
18932
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
18933
|
-
import { basename as basename4, dirname as dirname6, extname, join as join18, relative as
|
|
19281
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
19282
|
+
import { basename as basename4, dirname as dirname6, extname, join as join18, relative as relative9, sep as sep4 } from "node:path";
|
|
18934
19283
|
var roles2 = ["explorer", "judge", "fixer"];
|
|
18935
19284
|
var severity = { critical: 0, major: 1, minor: 2, cosmetic: 3 };
|
|
18936
19285
|
var open2 = /* @__PURE__ */ new Set(["new", "filed", "fixing", "fix-proposed"]);
|
|
@@ -19064,7 +19413,26 @@ var AgentReader = class {
|
|
|
19064
19413
|
const routine = this.routines().find((entry) => entry.id === issue.evidence?.routineId);
|
|
19065
19414
|
const ids = new Set(issue.candidateIds);
|
|
19066
19415
|
const candidates = this.sessions(Infinity).flatMap((session) => jsonLines(join18(paths.session(this.root, session.id), "candidates.jsonl"))).filter((candidate) => ids.has(candidate.id));
|
|
19067
|
-
return {
|
|
19416
|
+
return {
|
|
19417
|
+
issue,
|
|
19418
|
+
fix,
|
|
19419
|
+
gaveUpAfter: fix && this.gaveUpAfter(fix),
|
|
19420
|
+
attemptDiffs: fix && this.attemptDiffs(fix),
|
|
19421
|
+
candidates,
|
|
19422
|
+
routine
|
|
19423
|
+
};
|
|
19424
|
+
}
|
|
19425
|
+
/** Each attempt's diff file that exists, by attempt number, as an artifact path. */
|
|
19426
|
+
attemptDiffs(fix) {
|
|
19427
|
+
return Object.fromEntries((fix.attempts ?? []).map((attempt) => [attempt.n, paths.fixAttemptDiff(this.root, fix.id, attempt.n)]).filter(([, file]) => existsSync7(file)).map(([n, file]) => [n, relative9(this.root, file).split(sep4).join("/")]));
|
|
19428
|
+
}
|
|
19429
|
+
/** The fixer's attempt count, when this fix failed and reached the limit. CI fix attempts have their own limit. */
|
|
19430
|
+
gaveUpAfter(fix) {
|
|
19431
|
+
const limit = this.config()?.agents.fixer.attempts;
|
|
19432
|
+
const count = fixerAttempts(fix.attempts).length;
|
|
19433
|
+
if (fix.status !== "failed" || limit === void 0)
|
|
19434
|
+
return void 0;
|
|
19435
|
+
return count >= limit ? count : void 0;
|
|
19068
19436
|
}
|
|
19069
19437
|
screens() {
|
|
19070
19438
|
const map = this.appmap();
|
|
@@ -19144,8 +19512,8 @@ var AgentReader = class {
|
|
|
19144
19512
|
if (!review)
|
|
19145
19513
|
return void 0;
|
|
19146
19514
|
const recordings = walk(paths.reviewDir(this.root, pr)).map((file) => ({ file, kind: recordingKinds[extname(file).toLowerCase()] })).filter((entry) => Boolean(entry.kind)).map(({ file, kind }) => {
|
|
19147
|
-
const path =
|
|
19148
|
-
const inside2 =
|
|
19515
|
+
const path = relative9(this.root, file).split(sep4).join("/");
|
|
19516
|
+
const inside2 = relative9(paths.reviewDir(this.root, pr), file);
|
|
19149
19517
|
const claimId = inside2.match(/\bclaim-\d+(?!\d)/)?.[0];
|
|
19150
19518
|
const build = inside2.match(/\b(head|base)\b/)?.[1];
|
|
19151
19519
|
return { path, kind, ...claimId ? { claimId } : {}, ...build ? { build } : {} };
|
|
@@ -19158,13 +19526,19 @@ var AgentReader = class {
|
|
|
19158
19526
|
}
|
|
19159
19527
|
/** Each role as the config sets it, in the same form as the runtime labels. */
|
|
19160
19528
|
configuredAgents() {
|
|
19529
|
+
const config = this.config();
|
|
19530
|
+
if (!config)
|
|
19531
|
+
return void 0;
|
|
19532
|
+
return Object.fromEntries(roles2.map((role) => {
|
|
19533
|
+
const { enabled, use } = config.agents[role];
|
|
19534
|
+
const runtime = use.runtime === "cli" ? `cli:${use.command.split(/\s+/)[0]}` : `model:${use.via}/${use.model}`;
|
|
19535
|
+
return [role, { enabled, runtime }];
|
|
19536
|
+
}));
|
|
19537
|
+
}
|
|
19538
|
+
/** Undefined when the project has no config, unless the dashboard was given one. */
|
|
19539
|
+
config() {
|
|
19161
19540
|
try {
|
|
19162
|
-
|
|
19163
|
-
return Object.fromEntries(roles2.map((role) => {
|
|
19164
|
-
const { enabled, use } = config.agents[role];
|
|
19165
|
-
const runtime = use.runtime === "cli" ? `cli:${use.command.split(/\s+/)[0]}` : `model:${use.via}/${use.model}`;
|
|
19166
|
-
return [role, { enabled, runtime }];
|
|
19167
|
-
}));
|
|
19541
|
+
return loadConfig(this.root, {}, this.configFile);
|
|
19168
19542
|
} catch (error) {
|
|
19169
19543
|
if (this.configFile)
|
|
19170
19544
|
throw error;
|
|
@@ -19270,9 +19644,10 @@ var MIME = {
|
|
|
19270
19644
|
".webm": "video/webm",
|
|
19271
19645
|
".cast": "application/x-asciicast",
|
|
19272
19646
|
".svg": "image/svg+xml",
|
|
19273
|
-
".json": "application/json; charset=utf-8"
|
|
19647
|
+
".json": "application/json; charset=utf-8",
|
|
19648
|
+
".diff": "text/x-diff; charset=utf-8"
|
|
19274
19649
|
};
|
|
19275
|
-
var ARTIFACTS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".webm", ".cast", ".json"];
|
|
19650
|
+
var ARTIFACTS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".webm", ".cast", ".json", ".diff"];
|
|
19276
19651
|
async function startDashboard(options) {
|
|
19277
19652
|
const root2 = resolve16(options.root);
|
|
19278
19653
|
const host = options.host ?? "127.0.0.1";
|
|
@@ -19383,7 +19758,7 @@ async function handle2(req, res, ctx) {
|
|
|
19383
19758
|
}
|
|
19384
19759
|
function resolveArtifactPath(root2, requested) {
|
|
19385
19760
|
const candidate = resolve16(root2, requested);
|
|
19386
|
-
if (!
|
|
19761
|
+
if (!existsSync8(candidate))
|
|
19387
19762
|
return void 0;
|
|
19388
19763
|
let real;
|
|
19389
19764
|
let allowed;
|
|
@@ -19393,7 +19768,7 @@ function resolveArtifactPath(root2, requested) {
|
|
|
19393
19768
|
} catch {
|
|
19394
19769
|
return void 0;
|
|
19395
19770
|
}
|
|
19396
|
-
const rel =
|
|
19771
|
+
const rel = relative10(allowed, real);
|
|
19397
19772
|
if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${sep5}`))
|
|
19398
19773
|
return void 0;
|
|
19399
19774
|
const stat4 = statSync3(real);
|
|
@@ -19444,7 +19819,7 @@ function byteRange(header, size) {
|
|
|
19444
19819
|
function serveUi(res, path) {
|
|
19445
19820
|
const name = path === "/" ? "index.html" : path.replace(/^\//, "");
|
|
19446
19821
|
const file = resolve16(UI_DIR, name);
|
|
19447
|
-
if (!file.startsWith(UI_DIR) || !
|
|
19822
|
+
if (!file.startsWith(UI_DIR) || !existsSync8(file) || !statSync3(file).isFile()) {
|
|
19448
19823
|
serveUi(res, "/");
|
|
19449
19824
|
return;
|
|
19450
19825
|
}
|
|
@@ -19485,7 +19860,7 @@ function watchProject(root2, onChange) {
|
|
|
19485
19860
|
timer = setTimeout(onChange, 250);
|
|
19486
19861
|
};
|
|
19487
19862
|
const attach = () => {
|
|
19488
|
-
if (!
|
|
19863
|
+
if (!existsSync8(dir2))
|
|
19489
19864
|
return;
|
|
19490
19865
|
try {
|
|
19491
19866
|
const next = watch(dir2, { recursive: true }, changed);
|
|
@@ -19502,17 +19877,17 @@ function watchProject(root2, onChange) {
|
|
|
19502
19877
|
}
|
|
19503
19878
|
};
|
|
19504
19879
|
try {
|
|
19505
|
-
watcher =
|
|
19506
|
-
if (
|
|
19880
|
+
watcher = existsSync8(dir2) ? watch(dir2, { recursive: true }, changed) : watch(root2, { recursive: false }, () => {
|
|
19881
|
+
if (existsSync8(dir2))
|
|
19507
19882
|
attach();
|
|
19508
19883
|
});
|
|
19509
19884
|
watcher.on("error", () => {
|
|
19510
19885
|
watcher.close();
|
|
19511
19886
|
fallback = setInterval(changed, 5e3);
|
|
19512
19887
|
});
|
|
19513
|
-
if (!
|
|
19888
|
+
if (!existsSync8(dir2))
|
|
19514
19889
|
probe = setInterval(() => {
|
|
19515
|
-
if (
|
|
19890
|
+
if (existsSync8(dir2))
|
|
19516
19891
|
attach();
|
|
19517
19892
|
}, 100);
|
|
19518
19893
|
return {
|
|
@@ -19590,7 +19965,7 @@ import { chmod, mkdir as mkdir9, writeFile as writeFile12 } from "node:fs/promis
|
|
|
19590
19965
|
import { join as join22, resolve as resolve17 } from "node:path";
|
|
19591
19966
|
|
|
19592
19967
|
// ../cli/dist/commands/run.js
|
|
19593
|
-
import { existsSync as
|
|
19968
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
19594
19969
|
import { join as join21 } from "node:path";
|
|
19595
19970
|
|
|
19596
19971
|
// ../capture/dist/app-server.js
|
|
@@ -20279,7 +20654,7 @@ function severityRank2(f) {
|
|
|
20279
20654
|
}
|
|
20280
20655
|
|
|
20281
20656
|
// ../triage/dist/ledger.js
|
|
20282
|
-
import { existsSync as
|
|
20657
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
20283
20658
|
var IntentLedger = class _IntentLedger {
|
|
20284
20659
|
intents;
|
|
20285
20660
|
constructor(intents = []) {
|
|
@@ -20287,7 +20662,7 @@ var IntentLedger = class _IntentLedger {
|
|
|
20287
20662
|
}
|
|
20288
20663
|
static load(root2) {
|
|
20289
20664
|
const file = paths.intents(root2);
|
|
20290
|
-
if (!
|
|
20665
|
+
if (!existsSync9(file))
|
|
20291
20666
|
return new _IntentLedger([]);
|
|
20292
20667
|
const parsed = JSON.parse(readFileSync6(file, "utf8"));
|
|
20293
20668
|
return new _IntentLedger(parsed.intents.map((i) => ({
|
|
@@ -20881,7 +21256,7 @@ async function captureOne(args) {
|
|
|
20881
21256
|
});
|
|
20882
21257
|
const key = baselineKeyFor(target.id, viewport.name);
|
|
20883
21258
|
const baselinePath = store.pathFor(key);
|
|
20884
|
-
if (!baselinePath || !
|
|
21259
|
+
if (!baselinePath || !existsSync10(baselinePath)) {
|
|
20885
21260
|
store.put(key, viewport.name, readFileSync7(actualPath));
|
|
20886
21261
|
saveBaselineSnapshot(args.root, key, output2.snapshot);
|
|
20887
21262
|
return {
|
|
@@ -20941,7 +21316,7 @@ function resolveTargets(config, model, options) {
|
|
|
20941
21316
|
}
|
|
20942
21317
|
function loadAppModel(root2) {
|
|
20943
21318
|
const file = paths.appModel(root2);
|
|
20944
|
-
if (!
|
|
21319
|
+
if (!existsSync10(file))
|
|
20945
21320
|
return void 0;
|
|
20946
21321
|
try {
|
|
20947
21322
|
return JSON.parse(readFileSync7(file, "utf8"));
|
|
@@ -20951,7 +21326,7 @@ function loadAppModel(root2) {
|
|
|
20951
21326
|
}
|
|
20952
21327
|
function loadBaselineSnapshot(root2, key) {
|
|
20953
21328
|
const file = snapshotFile(root2, key);
|
|
20954
|
-
if (!
|
|
21329
|
+
if (!existsSync10(file))
|
|
20955
21330
|
return void 0;
|
|
20956
21331
|
try {
|
|
20957
21332
|
return JSON.parse(readFileSync7(file, "utf8"));
|
|
@@ -21090,7 +21465,7 @@ function preflight(command, config, env = process.env) {
|
|
|
21090
21465
|
if (problems.length)
|
|
21091
21466
|
throw new ConfigError(problems.join("\n"));
|
|
21092
21467
|
}
|
|
21093
|
-
async function runAgentCommand(command, args, root2, config, log) {
|
|
21468
|
+
async function runAgentCommand(command, args, root2, config, log, configFile) {
|
|
21094
21469
|
const flags = parseAgentFlags(command, args);
|
|
21095
21470
|
preflight(command, config);
|
|
21096
21471
|
const workspace = new Workspace(root2);
|
|
@@ -21170,7 +21545,8 @@ ${problems.map((problem) => `- ${problem.split("\n")[0]}`).join("\n")}`);
|
|
|
21170
21545
|
allowFork: Boolean(flags.allowFork),
|
|
21171
21546
|
maxSteps: flags.steps,
|
|
21172
21547
|
// Without the flag, agents.review.claims decides.
|
|
21173
|
-
claims: flags.claims ? true : void 0
|
|
21548
|
+
claims: flags.claims ? true : void 0,
|
|
21549
|
+
configFile
|
|
21174
21550
|
});
|
|
21175
21551
|
const count = (verdict) => review.findings.filter((finding) => finding.verdict === verdict).length;
|
|
21176
21552
|
const claims = review.claims ? `${review.claims.length} claim(s), ` : "";
|
|
@@ -21223,7 +21599,7 @@ ${problems.map((problem) => `- ${problem.split("\n")[0]}`).join("\n")}`);
|
|
|
21223
21599
|
const result3 = await retestFix(root2, config, issue, fix, (fix.retests?.length ?? 0) + 1, { onLog: log });
|
|
21224
21600
|
applyRetest(config, fix, result3);
|
|
21225
21601
|
await workspace.saveFix(fix);
|
|
21226
|
-
log(`Retest: ${result3.outcome}
|
|
21602
|
+
log(`Retest: ${result3.outcome}: ${result3.reason}`);
|
|
21227
21603
|
return;
|
|
21228
21604
|
}
|
|
21229
21605
|
let app;
|
|
@@ -21289,13 +21665,13 @@ ${problems.map((problem) => `- ${problem.split("\n")[0]}`).join("\n")}`);
|
|
|
21289
21665
|
|
|
21290
21666
|
// ../cli/dist/commands/doctor.js
|
|
21291
21667
|
import { spawnSync } from "node:child_process";
|
|
21292
|
-
import { existsSync as
|
|
21668
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
21293
21669
|
function runChecks(root2, config) {
|
|
21294
21670
|
const checks = [];
|
|
21295
21671
|
checks.push({
|
|
21296
21672
|
name: "config",
|
|
21297
|
-
ok:
|
|
21298
|
-
detail:
|
|
21673
|
+
ok: existsSync11(paths.config(root2)),
|
|
21674
|
+
detail: existsSync11(paths.config(root2)) ? `${paths.config(root2)} found` : legacyLayout(root2) ?? "No .bugpatrol/bugpatrol.yml. Run `bugpatrol init`.",
|
|
21299
21675
|
fatal: true
|
|
21300
21676
|
});
|
|
21301
21677
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
@@ -21345,14 +21721,14 @@ function runChecks(root2, config) {
|
|
|
21345
21721
|
});
|
|
21346
21722
|
checks.push({
|
|
21347
21723
|
name: "app-model",
|
|
21348
|
-
ok:
|
|
21349
|
-
detail:
|
|
21724
|
+
ok: existsSync11(paths.appModel(root2)),
|
|
21725
|
+
detail: existsSync11(paths.appModel(root2)) ? "AppModel present" : "No AppModel. Run `bugpatrol recon`.",
|
|
21350
21726
|
fatal: false
|
|
21351
21727
|
});
|
|
21352
21728
|
checks.push({
|
|
21353
21729
|
name: "intent-ledger",
|
|
21354
21730
|
ok: true,
|
|
21355
|
-
detail:
|
|
21731
|
+
detail: existsSync11(paths.intents(root2)) ? "Intent Ledger present" : "No Intent Ledger yet (created on first accept)",
|
|
21356
21732
|
fatal: false
|
|
21357
21733
|
});
|
|
21358
21734
|
return checks;
|
|
@@ -21384,12 +21760,12 @@ function doctorExitCode(checks) {
|
|
|
21384
21760
|
|
|
21385
21761
|
// ../cli/dist/commands/init.js
|
|
21386
21762
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
21387
|
-
import { appendFileSync, existsSync as
|
|
21388
|
-
import { join as join24, relative as
|
|
21763
|
+
import { appendFileSync, existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
21764
|
+
import { join as join24, relative as relative11 } from "node:path";
|
|
21389
21765
|
import { createInterface } from "node:readline/promises";
|
|
21390
21766
|
|
|
21391
21767
|
// ../recon/dist/bringup.js
|
|
21392
|
-
import { existsSync as
|
|
21768
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
21393
21769
|
import { join as join23 } from "node:path";
|
|
21394
21770
|
function detectBringUp(root2) {
|
|
21395
21771
|
const candidates = [];
|
|
@@ -21406,7 +21782,7 @@ function detectBringUp(root2) {
|
|
|
21406
21782
|
}
|
|
21407
21783
|
}
|
|
21408
21784
|
const pkgPath = join23(root2, "package.json");
|
|
21409
|
-
if (
|
|
21785
|
+
if (existsSync12(pkgPath)) {
|
|
21410
21786
|
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
21411
21787
|
const manager = detectPackageManager(root2);
|
|
21412
21788
|
for (const script of ["dev", "start", "preview", "serve"]) {
|
|
@@ -21420,7 +21796,7 @@ function detectBringUp(root2) {
|
|
|
21420
21796
|
}
|
|
21421
21797
|
}
|
|
21422
21798
|
}
|
|
21423
|
-
if (
|
|
21799
|
+
if (existsSync12(join23(root2, "docker-compose.yml")) || existsSync12(join23(root2, "compose.yaml"))) {
|
|
21424
21800
|
candidates.push({
|
|
21425
21801
|
command: "docker compose up",
|
|
21426
21802
|
rung: "convention",
|
|
@@ -21428,17 +21804,17 @@ function detectBringUp(root2) {
|
|
|
21428
21804
|
confidence: 0.5
|
|
21429
21805
|
});
|
|
21430
21806
|
}
|
|
21431
|
-
if (
|
|
21807
|
+
if (existsSync12(join23(root2, "Procfile"))) {
|
|
21432
21808
|
candidates.push({ command: "foreman start", rung: "convention", source: "Procfile", confidence: 0.3 });
|
|
21433
21809
|
}
|
|
21434
21810
|
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
21435
21811
|
}
|
|
21436
21812
|
function detectPackageManager(root2) {
|
|
21437
|
-
if (
|
|
21813
|
+
if (existsSync12(join23(root2, "pnpm-lock.yaml")))
|
|
21438
21814
|
return "pnpm";
|
|
21439
|
-
if (
|
|
21815
|
+
if (existsSync12(join23(root2, "bun.lockb")))
|
|
21440
21816
|
return "bun";
|
|
21441
|
-
if (
|
|
21817
|
+
if (existsSync12(join23(root2, "yarn.lock")))
|
|
21442
21818
|
return "yarn";
|
|
21443
21819
|
return "npm";
|
|
21444
21820
|
}
|
|
@@ -21459,7 +21835,7 @@ function detectStack(root2) {
|
|
|
21459
21835
|
["app.json", "expo"]
|
|
21460
21836
|
];
|
|
21461
21837
|
for (const [file, name] of signatures) {
|
|
21462
|
-
if (!
|
|
21838
|
+
if (!existsSync12(join23(root2, file)))
|
|
21463
21839
|
continue;
|
|
21464
21840
|
framework ??= name;
|
|
21465
21841
|
detectedFrom.push(file);
|
|
@@ -21475,7 +21851,7 @@ function detectStack(root2) {
|
|
|
21475
21851
|
}
|
|
21476
21852
|
function listWorkflows(root2) {
|
|
21477
21853
|
const dir2 = join23(root2, ".github", "workflows");
|
|
21478
|
-
if (!
|
|
21854
|
+
if (!existsSync12(dir2))
|
|
21479
21855
|
return [];
|
|
21480
21856
|
try {
|
|
21481
21857
|
return readdirSync4(dir2).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).map((f) => join23(dir2, f));
|
|
@@ -21553,7 +21929,7 @@ function xcodeProjects(root2) {
|
|
|
21553
21929
|
return [".", "ios"].flatMap((dir2) => list2(join24(root2, dir2)).filter((name) => name.endsWith(".xcodeproj")).map((name) => join24(root2, dir2, name, "project.pbxproj")));
|
|
21554
21930
|
}
|
|
21555
21931
|
function gradleFiles(root2) {
|
|
21556
|
-
return ["android/app", "app"].flatMap((dir2) => ["build.gradle", "build.gradle.kts"].map((file) => join24(root2, dir2, file))).filter((file) =>
|
|
21932
|
+
return ["android/app", "app"].flatMap((dir2) => ["build.gradle", "build.gradle.kts"].map((file) => join24(root2, dir2, file))).filter((file) => existsSync13(file));
|
|
21557
21933
|
}
|
|
21558
21934
|
function detectAppId(root2, platform) {
|
|
21559
21935
|
const expo = readJson3(join24(root2, "app.json"))?.expo;
|
|
@@ -21600,7 +21976,7 @@ function detectPort(root2, pkg, framework) {
|
|
|
21600
21976
|
};
|
|
21601
21977
|
}
|
|
21602
21978
|
function mobilePlatform(root2) {
|
|
21603
|
-
const hasIos = xcodeProjects(root2).length > 0 ||
|
|
21979
|
+
const hasIos = xcodeProjects(root2).length > 0 || existsSync13(join24(root2, "app.json")) || existsSync13(join24(root2, "ios"));
|
|
21604
21980
|
return process.platform === "darwin" && hasIos ? "ios" : "android";
|
|
21605
21981
|
}
|
|
21606
21982
|
function detectApp(root2) {
|
|
@@ -21612,8 +21988,8 @@ function detectApp(root2) {
|
|
|
21612
21988
|
return { platform: "electron", start: "npx electron . --remote-debugging-port=9222", cdpPort: 9222, notes };
|
|
21613
21989
|
}
|
|
21614
21990
|
const reactNative = deps.expo || deps["react-native"];
|
|
21615
|
-
const nativeIos = !
|
|
21616
|
-
const nativeAndroid = !
|
|
21991
|
+
const nativeIos = !existsSync13(join24(root2, "package.json")) && xcodeProjects(root2).length > 0;
|
|
21992
|
+
const nativeAndroid = !existsSync13(join24(root2, "package.json")) && gradleFiles(root2).length > 0;
|
|
21617
21993
|
if (reactNative || nativeIos || nativeAndroid) {
|
|
21618
21994
|
const platform = nativeIos ? "ios" : nativeAndroid ? "android" : mobilePlatform(root2);
|
|
21619
21995
|
notes.push(`Platform ${platform}: detected from ${reactNative ? "package.json" : platform === "ios" ? "the Xcode project" : "the Gradle files"}.`);
|
|
@@ -21717,7 +22093,7 @@ Example: Sign in with the email {{TEST_EMAIL}} and the password {{TEST_PASSWORD}
|
|
|
21717
22093
|
`;
|
|
21718
22094
|
function ignoreData(root2) {
|
|
21719
22095
|
const gitignore = join24(root2, ".gitignore");
|
|
21720
|
-
const ignored =
|
|
22096
|
+
const ignored = existsSync13(gitignore) ? readFileSync9(gitignore, "utf8") : "";
|
|
21721
22097
|
const lines = ignored.split("\n");
|
|
21722
22098
|
const { dir: dir2 } = layout(root2);
|
|
21723
22099
|
const line = `${dir2}/${DATA_DIR}/`;
|
|
@@ -21735,7 +22111,7 @@ ${line}
|
|
|
21735
22111
|
return true;
|
|
21736
22112
|
}
|
|
21737
22113
|
function relativeConfig(root2) {
|
|
21738
|
-
const shown =
|
|
22114
|
+
const shown = relative11(process.cwd(), paths.config(root2));
|
|
21739
22115
|
return shown && !shown.startsWith("..") ? shown : paths.config(root2);
|
|
21740
22116
|
}
|
|
21741
22117
|
function writeInitialConfig(root2, answers) {
|
|
@@ -21743,14 +22119,14 @@ function writeInitialConfig(root2, answers) {
|
|
|
21743
22119
|
mkdirSync6(paths.dir(root2), { recursive: true });
|
|
21744
22120
|
const configPath = paths.config(root2);
|
|
21745
22121
|
const configName = `${BUGPATROL_DIR}/${CONFIG_FILENAME}`;
|
|
21746
|
-
if (
|
|
22122
|
+
if (existsSync13(configPath))
|
|
21747
22123
|
result3.skipped.push(configName);
|
|
21748
22124
|
else {
|
|
21749
22125
|
writeFileSync6(configPath, renderConfig(answers));
|
|
21750
22126
|
result3.written.push(configName);
|
|
21751
22127
|
}
|
|
21752
22128
|
const guidePath = join24(paths.dir(root2), "instructions.md");
|
|
21753
|
-
if (
|
|
22129
|
+
if (existsSync13(guidePath))
|
|
21754
22130
|
result3.skipped.push(`${BUGPATROL_DIR}/instructions.md`);
|
|
21755
22131
|
else {
|
|
21756
22132
|
writeFileSync6(guidePath, INSTRUCTIONS);
|
|
@@ -21922,7 +22298,7 @@ async function runInit(root2, args, log) {
|
|
|
21922
22298
|
log("\nNext: review the TODO markers, then run `bugpatrol doctor`.");
|
|
21923
22299
|
return;
|
|
21924
22300
|
}
|
|
21925
|
-
if (
|
|
22301
|
+
if (existsSync13(paths.config(root2))) {
|
|
21926
22302
|
throw new ConfigError(`${relativeConfig(root2)} already exists. Edit it, or delete it and run init again.`);
|
|
21927
22303
|
}
|
|
21928
22304
|
const legacy = legacyLayout(root2);
|
|
@@ -22004,9 +22380,6 @@ run:
|
|
|
22004
22380
|
timeoutMs: 60000
|
|
22005
22381
|
seeds: []
|
|
22006
22382
|
|
|
22007
|
-
auth:
|
|
22008
|
-
kind: none # none | form | storageState | seededUser | ssoBypass | manual
|
|
22009
|
-
|
|
22010
22383
|
viewports:
|
|
22011
22384
|
- { name: desktop, width: 1440, height: 900 }
|
|
22012
22385
|
- { name: mobile, width: 390, height: 844 }
|
|
@@ -22062,7 +22435,7 @@ production:
|
|
|
22062
22435
|
`;
|
|
22063
22436
|
const configPath = paths.config(root2);
|
|
22064
22437
|
mkdirSync6(paths.dir(root2), { recursive: true });
|
|
22065
|
-
if (!
|
|
22438
|
+
if (!existsSync13(configPath))
|
|
22066
22439
|
writeFileSync6(configPath, config);
|
|
22067
22440
|
else
|
|
22068
22441
|
notes.push(`${relativeConfig(root2)} already exists and was left untouched.`);
|
|
@@ -22110,7 +22483,7 @@ ${issues.length} issue(s). Status: new = filed by the judge, not on GitHub; file
|
|
|
22110
22483
|
role: "judge",
|
|
22111
22484
|
source: "human",
|
|
22112
22485
|
scope: issue.screenId,
|
|
22113
|
-
text: `Not a bug: ${issue.title} \u2014 ${reason2}
|
|
22486
|
+
text: fitLesson(`Not a bug: ${issue.title} \u2014 ${reason2}`)
|
|
22114
22487
|
}
|
|
22115
22488
|
]);
|
|
22116
22489
|
log(`Dismissed ${issue.id}: ${issue.title}`);
|
|
@@ -22165,6 +22538,9 @@ async function runMemoryCommand(args, root2, log) {
|
|
|
22165
22538
|
const text3 = args.slice(1).find((part, index, all) => !part.startsWith("--") && all[index - 1] !== "--role" && all[index - 1] !== "--scope");
|
|
22166
22539
|
if (!role || !roles3.has(role) || !text3?.trim())
|
|
22167
22540
|
throw new ConfigError('Usage: bugpatrol memory add --role explorer|judge|fixer "text" [--scope s]');
|
|
22541
|
+
const tooLong = lessonTooLong(text3);
|
|
22542
|
+
if (tooLong)
|
|
22543
|
+
throw new ConfigError(tooLong);
|
|
22168
22544
|
const [lesson] = await workspace.upsertLessons([
|
|
22169
22545
|
{ role, source: "human", scope: flag(args.slice(1), "--scope"), text: text3 }
|
|
22170
22546
|
]);
|
|
@@ -22361,7 +22737,7 @@ var COMMAND_HELP = {
|
|
|
22361
22737
|
init: `bugpatrol init [flags]
|
|
22362
22738
|
Find the app and the LLMs, and write .bugpatrol/bugpatrol.yml and .bugpatrol/instructions.md.
|
|
22363
22739
|
--yes, -y use the detected values, with no questions
|
|
22364
|
-
--platform <p> web | electron | ios | android
|
|
22740
|
+
--platform <p> web | electron | ios | android | api | desktop | cli
|
|
22365
22741
|
--start "<command>" the command that starts the app
|
|
22366
22742
|
--url <url> web: the app URL
|
|
22367
22743
|
--app-id <id> ios, android: the bundle ID or the package name
|
|
@@ -22461,7 +22837,7 @@ legacyEnv(process.env);
|
|
|
22461
22837
|
var root = findProjectRoot(process.cwd());
|
|
22462
22838
|
function version() {
|
|
22463
22839
|
if (true)
|
|
22464
|
-
return "0.
|
|
22840
|
+
return "0.3.0";
|
|
22465
22841
|
try {
|
|
22466
22842
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
22467
22843
|
} catch {
|
|
@@ -22539,7 +22915,7 @@ try {
|
|
|
22539
22915
|
case "replay": {
|
|
22540
22916
|
const config = readConfig();
|
|
22541
22917
|
await runAgentCommand(command, args, root, config, (message2) => process.stdout.write(`${message2}
|
|
22542
|
-
`));
|
|
22918
|
+
`), selected.configFile);
|
|
22543
22919
|
break;
|
|
22544
22920
|
}
|
|
22545
22921
|
case "promote": {
|
|
@@ -22606,7 +22982,7 @@ try {
|
|
|
22606
22982
|
onReady: (url) => {
|
|
22607
22983
|
process.stdout.write(`Bugpatrol dashboard on ${url}
|
|
22608
22984
|
`);
|
|
22609
|
-
process.stdout.write("Watching .bugpatrol/runs
|
|
22985
|
+
process.stdout.write("Watching .bugpatrol/runs/: runs appear as they finish. Ctrl-C to stop.\n");
|
|
22610
22986
|
}
|
|
22611
22987
|
});
|
|
22612
22988
|
const stop = () => {
|