From 4cf3038ac09130a605f648db51ad06576d32ecf3 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Thu, 9 Jul 2026 04:07:19 +0200 Subject: [PATCH 1/2] feat(miner-cli): add claim-ledger claim/release/list commands Wire the local SQLite claim ledger into gittensory-miner with CLI subcommands mirroring the portfolio-queue pattern so miners can record, release, and inspect soft claims from the shell. Closes #4290 Co-authored-by: Cursor --- .../gittensory-miner/bin/gittensory-miner.js | 5 + .../lib/claim-ledger-cli.d.ts | 55 ++++ .../gittensory-miner/lib/claim-ledger-cli.js | 269 ++++++++++++++++++ packages/gittensory-miner/lib/cli.js | 3 + packages/gittensory-miner/package.json | 2 +- test/unit/miner-claim-ledger-cli.test.ts | 261 +++++++++++++++++ 6 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 packages/gittensory-miner/lib/claim-ledger-cli.d.ts create mode 100644 packages/gittensory-miner/lib/claim-ledger-cli.js create mode 100644 test/unit/miner-claim-ledger-cli.test.ts diff --git a/packages/gittensory-miner/bin/gittensory-miner.js b/packages/gittensory-miner/bin/gittensory-miner.js index 3105ae8114..2690aea417 100755 --- a/packages/gittensory-miner/bin/gittensory-miner.js +++ b/packages/gittensory-miner/bin/gittensory-miner.js @@ -6,6 +6,7 @@ import { runLedgerCli } from "../lib/event-ledger-cli.js"; import { runManagePoll } from "../lib/manage-poll.js"; import { runManageStatus } from "../lib/manage-status.js"; import { runPlanCli } from "../lib/plan-store-cli.js"; +import { runClaimCli } from "../lib/claim-ledger-cli.js"; import { runQueueCli } from "../lib/portfolio-queue-cli.js"; import { runStateCli } from "../lib/run-state-cli.js"; import { runInit } from "../lib/laptop-init.js"; @@ -42,6 +43,10 @@ if (cliArgs[0] === "queue") { process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2))); } +if (cliArgs[0] === "claim") { + process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2))); +} + if (cliArgs[0] === "ledger") { process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2))); } diff --git a/packages/gittensory-miner/lib/claim-ledger-cli.d.ts b/packages/gittensory-miner/lib/claim-ledger-cli.d.ts new file mode 100644 index 0000000000..b029b8e4b5 --- /dev/null +++ b/packages/gittensory-miner/lib/claim-ledger-cli.d.ts @@ -0,0 +1,55 @@ +import type { ClaimEntry, ClaimLedger, ClaimStatus } from "./claim-ledger.js"; + +export type ParsedClaimClaimArgs = + | { + repoFullName: string; + issueNumber: number; + note: string | undefined; + json: boolean; + } + | { error: string }; + +export type ParsedClaimReleaseArgs = + | { + repoFullName: string; + issueNumber: number; + json: boolean; + } + | { error: string }; + +export type ParsedClaimListArgs = + | { + json: boolean; + repoFullName: string | null; + status: ClaimStatus | null; + } + | { error: string }; + +export function parseClaimClaimArgs(args: string[]): ParsedClaimClaimArgs; + +export function parseClaimReleaseArgs(args: string[]): ParsedClaimReleaseArgs; + +export function parseClaimListArgs(args: string[]): ParsedClaimListArgs; + +export function renderClaimsTable(entries: ClaimEntry[]): string; + +export function runClaimClaim( + args: string[], + options?: { openClaimLedger?: () => ClaimLedger }, +): number; + +export function runClaimRelease( + args: string[], + options?: { openClaimLedger?: () => ClaimLedger }, +): number; + +export function runClaimList( + args: string[], + options?: { openClaimLedger?: () => ClaimLedger }, +): number; + +export function runClaimCli( + subcommand: string | undefined, + args: string[], + options?: { openClaimLedger?: () => ClaimLedger }, +): number; diff --git a/packages/gittensory-miner/lib/claim-ledger-cli.js b/packages/gittensory-miner/lib/claim-ledger-cli.js new file mode 100644 index 0000000000..724d76d19a --- /dev/null +++ b/packages/gittensory-miner/lib/claim-ledger-cli.js @@ -0,0 +1,269 @@ +import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js"; + +const CLAIM_CLAIM_USAGE = + "Usage: gittensory-miner claim claim [--note ] [--json]"; +const CLAIM_RELEASE_USAGE = "Usage: gittensory-miner claim release [--json]"; +const CLAIM_LIST_USAGE = + "Usage: gittensory-miner claim list [--repo ] [--status active|released|expired] [--json]"; + +function parseRepoArg(value, usage) { + if (!value) return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; +} + +function parseIssueNumberArg(value, usage) { + if (!value) return { error: usage }; + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1) { + return { error: "issue number must be a positive integer." }; + } + return { issueNumber: parsed }; +} + +export function parseClaimClaimArgs(args) { + const options = { json: false, note: undefined }; + const positional = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--note") { + const note = args[index + 1]; + if (!note || note.startsWith("-")) { + return { error: CLAIM_CLAIM_USAGE }; + } + options.note = note; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length !== 2) { + return { error: CLAIM_CLAIM_USAGE }; + } + + const repo = parseRepoArg(positional[0], CLAIM_CLAIM_USAGE); + if ("error" in repo) return repo; + const issue = parseIssueNumberArg(positional[1], CLAIM_CLAIM_USAGE); + if ("error" in issue) return issue; + + return { + repoFullName: repo.repoFullName, + issueNumber: issue.issueNumber, + note: options.note, + json: options.json, + }; +} + +export function parseClaimReleaseArgs(args) { + const options = { json: false }; + const positional = []; + + for (const token of args) { + if (token === "--json") { + options.json = true; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length !== 2) { + return { error: CLAIM_RELEASE_USAGE }; + } + + const repo = parseRepoArg(positional[0], CLAIM_RELEASE_USAGE); + if ("error" in repo) return repo; + const issue = parseIssueNumberArg(positional[1], CLAIM_RELEASE_USAGE); + if ("error" in issue) return issue; + + return { + repoFullName: repo.repoFullName, + issueNumber: issue.issueNumber, + json: options.json, + }; +} + +export function parseClaimListArgs(args) { + const options = { json: false, repoFullName: null, status: null }; + const positional = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) { + return { error: CLAIM_LIST_USAGE }; + } + const repo = parseRepoArg(repoArg, CLAIM_LIST_USAGE); + if ("error" in repo) return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--status") { + const statusArg = args[index + 1]; + if (!statusArg || statusArg.startsWith("-")) { + return { error: CLAIM_LIST_USAGE }; + } + if (!CLAIM_STATUSES.includes(statusArg)) { + return { error: `status must be one of: ${CLAIM_STATUSES.join(", ")}.` }; + } + options.status = statusArg; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length > 0) { + return { error: CLAIM_LIST_USAGE }; + } + + return options; +} + +function display(value) { + if (value === null || value === undefined) return "-"; + return String(value); +} + +export function renderClaimsTable(entries) { + if (!Array.isArray(entries) || entries.length === 0) return "no claim ledger entries"; + const header = [ + "repo".padEnd(24), + "issue".padStart(6), + "status".padEnd(10), + "claimed-at".padEnd(24), + "note".padEnd(16), + ].join(" "); + const lines = entries.map((entry) => + [ + entry.repoFullName.padEnd(24), + display(entry.issueNumber).padStart(6), + entry.status.padEnd(10), + display(entry.claimedAt).padEnd(24), + display(entry.note).padEnd(16), + ].join(" "), + ); + return [header, ...lines].join("\n"); +} + +function withClaimLedger(options, run) { + const ownsLedger = options.openClaimLedger === undefined; + const claimLedger = (options.openClaimLedger ?? openClaimLedger)(); + try { + return run(claimLedger); + } finally { + if (ownsLedger) claimLedger.close(); + } +} + +export function runClaimClaim(args, options = {}) { + const parsed = parseClaimClaimArgs(args); + if ("error" in parsed) { + console.error(parsed.error); + return 2; + } + + try { + return withClaimLedger(options, (claimLedger) => { + const claim = claimLedger.claimIssue( + parsed.repoFullName, + parsed.issueNumber, + parsed.note, + ); + if (parsed.json) { + console.log(JSON.stringify({ claim }, null, 2)); + } else { + console.log(claim.status); + } + return 0; + }); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + return 2; + } +} + +export function runClaimRelease(args, options = {}) { + const parsed = parseClaimReleaseArgs(args); + if ("error" in parsed) { + console.error(parsed.error); + return 2; + } + + try { + return withClaimLedger(options, (claimLedger) => { + const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber); + if (!claim) { + console.error("claim_not_found"); + return 2; + } + if (parsed.json) { + console.log(JSON.stringify({ claim }, null, 2)); + } else { + console.log(claim.status); + } + return 0; + }); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + return 2; + } +} + +export function runClaimList(args, options = {}) { + const parsed = parseClaimListArgs(args); + if ("error" in parsed) { + console.error(parsed.error); + return 2; + } + + try { + return withClaimLedger(options, (claimLedger) => { + const filter = {}; + if (parsed.repoFullName !== null) filter.repoFullName = parsed.repoFullName; + if (parsed.status !== null) filter.status = parsed.status; + const claims = claimLedger.listClaims(filter); + if (parsed.json) { + console.log(JSON.stringify({ claims }, null, 2)); + } else { + console.log(renderClaimsTable(claims)); + } + return 0; + }); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + return 2; + } +} + +export function runClaimCli(subcommand, args, options = {}) { + if (subcommand === "claim") return runClaimClaim(args, options); + if (subcommand === "release") return runClaimRelease(args, options); + if (subcommand === "list") return runClaimList(args, options); + console.error(`Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`); + return 2; +} diff --git a/packages/gittensory-miner/lib/cli.js b/packages/gittensory-miner/lib/cli.js index dddf07a446..3051efcf5a 100644 --- a/packages/gittensory-miner/lib/cli.js +++ b/packages/gittensory-miner/lib/cli.js @@ -22,6 +22,9 @@ export function printHelp(input) { " gittensory-miner queue list [--repo ] [--json] List portfolio backlog rows", " gittensory-miner queue next [--json] Claim the highest-priority queued item", " gittensory-miner queue done [--json]", + " gittensory-miner claim claim [--note ] [--json]", + " gittensory-miner claim release [--json]", + " gittensory-miner claim list [--repo ] [--status active|released|expired] [--json]", " gittensory-miner ledger list [--repo ] [--since ] [--type ] [--json]", " gittensory-miner plan list [--status pending|running|completed|failed] [--json]", " gittensory-miner plan show [--json]", diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index ae2c366a7f..7aba5d88d3 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -31,7 +31,7 @@ "lib" ], "scripts": { - "build": "node --check bin/gittensory-miner.js && node --check lib/version.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js" + "build": "node --check bin/gittensory-miner.js && node --check lib/version.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger-cli.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js" }, "dependencies": { "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0" diff --git a/test/unit/miner-claim-ledger-cli.test.ts b/test/unit/miner-claim-ledger-cli.test.ts new file mode 100644 index 0000000000..e73bdb3fee --- /dev/null +++ b/test/unit/miner-claim-ledger-cli.test.ts @@ -0,0 +1,261 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + closeDefaultClaimLedger, + openClaimLedger, +} from "../../packages/gittensory-miner/lib/claim-ledger.js"; +import type { ClaimEntry } from "../../packages/gittensory-miner/lib/claim-ledger.d.ts"; +import { + parseClaimClaimArgs, + parseClaimListArgs, + parseClaimReleaseArgs, + renderClaimsTable, + runClaimClaim, + runClaimCli, + runClaimList, + runClaimRelease, +} from "../../packages/gittensory-miner/lib/claim-ledger-cli.js"; + +const roots: string[] = []; +const ledgers: Array<{ close(): void }> = []; + +function tempClaimLedger() { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-claim-ledger-cli-")); + roots.push(root); + const ledger = openClaimLedger(join(root, "claim-ledger.sqlite3")); + ledgers.push(ledger); + return ledger; +} + +afterEach(() => { + for (const ledger of ledgers.splice(0)) ledger.close(); + closeDefaultClaimLedger(); + vi.restoreAllMocks(); + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe("gittensory-miner claim ledger CLI (#4290)", () => { + it("parseClaimClaimArgs, parseClaimReleaseArgs, and parseClaimListArgs validate argv", () => { + expect(parseClaimClaimArgs(["acme/widgets", "42", "--note", "wip", "--json"])).toEqual({ + repoFullName: "acme/widgets", + issueNumber: 42, + note: "wip", + json: true, + }); + expect(parseClaimClaimArgs(["acme/widgets", "42"])).toEqual({ + repoFullName: "acme/widgets", + issueNumber: 42, + note: undefined, + json: false, + }); + expect(parseClaimClaimArgs(["acme/widgets"])).toEqual({ + error: expect.stringContaining("Usage: gittensory-miner claim claim"), + }); + expect(parseClaimClaimArgs(["acme", "42"])).toEqual({ + error: "Repository must be in owner/repo form.", + }); + expect(parseClaimClaimArgs(["acme/widgets", "0"])).toEqual({ + error: "issue number must be a positive integer.", + }); + expect(parseClaimClaimArgs(["acme/widgets", "42", "--note"])).toEqual({ + error: expect.stringContaining("Usage: gittensory-miner claim claim"), + }); + expect(parseClaimClaimArgs(["acme/widgets", "42", "--verbose"])).toEqual({ + error: "Unknown option: --verbose", + }); + + expect(parseClaimReleaseArgs(["acme/widgets", "7", "--json"])).toEqual({ + repoFullName: "acme/widgets", + issueNumber: 7, + json: true, + }); + expect(parseClaimReleaseArgs(["acme/widgets"])).toEqual({ + error: expect.stringContaining("Usage: gittensory-miner claim release"), + }); + expect(parseClaimReleaseArgs(["acme/widgets", "7", "--bad"])).toEqual({ + error: "Unknown option: --bad", + }); + + expect(parseClaimListArgs([])).toEqual({ + json: false, + repoFullName: null, + status: null, + }); + expect(parseClaimListArgs(["--repo", "acme/widgets", "--status", "active", "--json"])).toEqual({ + json: true, + repoFullName: "acme/widgets", + status: "active", + }); + expect(parseClaimListArgs(["--status", "bogus"])).toEqual({ + error: "status must be one of: active, released, expired.", + }); + expect(parseClaimListArgs(["--repo"])).toEqual({ + error: expect.stringContaining("Usage: gittensory-miner claim list"), + }); + expect(parseClaimListArgs(["extra"])).toEqual({ + error: expect.stringContaining("Usage: gittensory-miner claim list"), + }); + expect(parseClaimListArgs(["--status", "released", "--unknown"])).toEqual({ + error: "Unknown option: --unknown", + }); + }); + + it("renderClaimsTable formats claim rows and empty output", () => { + const entries: ClaimEntry[] = [ + { + id: 1, + repoFullName: "acme/widgets", + issueNumber: 7, + status: "active", + claimedAt: "2026-07-04T12:00:00.000Z", + note: "wip", + }, + ]; + expect(renderClaimsTable([])).toBe("no claim ledger entries"); + expect(renderClaimsTable(entries)).toContain("acme/widgets"); + expect(renderClaimsTable(entries)).toContain(" 7"); + expect(renderClaimsTable(entries)).toContain("wip"); + expect( + renderClaimsTable([ + { + ...entries[0]!, + note: null, + }, + ]), + ).toContain("-"); + }); + + it("runClaimClaim records a claim and prints table or JSON output", () => { + const claimLedger = tempClaimLedger(); + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + expect( + runClaimClaim(["acme/widgets", "42", "--note", "on it"], { + openClaimLedger: () => claimLedger, + }), + ).toBe(0); + expect(log).toHaveBeenCalledWith("active"); + + log.mockClear(); + expect( + runClaimClaim(["acme/widgets", "42", "--json"], { + openClaimLedger: () => claimLedger, + }), + ).toBe(0); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ + claim: expect.objectContaining({ + repoFullName: "acme/widgets", + issueNumber: 42, + status: "active", + note: "on it", + }), + }); + }); + + it("runClaimRelease releases a claim and rejects missing entries", () => { + const claimLedger = tempClaimLedger(); + claimLedger.claimIssue("acme/widgets", 9); + + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + expect( + runClaimRelease(["acme/widgets", "9"], { + openClaimLedger: () => claimLedger, + }), + ).toBe(0); + expect(log).toHaveBeenCalledWith("released"); + + log.mockClear(); + claimLedger.claimIssue("acme/widgets", 10); + expect( + runClaimRelease(["acme/widgets", "10", "--json"], { + openClaimLedger: () => claimLedger, + }), + ).toBe(0); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ + claim: expect.objectContaining({ status: "released", issueNumber: 10 }), + }); + + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + expect( + runClaimRelease(["acme/widgets", "404"], { + openClaimLedger: () => claimLedger, + }), + ).toBe(2); + expect(error).toHaveBeenCalledWith("claim_not_found"); + }); + + it("runClaimList prints table and JSON output with repo and status filters", () => { + const claimLedger = tempClaimLedger(); + claimLedger.claimIssue("acme/widgets", 1); + claimLedger.claimIssue("acme/other", 2); + claimLedger.releaseClaim("acme/other", 2); + + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + expect( + runClaimList([], { + openClaimLedger: () => claimLedger, + }), + ).toBe(0); + expect(String(log.mock.calls[0]?.[0])).toContain("acme/widgets"); + + log.mockClear(); + expect( + runClaimList(["--repo", "acme/other", "--status", "released", "--json"], { + openClaimLedger: () => claimLedger, + }), + ).toBe(0); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ + claims: [expect.objectContaining({ repoFullName: "acme/other", status: "released" })], + }); + }); + + it("runClaimCli dispatches claim, release, and list subcommands", () => { + const claimLedger = tempClaimLedger(); + const options = { openClaimLedger: () => claimLedger }; + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + expect(runClaimCli("claim", ["acme/widgets", "3", "--json"], options)).toBe(0); + expect(runClaimCli("list", ["--json"], options)).toBe(0); + expect(runClaimCli("release", ["acme/widgets", "3"], options)).toBe(0); + expect(log).toHaveBeenCalled(); + }); + + it("rejects unknown claim subcommands, options, and ledger failures", () => { + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + expect(runClaimCli("peek", [])).toBe(2); + expect(String(error.mock.calls[0]?.[0])).toContain("Unknown claim subcommand"); + expect(runClaimCli(undefined, [])).toBe(2); + + expect(runClaimClaim(["acme/widgets"])).toBe(2); + expect( + runClaimClaim(["acme/widgets", "1"], { + openClaimLedger: () => { + throw new Error("ledger_broken"); + }, + }), + ).toBe(2); + expect(error).toHaveBeenCalledWith("ledger_broken"); + + error.mockClear(); + expect( + runClaimRelease(["acme/widgets", "1"], { + openClaimLedger: () => { + throw new Error("release_broken"); + }, + }), + ).toBe(2); + expect(error).toHaveBeenCalledWith("release_broken"); + + error.mockClear(); + expect( + runClaimList([], { + openClaimLedger: () => { + throw new Error("list_broken"); + }, + }), + ).toBe(2); + expect(error).toHaveBeenCalledWith("list_broken"); + }); +}); From 5519687551be64c0df3a09969fa5192ddc80447b Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Thu, 9 Jul 2026 04:20:32 +0200 Subject: [PATCH 2/2] ci: retrigger checks after flaky pr-reconciliation timeout Co-authored-by: Cursor