bughunters 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +841 -383
- package/package.json +1 -1
- package/src/ui/app.js +8 -4
- package/src/ui/style.css +1 -0
package/dist/bin.js
CHANGED
|
@@ -197,8 +197,6 @@ var toleranceSchema = z.object({
|
|
|
197
197
|
})).default([])
|
|
198
198
|
}).default({});
|
|
199
199
|
var decisionsSchema = z.object({
|
|
200
|
-
decider: z.enum(["jev", "model"]).default("jev"),
|
|
201
|
-
jev: z.object({ via: z.enum(["auto", "typesafe", "openrouter", "vercel"]).default("auto") }).default({}),
|
|
202
200
|
model: z.object({
|
|
203
201
|
via: z.enum(["auto", "openrouter", "vercel", "openai", "anthropic", "custom"]).default("auto"),
|
|
204
202
|
name: z.string().default("")
|
|
@@ -310,17 +308,39 @@ function runtimeFor(role, fallback) {
|
|
|
310
308
|
var roleBase = {
|
|
311
309
|
enabled: z.boolean().default(true),
|
|
312
310
|
maxSteps: z.number().int().positive().default(60),
|
|
313
|
-
|
|
311
|
+
/** A cost limit for one session, in USD. No default: the team decides. */
|
|
312
|
+
budgetUsd: z.number().nonnegative().optional(),
|
|
314
313
|
timeoutMs: z.number().int().positive().default(20 * 60 * 1e3)
|
|
315
314
|
};
|
|
315
|
+
var AGENT_CHECKS = [
|
|
316
|
+
"usability/contrast",
|
|
317
|
+
"usability/tap-target",
|
|
318
|
+
"layout/overlap",
|
|
319
|
+
"layout/overflow",
|
|
320
|
+
"layout/occlusion",
|
|
321
|
+
"layout/zero-size-interactive",
|
|
322
|
+
"layout/off-viewport",
|
|
323
|
+
"layout/horizontal-scroll",
|
|
324
|
+
"layout/shift-versus-baseline",
|
|
325
|
+
"rendering/broken-imagery",
|
|
326
|
+
"rendering/unstyled-content",
|
|
327
|
+
"pixel-diff"
|
|
328
|
+
];
|
|
316
329
|
var agentsSchema = z.object({
|
|
330
|
+
/** Automatic checks on each recorded screen, for example [usability/contrast, usability/tap-target]. */
|
|
331
|
+
checks: z.array(z.enum(AGENT_CHECKS)).default([]),
|
|
317
332
|
memory: z.object({
|
|
318
333
|
enabled: z.boolean().default(true),
|
|
319
334
|
maxPerSession: z.number().int().positive().default(5),
|
|
320
335
|
reflectMaxSteps: z.number().int().positive().default(10),
|
|
321
|
-
reflectBudgetUsd: z.number().nonnegative().
|
|
336
|
+
reflectBudgetUsd: z.number().nonnegative().optional()
|
|
337
|
+
}).default({}),
|
|
338
|
+
explorer: z.object({
|
|
339
|
+
...roleBase,
|
|
340
|
+
use: runtimeFor("explorer", { runtime: "model" }),
|
|
341
|
+
/** Full coverage needs many actions: one step is one tool call. */
|
|
342
|
+
maxSteps: z.number().int().positive().default(150)
|
|
322
343
|
}).default({}),
|
|
323
|
-
explorer: z.object({ ...roleBase, use: runtimeFor("explorer", { runtime: "model" }) }).default({}),
|
|
324
344
|
judge: z.object({
|
|
325
345
|
...roleBase,
|
|
326
346
|
use: runtimeFor("judge", { runtime: "model" })
|
|
@@ -340,7 +360,7 @@ var agentsSchema = z.object({
|
|
|
340
360
|
/** Fix attempts in total; each attempt after the first gets the last verdict as feedback. */
|
|
341
361
|
attempts: z.number().int().positive().default(2),
|
|
342
362
|
maxSteps: z.number().int().positive().default(30),
|
|
343
|
-
budgetUsd: z.number().nonnegative().
|
|
363
|
+
budgetUsd: z.number().nonnegative().optional()
|
|
344
364
|
}).default({}),
|
|
345
365
|
minSeverity: z.enum(["cosmetic", "minor", "major", "critical"]).default("minor"),
|
|
346
366
|
/**
|
|
@@ -359,12 +379,25 @@ var agentsSchema = z.object({
|
|
|
359
379
|
assetsBranch: z.string().default("bughunters-assets"),
|
|
360
380
|
labels: z.array(z.string()).default(["bughunters"]),
|
|
361
381
|
/** The scope in PR titles, e.g. 'app'. Default: the scope in fixer.commitMessage. */
|
|
362
|
-
prScope: z.string().optional()
|
|
382
|
+
prScope: z.string().optional(),
|
|
383
|
+
/** After a PR opens, wait for its CI checks, and let the fixer fix a failed check. */
|
|
384
|
+
ci: z.object({
|
|
385
|
+
enabled: z.boolean().default(true),
|
|
386
|
+
/** Fixer attempts for each PR before Bughunters gives up and tells the team. */
|
|
387
|
+
attempts: z.number().int().nonnegative().default(2),
|
|
388
|
+
/** How long one cycle waits for pending checks. */
|
|
389
|
+
waitMinutes: z.number().nonnegative().default(20)
|
|
390
|
+
}).default({})
|
|
363
391
|
}).default({}),
|
|
364
392
|
patrol: z.object({
|
|
365
393
|
intervalMinutes: z.number().positive().default(30),
|
|
366
394
|
/** Stop after this many cycles; 0 means run until stopped. */
|
|
367
|
-
cycles: z.number().int().nonnegative().default(0)
|
|
395
|
+
cycles: z.number().int().nonnegative().default(0),
|
|
396
|
+
/**
|
|
397
|
+
* At the start of each cycle, fetch this `remote/branch` and check out its
|
|
398
|
+
* latest commit (detached) in the source repository. false keeps the checkout.
|
|
399
|
+
*/
|
|
400
|
+
pull: z.union([z.string().regex(/^[^/]+\/.+$/, "Use remote/branch, e.g. origin/main"), z.literal(false)]).default("origin/main")
|
|
368
401
|
}).default({})
|
|
369
402
|
}).default({});
|
|
370
403
|
var bughuntersConfigSchema = z.object({
|
|
@@ -695,9 +728,10 @@ Agents
|
|
|
695
728
|
bughunters fix [--issue <id>...]
|
|
696
729
|
bughunters retest --issue <id>
|
|
697
730
|
bughunters publish [--issue <id>] [--dry-run]
|
|
731
|
+
bughunters ci [--issue <id>] [--wait]
|
|
698
732
|
bughunters github sync
|
|
699
733
|
bughunters worktrees clean
|
|
700
|
-
bughunters patrol [--once]
|
|
734
|
+
bughunters patrol [--once] [--force]
|
|
701
735
|
bughunters replay <routine-id>
|
|
702
736
|
bughunters issue list | dismiss <id> --reason "..." [--by name] | reopen <id>
|
|
703
737
|
bughunters memory list [--role r] | add --role r "text" [--scope s]
|
|
@@ -743,7 +777,6 @@ var COMMAND_HELP = {
|
|
|
743
777
|
--app-id <id> ios, android: the bundle ID or the package name
|
|
744
778
|
--agent <a> the LLM for all agents: claude | codex | kimi | pi | openrouter | vercel | openai | anthropic
|
|
745
779
|
--explorer, --judge, --fixer <a> the LLM for one agent
|
|
746
|
-
--jev <route> auto | typesafe | openrouter | vercel
|
|
747
780
|
--gate write a starter config for the deterministic web gate
|
|
748
781
|
`,
|
|
749
782
|
explore: `bughunters explore [--goal "..."] [--steps N]
|
|
@@ -772,10 +805,20 @@ var COMMAND_HELP = {
|
|
|
772
805
|
--dry-run write the reports to .bughunters/runs/publish/, and open nothing
|
|
773
806
|
--issue <id> publish this issue only
|
|
774
807
|
`,
|
|
775
|
-
|
|
808
|
+
ci: `bughunters ci [--issue <id>]... [--wait]
|
|
809
|
+
Read the CI checks of each open Bughunters PR. When a check fails, the fixer gets its log,
|
|
810
|
+
and Bughunters pushes the new commit to the PR branch. After agents.github.ci.attempts tries, it stops.
|
|
811
|
+
The patrol runs this step after publish.
|
|
812
|
+
--wait wait for the running checks, up to agents.github.ci.waitMinutes
|
|
813
|
+
--issue <id> only the PR of this issue
|
|
814
|
+
`,
|
|
815
|
+
patrol: `bughunters patrol [--once] [--force]
|
|
776
816
|
Run the full cycle again and again: setup, explore, judge, teardown, fix, retest, publish.
|
|
777
817
|
The fixer and GitHub steps run only when you turn them on.
|
|
818
|
+
Explore and judge run only when the commit changed since the last full cycle. The fixes, retests,
|
|
819
|
+
publish, and CI checks run in each cycle.
|
|
778
820
|
--once run one cycle, then stop
|
|
821
|
+
--force explore and judge in the first cycle also when the commit did not change
|
|
779
822
|
`,
|
|
780
823
|
github: `bughunters github sync
|
|
781
824
|
Read the state of each PR and issue from GitHub. A closed PR becomes a lesson. An issue closed as not planned becomes a dismissal.
|
|
@@ -910,9 +953,9 @@ async function startApp(app, opts) {
|
|
|
910
953
|
if (command2.readyWhen) {
|
|
911
954
|
const ready = new RegExp(command2.readyWhen);
|
|
912
955
|
await new Promise((done, reject) => {
|
|
913
|
-
const
|
|
956
|
+
const wait2 = setTimeout(() => finish(new Error("timed out waiting for ready output")), command2.timeoutMs);
|
|
914
957
|
const finish = (error) => {
|
|
915
|
-
clearTimeout(
|
|
958
|
+
clearTimeout(wait2);
|
|
916
959
|
child.stdout?.off("data", check);
|
|
917
960
|
child.stderr?.off("data", check);
|
|
918
961
|
child.off("exit", onExit);
|
|
@@ -1037,8 +1080,8 @@ function serialized(file, update) {
|
|
|
1037
1080
|
}
|
|
1038
1081
|
async function listJson(dir) {
|
|
1039
1082
|
try {
|
|
1040
|
-
const
|
|
1041
|
-
return Promise.all(
|
|
1083
|
+
const files2 = (await readdir(dir)).filter((file) => file.endsWith(".json"));
|
|
1084
|
+
return Promise.all(files2.map(async (file) => await readJson(join3(dir, file))));
|
|
1042
1085
|
} catch (error) {
|
|
1043
1086
|
if (error.code === "ENOENT") {
|
|
1044
1087
|
return [];
|
|
@@ -1222,8 +1265,8 @@ var Workspace = class {
|
|
|
1222
1265
|
async saveScreenshot(sessionId, png, label) {
|
|
1223
1266
|
const dir = paths.session(this.root, sessionId);
|
|
1224
1267
|
await mkdir(dir, { recursive: true });
|
|
1225
|
-
const
|
|
1226
|
-
const number =
|
|
1268
|
+
const files2 = await readdir(dir);
|
|
1269
|
+
const number = files2.filter((file2) => /^\d{3}-.*\.png$/.test(file2)).length + 1;
|
|
1227
1270
|
const file = join3(dir, `${String(number).padStart(3, "0")}-${basename(label).replace(/[^a-zA-Z0-9_-]/g, "-")}.png`);
|
|
1228
1271
|
await writeFile(file, png);
|
|
1229
1272
|
return relative(this.root, file);
|
|
@@ -1456,64 +1499,7 @@ function toMcp(part) {
|
|
|
1456
1499
|
import { accessSync, constants } from "node:fs";
|
|
1457
1500
|
import { delimiter, join as join5 } from "node:path";
|
|
1458
1501
|
|
|
1459
|
-
// ../decide/dist/
|
|
1460
|
-
var JevDecider = class {
|
|
1461
|
-
options;
|
|
1462
|
-
name = "jev";
|
|
1463
|
-
endpoint;
|
|
1464
|
-
model;
|
|
1465
|
-
lastUsage;
|
|
1466
|
-
timeoutMs;
|
|
1467
|
-
constructor(options) {
|
|
1468
|
-
this.options = options;
|
|
1469
|
-
this.endpoint = options.endpoint ?? "https://api.typesafe.ai/v1/systemone";
|
|
1470
|
-
this.model = options.model ?? "jev-latest";
|
|
1471
|
-
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
1472
|
-
}
|
|
1473
|
-
async ask(state, questions) {
|
|
1474
|
-
const controller = new AbortController();
|
|
1475
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1476
|
-
try {
|
|
1477
|
-
const response3 = await fetch(this.endpoint, {
|
|
1478
|
-
method: "POST",
|
|
1479
|
-
headers: {
|
|
1480
|
-
"content-type": "application/json",
|
|
1481
|
-
authorization: `Bearer ${this.options.apiKey}`
|
|
1482
|
-
},
|
|
1483
|
-
// Every question is evaluated against the same state in one call, so
|
|
1484
|
-
// the entire per-screen analysis is a single round trip.
|
|
1485
|
-
body: JSON.stringify({
|
|
1486
|
-
model: this.model,
|
|
1487
|
-
state,
|
|
1488
|
-
questions: Object.fromEntries(Object.entries(questions).map(([id2, question]) => [
|
|
1489
|
-
id2,
|
|
1490
|
-
question.type === "score" ? { type: "score", instructions: question.instructions, criteria: question.legend } : question
|
|
1491
|
-
]))
|
|
1492
|
-
}),
|
|
1493
|
-
signal: controller.signal
|
|
1494
|
-
});
|
|
1495
|
-
if (!response3.ok) {
|
|
1496
|
-
throw new InfrastructureError(`Jev returned ${response3.status}: ${await safeText(response3)}`);
|
|
1497
|
-
}
|
|
1498
|
-
const body = await response3.json();
|
|
1499
|
-
this.lastUsage = usageFrom(body.usage) ?? { input: Math.ceil(state.length / 4), output: 0, estimated: true };
|
|
1500
|
-
return normalizeAnswers(body.answers ?? {}, questions);
|
|
1501
|
-
} catch (cause) {
|
|
1502
|
-
if (cause instanceof InfrastructureError)
|
|
1503
|
-
throw cause;
|
|
1504
|
-
throw new InfrastructureError("Jev request failed", { cause });
|
|
1505
|
-
} finally {
|
|
1506
|
-
clearTimeout(timer);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1510
|
-
async function safeText(response3) {
|
|
1511
|
-
try {
|
|
1512
|
-
return (await response3.text()).slice(0, 400);
|
|
1513
|
-
} catch {
|
|
1514
|
-
return "<no body>";
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1502
|
+
// ../decide/dist/answers.js
|
|
1517
1503
|
function normalizeAnswers(raw, questions) {
|
|
1518
1504
|
const out = {};
|
|
1519
1505
|
for (const [key, question] of Object.entries(questions)) {
|
|
@@ -1940,19 +1926,11 @@ var Budget = class {
|
|
|
1940
1926
|
return { spentUsd: this.spent, capUsd: this.capUsd };
|
|
1941
1927
|
}
|
|
1942
1928
|
};
|
|
1943
|
-
|
|
1944
|
-
function estimateDecisionCost(stateChars, decider = "jev") {
|
|
1945
|
-
if (decider === "jev")
|
|
1946
|
-
return stateChars / 4 * JEV_USD_PER_INPUT_TOKEN;
|
|
1929
|
+
function estimateDecisionCost(stateChars) {
|
|
1947
1930
|
return ((stateChars / 4 + 1500) * 3 + 900 * 15) / 1e6;
|
|
1948
1931
|
}
|
|
1949
1932
|
|
|
1950
1933
|
// ../decide/dist/index.js
|
|
1951
|
-
var JEV_ROUTES = {
|
|
1952
|
-
typesafe: { key: "TYPESAFE_API_KEY", endpoint: "https://api.typesafe.ai/v1/systemone", model: "jev-latest" },
|
|
1953
|
-
openrouter: { key: "OPENROUTER_API_KEY", endpoint: "https://openrouter.ai/api/v1/systemone", model: "jev-latest" },
|
|
1954
|
-
vercel: { key: "AI_GATEWAY_API_KEY", endpoint: "https://ai-gateway.vercel.sh/typesafe/v1/systemone", model: "typesafe-ai/jev" }
|
|
1955
|
-
};
|
|
1956
1934
|
var MODEL_KEYS = {
|
|
1957
1935
|
openrouter: "OPENROUTER_API_KEY",
|
|
1958
1936
|
vercel: "AI_GATEWAY_API_KEY",
|
|
@@ -1960,24 +1938,8 @@ var MODEL_KEYS = {
|
|
|
1960
1938
|
anthropic: "ANTHROPIC_API_KEY",
|
|
1961
1939
|
custom: "BUGHUNTERS_MODEL_API_KEY"
|
|
1962
1940
|
};
|
|
1963
|
-
var jevOrder = ["typesafe", "openrouter", "vercel"];
|
|
1964
1941
|
var modelOrder = ["openrouter", "vercel", "openai", "anthropic", "custom"];
|
|
1965
1942
|
function resolveDecider(config, env) {
|
|
1966
|
-
let jevMissing = "";
|
|
1967
|
-
if (config.decider === "jev") {
|
|
1968
|
-
const routes2 = config.jev.via === "auto" ? jevOrder : [config.jev.via];
|
|
1969
|
-
for (const via of routes2) {
|
|
1970
|
-
const route2 = JEV_ROUTES[via];
|
|
1971
|
-
const key = env[route2.key];
|
|
1972
|
-
if (key)
|
|
1973
|
-
return {
|
|
1974
|
-
decider: new JevDecider({ apiKey: key, endpoint: route2.endpoint, model: route2.model }),
|
|
1975
|
-
via: `jev:${via}`,
|
|
1976
|
-
reason: `Decider: jev via ${via} (${route2.model}).`
|
|
1977
|
-
};
|
|
1978
|
-
}
|
|
1979
|
-
jevMissing = `decider is jev, but no ${routes2.map((via) => JEV_ROUTES[via].key).join(", ").replace(/, ([^,]*)$/, " or $1")} is set`;
|
|
1980
|
-
}
|
|
1981
1943
|
const routes = config.model.via === "auto" ? modelOrder : [config.model.via];
|
|
1982
1944
|
for (const via of routes) {
|
|
1983
1945
|
const key = env[MODEL_KEYS[via]];
|
|
@@ -1988,14 +1950,11 @@ function resolveDecider(config, env) {
|
|
|
1988
1950
|
return {
|
|
1989
1951
|
decider: new ModelDecider({ via, apiKey: key, endpoint, model: name }),
|
|
1990
1952
|
via: `model:${via}`,
|
|
1991
|
-
reason:
|
|
1953
|
+
reason: `Decider: model via ${via} (${name}).`
|
|
1992
1954
|
};
|
|
1993
1955
|
}
|
|
1994
|
-
const
|
|
1995
|
-
return {
|
|
1996
|
-
via: "none",
|
|
1997
|
-
reason: `Decider: none, so the judge decides each finding, because ${jevMissing ? `${jevMissing}, and ` : ""}no ${modelMissing} is set. Set a Jev key to cut cost.`
|
|
1998
|
-
};
|
|
1956
|
+
const missing2 = routes.map((via) => via === "custom" ? "BUGHUNTERS_MODEL_ENDPOINT and BUGHUNTERS_MODEL_API_KEY" : MODEL_KEYS[via]).join(", ").replace(/, ([^,]*)$/, " or $1");
|
|
1957
|
+
return { via: "none", reason: `Decider: none, because no ${missing2} is set.` };
|
|
1999
1958
|
}
|
|
2000
1959
|
|
|
2001
1960
|
// ../agents/dist/runtime/cli.js
|
|
@@ -2187,7 +2146,7 @@ var ModelRuntime = class {
|
|
|
2187
2146
|
return outcome("done", calls.output);
|
|
2188
2147
|
}
|
|
2189
2148
|
}
|
|
2190
|
-
if (costUsd >= task.budgetUsd) {
|
|
2149
|
+
if (task.budgetUsd !== void 0 && costUsd >= task.budgetUsd) {
|
|
2191
2150
|
return outcome("budget", lastText);
|
|
2192
2151
|
}
|
|
2193
2152
|
}
|
|
@@ -2565,7 +2524,6 @@ var AgentSession = class {
|
|
|
2565
2524
|
lastScreenTrailIndex = 0;
|
|
2566
2525
|
/** Where the app was when lastScreenId was recorded. */
|
|
2567
2526
|
lastScreenLocation;
|
|
2568
|
-
decisionSpentUsd = 0;
|
|
2569
2527
|
/** Issues already counted as seen again in this session. */
|
|
2570
2528
|
seenIssues = /* @__PURE__ */ new Set();
|
|
2571
2529
|
cancelled = false;
|
|
@@ -2664,6 +2622,26 @@ ${lessons.map((lesson) => `- [${lesson.scope ?? "app"}] ${lesson.text}`).join("\
|
|
|
2664
2622
|
|
|
2665
2623
|
` : "";
|
|
2666
2624
|
}
|
|
2625
|
+
var MEMORY = {
|
|
2626
|
+
explorer: `- How to reach a screen that is hard to find, or a control that needs a special action.
|
|
2627
|
+
- How the app behaves in a way that looks wrong but is correct, for example a slow screen or a test account limit.`,
|
|
2628
|
+
judge: `- A behavior that the team or the code shows is intended, so the explorer does not report it again.
|
|
2629
|
+
- For the fixer: why a fix did not work, when the screenshots or the logs show the cause.`,
|
|
2630
|
+
fixer: `- Where the code for a feature lives, or a file that must change together with another file.
|
|
2631
|
+
- How to build, test, or run this codebase, for example a platform file (.web.tsx, .ios.tsx) that the app loads.
|
|
2632
|
+
- Why an earlier change did not work, when you find the cause.`
|
|
2633
|
+
};
|
|
2634
|
+
function memoryPart(role) {
|
|
2635
|
+
return `MEMORY
|
|
2636
|
+
If you have the save_lesson tool, use it when you learn a lasting fact about this app or its code that a
|
|
2637
|
+
later session needs. Write one short, specific sentence. Use "for" to save it for another role. Save facts
|
|
2638
|
+
such as these:
|
|
2639
|
+
${MEMORY[role]}
|
|
2640
|
+
Do not save the details of one bug, or a fact that the app guide or a lesson already says.
|
|
2641
|
+
When save_lesson shows a similar lesson, confirm it with "same" if it has the same meaning.
|
|
2642
|
+
|
|
2643
|
+
`;
|
|
2644
|
+
}
|
|
2667
2645
|
function explorerCommon(platform, instructions, lessons = []) {
|
|
2668
2646
|
return `HOW THE TOOLS WORK
|
|
2669
2647
|
- Each action tool returns a screenshot and a list of elements. Refs such as [e12] are valid only for
|
|
@@ -2679,24 +2657,38 @@ RULES
|
|
|
2679
2657
|
- Do not use the same failed action more than two times. Try another way, or report the problem.
|
|
2680
2658
|
- Keep your notes short. Spend your steps on actions, not on long thoughts.
|
|
2681
2659
|
|
|
2682
|
-
${lessonPart(lessons)}APP GUIDE
|
|
2660
|
+
${memoryPart("explorer")}${lessonPart(lessons)}APP GUIDE
|
|
2683
2661
|
${instructions.trim() || "(No guide was given. Explore carefully and do not change any data.)"}`;
|
|
2684
2662
|
}
|
|
2685
|
-
function explorerSystem(platform, instructions, lessons = []) {
|
|
2663
|
+
function explorerSystem(platform, instructions, lessons = [], checks = []) {
|
|
2664
|
+
const automatic = checks.length ? `record_screen also runs automatic checks (${checks.join(", ")}) and sends what they find to the QA lead.
|
|
2665
|
+
Do not report those findings again with report_bug. Report what the checks cannot see.
|
|
2666
|
+
` : "";
|
|
2686
2667
|
return `You are the explorer on an automated QA team. You use the app through tools, like a careful
|
|
2687
2668
|
human tester, and you look for problems that a real user would notice.
|
|
2688
2669
|
|
|
2689
|
-
YOUR JOB
|
|
2670
|
+
YOUR JOB IS FULL COVERAGE
|
|
2671
|
+
Your goal is to reach every screen of the app and to use every control on each screen. A bug hides in
|
|
2672
|
+
the screen or the button that nobody opened. Partial coverage is a failed session.
|
|
2673
|
+
|
|
2690
2674
|
1. Enter the app. Follow the app guide below. When you are in the app, call save_routine with the id
|
|
2691
2675
|
"enter-app" at once.
|
|
2692
2676
|
2. Record each screen that you see for the first time, before you do anything on it. Call record_screen
|
|
2693
2677
|
with a short kebab-case id, a name, and one sentence about what the screen is for. A dialog, a sheet,
|
|
2694
|
-
or a
|
|
2678
|
+
a menu, a tab, or a modal with its own content is a screen too.
|
|
2695
2679
|
3. Explore breadth first. Visit each item of the main navigation before you go deep into one area.
|
|
2696
|
-
4.
|
|
2680
|
+
4. On each screen, use every control that is safe: each button, link, tab, toggle, menu, list row,
|
|
2681
|
+
icon button, and field. Open each menu and each dialog, look at it, then close it. Scroll to the end
|
|
2682
|
+
of each list and each page, and use what appears. Try an empty and a wrong value in each form.
|
|
2683
|
+
5. Keep a list in your notes of the controls and the screens that you did not try yet. Go back to them.
|
|
2684
|
+
When a screen is done, go to the next screen that you did not visit.
|
|
2685
|
+
6. Report every problem with report_bug as soon as you see it, with the screenshot on screen. Then
|
|
2697
2686
|
continue. Do not stop at the first problem.
|
|
2698
|
-
|
|
2699
|
-
|
|
2687
|
+
7. Do not spend steps on waits and repeats. Wait for a slow screen at most two times. Use run_routine to
|
|
2688
|
+
go back to a known screen quickly.
|
|
2689
|
+
8. Call finish only when you tried every screen and every control that you found, or when your steps are
|
|
2690
|
+
almost gone. The summary names the screens you covered, the problems you reported, and the screens and
|
|
2691
|
+
controls that you did not reach, so the next session starts there.
|
|
2700
2692
|
|
|
2701
2693
|
WHAT TO REPORT
|
|
2702
2694
|
- A control that does nothing, or does the wrong thing.
|
|
@@ -2704,15 +2696,16 @@ WHAT TO REPORT
|
|
|
2704
2696
|
- Layout that is broken: content off screen, elements on top of each other, empty gaps, wrong alignment.
|
|
2705
2697
|
- Wrong, missing, or contradictory data. Placeholder text such as "undefined", "NaN", "null", or "{{".
|
|
2706
2698
|
- Error messages, crash screens, blank screens.
|
|
2699
|
+
- Console errors and failed requests. The tools list them under "Console errors" and "Failed requests".
|
|
2700
|
+
Report one when it breaks what the user sees or does, for example data that does not load or an action
|
|
2701
|
+
that fails. Name the error in what_is_wrong. Do not report noise that has no effect on the user.
|
|
2707
2702
|
- Use the id from record_screen for the screen you are on when you call report_bug. Use 'unrecorded' if needed.
|
|
2708
2703
|
- A loading state that does not end. If a screen still shows a spinner or skeleton rows after two waits
|
|
2709
2704
|
of 10 seconds, report it as a bug and continue somewhere else. Do not wait again and again.
|
|
2710
2705
|
- A dead end: a screen with no way back.
|
|
2711
2706
|
- Inconsistent UI: the same thing named or styled in two different ways.
|
|
2712
2707
|
Do not report the things that the app guide tells you to ignore.
|
|
2713
|
-
|
|
2714
|
-
to the QA lead. Do not report those findings again with report_bug. Report what the checks cannot see.
|
|
2715
|
-
|
|
2708
|
+
${automatic}
|
|
2716
2709
|
${explorerCommon(platform, instructions, lessons)}`;
|
|
2717
2710
|
}
|
|
2718
2711
|
function explorerRetestSystem(platform, instructions, lessons = []) {
|
|
@@ -2736,7 +2729,9 @@ Call view_retest to inspect the before and after screenshots. Then call verdict.
|
|
|
2736
2729
|
fixed: the problem is gone on EVERY affected screen and nothing new is broken.
|
|
2737
2730
|
not-fixed: the problem is still visible on at least one screen. Name it.
|
|
2738
2731
|
unclear: the explorer did not reach at least one screen and none shows the problem still present. Name the missing screens.
|
|
2739
|
-
|
|
2732
|
+
Give a cause only when the screenshots or the logs show it. Do not guess one.
|
|
2733
|
+
|
|
2734
|
+
${memoryPart("judge")}${lessonPart(lessons)}`;
|
|
2740
2735
|
}
|
|
2741
2736
|
function explorerPrompt(input) {
|
|
2742
2737
|
const screens = input.screens.length ? input.screens.map((screen) => `- ${screen.id}: ${screen.name}. ${screen.description}`).join("\n") : "(none yet)";
|
|
@@ -2744,7 +2739,7 @@ function explorerPrompt(input) {
|
|
|
2744
2739
|
const health = routine.lastReplay ? routine.lastReplay.ok ? "works" : routine.lastReplay.onFixBuild ? "BROKEN (seen on a fix build), repair it" : "BROKEN, repair it" : "not replayed";
|
|
2745
2740
|
return `- ${routine.id} (${routine.steps.length} steps, ${health}): ${routine.description}`;
|
|
2746
2741
|
}).join("\n") : "(none yet)";
|
|
2747
|
-
const goal = input.goal ?? (input.screens.length ? 'Enter the app. If "enter-app" works, use run_routine for it. Then
|
|
2742
|
+
const goal = input.goal ?? (input.screens.length ? 'Enter the app. If "enter-app" works, use run_routine for it. Then find the screens that are NOT in the known list below, and record and test each one. On a known screen, use each control that an earlier session did not try. The goal is every screen and every control of the app.' : "This is the first visit. Enter the app, then map every screen that you can reach, use every safe control on each one, and report every problem.");
|
|
2748
2743
|
return `GOAL
|
|
2749
2744
|
${goal}
|
|
2750
2745
|
|
|
@@ -2798,11 +2793,12 @@ HOW TO WRITE AN ISSUE
|
|
|
2798
2793
|
|
|
2799
2794
|
Be strict. A report that nobody can act on costs the team time. When you have decided every candidate,
|
|
2800
2795
|
call finish with one sentence per decision.
|
|
2801
|
-
|
|
2796
|
+
|
|
2797
|
+
${memoryPart("judge")}${lessonPart(lessons)}`;
|
|
2802
2798
|
}
|
|
2803
2799
|
function judgePrompt(sessionIds, candidates, issues) {
|
|
2804
2800
|
const list3 = candidates.map((candidate) => {
|
|
2805
|
-
const route2 = candidate.route ? `
|
|
2801
|
+
const route2 = candidate.route ? ` Route: ${candidate.route.reason}` : "";
|
|
2806
2802
|
return `- ${candidate.id} [${candidate.source}, ${candidate.severity}] on ${candidate.screenId ?? "unknown screen"}: ${candidate.summary}.${route2}`;
|
|
2807
2803
|
}).join("\n");
|
|
2808
2804
|
const open2 = issues.filter((issue) => issue.status !== "dismissed" && issue.status !== "fixed");
|
|
@@ -2855,7 +2851,111 @@ a QA lead confirmed it. Fix the cause, not the symptom.
|
|
|
2855
2851
|
your summary, with the file and line. Bughunters shows your explanation to the team.
|
|
2856
2852
|
- Do not commit and do not push. Bughunters commits the change on its own branch.
|
|
2857
2853
|
- End with a short summary: the cause, the change, and what you ran to verify it.
|
|
2858
|
-
|
|
2854
|
+
|
|
2855
|
+
${memoryPart("fixer")}${lessonPart(lessons)}`;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// ../agents/dist/tools/memory.js
|
|
2859
|
+
var roles = ["explorer", "judge", "fixer"];
|
|
2860
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
2861
|
+
"the",
|
|
2862
|
+
"and",
|
|
2863
|
+
"for",
|
|
2864
|
+
"with",
|
|
2865
|
+
"that",
|
|
2866
|
+
"this",
|
|
2867
|
+
"from",
|
|
2868
|
+
"into",
|
|
2869
|
+
"not",
|
|
2870
|
+
"are",
|
|
2871
|
+
"was",
|
|
2872
|
+
"has",
|
|
2873
|
+
"have",
|
|
2874
|
+
"its",
|
|
2875
|
+
"use",
|
|
2876
|
+
"when",
|
|
2877
|
+
"then",
|
|
2878
|
+
"than",
|
|
2879
|
+
"there",
|
|
2880
|
+
"can",
|
|
2881
|
+
"does",
|
|
2882
|
+
"will",
|
|
2883
|
+
"must",
|
|
2884
|
+
"only",
|
|
2885
|
+
"also",
|
|
2886
|
+
"you"
|
|
2887
|
+
]);
|
|
2888
|
+
function words(text3) {
|
|
2889
|
+
return new Set(text3.toLowerCase().match(/[a-z0-9_.:/-]+/g)?.map((word) => word.replace(/[.:]+$/, "").replace(/s$/, "")).filter((word) => word.length > 2 && !STOP.has(word)) ?? []);
|
|
2890
|
+
}
|
|
2891
|
+
function similarLesson(lessons, role, text3) {
|
|
2892
|
+
const mine = words(text3);
|
|
2893
|
+
let best;
|
|
2894
|
+
for (const lesson of lessons) {
|
|
2895
|
+
if (lesson.role !== role || lesson.retired)
|
|
2896
|
+
continue;
|
|
2897
|
+
const theirs = words(lesson.text);
|
|
2898
|
+
const shared = [...mine].filter((word) => theirs.has(word)).length;
|
|
2899
|
+
const score = shared / Math.max(1, Math.min(mine.size, theirs.size));
|
|
2900
|
+
if (shared >= 4 && score >= 0.6 && (!best || score > best.score))
|
|
2901
|
+
best = { lesson, score };
|
|
2902
|
+
}
|
|
2903
|
+
return best?.lesson;
|
|
2904
|
+
}
|
|
2905
|
+
function lessonTools(session, role) {
|
|
2906
|
+
const memory = session.config.agents.memory;
|
|
2907
|
+
if (!memory.enabled)
|
|
2908
|
+
return [];
|
|
2909
|
+
let saved = 0;
|
|
2910
|
+
return [{
|
|
2911
|
+
name: "save_lesson",
|
|
2912
|
+
description: 'Save one lasting fact about this app or its code for later sessions: one short, specific sentence. Use "for" to save it for another role (explorer, judge, or fixer). When a similar lesson exists, the tool shows it and saves nothing.',
|
|
2913
|
+
inputSchema: {
|
|
2914
|
+
type: "object",
|
|
2915
|
+
properties: {
|
|
2916
|
+
text: { type: "string" },
|
|
2917
|
+
for: { type: "string", enum: roles },
|
|
2918
|
+
scope: { type: "string", description: "A screen id, or leave it out for the whole app." },
|
|
2919
|
+
same: { type: "string", description: "The id of a saved lesson that has the same meaning. It confirms that lesson, and saves no new one." },
|
|
2920
|
+
different: { type: "boolean", description: "true saves the lesson also when a similar lesson exists." }
|
|
2921
|
+
},
|
|
2922
|
+
required: ["text"],
|
|
2923
|
+
additionalProperties: false
|
|
2924
|
+
},
|
|
2925
|
+
async run(input) {
|
|
2926
|
+
if (saved >= memory.maxPerSession) {
|
|
2927
|
+
return { content: [{ type: "text", text: "The lesson limit for this session is reached." }] };
|
|
2928
|
+
}
|
|
2929
|
+
const text3 = session.vars.redact(String(input.text ?? "")).trim().slice(0, 240);
|
|
2930
|
+
if (!text3)
|
|
2931
|
+
return { content: [{ type: "text", text: "The lesson needs a text." }], isError: true };
|
|
2932
|
+
const target = roles.includes(input.for) ? input.for : role;
|
|
2933
|
+
const { lessons } = await session.workspace.readMemory();
|
|
2934
|
+
const confirmed = input.same ? lessons.find((lesson2) => lesson2.id === input.same && !lesson2.retired) : void 0;
|
|
2935
|
+
if (confirmed) {
|
|
2936
|
+
await session.workspace.upsertLessons([{
|
|
2937
|
+
role: confirmed.role,
|
|
2938
|
+
text: confirmed.text,
|
|
2939
|
+
source: confirmed.source,
|
|
2940
|
+
scope: confirmed.scope
|
|
2941
|
+
}]);
|
|
2942
|
+
return { content: [{ type: "text", text: `Confirmed ${confirmed.id}. It has one more hit now.` }] };
|
|
2943
|
+
}
|
|
2944
|
+
const similar = input.different === true ? void 0 : similarLesson(lessons, target, text3);
|
|
2945
|
+
if (similar && similar.text.toLowerCase() !== text3.toLowerCase()) {
|
|
2946
|
+
return { content: [{ type: "text", text: `The ${target} already has a similar lesson: ${similar.id}: "${similar.text}" If it has the same meaning, call save_lesson with same: "${similar.id}". If your lesson tells something new, call it again with different: true.` }] };
|
|
2947
|
+
}
|
|
2948
|
+
const [lesson] = await session.workspace.upsertLessons([{
|
|
2949
|
+
role: target,
|
|
2950
|
+
text: text3,
|
|
2951
|
+
source: "agent",
|
|
2952
|
+
scope: input.scope ? String(input.scope) : void 0
|
|
2953
|
+
}]);
|
|
2954
|
+
saved++;
|
|
2955
|
+
session.emit({ kind: "lesson", summary: `Learned (${target}): ${lesson.text}` });
|
|
2956
|
+
return { content: [{ type: "text", text: `Saved ${lesson.id} for the ${target}.` }] };
|
|
2957
|
+
}
|
|
2958
|
+
}];
|
|
2859
2959
|
}
|
|
2860
2960
|
|
|
2861
2961
|
// ../agents/dist/roles/fixer.js
|
|
@@ -3197,10 +3297,10 @@ ${issue.body}
|
|
|
3197
3297
|
Evidence:
|
|
3198
3298
|
${evidence}
|
|
3199
3299
|
Reproduction: ${repro}` + (last ? `
|
|
3200
|
-
Your last change did not fix the issue. The QA lead said: ${last.reason}. After screenshots:
|
|
3300
|
+
Your last change did not fix the issue in the running app. The QA lead said: ${last.reason}. After screenshots:
|
|
3201
3301
|
${after}
|
|
3202
|
-
|
|
3203
|
-
tools: runtime.label.startsWith("cli:") ? [
|
|
3302
|
+
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.` : ""),
|
|
3303
|
+
tools: [...runtime.label.startsWith("cli:") ? [] : modelTools(worktree), ...lessonTools(session, "fixer"), finishTool()],
|
|
3204
3304
|
maxSteps: config.maxSteps,
|
|
3205
3305
|
budgetUsd: config.budgetUsd,
|
|
3206
3306
|
timeoutMs: config.timeoutMs
|
|
@@ -3227,12 +3327,12 @@ function stepWords(step) {
|
|
|
3227
3327
|
}
|
|
3228
3328
|
|
|
3229
3329
|
// ../agents/dist/github.js
|
|
3230
|
-
var defaultGh = (args2, opts) => new Promise((
|
|
3330
|
+
var defaultGh = (args2, opts) => new Promise((resolve12, reject) => {
|
|
3231
3331
|
const child = execFile2("gh", args2, { cwd: opts?.cwd, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
3232
3332
|
if (error)
|
|
3233
3333
|
reject(Object.assign(error, { stderr, stdout }));
|
|
3234
3334
|
else
|
|
3235
|
-
|
|
3335
|
+
resolve12(stdout.trim());
|
|
3236
3336
|
});
|
|
3237
3337
|
child.stdin?.end(opts?.input);
|
|
3238
3338
|
});
|
|
@@ -4472,14 +4572,15 @@ async function evaluateScreen(session, input) {
|
|
|
4472
4572
|
if (firstSight)
|
|
4473
4573
|
await saveBaseline(session, baseline, screenshot, snapshot);
|
|
4474
4574
|
const native = observation.platform === "ios" || observation.platform === "android";
|
|
4575
|
+
const enabled = new Set(session.config.agents.checks);
|
|
4475
4576
|
const violations = evaluateAll(snapshot, {
|
|
4476
4577
|
baseline: baseline.snapshot,
|
|
4477
|
-
disabled: native
|
|
4578
|
+
disabled: RULES.map((rule) => rule.id).filter((id2) => !enabled.has(id2) || native && NATIVE_DISABLED.includes(id2))
|
|
4478
4579
|
});
|
|
4479
4580
|
const entries = groupViolations(violations);
|
|
4480
4581
|
let diffPath;
|
|
4481
4582
|
let visual;
|
|
4482
|
-
if (!firstSight) {
|
|
4583
|
+
if (!firstSight && enabled.has("pixel-diff")) {
|
|
4483
4584
|
diffPath = join8(paths.session(session.root, session.sessionId), `${screenId}-diff.png`);
|
|
4484
4585
|
const comparison = await diff({
|
|
4485
4586
|
baselinePath: baseline.png,
|
|
@@ -4499,7 +4600,7 @@ async function evaluateScreen(session, input) {
|
|
|
4499
4600
|
await closeAbsentChecks(session, screenId, new Set(entries.map((entry) => entryFingerprint(screenId, entry))));
|
|
4500
4601
|
if (fresh.length === 0)
|
|
4501
4602
|
return [];
|
|
4502
|
-
const routeResult =
|
|
4603
|
+
const routeResult = { to: "judge", reason: "Automatic check" };
|
|
4503
4604
|
const candidates = [];
|
|
4504
4605
|
for (const entry of fresh) {
|
|
4505
4606
|
const candidate = toCandidate(session, screenId, entry, {
|
|
@@ -4608,6 +4709,7 @@ async function addOccurrence(session, issueId) {
|
|
|
4608
4709
|
}
|
|
4609
4710
|
async function closeAbsentChecks(session, screenId, fired) {
|
|
4610
4711
|
const closed = [];
|
|
4712
|
+
const enabled = new Set(session.config.agents.checks);
|
|
4611
4713
|
const ids = new Set((await session.workspace.listIssues()).filter((issue) => issue.screenId === screenId && ["new", "filed", "fixing", "fix-proposed"].includes(issue.status)).map((issue) => issue.id));
|
|
4612
4714
|
const sources = /* @__PURE__ */ new Map();
|
|
4613
4715
|
for (const record of await session.workspace.listSessions(Infinity)) {
|
|
@@ -4621,6 +4723,8 @@ async function closeAbsentChecks(session, screenId, fired) {
|
|
|
4621
4723
|
const candidates = issue.candidateIds.map((candidateId) => sources.get(candidateId));
|
|
4622
4724
|
if (candidates.some((candidate) => !candidate || candidate.source === "explorer" || candidate.screenId !== screenId))
|
|
4623
4725
|
continue;
|
|
4726
|
+
if (candidates.some((candidate) => !enabled.has(candidate.ruleId ?? candidate.source)))
|
|
4727
|
+
continue;
|
|
4624
4728
|
if (candidates.some((candidate) => fired.has(candidate.fingerprint))) {
|
|
4625
4729
|
if (issue.notSeen)
|
|
4626
4730
|
await session.workspace.saveIssue({ ...issue, notSeen: 0 });
|
|
@@ -4670,57 +4774,6 @@ function toCandidate(session, screenId, entry, extra) {
|
|
|
4670
4774
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4671
4775
|
};
|
|
4672
4776
|
}
|
|
4673
|
-
async function decide(session, screenId, snapshot, violations, visual) {
|
|
4674
|
-
const config = session.config.decisions;
|
|
4675
|
-
const { decider } = resolveDecider(config, process.env);
|
|
4676
|
-
if (!decider)
|
|
4677
|
-
return { to: "judge", reason: "no decider key is set, so the judge decides" };
|
|
4678
|
-
const { text: text3 } = buildState({
|
|
4679
|
-
screen: { id: screenId, description: snapshot.title ?? snapshot.url },
|
|
4680
|
-
product: { summary: "", audience: "", domainVocabulary: [] },
|
|
4681
|
-
assertions: violationsToAssertions(violations),
|
|
4682
|
-
diff: visual ? {
|
|
4683
|
-
changedPixels: visual.changedPixels,
|
|
4684
|
-
changedPercent: visual.changedFraction * 100,
|
|
4685
|
-
maskedPercent: visual.maskedFraction * 100,
|
|
4686
|
-
regions: visual.regions.length
|
|
4687
|
-
} : void 0,
|
|
4688
|
-
console: snapshot.consoleErrors
|
|
4689
|
-
});
|
|
4690
|
-
const budget = new Budget(config.budget.perRunUsd);
|
|
4691
|
-
budget.record(session.decisionSpentUsd);
|
|
4692
|
-
const estimate = estimateDecisionCost(text3.length, decider.name);
|
|
4693
|
-
if (!budget.canSpend(estimate)) {
|
|
4694
|
-
return { to: "judge", reason: "decider budget exhausted" };
|
|
4695
|
-
}
|
|
4696
|
-
try {
|
|
4697
|
-
const answers = await decider.ask(text3, SCREEN_QUESTIONS);
|
|
4698
|
-
session.decisionSpentUsd += estimate;
|
|
4699
|
-
const result3 = route({
|
|
4700
|
-
answers,
|
|
4701
|
-
thresholds: config.confidence,
|
|
4702
|
-
hasDeterministicRegression: false,
|
|
4703
|
-
matchedLedger: false
|
|
4704
|
-
});
|
|
4705
|
-
session.emit({
|
|
4706
|
-
kind: "tool-result",
|
|
4707
|
-
tool: "decider",
|
|
4708
|
-
summary: `Decider routed ${screenId} to ${result3.route}`,
|
|
4709
|
-
costUsd: estimate,
|
|
4710
|
-
tokens: decider.lastUsage,
|
|
4711
|
-
model: decider.model ?? decider.name
|
|
4712
|
-
});
|
|
4713
|
-
const choice = answers["route"];
|
|
4714
|
-
const confidence = choice?.confidence;
|
|
4715
|
-
return {
|
|
4716
|
-
to: result3.route === "ignore" || result3.route === "intent" ? "ignore" : "judge",
|
|
4717
|
-
reason: result3.reason,
|
|
4718
|
-
confidence
|
|
4719
|
-
};
|
|
4720
|
-
} catch {
|
|
4721
|
-
return { to: "judge", reason: "decider unavailable" };
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
4777
|
|
|
4725
4778
|
// ../agents/dist/replay.js
|
|
4726
4779
|
async function replaySteps(session, steps, opts = {}) {
|
|
@@ -4851,7 +4904,7 @@ async function actWhenReady(driver, action, session, windowMs) {
|
|
|
4851
4904
|
if (result3.ok || !retry || Date.now() >= deadline || session.cancelled) {
|
|
4852
4905
|
return { ...result3, degraded };
|
|
4853
4906
|
}
|
|
4854
|
-
await new Promise((
|
|
4907
|
+
await new Promise((resolve12) => setTimeout(resolve12, 500));
|
|
4855
4908
|
}
|
|
4856
4909
|
}
|
|
4857
4910
|
function toAction(step, session) {
|
|
@@ -4897,14 +4950,20 @@ function elementLine(element) {
|
|
|
4897
4950
|
].filter(Boolean).join(" ");
|
|
4898
4951
|
return `[${element.ref}] ${element.role} ${JSON.stringify(element.name)} (${box.x},${box.y} ${box.width}x${box.height})${flags ? ` ${flags}` : ""}`;
|
|
4899
4952
|
}
|
|
4953
|
+
function errorLines(observation) {
|
|
4954
|
+
const unique = (items = []) => [...new Set(items)].map((item) => item.slice(0, 200));
|
|
4955
|
+
const lines = (label, items) => items.length ? [`${label}:`, ...items.slice(0, 5).map((item) => `- ${item}`), ...items.length > 5 ? [`- (${items.length - 5} more)`] : []] : [];
|
|
4956
|
+
return [...lines("Console errors", unique(observation.consoleErrors)), ...lines("Failed requests", unique(observation.networkErrors))];
|
|
4957
|
+
}
|
|
4900
4958
|
function observationText(observation) {
|
|
4901
4959
|
const windows = observation.windows?.map((window) => `${window.active ? "*" : " "} ${window.title} (${window.location})`).join("; ") ?? "(none)";
|
|
4902
4960
|
return [
|
|
4903
4961
|
`Location: ${observation.location}`,
|
|
4904
4962
|
`Windows: ${windows}`,
|
|
4963
|
+
...errorLines(observation),
|
|
4905
4964
|
"Elements:",
|
|
4906
4965
|
...observation.elements.slice(0, 117).map(elementLine)
|
|
4907
|
-
].slice(0,
|
|
4966
|
+
].slice(0, 130).join("\n");
|
|
4908
4967
|
}
|
|
4909
4968
|
async function observe(session, label, summary) {
|
|
4910
4969
|
const observation = await session.driver.observe();
|
|
@@ -5232,7 +5291,7 @@ function explorerTools(session) {
|
|
|
5232
5291
|
},
|
|
5233
5292
|
{
|
|
5234
5293
|
name: "record_screen",
|
|
5235
|
-
description: "Record this screen
|
|
5294
|
+
description: "Record this screen and its routine in the app map.",
|
|
5236
5295
|
inputSchema: schema({ id: string, name: string, description: string }, ["id", "name", "description"]),
|
|
5237
5296
|
async run(input) {
|
|
5238
5297
|
const id2 = slug(arg(input, "id"));
|
|
@@ -5242,8 +5301,7 @@ function explorerTools(session) {
|
|
|
5242
5301
|
await session.driver.settle();
|
|
5243
5302
|
const observation = await session.driver.observe();
|
|
5244
5303
|
const screenshot = await session.capture(observation, id2);
|
|
5245
|
-
const
|
|
5246
|
-
const findings = await evaluateScreen(session, { screenId: id2, observation, snapshot });
|
|
5304
|
+
const findings = session.config.agents.checks.length ? await evaluateScreen(session, { screenId: id2, observation, snapshot: session.driver.snapshot(observation, id2) }) : [];
|
|
5247
5305
|
const previous = session.lastScreenId;
|
|
5248
5306
|
const edge = transition(session, id2);
|
|
5249
5307
|
const routineId = `screen-${id2}`;
|
|
@@ -5271,15 +5329,17 @@ function explorerTools(session) {
|
|
|
5271
5329
|
session.lastScreenTrailIndex = session.trail.length;
|
|
5272
5330
|
const screens = (await session.workspace.readAppMap())?.screens ?? [];
|
|
5273
5331
|
const names = screens.map((screen) => screen.id).join(", ") || id2;
|
|
5274
|
-
const found = findings.length ?
|
|
5332
|
+
const found = !session.config.agents.checks.length ? "" : findings.length ? ` ${findings.length} automatic finding(s): ${findings.map((item) => item.summary).join("; ")}.` : " No automatic findings.";
|
|
5333
|
+
const errors = errorLines(observation);
|
|
5275
5334
|
session.emit({
|
|
5276
5335
|
kind: "screen",
|
|
5277
|
-
summary: `Recorded ${name} (${
|
|
5336
|
+
summary: `Recorded ${name}${findings.length ? ` (${findings.length} automatic finding(s))` : ""}`,
|
|
5278
5337
|
screenId: id2,
|
|
5279
5338
|
screenshot
|
|
5280
5339
|
});
|
|
5281
5340
|
return {
|
|
5282
|
-
...text(`Recorded ${name}
|
|
5341
|
+
...text(session.vars.redact(`Recorded ${name}.${found} Known screens: ${names}${errors.length ? `
|
|
5342
|
+
${errors.join("\n")}` : ""}`)),
|
|
5283
5343
|
meta: { summary: "", screenshot, screenId: id2 }
|
|
5284
5344
|
};
|
|
5285
5345
|
}
|
|
@@ -5395,6 +5455,7 @@ Expected: ${arg(input, "expected")}`),
|
|
|
5395
5455
|
run: (input) => act(session, { kind: "window", match: arg(input, "match") })
|
|
5396
5456
|
});
|
|
5397
5457
|
}
|
|
5458
|
+
tools.push(...lessonTools(session, "explorer"));
|
|
5398
5459
|
return tools;
|
|
5399
5460
|
}
|
|
5400
5461
|
|
|
@@ -5412,9 +5473,9 @@ var string2 = { type: "string" };
|
|
|
5412
5473
|
function issueTitle(value) {
|
|
5413
5474
|
if (value.length <= 80)
|
|
5414
5475
|
return value;
|
|
5415
|
-
const
|
|
5476
|
+
const words2 = value.trim().split(/\s+/);
|
|
5416
5477
|
let title = "";
|
|
5417
|
-
for (const word of
|
|
5478
|
+
for (const word of words2) {
|
|
5418
5479
|
const next = title ? `${title} ${word}` : word;
|
|
5419
5480
|
if (next.length > 79)
|
|
5420
5481
|
break;
|
|
@@ -5467,6 +5528,7 @@ function triageEntries(candidates, decision, reason, issueId) {
|
|
|
5467
5528
|
}
|
|
5468
5529
|
function judgeTools(session, sessionIds, runtimeLabel) {
|
|
5469
5530
|
return [
|
|
5531
|
+
...lessonTools(session, "judge"),
|
|
5470
5532
|
{
|
|
5471
5533
|
name: "list_candidates",
|
|
5472
5534
|
description: "List undecided candidates and existing issues.",
|
|
@@ -5823,7 +5885,7 @@ async function runExplorer(session, runtime, opts = {}) {
|
|
|
5823
5885
|
const task = {
|
|
5824
5886
|
role: "explorer",
|
|
5825
5887
|
sessionId: session.sessionId,
|
|
5826
|
-
system: explorerSystem(session.config.app.platform, instructions, lessonsFor(await session.workspace.readMemory(), "explorer")),
|
|
5888
|
+
system: explorerSystem(session.config.app.platform, instructions, lessonsFor(await session.workspace.readMemory(), "explorer"), session.config.agents.checks),
|
|
5827
5889
|
prompt: explorerPrompt({
|
|
5828
5890
|
goal: opts.goal,
|
|
5829
5891
|
screens: map?.screens ?? [],
|
|
@@ -5846,13 +5908,13 @@ async function runExplorer(session, runtime, opts = {}) {
|
|
|
5846
5908
|
await session.workspace.endSession(session.sessionId, {
|
|
5847
5909
|
status: outcome.stop === "error" ? "failed" : "finished",
|
|
5848
5910
|
steps: outcome.steps,
|
|
5849
|
-
costUsd: outcome.costUsd
|
|
5911
|
+
costUsd: outcome.costUsd,
|
|
5850
5912
|
summary: outcome.summary,
|
|
5851
5913
|
candidates: candidates.length,
|
|
5852
5914
|
screensFound: screens.map((item) => item.id)
|
|
5853
5915
|
});
|
|
5854
5916
|
session.emit({ kind: "session-end", summary: outcome.summary ?? `Explorer stopped: ${outcome.stop}` });
|
|
5855
|
-
await session.idle(outcome.costUsd
|
|
5917
|
+
await session.idle(outcome.costUsd);
|
|
5856
5918
|
const learned = await reflectOnSession(session.root, session.config, session.sessionId, { vars: session.vars, onLog: (message2) => session.emit({ kind: "error", summary: message2 }) });
|
|
5857
5919
|
if (learned)
|
|
5858
5920
|
session.emit({ kind: "lesson", summary: `Saved ${learned} lesson(s) for the next sessions` });
|
|
@@ -6026,7 +6088,7 @@ async function deleteBranch(fix, onLog) {
|
|
|
6026
6088
|
|
|
6027
6089
|
// ../agents/dist/roles/retest.js
|
|
6028
6090
|
import { execFile as execFile5 } from "node:child_process";
|
|
6029
|
-
import { readFile as
|
|
6091
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
6030
6092
|
import { resolve as resolve7 } from "node:path";
|
|
6031
6093
|
import { promisify as promisify5 } from "node:util";
|
|
6032
6094
|
|
|
@@ -6086,7 +6148,7 @@ async function observeDom(page) {
|
|
|
6086
6148
|
};
|
|
6087
6149
|
const trim = (value) => value?.trim().replace(/\s+/g, " ").slice(0, 80) ?? "";
|
|
6088
6150
|
const implicitRole = (tag, type) => {
|
|
6089
|
-
const
|
|
6151
|
+
const roles4 = {
|
|
6090
6152
|
a: "link",
|
|
6091
6153
|
button: "button",
|
|
6092
6154
|
select: "combobox",
|
|
@@ -6109,7 +6171,7 @@ async function observeDom(page) {
|
|
|
6109
6171
|
if (/^h[1-6]$/.test(tag)) {
|
|
6110
6172
|
return "heading";
|
|
6111
6173
|
}
|
|
6112
|
-
return
|
|
6174
|
+
return roles4[tag] ?? "button";
|
|
6113
6175
|
};
|
|
6114
6176
|
for (const element of Array.from(document.querySelectorAll(candidates))) {
|
|
6115
6177
|
if (result3.length >= 150) {
|
|
@@ -6262,6 +6324,7 @@ var WebDriver = class {
|
|
|
6262
6324
|
page;
|
|
6263
6325
|
lastObservation;
|
|
6264
6326
|
errors = /* @__PURE__ */ new Map();
|
|
6327
|
+
failures = /* @__PURE__ */ new Map();
|
|
6265
6328
|
watched = /* @__PURE__ */ new WeakSet();
|
|
6266
6329
|
constructor(options) {
|
|
6267
6330
|
this.options = options;
|
|
@@ -6291,11 +6354,30 @@ var WebDriver = class {
|
|
|
6291
6354
|
const errors = [];
|
|
6292
6355
|
this.errors.set(page, errors);
|
|
6293
6356
|
page.on("console", (message2) => {
|
|
6294
|
-
if (message2.type() === "error") {
|
|
6357
|
+
if (message2.type() === "error" && !message2.text().startsWith("Failed to load resource:")) {
|
|
6295
6358
|
errors.push(message2.text());
|
|
6296
6359
|
}
|
|
6297
6360
|
});
|
|
6298
6361
|
page.on("pageerror", (error) => errors.push(`${error.name}: ${error.message}`));
|
|
6362
|
+
const failures = [];
|
|
6363
|
+
this.failures.set(page, failures);
|
|
6364
|
+
const add = (line) => {
|
|
6365
|
+
if (failures.length < 50)
|
|
6366
|
+
failures.push(line);
|
|
6367
|
+
};
|
|
6368
|
+
page.on("response", (response3) => {
|
|
6369
|
+
const type = response3.request().resourceType();
|
|
6370
|
+
if (response3.status() >= 400 && ["fetch", "xhr", "document"].includes(type)) {
|
|
6371
|
+
add(`${response3.request().method()} ${response3.url()} \u2192 ${response3.status()}`);
|
|
6372
|
+
}
|
|
6373
|
+
});
|
|
6374
|
+
page.on("requestfailed", (request) => {
|
|
6375
|
+
const reason = request.failure()?.errorText ?? "failed";
|
|
6376
|
+
if (/ERR_ABORTED|NS_BINDING_ABORTED|cancelled/i.test(reason))
|
|
6377
|
+
return;
|
|
6378
|
+
if (["fetch", "xhr", "document"].includes(request.resourceType()))
|
|
6379
|
+
add(`${request.method()} ${request.url()} \u2192 ${reason}`);
|
|
6380
|
+
});
|
|
6299
6381
|
}
|
|
6300
6382
|
async observe() {
|
|
6301
6383
|
const page = this.activePage();
|
|
@@ -6327,6 +6409,7 @@ var WebDriver = class {
|
|
|
6327
6409
|
windows,
|
|
6328
6410
|
volatileRegions: [],
|
|
6329
6411
|
consoleErrors: errors.splice(0),
|
|
6412
|
+
networkErrors: (this.failures.get(page) ?? []).splice(0),
|
|
6330
6413
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6331
6414
|
};
|
|
6332
6415
|
this.lastObservation = observation;
|
|
@@ -6952,7 +7035,7 @@ var MaestroDriver = class {
|
|
|
6952
7035
|
await this.run([`openLink: ${yamlString(action.url)}`]);
|
|
6953
7036
|
}
|
|
6954
7037
|
async wait(action) {
|
|
6955
|
-
await new Promise((
|
|
7038
|
+
await new Promise((resolve12) => setTimeout(resolve12, action.ms));
|
|
6956
7039
|
}
|
|
6957
7040
|
async switchWindow() {
|
|
6958
7041
|
throw new Error("Window switching is unsupported on mobile");
|
|
@@ -7013,7 +7096,7 @@ var MaestroDriver = class {
|
|
|
7013
7096
|
return { frames, stable: true };
|
|
7014
7097
|
}
|
|
7015
7098
|
previous = current;
|
|
7016
|
-
await new Promise((
|
|
7099
|
+
await new Promise((resolve12) => setTimeout(resolve12, interval));
|
|
7017
7100
|
}
|
|
7018
7101
|
return { frames, stable: false };
|
|
7019
7102
|
}
|
|
@@ -7055,6 +7138,60 @@ function createDriver(config, vars) {
|
|
|
7055
7138
|
});
|
|
7056
7139
|
}
|
|
7057
7140
|
|
|
7141
|
+
// ../agents/dist/roles/overlay.js
|
|
7142
|
+
import { mkdir as mkdir4, readFile as readFile9, readdir as readdir3, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
7143
|
+
import { dirname as dirname3, join as join11, relative as relative3, sep as sep3 } from "node:path";
|
|
7144
|
+
async function files(dir, skip) {
|
|
7145
|
+
const out = [];
|
|
7146
|
+
let entries;
|
|
7147
|
+
try {
|
|
7148
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
7149
|
+
} catch {
|
|
7150
|
+
return out;
|
|
7151
|
+
}
|
|
7152
|
+
for (const entry of entries) {
|
|
7153
|
+
const path = join11(dir, entry.name);
|
|
7154
|
+
if (path === skip)
|
|
7155
|
+
continue;
|
|
7156
|
+
if (entry.isDirectory())
|
|
7157
|
+
out.push(...await files(path, skip));
|
|
7158
|
+
else if (entry.isFile())
|
|
7159
|
+
out.push(path);
|
|
7160
|
+
}
|
|
7161
|
+
return out;
|
|
7162
|
+
}
|
|
7163
|
+
async function overlayBughunters(root2, source, worktree) {
|
|
7164
|
+
const where = relative3(source, root2);
|
|
7165
|
+
if (where.startsWith("..") || where.split(sep3)[0] === "..")
|
|
7166
|
+
return async () => {
|
|
7167
|
+
};
|
|
7168
|
+
const from = join11(root2, BUGHUNTERS_DIR);
|
|
7169
|
+
const saved = [];
|
|
7170
|
+
for (const file of await files(from, join11(from, DATA_DIR))) {
|
|
7171
|
+
const target = join11(worktree, where, relative3(root2, file));
|
|
7172
|
+
const content = await readFile9(file);
|
|
7173
|
+
let before;
|
|
7174
|
+
try {
|
|
7175
|
+
before = await readFile9(target);
|
|
7176
|
+
} catch {
|
|
7177
|
+
before = void 0;
|
|
7178
|
+
}
|
|
7179
|
+
if (before?.equals(content))
|
|
7180
|
+
continue;
|
|
7181
|
+
saved.push({ path: target, content: before });
|
|
7182
|
+
await mkdir4(dirname3(target), { recursive: true });
|
|
7183
|
+
await writeFile6(target, content);
|
|
7184
|
+
}
|
|
7185
|
+
return async () => {
|
|
7186
|
+
for (const item of saved.reverse()) {
|
|
7187
|
+
if (item.content)
|
|
7188
|
+
await writeFile6(item.path, item.content);
|
|
7189
|
+
else
|
|
7190
|
+
await rm4(item.path, { force: true });
|
|
7191
|
+
}
|
|
7192
|
+
};
|
|
7193
|
+
}
|
|
7194
|
+
|
|
7058
7195
|
// ../agents/dist/roles/retest.js
|
|
7059
7196
|
var exec4 = promisify5(execFile5);
|
|
7060
7197
|
var schema4 = (properties = {}, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
@@ -7064,7 +7201,7 @@ async function image2(root2, file) {
|
|
|
7064
7201
|
if (!file)
|
|
7065
7202
|
return [];
|
|
7066
7203
|
try {
|
|
7067
|
-
return [{ type: "image", png: await
|
|
7204
|
+
return [{ type: "image", png: await readFile10(resolve7(root2, file)) }];
|
|
7068
7205
|
} catch {
|
|
7069
7206
|
return [];
|
|
7070
7207
|
}
|
|
@@ -7130,9 +7267,13 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
7130
7267
|
const vars = new Vars(config.app.secrets);
|
|
7131
7268
|
let app;
|
|
7132
7269
|
let driver;
|
|
7270
|
+
let restore = async () => {
|
|
7271
|
+
};
|
|
7133
7272
|
try {
|
|
7134
7273
|
if (!options.build)
|
|
7135
7274
|
await linkEnvFiles(fix.repo, fix.worktree);
|
|
7275
|
+
if (!options.build)
|
|
7276
|
+
restore = await overlayBughunters(root2, fix.repo, fix.worktree);
|
|
7136
7277
|
if (!options.build && config.agents.fixer.retest.prepare) {
|
|
7137
7278
|
await exec4("/bin/sh", ["-c", config.agents.fixer.retest.prepare], { cwd: fix.worktree, timeout: 6e5, maxBuffer: 4 * 1024 * 1024 });
|
|
7138
7279
|
}
|
|
@@ -7169,7 +7310,8 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
7169
7310
|
"open",
|
|
7170
7311
|
"wait",
|
|
7171
7312
|
"run_routine",
|
|
7172
|
-
"switch_window"
|
|
7313
|
+
"switch_window",
|
|
7314
|
+
"save_lesson"
|
|
7173
7315
|
]);
|
|
7174
7316
|
const tools = explorerTools(session2).filter((tool) => allowed.has(tool.name)).map((tool) => tool.name !== "run_routine" ? tool : { ...tool, async run(input) {
|
|
7175
7317
|
const id2 = String(input.id ?? "");
|
|
@@ -7259,7 +7401,7 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
7259
7401
|
}
|
|
7260
7402
|
});
|
|
7261
7403
|
const guide = config.app.instructions;
|
|
7262
|
-
const instructions = guide ? await
|
|
7404
|
+
const instructions = guide ? await readFile10(resolve7(root2, guide), "utf8") : "";
|
|
7263
7405
|
const result3 = await explorerRuntime.run({
|
|
7264
7406
|
role: "explorer",
|
|
7265
7407
|
sessionId: record2.id,
|
|
@@ -7313,12 +7455,17 @@ ${targets.map((target, index) => `${index + 1}. screen ${target.shot.screenId ??
|
|
|
7313
7455
|
} catch (error) {
|
|
7314
7456
|
retest.outcome = "error";
|
|
7315
7457
|
retest.reason = `Retest preparation or exploration failed: ${String(error)}`;
|
|
7458
|
+
deps.onLog?.(retest.reason);
|
|
7316
7459
|
return retest;
|
|
7317
7460
|
} finally {
|
|
7318
7461
|
try {
|
|
7319
7462
|
await driver?.close();
|
|
7320
7463
|
} finally {
|
|
7321
|
-
|
|
7464
|
+
try {
|
|
7465
|
+
await app?.stop();
|
|
7466
|
+
} finally {
|
|
7467
|
+
await restore();
|
|
7468
|
+
}
|
|
7322
7469
|
}
|
|
7323
7470
|
}
|
|
7324
7471
|
if (!targets.some((target) => target.shot.after)) {
|
|
@@ -7362,7 +7509,7 @@ Fixer: ${fix.summary ?? ""}` }
|
|
|
7362
7509
|
judged = { outcome: input.outcome, reason: String(input.reason ?? "") };
|
|
7363
7510
|
return { ...response2(`Retest verdict: ${judged.outcome} \u2014 ${judged.reason}`), done: true };
|
|
7364
7511
|
}
|
|
7365
|
-
}];
|
|
7512
|
+
}, ...lessonTools(session, "judge")];
|
|
7366
7513
|
const result3 = await judgeRuntime.run({
|
|
7367
7514
|
role: "judge",
|
|
7368
7515
|
sessionId: record.id,
|
|
@@ -7531,8 +7678,8 @@ async function runFixCycle(root2, config, deps = {}) {
|
|
|
7531
7678
|
}
|
|
7532
7679
|
|
|
7533
7680
|
// ../agents/dist/roles/publish.js
|
|
7534
|
-
import { mkdir as
|
|
7535
|
-
import { join as
|
|
7681
|
+
import { mkdir as mkdir5, readFile as readFile11, writeFile as writeFile7 } from "node:fs/promises";
|
|
7682
|
+
import { join as join12, resolve as resolve8 } from "node:path";
|
|
7536
7683
|
|
|
7537
7684
|
// ../agents/dist/report.js
|
|
7538
7685
|
function buildReport(input) {
|
|
@@ -7650,7 +7797,7 @@ async function image3(root2, path) {
|
|
|
7650
7797
|
if (!path)
|
|
7651
7798
|
return [];
|
|
7652
7799
|
try {
|
|
7653
|
-
return [{ type: "image", png: await
|
|
7800
|
+
return [{ type: "image", png: await readFile11(resolve8(root2, path)) }];
|
|
7654
7801
|
} catch {
|
|
7655
7802
|
return [];
|
|
7656
7803
|
}
|
|
@@ -7842,9 +7989,9 @@ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
|
|
|
7842
7989
|
let url;
|
|
7843
7990
|
if (deps.dryRun) {
|
|
7844
7991
|
const dir = paths.publish(root2);
|
|
7845
|
-
await
|
|
7846
|
-
url =
|
|
7847
|
-
await
|
|
7992
|
+
await mkdir5(dir, { recursive: true });
|
|
7993
|
+
url = join12(dir, `${item.issue.id}.md`);
|
|
7994
|
+
await writeFile7(url, body);
|
|
7848
7995
|
} else if (item.kind === "pr" && item.fix) {
|
|
7849
7996
|
const opened = await createPr(gh, {
|
|
7850
7997
|
repo: resolved.repo,
|
|
@@ -7951,10 +8098,276 @@ ${items.map((item) => `${item.issue.id}: ${item.issue.title}`).join("\n")}`),
|
|
|
7951
8098
|
return outcomes;
|
|
7952
8099
|
}
|
|
7953
8100
|
|
|
8101
|
+
// ../agents/dist/roles/ci.js
|
|
8102
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
8103
|
+
import { access } from "node:fs/promises";
|
|
8104
|
+
import { resolve as resolve9 } from "node:path";
|
|
8105
|
+
import { promisify as promisify6 } from "node:util";
|
|
8106
|
+
var git4 = async (cwd, ...args2) => (await promisify6(execFile6)("git", args2, { cwd, maxBuffer: 16 * 1024 * 1024 })).stdout.trim();
|
|
8107
|
+
async function readChecks(gh, repo, pr) {
|
|
8108
|
+
let out;
|
|
8109
|
+
try {
|
|
8110
|
+
out = await gh(["pr", "checks", String(pr), "--repo", repo, "--json", "name,bucket,link,description,workflow"]);
|
|
8111
|
+
} catch (error) {
|
|
8112
|
+
out = String(error.stdout ?? "");
|
|
8113
|
+
if (!out.trim().startsWith("[")) {
|
|
8114
|
+
if (/no checks/i.test(String(error.stderr ?? "")))
|
|
8115
|
+
return [];
|
|
8116
|
+
throw error;
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
return out.trim() ? JSON.parse(out) : [];
|
|
8120
|
+
}
|
|
8121
|
+
async function failureLog(gh, repo, failed2) {
|
|
8122
|
+
const parts = [];
|
|
8123
|
+
const runs = /* @__PURE__ */ new Set();
|
|
8124
|
+
for (const check of failed2) {
|
|
8125
|
+
const run2 = check.link?.match(/\/actions\/runs\/(\d+)/)?.[1];
|
|
8126
|
+
if (run2 && !runs.has(run2)) {
|
|
8127
|
+
runs.add(run2);
|
|
8128
|
+
try {
|
|
8129
|
+
const log = await gh(["run", "view", run2, "--repo", repo, "--log-failed"]);
|
|
8130
|
+
parts.push(`### ${check.workflow || check.name} (run ${run2})
|
|
8131
|
+
${log.split("\n").slice(-150).join("\n")}`);
|
|
8132
|
+
continue;
|
|
8133
|
+
} catch {
|
|
8134
|
+
}
|
|
8135
|
+
}
|
|
8136
|
+
if (!run2)
|
|
8137
|
+
parts.push(`### ${check.name}
|
|
8138
|
+
${check.description ?? ""}
|
|
8139
|
+
${check.link ?? ""}`);
|
|
8140
|
+
}
|
|
8141
|
+
return parts.join("\n\n").slice(-12e3);
|
|
8142
|
+
}
|
|
8143
|
+
function ciPrompt(pr, failed2, log) {
|
|
8144
|
+
return `The CI checks failed on PR #${pr}, which has your earlier fix. Make every check pass.
|
|
8145
|
+
|
|
8146
|
+
FAILED CHECKS
|
|
8147
|
+
${failed2.map((check) => `- ${check.name}${check.link ? ` (${check.link})` : ""}`).join("\n")}
|
|
8148
|
+
|
|
8149
|
+
THE END OF THE FAILED LOGS
|
|
8150
|
+
${log || "(no log is available)"}
|
|
8151
|
+
|
|
8152
|
+
- Find the cause in the log. Fix the code or the test, not the CI config.
|
|
8153
|
+
- Run the same command as the failed check in this worktree, and make it pass before you finish.
|
|
8154
|
+
- Keep the original fix. Do not revert it to make CI pass.
|
|
8155
|
+
- If the failure has nothing to do with this change (a flaky test, an outage), change nothing, and say so.`;
|
|
8156
|
+
}
|
|
8157
|
+
async function watchCi(root2, config, deps = {}) {
|
|
8158
|
+
const problems = [];
|
|
8159
|
+
const settings = config.agents.github.ci;
|
|
8160
|
+
if (!config.agents.github.enabled || !settings.enabled)
|
|
8161
|
+
return { problems };
|
|
8162
|
+
const gh = deps.gh ?? defaultGh;
|
|
8163
|
+
const ready = await ghReady(gh);
|
|
8164
|
+
if (!ready.ok) {
|
|
8165
|
+
deps.onLog?.(ready.reason);
|
|
8166
|
+
return { problems };
|
|
8167
|
+
}
|
|
8168
|
+
const workspace = new Workspace(root2);
|
|
8169
|
+
const fixes = (await workspace.listFixes()).filter((fix) => fix.pr && (fix.pr.state ?? "open") === "open" && (!deps.issueIds || deps.issueIds.includes(fix.issueId)));
|
|
8170
|
+
if (!fixes.length)
|
|
8171
|
+
return { problems };
|
|
8172
|
+
const { repo } = await resolveRepo(gh, config, resolve9(root2, config.app.source));
|
|
8173
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
8174
|
+
for (const fix of fixes) {
|
|
8175
|
+
const pr = fix.pr.number;
|
|
8176
|
+
const deadline = Date.now() + settings.waitMinutes * 6e4;
|
|
8177
|
+
for (; ; ) {
|
|
8178
|
+
const checks = await readChecks(gh, repo, pr);
|
|
8179
|
+
const pending = checks.filter((check) => check.bucket === "pending");
|
|
8180
|
+
const failed2 = checks.filter((check) => check.bucket === "fail" || check.bucket === "cancel");
|
|
8181
|
+
const save = async (state) => {
|
|
8182
|
+
fix.ci = {
|
|
8183
|
+
state,
|
|
8184
|
+
head: fix.commit,
|
|
8185
|
+
failing: failed2.map((check) => check.name),
|
|
8186
|
+
attempts: fix.ci?.attempts ?? 0,
|
|
8187
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8188
|
+
};
|
|
8189
|
+
await workspace.saveFix(fix);
|
|
8190
|
+
};
|
|
8191
|
+
if (!checks.length) {
|
|
8192
|
+
await save("none");
|
|
8193
|
+
break;
|
|
8194
|
+
}
|
|
8195
|
+
if (pending.length && !failed2.length) {
|
|
8196
|
+
if (deps.wait && Date.now() < deadline) {
|
|
8197
|
+
await sleep(3e4);
|
|
8198
|
+
continue;
|
|
8199
|
+
}
|
|
8200
|
+
await save("pending");
|
|
8201
|
+
deps.onLog?.(`CI: PR #${pr} has ${pending.length} check(s) still running. The next cycle looks again.`);
|
|
8202
|
+
break;
|
|
8203
|
+
}
|
|
8204
|
+
if (!failed2.length) {
|
|
8205
|
+
await save("passed");
|
|
8206
|
+
deps.onLog?.(`CI: all ${checks.length} check(s) passed on PR #${pr}.`);
|
|
8207
|
+
break;
|
|
8208
|
+
}
|
|
8209
|
+
if ((fix.ci?.attempts ?? 0) >= settings.attempts) {
|
|
8210
|
+
await save("gave-up");
|
|
8211
|
+
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.`;
|
|
8212
|
+
problems.push(problem);
|
|
8213
|
+
deps.onLog?.(problem);
|
|
8214
|
+
break;
|
|
8215
|
+
}
|
|
8216
|
+
await save("failed");
|
|
8217
|
+
deps.onLog?.(`CI: PR #${pr} failed ${failed2.map((check) => check.name).join(", ")}. The fixer tries to fix it.`);
|
|
8218
|
+
const pushed = await fixCi(root2, config, fix, failed2, await failureLog(gh, repo, failed2), deps);
|
|
8219
|
+
fix.ci = { ...fix.ci, attempts: (fix.ci?.attempts ?? 0) + 1 };
|
|
8220
|
+
await workspace.saveFix(fix);
|
|
8221
|
+
if (!pushed) {
|
|
8222
|
+
if (fix.ci.attempts >= settings.attempts) {
|
|
8223
|
+
const problem = `CI: the fixer could not fix PR #${pr}. A person must look at it.`;
|
|
8224
|
+
fix.ci = { ...fix.ci, state: "gave-up" };
|
|
8225
|
+
await workspace.saveFix(fix);
|
|
8226
|
+
problems.push(problem);
|
|
8227
|
+
deps.onLog?.(problem);
|
|
8228
|
+
break;
|
|
8229
|
+
}
|
|
8230
|
+
continue;
|
|
8231
|
+
}
|
|
8232
|
+
if (!deps.wait) {
|
|
8233
|
+
fix.ci = { ...fix.ci, state: "pending" };
|
|
8234
|
+
await workspace.saveFix(fix);
|
|
8235
|
+
break;
|
|
8236
|
+
}
|
|
8237
|
+
await sleep(3e4);
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
return { problems };
|
|
8241
|
+
}
|
|
8242
|
+
async function fixCi(root2, config, fix, failed2, log, deps) {
|
|
8243
|
+
try {
|
|
8244
|
+
await access(fix.worktree);
|
|
8245
|
+
} catch {
|
|
8246
|
+
deps.onLog?.(`CI: the worktree of ${fix.issueId} is gone, so the fixer cannot change PR #${fix.pr.number}.`);
|
|
8247
|
+
return false;
|
|
8248
|
+
}
|
|
8249
|
+
const workspace = new Workspace(root2);
|
|
8250
|
+
const record = await workspace.startSession("fixer");
|
|
8251
|
+
const session = new AgentSession(root2, config, new Vars(config.app.secrets), record.id, "fixer", void 0, deps.onLog);
|
|
8252
|
+
const fixer = config.agents.fixer;
|
|
8253
|
+
const runtime = (deps.createRuntime ?? createRuntime)(fixer.use);
|
|
8254
|
+
session.emit({ kind: "fix", summary: `Fixing CI on PR #${fix.pr.number}: ${failed2.map((check) => check.name).join(", ")}` });
|
|
8255
|
+
let summary = "";
|
|
8256
|
+
let status = "failed";
|
|
8257
|
+
try {
|
|
8258
|
+
const outcome = await runtime.run({
|
|
8259
|
+
role: "fixer",
|
|
8260
|
+
sessionId: session.sessionId,
|
|
8261
|
+
workdir: fix.worktree,
|
|
8262
|
+
system: fixerSystem(lessonsFor(await workspace.readMemory(), "fixer")),
|
|
8263
|
+
prompt: ciPrompt(fix.pr.number, failed2, log),
|
|
8264
|
+
tools: [...runtime.label.startsWith("cli:") ? [] : modelTools(fix.worktree), ...lessonTools(session, "fixer"), finishTool()],
|
|
8265
|
+
maxSteps: fixer.maxSteps,
|
|
8266
|
+
budgetUsd: fixer.budgetUsd,
|
|
8267
|
+
timeoutMs: fixer.timeoutMs
|
|
8268
|
+
}, session.emit);
|
|
8269
|
+
summary = outcome.summary ?? "";
|
|
8270
|
+
if (outcome.stop !== "done")
|
|
8271
|
+
throw new Error(outcome.error ?? `Fixer stopped: ${outcome.stop}`);
|
|
8272
|
+
if (!await git4(fix.worktree, "status", "--porcelain")) {
|
|
8273
|
+
summary = summary || "The fixer made no change.";
|
|
8274
|
+
session.emit({ kind: "fix", summary: `No change for CI on PR #${fix.pr.number}: ${summary.split("\n")[0]}` });
|
|
8275
|
+
status = "finished";
|
|
8276
|
+
return false;
|
|
8277
|
+
}
|
|
8278
|
+
if (fixer.verify) {
|
|
8279
|
+
await promisify6(execFile6)("/bin/sh", ["-c", fixer.verify], { cwd: fix.worktree, timeout: 3e5, maxBuffer: 4 * 1024 * 1024 });
|
|
8280
|
+
}
|
|
8281
|
+
await git4(fix.worktree, "add", "-A");
|
|
8282
|
+
const committed = await commitFix(fix.worktree, "pass the CI checks", fixer.commitMessage);
|
|
8283
|
+
if (!committed.ok)
|
|
8284
|
+
throw new Error(`The commit hook rejected the commit: ${committed.reason}`);
|
|
8285
|
+
fix.commit = await git4(fix.worktree, "rev-parse", "HEAD");
|
|
8286
|
+
await git4(fix.worktree, "push", "origin", fix.branch);
|
|
8287
|
+
session.emit({ kind: "fix", summary: `Pushed a CI fix to PR #${fix.pr.number} (${fix.commit.slice(0, 7)})` });
|
|
8288
|
+
status = "finished";
|
|
8289
|
+
return true;
|
|
8290
|
+
} catch (error) {
|
|
8291
|
+
summary = String(error).slice(0, 500);
|
|
8292
|
+
session.emit({ kind: "error", summary: `CI fix failed on PR #${fix.pr.number}: ${summary}` });
|
|
8293
|
+
return false;
|
|
8294
|
+
} finally {
|
|
8295
|
+
await workspace.endSession(session.sessionId, { status, summary });
|
|
8296
|
+
await session.idle();
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
|
|
7954
8300
|
// ../agents/dist/patrol.js
|
|
8301
|
+
import { execFile as execFile7 } from "node:child_process";
|
|
8302
|
+
import { resolve as resolve10 } from "node:path";
|
|
8303
|
+
import { promisify as promisify7 } from "node:util";
|
|
8304
|
+
var exec5 = promisify7(execFile7);
|
|
8305
|
+
async function git5(cwd, ...args2) {
|
|
8306
|
+
return (await exec5("git", args2, { cwd })).stdout.trim();
|
|
8307
|
+
}
|
|
8308
|
+
async function pullSource(root2, config, onLog) {
|
|
8309
|
+
const target = config.agents.patrol.pull;
|
|
8310
|
+
if (!target)
|
|
8311
|
+
return false;
|
|
8312
|
+
const slash = target.indexOf("/");
|
|
8313
|
+
const remote = target.slice(0, slash);
|
|
8314
|
+
const branch = target.slice(slash + 1);
|
|
8315
|
+
const source = resolve10(root2, config.app.source);
|
|
8316
|
+
try {
|
|
8317
|
+
if (await git5(source, "status", "--porcelain", "--untracked-files=no")) {
|
|
8318
|
+
onLog?.(`Did not pull ${target}: the source repository has uncommitted changes.`);
|
|
8319
|
+
return false;
|
|
8320
|
+
}
|
|
8321
|
+
await git5(source, "fetch", "-q", remote, `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`);
|
|
8322
|
+
await git5(source, "checkout", "-q", "--detach", `${remote}/${branch}`);
|
|
8323
|
+
onLog?.(`Pulled ${target} at ${await git5(source, "rev-parse", "--short", "HEAD")}.`);
|
|
8324
|
+
return true;
|
|
8325
|
+
} catch (error) {
|
|
8326
|
+
const reason = error.stderr?.trim() || error.message;
|
|
8327
|
+
onLog?.(`Did not pull ${target}: ${reason}. The cycle uses the current checkout.`);
|
|
8328
|
+
return false;
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
async function sourceCommit(root2, config) {
|
|
8332
|
+
const source = resolve10(root2, config.app.source);
|
|
8333
|
+
try {
|
|
8334
|
+
if (await git5(source, "status", "--porcelain", "--untracked-files=no"))
|
|
8335
|
+
return void 0;
|
|
8336
|
+
return await git5(source, "rev-parse", "HEAD");
|
|
8337
|
+
} catch {
|
|
8338
|
+
return void 0;
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
function alive(pid) {
|
|
8342
|
+
try {
|
|
8343
|
+
process.kill(pid, 0);
|
|
8344
|
+
return true;
|
|
8345
|
+
} catch {
|
|
8346
|
+
return false;
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
function wait(minutes) {
|
|
8350
|
+
return new Promise((done) => {
|
|
8351
|
+
const finish = () => {
|
|
8352
|
+
clearTimeout(timer);
|
|
8353
|
+
process.off("SIGINT", finish);
|
|
8354
|
+
process.off("SIGTERM", finish);
|
|
8355
|
+
done();
|
|
8356
|
+
};
|
|
8357
|
+
const timer = setTimeout(finish, minutes * 6e4);
|
|
8358
|
+
process.once("SIGINT", finish);
|
|
8359
|
+
process.once("SIGTERM", finish);
|
|
8360
|
+
});
|
|
8361
|
+
}
|
|
7955
8362
|
async function runPatrol(options) {
|
|
8363
|
+
const problems = [];
|
|
7956
8364
|
const { root: root2, config } = options;
|
|
7957
8365
|
const workspace = new Workspace(root2);
|
|
8366
|
+
const previous = (await workspace.readAgents()).patrol;
|
|
8367
|
+
if (previous?.state === "running" && previous.pid && previous.pid !== process.pid && alive(previous.pid)) {
|
|
8368
|
+
options.onLog?.(`A patrol already runs (pid ${previous.pid}): did not start another.`);
|
|
8369
|
+
return { problems };
|
|
8370
|
+
}
|
|
7958
8371
|
const runtime = options.createRuntime ?? createRuntime;
|
|
7959
8372
|
let interrupted = false;
|
|
7960
8373
|
let activeSession;
|
|
@@ -7964,6 +8377,11 @@ async function runPatrol(options) {
|
|
|
7964
8377
|
activeSession.cancelled = true;
|
|
7965
8378
|
}
|
|
7966
8379
|
};
|
|
8380
|
+
const idle = () => workspace.setPatrol({
|
|
8381
|
+
state: "stopped",
|
|
8382
|
+
nextAt: new Date(Date.now() + config.agents.patrol.intervalMinutes * 6e4).toISOString()
|
|
8383
|
+
});
|
|
8384
|
+
let tested = options.force ? void 0 : previous?.commit;
|
|
7967
8385
|
process.on("SIGINT", onSignal);
|
|
7968
8386
|
process.on("SIGTERM", onSignal);
|
|
7969
8387
|
try {
|
|
@@ -7971,46 +8389,65 @@ async function runPatrol(options) {
|
|
|
7971
8389
|
if (config.agents.patrol.cycles && cycle > config.agents.patrol.cycles) {
|
|
7972
8390
|
break;
|
|
7973
8391
|
}
|
|
8392
|
+
if (cycle > 1) {
|
|
8393
|
+
await wait(config.agents.patrol.intervalMinutes);
|
|
8394
|
+
if (interrupted)
|
|
8395
|
+
break;
|
|
8396
|
+
}
|
|
7974
8397
|
await workspace.setPatrol({ state: "running", cycle, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8398
|
+
await pullSource(root2, config, options.onLog);
|
|
7975
8399
|
if (config.agents.github.enabled)
|
|
7976
8400
|
await syncGitHub(root2, config, { onLog: options.onLog });
|
|
7977
8401
|
await cleanWorktrees(root2, config, { onLog: options.onLog });
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
}
|
|
7982
|
-
}
|
|
7983
|
-
const
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
driver = (options.createDriver ?? createDriver)(config, vars.resolve.bind(vars));
|
|
7989
|
-
await driver.connect();
|
|
7990
|
-
let explorerId;
|
|
7991
|
-
if (config.agents.explorer.enabled && !interrupted) {
|
|
7992
|
-
const record = await workspace.startSession("explorer");
|
|
7993
|
-
explorerId = record.id;
|
|
7994
|
-
const session = new AgentSession(root2, config, vars, record.id, "explorer", driver, options.onLog);
|
|
7995
|
-
activeSession = session;
|
|
7996
|
-
if (!interrupted) {
|
|
7997
|
-
await runExplorer(session, runtime(config.agents.explorer.use));
|
|
7998
|
-
} else {
|
|
7999
|
-
await workspace.endSession(record.id, { summary: "Interrupted" });
|
|
8402
|
+
const commit = await sourceCommit(root2, config);
|
|
8403
|
+
const fresh = !commit || commit !== tested;
|
|
8404
|
+
if (!fresh) {
|
|
8405
|
+
options.onLog?.(`No new commit since the last cycle (${commit.slice(0, 7)}): skipped explore and judge. The pending fixes, retests, publish, and CI checks still run.`);
|
|
8406
|
+
}
|
|
8407
|
+
const before = problems.length;
|
|
8408
|
+
if (fresh) {
|
|
8409
|
+
for (const role of ["explorer", "judge", "fixer"]) {
|
|
8410
|
+
if (!config.agents[role].enabled) {
|
|
8411
|
+
await workspace.setAgentStatus(role, { state: "off" });
|
|
8000
8412
|
}
|
|
8001
|
-
activeSession = void 0;
|
|
8002
8413
|
}
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
activeSession = session;
|
|
8007
|
-
await runJudge(session, runtime(config.agents.judge.use), { sessionIds: [explorerId] });
|
|
8008
|
-
}
|
|
8009
|
-
} finally {
|
|
8414
|
+
const vars = new Vars(config.app.secrets);
|
|
8415
|
+
let app;
|
|
8416
|
+
let driver;
|
|
8010
8417
|
try {
|
|
8011
|
-
await
|
|
8418
|
+
app = await startApp(config.app, { root: root2, vars, emit: options.onLog });
|
|
8419
|
+
driver = (options.createDriver ?? createDriver)(config, vars.resolve.bind(vars));
|
|
8420
|
+
await driver.connect();
|
|
8421
|
+
let explorerId;
|
|
8422
|
+
if (config.agents.explorer.enabled && !interrupted) {
|
|
8423
|
+
const record = await workspace.startSession("explorer");
|
|
8424
|
+
explorerId = record.id;
|
|
8425
|
+
const session = new AgentSession(root2, config, vars, record.id, "explorer", driver, options.onLog);
|
|
8426
|
+
activeSession = session;
|
|
8427
|
+
if (!interrupted) {
|
|
8428
|
+
await runExplorer(session, runtime(config.agents.explorer.use));
|
|
8429
|
+
} else {
|
|
8430
|
+
await workspace.endSession(record.id, { summary: "Interrupted" });
|
|
8431
|
+
}
|
|
8432
|
+
activeSession = void 0;
|
|
8433
|
+
}
|
|
8434
|
+
if (config.agents.judge.enabled && explorerId && !interrupted) {
|
|
8435
|
+
const record = await workspace.startSession("judge");
|
|
8436
|
+
const session = new AgentSession(root2, config, vars, record.id, "judge", void 0, options.onLog);
|
|
8437
|
+
activeSession = session;
|
|
8438
|
+
await runJudge(session, runtime(config.agents.judge.use), { sessionIds: [explorerId] });
|
|
8439
|
+
}
|
|
8440
|
+
} catch (error) {
|
|
8441
|
+
activeSession = void 0;
|
|
8442
|
+
const problem = `Cycle ${cycle}: ${String(error)}`;
|
|
8443
|
+
problems.push(problem);
|
|
8444
|
+
options.onLog?.(problem);
|
|
8012
8445
|
} finally {
|
|
8013
|
-
|
|
8446
|
+
try {
|
|
8447
|
+
await driver?.close();
|
|
8448
|
+
} finally {
|
|
8449
|
+
await app?.stop();
|
|
8450
|
+
}
|
|
8014
8451
|
}
|
|
8015
8452
|
}
|
|
8016
8453
|
if (!interrupted && config.agents.explorer.enabled && config.agents.judge.enabled) {
|
|
@@ -8024,16 +8461,21 @@ async function runPatrol(options) {
|
|
|
8024
8461
|
isInterrupted: () => interrupted
|
|
8025
8462
|
});
|
|
8026
8463
|
}
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8464
|
+
const fixes = interrupted ? [] : await runFixCycle(root2, config, {
|
|
8465
|
+
createDriver: options.createDriver,
|
|
8466
|
+
createRuntime: options.createRuntime,
|
|
8467
|
+
onLog: options.onLog,
|
|
8468
|
+
onSession: (session) => {
|
|
8469
|
+
activeSession = session;
|
|
8470
|
+
},
|
|
8471
|
+
isInterrupted: () => interrupted
|
|
8472
|
+
});
|
|
8473
|
+
for (const fix of fixes) {
|
|
8474
|
+
const last = fix.retests?.at(-1);
|
|
8475
|
+
if (last?.outcome === "error")
|
|
8476
|
+
problems.push(`Cycle ${cycle}: the retest of ${fix.issueId} failed: ${last.reason}`);
|
|
8477
|
+
}
|
|
8478
|
+
const cycleFailed = problems.length > before;
|
|
8037
8479
|
if (!interrupted && config.agents.github.enabled)
|
|
8038
8480
|
await runPublisher(root2, config, {
|
|
8039
8481
|
createRuntime: options.createRuntime,
|
|
@@ -8042,31 +8484,27 @@ async function runPatrol(options) {
|
|
|
8042
8484
|
activeSession = session;
|
|
8043
8485
|
}
|
|
8044
8486
|
});
|
|
8487
|
+
if (!interrupted && config.agents.github.enabled) {
|
|
8488
|
+
const ci = await watchCi(root2, config, { createRuntime: options.createRuntime, onLog: options.onLog, wait: true });
|
|
8489
|
+
problems.push(...ci.problems.map((problem) => `Cycle ${cycle}: ${problem}`));
|
|
8490
|
+
}
|
|
8045
8491
|
if (!interrupted && config.agents.github.enabled)
|
|
8046
8492
|
await syncGitHub(root2, config, { onLog: options.onLog });
|
|
8047
|
-
|
|
8048
|
-
|
|
8493
|
+
if (!interrupted && fresh && commit && !cycleFailed) {
|
|
8494
|
+
tested = commit;
|
|
8495
|
+
await workspace.setPatrol({ commit });
|
|
8496
|
+
}
|
|
8497
|
+
await idle();
|
|
8049
8498
|
if (options.once || interrupted) {
|
|
8050
8499
|
break;
|
|
8051
8500
|
}
|
|
8052
|
-
await new Promise((done) => {
|
|
8053
|
-
const finish = () => {
|
|
8054
|
-
clearTimeout(timer);
|
|
8055
|
-
process.off("SIGINT", stop);
|
|
8056
|
-
process.off("SIGTERM", stop);
|
|
8057
|
-
done();
|
|
8058
|
-
};
|
|
8059
|
-
const timer = setTimeout(finish, config.agents.patrol.intervalMinutes * 6e4);
|
|
8060
|
-
const stop = () => finish();
|
|
8061
|
-
process.once("SIGINT", stop);
|
|
8062
|
-
process.once("SIGTERM", stop);
|
|
8063
|
-
});
|
|
8064
8501
|
}
|
|
8065
8502
|
} finally {
|
|
8066
|
-
await workspace.setPatrol({ state: "stopped" });
|
|
8503
|
+
await workspace.setPatrol({ state: "stopped", nextAt: void 0 });
|
|
8067
8504
|
process.off("SIGINT", onSignal);
|
|
8068
8505
|
process.off("SIGTERM", onSignal);
|
|
8069
8506
|
}
|
|
8507
|
+
return { problems };
|
|
8070
8508
|
}
|
|
8071
8509
|
|
|
8072
8510
|
// ../cli/dist/commands/doctor.js
|
|
@@ -8115,15 +8553,13 @@ function runChecks(root2, config) {
|
|
|
8115
8553
|
}
|
|
8116
8554
|
function agentChecks(config, env = process.env) {
|
|
8117
8555
|
const checks = [];
|
|
8118
|
-
const
|
|
8119
|
-
for (const role of
|
|
8556
|
+
const roles4 = ["explorer", "judge", "fixer"].filter((role) => role !== "fixer" || config.agents.fixer.enabled);
|
|
8557
|
+
for (const role of roles4) {
|
|
8120
8558
|
const use = config.agents[role].use;
|
|
8121
8559
|
const problem = runtimeProblem(role, use, env);
|
|
8122
8560
|
const label = use.runtime === "cli" ? `cli: ${use.agent ?? use.command.split(" ")[0]}` : `${use.via}: ${use.model ?? "default model"}`;
|
|
8123
8561
|
checks.push({ name: role, ok: !problem, detail: problem ?? `The ${role} uses ${label}`, fatal: true });
|
|
8124
8562
|
}
|
|
8125
|
-
const decider = resolveDecider(config.decisions, env);
|
|
8126
|
-
checks.push({ name: "decider", ok: Boolean(decider.decider), detail: decider.reason, fatal: false });
|
|
8127
8563
|
if (config.agents.github.enabled) {
|
|
8128
8564
|
const installed = spawnSync("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
8129
8565
|
const loggedIn = installed && spawnSync("gh", ["auth", "status"], { stdio: "ignore" }).status === 0;
|
|
@@ -8138,12 +8574,12 @@ function doctorExitCode(checks) {
|
|
|
8138
8574
|
// ../cli/dist/commands/init.js
|
|
8139
8575
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8140
8576
|
import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
8141
|
-
import { join as
|
|
8577
|
+
import { join as join14, relative as relative4 } from "node:path";
|
|
8142
8578
|
import { createInterface } from "node:readline/promises";
|
|
8143
8579
|
|
|
8144
8580
|
// ../recon/dist/bringup.js
|
|
8145
8581
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
8146
|
-
import { join as
|
|
8582
|
+
import { join as join13 } from "node:path";
|
|
8147
8583
|
function detectBringUp(root2) {
|
|
8148
8584
|
const candidates = [];
|
|
8149
8585
|
for (const workflow of listWorkflows(root2)) {
|
|
@@ -8153,7 +8589,7 @@ function detectBringUp(root2) {
|
|
|
8153
8589
|
candidates.push({ command: match[0].replace(/^run:\s*/, ""), rung: "precedent", source: workflow, confidence: 0.8 });
|
|
8154
8590
|
}
|
|
8155
8591
|
}
|
|
8156
|
-
const pkgPath =
|
|
8592
|
+
const pkgPath = join13(root2, "package.json");
|
|
8157
8593
|
if (existsSync5(pkgPath)) {
|
|
8158
8594
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
8159
8595
|
const manager = detectPackageManager(root2);
|
|
@@ -8168,20 +8604,20 @@ function detectBringUp(root2) {
|
|
|
8168
8604
|
}
|
|
8169
8605
|
}
|
|
8170
8606
|
}
|
|
8171
|
-
if (existsSync5(
|
|
8607
|
+
if (existsSync5(join13(root2, "docker-compose.yml")) || existsSync5(join13(root2, "compose.yaml"))) {
|
|
8172
8608
|
candidates.push({ command: "docker compose up", rung: "convention", source: "docker-compose.yml", confidence: 0.5 });
|
|
8173
8609
|
}
|
|
8174
|
-
if (existsSync5(
|
|
8610
|
+
if (existsSync5(join13(root2, "Procfile"))) {
|
|
8175
8611
|
candidates.push({ command: "foreman start", rung: "convention", source: "Procfile", confidence: 0.3 });
|
|
8176
8612
|
}
|
|
8177
8613
|
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
8178
8614
|
}
|
|
8179
8615
|
function detectPackageManager(root2) {
|
|
8180
|
-
if (existsSync5(
|
|
8616
|
+
if (existsSync5(join13(root2, "pnpm-lock.yaml")))
|
|
8181
8617
|
return "pnpm";
|
|
8182
|
-
if (existsSync5(
|
|
8618
|
+
if (existsSync5(join13(root2, "bun.lockb")))
|
|
8183
8619
|
return "bun";
|
|
8184
|
-
if (existsSync5(
|
|
8620
|
+
if (existsSync5(join13(root2, "yarn.lock")))
|
|
8185
8621
|
return "yarn";
|
|
8186
8622
|
return "npm";
|
|
8187
8623
|
}
|
|
@@ -8202,7 +8638,7 @@ function detectStack(root2) {
|
|
|
8202
8638
|
["app.json", "expo"]
|
|
8203
8639
|
];
|
|
8204
8640
|
for (const [file, name] of signatures) {
|
|
8205
|
-
if (!existsSync5(
|
|
8641
|
+
if (!existsSync5(join13(root2, file)))
|
|
8206
8642
|
continue;
|
|
8207
8643
|
framework ??= name;
|
|
8208
8644
|
detectedFrom.push(file);
|
|
@@ -8217,12 +8653,12 @@ function detectStack(root2) {
|
|
|
8217
8653
|
};
|
|
8218
8654
|
}
|
|
8219
8655
|
function listWorkflows(root2) {
|
|
8220
|
-
const dir =
|
|
8656
|
+
const dir = join13(root2, ".github", "workflows");
|
|
8221
8657
|
if (!existsSync5(dir))
|
|
8222
8658
|
return [];
|
|
8223
8659
|
try {
|
|
8224
8660
|
const { readdirSync: readdirSync4 } = __require("node:fs");
|
|
8225
|
-
return readdirSync4(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).map((f) =>
|
|
8661
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).map((f) => join13(dir, f));
|
|
8226
8662
|
} catch {
|
|
8227
8663
|
return [];
|
|
8228
8664
|
}
|
|
@@ -8236,21 +8672,15 @@ var KEY_PROVIDERS = {
|
|
|
8236
8672
|
openai: { env: "OPENAI_API_KEY", label: "OpenAI", url: "https://platform.openai.com/api-keys", model: "gpt-5-mini" },
|
|
8237
8673
|
anthropic: { env: "ANTHROPIC_API_KEY", label: "Anthropic", url: "https://console.anthropic.com/settings/keys", model: "claude-haiku-4-5" }
|
|
8238
8674
|
};
|
|
8239
|
-
var JEV_ROUTES2 = {
|
|
8240
|
-
typesafe: { env: "TYPESAFE_API_KEY", label: "TypeSafe", url: "https://typesafe.ai" },
|
|
8241
|
-
openrouter: { env: "OPENROUTER_API_KEY", label: "OpenRouter", url: "https://openrouter.ai/keys" },
|
|
8242
|
-
vercel: { env: "AI_GATEWAY_API_KEY", label: "Vercel AI Gateway", url: "https://vercel.com/ai-gateway" }
|
|
8243
|
-
};
|
|
8244
8675
|
var CLI_LABELS = { claude: "Claude Code", codex: "Codex", kimi: "Kimi CLI", pi: "pi" };
|
|
8245
8676
|
var run = (command2, args2) => spawnSync2(command2, args2, { encoding: "utf8", timeout: 1e4 }).stdout ?? "";
|
|
8246
|
-
function detectProviders(env = process.env,
|
|
8677
|
+
function detectProviders(env = process.env, exec6 = run) {
|
|
8247
8678
|
const found = CLI_AGENTS.filter((agent) => onPath(agent, env));
|
|
8248
|
-
const piList = found.includes("pi") ?
|
|
8679
|
+
const piList = found.includes("pi") ? exec6("pi", ["list"]) : "";
|
|
8249
8680
|
const piWithoutMcp = found.includes("pi") && !piList.includes("pi-mcp-adapter");
|
|
8250
8681
|
return {
|
|
8251
8682
|
clis: found.filter((agent) => agent !== "pi" || !piWithoutMcp),
|
|
8252
8683
|
keys: Object.keys(KEY_PROVIDERS).filter((key) => env[KEY_PROVIDERS[key].env]),
|
|
8253
|
-
jev: Object.keys(JEV_ROUTES2).filter((route2) => env[JEV_ROUTES2[route2].env]),
|
|
8254
8684
|
piWithoutMcp,
|
|
8255
8685
|
piPermissionModes: piList.includes("pi-permission-modes")
|
|
8256
8686
|
};
|
|
@@ -8285,18 +8715,18 @@ function list(dir) {
|
|
|
8285
8715
|
}
|
|
8286
8716
|
}
|
|
8287
8717
|
function xcodeProjects(root2) {
|
|
8288
|
-
return [".", "ios"].flatMap((dir) => list(
|
|
8718
|
+
return [".", "ios"].flatMap((dir) => list(join14(root2, dir)).filter((name) => name.endsWith(".xcodeproj")).map((name) => join14(root2, dir, name, "project.pbxproj")));
|
|
8289
8719
|
}
|
|
8290
8720
|
function gradleFiles(root2) {
|
|
8291
|
-
return ["android/app", "app"].flatMap((dir) => ["build.gradle", "build.gradle.kts"].map((file) =>
|
|
8721
|
+
return ["android/app", "app"].flatMap((dir) => ["build.gradle", "build.gradle.kts"].map((file) => join14(root2, dir, file))).filter((file) => existsSync6(file));
|
|
8292
8722
|
}
|
|
8293
8723
|
function detectAppId(root2, platform) {
|
|
8294
|
-
const expo = readJson2(
|
|
8724
|
+
const expo = readJson2(join14(root2, "app.json"))?.expo;
|
|
8295
8725
|
const fromJson = platform === "ios" ? expo?.ios?.bundleIdentifier : expo?.android?.package;
|
|
8296
8726
|
if (fromJson)
|
|
8297
8727
|
return { appId: fromJson, source: "app.json" };
|
|
8298
8728
|
for (const file of ["app.config.ts", "app.config.js"]) {
|
|
8299
|
-
const match = readText(
|
|
8729
|
+
const match = readText(join14(root2, file)).match(platform === "ios" ? /bundleIdentifier:\s*['"`]([\w.-]+)['"`]/ : /package:\s*['"`]([\w.-]+)['"`]/);
|
|
8300
8730
|
if (match)
|
|
8301
8731
|
return { appId: match[1], source: file };
|
|
8302
8732
|
}
|
|
@@ -8322,21 +8752,21 @@ function detectPort(root2, pkg, framework) {
|
|
|
8322
8752
|
if (fromScript)
|
|
8323
8753
|
return { port: Number(fromScript), source: "package.json" };
|
|
8324
8754
|
for (const file of ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) {
|
|
8325
|
-
const match = readText(
|
|
8755
|
+
const match = readText(join14(root2, file)).match(/\bport:\s*(\d{2,5})\b/);
|
|
8326
8756
|
if (match)
|
|
8327
8757
|
return { port: Number(match[1]), source: file };
|
|
8328
8758
|
}
|
|
8329
|
-
const fromEnv = readText(
|
|
8759
|
+
const fromEnv = readText(join14(root2, ".env")).match(/^PORT=(\d{2,5})\s*$/m)?.[1];
|
|
8330
8760
|
if (fromEnv)
|
|
8331
8761
|
return { port: Number(fromEnv), source: ".env" };
|
|
8332
8762
|
return { port: framework && FRAMEWORK_PORTS[framework] || 3e3, source: framework ? `the ${framework} default` : "the usual default" };
|
|
8333
8763
|
}
|
|
8334
8764
|
function mobilePlatform(root2) {
|
|
8335
|
-
const hasIos = xcodeProjects(root2).length > 0 || existsSync6(
|
|
8765
|
+
const hasIos = xcodeProjects(root2).length > 0 || existsSync6(join14(root2, "app.json")) || existsSync6(join14(root2, "ios"));
|
|
8336
8766
|
return process.platform === "darwin" && hasIos ? "ios" : "android";
|
|
8337
8767
|
}
|
|
8338
8768
|
function detectApp(root2) {
|
|
8339
|
-
const pkg = readJson2(
|
|
8769
|
+
const pkg = readJson2(join14(root2, "package.json")) ?? {};
|
|
8340
8770
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
8341
8771
|
const notes = [];
|
|
8342
8772
|
if (deps.electron) {
|
|
@@ -8344,8 +8774,8 @@ function detectApp(root2) {
|
|
|
8344
8774
|
return { platform: "electron", start: "npx electron . --remote-debugging-port=9222", cdpPort: 9222, notes };
|
|
8345
8775
|
}
|
|
8346
8776
|
const reactNative = deps.expo || deps["react-native"];
|
|
8347
|
-
const nativeIos = !existsSync6(
|
|
8348
|
-
const nativeAndroid = !existsSync6(
|
|
8777
|
+
const nativeIos = !existsSync6(join14(root2, "package.json")) && xcodeProjects(root2).length > 0;
|
|
8778
|
+
const nativeAndroid = !existsSync6(join14(root2, "package.json")) && gradleFiles(root2).length > 0;
|
|
8349
8779
|
if (reactNative || nativeIos || nativeAndroid) {
|
|
8350
8780
|
const platform = nativeIos ? "ios" : nativeAndroid ? "android" : mobilePlatform(root2);
|
|
8351
8781
|
notes.push(`Platform ${platform}: detected from ${reactNative ? "package.json" : platform === "ios" ? "the Xcode project" : "the Gradle files"}.`);
|
|
@@ -8415,7 +8845,7 @@ function renderConfig(answers) {
|
|
|
8415
8845
|
lines.push(` appId: ${answers.appId ?? "com.example.app"}${answers.appId ? "" : " # TODO: your bundle ID or package name"}`);
|
|
8416
8846
|
lines.push(" # device: <simulator UDID or emulator serial> # default: the booted one");
|
|
8417
8847
|
}
|
|
8418
|
-
lines.push(" instructions: .bughunters/instructions.md # plain English for the explorer: sign in, main flows, never-do list", " secrets: [] # env var names the explorer may use as {{NAME}}, e.g. [TEST_PASSWORD]", "", "# Each agent runs on an LLM: a local agent CLI (claude, codex, kimi, pi) or an API key.", "agents:", " explorer: # uses the app and reports what looks wrong", ` use: ${useLine("explorer", answers.providers.explorer, answers.piPermissionModes)}`, " judge: # decides which reports are real bugs, and writes the issues", ` use: ${useLine("judge", answers.providers.judge, answers.piPermissionModes)}`, " fixer: # writes a fix in its own git worktree", " enabled: false # turn on when you want Bughunters to fix bugs", ` use: ${useLine("fixer", answers.providers.fixer, answers.piPermissionModes)}`, " github:", " enabled: false # turn on to open PRs and issues (needs the gh CLI)", ""
|
|
8848
|
+
lines.push(" instructions: .bughunters/instructions.md # plain English for the explorer: sign in, main flows, never-do list", " secrets: [] # env var names the explorer may use as {{NAME}}, e.g. [TEST_PASSWORD]", "", "# Each agent runs on an LLM: a local agent CLI (claude, codex, kimi, pi) or an API key.", "agents:", " explorer: # uses the app and reports what looks wrong", ` use: ${useLine("explorer", answers.providers.explorer, answers.piPermissionModes)}`, " judge: # decides which reports are real bugs, and writes the issues", ` use: ${useLine("judge", answers.providers.judge, answers.piPermissionModes)}`, " fixer: # writes a fix in its own git worktree", " enabled: false # turn on when you want Bughunters to fix bugs", ` use: ${useLine("fixer", answers.providers.fixer, answers.piPermissionModes)}`, " github:", " enabled: false # turn on to open PRs and issues (needs the gh CLI)", "");
|
|
8419
8849
|
return lines.join("\n");
|
|
8420
8850
|
}
|
|
8421
8851
|
var INSTRUCTIONS = `# About the app
|
|
@@ -8438,7 +8868,7 @@ Example: Sign in with the email {{TEST_EMAIL}} and the password {{TEST_PASSWORD}
|
|
|
8438
8868
|
- Do not make payments.
|
|
8439
8869
|
`;
|
|
8440
8870
|
function ignoreData(root2) {
|
|
8441
|
-
const gitignore =
|
|
8871
|
+
const gitignore = join14(root2, ".gitignore");
|
|
8442
8872
|
const ignored = existsSync6(gitignore) ? readFileSync5(gitignore, "utf8") : "";
|
|
8443
8873
|
const lines = ignored.split("\n");
|
|
8444
8874
|
if (lines.some((line) => /^\/?\.bughunters\/runs\/?$/.test(line.trim())))
|
|
@@ -8455,7 +8885,7 @@ ${IGNORE_LINE}
|
|
|
8455
8885
|
return true;
|
|
8456
8886
|
}
|
|
8457
8887
|
function relativeConfig(root2) {
|
|
8458
|
-
const shown =
|
|
8888
|
+
const shown = relative4(process.cwd(), paths.config(root2));
|
|
8459
8889
|
return shown && !shown.startsWith("..") ? shown : paths.config(root2);
|
|
8460
8890
|
}
|
|
8461
8891
|
var IGNORE_LINE = `${BUGHUNTERS_DIR}/${DATA_DIR}/`;
|
|
@@ -8470,7 +8900,7 @@ function writeInitialConfig(root2, answers) {
|
|
|
8470
8900
|
writeFileSync3(configPath, renderConfig(answers));
|
|
8471
8901
|
result3.written.push(configName);
|
|
8472
8902
|
}
|
|
8473
|
-
const guidePath =
|
|
8903
|
+
const guidePath = join14(paths.dir(root2), "instructions.md");
|
|
8474
8904
|
if (existsSync6(guidePath))
|
|
8475
8905
|
result3.skipped.push(`${BUGHUNTERS_DIR}/instructions.md`);
|
|
8476
8906
|
else {
|
|
@@ -8485,12 +8915,6 @@ function writeInitialConfig(root2, answers) {
|
|
|
8485
8915
|
result3.warnings.push(`Set ${route2.env} before you run Bughunters. Get a key at ${route2.url}`);
|
|
8486
8916
|
}
|
|
8487
8917
|
}
|
|
8488
|
-
if (answers.jev !== "auto" && !process.env[JEV_ROUTES2[answers.jev].env]) {
|
|
8489
|
-
result3.warnings.push(`Set ${JEV_ROUTES2[answers.jev].env} to use Jev. Get a key at ${JEV_ROUTES2[answers.jev].url}`);
|
|
8490
|
-
}
|
|
8491
|
-
if (answers.jev === "auto" && !Object.values(JEV_ROUTES2).some((route2) => process.env[route2.env])) {
|
|
8492
|
-
result3.warnings.push("No Jev key is set, so Bughunters costs more. Get a key at https://typesafe.ai");
|
|
8493
|
-
}
|
|
8494
8918
|
return result3;
|
|
8495
8919
|
}
|
|
8496
8920
|
var PROVIDERS = [...CLI_AGENTS, ...Object.keys(KEY_PROVIDERS)];
|
|
@@ -8535,9 +8959,6 @@ function parseInitFlags(args2) {
|
|
|
8535
8959
|
case "--fixer":
|
|
8536
8960
|
flags[flag2.slice(2)] = oneOf(flag2, args2[++index], PROVIDERS);
|
|
8537
8961
|
break;
|
|
8538
|
-
case "--jev":
|
|
8539
|
-
flags.jev = oneOf(flag2, args2[++index], ["typesafe", "openrouter", "vercel", "auto"]);
|
|
8540
|
-
break;
|
|
8541
8962
|
default:
|
|
8542
8963
|
throw new ConfigError(`Unknown flag for init: ${flag2}`);
|
|
8543
8964
|
}
|
|
@@ -8584,7 +9005,6 @@ function defaultAnswers(guess, detected, flags) {
|
|
|
8584
9005
|
cdpPort: guess.cdpPort,
|
|
8585
9006
|
ready: platform === guess.platform ? guess.ready : void 0,
|
|
8586
9007
|
providers,
|
|
8587
|
-
jev: flags.jev ?? detected.jev[0] ?? "auto",
|
|
8588
9008
|
piPermissionModes: detected.piPermissionModes
|
|
8589
9009
|
};
|
|
8590
9010
|
}
|
|
@@ -8637,15 +9057,6 @@ async function interview(ask, root2, guess, detected, answers) {
|
|
|
8637
9057
|
next.providers.explorer = await choose(ask, "Explorer: it uses the app and reports what looks wrong.", options, next.providers.explorer);
|
|
8638
9058
|
next.providers.judge = await choose(ask, "Judge: it decides which reports are real bugs.", options, next.providers.explorer);
|
|
8639
9059
|
next.providers.fixer = await choose(ask, "Fixer: it writes the fixes (off until you turn it on).", options, next.providers.fixer);
|
|
8640
|
-
process.stdout.write("\nJev triages each finding in one fast, low-cost call. The LLM judge then sees only the real findings.\n");
|
|
8641
|
-
const jevOptions = [
|
|
8642
|
-
...Object.keys(JEV_ROUTES2).map((route2) => ({
|
|
8643
|
-
value: route2,
|
|
8644
|
-
label: `${JEV_ROUTES2[route2].label} (${JEV_ROUTES2[route2].env}${detected.jev.includes(route2) ? " is set" : `, get a key at ${JEV_ROUTES2[route2].url}`})`
|
|
8645
|
-
})),
|
|
8646
|
-
{ value: "auto", label: "Try each key in order (with no key, the LLM does this work at a higher cost)" }
|
|
8647
|
-
];
|
|
8648
|
-
next.jev = await choose(ask, "How does Bughunters reach Jev?", jevOptions, next.jev === "auto" ? detected.jev[0] ?? "typesafe" : next.jev);
|
|
8649
9060
|
return next;
|
|
8650
9061
|
}
|
|
8651
9062
|
async function runInit(root2, args2, log) {
|
|
@@ -8778,9 +9189,6 @@ determinism:
|
|
|
8778
9189
|
blockThirdPartyRequests: true
|
|
8779
9190
|
|
|
8780
9191
|
decisions:
|
|
8781
|
-
decider: jev # jev | model
|
|
8782
|
-
jev:
|
|
8783
|
-
via: auto # auto | typesafe | openrouter | vercel
|
|
8784
9192
|
model:
|
|
8785
9193
|
via: auto # auto | openrouter | vercel | openai | anthropic | custom
|
|
8786
9194
|
name: "" # optional model id override
|
|
@@ -8812,7 +9220,7 @@ production:
|
|
|
8812
9220
|
|
|
8813
9221
|
// ../cli/dist/commands/run.js
|
|
8814
9222
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8815
|
-
import { join as
|
|
9223
|
+
import { join as join16 } from "node:path";
|
|
8816
9224
|
|
|
8817
9225
|
// ../capture/dist/determinism.js
|
|
8818
9226
|
var DETERMINISTIC_CHROMIUM_ARGS = [
|
|
@@ -9004,7 +9412,7 @@ async function assertNoFontFallback(page, failOnFallback) {
|
|
|
9004
9412
|
|
|
9005
9413
|
// ../capture/dist/capture.js
|
|
9006
9414
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9007
|
-
import { dirname as
|
|
9415
|
+
import { dirname as dirname4 } from "node:path";
|
|
9008
9416
|
|
|
9009
9417
|
// ../capture/dist/pixel-contrast.js
|
|
9010
9418
|
import { PNG as PNG3 } from "pngjs";
|
|
@@ -9077,7 +9485,7 @@ async function captureScreen(page, config, request) {
|
|
|
9077
9485
|
const stability = await waitForStableFrame(page, config.determinism.stabilityGate);
|
|
9078
9486
|
const masks = await resolveMasks(page, config.mask, request.url);
|
|
9079
9487
|
const consoleErrors2 = collectedErrors.get(page) ?? [];
|
|
9080
|
-
mkdirSync3(
|
|
9488
|
+
mkdirSync3(dirname4(request.outPath), { recursive: true });
|
|
9081
9489
|
const buffer = await page.screenshot({
|
|
9082
9490
|
path: request.outPath,
|
|
9083
9491
|
fullPage: request.fullPage ?? true,
|
|
@@ -9215,7 +9623,7 @@ function bodyMentions(body, selector) {
|
|
|
9215
9623
|
async function stopProcess(child) {
|
|
9216
9624
|
if (child.exitCode !== null || child.pid === void 0)
|
|
9217
9625
|
return;
|
|
9218
|
-
const done = new Promise((
|
|
9626
|
+
const done = new Promise((resolve12) => child.once("exit", () => resolve12()));
|
|
9219
9627
|
try {
|
|
9220
9628
|
process.kill(-child.pid, "SIGTERM");
|
|
9221
9629
|
} catch {
|
|
@@ -9233,7 +9641,7 @@ async function stopProcess(child) {
|
|
|
9233
9641
|
|
|
9234
9642
|
// ../cli/dist/commands/run-pipeline.js
|
|
9235
9643
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9236
|
-
import { join as
|
|
9644
|
+
import { join as join15 } from "node:path";
|
|
9237
9645
|
|
|
9238
9646
|
// ../triage/dist/ledger.js
|
|
9239
9647
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
|
|
@@ -9335,8 +9743,8 @@ async function cluster(findings, options = {}) {
|
|
|
9335
9743
|
function deterministicPass(findings, changedFilesByFinding) {
|
|
9336
9744
|
const buckets = /* @__PURE__ */ new Map();
|
|
9337
9745
|
for (const finding of findings) {
|
|
9338
|
-
const
|
|
9339
|
-
const key = [finding.ruleId, [...
|
|
9746
|
+
const files2 = changedFilesByFinding?.get(finding.id) ?? finding.suspectedFiles;
|
|
9747
|
+
const key = [finding.ruleId, [...files2].sort().join(",")].join("::");
|
|
9340
9748
|
const bucket = buckets.get(key);
|
|
9341
9749
|
if (bucket)
|
|
9342
9750
|
bucket.push(finding);
|
|
@@ -9664,7 +10072,7 @@ async function executeRun(options) {
|
|
|
9664
10072
|
console: screen.snapshot.consoleErrors,
|
|
9665
10073
|
knownIntents: ledger.summaries()
|
|
9666
10074
|
});
|
|
9667
|
-
const estimate = decider ? estimateDecisionCost(text3.length
|
|
10075
|
+
const estimate = decider ? estimateDecisionCost(text3.length) : 0;
|
|
9668
10076
|
let answers = {};
|
|
9669
10077
|
if (!decider) {
|
|
9670
10078
|
trace2.decision = {
|
|
@@ -9913,13 +10321,13 @@ function worstOf(a, b) {
|
|
|
9913
10321
|
function writeArtifacts(root2, run2, findings, meta) {
|
|
9914
10322
|
const dir = paths.run(root2, run2.id);
|
|
9915
10323
|
mkdirSync4(dir, { recursive: true });
|
|
9916
|
-
const reportPath =
|
|
10324
|
+
const reportPath = join15(dir, "report.html");
|
|
9917
10325
|
writeFileSync5(reportPath, renderHtml({ run: run2, findings, suppressed: meta.suppressed, quarantined: 0, notes: meta.notes }));
|
|
9918
|
-
writeFileSync5(
|
|
9919
|
-
writeFileSync5(
|
|
9920
|
-
writeFileSync5(
|
|
10326
|
+
writeFileSync5(join15(dir, "junit.xml"), toJUnit(run2, findings));
|
|
10327
|
+
writeFileSync5(join15(dir, "results.sarif"), toSarif(findings));
|
|
10328
|
+
writeFileSync5(join15(dir, "run.json"), `${JSON.stringify({ run: run2, findings }, null, 2)}
|
|
9921
10329
|
`);
|
|
9922
|
-
writeFileSync5(
|
|
10330
|
+
writeFileSync5(join15(dir, "trace.json"), `${JSON.stringify(meta.trace, null, 2)}
|
|
9923
10331
|
`);
|
|
9924
10332
|
return { reportPath, runDir: dir };
|
|
9925
10333
|
}
|
|
@@ -9948,7 +10356,7 @@ async function runCommand(options) {
|
|
|
9948
10356
|
if (server.external)
|
|
9949
10357
|
log(`Using the app already serving at ${requireRun(config).url}`);
|
|
9950
10358
|
const captured = [];
|
|
9951
|
-
const runDir =
|
|
10359
|
+
const runDir = join16(paths.runs(root2), "latest");
|
|
9952
10360
|
mkdirSync5(runDir, { recursive: true });
|
|
9953
10361
|
const live = new LiveProgressWriter(root2, targets.length * config.viewports.length);
|
|
9954
10362
|
live.start();
|
|
@@ -10052,7 +10460,7 @@ var LiveProgressWriter = class {
|
|
|
10052
10460
|
async function captureOne(args2) {
|
|
10053
10461
|
const { target, viewport, session, config, store, runDir } = args2;
|
|
10054
10462
|
const slug2 = `${slugify(target.id)}--${viewport.name}`;
|
|
10055
|
-
const actualPath =
|
|
10463
|
+
const actualPath = join16(runDir, `${slug2}.actual.png`);
|
|
10056
10464
|
const output = await captureScreen(session.page, config, {
|
|
10057
10465
|
screenId: target.id,
|
|
10058
10466
|
url: target.url,
|
|
@@ -10077,7 +10485,7 @@ async function captureOne(args2) {
|
|
|
10077
10485
|
missingFonts: output.missingFonts
|
|
10078
10486
|
};
|
|
10079
10487
|
}
|
|
10080
|
-
const diffPath =
|
|
10488
|
+
const diffPath = join16(runDir, `${slug2}.diff.png`);
|
|
10081
10489
|
const comparison = await diff({
|
|
10082
10490
|
baselinePath,
|
|
10083
10491
|
actualPath,
|
|
@@ -10140,7 +10548,7 @@ function loadBaselineSnapshot(root2, key) {
|
|
|
10140
10548
|
}
|
|
10141
10549
|
}
|
|
10142
10550
|
function snapshotFile(root2, key) {
|
|
10143
|
-
return
|
|
10551
|
+
return join16(paths.baselines(root2), `${slugify(key)}-${shortHash(key, 8)}.snapshot.json`);
|
|
10144
10552
|
}
|
|
10145
10553
|
function saveBaselineSnapshot(root2, key, snapshot) {
|
|
10146
10554
|
mkdirSync5(paths.baselines(root2), { recursive: true });
|
|
@@ -10241,12 +10649,12 @@ function formatRunSummary(result3) {
|
|
|
10241
10649
|
// ../dashboard/dist/server.js
|
|
10242
10650
|
import { createServer as createServer2 } from "node:http";
|
|
10243
10651
|
import { createReadStream, existsSync as existsSync10, realpathSync, statSync as statSync2, watch } from "node:fs";
|
|
10244
|
-
import { extname, join as
|
|
10652
|
+
import { extname, join as join19, relative as relative5, resolve as resolve11, sep as sep4 } from "node:path";
|
|
10245
10653
|
import { fileURLToPath } from "node:url";
|
|
10246
10654
|
|
|
10247
10655
|
// ../dashboard/dist/project.js
|
|
10248
10656
|
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
10249
|
-
import { join as
|
|
10657
|
+
import { join as join17 } from "node:path";
|
|
10250
10658
|
var ProjectReader = class {
|
|
10251
10659
|
root;
|
|
10252
10660
|
constructor(root2) {
|
|
@@ -10263,16 +10671,16 @@ var ProjectReader = class {
|
|
|
10263
10671
|
const dir = paths.runs(this.root);
|
|
10264
10672
|
if (!existsSync9(dir))
|
|
10265
10673
|
return [];
|
|
10266
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== "latest").map((e) => ({ name: e.name, mtime: safeMtime(
|
|
10674
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== "latest").map((e) => ({ name: e.name, mtime: safeMtime(join17(dir, e.name)) })).sort((a, b) => b.mtime - a.mtime).map((e) => e.name);
|
|
10267
10675
|
}
|
|
10268
10676
|
readRun(runId) {
|
|
10269
|
-
const dir =
|
|
10270
|
-
const runFile =
|
|
10677
|
+
const dir = join17(paths.runs(this.root), runId);
|
|
10678
|
+
const runFile = join17(dir, "run.json");
|
|
10271
10679
|
if (!existsSync9(runFile))
|
|
10272
10680
|
return void 0;
|
|
10273
10681
|
try {
|
|
10274
10682
|
const parsed = JSON.parse(readFileSync8(runFile, "utf8"));
|
|
10275
|
-
const traceFile =
|
|
10683
|
+
const traceFile = join17(dir, "trace.json");
|
|
10276
10684
|
const trace = existsSync9(traceFile) ? JSON.parse(readFileSync8(traceFile, "utf8")) : void 0;
|
|
10277
10685
|
return { id: runId, dir, run: parsed.run, findings: parsed.findings, trace, mtimeMs: safeMtime(dir) };
|
|
10278
10686
|
} catch {
|
|
@@ -10358,8 +10766,8 @@ function safeMtime(path) {
|
|
|
10358
10766
|
|
|
10359
10767
|
// ../dashboard/dist/agents.js
|
|
10360
10768
|
import { readFileSync as readFileSync9, readdirSync as readdirSync3 } from "node:fs";
|
|
10361
|
-
import { basename as basename4, join as
|
|
10362
|
-
var
|
|
10769
|
+
import { basename as basename4, join as join18 } from "node:path";
|
|
10770
|
+
var roles2 = ["explorer", "judge", "fixer"];
|
|
10363
10771
|
var severity = { critical: 0, major: 1, minor: 2, cosmetic: 3 };
|
|
10364
10772
|
var open = /* @__PURE__ */ new Set(["new", "filed", "fixing", "fix-proposed"]);
|
|
10365
10773
|
function readJson3(file) {
|
|
@@ -10415,7 +10823,7 @@ var AgentReader = class {
|
|
|
10415
10823
|
const value = readJson3(paths.agents(this.root));
|
|
10416
10824
|
if (!Array.isArray(value?.agents))
|
|
10417
10825
|
return void 0;
|
|
10418
|
-
const agents = value.agents.filter((agent) => agent &&
|
|
10826
|
+
const agents = value.agents.filter((agent) => agent && roles2.includes(agent.role) && typeof agent.state === "string").map((agent) => {
|
|
10419
10827
|
const busy = agent.state === "working" || agent.state === "waiting";
|
|
10420
10828
|
return busy && !processAlive(agent.pid) ? { ...agent, state: "idle", activity: STOPPED } : agent;
|
|
10421
10829
|
});
|
|
@@ -10430,28 +10838,31 @@ var AgentReader = class {
|
|
|
10430
10838
|
}
|
|
10431
10839
|
issues() {
|
|
10432
10840
|
const fixes = this.fixes();
|
|
10433
|
-
return list2(paths.issues(this.root)).filter((name) => name.endsWith(".json")).map((name) => readJson3(
|
|
10841
|
+
return list2(paths.issues(this.root)).filter((name) => name.endsWith(".json")).map((name) => readJson3(join18(paths.issues(this.root), name))).filter((issue) => Boolean(issue?.id && issue.title && issue.lastSeenAt && issue.evidence && issue.judgement && Array.isArray(issue.candidateIds))).map((issue) => {
|
|
10842
|
+
const fix = fixes.find((entry) => entry.id === issue.fixId || entry.issueId === issue.id);
|
|
10843
|
+
return { ...issue, pr: fix?.pr ? { ...fix.pr, ci: fix.ci?.state } : void 0 };
|
|
10844
|
+
}).sort((a, b) => (severity[a.severity] ?? 9) - (severity[b.severity] ?? 9) || b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
10434
10845
|
}
|
|
10435
10846
|
fixes() {
|
|
10436
|
-
return list2(paths.fixes(this.root)).filter((name) => name.endsWith(".json")).map((name) => readJson3(
|
|
10847
|
+
return list2(paths.fixes(this.root)).filter((name) => name.endsWith(".json")).map((name) => readJson3(join18(paths.fixes(this.root), name))).filter((fix) => Boolean(fix?.id && fix.issueId));
|
|
10437
10848
|
}
|
|
10438
10849
|
routines() {
|
|
10439
|
-
return list2(paths.routines(this.root)).filter((name) => name.endsWith(".json")).map((name) => readJson3(
|
|
10850
|
+
return list2(paths.routines(this.root)).filter((name) => name.endsWith(".json")).map((name) => readJson3(join18(paths.routines(this.root), name))).filter((routine) => Boolean(routine?.id && Array.isArray(routine.steps)));
|
|
10440
10851
|
}
|
|
10441
10852
|
sessions(limit = 50) {
|
|
10442
|
-
return list2(paths.sessions(this.root)).filter((id2) => /^[\w-]+$/.test(id2)).map((id2) => ({ id: id2, value: readJson3(
|
|
10853
|
+
return list2(paths.sessions(this.root)).filter((id2) => /^[\w-]+$/.test(id2)).map((id2) => ({ id: id2, value: readJson3(join18(paths.session(this.root, id2), "session.json")) })).filter(({ id: id2, value }) => value?.id === id2).map(({ value }) => value).filter((session) => Boolean(session?.id && session.startedAt && Array.isArray(session.screensFound) && Array.isArray(session.issues))).map((session) => session.status === "running" && !processAlive(session.pid) ? { ...session, status: "failed", summary: session.summary ?? `Interrupted. ${STOPPED}` } : session).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, limit);
|
|
10443
10854
|
}
|
|
10444
10855
|
session(id2) {
|
|
10445
10856
|
const session = this.sessions(Infinity).find((entry) => entry.id === id2);
|
|
10446
10857
|
if (!session)
|
|
10447
10858
|
return void 0;
|
|
10448
10859
|
const dir = paths.session(this.root, id2);
|
|
10449
|
-
const events = jsonLines(
|
|
10860
|
+
const events = jsonLines(join18(dir, "events.jsonl"));
|
|
10450
10861
|
return {
|
|
10451
10862
|
// A running session has no totals yet: add up its events so far.
|
|
10452
10863
|
session: session.status === "running" ? { ...session, ...usageOf(events) } : session,
|
|
10453
10864
|
events,
|
|
10454
|
-
candidates: jsonLines(
|
|
10865
|
+
candidates: jsonLines(join18(dir, "candidates.jsonl"))
|
|
10455
10866
|
};
|
|
10456
10867
|
}
|
|
10457
10868
|
issue(id2) {
|
|
@@ -10461,7 +10872,7 @@ var AgentReader = class {
|
|
|
10461
10872
|
const fix = this.fixes().find((entry) => entry.id === issue.fixId || entry.issueId === id2);
|
|
10462
10873
|
const routine = this.routines().find((entry) => entry.id === issue.evidence?.routineId);
|
|
10463
10874
|
const ids = new Set(issue.candidateIds);
|
|
10464
|
-
const candidates = this.sessions(Infinity).flatMap((session) => jsonLines(
|
|
10875
|
+
const candidates = this.sessions(Infinity).flatMap((session) => jsonLines(join18(paths.session(this.root, session.id), "candidates.jsonl"))).filter((candidate) => ids.has(candidate.id));
|
|
10465
10876
|
return { issue, fix, candidates, routine };
|
|
10466
10877
|
}
|
|
10467
10878
|
screens() {
|
|
@@ -10510,6 +10921,10 @@ var AgentReader = class {
|
|
|
10510
10921
|
edges.push({ from: "__start", to: screen.id, kind: "route", via: "route", count: 1, steps: 0 });
|
|
10511
10922
|
}
|
|
10512
10923
|
}
|
|
10924
|
+
if (map.screens.length && !edges.some((edge) => edge.from === "__start")) {
|
|
10925
|
+
const entry = byRoutine.get("enter-app")?.requires?.map((id2) => screenByRoutine.get(id2)).find(Boolean) ?? map.screens[0].id;
|
|
10926
|
+
edges.push({ from: "__start", to: entry, kind: "route", via: "route", count: 1, steps: 0 });
|
|
10927
|
+
}
|
|
10513
10928
|
const screens = map.screens.map((screen) => ({
|
|
10514
10929
|
...screen,
|
|
10515
10930
|
openIssues: issues.filter((issue) => issue.screenId === screen.id).length
|
|
@@ -10518,16 +10933,30 @@ var AgentReader = class {
|
|
|
10518
10933
|
screens.push({ id: "__start", name: "App start", virtual: true, openIssues: 0 });
|
|
10519
10934
|
return { ...map, screens, edges, entryId: screens.length ? "__start" : void 0 };
|
|
10520
10935
|
}
|
|
10936
|
+
/** Each role as the config sets it, in the same form as the runtime labels. */
|
|
10937
|
+
configuredAgents() {
|
|
10938
|
+
try {
|
|
10939
|
+
const config = loadConfig(this.root);
|
|
10940
|
+
return Object.fromEntries(roles2.map((role) => {
|
|
10941
|
+
const { enabled, use } = config.agents[role];
|
|
10942
|
+
const runtime = use.runtime === "cli" ? `cli:${use.command.split(/\s+/)[0]}` : `model:${use.via}/${use.model}`;
|
|
10943
|
+
return [role, { enabled, runtime }];
|
|
10944
|
+
}));
|
|
10945
|
+
} catch {
|
|
10946
|
+
return void 0;
|
|
10947
|
+
}
|
|
10948
|
+
}
|
|
10521
10949
|
overview(now = /* @__PURE__ */ new Date()) {
|
|
10522
10950
|
const agentFile = this.agents();
|
|
10523
10951
|
const appmap = this.appmap();
|
|
10524
10952
|
const issues = this.issues();
|
|
10525
10953
|
const sessions = this.sessions(Infinity);
|
|
10526
10954
|
const today = now.toLocaleDateString("en-CA");
|
|
10527
|
-
const
|
|
10955
|
+
const configured = this.configuredAgents();
|
|
10956
|
+
const agents = roles2.map((role) => agentFile?.agents.find((agent) => agent.role === role) ?? {
|
|
10528
10957
|
role,
|
|
10529
|
-
state: "off",
|
|
10530
|
-
runtime: "",
|
|
10958
|
+
state: configured?.[role].enabled === false ? "off" : configured ? "idle" : "off",
|
|
10959
|
+
runtime: configured?.[role].runtime ?? "",
|
|
10531
10960
|
updatedAt: "",
|
|
10532
10961
|
spentUsd: 0
|
|
10533
10962
|
});
|
|
@@ -10537,9 +10966,9 @@ var AgentReader = class {
|
|
|
10537
10966
|
const screenshot = [...detail?.events ?? []].reverse().find((event) => event.screenshot)?.screenshot;
|
|
10538
10967
|
const openIssues = issues.filter((issue) => open.has(issue.status));
|
|
10539
10968
|
const fixes = this.fixes();
|
|
10540
|
-
const attention = openIssues.
|
|
10969
|
+
const attention = openIssues.slice(0, 8).map((issue) => {
|
|
10541
10970
|
const fix = fixes.find((entry) => entry.id === issue.fixId || entry.issueId === issue.id);
|
|
10542
|
-
return { ...issue, pr: fix?.pr, fix: fix ? { status: fix.status } : void 0 };
|
|
10971
|
+
return { ...issue, pr: fix?.pr ? { ...fix.pr, ci: fix.ci?.state } : void 0, fix: fix ? { status: fix.status } : void 0 };
|
|
10543
10972
|
});
|
|
10544
10973
|
const prStates = fixes.map((fix) => fix.pr?.state).filter(Boolean);
|
|
10545
10974
|
const issueStates = issues.map((issue) => issue.github?.state).filter(Boolean);
|
|
@@ -10591,7 +11020,7 @@ function usageReport(sessions, now = /* @__PURE__ */ new Date()) {
|
|
|
10591
11020
|
rows.set(key, row);
|
|
10592
11021
|
}
|
|
10593
11022
|
}
|
|
10594
|
-
const week = [...rows.values()].sort((a, b) =>
|
|
11023
|
+
const week = [...rows.values()].sort((a, b) => roles2.indexOf(a.role) - roles2.indexOf(b.role) || b.tokens.input + b.tokens.output - (a.tokens.input + a.tokens.output));
|
|
10595
11024
|
return { todayByRole, week, ...weekTotal ? { weekTotal } : {} };
|
|
10596
11025
|
}
|
|
10597
11026
|
function isFeedEvent(event) {
|
|
@@ -10859,7 +11288,7 @@ function hostOf(href) {
|
|
|
10859
11288
|
}
|
|
10860
11289
|
|
|
10861
11290
|
// ../dashboard/dist/server.js
|
|
10862
|
-
var UI_DIR =
|
|
11291
|
+
var UI_DIR = resolve11(fileURLToPath(new URL(".", import.meta.url)), "..", "src", "ui");
|
|
10863
11292
|
var MIME = {
|
|
10864
11293
|
".html": "text/html; charset=utf-8",
|
|
10865
11294
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -10870,7 +11299,7 @@ var MIME = {
|
|
|
10870
11299
|
".json": "application/json; charset=utf-8"
|
|
10871
11300
|
};
|
|
10872
11301
|
async function startDashboard(options) {
|
|
10873
|
-
const root2 =
|
|
11302
|
+
const root2 = resolve11(options.root);
|
|
10874
11303
|
const host = options.host ?? "127.0.0.1";
|
|
10875
11304
|
const reader = new ProjectReader(root2);
|
|
10876
11305
|
const agents = new AgentReader(root2);
|
|
@@ -10965,7 +11394,7 @@ async function handle2(req, res, ctx) {
|
|
|
10965
11394
|
return serveUi(res, path);
|
|
10966
11395
|
}
|
|
10967
11396
|
function resolveArtifactPath(root2, requested) {
|
|
10968
|
-
const candidate =
|
|
11397
|
+
const candidate = resolve11(root2, requested);
|
|
10969
11398
|
if (!existsSync10(candidate))
|
|
10970
11399
|
return void 0;
|
|
10971
11400
|
let real;
|
|
@@ -10976,8 +11405,8 @@ function resolveArtifactPath(root2, requested) {
|
|
|
10976
11405
|
} catch {
|
|
10977
11406
|
return void 0;
|
|
10978
11407
|
}
|
|
10979
|
-
const rel =
|
|
10980
|
-
if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${
|
|
11408
|
+
const rel = relative5(allowed, real);
|
|
11409
|
+
if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${sep4}`))
|
|
10981
11410
|
return void 0;
|
|
10982
11411
|
const stat = statSync2(real);
|
|
10983
11412
|
if (!stat.isFile())
|
|
@@ -10998,7 +11427,7 @@ function serveArtifact(res, root2, requested) {
|
|
|
10998
11427
|
}
|
|
10999
11428
|
function serveUi(res, path) {
|
|
11000
11429
|
const name = path === "/" ? "index.html" : path.replace(/^\//, "");
|
|
11001
|
-
const file =
|
|
11430
|
+
const file = resolve11(UI_DIR, name);
|
|
11002
11431
|
if (!file.startsWith(UI_DIR) || !existsSync10(file) || !statSync2(file).isFile()) {
|
|
11003
11432
|
return serveUi(res, "/");
|
|
11004
11433
|
}
|
|
@@ -11131,14 +11560,19 @@ function parseAgentFlags(command2, args2) {
|
|
|
11131
11560
|
fix: ["--issue"],
|
|
11132
11561
|
retest: ["--issue"],
|
|
11133
11562
|
publish: ["--issue"],
|
|
11563
|
+
ci: ["--issue"],
|
|
11134
11564
|
patrol: []
|
|
11135
11565
|
};
|
|
11136
11566
|
if (!values[command2])
|
|
11137
11567
|
throw new ConfigError(`Unknown agent command ${command2}`);
|
|
11138
11568
|
for (let index = 0; index < args2.length; index++) {
|
|
11139
11569
|
const flag2 = args2[index];
|
|
11140
|
-
if (command2 === "patrol" && flag2 === "--once") {
|
|
11141
|
-
flags.
|
|
11570
|
+
if (command2 === "patrol" && (flag2 === "--once" || flag2 === "--force")) {
|
|
11571
|
+
flags[flag2.slice(2)] = true;
|
|
11572
|
+
continue;
|
|
11573
|
+
}
|
|
11574
|
+
if (command2 === "ci" && flag2 === "--wait") {
|
|
11575
|
+
flags.wait = true;
|
|
11142
11576
|
continue;
|
|
11143
11577
|
}
|
|
11144
11578
|
if (command2 === "publish" && flag2 === "--dry-run") {
|
|
@@ -11178,6 +11612,8 @@ function rolesFor(command2, config) {
|
|
|
11178
11612
|
case "judge":
|
|
11179
11613
|
case "publish":
|
|
11180
11614
|
return ["judge"];
|
|
11615
|
+
case "ci":
|
|
11616
|
+
return ["fixer"];
|
|
11181
11617
|
case "retest":
|
|
11182
11618
|
return ["explorer", "judge"];
|
|
11183
11619
|
case "fix":
|
|
@@ -11192,15 +11628,36 @@ function preflight(command2, config, env = process.env) {
|
|
|
11192
11628
|
const problems = rolesFor(command2, config).map((role) => runtimeProblem(role, config.agents[role].use, env)).filter((problem) => Boolean(problem));
|
|
11193
11629
|
if (problems.length)
|
|
11194
11630
|
throw new ConfigError(problems.join("\n"));
|
|
11195
|
-
return ["explore", "patrol"].includes(command2) ? [resolveDecider(config.decisions, env).reason] : [];
|
|
11196
11631
|
}
|
|
11197
11632
|
async function runAgentCommand(command2, args2, root2, config, log) {
|
|
11198
11633
|
const flags = parseAgentFlags(command2, args2);
|
|
11199
|
-
|
|
11200
|
-
log(line);
|
|
11634
|
+
preflight(command2, config);
|
|
11201
11635
|
const workspace = new Workspace(root2);
|
|
11202
11636
|
if (command2 === "patrol") {
|
|
11203
|
-
await runPatrol({ root: root2, config, once: Boolean(flags.once), onLog: log });
|
|
11637
|
+
const { problems } = await runPatrol({ root: root2, config, once: Boolean(flags.once), force: Boolean(flags.force), onLog: log });
|
|
11638
|
+
if (problems.length) {
|
|
11639
|
+
log(`The patrol had ${problems.length} problem(s):
|
|
11640
|
+
${problems.map((problem) => `- ${problem.split("\n")[0]}`).join("\n")}`);
|
|
11641
|
+
process.exitCode = 1;
|
|
11642
|
+
}
|
|
11643
|
+
return;
|
|
11644
|
+
}
|
|
11645
|
+
if (command2 === "ci") {
|
|
11646
|
+
if (!config.agents.github.enabled) {
|
|
11647
|
+
throw new ConfigError("GitHub is off. Set agents.github.enabled: true in .bughunters/bughunters.yml.");
|
|
11648
|
+
}
|
|
11649
|
+
const { problems } = await watchCi(root2, config, {
|
|
11650
|
+
onLog: log,
|
|
11651
|
+
wait: Boolean(flags.wait),
|
|
11652
|
+
issueIds: flags.issue
|
|
11653
|
+
});
|
|
11654
|
+
const fixes = (await workspace.listFixes()).filter((fix) => fix.pr && fix.ci && (!flags.issue || flags.issue.includes(fix.issueId)));
|
|
11655
|
+
if (!fixes.length)
|
|
11656
|
+
log("No open Bughunters PR has CI checks yet.");
|
|
11657
|
+
for (const fix of fixes)
|
|
11658
|
+
log(` PR #${fix.pr.number} ${fix.ci.state.padEnd(8)} ${fix.issueId} ${fix.pr.url}`);
|
|
11659
|
+
if (problems.length)
|
|
11660
|
+
process.exitCode = 1;
|
|
11204
11661
|
return;
|
|
11205
11662
|
}
|
|
11206
11663
|
if (command2 === "publish") {
|
|
@@ -11392,7 +11849,7 @@ function bySeverity(a, b) {
|
|
|
11392
11849
|
}
|
|
11393
11850
|
|
|
11394
11851
|
// ../cli/dist/commands/memory.js
|
|
11395
|
-
var
|
|
11852
|
+
var roles3 = /* @__PURE__ */ new Set(["explorer", "judge", "fixer"]);
|
|
11396
11853
|
var flag = (args2, name) => {
|
|
11397
11854
|
const at = args2.indexOf(name);
|
|
11398
11855
|
return at < 0 ? void 0 : args2[at + 1];
|
|
@@ -11402,7 +11859,7 @@ async function runMemoryCommand(args2, root2, log) {
|
|
|
11402
11859
|
const workspace = new Workspace(root2);
|
|
11403
11860
|
if (action === "list") {
|
|
11404
11861
|
const role = flag(args2.slice(1), "--role");
|
|
11405
|
-
if (role && !
|
|
11862
|
+
if (role && !roles3.has(role))
|
|
11406
11863
|
throw new ConfigError(`Unknown memory role: ${role}`);
|
|
11407
11864
|
const lessons = (await workspace.readMemory()).lessons.filter((lesson) => !role || lesson.role === role);
|
|
11408
11865
|
log("id role hits source scope text");
|
|
@@ -11415,7 +11872,7 @@ async function runMemoryCommand(args2, root2, log) {
|
|
|
11415
11872
|
if (action === "add") {
|
|
11416
11873
|
const role = flag(args2.slice(1), "--role");
|
|
11417
11874
|
const text3 = args2.slice(1).find((part, index, all) => !part.startsWith("--") && all[index - 1] !== "--role" && all[index - 1] !== "--scope");
|
|
11418
|
-
if (!role || !
|
|
11875
|
+
if (!role || !roles3.has(role) || !text3?.trim())
|
|
11419
11876
|
throw new ConfigError('Usage: bughunters memory add --role explorer|judge|fixer "text" [--scope s]');
|
|
11420
11877
|
const [lesson] = await workspace.upsertLessons([{
|
|
11421
11878
|
role,
|
|
@@ -11449,7 +11906,7 @@ var [command, ...args] = process.argv.slice(2);
|
|
|
11449
11906
|
var root = findProjectRoot(process.cwd());
|
|
11450
11907
|
function version() {
|
|
11451
11908
|
if (true)
|
|
11452
|
-
return "0.1.
|
|
11909
|
+
return "0.1.2";
|
|
11453
11910
|
try {
|
|
11454
11911
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
11455
11912
|
} catch {
|
|
@@ -11513,6 +11970,7 @@ try {
|
|
|
11513
11970
|
case "fix":
|
|
11514
11971
|
case "retest":
|
|
11515
11972
|
case "publish":
|
|
11973
|
+
case "ci":
|
|
11516
11974
|
case "patrol":
|
|
11517
11975
|
case "replay": {
|
|
11518
11976
|
const config = loadConfig(root);
|