Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function printHelp(input) {
" gittensory-miner loop --search <query> --miner-login <login> [--max-cycles <n>] [--cycle-delay-ms <ms>] [--dry-run] [--json]",
" Autonomous discover->claim->attempt->reenter loop",
" gittensory-miner queue list [--repo <owner/repo>] [--json] List portfolio backlog rows",
" gittensory-miner queue next [--dry-run] [--json] Claim the highest-priority queued item",
" gittensory-miner queue next [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json] Claim next item under WIP caps",
" gittensory-miner queue claim-batch [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]",
" gittensory-miner queue done <owner/repo> <identifier> [--dry-run] [--json]",
" gittensory-miner queue release <owner/repo> <identifier> [--dry-run] [--json] Return a claimed item to the queue",
Expand Down
4 changes: 4 additions & 0 deletions packages/gittensory-miner/lib/portfolio-queue-caps.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function resolvePortfolioQueueCaps(options?: {
env?: Record<string, string | undefined>;
cliCaps?: { globalWipCap?: number; perRepoWipCap?: number };
}): { globalWipCap: number; perRepoWipCap: number };
64 changes: 64 additions & 0 deletions packages/gittensory-miner/lib/portfolio-queue-caps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { parse as parseYaml } from "yaml";
import { normalizePortfolioCaps } from "./portfolio-queue-manager.js";
import { resolveMinerStateDir } from "./status.js";

const CONFIG_FILE_CANDIDATES = Object.freeze([
".gittensory-miner.yml",
".github/gittensory-miner.yml",
".gittensory-miner.json",
".github/gittensory-miner.json",
]);

function discoverConfigFile(cwd) {
for (const candidate of CONFIG_FILE_CANDIDATES) {
const path = join(cwd, candidate);
if (existsSync(path)) return path;
}
return null;
}

function readConfigCaps(stateDir) {
const configPath = discoverConfigFile(stateDir);
if (!configPath) return null;
try {
const raw = configPath.endsWith(".json") ? JSON.parse(readFileSync(configPath, "utf8")) : parseYaml(readFileSync(configPath, "utf8"));
const portfolioQueue = raw?.portfolioQueue;
if (!portfolioQueue || typeof portfolioQueue !== "object" || Array.isArray(portfolioQueue)) return null;
return normalizePortfolioCaps(portfolioQueue);
} catch {
return null;
}
}

function readEnvCaps(env) {
const caps = {};
if (typeof env.GITTENSORY_MINER_GLOBAL_WIP_CAP === "string" && env.GITTENSORY_MINER_GLOBAL_WIP_CAP.trim()) {
caps.globalWipCap = Number(env.GITTENSORY_MINER_GLOBAL_WIP_CAP);
}
if (typeof env.GITTENSORY_MINER_PER_REPO_WIP_CAP === "string" && env.GITTENSORY_MINER_PER_REPO_WIP_CAP.trim()) {
caps.perRepoWipCap = Number(env.GITTENSORY_MINER_PER_REPO_WIP_CAP);
}
return Object.keys(caps).length > 0 ? normalizePortfolioCaps(caps) : null;
}

/**
* Resolve WIP caps for portfolio claiming: operator `.gittensory-miner.yml` in the state dir, then env, then CLI
* flags (when provided), defaulting to `{ globalWipCap: 1, perRepoWipCap: 1 }`.
* @param {{ env?: NodeJS.ProcessEnv, cliCaps?: { globalWipCap?: number, perRepoWipCap?: number } }} [options]
*/
export function resolvePortfolioQueueCaps(options = {}) {
const env = options.env ?? process.env;
let caps = readConfigCaps(resolveMinerStateDir(env)) ?? { globalWipCap: 1, perRepoWipCap: 1 };
const envCaps = readEnvCaps(env);
if (envCaps) caps = envCaps;
const cliCaps = options.cliCaps ?? {};
if (cliCaps.globalWipCap !== undefined || cliCaps.perRepoWipCap !== undefined) {
caps = normalizePortfolioCaps({
globalWipCap: cliCaps.globalWipCap ?? caps.globalWipCap,
perRepoWipCap: cliCaps.perRepoWipCap ?? caps.perRepoWipCap,
});
}
return caps;
}
30 changes: 26 additions & 4 deletions packages/gittensory-miner/lib/portfolio-queue-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ export type ParsedQueueListArgs =
}
| { error: string };

export type ParsedQueueNextArgs = { json: boolean; dryRun: boolean } | { error: string };
export type ParsedQueueNextArgs =
| {
json: boolean;
dryRun: boolean;
globalWipCap?: number;
perRepoWipCap?: number;
}
| { error: string };

export type ParsedQueueDoneArgs =
| {
Expand All @@ -30,7 +37,12 @@ export function parseQueueReleaseArgs(args: string[]): ParsedQueueDoneArgs;
export function parseQueueRequeueArgs(args: string[]): ParsedQueueDoneArgs;

export type ParsedQueueClaimBatchArgs =
| { json: boolean; dryRun: boolean; globalWipCap: number; perRepoWipCap: number }
| {
json: boolean;
dryRun: boolean;
globalWipCap?: number;
perRepoWipCap?: number;
}
| { error: string };

export function parseQueueClaimBatchArgs(args: string[]): ParsedQueueClaimBatchArgs;
Expand All @@ -44,7 +56,11 @@ export function runQueueList(

export function runQueueNext(
args: string[],
options?: { initPortfolioQueue?: () => PortfolioQueueStore },
options?: {
env?: Record<string, string | undefined>;
dbPath?: string;
initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager;
},
): number;

export function runQueueDone(
Expand All @@ -64,13 +80,19 @@ export function runQueueRequeue(

export function runQueueClaimBatch(
args: string[],
options?: { initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager },
options?: {
env?: Record<string, string | undefined>;
dbPath?: string;
initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager;
},
): number;

export function runQueueCli(
subcommand: string | undefined,
args: string[],
options?: {
env?: Record<string, string | undefined>;
dbPath?: string;
initPortfolioQueue?: () => PortfolioQueueStore;
initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager;
},
Expand Down
117 changes: 73 additions & 44 deletions packages/gittensory-miner/lib/portfolio-queue-cli.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { initPortfolioQueueStore } from "./portfolio-queue.js";
import { initPortfolioQueueManager } from "./portfolio-queue-manager.js";
import { resolvePortfolioQueueCaps } from "./portfolio-queue-caps.js";
import { runPortfolioDashboard } from "./portfolio-dashboard.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const QUEUE_LIST_USAGE = "Usage: gittensory-miner queue list [--repo <owner/repo>] [--json]";
const QUEUE_NEXT_USAGE = "Usage: gittensory-miner queue next [--dry-run] [--json]";
const QUEUE_NEXT_USAGE =
"Usage: gittensory-miner queue next [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]";
const QUEUE_DONE_USAGE = "Usage: gittensory-miner queue done <owner/repo> <identifier> [--dry-run] [--json]";
const QUEUE_RELEASE_USAGE = "Usage: gittensory-miner queue release <owner/repo> <identifier> [--dry-run] [--json]";
const QUEUE_REQUEUE_USAGE = "Usage: gittensory-miner queue requeue <owner/repo> <identifier> [--dry-run] [--json]";
Expand Down Expand Up @@ -44,6 +46,38 @@ function parseJsonFlag(args) {
return { positional, ...options };
}

function parsePortfolioQueueCapArgs(args, usage) {
const options = { json: false, dryRun: false, globalWipCap: undefined, perRepoWipCap: undefined };

for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--json") {
options.json = true;
continue;
}
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--global-wip" || token === "--per-repo-wip") {
const value = Number(args[index + 1]);
if (args[index + 1] === undefined || !Number.isFinite(value) || value < 0) {
return { error: usage };
}
if (token === "--global-wip") options.globalWipCap = value;
else options.perRepoWipCap = value;
index += 1;
continue;
}
if (token.startsWith("-")) {
return { error: `Unknown option: ${token}` };
}
return { error: usage };
}

return options;
}

export function parseQueueListArgs(args) {
const options = { json: false, repoFullName: null };
const positional = [];
Expand Down Expand Up @@ -79,12 +113,7 @@ export function parseQueueListArgs(args) {
}

export function parseQueueNextArgs(args) {
const parsed = parseJsonFlag(args);
if ("error" in parsed) return parsed;
if (parsed.positional.length > 0) {
return { error: QUEUE_NEXT_USAGE };
}
return { json: parsed.json, dryRun: parsed.dryRun };
return parsePortfolioQueueCapArgs(args, QUEUE_NEXT_USAGE);
}

/** Shared `<owner/repo> <identifier> [--json]` parse for the item-targeting subcommands (done/release/requeue).
Expand Down Expand Up @@ -188,27 +217,44 @@ export function runQueueNext(args, options = {}) {
}

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run" };
const caps = resolvePortfolioQueueCaps({
env: options.env ?? process.env,
cliCaps: { globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap },
});
const dryRunResult = { outcome: "dry_run", ...caps };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult, null, 2));
} else {
console.log("DRY RUN: would dequeue the highest-priority queued item. No portfolio-queue write was made.");
console.log(
`DRY RUN: would claim the next queued item (global-wip: ${caps.globalWipCap}, per-repo-wip: ${caps.perRepoWipCap}). No portfolio-queue write was made.`,
);
}
return 0;
}

const caps = resolvePortfolioQueueCaps({
env: options.env ?? process.env,
cliCaps: { globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap },
});

const ownsManager = options.initPortfolioQueueManager === undefined;
let manager;
try {
return withPortfolioQueue(options, (portfolioQueue) => {
const entry = portfolioQueue.dequeueNext();
if (parsed.json) {
console.log(JSON.stringify({ entry }, null, 2));
} else {
console.log(entry ? entry.identifier : "none");
}
return 0;
manager = (options.initPortfolioQueueManager ?? initPortfolioQueueManager)({
caps,
dbPath: options.dbPath,
});
const entry = manager.claimNextBatch()[0] ?? null;
if (parsed.json) {
console.log(JSON.stringify({ entry }, null, 2));
} else {
console.log(entry ? entry.identifier : "none");
}
return 0;
} catch (error) {
return reportCliFailure(parsed.json, describeCliError(error));
} finally {
if (ownsManager) manager?.close();
}
}

Expand Down Expand Up @@ -320,30 +366,7 @@ export function runQueueRequeue(args, options = {}) {
}

export function parseQueueClaimBatchArgs(args) {
const options = { json: false, dryRun: false, globalWipCap: 1, perRepoWipCap: 1 };
for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--json") {
options.json = true;
continue;
}
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--global-wip" || token === "--per-repo-wip") {
const value = Number(args[index + 1]);
if (args[index + 1] === undefined || !Number.isFinite(value) || value < 0) {
return { error: QUEUE_CLAIM_BATCH_USAGE };
}
if (token === "--global-wip") options.globalWipCap = value;
else options.perRepoWipCap = value;
index += 1;
continue;
}
return { error: QUEUE_CLAIM_BATCH_USAGE };
}
return options;
return parsePortfolioQueueCapArgs(args, QUEUE_CLAIM_BATCH_USAGE);
}

/** Claim the next caps-aware batch via the WIP-cap-aware batch claimer (portfolio-queue-manager.js), which also
Expand All @@ -354,13 +377,18 @@ export function runQueueClaimBatch(args, options = {}) {
return reportCliFailure(argsWantJson(args), parsed.error);
}

const caps = resolvePortfolioQueueCaps({
env: options.env ?? process.env,
cliCaps: { globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap },
});

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run", globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap };
const dryRunResult = { outcome: "dry_run", ...caps };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult, null, 2));
} else {
console.log(
`DRY RUN: would claim a batch (global-wip: ${parsed.globalWipCap}, per-repo-wip: ${parsed.perRepoWipCap}). No portfolio-queue write was made.`,
`DRY RUN: would claim a batch (global-wip: ${caps.globalWipCap}, per-repo-wip: ${caps.perRepoWipCap}). No portfolio-queue write was made.`,
);
}
return 0;
Expand All @@ -372,7 +400,8 @@ export function runQueueClaimBatch(args, options = {}) {
let manager;
try {
manager = (options.initPortfolioQueueManager ?? initPortfolioQueueManager)({
caps: { globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap },
caps,
dbPath: options.dbPath,
});
const claimed = manager.claimNextBatch();
if (parsed.json) {
Expand Down
2 changes: 1 addition & 1 deletion packages/gittensory-miner/lib/portfolio-queue-manager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Stateful PortfolioQueueManager (#4285): compose the persisted SQLite portfolio/queue store
// (portfolio-queue.js, #2292) with the pure engine selector (nextEligibleItems, queue.ts, #2326) so batch
// claiming respects global/per-repo WIP caps and cross-repo diversification instead of a naive priority-only
// single-row dequeue. Caps are plain constructor arguments — not wired to .gittensory-miner.yml here.
// single-row dequeue. Caps resolve via resolvePortfolioQueueCaps() for queue next / claim-batch CLI paths.
import { nextEligibleItems } from "@jsonbored/gittensory-engine";
import { initPortfolioQueueStore } from "./portfolio-queue.js";
import { DEFAULT_MAX_LEASE_MS, sweepStuckItems } from "./portfolio-queue-expiry.js";
Expand Down
4 changes: 2 additions & 2 deletions test/unit/miner-cli-json-error-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ describe("miner CLI --json error coverage (#4836)", () => {
expectJsonError(
() =>
runQueueNext(["--json"], {
initPortfolioQueue: () =>
({ dequeueNext: () => { throw new Error("next_db"); }, close: () => {} }) as never,
initPortfolioQueueManager: () =>
({ claimNextBatch: () => { throw new Error("next_db"); }, close: () => {} }) as never,
}),
"next_db",
);
Expand Down
Loading