Skip to content

Commit 6f40ce1

Browse files
feat(miner-cli): claim-ledger commands (claim / release / list) (#4348)
* 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 <cursoragent@cursor.com> * ci: retrigger checks after flaky pr-reconciliation timeout Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent feb2ba8 commit 6f40ce1

6 files changed

Lines changed: 594 additions & 1 deletion

File tree

packages/gittensory-miner/bin/gittensory-miner.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { runLedgerCli } from "../lib/event-ledger-cli.js";
66
import { runManagePoll } from "../lib/manage-poll.js";
77
import { runManageStatus } from "../lib/manage-status.js";
88
import { runPlanCli } from "../lib/plan-store-cli.js";
9+
import { runClaimCli } from "../lib/claim-ledger-cli.js";
910
import { runQueueCli } from "../lib/portfolio-queue-cli.js";
1011
import { runStateCli } from "../lib/run-state-cli.js";
1112
import { runInit } from "../lib/laptop-init.js";
@@ -42,6 +43,10 @@ if (cliArgs[0] === "queue") {
4243
process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2)));
4344
}
4445

46+
if (cliArgs[0] === "claim") {
47+
process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2)));
48+
}
49+
4550
if (cliArgs[0] === "ledger") {
4651
process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2)));
4752
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { ClaimEntry, ClaimLedger, ClaimStatus } from "./claim-ledger.js";
2+
3+
export type ParsedClaimClaimArgs =
4+
| {
5+
repoFullName: string;
6+
issueNumber: number;
7+
note: string | undefined;
8+
json: boolean;
9+
}
10+
| { error: string };
11+
12+
export type ParsedClaimReleaseArgs =
13+
| {
14+
repoFullName: string;
15+
issueNumber: number;
16+
json: boolean;
17+
}
18+
| { error: string };
19+
20+
export type ParsedClaimListArgs =
21+
| {
22+
json: boolean;
23+
repoFullName: string | null;
24+
status: ClaimStatus | null;
25+
}
26+
| { error: string };
27+
28+
export function parseClaimClaimArgs(args: string[]): ParsedClaimClaimArgs;
29+
30+
export function parseClaimReleaseArgs(args: string[]): ParsedClaimReleaseArgs;
31+
32+
export function parseClaimListArgs(args: string[]): ParsedClaimListArgs;
33+
34+
export function renderClaimsTable(entries: ClaimEntry[]): string;
35+
36+
export function runClaimClaim(
37+
args: string[],
38+
options?: { openClaimLedger?: () => ClaimLedger },
39+
): number;
40+
41+
export function runClaimRelease(
42+
args: string[],
43+
options?: { openClaimLedger?: () => ClaimLedger },
44+
): number;
45+
46+
export function runClaimList(
47+
args: string[],
48+
options?: { openClaimLedger?: () => ClaimLedger },
49+
): number;
50+
51+
export function runClaimCli(
52+
subcommand: string | undefined,
53+
args: string[],
54+
options?: { openClaimLedger?: () => ClaimLedger },
55+
): number;
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js";
2+
3+
const CLAIM_CLAIM_USAGE =
4+
"Usage: gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--json]";
5+
const CLAIM_RELEASE_USAGE = "Usage: gittensory-miner claim release <owner/repo> <issue#> [--json]";
6+
const CLAIM_LIST_USAGE =
7+
"Usage: gittensory-miner claim list [--repo <owner/repo>] [--status active|released|expired] [--json]";
8+
9+
function parseRepoArg(value, usage) {
10+
if (!value) return { error: usage };
11+
const trimmed = value.trim();
12+
const [owner, repo, extra] = trimmed.split("/");
13+
if (!owner || !repo || extra !== undefined) {
14+
return { error: "Repository must be in owner/repo form." };
15+
}
16+
return { repoFullName: `${owner}/${repo}` };
17+
}
18+
19+
function parseIssueNumberArg(value, usage) {
20+
if (!value) return { error: usage };
21+
const parsed = Number(value);
22+
if (!Number.isInteger(parsed) || parsed < 1) {
23+
return { error: "issue number must be a positive integer." };
24+
}
25+
return { issueNumber: parsed };
26+
}
27+
28+
export function parseClaimClaimArgs(args) {
29+
const options = { json: false, note: undefined };
30+
const positional = [];
31+
32+
for (let index = 0; index < args.length; index += 1) {
33+
const token = args[index];
34+
if (token === "--json") {
35+
options.json = true;
36+
continue;
37+
}
38+
if (token === "--note") {
39+
const note = args[index + 1];
40+
if (!note || note.startsWith("-")) {
41+
return { error: CLAIM_CLAIM_USAGE };
42+
}
43+
options.note = note;
44+
index += 1;
45+
continue;
46+
}
47+
if (token.startsWith("-")) {
48+
return { error: `Unknown option: ${token}` };
49+
}
50+
positional.push(token);
51+
}
52+
53+
if (positional.length !== 2) {
54+
return { error: CLAIM_CLAIM_USAGE };
55+
}
56+
57+
const repo = parseRepoArg(positional[0], CLAIM_CLAIM_USAGE);
58+
if ("error" in repo) return repo;
59+
const issue = parseIssueNumberArg(positional[1], CLAIM_CLAIM_USAGE);
60+
if ("error" in issue) return issue;
61+
62+
return {
63+
repoFullName: repo.repoFullName,
64+
issueNumber: issue.issueNumber,
65+
note: options.note,
66+
json: options.json,
67+
};
68+
}
69+
70+
export function parseClaimReleaseArgs(args) {
71+
const options = { json: false };
72+
const positional = [];
73+
74+
for (const token of args) {
75+
if (token === "--json") {
76+
options.json = true;
77+
continue;
78+
}
79+
if (token.startsWith("-")) {
80+
return { error: `Unknown option: ${token}` };
81+
}
82+
positional.push(token);
83+
}
84+
85+
if (positional.length !== 2) {
86+
return { error: CLAIM_RELEASE_USAGE };
87+
}
88+
89+
const repo = parseRepoArg(positional[0], CLAIM_RELEASE_USAGE);
90+
if ("error" in repo) return repo;
91+
const issue = parseIssueNumberArg(positional[1], CLAIM_RELEASE_USAGE);
92+
if ("error" in issue) return issue;
93+
94+
return {
95+
repoFullName: repo.repoFullName,
96+
issueNumber: issue.issueNumber,
97+
json: options.json,
98+
};
99+
}
100+
101+
export function parseClaimListArgs(args) {
102+
const options = { json: false, repoFullName: null, status: null };
103+
const positional = [];
104+
105+
for (let index = 0; index < args.length; index += 1) {
106+
const token = args[index];
107+
if (token === "--json") {
108+
options.json = true;
109+
continue;
110+
}
111+
if (token === "--repo") {
112+
const repoArg = args[index + 1];
113+
if (!repoArg || repoArg.startsWith("-")) {
114+
return { error: CLAIM_LIST_USAGE };
115+
}
116+
const repo = parseRepoArg(repoArg, CLAIM_LIST_USAGE);
117+
if ("error" in repo) return repo;
118+
options.repoFullName = repo.repoFullName;
119+
index += 1;
120+
continue;
121+
}
122+
if (token === "--status") {
123+
const statusArg = args[index + 1];
124+
if (!statusArg || statusArg.startsWith("-")) {
125+
return { error: CLAIM_LIST_USAGE };
126+
}
127+
if (!CLAIM_STATUSES.includes(statusArg)) {
128+
return { error: `status must be one of: ${CLAIM_STATUSES.join(", ")}.` };
129+
}
130+
options.status = statusArg;
131+
index += 1;
132+
continue;
133+
}
134+
if (token.startsWith("-")) {
135+
return { error: `Unknown option: ${token}` };
136+
}
137+
positional.push(token);
138+
}
139+
140+
if (positional.length > 0) {
141+
return { error: CLAIM_LIST_USAGE };
142+
}
143+
144+
return options;
145+
}
146+
147+
function display(value) {
148+
if (value === null || value === undefined) return "-";
149+
return String(value);
150+
}
151+
152+
export function renderClaimsTable(entries) {
153+
if (!Array.isArray(entries) || entries.length === 0) return "no claim ledger entries";
154+
const header = [
155+
"repo".padEnd(24),
156+
"issue".padStart(6),
157+
"status".padEnd(10),
158+
"claimed-at".padEnd(24),
159+
"note".padEnd(16),
160+
].join(" ");
161+
const lines = entries.map((entry) =>
162+
[
163+
entry.repoFullName.padEnd(24),
164+
display(entry.issueNumber).padStart(6),
165+
entry.status.padEnd(10),
166+
display(entry.claimedAt).padEnd(24),
167+
display(entry.note).padEnd(16),
168+
].join(" "),
169+
);
170+
return [header, ...lines].join("\n");
171+
}
172+
173+
function withClaimLedger(options, run) {
174+
const ownsLedger = options.openClaimLedger === undefined;
175+
const claimLedger = (options.openClaimLedger ?? openClaimLedger)();
176+
try {
177+
return run(claimLedger);
178+
} finally {
179+
if (ownsLedger) claimLedger.close();
180+
}
181+
}
182+
183+
export function runClaimClaim(args, options = {}) {
184+
const parsed = parseClaimClaimArgs(args);
185+
if ("error" in parsed) {
186+
console.error(parsed.error);
187+
return 2;
188+
}
189+
190+
try {
191+
return withClaimLedger(options, (claimLedger) => {
192+
const claim = claimLedger.claimIssue(
193+
parsed.repoFullName,
194+
parsed.issueNumber,
195+
parsed.note,
196+
);
197+
if (parsed.json) {
198+
console.log(JSON.stringify({ claim }, null, 2));
199+
} else {
200+
console.log(claim.status);
201+
}
202+
return 0;
203+
});
204+
} catch (error) {
205+
console.error(error instanceof Error ? error.message : String(error));
206+
return 2;
207+
}
208+
}
209+
210+
export function runClaimRelease(args, options = {}) {
211+
const parsed = parseClaimReleaseArgs(args);
212+
if ("error" in parsed) {
213+
console.error(parsed.error);
214+
return 2;
215+
}
216+
217+
try {
218+
return withClaimLedger(options, (claimLedger) => {
219+
const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber);
220+
if (!claim) {
221+
console.error("claim_not_found");
222+
return 2;
223+
}
224+
if (parsed.json) {
225+
console.log(JSON.stringify({ claim }, null, 2));
226+
} else {
227+
console.log(claim.status);
228+
}
229+
return 0;
230+
});
231+
} catch (error) {
232+
console.error(error instanceof Error ? error.message : String(error));
233+
return 2;
234+
}
235+
}
236+
237+
export function runClaimList(args, options = {}) {
238+
const parsed = parseClaimListArgs(args);
239+
if ("error" in parsed) {
240+
console.error(parsed.error);
241+
return 2;
242+
}
243+
244+
try {
245+
return withClaimLedger(options, (claimLedger) => {
246+
const filter = {};
247+
if (parsed.repoFullName !== null) filter.repoFullName = parsed.repoFullName;
248+
if (parsed.status !== null) filter.status = parsed.status;
249+
const claims = claimLedger.listClaims(filter);
250+
if (parsed.json) {
251+
console.log(JSON.stringify({ claims }, null, 2));
252+
} else {
253+
console.log(renderClaimsTable(claims));
254+
}
255+
return 0;
256+
});
257+
} catch (error) {
258+
console.error(error instanceof Error ? error.message : String(error));
259+
return 2;
260+
}
261+
}
262+
263+
export function runClaimCli(subcommand, args, options = {}) {
264+
if (subcommand === "claim") return runClaimClaim(args, options);
265+
if (subcommand === "release") return runClaimRelease(args, options);
266+
if (subcommand === "list") return runClaimList(args, options);
267+
console.error(`Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`);
268+
return 2;
269+
}

packages/gittensory-miner/lib/cli.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export function printHelp(input) {
2222
" gittensory-miner queue list [--repo <owner/repo>] [--json] List portfolio backlog rows",
2323
" gittensory-miner queue next [--json] Claim the highest-priority queued item",
2424
" gittensory-miner queue done <owner/repo> <identifier> [--json]",
25+
" gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--json]",
26+
" gittensory-miner claim release <owner/repo> <issue#> [--json]",
27+
" gittensory-miner claim list [--repo <owner/repo>] [--status active|released|expired] [--json]",
2528
" gittensory-miner ledger list [--repo <owner/repo>] [--since <seq>] [--type <eventType>] [--json]",
2629
" gittensory-miner plan list [--status pending|running|completed|failed] [--json]",
2730
" gittensory-miner plan show <planId> [--json]",

packages/gittensory-miner/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"lib"
3232
],
3333
"scripts": {
34-
"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"
34+
"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"
3535
},
3636
"dependencies": {
3737
"@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0"

0 commit comments

Comments
 (0)