diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cb7e9..247f752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Until the first release, changes are recorded under [Unreleased]. ### Fixed +- `gather.mjs --pr` keeps a PR that a bot app opened (such as `app/astral-automations-bot` on astral-sh/uv), instead of returning an empty dossier; only release-automation PRs still drop. PRs found by `--search` or issue links still drop bot authors. Test in `dev/test/gather.test.mjs`. - The motion page declares its charset, so arrows and dots no longer come out garbled when `motion.html` is opened from disk. - Margin notes: on a narrow screen a note is set off from the text by a rule, so it doesn't read as body text; on a wide one it sits level with its line without making the paragraph taller (it was floated, and a paragraph in the chapter grid grew to hold it, leaving a gap). `lint.mjs` flags a note placed mid-paragraph or mid-item, which splits the text on a phone; the example's one note moves to the end of its item. - The `delta` component's before and after values lost their layout when the margin note class was renamed; the template styles `.delta .side` again. diff --git a/dev/test/gather.test.mjs b/dev/test/gather.test.mjs index 2627a6c..17bfb32 100644 --- a/dev/test/gather.test.mjs +++ b/dev/test/gather.test.mjs @@ -383,6 +383,22 @@ test("a release PR stays when asked, a zero window skips nearby work, and a one- assert.match(md, /\| when \(UTC\) \| what \| ref \| title \|/); })); +test("a PR named with --pr stays when a bot app opened it, and the same PR found by search drops", () => serial(async () => { + // astral-sh/uv ships stacked work from app/astral-automations-bot; --pr 22619 used to come back empty. + const botPr = { ...ghFx.prs["acme/app#120"], number: 140, title: "Implement RemoteSource for SafeUrl", author: { login: "app/acme-automations", is_bot: true } }; + const base = fixtureCommand(); + const command = (cmd, args) => { + if (cmd === "gh" && args[0] === "pr" && args[1] === "view" && args[2] === "140") return JSON.stringify(botPr); + if (cmd === "gh" && args[0] === "api" && args[1] === "repos/acme/app/pulls/140/comments") return "[]"; + if (cmd === "gh" && args[0] === "pr" && args[1] === "list" && args.includes("safe url")) return JSON.stringify([{ number: 140 }]); + return base(cmd, args); + }; + const named = await gather(["--out", mkdtempSync(join(tmpdir(), "gather-app-")), "--repo", "acme/app", "--pr", "140", "--window", "0"], { command }); + assert.deepEqual(named.prs.map((p) => p.number), [140]); + const found = await gather(["--out", mkdtempSync(join(tmpdir(), "gather-app-")), "--repo", "acme/app", "--search", "safe url", "--window", "0"], { command }); + assert.deepEqual(found.prs.map((p) => p.number), []); +})); + test("search syntax in --prior passes through, and a failed visibility lookup is unknown", () => serial(async () => { const calls = []; const dir = mkdtempSync(join(tmpdir(), "gather-prior-")); diff --git a/skills/buildnotes/scripts/gather.mjs b/skills/buildnotes/scripts/gather.mjs index b4c8b04..208f56d 100644 --- a/skills/buildnotes/scripts/gather.mjs +++ b/skills/buildnotes/scripts/gather.mjs @@ -58,7 +58,8 @@ const SUMMARY_BOTS = /^(coderabbitai|copilot-pull-request-reviewer|sourcery-ai|g const OTHER_BOTS = /^(cursor|cursoragent|chatgpt-codex-connector|codex|copilot|copilot-swe-agent|devin-ai-integration|claude|graphite-app|linear)$/i; const BADGE_HOSTS = /shields\.io|badge|coderabbit\.ai|vercel\.com\/button|codecov\.io|avatars\.githubusercontent|open-in-|deploy-button/i; // Release-automation PRs (version bumps) carry no story. -const isBotPr = (p) => /^app\/|\[bot\]$/.test(p.author?.login ?? "") || /^chore(\(release\))?: (version packages|release)/i.test(p.title); +const isReleasePr = (p) => /^chore(\(release\))?: (version packages|release)/i.test(p.title); +const isBotPr = (p) => /^app\/|\[bot\]$/.test(p.author?.login ?? "") || isReleasePr(p); // Story-signal vocabularies. const FEEDBACK_RE = /\b(review(er)?|found|flagged|adversarial|caught|race|CSRF|vulnerab\w*|bug)\b/i; @@ -113,7 +114,9 @@ async function addIssue(n, depth, { follow = true } = {}) { for (const p of mentioned) if (new RegExp(`#${n}\\b`).test(p.body ?? "")) addPr(p.number); } -function addPr(n, r = repo) { +// A PR named with --pr is kept even when a bot app opened it (some teams ship real work through +// one); only release-automation PRs still drop. Bot PRs found by search or issue links drop. +function addPr(n, r = repo, { named = false } = {}) { n = Number(n); const key = `${r}#${n}`; if (seenPr.has(key)) return; @@ -128,7 +131,7 @@ function addPr(n, r = repo) { console.error(`pr ${key}: fetch failed, skipped`); return; } - if (isBotPr(p) && !args["keep-bot-prs"]) return; + if ((named ? isReleasePr(p) : isBotPr(p)) && !args["keep-bot-prs"]) return; const inline = ghJsonSafe(["api", `repos/${r}/pulls/${n}/comments`, "--paginate"]) ?? []; const threads = threadsOf(inline); @@ -927,7 +930,7 @@ async function run(argv) { ({ renames, headFiles } = loadRenames()); for (const n of list(args.issue)) await addIssue(n, 0); - for (const n of list(args.pr)) addPr(n); + for (const n of list(args.pr)) addPr(n, repo, { named: true }); // Issues a selected PR closes, or says it is part of ("Part of #12", "Refs #12"), often hold the // plan, the problem statement and its numbers. Pull their bodies, without widening the search to // every PR that mentions them. A number that turns out to be a PR is skipped.