bughunters 0.1.0 → 0.1.1
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 +120 -37
- package/package.json +1 -1
- package/src/ui/app.js +1 -1
package/dist/bin.js
CHANGED
|
@@ -364,7 +364,12 @@ var agentsSchema = z.object({
|
|
|
364
364
|
patrol: z.object({
|
|
365
365
|
intervalMinutes: z.number().positive().default(30),
|
|
366
366
|
/** Stop after this many cycles; 0 means run until stopped. */
|
|
367
|
-
cycles: z.number().int().nonnegative().default(0)
|
|
367
|
+
cycles: z.number().int().nonnegative().default(0),
|
|
368
|
+
/**
|
|
369
|
+
* At the start of each cycle, fetch this `remote/branch` and check out its
|
|
370
|
+
* latest commit (detached) in the source repository. false keeps the checkout.
|
|
371
|
+
*/
|
|
372
|
+
pull: z.union([z.string().regex(/^[^/]+\/.+$/, "Use remote/branch, e.g. origin/main"), z.literal(false)]).default("origin/main")
|
|
368
373
|
}).default({})
|
|
369
374
|
}).default({});
|
|
370
375
|
var bughuntersConfigSchema = z.object({
|
|
@@ -697,7 +702,7 @@ Agents
|
|
|
697
702
|
bughunters publish [--issue <id>] [--dry-run]
|
|
698
703
|
bughunters github sync
|
|
699
704
|
bughunters worktrees clean
|
|
700
|
-
bughunters patrol [--once]
|
|
705
|
+
bughunters patrol [--once] [--force]
|
|
701
706
|
bughunters replay <routine-id>
|
|
702
707
|
bughunters issue list | dismiss <id> --reason "..." [--by name] | reopen <id>
|
|
703
708
|
bughunters memory list [--role r] | add --role r "text" [--scope s]
|
|
@@ -772,10 +777,12 @@ var COMMAND_HELP = {
|
|
|
772
777
|
--dry-run write the reports to .bughunters/runs/publish/, and open nothing
|
|
773
778
|
--issue <id> publish this issue only
|
|
774
779
|
`,
|
|
775
|
-
patrol: `bughunters patrol [--once]
|
|
780
|
+
patrol: `bughunters patrol [--once] [--force]
|
|
776
781
|
Run the full cycle again and again: setup, explore, judge, teardown, fix, retest, publish.
|
|
777
782
|
The fixer and GitHub steps run only when you turn them on.
|
|
783
|
+
A cycle runs only when the commit changed since the last full cycle. Otherwise it only syncs GitHub.
|
|
778
784
|
--once run one cycle, then stop
|
|
785
|
+
--force run the first cycle also when the commit did not change
|
|
779
786
|
`,
|
|
780
787
|
github: `bughunters github sync
|
|
781
788
|
Read the state of each PR and issue from GitHub. A closed PR becomes a lesson. An issue closed as not planned becomes a dismissal.
|
|
@@ -910,9 +917,9 @@ async function startApp(app, opts) {
|
|
|
910
917
|
if (command2.readyWhen) {
|
|
911
918
|
const ready = new RegExp(command2.readyWhen);
|
|
912
919
|
await new Promise((done, reject) => {
|
|
913
|
-
const
|
|
920
|
+
const wait2 = setTimeout(() => finish(new Error("timed out waiting for ready output")), command2.timeoutMs);
|
|
914
921
|
const finish = (error) => {
|
|
915
|
-
clearTimeout(
|
|
922
|
+
clearTimeout(wait2);
|
|
916
923
|
child.stdout?.off("data", check);
|
|
917
924
|
child.stderr?.off("data", check);
|
|
918
925
|
child.off("exit", onExit);
|
|
@@ -3227,12 +3234,12 @@ function stepWords(step) {
|
|
|
3227
3234
|
}
|
|
3228
3235
|
|
|
3229
3236
|
// ../agents/dist/github.js
|
|
3230
|
-
var defaultGh = (args2, opts) => new Promise((
|
|
3237
|
+
var defaultGh = (args2, opts) => new Promise((resolve11, reject) => {
|
|
3231
3238
|
const child = execFile2("gh", args2, { cwd: opts?.cwd, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
3232
3239
|
if (error)
|
|
3233
3240
|
reject(Object.assign(error, { stderr, stdout }));
|
|
3234
3241
|
else
|
|
3235
|
-
|
|
3242
|
+
resolve11(stdout.trim());
|
|
3236
3243
|
});
|
|
3237
3244
|
child.stdin?.end(opts?.input);
|
|
3238
3245
|
});
|
|
@@ -4851,7 +4858,7 @@ async function actWhenReady(driver, action, session, windowMs) {
|
|
|
4851
4858
|
if (result3.ok || !retry || Date.now() >= deadline || session.cancelled) {
|
|
4852
4859
|
return { ...result3, degraded };
|
|
4853
4860
|
}
|
|
4854
|
-
await new Promise((
|
|
4861
|
+
await new Promise((resolve11) => setTimeout(resolve11, 500));
|
|
4855
4862
|
}
|
|
4856
4863
|
}
|
|
4857
4864
|
function toAction(step, session) {
|
|
@@ -6952,7 +6959,7 @@ var MaestroDriver = class {
|
|
|
6952
6959
|
await this.run([`openLink: ${yamlString(action.url)}`]);
|
|
6953
6960
|
}
|
|
6954
6961
|
async wait(action) {
|
|
6955
|
-
await new Promise((
|
|
6962
|
+
await new Promise((resolve11) => setTimeout(resolve11, action.ms));
|
|
6956
6963
|
}
|
|
6957
6964
|
async switchWindow() {
|
|
6958
6965
|
throw new Error("Window switching is unsupported on mobile");
|
|
@@ -7013,7 +7020,7 @@ var MaestroDriver = class {
|
|
|
7013
7020
|
return { frames, stable: true };
|
|
7014
7021
|
}
|
|
7015
7022
|
previous = current;
|
|
7016
|
-
await new Promise((
|
|
7023
|
+
await new Promise((resolve11) => setTimeout(resolve11, interval));
|
|
7017
7024
|
}
|
|
7018
7025
|
return { frames, stable: false };
|
|
7019
7026
|
}
|
|
@@ -7952,9 +7959,75 @@ ${items.map((item) => `${item.issue.id}: ${item.issue.title}`).join("\n")}`),
|
|
|
7952
7959
|
}
|
|
7953
7960
|
|
|
7954
7961
|
// ../agents/dist/patrol.js
|
|
7962
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
7963
|
+
import { resolve as resolve9 } from "node:path";
|
|
7964
|
+
import { promisify as promisify6 } from "node:util";
|
|
7965
|
+
var exec5 = promisify6(execFile6);
|
|
7966
|
+
async function git4(cwd, ...args2) {
|
|
7967
|
+
return (await exec5("git", args2, { cwd })).stdout.trim();
|
|
7968
|
+
}
|
|
7969
|
+
async function pullSource(root2, config, onLog) {
|
|
7970
|
+
const target = config.agents.patrol.pull;
|
|
7971
|
+
if (!target)
|
|
7972
|
+
return false;
|
|
7973
|
+
const slash = target.indexOf("/");
|
|
7974
|
+
const remote = target.slice(0, slash);
|
|
7975
|
+
const branch = target.slice(slash + 1);
|
|
7976
|
+
const source = resolve9(root2, config.app.source);
|
|
7977
|
+
try {
|
|
7978
|
+
if (await git4(source, "status", "--porcelain", "--untracked-files=no")) {
|
|
7979
|
+
onLog?.(`Did not pull ${target}: the source repository has uncommitted changes.`);
|
|
7980
|
+
return false;
|
|
7981
|
+
}
|
|
7982
|
+
await git4(source, "fetch", "-q", remote, `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`);
|
|
7983
|
+
await git4(source, "checkout", "-q", "--detach", `${remote}/${branch}`);
|
|
7984
|
+
onLog?.(`Pulled ${target} at ${await git4(source, "rev-parse", "--short", "HEAD")}.`);
|
|
7985
|
+
return true;
|
|
7986
|
+
} catch (error) {
|
|
7987
|
+
const reason = error.stderr?.trim() || error.message;
|
|
7988
|
+
onLog?.(`Did not pull ${target}: ${reason}. The cycle uses the current checkout.`);
|
|
7989
|
+
return false;
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
async function sourceCommit(root2, config) {
|
|
7993
|
+
const source = resolve9(root2, config.app.source);
|
|
7994
|
+
try {
|
|
7995
|
+
if (await git4(source, "status", "--porcelain", "--untracked-files=no"))
|
|
7996
|
+
return void 0;
|
|
7997
|
+
return await git4(source, "rev-parse", "HEAD");
|
|
7998
|
+
} catch {
|
|
7999
|
+
return void 0;
|
|
8000
|
+
}
|
|
8001
|
+
}
|
|
8002
|
+
function alive(pid) {
|
|
8003
|
+
try {
|
|
8004
|
+
process.kill(pid, 0);
|
|
8005
|
+
return true;
|
|
8006
|
+
} catch {
|
|
8007
|
+
return false;
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
function wait(minutes) {
|
|
8011
|
+
return new Promise((done) => {
|
|
8012
|
+
const finish = () => {
|
|
8013
|
+
clearTimeout(timer);
|
|
8014
|
+
process.off("SIGINT", finish);
|
|
8015
|
+
process.off("SIGTERM", finish);
|
|
8016
|
+
done();
|
|
8017
|
+
};
|
|
8018
|
+
const timer = setTimeout(finish, minutes * 6e4);
|
|
8019
|
+
process.once("SIGINT", finish);
|
|
8020
|
+
process.once("SIGTERM", finish);
|
|
8021
|
+
});
|
|
8022
|
+
}
|
|
7955
8023
|
async function runPatrol(options) {
|
|
7956
8024
|
const { root: root2, config } = options;
|
|
7957
8025
|
const workspace = new Workspace(root2);
|
|
8026
|
+
const previous = (await workspace.readAgents()).patrol;
|
|
8027
|
+
if (previous?.state === "running" && previous.pid && previous.pid !== process.pid && alive(previous.pid)) {
|
|
8028
|
+
options.onLog?.(`A patrol already runs (pid ${previous.pid}): did not start another.`);
|
|
8029
|
+
return;
|
|
8030
|
+
}
|
|
7958
8031
|
const runtime = options.createRuntime ?? createRuntime;
|
|
7959
8032
|
let interrupted = false;
|
|
7960
8033
|
let activeSession;
|
|
@@ -7964,6 +8037,11 @@ async function runPatrol(options) {
|
|
|
7964
8037
|
activeSession.cancelled = true;
|
|
7965
8038
|
}
|
|
7966
8039
|
};
|
|
8040
|
+
const idle = () => workspace.setPatrol({
|
|
8041
|
+
state: "stopped",
|
|
8042
|
+
nextAt: new Date(Date.now() + config.agents.patrol.intervalMinutes * 6e4).toISOString()
|
|
8043
|
+
});
|
|
8044
|
+
let tested = options.force ? void 0 : previous?.commit;
|
|
7967
8045
|
process.on("SIGINT", onSignal);
|
|
7968
8046
|
process.on("SIGTERM", onSignal);
|
|
7969
8047
|
try {
|
|
@@ -7971,10 +8049,24 @@ async function runPatrol(options) {
|
|
|
7971
8049
|
if (config.agents.patrol.cycles && cycle > config.agents.patrol.cycles) {
|
|
7972
8050
|
break;
|
|
7973
8051
|
}
|
|
8052
|
+
if (cycle > 1) {
|
|
8053
|
+
await wait(config.agents.patrol.intervalMinutes);
|
|
8054
|
+
if (interrupted)
|
|
8055
|
+
break;
|
|
8056
|
+
}
|
|
7974
8057
|
await workspace.setPatrol({ state: "running", cycle, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8058
|
+
await pullSource(root2, config, options.onLog);
|
|
7975
8059
|
if (config.agents.github.enabled)
|
|
7976
8060
|
await syncGitHub(root2, config, { onLog: options.onLog });
|
|
7977
8061
|
await cleanWorktrees(root2, config, { onLog: options.onLog });
|
|
8062
|
+
const commit = await sourceCommit(root2, config);
|
|
8063
|
+
if (commit && commit === tested) {
|
|
8064
|
+
options.onLog?.(`No new commit since the last cycle (${commit.slice(0, 7)}): skipped the cycle.`);
|
|
8065
|
+
await idle();
|
|
8066
|
+
if (options.once)
|
|
8067
|
+
break;
|
|
8068
|
+
continue;
|
|
8069
|
+
}
|
|
7978
8070
|
for (const role of ["explorer", "judge", "fixer"]) {
|
|
7979
8071
|
if (!config.agents[role].enabled) {
|
|
7980
8072
|
await workspace.setAgentStatus(role, { state: "off" });
|
|
@@ -8044,26 +8136,17 @@ async function runPatrol(options) {
|
|
|
8044
8136
|
});
|
|
8045
8137
|
if (!interrupted && config.agents.github.enabled)
|
|
8046
8138
|
await syncGitHub(root2, config, { onLog: options.onLog });
|
|
8047
|
-
|
|
8048
|
-
|
|
8139
|
+
if (!interrupted && commit) {
|
|
8140
|
+
tested = commit;
|
|
8141
|
+
await workspace.setPatrol({ commit });
|
|
8142
|
+
}
|
|
8143
|
+
await idle();
|
|
8049
8144
|
if (options.once || interrupted) {
|
|
8050
8145
|
break;
|
|
8051
8146
|
}
|
|
8052
|
-
await new Promise((done) => {
|
|
8053
|
-
const finish = () => {
|
|
8054
|
-
clearTimeout(timer);
|
|
8055
|
-
process.off("SIGINT", stop);
|
|
8056
|
-
process.off("SIGTERM", stop);
|
|
8057
|
-
done();
|
|
8058
|
-
};
|
|
8059
|
-
const timer = setTimeout(finish, config.agents.patrol.intervalMinutes * 6e4);
|
|
8060
|
-
const stop = () => finish();
|
|
8061
|
-
process.once("SIGINT", stop);
|
|
8062
|
-
process.once("SIGTERM", stop);
|
|
8063
|
-
});
|
|
8064
8147
|
}
|
|
8065
8148
|
} finally {
|
|
8066
|
-
await workspace.setPatrol({ state: "stopped" });
|
|
8149
|
+
await workspace.setPatrol({ state: "stopped", nextAt: void 0 });
|
|
8067
8150
|
process.off("SIGINT", onSignal);
|
|
8068
8151
|
process.off("SIGTERM", onSignal);
|
|
8069
8152
|
}
|
|
@@ -8243,9 +8326,9 @@ var JEV_ROUTES2 = {
|
|
|
8243
8326
|
};
|
|
8244
8327
|
var CLI_LABELS = { claude: "Claude Code", codex: "Codex", kimi: "Kimi CLI", pi: "pi" };
|
|
8245
8328
|
var run = (command2, args2) => spawnSync2(command2, args2, { encoding: "utf8", timeout: 1e4 }).stdout ?? "";
|
|
8246
|
-
function detectProviders(env = process.env,
|
|
8329
|
+
function detectProviders(env = process.env, exec6 = run) {
|
|
8247
8330
|
const found = CLI_AGENTS.filter((agent) => onPath(agent, env));
|
|
8248
|
-
const piList = found.includes("pi") ?
|
|
8331
|
+
const piList = found.includes("pi") ? exec6("pi", ["list"]) : "";
|
|
8249
8332
|
const piWithoutMcp = found.includes("pi") && !piList.includes("pi-mcp-adapter");
|
|
8250
8333
|
return {
|
|
8251
8334
|
clis: found.filter((agent) => agent !== "pi" || !piWithoutMcp),
|
|
@@ -9215,7 +9298,7 @@ function bodyMentions(body, selector) {
|
|
|
9215
9298
|
async function stopProcess(child) {
|
|
9216
9299
|
if (child.exitCode !== null || child.pid === void 0)
|
|
9217
9300
|
return;
|
|
9218
|
-
const done = new Promise((
|
|
9301
|
+
const done = new Promise((resolve11) => child.once("exit", () => resolve11()));
|
|
9219
9302
|
try {
|
|
9220
9303
|
process.kill(-child.pid, "SIGTERM");
|
|
9221
9304
|
} catch {
|
|
@@ -10241,7 +10324,7 @@ function formatRunSummary(result3) {
|
|
|
10241
10324
|
// ../dashboard/dist/server.js
|
|
10242
10325
|
import { createServer as createServer2 } from "node:http";
|
|
10243
10326
|
import { createReadStream, existsSync as existsSync10, realpathSync, statSync as statSync2, watch } from "node:fs";
|
|
10244
|
-
import { extname, join as join18, relative as relative4, resolve as
|
|
10327
|
+
import { extname, join as join18, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
|
|
10245
10328
|
import { fileURLToPath } from "node:url";
|
|
10246
10329
|
|
|
10247
10330
|
// ../dashboard/dist/project.js
|
|
@@ -10859,7 +10942,7 @@ function hostOf(href) {
|
|
|
10859
10942
|
}
|
|
10860
10943
|
|
|
10861
10944
|
// ../dashboard/dist/server.js
|
|
10862
|
-
var UI_DIR =
|
|
10945
|
+
var UI_DIR = resolve10(fileURLToPath(new URL(".", import.meta.url)), "..", "src", "ui");
|
|
10863
10946
|
var MIME = {
|
|
10864
10947
|
".html": "text/html; charset=utf-8",
|
|
10865
10948
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -10870,7 +10953,7 @@ var MIME = {
|
|
|
10870
10953
|
".json": "application/json; charset=utf-8"
|
|
10871
10954
|
};
|
|
10872
10955
|
async function startDashboard(options) {
|
|
10873
|
-
const root2 =
|
|
10956
|
+
const root2 = resolve10(options.root);
|
|
10874
10957
|
const host = options.host ?? "127.0.0.1";
|
|
10875
10958
|
const reader = new ProjectReader(root2);
|
|
10876
10959
|
const agents = new AgentReader(root2);
|
|
@@ -10965,7 +11048,7 @@ async function handle2(req, res, ctx) {
|
|
|
10965
11048
|
return serveUi(res, path);
|
|
10966
11049
|
}
|
|
10967
11050
|
function resolveArtifactPath(root2, requested) {
|
|
10968
|
-
const candidate =
|
|
11051
|
+
const candidate = resolve10(root2, requested);
|
|
10969
11052
|
if (!existsSync10(candidate))
|
|
10970
11053
|
return void 0;
|
|
10971
11054
|
let real;
|
|
@@ -10998,7 +11081,7 @@ function serveArtifact(res, root2, requested) {
|
|
|
10998
11081
|
}
|
|
10999
11082
|
function serveUi(res, path) {
|
|
11000
11083
|
const name = path === "/" ? "index.html" : path.replace(/^\//, "");
|
|
11001
|
-
const file =
|
|
11084
|
+
const file = resolve10(UI_DIR, name);
|
|
11002
11085
|
if (!file.startsWith(UI_DIR) || !existsSync10(file) || !statSync2(file).isFile()) {
|
|
11003
11086
|
return serveUi(res, "/");
|
|
11004
11087
|
}
|
|
@@ -11137,8 +11220,8 @@ function parseAgentFlags(command2, args2) {
|
|
|
11137
11220
|
throw new ConfigError(`Unknown agent command ${command2}`);
|
|
11138
11221
|
for (let index = 0; index < args2.length; index++) {
|
|
11139
11222
|
const flag2 = args2[index];
|
|
11140
|
-
if (command2 === "patrol" && flag2 === "--once") {
|
|
11141
|
-
flags.
|
|
11223
|
+
if (command2 === "patrol" && (flag2 === "--once" || flag2 === "--force")) {
|
|
11224
|
+
flags[flag2.slice(2)] = true;
|
|
11142
11225
|
continue;
|
|
11143
11226
|
}
|
|
11144
11227
|
if (command2 === "publish" && flag2 === "--dry-run") {
|
|
@@ -11200,7 +11283,7 @@ async function runAgentCommand(command2, args2, root2, config, log) {
|
|
|
11200
11283
|
log(line);
|
|
11201
11284
|
const workspace = new Workspace(root2);
|
|
11202
11285
|
if (command2 === "patrol") {
|
|
11203
|
-
await runPatrol({ root: root2, config, once: Boolean(flags.once), onLog: log });
|
|
11286
|
+
await runPatrol({ root: root2, config, once: Boolean(flags.once), force: Boolean(flags.force), onLog: log });
|
|
11204
11287
|
return;
|
|
11205
11288
|
}
|
|
11206
11289
|
if (command2 === "publish") {
|
|
@@ -11449,7 +11532,7 @@ var [command, ...args] = process.argv.slice(2);
|
|
|
11449
11532
|
var root = findProjectRoot(process.cwd());
|
|
11450
11533
|
function version() {
|
|
11451
11534
|
if (true)
|
|
11452
|
-
return "0.1.
|
|
11535
|
+
return "0.1.1";
|
|
11453
11536
|
try {
|
|
11454
11537
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
11455
11538
|
} catch {
|
package/package.json
CHANGED
package/src/ui/app.js
CHANGED
|
@@ -562,7 +562,7 @@ function renderOverview() {
|
|
|
562
562
|
data.patrol?.nextAt ? el('p', { class: 'muted',
|
|
563
563
|
text: `Next patrol at ${new Date(data.patrol.nextAt).toLocaleTimeString([], {
|
|
564
564
|
hour: '2-digit', minute: '2-digit',
|
|
565
|
-
})}`,
|
|
565
|
+
})}${data.patrol.commit ? ` · last tested commit ${data.patrol.commit.slice(0, 7)}` : ''}`,
|
|
566
566
|
}) : null,
|
|
567
567
|
]),
|
|
568
568
|
]);
|