bugpatrol 0.4.4 → 0.4.5

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.
Files changed (2) hide show
  1. package/dist/bin.js +76 -13
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -24382,6 +24382,7 @@ SKIP WHAT IS ALREADY ON GITHUB
24382
24382
  list_items also shows the Bugpatrol PRs and issues on GitHub. Other machines and earlier runs filed some of them.
24383
24383
  - If one of them already covers an item (the same problem, or a PR that changes the same code for it), skip the
24384
24384
  item. Reason: "Duplicate of #<number>".
24385
+ - A PR item's own issue, which list_items names, never counts: the PR closes it, so publish the item.
24385
24386
  - If a closed PR made the same change, skip the item. Reason: "Repeats the rejected PR #<number>".
24386
24387
  Write a plain, short summary of 2\u20135 sentences: what is wrong, for whom, and for a PR what changed and how Bugpatrol checked it.
24387
24388
  Do not repeat the full report. Bugpatrol adds screenshots, steps, the fix, and verification. Call finish when done.
@@ -40542,6 +40543,10 @@ var rank2 = { cosmetic: 0, minor: 1, major: 2, critical: 3 };
40542
40543
  function publishable(fix) {
40543
40544
  return fix.status === "verified" || fix.status === "proposed" && !judgedRetests(fix.retests).length;
40544
40545
  }
40546
+ var ownIssue = (item) => item.kind === "pr" ? item.issue.github?.number : void 0;
40547
+ var ownIssueNote = (item) => ownIssue(item) ? `This item's own GitHub issue is #${ownIssue(item)}. It is not a duplicate; this PR closes it.` : "";
40548
+ var citesOwnIssue = (item, text3) => ownIssue(item) !== void 0 && new RegExp(`#${ownIssue(item)}\\b`).test(text3);
40549
+ var skipHolds = (item, skip) => skip !== void 0 && !citesOwnIssue(item, skip.reason);
40545
40550
  var result2 = (text3, isError = false) => ({ content: [{ type: "text", text: text3 }], isError });
40546
40551
  var schema4 = (properties = {}, required = []) => ({
40547
40552
  type: "object",
@@ -40604,6 +40609,27 @@ async function reuseFiled(gh, repo, workspace, issues, outcomes, onLog) {
40604
40609
  }
40605
40610
  return handled;
40606
40611
  }
40612
+ async function recheckFalseSkips(gh, repo, items, onLog) {
40613
+ const kept = [];
40614
+ for (const item of items) {
40615
+ const own = ownIssue(item);
40616
+ if (!item.issue.publishSkipped || own === void 0) {
40617
+ kept.push(item);
40618
+ continue;
40619
+ }
40620
+ try {
40621
+ const { state: state2, closedByPullRequestsReferences: prs = [] } = JSON.parse(await gh(["issue", "view", String(own), "--repo", repo, "--json", "state,closedByPullRequestsReferences"]));
40622
+ if (state2 === "CLOSED" || prs.length) {
40623
+ onLog?.(`${item.issue.id} stays skipped: #${own} is ${prs.length ? `closed by PR #${prs[0].number}` : "closed"}.`);
40624
+ continue;
40625
+ }
40626
+ kept.push(item);
40627
+ } catch (error) {
40628
+ onLog?.(`GitHub lookup of #${own} failed, so ${item.issue.id} waits for the next run: ${String(error)}`);
40629
+ }
40630
+ }
40631
+ return kept;
40632
+ }
40607
40633
  async function runPublisher(root2, config, deps = {}) {
40608
40634
  if (!config.agents.github.enabled && !deps.dryRun)
40609
40635
  return [];
@@ -40612,14 +40638,18 @@ async function runPublisher(root2, config, deps = {}) {
40612
40638
  const fixes = new Map((await workspace.listFixes()).map((fix) => [fix.issueId, fix]));
40613
40639
  const issues = (await workspace.listIssues()).filter((issue) => !deps.issueIds || deps.issueIds.includes(issue.id));
40614
40640
  let items = issues.flatMap((issue) => {
40615
- if (issue.status === "dismissed" || issue.publishSkipped || issue.fixRejected)
40641
+ if (issue.status === "dismissed" || issue.fixRejected)
40616
40642
  return [];
40617
40643
  const found = fixes.get(issue.id);
40618
40644
  const fix = found?.external ? void 0 : found;
40619
40645
  if (fix?.status === "rejected")
40620
40646
  return [];
40621
- if (fix && !fix.pr && publishable(fix))
40622
- return [{ kind: "pr", issue, fix }];
40647
+ if (fix && !fix.pr && publishable(fix)) {
40648
+ const item = { kind: "pr", issue, fix };
40649
+ return skipHolds(item, issue.publishSkipped) ? [] : [item];
40650
+ }
40651
+ if (issue.publishSkipped)
40652
+ return [];
40623
40653
  if (!issue.github && !["fixed", "dismissed"].includes(issue.status) && rank2[issue.severity] >= rank2[config.agents.github.issueMinSeverity] && (!fix || ["declined", "failed"].includes(fix.status)))
40624
40654
  return [{ kind: "issue", issue, fix }];
40625
40655
  return [];
@@ -40639,7 +40669,7 @@ async function runPublisher(root2, config, deps = {}) {
40639
40669
  }
40640
40670
  resolved = { repo: ready.repo, defaultBranch: ready.defaultBranch };
40641
40671
  const handled = await reuseFiled(gh, resolved.repo, workspace, [...regressions, ...items.filter((item) => item.kind === "issue").map((item) => item.issue)], outcomes, deps.onLog);
40642
- items = items.filter((item) => item.kind === "pr" || !handled.has(item.issue.id));
40672
+ items = await recheckFalseSkips(gh, resolved.repo, items.filter((item) => item.kind === "pr" || !handled.has(item.issue.id)), deps.onLog);
40643
40673
  if (!items.length)
40644
40674
  return outcomes;
40645
40675
  }
@@ -40652,6 +40682,8 @@ async function runPublisher(root2, config, deps = {}) {
40652
40682
  let steps = 0;
40653
40683
  let costUsd = 0;
40654
40684
  let status = "finished";
40685
+ let ending = "finished without publishing or skipping it";
40686
+ const publishErrors = /* @__PURE__ */ new Map();
40655
40687
  let assetsReady = false;
40656
40688
  let labelsReady = false;
40657
40689
  const get = (id2) => items.find((item) => item.issue.id === id2 && !done.has(id2));
@@ -40699,7 +40731,7 @@ async function runPublisher(root2, config, deps = {}) {
40699
40731
  description: "List issues ready to publish.",
40700
40732
  inputSchema: schema4(),
40701
40733
  async run() {
40702
- const list3 = items.filter((item) => !done.has(item.issue.id)).map((item) => `${item.issue.id} [${item.kind}, ${item.issue.severity}, fix: ${item.fix?.status ?? "none"}, retest: ${item.fix?.retests?.at(-1)?.outcome ?? "none"}]: ${item.issue.title}`).join("\n");
40734
+ const list3 = items.filter((item) => !done.has(item.issue.id)).map((item) => `${item.issue.id} [${item.kind}, ${item.issue.severity}, fix: ${item.fix?.status ?? "none"}, retest: ${item.fix?.retests?.at(-1)?.outcome ?? "none"}]: ${item.issue.title} ${ownIssueNote(item)}`.trimEnd()).join("\n");
40703
40735
  const style = recentTitles.length ? `
40704
40736
 
40705
40737
  RECENT MERGED PR TITLES IN THIS REPO (use the same types and scopes)
@@ -40728,7 +40760,8 @@ ${item.issue.body}
40728
40760
  Judgement: ${item.issue.judgement.reason}
40729
40761
  Fix: ${item.fix?.summary ?? "(none)"}
40730
40762
  Diff: ${item.fix?.diffStat ?? "(none)"}
40731
- Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
40763
+ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}
40764
+ ${ownIssueNote(item)}`;
40732
40765
  const before = [...pathsFor({ ...item, fix: void 0 }, candidates)].slice(0, 4);
40733
40766
  const after = (last?.captures?.length ? last.captures.map((shot) => shot.after) : [last?.after]).filter((path) => Boolean(path)).slice(0, 4);
40734
40767
  const content = [
@@ -40783,7 +40816,7 @@ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
40783
40816
  try {
40784
40817
  const latestIssue = await workspace.readIssue(item.issue.id);
40785
40818
  const latestFix = item.fix && await workspace.readFix(item.fix.id);
40786
- if (!latestIssue || latestIssue.status === "dismissed" || latestIssue.publishSkipped || latestIssue.fixRejected || latestFix?.status === "rejected" || item.kind === "issue" && latestIssue.github || item.kind === "pr" && (!latestFix || latestFix.pr))
40819
+ if (!latestIssue || latestIssue.status === "dismissed" || skipHolds(item, latestIssue.publishSkipped) || latestIssue.fixRejected || latestFix?.status === "rejected" || item.kind === "issue" && latestIssue.github || item.kind === "pr" && (!latestFix || latestFix.pr))
40787
40820
  return result2("This item was dismissed or already handled.", true);
40788
40821
  Object.assign(item.issue, latestIssue);
40789
40822
  if (item.fix && latestFix)
@@ -40842,6 +40875,10 @@ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
40842
40875
  at: (/* @__PURE__ */ new Date()).toISOString()
40843
40876
  };
40844
40877
  await workspace.saveFix(item.fix);
40878
+ if (item.issue.publishSkipped) {
40879
+ item.issue.publishSkipped = void 0;
40880
+ await workspace.saveIssue(item.issue);
40881
+ }
40845
40882
  } else {
40846
40883
  const opened = await createIssue(gh, {
40847
40884
  repo: resolved.repo,
@@ -40866,6 +40903,7 @@ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
40866
40903
  } catch (error) {
40867
40904
  if (item.fix?.commit)
40868
40905
  await workspace.saveFix(item.fix);
40906
+ publishErrors.set(item.issue.id, String(error));
40869
40907
  return result2(String(error), true);
40870
40908
  }
40871
40909
  }
@@ -40881,8 +40919,10 @@ Retest: ${last?.outcome ?? "none"} \u2014 ${last?.reason ?? ""}`;
40881
40919
  const reason2 = String(input.reason ?? "").trim();
40882
40920
  if (!reason2)
40883
40921
  return result2("A reason is required.", true);
40922
+ if (citesOwnIssue(item, reason2))
40923
+ return result2(`${ownIssueNote(item)} Publish it instead of skipping.`, true);
40884
40924
  const latest = await workspace.readIssue(item.issue.id);
40885
- if (!latest || latest.status === "dismissed" || latest.publishSkipped)
40925
+ if (!latest || latest.status === "dismissed" || skipHolds(item, latest.publishSkipped))
40886
40926
  return result2("This item was dismissed or already handled.", true);
40887
40927
  Object.assign(item.issue, latest);
40888
40928
  item.issue.publishSkipped = { reason: vars.redact(reason2), at: (/* @__PURE__ */ new Date()).toISOString() };
@@ -40922,15 +40962,29 @@ ${items.map((item) => `${item.issue.id}: ${item.issue.title}`).join("\n")}`),
40922
40962
  }, session.emit);
40923
40963
  steps = outcome2.steps;
40924
40964
  costUsd = outcome2.costUsd;
40965
+ if (outcome2.stop !== "done")
40966
+ ending = `stopped early (${outcome2.stop})`;
40925
40967
  } catch (error) {
40926
40968
  status = "failed";
40969
+ ending = `failed: ${String(error)}`;
40927
40970
  session.emit({ kind: "error", summary: `GitHub publishing failed: ${String(error)}` });
40928
40971
  deps.onLog?.(`GitHub publishing failed: ${String(error)}`);
40929
40972
  } finally {
40973
+ const left = session.cancelled ? [] : items.filter((item) => !done.has(item.issue.id));
40974
+ for (const { issue } of left) {
40975
+ const error = publishErrors.get(issue.id);
40976
+ outcomes.push({
40977
+ issueId: issue.id,
40978
+ kind: "unpublished",
40979
+ reason: vars.redact(`The publisher ${ending}.${error ? ` Its publish failed: ${error}` : ""}`)
40980
+ });
40981
+ }
40982
+ if (left.length)
40983
+ status = "failed";
40930
40984
  const prs = outcomes.filter((item) => item.kind === "pr").length;
40931
40985
  const issues2 = outcomes.filter((item) => item.kind === "issue").length;
40932
40986
  const skipped = outcomes.filter((item) => item.kind === "skipped").length;
40933
- const summary = `${deps.dryRun ? "Drafted" : "Published"} ${prs} PR(s) and ${issues2} issue(s); skipped ${skipped}`;
40987
+ const summary = `${deps.dryRun ? "Drafted" : "Published"} ${prs} PR(s) and ${issues2} issue(s); skipped ${skipped}${left.length ? `; ${left.length} left unpublished` : ""}`;
40934
40988
  await workspace.endSession(record.id, { summary, status, steps, costUsd });
40935
40989
  session.emit({ kind: "session-end", summary });
40936
40990
  await session.idle(costUsd);
@@ -42223,14 +42277,19 @@ async function runPatrol(options) {
42223
42277
  problems.push(`Cycle ${cycle}: the retest of ${fix.issueId} failed: ${last.reason}`);
42224
42278
  }
42225
42279
  const cycleFailed = problems.length > before;
42226
- if (!interrupted && config.agents.github.enabled)
42227
- await runPublisher(root2, config, {
42280
+ if (!interrupted && config.agents.github.enabled) {
42281
+ const outcomes = await runPublisher(root2, config, {
42228
42282
  createRuntime: options.createRuntime,
42229
42283
  onLog: options.onLog,
42230
42284
  onSession: (session) => {
42231
42285
  activeSession = session;
42232
42286
  }
42233
42287
  });
42288
+ for (const item of outcomes.filter((outcome2) => outcome2.kind === "unpublished")) {
42289
+ problems.push(`Cycle ${cycle}: ${item.issueId} was not published: ${item.reason}`);
42290
+ options.onLog?.(problems.at(-1));
42291
+ }
42292
+ }
42234
42293
  if (!interrupted && config.agents.github.enabled) {
42235
42294
  const ci = await watchCi(root2, config, {
42236
42295
  createRuntime: options.createRuntime,
@@ -47350,11 +47409,15 @@ ${problems.map((problem) => `- ${problem.split("\n")[0]}`).join("\n")}`);
47350
47409
  return;
47351
47410
  }
47352
47411
  for (const item of outcomes) {
47353
- const kind = item.kind === "pr" ? flags.dryRun ? "PR draft" : "PR" : item.kind === "issue" ? flags.dryRun ? "issue draft" : "issue" : "skipped";
47412
+ const kind = item.kind === "pr" ? flags.dryRun ? "PR draft" : "PR" : item.kind === "issue" ? flags.dryRun ? "issue draft" : "issue" : item.kind;
47354
47413
  log(` ${kind.padEnd(11)} ${item.issueId} ${item.url ?? item.reason ?? ""}`);
47355
47414
  }
47356
47415
  const count = (kind) => outcomes.filter((item) => item.kind === kind).length;
47357
47416
  log(`${flags.dryRun ? "Wrote" : "Opened"} ${count("pr")} PR(s) and ${count("issue")} issue(s); skipped ${count("skipped")}.`);
47417
+ if (count("unpublished")) {
47418
+ log(`${count("unpublished")} item(s) were not published. Run \`bugpatrol publish\` again after you fix the cause.`);
47419
+ process.exitCode = 1;
47420
+ }
47358
47421
  return;
47359
47422
  }
47360
47423
  if (command === "review") {
@@ -49330,7 +49393,7 @@ legacyEnv(process.env);
49330
49393
  var root = findProjectRoot(process.cwd());
49331
49394
  function version() {
49332
49395
  if (true)
49333
- return "0.4.4";
49396
+ return "0.4.5";
49334
49397
  try {
49335
49398
  return JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf8")).version;
49336
49399
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bugpatrol",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "AI agents that explore your app, find bugs, and fix them.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",