bughunters 0.1.1 → 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 +756 -381
- package/package.json +1 -1
- package/src/ui/app.js +7 -3
- 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,7 +379,15 @@ 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),
|
|
@@ -700,6 +728,7 @@ Agents
|
|
|
700
728
|
bughunters fix [--issue <id>...]
|
|
701
729
|
bughunters retest --issue <id>
|
|
702
730
|
bughunters publish [--issue <id>] [--dry-run]
|
|
731
|
+
bughunters ci [--issue <id>] [--wait]
|
|
703
732
|
bughunters github sync
|
|
704
733
|
bughunters worktrees clean
|
|
705
734
|
bughunters patrol [--once] [--force]
|
|
@@ -748,7 +777,6 @@ var COMMAND_HELP = {
|
|
|
748
777
|
--app-id <id> ios, android: the bundle ID or the package name
|
|
749
778
|
--agent <a> the LLM for all agents: claude | codex | kimi | pi | openrouter | vercel | openai | anthropic
|
|
750
779
|
--explorer, --judge, --fixer <a> the LLM for one agent
|
|
751
|
-
--jev <route> auto | typesafe | openrouter | vercel
|
|
752
780
|
--gate write a starter config for the deterministic web gate
|
|
753
781
|
`,
|
|
754
782
|
explore: `bughunters explore [--goal "..."] [--steps N]
|
|
@@ -776,13 +804,21 @@ var COMMAND_HELP = {
|
|
|
776
804
|
Needs agents.github.enabled: true and a logged-in gh CLI.
|
|
777
805
|
--dry-run write the reports to .bughunters/runs/publish/, and open nothing
|
|
778
806
|
--issue <id> publish this issue only
|
|
807
|
+
`,
|
|
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
|
|
779
814
|
`,
|
|
780
815
|
patrol: `bughunters patrol [--once] [--force]
|
|
781
816
|
Run the full cycle again and again: setup, explore, judge, teardown, fix, retest, publish.
|
|
782
817
|
The fixer and GitHub steps run only when you turn them on.
|
|
783
|
-
|
|
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.
|
|
784
820
|
--once run one cycle, then stop
|
|
785
|
-
--force
|
|
821
|
+
--force explore and judge in the first cycle also when the commit did not change
|
|
786
822
|
`,
|
|
787
823
|
github: `bughunters github sync
|
|
788
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.
|
|
@@ -1044,8 +1080,8 @@ function serialized(file, update) {
|
|
|
1044
1080
|
}
|
|
1045
1081
|
async function listJson(dir) {
|
|
1046
1082
|
try {
|
|
1047
|
-
const
|
|
1048
|
-
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))));
|
|
1049
1085
|
} catch (error) {
|
|
1050
1086
|
if (error.code === "ENOENT") {
|
|
1051
1087
|
return [];
|
|
@@ -1229,8 +1265,8 @@ var Workspace = class {
|
|
|
1229
1265
|
async saveScreenshot(sessionId, png, label) {
|
|
1230
1266
|
const dir = paths.session(this.root, sessionId);
|
|
1231
1267
|
await mkdir(dir, { recursive: true });
|
|
1232
|
-
const
|
|
1233
|
-
const number =
|
|
1268
|
+
const files2 = await readdir(dir);
|
|
1269
|
+
const number = files2.filter((file2) => /^\d{3}-.*\.png$/.test(file2)).length + 1;
|
|
1234
1270
|
const file = join3(dir, `${String(number).padStart(3, "0")}-${basename(label).replace(/[^a-zA-Z0-9_-]/g, "-")}.png`);
|
|
1235
1271
|
await writeFile(file, png);
|
|
1236
1272
|
return relative(this.root, file);
|
|
@@ -1463,64 +1499,7 @@ function toMcp(part) {
|
|
|
1463
1499
|
import { accessSync, constants } from "node:fs";
|
|
1464
1500
|
import { delimiter, join as join5 } from "node:path";
|
|
1465
1501
|
|
|
1466
|
-
// ../decide/dist/
|
|
1467
|
-
var JevDecider = class {
|
|
1468
|
-
options;
|
|
1469
|
-
name = "jev";
|
|
1470
|
-
endpoint;
|
|
1471
|
-
model;
|
|
1472
|
-
lastUsage;
|
|
1473
|
-
timeoutMs;
|
|
1474
|
-
constructor(options) {
|
|
1475
|
-
this.options = options;
|
|
1476
|
-
this.endpoint = options.endpoint ?? "https://api.typesafe.ai/v1/systemone";
|
|
1477
|
-
this.model = options.model ?? "jev-latest";
|
|
1478
|
-
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
1479
|
-
}
|
|
1480
|
-
async ask(state, questions) {
|
|
1481
|
-
const controller = new AbortController();
|
|
1482
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1483
|
-
try {
|
|
1484
|
-
const response3 = await fetch(this.endpoint, {
|
|
1485
|
-
method: "POST",
|
|
1486
|
-
headers: {
|
|
1487
|
-
"content-type": "application/json",
|
|
1488
|
-
authorization: `Bearer ${this.options.apiKey}`
|
|
1489
|
-
},
|
|
1490
|
-
// Every question is evaluated against the same state in one call, so
|
|
1491
|
-
// the entire per-screen analysis is a single round trip.
|
|
1492
|
-
body: JSON.stringify({
|
|
1493
|
-
model: this.model,
|
|
1494
|
-
state,
|
|
1495
|
-
questions: Object.fromEntries(Object.entries(questions).map(([id2, question]) => [
|
|
1496
|
-
id2,
|
|
1497
|
-
question.type === "score" ? { type: "score", instructions: question.instructions, criteria: question.legend } : question
|
|
1498
|
-
]))
|
|
1499
|
-
}),
|
|
1500
|
-
signal: controller.signal
|
|
1501
|
-
});
|
|
1502
|
-
if (!response3.ok) {
|
|
1503
|
-
throw new InfrastructureError(`Jev returned ${response3.status}: ${await safeText(response3)}`);
|
|
1504
|
-
}
|
|
1505
|
-
const body = await response3.json();
|
|
1506
|
-
this.lastUsage = usageFrom(body.usage) ?? { input: Math.ceil(state.length / 4), output: 0, estimated: true };
|
|
1507
|
-
return normalizeAnswers(body.answers ?? {}, questions);
|
|
1508
|
-
} catch (cause) {
|
|
1509
|
-
if (cause instanceof InfrastructureError)
|
|
1510
|
-
throw cause;
|
|
1511
|
-
throw new InfrastructureError("Jev request failed", { cause });
|
|
1512
|
-
} finally {
|
|
1513
|
-
clearTimeout(timer);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
};
|
|
1517
|
-
async function safeText(response3) {
|
|
1518
|
-
try {
|
|
1519
|
-
return (await response3.text()).slice(0, 400);
|
|
1520
|
-
} catch {
|
|
1521
|
-
return "<no body>";
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1502
|
+
// ../decide/dist/answers.js
|
|
1524
1503
|
function normalizeAnswers(raw, questions) {
|
|
1525
1504
|
const out = {};
|
|
1526
1505
|
for (const [key, question] of Object.entries(questions)) {
|
|
@@ -1947,19 +1926,11 @@ var Budget = class {
|
|
|
1947
1926
|
return { spentUsd: this.spent, capUsd: this.capUsd };
|
|
1948
1927
|
}
|
|
1949
1928
|
};
|
|
1950
|
-
|
|
1951
|
-
function estimateDecisionCost(stateChars, decider = "jev") {
|
|
1952
|
-
if (decider === "jev")
|
|
1953
|
-
return stateChars / 4 * JEV_USD_PER_INPUT_TOKEN;
|
|
1929
|
+
function estimateDecisionCost(stateChars) {
|
|
1954
1930
|
return ((stateChars / 4 + 1500) * 3 + 900 * 15) / 1e6;
|
|
1955
1931
|
}
|
|
1956
1932
|
|
|
1957
1933
|
// ../decide/dist/index.js
|
|
1958
|
-
var JEV_ROUTES = {
|
|
1959
|
-
typesafe: { key: "TYPESAFE_API_KEY", endpoint: "https://api.typesafe.ai/v1/systemone", model: "jev-latest" },
|
|
1960
|
-
openrouter: { key: "OPENROUTER_API_KEY", endpoint: "https://openrouter.ai/api/v1/systemone", model: "jev-latest" },
|
|
1961
|
-
vercel: { key: "AI_GATEWAY_API_KEY", endpoint: "https://ai-gateway.vercel.sh/typesafe/v1/systemone", model: "typesafe-ai/jev" }
|
|
1962
|
-
};
|
|
1963
1934
|
var MODEL_KEYS = {
|
|
1964
1935
|
openrouter: "OPENROUTER_API_KEY",
|
|
1965
1936
|
vercel: "AI_GATEWAY_API_KEY",
|
|
@@ -1967,24 +1938,8 @@ var MODEL_KEYS = {
|
|
|
1967
1938
|
anthropic: "ANTHROPIC_API_KEY",
|
|
1968
1939
|
custom: "BUGHUNTERS_MODEL_API_KEY"
|
|
1969
1940
|
};
|
|
1970
|
-
var jevOrder = ["typesafe", "openrouter", "vercel"];
|
|
1971
1941
|
var modelOrder = ["openrouter", "vercel", "openai", "anthropic", "custom"];
|
|
1972
1942
|
function resolveDecider(config, env) {
|
|
1973
|
-
let jevMissing = "";
|
|
1974
|
-
if (config.decider === "jev") {
|
|
1975
|
-
const routes2 = config.jev.via === "auto" ? jevOrder : [config.jev.via];
|
|
1976
|
-
for (const via of routes2) {
|
|
1977
|
-
const route2 = JEV_ROUTES[via];
|
|
1978
|
-
const key = env[route2.key];
|
|
1979
|
-
if (key)
|
|
1980
|
-
return {
|
|
1981
|
-
decider: new JevDecider({ apiKey: key, endpoint: route2.endpoint, model: route2.model }),
|
|
1982
|
-
via: `jev:${via}`,
|
|
1983
|
-
reason: `Decider: jev via ${via} (${route2.model}).`
|
|
1984
|
-
};
|
|
1985
|
-
}
|
|
1986
|
-
jevMissing = `decider is jev, but no ${routes2.map((via) => JEV_ROUTES[via].key).join(", ").replace(/, ([^,]*)$/, " or $1")} is set`;
|
|
1987
|
-
}
|
|
1988
1943
|
const routes = config.model.via === "auto" ? modelOrder : [config.model.via];
|
|
1989
1944
|
for (const via of routes) {
|
|
1990
1945
|
const key = env[MODEL_KEYS[via]];
|
|
@@ -1995,14 +1950,11 @@ function resolveDecider(config, env) {
|
|
|
1995
1950
|
return {
|
|
1996
1951
|
decider: new ModelDecider({ via, apiKey: key, endpoint, model: name }),
|
|
1997
1952
|
via: `model:${via}`,
|
|
1998
|
-
reason:
|
|
1953
|
+
reason: `Decider: model via ${via} (${name}).`
|
|
1999
1954
|
};
|
|
2000
1955
|
}
|
|
2001
|
-
const
|
|
2002
|
-
return {
|
|
2003
|
-
via: "none",
|
|
2004
|
-
reason: `Decider: none, so the judge decides each finding, because ${jevMissing ? `${jevMissing}, and ` : ""}no ${modelMissing} is set. Set a Jev key to cut cost.`
|
|
2005
|
-
};
|
|
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.` };
|
|
2006
1958
|
}
|
|
2007
1959
|
|
|
2008
1960
|
// ../agents/dist/runtime/cli.js
|
|
@@ -2194,7 +2146,7 @@ var ModelRuntime = class {
|
|
|
2194
2146
|
return outcome("done", calls.output);
|
|
2195
2147
|
}
|
|
2196
2148
|
}
|
|
2197
|
-
if (costUsd >= task.budgetUsd) {
|
|
2149
|
+
if (task.budgetUsd !== void 0 && costUsd >= task.budgetUsd) {
|
|
2198
2150
|
return outcome("budget", lastText);
|
|
2199
2151
|
}
|
|
2200
2152
|
}
|
|
@@ -2572,7 +2524,6 @@ var AgentSession = class {
|
|
|
2572
2524
|
lastScreenTrailIndex = 0;
|
|
2573
2525
|
/** Where the app was when lastScreenId was recorded. */
|
|
2574
2526
|
lastScreenLocation;
|
|
2575
|
-
decisionSpentUsd = 0;
|
|
2576
2527
|
/** Issues already counted as seen again in this session. */
|
|
2577
2528
|
seenIssues = /* @__PURE__ */ new Set();
|
|
2578
2529
|
cancelled = false;
|
|
@@ -2671,6 +2622,26 @@ ${lessons.map((lesson) => `- [${lesson.scope ?? "app"}] ${lesson.text}`).join("\
|
|
|
2671
2622
|
|
|
2672
2623
|
` : "";
|
|
2673
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
|
+
}
|
|
2674
2645
|
function explorerCommon(platform, instructions, lessons = []) {
|
|
2675
2646
|
return `HOW THE TOOLS WORK
|
|
2676
2647
|
- Each action tool returns a screenshot and a list of elements. Refs such as [e12] are valid only for
|
|
@@ -2686,24 +2657,38 @@ RULES
|
|
|
2686
2657
|
- Do not use the same failed action more than two times. Try another way, or report the problem.
|
|
2687
2658
|
- Keep your notes short. Spend your steps on actions, not on long thoughts.
|
|
2688
2659
|
|
|
2689
|
-
${lessonPart(lessons)}APP GUIDE
|
|
2660
|
+
${memoryPart("explorer")}${lessonPart(lessons)}APP GUIDE
|
|
2690
2661
|
${instructions.trim() || "(No guide was given. Explore carefully and do not change any data.)"}`;
|
|
2691
2662
|
}
|
|
2692
|
-
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
|
+
` : "";
|
|
2693
2667
|
return `You are the explorer on an automated QA team. You use the app through tools, like a careful
|
|
2694
2668
|
human tester, and you look for problems that a real user would notice.
|
|
2695
2669
|
|
|
2696
|
-
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
|
+
|
|
2697
2674
|
1. Enter the app. Follow the app guide below. When you are in the app, call save_routine with the id
|
|
2698
2675
|
"enter-app" at once.
|
|
2699
2676
|
2. Record each screen that you see for the first time, before you do anything on it. Call record_screen
|
|
2700
2677
|
with a short kebab-case id, a name, and one sentence about what the screen is for. A dialog, a sheet,
|
|
2701
|
-
or a
|
|
2678
|
+
a menu, a tab, or a modal with its own content is a screen too.
|
|
2702
2679
|
3. Explore breadth first. Visit each item of the main navigation before you go deep into one area.
|
|
2703
|
-
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
|
|
2704
2686
|
continue. Do not stop at the first problem.
|
|
2705
|
-
|
|
2706
|
-
|
|
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.
|
|
2707
2692
|
|
|
2708
2693
|
WHAT TO REPORT
|
|
2709
2694
|
- A control that does nothing, or does the wrong thing.
|
|
@@ -2711,15 +2696,16 @@ WHAT TO REPORT
|
|
|
2711
2696
|
- Layout that is broken: content off screen, elements on top of each other, empty gaps, wrong alignment.
|
|
2712
2697
|
- Wrong, missing, or contradictory data. Placeholder text such as "undefined", "NaN", "null", or "{{".
|
|
2713
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.
|
|
2714
2702
|
- Use the id from record_screen for the screen you are on when you call report_bug. Use 'unrecorded' if needed.
|
|
2715
2703
|
- A loading state that does not end. If a screen still shows a spinner or skeleton rows after two waits
|
|
2716
2704
|
of 10 seconds, report it as a bug and continue somewhere else. Do not wait again and again.
|
|
2717
2705
|
- A dead end: a screen with no way back.
|
|
2718
2706
|
- Inconsistent UI: the same thing named or styled in two different ways.
|
|
2719
2707
|
Do not report the things that the app guide tells you to ignore.
|
|
2720
|
-
|
|
2721
|
-
to the QA lead. Do not report those findings again with report_bug. Report what the checks cannot see.
|
|
2722
|
-
|
|
2708
|
+
${automatic}
|
|
2723
2709
|
${explorerCommon(platform, instructions, lessons)}`;
|
|
2724
2710
|
}
|
|
2725
2711
|
function explorerRetestSystem(platform, instructions, lessons = []) {
|
|
@@ -2743,7 +2729,9 @@ Call view_retest to inspect the before and after screenshots. Then call verdict.
|
|
|
2743
2729
|
fixed: the problem is gone on EVERY affected screen and nothing new is broken.
|
|
2744
2730
|
not-fixed: the problem is still visible on at least one screen. Name it.
|
|
2745
2731
|
unclear: the explorer did not reach at least one screen and none shows the problem still present. Name the missing screens.
|
|
2746
|
-
|
|
2732
|
+
Give a cause only when the screenshots or the logs show it. Do not guess one.
|
|
2733
|
+
|
|
2734
|
+
${memoryPart("judge")}${lessonPart(lessons)}`;
|
|
2747
2735
|
}
|
|
2748
2736
|
function explorerPrompt(input) {
|
|
2749
2737
|
const screens = input.screens.length ? input.screens.map((screen) => `- ${screen.id}: ${screen.name}. ${screen.description}`).join("\n") : "(none yet)";
|
|
@@ -2751,7 +2739,7 @@ function explorerPrompt(input) {
|
|
|
2751
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";
|
|
2752
2740
|
return `- ${routine.id} (${routine.steps.length} steps, ${health}): ${routine.description}`;
|
|
2753
2741
|
}).join("\n") : "(none yet)";
|
|
2754
|
-
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.");
|
|
2755
2743
|
return `GOAL
|
|
2756
2744
|
${goal}
|
|
2757
2745
|
|
|
@@ -2805,11 +2793,12 @@ HOW TO WRITE AN ISSUE
|
|
|
2805
2793
|
|
|
2806
2794
|
Be strict. A report that nobody can act on costs the team time. When you have decided every candidate,
|
|
2807
2795
|
call finish with one sentence per decision.
|
|
2808
|
-
|
|
2796
|
+
|
|
2797
|
+
${memoryPart("judge")}${lessonPart(lessons)}`;
|
|
2809
2798
|
}
|
|
2810
2799
|
function judgePrompt(sessionIds, candidates, issues) {
|
|
2811
2800
|
const list3 = candidates.map((candidate) => {
|
|
2812
|
-
const route2 = candidate.route ? `
|
|
2801
|
+
const route2 = candidate.route ? ` Route: ${candidate.route.reason}` : "";
|
|
2813
2802
|
return `- ${candidate.id} [${candidate.source}, ${candidate.severity}] on ${candidate.screenId ?? "unknown screen"}: ${candidate.summary}.${route2}`;
|
|
2814
2803
|
}).join("\n");
|
|
2815
2804
|
const open2 = issues.filter((issue) => issue.status !== "dismissed" && issue.status !== "fixed");
|
|
@@ -2862,7 +2851,111 @@ a QA lead confirmed it. Fix the cause, not the symptom.
|
|
|
2862
2851
|
your summary, with the file and line. Bughunters shows your explanation to the team.
|
|
2863
2852
|
- Do not commit and do not push. Bughunters commits the change on its own branch.
|
|
2864
2853
|
- End with a short summary: the cause, the change, and what you ran to verify it.
|
|
2865
|
-
|
|
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
|
+
}];
|
|
2866
2959
|
}
|
|
2867
2960
|
|
|
2868
2961
|
// ../agents/dist/roles/fixer.js
|
|
@@ -3204,10 +3297,10 @@ ${issue.body}
|
|
|
3204
3297
|
Evidence:
|
|
3205
3298
|
${evidence}
|
|
3206
3299
|
Reproduction: ${repro}` + (last ? `
|
|
3207
|
-
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:
|
|
3208
3301
|
${after}
|
|
3209
|
-
|
|
3210
|
-
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()],
|
|
3211
3304
|
maxSteps: config.maxSteps,
|
|
3212
3305
|
budgetUsd: config.budgetUsd,
|
|
3213
3306
|
timeoutMs: config.timeoutMs
|
|
@@ -3234,12 +3327,12 @@ function stepWords(step) {
|
|
|
3234
3327
|
}
|
|
3235
3328
|
|
|
3236
3329
|
// ../agents/dist/github.js
|
|
3237
|
-
var defaultGh = (args2, opts) => new Promise((
|
|
3330
|
+
var defaultGh = (args2, opts) => new Promise((resolve12, reject) => {
|
|
3238
3331
|
const child = execFile2("gh", args2, { cwd: opts?.cwd, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
3239
3332
|
if (error)
|
|
3240
3333
|
reject(Object.assign(error, { stderr, stdout }));
|
|
3241
3334
|
else
|
|
3242
|
-
|
|
3335
|
+
resolve12(stdout.trim());
|
|
3243
3336
|
});
|
|
3244
3337
|
child.stdin?.end(opts?.input);
|
|
3245
3338
|
});
|
|
@@ -4479,14 +4572,15 @@ async function evaluateScreen(session, input) {
|
|
|
4479
4572
|
if (firstSight)
|
|
4480
4573
|
await saveBaseline(session, baseline, screenshot, snapshot);
|
|
4481
4574
|
const native = observation.platform === "ios" || observation.platform === "android";
|
|
4575
|
+
const enabled = new Set(session.config.agents.checks);
|
|
4482
4576
|
const violations = evaluateAll(snapshot, {
|
|
4483
4577
|
baseline: baseline.snapshot,
|
|
4484
|
-
disabled: native
|
|
4578
|
+
disabled: RULES.map((rule) => rule.id).filter((id2) => !enabled.has(id2) || native && NATIVE_DISABLED.includes(id2))
|
|
4485
4579
|
});
|
|
4486
4580
|
const entries = groupViolations(violations);
|
|
4487
4581
|
let diffPath;
|
|
4488
4582
|
let visual;
|
|
4489
|
-
if (!firstSight) {
|
|
4583
|
+
if (!firstSight && enabled.has("pixel-diff")) {
|
|
4490
4584
|
diffPath = join8(paths.session(session.root, session.sessionId), `${screenId}-diff.png`);
|
|
4491
4585
|
const comparison = await diff({
|
|
4492
4586
|
baselinePath: baseline.png,
|
|
@@ -4506,7 +4600,7 @@ async function evaluateScreen(session, input) {
|
|
|
4506
4600
|
await closeAbsentChecks(session, screenId, new Set(entries.map((entry) => entryFingerprint(screenId, entry))));
|
|
4507
4601
|
if (fresh.length === 0)
|
|
4508
4602
|
return [];
|
|
4509
|
-
const routeResult =
|
|
4603
|
+
const routeResult = { to: "judge", reason: "Automatic check" };
|
|
4510
4604
|
const candidates = [];
|
|
4511
4605
|
for (const entry of fresh) {
|
|
4512
4606
|
const candidate = toCandidate(session, screenId, entry, {
|
|
@@ -4615,6 +4709,7 @@ async function addOccurrence(session, issueId) {
|
|
|
4615
4709
|
}
|
|
4616
4710
|
async function closeAbsentChecks(session, screenId, fired) {
|
|
4617
4711
|
const closed = [];
|
|
4712
|
+
const enabled = new Set(session.config.agents.checks);
|
|
4618
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));
|
|
4619
4714
|
const sources = /* @__PURE__ */ new Map();
|
|
4620
4715
|
for (const record of await session.workspace.listSessions(Infinity)) {
|
|
@@ -4628,6 +4723,8 @@ async function closeAbsentChecks(session, screenId, fired) {
|
|
|
4628
4723
|
const candidates = issue.candidateIds.map((candidateId) => sources.get(candidateId));
|
|
4629
4724
|
if (candidates.some((candidate) => !candidate || candidate.source === "explorer" || candidate.screenId !== screenId))
|
|
4630
4725
|
continue;
|
|
4726
|
+
if (candidates.some((candidate) => !enabled.has(candidate.ruleId ?? candidate.source)))
|
|
4727
|
+
continue;
|
|
4631
4728
|
if (candidates.some((candidate) => fired.has(candidate.fingerprint))) {
|
|
4632
4729
|
if (issue.notSeen)
|
|
4633
4730
|
await session.workspace.saveIssue({ ...issue, notSeen: 0 });
|
|
@@ -4677,57 +4774,6 @@ function toCandidate(session, screenId, entry, extra) {
|
|
|
4677
4774
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4678
4775
|
};
|
|
4679
4776
|
}
|
|
4680
|
-
async function decide(session, screenId, snapshot, violations, visual) {
|
|
4681
|
-
const config = session.config.decisions;
|
|
4682
|
-
const { decider } = resolveDecider(config, process.env);
|
|
4683
|
-
if (!decider)
|
|
4684
|
-
return { to: "judge", reason: "no decider key is set, so the judge decides" };
|
|
4685
|
-
const { text: text3 } = buildState({
|
|
4686
|
-
screen: { id: screenId, description: snapshot.title ?? snapshot.url },
|
|
4687
|
-
product: { summary: "", audience: "", domainVocabulary: [] },
|
|
4688
|
-
assertions: violationsToAssertions(violations),
|
|
4689
|
-
diff: visual ? {
|
|
4690
|
-
changedPixels: visual.changedPixels,
|
|
4691
|
-
changedPercent: visual.changedFraction * 100,
|
|
4692
|
-
maskedPercent: visual.maskedFraction * 100,
|
|
4693
|
-
regions: visual.regions.length
|
|
4694
|
-
} : void 0,
|
|
4695
|
-
console: snapshot.consoleErrors
|
|
4696
|
-
});
|
|
4697
|
-
const budget = new Budget(config.budget.perRunUsd);
|
|
4698
|
-
budget.record(session.decisionSpentUsd);
|
|
4699
|
-
const estimate = estimateDecisionCost(text3.length, decider.name);
|
|
4700
|
-
if (!budget.canSpend(estimate)) {
|
|
4701
|
-
return { to: "judge", reason: "decider budget exhausted" };
|
|
4702
|
-
}
|
|
4703
|
-
try {
|
|
4704
|
-
const answers = await decider.ask(text3, SCREEN_QUESTIONS);
|
|
4705
|
-
session.decisionSpentUsd += estimate;
|
|
4706
|
-
const result3 = route({
|
|
4707
|
-
answers,
|
|
4708
|
-
thresholds: config.confidence,
|
|
4709
|
-
hasDeterministicRegression: false,
|
|
4710
|
-
matchedLedger: false
|
|
4711
|
-
});
|
|
4712
|
-
session.emit({
|
|
4713
|
-
kind: "tool-result",
|
|
4714
|
-
tool: "decider",
|
|
4715
|
-
summary: `Decider routed ${screenId} to ${result3.route}`,
|
|
4716
|
-
costUsd: estimate,
|
|
4717
|
-
tokens: decider.lastUsage,
|
|
4718
|
-
model: decider.model ?? decider.name
|
|
4719
|
-
});
|
|
4720
|
-
const choice = answers["route"];
|
|
4721
|
-
const confidence = choice?.confidence;
|
|
4722
|
-
return {
|
|
4723
|
-
to: result3.route === "ignore" || result3.route === "intent" ? "ignore" : "judge",
|
|
4724
|
-
reason: result3.reason,
|
|
4725
|
-
confidence
|
|
4726
|
-
};
|
|
4727
|
-
} catch {
|
|
4728
|
-
return { to: "judge", reason: "decider unavailable" };
|
|
4729
|
-
}
|
|
4730
|
-
}
|
|
4731
4777
|
|
|
4732
4778
|
// ../agents/dist/replay.js
|
|
4733
4779
|
async function replaySteps(session, steps, opts = {}) {
|
|
@@ -4858,7 +4904,7 @@ async function actWhenReady(driver, action, session, windowMs) {
|
|
|
4858
4904
|
if (result3.ok || !retry || Date.now() >= deadline || session.cancelled) {
|
|
4859
4905
|
return { ...result3, degraded };
|
|
4860
4906
|
}
|
|
4861
|
-
await new Promise((
|
|
4907
|
+
await new Promise((resolve12) => setTimeout(resolve12, 500));
|
|
4862
4908
|
}
|
|
4863
4909
|
}
|
|
4864
4910
|
function toAction(step, session) {
|
|
@@ -4904,14 +4950,20 @@ function elementLine(element) {
|
|
|
4904
4950
|
].filter(Boolean).join(" ");
|
|
4905
4951
|
return `[${element.ref}] ${element.role} ${JSON.stringify(element.name)} (${box.x},${box.y} ${box.width}x${box.height})${flags ? ` ${flags}` : ""}`;
|
|
4906
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
|
+
}
|
|
4907
4958
|
function observationText(observation) {
|
|
4908
4959
|
const windows = observation.windows?.map((window) => `${window.active ? "*" : " "} ${window.title} (${window.location})`).join("; ") ?? "(none)";
|
|
4909
4960
|
return [
|
|
4910
4961
|
`Location: ${observation.location}`,
|
|
4911
4962
|
`Windows: ${windows}`,
|
|
4963
|
+
...errorLines(observation),
|
|
4912
4964
|
"Elements:",
|
|
4913
4965
|
...observation.elements.slice(0, 117).map(elementLine)
|
|
4914
|
-
].slice(0,
|
|
4966
|
+
].slice(0, 130).join("\n");
|
|
4915
4967
|
}
|
|
4916
4968
|
async function observe(session, label, summary) {
|
|
4917
4969
|
const observation = await session.driver.observe();
|
|
@@ -5239,7 +5291,7 @@ function explorerTools(session) {
|
|
|
5239
5291
|
},
|
|
5240
5292
|
{
|
|
5241
5293
|
name: "record_screen",
|
|
5242
|
-
description: "Record this screen
|
|
5294
|
+
description: "Record this screen and its routine in the app map.",
|
|
5243
5295
|
inputSchema: schema({ id: string, name: string, description: string }, ["id", "name", "description"]),
|
|
5244
5296
|
async run(input) {
|
|
5245
5297
|
const id2 = slug(arg(input, "id"));
|
|
@@ -5249,8 +5301,7 @@ function explorerTools(session) {
|
|
|
5249
5301
|
await session.driver.settle();
|
|
5250
5302
|
const observation = await session.driver.observe();
|
|
5251
5303
|
const screenshot = await session.capture(observation, id2);
|
|
5252
|
-
const
|
|
5253
|
-
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) }) : [];
|
|
5254
5305
|
const previous = session.lastScreenId;
|
|
5255
5306
|
const edge = transition(session, id2);
|
|
5256
5307
|
const routineId = `screen-${id2}`;
|
|
@@ -5278,15 +5329,17 @@ function explorerTools(session) {
|
|
|
5278
5329
|
session.lastScreenTrailIndex = session.trail.length;
|
|
5279
5330
|
const screens = (await session.workspace.readAppMap())?.screens ?? [];
|
|
5280
5331
|
const names = screens.map((screen) => screen.id).join(", ") || id2;
|
|
5281
|
-
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);
|
|
5282
5334
|
session.emit({
|
|
5283
5335
|
kind: "screen",
|
|
5284
|
-
summary: `Recorded ${name} (${
|
|
5336
|
+
summary: `Recorded ${name}${findings.length ? ` (${findings.length} automatic finding(s))` : ""}`,
|
|
5285
5337
|
screenId: id2,
|
|
5286
5338
|
screenshot
|
|
5287
5339
|
});
|
|
5288
5340
|
return {
|
|
5289
|
-
...text(`Recorded ${name}
|
|
5341
|
+
...text(session.vars.redact(`Recorded ${name}.${found} Known screens: ${names}${errors.length ? `
|
|
5342
|
+
${errors.join("\n")}` : ""}`)),
|
|
5290
5343
|
meta: { summary: "", screenshot, screenId: id2 }
|
|
5291
5344
|
};
|
|
5292
5345
|
}
|
|
@@ -5402,6 +5455,7 @@ Expected: ${arg(input, "expected")}`),
|
|
|
5402
5455
|
run: (input) => act(session, { kind: "window", match: arg(input, "match") })
|
|
5403
5456
|
});
|
|
5404
5457
|
}
|
|
5458
|
+
tools.push(...lessonTools(session, "explorer"));
|
|
5405
5459
|
return tools;
|
|
5406
5460
|
}
|
|
5407
5461
|
|
|
@@ -5419,9 +5473,9 @@ var string2 = { type: "string" };
|
|
|
5419
5473
|
function issueTitle(value) {
|
|
5420
5474
|
if (value.length <= 80)
|
|
5421
5475
|
return value;
|
|
5422
|
-
const
|
|
5476
|
+
const words2 = value.trim().split(/\s+/);
|
|
5423
5477
|
let title = "";
|
|
5424
|
-
for (const word of
|
|
5478
|
+
for (const word of words2) {
|
|
5425
5479
|
const next = title ? `${title} ${word}` : word;
|
|
5426
5480
|
if (next.length > 79)
|
|
5427
5481
|
break;
|
|
@@ -5474,6 +5528,7 @@ function triageEntries(candidates, decision, reason, issueId) {
|
|
|
5474
5528
|
}
|
|
5475
5529
|
function judgeTools(session, sessionIds, runtimeLabel) {
|
|
5476
5530
|
return [
|
|
5531
|
+
...lessonTools(session, "judge"),
|
|
5477
5532
|
{
|
|
5478
5533
|
name: "list_candidates",
|
|
5479
5534
|
description: "List undecided candidates and existing issues.",
|
|
@@ -5830,7 +5885,7 @@ async function runExplorer(session, runtime, opts = {}) {
|
|
|
5830
5885
|
const task = {
|
|
5831
5886
|
role: "explorer",
|
|
5832
5887
|
sessionId: session.sessionId,
|
|
5833
|
-
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),
|
|
5834
5889
|
prompt: explorerPrompt({
|
|
5835
5890
|
goal: opts.goal,
|
|
5836
5891
|
screens: map?.screens ?? [],
|
|
@@ -5853,13 +5908,13 @@ async function runExplorer(session, runtime, opts = {}) {
|
|
|
5853
5908
|
await session.workspace.endSession(session.sessionId, {
|
|
5854
5909
|
status: outcome.stop === "error" ? "failed" : "finished",
|
|
5855
5910
|
steps: outcome.steps,
|
|
5856
|
-
costUsd: outcome.costUsd
|
|
5911
|
+
costUsd: outcome.costUsd,
|
|
5857
5912
|
summary: outcome.summary,
|
|
5858
5913
|
candidates: candidates.length,
|
|
5859
5914
|
screensFound: screens.map((item) => item.id)
|
|
5860
5915
|
});
|
|
5861
5916
|
session.emit({ kind: "session-end", summary: outcome.summary ?? `Explorer stopped: ${outcome.stop}` });
|
|
5862
|
-
await session.idle(outcome.costUsd
|
|
5917
|
+
await session.idle(outcome.costUsd);
|
|
5863
5918
|
const learned = await reflectOnSession(session.root, session.config, session.sessionId, { vars: session.vars, onLog: (message2) => session.emit({ kind: "error", summary: message2 }) });
|
|
5864
5919
|
if (learned)
|
|
5865
5920
|
session.emit({ kind: "lesson", summary: `Saved ${learned} lesson(s) for the next sessions` });
|
|
@@ -6033,7 +6088,7 @@ async function deleteBranch(fix, onLog) {
|
|
|
6033
6088
|
|
|
6034
6089
|
// ../agents/dist/roles/retest.js
|
|
6035
6090
|
import { execFile as execFile5 } from "node:child_process";
|
|
6036
|
-
import { readFile as
|
|
6091
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
6037
6092
|
import { resolve as resolve7 } from "node:path";
|
|
6038
6093
|
import { promisify as promisify5 } from "node:util";
|
|
6039
6094
|
|
|
@@ -6093,7 +6148,7 @@ async function observeDom(page) {
|
|
|
6093
6148
|
};
|
|
6094
6149
|
const trim = (value) => value?.trim().replace(/\s+/g, " ").slice(0, 80) ?? "";
|
|
6095
6150
|
const implicitRole = (tag, type) => {
|
|
6096
|
-
const
|
|
6151
|
+
const roles4 = {
|
|
6097
6152
|
a: "link",
|
|
6098
6153
|
button: "button",
|
|
6099
6154
|
select: "combobox",
|
|
@@ -6116,7 +6171,7 @@ async function observeDom(page) {
|
|
|
6116
6171
|
if (/^h[1-6]$/.test(tag)) {
|
|
6117
6172
|
return "heading";
|
|
6118
6173
|
}
|
|
6119
|
-
return
|
|
6174
|
+
return roles4[tag] ?? "button";
|
|
6120
6175
|
};
|
|
6121
6176
|
for (const element of Array.from(document.querySelectorAll(candidates))) {
|
|
6122
6177
|
if (result3.length >= 150) {
|
|
@@ -6269,6 +6324,7 @@ var WebDriver = class {
|
|
|
6269
6324
|
page;
|
|
6270
6325
|
lastObservation;
|
|
6271
6326
|
errors = /* @__PURE__ */ new Map();
|
|
6327
|
+
failures = /* @__PURE__ */ new Map();
|
|
6272
6328
|
watched = /* @__PURE__ */ new WeakSet();
|
|
6273
6329
|
constructor(options) {
|
|
6274
6330
|
this.options = options;
|
|
@@ -6298,11 +6354,30 @@ var WebDriver = class {
|
|
|
6298
6354
|
const errors = [];
|
|
6299
6355
|
this.errors.set(page, errors);
|
|
6300
6356
|
page.on("console", (message2) => {
|
|
6301
|
-
if (message2.type() === "error") {
|
|
6357
|
+
if (message2.type() === "error" && !message2.text().startsWith("Failed to load resource:")) {
|
|
6302
6358
|
errors.push(message2.text());
|
|
6303
6359
|
}
|
|
6304
6360
|
});
|
|
6305
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
|
+
});
|
|
6306
6381
|
}
|
|
6307
6382
|
async observe() {
|
|
6308
6383
|
const page = this.activePage();
|
|
@@ -6334,6 +6409,7 @@ var WebDriver = class {
|
|
|
6334
6409
|
windows,
|
|
6335
6410
|
volatileRegions: [],
|
|
6336
6411
|
consoleErrors: errors.splice(0),
|
|
6412
|
+
networkErrors: (this.failures.get(page) ?? []).splice(0),
|
|
6337
6413
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6338
6414
|
};
|
|
6339
6415
|
this.lastObservation = observation;
|
|
@@ -6959,7 +7035,7 @@ var MaestroDriver = class {
|
|
|
6959
7035
|
await this.run([`openLink: ${yamlString(action.url)}`]);
|
|
6960
7036
|
}
|
|
6961
7037
|
async wait(action) {
|
|
6962
|
-
await new Promise((
|
|
7038
|
+
await new Promise((resolve12) => setTimeout(resolve12, action.ms));
|
|
6963
7039
|
}
|
|
6964
7040
|
async switchWindow() {
|
|
6965
7041
|
throw new Error("Window switching is unsupported on mobile");
|
|
@@ -7020,7 +7096,7 @@ var MaestroDriver = class {
|
|
|
7020
7096
|
return { frames, stable: true };
|
|
7021
7097
|
}
|
|
7022
7098
|
previous = current;
|
|
7023
|
-
await new Promise((
|
|
7099
|
+
await new Promise((resolve12) => setTimeout(resolve12, interval));
|
|
7024
7100
|
}
|
|
7025
7101
|
return { frames, stable: false };
|
|
7026
7102
|
}
|
|
@@ -7062,6 +7138,60 @@ function createDriver(config, vars) {
|
|
|
7062
7138
|
});
|
|
7063
7139
|
}
|
|
7064
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
|
+
|
|
7065
7195
|
// ../agents/dist/roles/retest.js
|
|
7066
7196
|
var exec4 = promisify5(execFile5);
|
|
7067
7197
|
var schema4 = (properties = {}, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
@@ -7071,7 +7201,7 @@ async function image2(root2, file) {
|
|
|
7071
7201
|
if (!file)
|
|
7072
7202
|
return [];
|
|
7073
7203
|
try {
|
|
7074
|
-
return [{ type: "image", png: await
|
|
7204
|
+
return [{ type: "image", png: await readFile10(resolve7(root2, file)) }];
|
|
7075
7205
|
} catch {
|
|
7076
7206
|
return [];
|
|
7077
7207
|
}
|
|
@@ -7137,9 +7267,13 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
7137
7267
|
const vars = new Vars(config.app.secrets);
|
|
7138
7268
|
let app;
|
|
7139
7269
|
let driver;
|
|
7270
|
+
let restore = async () => {
|
|
7271
|
+
};
|
|
7140
7272
|
try {
|
|
7141
7273
|
if (!options.build)
|
|
7142
7274
|
await linkEnvFiles(fix.repo, fix.worktree);
|
|
7275
|
+
if (!options.build)
|
|
7276
|
+
restore = await overlayBughunters(root2, fix.repo, fix.worktree);
|
|
7143
7277
|
if (!options.build && config.agents.fixer.retest.prepare) {
|
|
7144
7278
|
await exec4("/bin/sh", ["-c", config.agents.fixer.retest.prepare], { cwd: fix.worktree, timeout: 6e5, maxBuffer: 4 * 1024 * 1024 });
|
|
7145
7279
|
}
|
|
@@ -7176,7 +7310,8 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
7176
7310
|
"open",
|
|
7177
7311
|
"wait",
|
|
7178
7312
|
"run_routine",
|
|
7179
|
-
"switch_window"
|
|
7313
|
+
"switch_window",
|
|
7314
|
+
"save_lesson"
|
|
7180
7315
|
]);
|
|
7181
7316
|
const tools = explorerTools(session2).filter((tool) => allowed.has(tool.name)).map((tool) => tool.name !== "run_routine" ? tool : { ...tool, async run(input) {
|
|
7182
7317
|
const id2 = String(input.id ?? "");
|
|
@@ -7266,7 +7401,7 @@ async function retestFix(root2, config, issue, fix, attempt, deps, options = {})
|
|
|
7266
7401
|
}
|
|
7267
7402
|
});
|
|
7268
7403
|
const guide = config.app.instructions;
|
|
7269
|
-
const instructions = guide ? await
|
|
7404
|
+
const instructions = guide ? await readFile10(resolve7(root2, guide), "utf8") : "";
|
|
7270
7405
|
const result3 = await explorerRuntime.run({
|
|
7271
7406
|
role: "explorer",
|
|
7272
7407
|
sessionId: record2.id,
|
|
@@ -7320,12 +7455,17 @@ ${targets.map((target, index) => `${index + 1}. screen ${target.shot.screenId ??
|
|
|
7320
7455
|
} catch (error) {
|
|
7321
7456
|
retest.outcome = "error";
|
|
7322
7457
|
retest.reason = `Retest preparation or exploration failed: ${String(error)}`;
|
|
7458
|
+
deps.onLog?.(retest.reason);
|
|
7323
7459
|
return retest;
|
|
7324
7460
|
} finally {
|
|
7325
7461
|
try {
|
|
7326
7462
|
await driver?.close();
|
|
7327
7463
|
} finally {
|
|
7328
|
-
|
|
7464
|
+
try {
|
|
7465
|
+
await app?.stop();
|
|
7466
|
+
} finally {
|
|
7467
|
+
await restore();
|
|
7468
|
+
}
|
|
7329
7469
|
}
|
|
7330
7470
|
}
|
|
7331
7471
|
if (!targets.some((target) => target.shot.after)) {
|
|
@@ -7369,7 +7509,7 @@ Fixer: ${fix.summary ?? ""}` }
|
|
|
7369
7509
|
judged = { outcome: input.outcome, reason: String(input.reason ?? "") };
|
|
7370
7510
|
return { ...response2(`Retest verdict: ${judged.outcome} \u2014 ${judged.reason}`), done: true };
|
|
7371
7511
|
}
|
|
7372
|
-
}];
|
|
7512
|
+
}, ...lessonTools(session, "judge")];
|
|
7373
7513
|
const result3 = await judgeRuntime.run({
|
|
7374
7514
|
role: "judge",
|
|
7375
7515
|
sessionId: record.id,
|
|
@@ -7538,8 +7678,8 @@ async function runFixCycle(root2, config, deps = {}) {
|
|
|
7538
7678
|
}
|
|
7539
7679
|
|
|
7540
7680
|
// ../agents/dist/roles/publish.js
|
|
7541
|
-
import { mkdir as
|
|
7542
|
-
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";
|
|
7543
7683
|
|
|
7544
7684
|
// ../agents/dist/report.js
|
|
7545
7685
|
function buildReport(input) {
|
|
@@ -7657,7 +7797,7 @@ async function image3(root2, path) {
|
|
|
7657
7797
|
if (!path)
|
|
7658
7798
|
return [];
|
|
7659
7799
|
try {
|
|
7660
|
-
return [{ type: "image", png: await
|
|
7800
|
+
return [{ type: "image", png: await readFile11(resolve8(root2, path)) }];
|
|
7661
7801
|
} catch {
|
|
7662
7802
|
return [];
|
|
7663
7803
|
}
|
|
@@ -7849,9 +7989,9 @@ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
|
|
|
7849
7989
|
let url;
|
|
7850
7990
|
if (deps.dryRun) {
|
|
7851
7991
|
const dir = paths.publish(root2);
|
|
7852
|
-
await
|
|
7853
|
-
url =
|
|
7854
|
-
await
|
|
7992
|
+
await mkdir5(dir, { recursive: true });
|
|
7993
|
+
url = join12(dir, `${item.issue.id}.md`);
|
|
7994
|
+
await writeFile7(url, body);
|
|
7855
7995
|
} else if (item.kind === "pr" && item.fix) {
|
|
7856
7996
|
const opened = await createPr(gh, {
|
|
7857
7997
|
repo: resolved.repo,
|
|
@@ -7958,12 +8098,211 @@ ${items.map((item) => `${item.issue.id}: ${item.issue.title}`).join("\n")}`),
|
|
|
7958
8098
|
return outcomes;
|
|
7959
8099
|
}
|
|
7960
8100
|
|
|
7961
|
-
// ../agents/dist/
|
|
8101
|
+
// ../agents/dist/roles/ci.js
|
|
7962
8102
|
import { execFile as execFile6 } from "node:child_process";
|
|
8103
|
+
import { access } from "node:fs/promises";
|
|
7963
8104
|
import { resolve as resolve9 } from "node:path";
|
|
7964
8105
|
import { promisify as promisify6 } from "node:util";
|
|
7965
|
-
var
|
|
7966
|
-
async function
|
|
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
|
+
|
|
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) {
|
|
7967
8306
|
return (await exec5("git", args2, { cwd })).stdout.trim();
|
|
7968
8307
|
}
|
|
7969
8308
|
async function pullSource(root2, config, onLog) {
|
|
@@ -7973,15 +8312,15 @@ async function pullSource(root2, config, onLog) {
|
|
|
7973
8312
|
const slash = target.indexOf("/");
|
|
7974
8313
|
const remote = target.slice(0, slash);
|
|
7975
8314
|
const branch = target.slice(slash + 1);
|
|
7976
|
-
const source =
|
|
8315
|
+
const source = resolve10(root2, config.app.source);
|
|
7977
8316
|
try {
|
|
7978
|
-
if (await
|
|
8317
|
+
if (await git5(source, "status", "--porcelain", "--untracked-files=no")) {
|
|
7979
8318
|
onLog?.(`Did not pull ${target}: the source repository has uncommitted changes.`);
|
|
7980
8319
|
return false;
|
|
7981
8320
|
}
|
|
7982
|
-
await
|
|
7983
|
-
await
|
|
7984
|
-
onLog?.(`Pulled ${target} at ${await
|
|
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")}.`);
|
|
7985
8324
|
return true;
|
|
7986
8325
|
} catch (error) {
|
|
7987
8326
|
const reason = error.stderr?.trim() || error.message;
|
|
@@ -7990,11 +8329,11 @@ async function pullSource(root2, config, onLog) {
|
|
|
7990
8329
|
}
|
|
7991
8330
|
}
|
|
7992
8331
|
async function sourceCommit(root2, config) {
|
|
7993
|
-
const source =
|
|
8332
|
+
const source = resolve10(root2, config.app.source);
|
|
7994
8333
|
try {
|
|
7995
|
-
if (await
|
|
8334
|
+
if (await git5(source, "status", "--porcelain", "--untracked-files=no"))
|
|
7996
8335
|
return void 0;
|
|
7997
|
-
return await
|
|
8336
|
+
return await git5(source, "rev-parse", "HEAD");
|
|
7998
8337
|
} catch {
|
|
7999
8338
|
return void 0;
|
|
8000
8339
|
}
|
|
@@ -8021,12 +8360,13 @@ function wait(minutes) {
|
|
|
8021
8360
|
});
|
|
8022
8361
|
}
|
|
8023
8362
|
async function runPatrol(options) {
|
|
8363
|
+
const problems = [];
|
|
8024
8364
|
const { root: root2, config } = options;
|
|
8025
8365
|
const workspace = new Workspace(root2);
|
|
8026
8366
|
const previous = (await workspace.readAgents()).patrol;
|
|
8027
8367
|
if (previous?.state === "running" && previous.pid && previous.pid !== process.pid && alive(previous.pid)) {
|
|
8028
8368
|
options.onLog?.(`A patrol already runs (pid ${previous.pid}): did not start another.`);
|
|
8029
|
-
return;
|
|
8369
|
+
return { problems };
|
|
8030
8370
|
}
|
|
8031
8371
|
const runtime = options.createRuntime ?? createRuntime;
|
|
8032
8372
|
let interrupted = false;
|
|
@@ -8060,49 +8400,54 @@ async function runPatrol(options) {
|
|
|
8060
8400
|
await syncGitHub(root2, config, { onLog: options.onLog });
|
|
8061
8401
|
await cleanWorktrees(root2, config, { onLog: options.onLog });
|
|
8062
8402
|
const commit = await sourceCommit(root2, config);
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
await workspace.setAgentStatus(role, { state: "off" });
|
|
8073
|
-
}
|
|
8074
|
-
}
|
|
8075
|
-
const vars = new Vars(config.app.secrets);
|
|
8076
|
-
let app;
|
|
8077
|
-
let driver;
|
|
8078
|
-
try {
|
|
8079
|
-
app = await startApp(config.app, { root: root2, vars, emit: options.onLog });
|
|
8080
|
-
driver = (options.createDriver ?? createDriver)(config, vars.resolve.bind(vars));
|
|
8081
|
-
await driver.connect();
|
|
8082
|
-
let explorerId;
|
|
8083
|
-
if (config.agents.explorer.enabled && !interrupted) {
|
|
8084
|
-
const record = await workspace.startSession("explorer");
|
|
8085
|
-
explorerId = record.id;
|
|
8086
|
-
const session = new AgentSession(root2, config, vars, record.id, "explorer", driver, options.onLog);
|
|
8087
|
-
activeSession = session;
|
|
8088
|
-
if (!interrupted) {
|
|
8089
|
-
await runExplorer(session, runtime(config.agents.explorer.use));
|
|
8090
|
-
} else {
|
|
8091
|
-
await workspace.endSession(record.id, { summary: "Interrupted" });
|
|
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" });
|
|
8092
8412
|
}
|
|
8093
|
-
activeSession = void 0;
|
|
8094
|
-
}
|
|
8095
|
-
if (config.agents.judge.enabled && explorerId && !interrupted) {
|
|
8096
|
-
const record = await workspace.startSession("judge");
|
|
8097
|
-
const session = new AgentSession(root2, config, vars, record.id, "judge", void 0, options.onLog);
|
|
8098
|
-
activeSession = session;
|
|
8099
|
-
await runJudge(session, runtime(config.agents.judge.use), { sessionIds: [explorerId] });
|
|
8100
8413
|
}
|
|
8101
|
-
|
|
8414
|
+
const vars = new Vars(config.app.secrets);
|
|
8415
|
+
let app;
|
|
8416
|
+
let driver;
|
|
8102
8417
|
try {
|
|
8103
|
-
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);
|
|
8104
8445
|
} finally {
|
|
8105
|
-
|
|
8446
|
+
try {
|
|
8447
|
+
await driver?.close();
|
|
8448
|
+
} finally {
|
|
8449
|
+
await app?.stop();
|
|
8450
|
+
}
|
|
8106
8451
|
}
|
|
8107
8452
|
}
|
|
8108
8453
|
if (!interrupted && config.agents.explorer.enabled && config.agents.judge.enabled) {
|
|
@@ -8116,16 +8461,21 @@ async function runPatrol(options) {
|
|
|
8116
8461
|
isInterrupted: () => interrupted
|
|
8117
8462
|
});
|
|
8118
8463
|
}
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
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;
|
|
8129
8479
|
if (!interrupted && config.agents.github.enabled)
|
|
8130
8480
|
await runPublisher(root2, config, {
|
|
8131
8481
|
createRuntime: options.createRuntime,
|
|
@@ -8134,9 +8484,13 @@ async function runPatrol(options) {
|
|
|
8134
8484
|
activeSession = session;
|
|
8135
8485
|
}
|
|
8136
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
|
+
}
|
|
8137
8491
|
if (!interrupted && config.agents.github.enabled)
|
|
8138
8492
|
await syncGitHub(root2, config, { onLog: options.onLog });
|
|
8139
|
-
if (!interrupted && commit) {
|
|
8493
|
+
if (!interrupted && fresh && commit && !cycleFailed) {
|
|
8140
8494
|
tested = commit;
|
|
8141
8495
|
await workspace.setPatrol({ commit });
|
|
8142
8496
|
}
|
|
@@ -8150,6 +8504,7 @@ async function runPatrol(options) {
|
|
|
8150
8504
|
process.off("SIGINT", onSignal);
|
|
8151
8505
|
process.off("SIGTERM", onSignal);
|
|
8152
8506
|
}
|
|
8507
|
+
return { problems };
|
|
8153
8508
|
}
|
|
8154
8509
|
|
|
8155
8510
|
// ../cli/dist/commands/doctor.js
|
|
@@ -8198,15 +8553,13 @@ function runChecks(root2, config) {
|
|
|
8198
8553
|
}
|
|
8199
8554
|
function agentChecks(config, env = process.env) {
|
|
8200
8555
|
const checks = [];
|
|
8201
|
-
const
|
|
8202
|
-
for (const role of
|
|
8556
|
+
const roles4 = ["explorer", "judge", "fixer"].filter((role) => role !== "fixer" || config.agents.fixer.enabled);
|
|
8557
|
+
for (const role of roles4) {
|
|
8203
8558
|
const use = config.agents[role].use;
|
|
8204
8559
|
const problem = runtimeProblem(role, use, env);
|
|
8205
8560
|
const label = use.runtime === "cli" ? `cli: ${use.agent ?? use.command.split(" ")[0]}` : `${use.via}: ${use.model ?? "default model"}`;
|
|
8206
8561
|
checks.push({ name: role, ok: !problem, detail: problem ?? `The ${role} uses ${label}`, fatal: true });
|
|
8207
8562
|
}
|
|
8208
|
-
const decider = resolveDecider(config.decisions, env);
|
|
8209
|
-
checks.push({ name: "decider", ok: Boolean(decider.decider), detail: decider.reason, fatal: false });
|
|
8210
8563
|
if (config.agents.github.enabled) {
|
|
8211
8564
|
const installed = spawnSync("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
8212
8565
|
const loggedIn = installed && spawnSync("gh", ["auth", "status"], { stdio: "ignore" }).status === 0;
|
|
@@ -8221,12 +8574,12 @@ function doctorExitCode(checks) {
|
|
|
8221
8574
|
// ../cli/dist/commands/init.js
|
|
8222
8575
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8223
8576
|
import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
8224
|
-
import { join as
|
|
8577
|
+
import { join as join14, relative as relative4 } from "node:path";
|
|
8225
8578
|
import { createInterface } from "node:readline/promises";
|
|
8226
8579
|
|
|
8227
8580
|
// ../recon/dist/bringup.js
|
|
8228
8581
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
8229
|
-
import { join as
|
|
8582
|
+
import { join as join13 } from "node:path";
|
|
8230
8583
|
function detectBringUp(root2) {
|
|
8231
8584
|
const candidates = [];
|
|
8232
8585
|
for (const workflow of listWorkflows(root2)) {
|
|
@@ -8236,7 +8589,7 @@ function detectBringUp(root2) {
|
|
|
8236
8589
|
candidates.push({ command: match[0].replace(/^run:\s*/, ""), rung: "precedent", source: workflow, confidence: 0.8 });
|
|
8237
8590
|
}
|
|
8238
8591
|
}
|
|
8239
|
-
const pkgPath =
|
|
8592
|
+
const pkgPath = join13(root2, "package.json");
|
|
8240
8593
|
if (existsSync5(pkgPath)) {
|
|
8241
8594
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
8242
8595
|
const manager = detectPackageManager(root2);
|
|
@@ -8251,20 +8604,20 @@ function detectBringUp(root2) {
|
|
|
8251
8604
|
}
|
|
8252
8605
|
}
|
|
8253
8606
|
}
|
|
8254
|
-
if (existsSync5(
|
|
8607
|
+
if (existsSync5(join13(root2, "docker-compose.yml")) || existsSync5(join13(root2, "compose.yaml"))) {
|
|
8255
8608
|
candidates.push({ command: "docker compose up", rung: "convention", source: "docker-compose.yml", confidence: 0.5 });
|
|
8256
8609
|
}
|
|
8257
|
-
if (existsSync5(
|
|
8610
|
+
if (existsSync5(join13(root2, "Procfile"))) {
|
|
8258
8611
|
candidates.push({ command: "foreman start", rung: "convention", source: "Procfile", confidence: 0.3 });
|
|
8259
8612
|
}
|
|
8260
8613
|
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
8261
8614
|
}
|
|
8262
8615
|
function detectPackageManager(root2) {
|
|
8263
|
-
if (existsSync5(
|
|
8616
|
+
if (existsSync5(join13(root2, "pnpm-lock.yaml")))
|
|
8264
8617
|
return "pnpm";
|
|
8265
|
-
if (existsSync5(
|
|
8618
|
+
if (existsSync5(join13(root2, "bun.lockb")))
|
|
8266
8619
|
return "bun";
|
|
8267
|
-
if (existsSync5(
|
|
8620
|
+
if (existsSync5(join13(root2, "yarn.lock")))
|
|
8268
8621
|
return "yarn";
|
|
8269
8622
|
return "npm";
|
|
8270
8623
|
}
|
|
@@ -8285,7 +8638,7 @@ function detectStack(root2) {
|
|
|
8285
8638
|
["app.json", "expo"]
|
|
8286
8639
|
];
|
|
8287
8640
|
for (const [file, name] of signatures) {
|
|
8288
|
-
if (!existsSync5(
|
|
8641
|
+
if (!existsSync5(join13(root2, file)))
|
|
8289
8642
|
continue;
|
|
8290
8643
|
framework ??= name;
|
|
8291
8644
|
detectedFrom.push(file);
|
|
@@ -8300,12 +8653,12 @@ function detectStack(root2) {
|
|
|
8300
8653
|
};
|
|
8301
8654
|
}
|
|
8302
8655
|
function listWorkflows(root2) {
|
|
8303
|
-
const dir =
|
|
8656
|
+
const dir = join13(root2, ".github", "workflows");
|
|
8304
8657
|
if (!existsSync5(dir))
|
|
8305
8658
|
return [];
|
|
8306
8659
|
try {
|
|
8307
8660
|
const { readdirSync: readdirSync4 } = __require("node:fs");
|
|
8308
|
-
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));
|
|
8309
8662
|
} catch {
|
|
8310
8663
|
return [];
|
|
8311
8664
|
}
|
|
@@ -8319,11 +8672,6 @@ var KEY_PROVIDERS = {
|
|
|
8319
8672
|
openai: { env: "OPENAI_API_KEY", label: "OpenAI", url: "https://platform.openai.com/api-keys", model: "gpt-5-mini" },
|
|
8320
8673
|
anthropic: { env: "ANTHROPIC_API_KEY", label: "Anthropic", url: "https://console.anthropic.com/settings/keys", model: "claude-haiku-4-5" }
|
|
8321
8674
|
};
|
|
8322
|
-
var JEV_ROUTES2 = {
|
|
8323
|
-
typesafe: { env: "TYPESAFE_API_KEY", label: "TypeSafe", url: "https://typesafe.ai" },
|
|
8324
|
-
openrouter: { env: "OPENROUTER_API_KEY", label: "OpenRouter", url: "https://openrouter.ai/keys" },
|
|
8325
|
-
vercel: { env: "AI_GATEWAY_API_KEY", label: "Vercel AI Gateway", url: "https://vercel.com/ai-gateway" }
|
|
8326
|
-
};
|
|
8327
8675
|
var CLI_LABELS = { claude: "Claude Code", codex: "Codex", kimi: "Kimi CLI", pi: "pi" };
|
|
8328
8676
|
var run = (command2, args2) => spawnSync2(command2, args2, { encoding: "utf8", timeout: 1e4 }).stdout ?? "";
|
|
8329
8677
|
function detectProviders(env = process.env, exec6 = run) {
|
|
@@ -8333,7 +8681,6 @@ function detectProviders(env = process.env, exec6 = run) {
|
|
|
8333
8681
|
return {
|
|
8334
8682
|
clis: found.filter((agent) => agent !== "pi" || !piWithoutMcp),
|
|
8335
8683
|
keys: Object.keys(KEY_PROVIDERS).filter((key) => env[KEY_PROVIDERS[key].env]),
|
|
8336
|
-
jev: Object.keys(JEV_ROUTES2).filter((route2) => env[JEV_ROUTES2[route2].env]),
|
|
8337
8684
|
piWithoutMcp,
|
|
8338
8685
|
piPermissionModes: piList.includes("pi-permission-modes")
|
|
8339
8686
|
};
|
|
@@ -8368,18 +8715,18 @@ function list(dir) {
|
|
|
8368
8715
|
}
|
|
8369
8716
|
}
|
|
8370
8717
|
function xcodeProjects(root2) {
|
|
8371
|
-
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")));
|
|
8372
8719
|
}
|
|
8373
8720
|
function gradleFiles(root2) {
|
|
8374
|
-
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));
|
|
8375
8722
|
}
|
|
8376
8723
|
function detectAppId(root2, platform) {
|
|
8377
|
-
const expo = readJson2(
|
|
8724
|
+
const expo = readJson2(join14(root2, "app.json"))?.expo;
|
|
8378
8725
|
const fromJson = platform === "ios" ? expo?.ios?.bundleIdentifier : expo?.android?.package;
|
|
8379
8726
|
if (fromJson)
|
|
8380
8727
|
return { appId: fromJson, source: "app.json" };
|
|
8381
8728
|
for (const file of ["app.config.ts", "app.config.js"]) {
|
|
8382
|
-
const match = readText(
|
|
8729
|
+
const match = readText(join14(root2, file)).match(platform === "ios" ? /bundleIdentifier:\s*['"`]([\w.-]+)['"`]/ : /package:\s*['"`]([\w.-]+)['"`]/);
|
|
8383
8730
|
if (match)
|
|
8384
8731
|
return { appId: match[1], source: file };
|
|
8385
8732
|
}
|
|
@@ -8405,21 +8752,21 @@ function detectPort(root2, pkg, framework) {
|
|
|
8405
8752
|
if (fromScript)
|
|
8406
8753
|
return { port: Number(fromScript), source: "package.json" };
|
|
8407
8754
|
for (const file of ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) {
|
|
8408
|
-
const match = readText(
|
|
8755
|
+
const match = readText(join14(root2, file)).match(/\bport:\s*(\d{2,5})\b/);
|
|
8409
8756
|
if (match)
|
|
8410
8757
|
return { port: Number(match[1]), source: file };
|
|
8411
8758
|
}
|
|
8412
|
-
const fromEnv = readText(
|
|
8759
|
+
const fromEnv = readText(join14(root2, ".env")).match(/^PORT=(\d{2,5})\s*$/m)?.[1];
|
|
8413
8760
|
if (fromEnv)
|
|
8414
8761
|
return { port: Number(fromEnv), source: ".env" };
|
|
8415
8762
|
return { port: framework && FRAMEWORK_PORTS[framework] || 3e3, source: framework ? `the ${framework} default` : "the usual default" };
|
|
8416
8763
|
}
|
|
8417
8764
|
function mobilePlatform(root2) {
|
|
8418
|
-
const hasIos = xcodeProjects(root2).length > 0 || existsSync6(
|
|
8765
|
+
const hasIos = xcodeProjects(root2).length > 0 || existsSync6(join14(root2, "app.json")) || existsSync6(join14(root2, "ios"));
|
|
8419
8766
|
return process.platform === "darwin" && hasIos ? "ios" : "android";
|
|
8420
8767
|
}
|
|
8421
8768
|
function detectApp(root2) {
|
|
8422
|
-
const pkg = readJson2(
|
|
8769
|
+
const pkg = readJson2(join14(root2, "package.json")) ?? {};
|
|
8423
8770
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
8424
8771
|
const notes = [];
|
|
8425
8772
|
if (deps.electron) {
|
|
@@ -8427,8 +8774,8 @@ function detectApp(root2) {
|
|
|
8427
8774
|
return { platform: "electron", start: "npx electron . --remote-debugging-port=9222", cdpPort: 9222, notes };
|
|
8428
8775
|
}
|
|
8429
8776
|
const reactNative = deps.expo || deps["react-native"];
|
|
8430
|
-
const nativeIos = !existsSync6(
|
|
8431
|
-
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;
|
|
8432
8779
|
if (reactNative || nativeIos || nativeAndroid) {
|
|
8433
8780
|
const platform = nativeIos ? "ios" : nativeAndroid ? "android" : mobilePlatform(root2);
|
|
8434
8781
|
notes.push(`Platform ${platform}: detected from ${reactNative ? "package.json" : platform === "ios" ? "the Xcode project" : "the Gradle files"}.`);
|
|
@@ -8498,7 +8845,7 @@ function renderConfig(answers) {
|
|
|
8498
8845
|
lines.push(` appId: ${answers.appId ?? "com.example.app"}${answers.appId ? "" : " # TODO: your bundle ID or package name"}`);
|
|
8499
8846
|
lines.push(" # device: <simulator UDID or emulator serial> # default: the booted one");
|
|
8500
8847
|
}
|
|
8501
|
-
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)", "");
|
|
8502
8849
|
return lines.join("\n");
|
|
8503
8850
|
}
|
|
8504
8851
|
var INSTRUCTIONS = `# About the app
|
|
@@ -8521,7 +8868,7 @@ Example: Sign in with the email {{TEST_EMAIL}} and the password {{TEST_PASSWORD}
|
|
|
8521
8868
|
- Do not make payments.
|
|
8522
8869
|
`;
|
|
8523
8870
|
function ignoreData(root2) {
|
|
8524
|
-
const gitignore =
|
|
8871
|
+
const gitignore = join14(root2, ".gitignore");
|
|
8525
8872
|
const ignored = existsSync6(gitignore) ? readFileSync5(gitignore, "utf8") : "";
|
|
8526
8873
|
const lines = ignored.split("\n");
|
|
8527
8874
|
if (lines.some((line) => /^\/?\.bughunters\/runs\/?$/.test(line.trim())))
|
|
@@ -8538,7 +8885,7 @@ ${IGNORE_LINE}
|
|
|
8538
8885
|
return true;
|
|
8539
8886
|
}
|
|
8540
8887
|
function relativeConfig(root2) {
|
|
8541
|
-
const shown =
|
|
8888
|
+
const shown = relative4(process.cwd(), paths.config(root2));
|
|
8542
8889
|
return shown && !shown.startsWith("..") ? shown : paths.config(root2);
|
|
8543
8890
|
}
|
|
8544
8891
|
var IGNORE_LINE = `${BUGHUNTERS_DIR}/${DATA_DIR}/`;
|
|
@@ -8553,7 +8900,7 @@ function writeInitialConfig(root2, answers) {
|
|
|
8553
8900
|
writeFileSync3(configPath, renderConfig(answers));
|
|
8554
8901
|
result3.written.push(configName);
|
|
8555
8902
|
}
|
|
8556
|
-
const guidePath =
|
|
8903
|
+
const guidePath = join14(paths.dir(root2), "instructions.md");
|
|
8557
8904
|
if (existsSync6(guidePath))
|
|
8558
8905
|
result3.skipped.push(`${BUGHUNTERS_DIR}/instructions.md`);
|
|
8559
8906
|
else {
|
|
@@ -8568,12 +8915,6 @@ function writeInitialConfig(root2, answers) {
|
|
|
8568
8915
|
result3.warnings.push(`Set ${route2.env} before you run Bughunters. Get a key at ${route2.url}`);
|
|
8569
8916
|
}
|
|
8570
8917
|
}
|
|
8571
|
-
if (answers.jev !== "auto" && !process.env[JEV_ROUTES2[answers.jev].env]) {
|
|
8572
|
-
result3.warnings.push(`Set ${JEV_ROUTES2[answers.jev].env} to use Jev. Get a key at ${JEV_ROUTES2[answers.jev].url}`);
|
|
8573
|
-
}
|
|
8574
|
-
if (answers.jev === "auto" && !Object.values(JEV_ROUTES2).some((route2) => process.env[route2.env])) {
|
|
8575
|
-
result3.warnings.push("No Jev key is set, so Bughunters costs more. Get a key at https://typesafe.ai");
|
|
8576
|
-
}
|
|
8577
8918
|
return result3;
|
|
8578
8919
|
}
|
|
8579
8920
|
var PROVIDERS = [...CLI_AGENTS, ...Object.keys(KEY_PROVIDERS)];
|
|
@@ -8618,9 +8959,6 @@ function parseInitFlags(args2) {
|
|
|
8618
8959
|
case "--fixer":
|
|
8619
8960
|
flags[flag2.slice(2)] = oneOf(flag2, args2[++index], PROVIDERS);
|
|
8620
8961
|
break;
|
|
8621
|
-
case "--jev":
|
|
8622
|
-
flags.jev = oneOf(flag2, args2[++index], ["typesafe", "openrouter", "vercel", "auto"]);
|
|
8623
|
-
break;
|
|
8624
8962
|
default:
|
|
8625
8963
|
throw new ConfigError(`Unknown flag for init: ${flag2}`);
|
|
8626
8964
|
}
|
|
@@ -8667,7 +9005,6 @@ function defaultAnswers(guess, detected, flags) {
|
|
|
8667
9005
|
cdpPort: guess.cdpPort,
|
|
8668
9006
|
ready: platform === guess.platform ? guess.ready : void 0,
|
|
8669
9007
|
providers,
|
|
8670
|
-
jev: flags.jev ?? detected.jev[0] ?? "auto",
|
|
8671
9008
|
piPermissionModes: detected.piPermissionModes
|
|
8672
9009
|
};
|
|
8673
9010
|
}
|
|
@@ -8720,15 +9057,6 @@ async function interview(ask, root2, guess, detected, answers) {
|
|
|
8720
9057
|
next.providers.explorer = await choose(ask, "Explorer: it uses the app and reports what looks wrong.", options, next.providers.explorer);
|
|
8721
9058
|
next.providers.judge = await choose(ask, "Judge: it decides which reports are real bugs.", options, next.providers.explorer);
|
|
8722
9059
|
next.providers.fixer = await choose(ask, "Fixer: it writes the fixes (off until you turn it on).", options, next.providers.fixer);
|
|
8723
|
-
process.stdout.write("\nJev triages each finding in one fast, low-cost call. The LLM judge then sees only the real findings.\n");
|
|
8724
|
-
const jevOptions = [
|
|
8725
|
-
...Object.keys(JEV_ROUTES2).map((route2) => ({
|
|
8726
|
-
value: route2,
|
|
8727
|
-
label: `${JEV_ROUTES2[route2].label} (${JEV_ROUTES2[route2].env}${detected.jev.includes(route2) ? " is set" : `, get a key at ${JEV_ROUTES2[route2].url}`})`
|
|
8728
|
-
})),
|
|
8729
|
-
{ value: "auto", label: "Try each key in order (with no key, the LLM does this work at a higher cost)" }
|
|
8730
|
-
];
|
|
8731
|
-
next.jev = await choose(ask, "How does Bughunters reach Jev?", jevOptions, next.jev === "auto" ? detected.jev[0] ?? "typesafe" : next.jev);
|
|
8732
9060
|
return next;
|
|
8733
9061
|
}
|
|
8734
9062
|
async function runInit(root2, args2, log) {
|
|
@@ -8861,9 +9189,6 @@ determinism:
|
|
|
8861
9189
|
blockThirdPartyRequests: true
|
|
8862
9190
|
|
|
8863
9191
|
decisions:
|
|
8864
|
-
decider: jev # jev | model
|
|
8865
|
-
jev:
|
|
8866
|
-
via: auto # auto | typesafe | openrouter | vercel
|
|
8867
9192
|
model:
|
|
8868
9193
|
via: auto # auto | openrouter | vercel | openai | anthropic | custom
|
|
8869
9194
|
name: "" # optional model id override
|
|
@@ -8895,7 +9220,7 @@ production:
|
|
|
8895
9220
|
|
|
8896
9221
|
// ../cli/dist/commands/run.js
|
|
8897
9222
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8898
|
-
import { join as
|
|
9223
|
+
import { join as join16 } from "node:path";
|
|
8899
9224
|
|
|
8900
9225
|
// ../capture/dist/determinism.js
|
|
8901
9226
|
var DETERMINISTIC_CHROMIUM_ARGS = [
|
|
@@ -9087,7 +9412,7 @@ async function assertNoFontFallback(page, failOnFallback) {
|
|
|
9087
9412
|
|
|
9088
9413
|
// ../capture/dist/capture.js
|
|
9089
9414
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9090
|
-
import { dirname as
|
|
9415
|
+
import { dirname as dirname4 } from "node:path";
|
|
9091
9416
|
|
|
9092
9417
|
// ../capture/dist/pixel-contrast.js
|
|
9093
9418
|
import { PNG as PNG3 } from "pngjs";
|
|
@@ -9160,7 +9485,7 @@ async function captureScreen(page, config, request) {
|
|
|
9160
9485
|
const stability = await waitForStableFrame(page, config.determinism.stabilityGate);
|
|
9161
9486
|
const masks = await resolveMasks(page, config.mask, request.url);
|
|
9162
9487
|
const consoleErrors2 = collectedErrors.get(page) ?? [];
|
|
9163
|
-
mkdirSync3(
|
|
9488
|
+
mkdirSync3(dirname4(request.outPath), { recursive: true });
|
|
9164
9489
|
const buffer = await page.screenshot({
|
|
9165
9490
|
path: request.outPath,
|
|
9166
9491
|
fullPage: request.fullPage ?? true,
|
|
@@ -9298,7 +9623,7 @@ function bodyMentions(body, selector) {
|
|
|
9298
9623
|
async function stopProcess(child) {
|
|
9299
9624
|
if (child.exitCode !== null || child.pid === void 0)
|
|
9300
9625
|
return;
|
|
9301
|
-
const done = new Promise((
|
|
9626
|
+
const done = new Promise((resolve12) => child.once("exit", () => resolve12()));
|
|
9302
9627
|
try {
|
|
9303
9628
|
process.kill(-child.pid, "SIGTERM");
|
|
9304
9629
|
} catch {
|
|
@@ -9316,7 +9641,7 @@ async function stopProcess(child) {
|
|
|
9316
9641
|
|
|
9317
9642
|
// ../cli/dist/commands/run-pipeline.js
|
|
9318
9643
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9319
|
-
import { join as
|
|
9644
|
+
import { join as join15 } from "node:path";
|
|
9320
9645
|
|
|
9321
9646
|
// ../triage/dist/ledger.js
|
|
9322
9647
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
|
|
@@ -9418,8 +9743,8 @@ async function cluster(findings, options = {}) {
|
|
|
9418
9743
|
function deterministicPass(findings, changedFilesByFinding) {
|
|
9419
9744
|
const buckets = /* @__PURE__ */ new Map();
|
|
9420
9745
|
for (const finding of findings) {
|
|
9421
|
-
const
|
|
9422
|
-
const key = [finding.ruleId, [...
|
|
9746
|
+
const files2 = changedFilesByFinding?.get(finding.id) ?? finding.suspectedFiles;
|
|
9747
|
+
const key = [finding.ruleId, [...files2].sort().join(",")].join("::");
|
|
9423
9748
|
const bucket = buckets.get(key);
|
|
9424
9749
|
if (bucket)
|
|
9425
9750
|
bucket.push(finding);
|
|
@@ -9747,7 +10072,7 @@ async function executeRun(options) {
|
|
|
9747
10072
|
console: screen.snapshot.consoleErrors,
|
|
9748
10073
|
knownIntents: ledger.summaries()
|
|
9749
10074
|
});
|
|
9750
|
-
const estimate = decider ? estimateDecisionCost(text3.length
|
|
10075
|
+
const estimate = decider ? estimateDecisionCost(text3.length) : 0;
|
|
9751
10076
|
let answers = {};
|
|
9752
10077
|
if (!decider) {
|
|
9753
10078
|
trace2.decision = {
|
|
@@ -9996,13 +10321,13 @@ function worstOf(a, b) {
|
|
|
9996
10321
|
function writeArtifacts(root2, run2, findings, meta) {
|
|
9997
10322
|
const dir = paths.run(root2, run2.id);
|
|
9998
10323
|
mkdirSync4(dir, { recursive: true });
|
|
9999
|
-
const reportPath =
|
|
10324
|
+
const reportPath = join15(dir, "report.html");
|
|
10000
10325
|
writeFileSync5(reportPath, renderHtml({ run: run2, findings, suppressed: meta.suppressed, quarantined: 0, notes: meta.notes }));
|
|
10001
|
-
writeFileSync5(
|
|
10002
|
-
writeFileSync5(
|
|
10003
|
-
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)}
|
|
10004
10329
|
`);
|
|
10005
|
-
writeFileSync5(
|
|
10330
|
+
writeFileSync5(join15(dir, "trace.json"), `${JSON.stringify(meta.trace, null, 2)}
|
|
10006
10331
|
`);
|
|
10007
10332
|
return { reportPath, runDir: dir };
|
|
10008
10333
|
}
|
|
@@ -10031,7 +10356,7 @@ async function runCommand(options) {
|
|
|
10031
10356
|
if (server.external)
|
|
10032
10357
|
log(`Using the app already serving at ${requireRun(config).url}`);
|
|
10033
10358
|
const captured = [];
|
|
10034
|
-
const runDir =
|
|
10359
|
+
const runDir = join16(paths.runs(root2), "latest");
|
|
10035
10360
|
mkdirSync5(runDir, { recursive: true });
|
|
10036
10361
|
const live = new LiveProgressWriter(root2, targets.length * config.viewports.length);
|
|
10037
10362
|
live.start();
|
|
@@ -10135,7 +10460,7 @@ var LiveProgressWriter = class {
|
|
|
10135
10460
|
async function captureOne(args2) {
|
|
10136
10461
|
const { target, viewport, session, config, store, runDir } = args2;
|
|
10137
10462
|
const slug2 = `${slugify(target.id)}--${viewport.name}`;
|
|
10138
|
-
const actualPath =
|
|
10463
|
+
const actualPath = join16(runDir, `${slug2}.actual.png`);
|
|
10139
10464
|
const output = await captureScreen(session.page, config, {
|
|
10140
10465
|
screenId: target.id,
|
|
10141
10466
|
url: target.url,
|
|
@@ -10160,7 +10485,7 @@ async function captureOne(args2) {
|
|
|
10160
10485
|
missingFonts: output.missingFonts
|
|
10161
10486
|
};
|
|
10162
10487
|
}
|
|
10163
|
-
const diffPath =
|
|
10488
|
+
const diffPath = join16(runDir, `${slug2}.diff.png`);
|
|
10164
10489
|
const comparison = await diff({
|
|
10165
10490
|
baselinePath,
|
|
10166
10491
|
actualPath,
|
|
@@ -10223,7 +10548,7 @@ function loadBaselineSnapshot(root2, key) {
|
|
|
10223
10548
|
}
|
|
10224
10549
|
}
|
|
10225
10550
|
function snapshotFile(root2, key) {
|
|
10226
|
-
return
|
|
10551
|
+
return join16(paths.baselines(root2), `${slugify(key)}-${shortHash(key, 8)}.snapshot.json`);
|
|
10227
10552
|
}
|
|
10228
10553
|
function saveBaselineSnapshot(root2, key, snapshot) {
|
|
10229
10554
|
mkdirSync5(paths.baselines(root2), { recursive: true });
|
|
@@ -10324,12 +10649,12 @@ function formatRunSummary(result3) {
|
|
|
10324
10649
|
// ../dashboard/dist/server.js
|
|
10325
10650
|
import { createServer as createServer2 } from "node:http";
|
|
10326
10651
|
import { createReadStream, existsSync as existsSync10, realpathSync, statSync as statSync2, watch } from "node:fs";
|
|
10327
|
-
import { extname, join as
|
|
10652
|
+
import { extname, join as join19, relative as relative5, resolve as resolve11, sep as sep4 } from "node:path";
|
|
10328
10653
|
import { fileURLToPath } from "node:url";
|
|
10329
10654
|
|
|
10330
10655
|
// ../dashboard/dist/project.js
|
|
10331
10656
|
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
10332
|
-
import { join as
|
|
10657
|
+
import { join as join17 } from "node:path";
|
|
10333
10658
|
var ProjectReader = class {
|
|
10334
10659
|
root;
|
|
10335
10660
|
constructor(root2) {
|
|
@@ -10346,16 +10671,16 @@ var ProjectReader = class {
|
|
|
10346
10671
|
const dir = paths.runs(this.root);
|
|
10347
10672
|
if (!existsSync9(dir))
|
|
10348
10673
|
return [];
|
|
10349
|
-
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);
|
|
10350
10675
|
}
|
|
10351
10676
|
readRun(runId) {
|
|
10352
|
-
const dir =
|
|
10353
|
-
const runFile =
|
|
10677
|
+
const dir = join17(paths.runs(this.root), runId);
|
|
10678
|
+
const runFile = join17(dir, "run.json");
|
|
10354
10679
|
if (!existsSync9(runFile))
|
|
10355
10680
|
return void 0;
|
|
10356
10681
|
try {
|
|
10357
10682
|
const parsed = JSON.parse(readFileSync8(runFile, "utf8"));
|
|
10358
|
-
const traceFile =
|
|
10683
|
+
const traceFile = join17(dir, "trace.json");
|
|
10359
10684
|
const trace = existsSync9(traceFile) ? JSON.parse(readFileSync8(traceFile, "utf8")) : void 0;
|
|
10360
10685
|
return { id: runId, dir, run: parsed.run, findings: parsed.findings, trace, mtimeMs: safeMtime(dir) };
|
|
10361
10686
|
} catch {
|
|
@@ -10441,8 +10766,8 @@ function safeMtime(path) {
|
|
|
10441
10766
|
|
|
10442
10767
|
// ../dashboard/dist/agents.js
|
|
10443
10768
|
import { readFileSync as readFileSync9, readdirSync as readdirSync3 } from "node:fs";
|
|
10444
|
-
import { basename as basename4, join as
|
|
10445
|
-
var
|
|
10769
|
+
import { basename as basename4, join as join18 } from "node:path";
|
|
10770
|
+
var roles2 = ["explorer", "judge", "fixer"];
|
|
10446
10771
|
var severity = { critical: 0, major: 1, minor: 2, cosmetic: 3 };
|
|
10447
10772
|
var open = /* @__PURE__ */ new Set(["new", "filed", "fixing", "fix-proposed"]);
|
|
10448
10773
|
function readJson3(file) {
|
|
@@ -10498,7 +10823,7 @@ var AgentReader = class {
|
|
|
10498
10823
|
const value = readJson3(paths.agents(this.root));
|
|
10499
10824
|
if (!Array.isArray(value?.agents))
|
|
10500
10825
|
return void 0;
|
|
10501
|
-
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) => {
|
|
10502
10827
|
const busy = agent.state === "working" || agent.state === "waiting";
|
|
10503
10828
|
return busy && !processAlive(agent.pid) ? { ...agent, state: "idle", activity: STOPPED } : agent;
|
|
10504
10829
|
});
|
|
@@ -10513,28 +10838,31 @@ var AgentReader = class {
|
|
|
10513
10838
|
}
|
|
10514
10839
|
issues() {
|
|
10515
10840
|
const fixes = this.fixes();
|
|
10516
|
-
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));
|
|
10517
10845
|
}
|
|
10518
10846
|
fixes() {
|
|
10519
|
-
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));
|
|
10520
10848
|
}
|
|
10521
10849
|
routines() {
|
|
10522
|
-
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)));
|
|
10523
10851
|
}
|
|
10524
10852
|
sessions(limit = 50) {
|
|
10525
|
-
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);
|
|
10526
10854
|
}
|
|
10527
10855
|
session(id2) {
|
|
10528
10856
|
const session = this.sessions(Infinity).find((entry) => entry.id === id2);
|
|
10529
10857
|
if (!session)
|
|
10530
10858
|
return void 0;
|
|
10531
10859
|
const dir = paths.session(this.root, id2);
|
|
10532
|
-
const events = jsonLines(
|
|
10860
|
+
const events = jsonLines(join18(dir, "events.jsonl"));
|
|
10533
10861
|
return {
|
|
10534
10862
|
// A running session has no totals yet: add up its events so far.
|
|
10535
10863
|
session: session.status === "running" ? { ...session, ...usageOf(events) } : session,
|
|
10536
10864
|
events,
|
|
10537
|
-
candidates: jsonLines(
|
|
10865
|
+
candidates: jsonLines(join18(dir, "candidates.jsonl"))
|
|
10538
10866
|
};
|
|
10539
10867
|
}
|
|
10540
10868
|
issue(id2) {
|
|
@@ -10544,7 +10872,7 @@ var AgentReader = class {
|
|
|
10544
10872
|
const fix = this.fixes().find((entry) => entry.id === issue.fixId || entry.issueId === id2);
|
|
10545
10873
|
const routine = this.routines().find((entry) => entry.id === issue.evidence?.routineId);
|
|
10546
10874
|
const ids = new Set(issue.candidateIds);
|
|
10547
|
-
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));
|
|
10548
10876
|
return { issue, fix, candidates, routine };
|
|
10549
10877
|
}
|
|
10550
10878
|
screens() {
|
|
@@ -10593,6 +10921,10 @@ var AgentReader = class {
|
|
|
10593
10921
|
edges.push({ from: "__start", to: screen.id, kind: "route", via: "route", count: 1, steps: 0 });
|
|
10594
10922
|
}
|
|
10595
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
|
+
}
|
|
10596
10928
|
const screens = map.screens.map((screen) => ({
|
|
10597
10929
|
...screen,
|
|
10598
10930
|
openIssues: issues.filter((issue) => issue.screenId === screen.id).length
|
|
@@ -10601,16 +10933,30 @@ var AgentReader = class {
|
|
|
10601
10933
|
screens.push({ id: "__start", name: "App start", virtual: true, openIssues: 0 });
|
|
10602
10934
|
return { ...map, screens, edges, entryId: screens.length ? "__start" : void 0 };
|
|
10603
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
|
+
}
|
|
10604
10949
|
overview(now = /* @__PURE__ */ new Date()) {
|
|
10605
10950
|
const agentFile = this.agents();
|
|
10606
10951
|
const appmap = this.appmap();
|
|
10607
10952
|
const issues = this.issues();
|
|
10608
10953
|
const sessions = this.sessions(Infinity);
|
|
10609
10954
|
const today = now.toLocaleDateString("en-CA");
|
|
10610
|
-
const
|
|
10955
|
+
const configured = this.configuredAgents();
|
|
10956
|
+
const agents = roles2.map((role) => agentFile?.agents.find((agent) => agent.role === role) ?? {
|
|
10611
10957
|
role,
|
|
10612
|
-
state: "off",
|
|
10613
|
-
runtime: "",
|
|
10958
|
+
state: configured?.[role].enabled === false ? "off" : configured ? "idle" : "off",
|
|
10959
|
+
runtime: configured?.[role].runtime ?? "",
|
|
10614
10960
|
updatedAt: "",
|
|
10615
10961
|
spentUsd: 0
|
|
10616
10962
|
});
|
|
@@ -10620,9 +10966,9 @@ var AgentReader = class {
|
|
|
10620
10966
|
const screenshot = [...detail?.events ?? []].reverse().find((event) => event.screenshot)?.screenshot;
|
|
10621
10967
|
const openIssues = issues.filter((issue) => open.has(issue.status));
|
|
10622
10968
|
const fixes = this.fixes();
|
|
10623
|
-
const attention = openIssues.
|
|
10969
|
+
const attention = openIssues.slice(0, 8).map((issue) => {
|
|
10624
10970
|
const fix = fixes.find((entry) => entry.id === issue.fixId || entry.issueId === issue.id);
|
|
10625
|
-
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 };
|
|
10626
10972
|
});
|
|
10627
10973
|
const prStates = fixes.map((fix) => fix.pr?.state).filter(Boolean);
|
|
10628
10974
|
const issueStates = issues.map((issue) => issue.github?.state).filter(Boolean);
|
|
@@ -10674,7 +11020,7 @@ function usageReport(sessions, now = /* @__PURE__ */ new Date()) {
|
|
|
10674
11020
|
rows.set(key, row);
|
|
10675
11021
|
}
|
|
10676
11022
|
}
|
|
10677
|
-
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));
|
|
10678
11024
|
return { todayByRole, week, ...weekTotal ? { weekTotal } : {} };
|
|
10679
11025
|
}
|
|
10680
11026
|
function isFeedEvent(event) {
|
|
@@ -10942,7 +11288,7 @@ function hostOf(href) {
|
|
|
10942
11288
|
}
|
|
10943
11289
|
|
|
10944
11290
|
// ../dashboard/dist/server.js
|
|
10945
|
-
var UI_DIR =
|
|
11291
|
+
var UI_DIR = resolve11(fileURLToPath(new URL(".", import.meta.url)), "..", "src", "ui");
|
|
10946
11292
|
var MIME = {
|
|
10947
11293
|
".html": "text/html; charset=utf-8",
|
|
10948
11294
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -10953,7 +11299,7 @@ var MIME = {
|
|
|
10953
11299
|
".json": "application/json; charset=utf-8"
|
|
10954
11300
|
};
|
|
10955
11301
|
async function startDashboard(options) {
|
|
10956
|
-
const root2 =
|
|
11302
|
+
const root2 = resolve11(options.root);
|
|
10957
11303
|
const host = options.host ?? "127.0.0.1";
|
|
10958
11304
|
const reader = new ProjectReader(root2);
|
|
10959
11305
|
const agents = new AgentReader(root2);
|
|
@@ -11048,7 +11394,7 @@ async function handle2(req, res, ctx) {
|
|
|
11048
11394
|
return serveUi(res, path);
|
|
11049
11395
|
}
|
|
11050
11396
|
function resolveArtifactPath(root2, requested) {
|
|
11051
|
-
const candidate =
|
|
11397
|
+
const candidate = resolve11(root2, requested);
|
|
11052
11398
|
if (!existsSync10(candidate))
|
|
11053
11399
|
return void 0;
|
|
11054
11400
|
let real;
|
|
@@ -11059,8 +11405,8 @@ function resolveArtifactPath(root2, requested) {
|
|
|
11059
11405
|
} catch {
|
|
11060
11406
|
return void 0;
|
|
11061
11407
|
}
|
|
11062
|
-
const rel =
|
|
11063
|
-
if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${
|
|
11408
|
+
const rel = relative5(allowed, real);
|
|
11409
|
+
if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${sep4}`))
|
|
11064
11410
|
return void 0;
|
|
11065
11411
|
const stat = statSync2(real);
|
|
11066
11412
|
if (!stat.isFile())
|
|
@@ -11081,7 +11427,7 @@ function serveArtifact(res, root2, requested) {
|
|
|
11081
11427
|
}
|
|
11082
11428
|
function serveUi(res, path) {
|
|
11083
11429
|
const name = path === "/" ? "index.html" : path.replace(/^\//, "");
|
|
11084
|
-
const file =
|
|
11430
|
+
const file = resolve11(UI_DIR, name);
|
|
11085
11431
|
if (!file.startsWith(UI_DIR) || !existsSync10(file) || !statSync2(file).isFile()) {
|
|
11086
11432
|
return serveUi(res, "/");
|
|
11087
11433
|
}
|
|
@@ -11214,6 +11560,7 @@ function parseAgentFlags(command2, args2) {
|
|
|
11214
11560
|
fix: ["--issue"],
|
|
11215
11561
|
retest: ["--issue"],
|
|
11216
11562
|
publish: ["--issue"],
|
|
11563
|
+
ci: ["--issue"],
|
|
11217
11564
|
patrol: []
|
|
11218
11565
|
};
|
|
11219
11566
|
if (!values[command2])
|
|
@@ -11224,6 +11571,10 @@ function parseAgentFlags(command2, args2) {
|
|
|
11224
11571
|
flags[flag2.slice(2)] = true;
|
|
11225
11572
|
continue;
|
|
11226
11573
|
}
|
|
11574
|
+
if (command2 === "ci" && flag2 === "--wait") {
|
|
11575
|
+
flags.wait = true;
|
|
11576
|
+
continue;
|
|
11577
|
+
}
|
|
11227
11578
|
if (command2 === "publish" && flag2 === "--dry-run") {
|
|
11228
11579
|
flags.dryRun = true;
|
|
11229
11580
|
continue;
|
|
@@ -11261,6 +11612,8 @@ function rolesFor(command2, config) {
|
|
|
11261
11612
|
case "judge":
|
|
11262
11613
|
case "publish":
|
|
11263
11614
|
return ["judge"];
|
|
11615
|
+
case "ci":
|
|
11616
|
+
return ["fixer"];
|
|
11264
11617
|
case "retest":
|
|
11265
11618
|
return ["explorer", "judge"];
|
|
11266
11619
|
case "fix":
|
|
@@ -11275,15 +11628,36 @@ function preflight(command2, config, env = process.env) {
|
|
|
11275
11628
|
const problems = rolesFor(command2, config).map((role) => runtimeProblem(role, config.agents[role].use, env)).filter((problem) => Boolean(problem));
|
|
11276
11629
|
if (problems.length)
|
|
11277
11630
|
throw new ConfigError(problems.join("\n"));
|
|
11278
|
-
return ["explore", "patrol"].includes(command2) ? [resolveDecider(config.decisions, env).reason] : [];
|
|
11279
11631
|
}
|
|
11280
11632
|
async function runAgentCommand(command2, args2, root2, config, log) {
|
|
11281
11633
|
const flags = parseAgentFlags(command2, args2);
|
|
11282
|
-
|
|
11283
|
-
log(line);
|
|
11634
|
+
preflight(command2, config);
|
|
11284
11635
|
const workspace = new Workspace(root2);
|
|
11285
11636
|
if (command2 === "patrol") {
|
|
11286
|
-
await runPatrol({ root: root2, config, once: Boolean(flags.once), force: Boolean(flags.force), 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;
|
|
11287
11661
|
return;
|
|
11288
11662
|
}
|
|
11289
11663
|
if (command2 === "publish") {
|
|
@@ -11475,7 +11849,7 @@ function bySeverity(a, b) {
|
|
|
11475
11849
|
}
|
|
11476
11850
|
|
|
11477
11851
|
// ../cli/dist/commands/memory.js
|
|
11478
|
-
var
|
|
11852
|
+
var roles3 = /* @__PURE__ */ new Set(["explorer", "judge", "fixer"]);
|
|
11479
11853
|
var flag = (args2, name) => {
|
|
11480
11854
|
const at = args2.indexOf(name);
|
|
11481
11855
|
return at < 0 ? void 0 : args2[at + 1];
|
|
@@ -11485,7 +11859,7 @@ async function runMemoryCommand(args2, root2, log) {
|
|
|
11485
11859
|
const workspace = new Workspace(root2);
|
|
11486
11860
|
if (action === "list") {
|
|
11487
11861
|
const role = flag(args2.slice(1), "--role");
|
|
11488
|
-
if (role && !
|
|
11862
|
+
if (role && !roles3.has(role))
|
|
11489
11863
|
throw new ConfigError(`Unknown memory role: ${role}`);
|
|
11490
11864
|
const lessons = (await workspace.readMemory()).lessons.filter((lesson) => !role || lesson.role === role);
|
|
11491
11865
|
log("id role hits source scope text");
|
|
@@ -11498,7 +11872,7 @@ async function runMemoryCommand(args2, root2, log) {
|
|
|
11498
11872
|
if (action === "add") {
|
|
11499
11873
|
const role = flag(args2.slice(1), "--role");
|
|
11500
11874
|
const text3 = args2.slice(1).find((part, index, all) => !part.startsWith("--") && all[index - 1] !== "--role" && all[index - 1] !== "--scope");
|
|
11501
|
-
if (!role || !
|
|
11875
|
+
if (!role || !roles3.has(role) || !text3?.trim())
|
|
11502
11876
|
throw new ConfigError('Usage: bughunters memory add --role explorer|judge|fixer "text" [--scope s]');
|
|
11503
11877
|
const [lesson] = await workspace.upsertLessons([{
|
|
11504
11878
|
role,
|
|
@@ -11532,7 +11906,7 @@ var [command, ...args] = process.argv.slice(2);
|
|
|
11532
11906
|
var root = findProjectRoot(process.cwd());
|
|
11533
11907
|
function version() {
|
|
11534
11908
|
if (true)
|
|
11535
|
-
return "0.1.
|
|
11909
|
+
return "0.1.2";
|
|
11536
11910
|
try {
|
|
11537
11911
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
11538
11912
|
} catch {
|
|
@@ -11596,6 +11970,7 @@ try {
|
|
|
11596
11970
|
case "fix":
|
|
11597
11971
|
case "retest":
|
|
11598
11972
|
case "publish":
|
|
11973
|
+
case "ci":
|
|
11599
11974
|
case "patrol":
|
|
11600
11975
|
case "replay": {
|
|
11601
11976
|
const config = loadConfig(root);
|