@loopover/miner 0.1.0

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 (218) hide show
  1. package/DEPLOYMENT.md +156 -0
  2. package/Dockerfile +38 -0
  3. package/README.md +269 -0
  4. package/bin/gittensory-miner-mcp.d.ts +64 -0
  5. package/bin/gittensory-miner-mcp.js +279 -0
  6. package/bin/gittensory-miner.js +196 -0
  7. package/docs/coding-agent-driver.md +140 -0
  8. package/docs/config-precedence.md +96 -0
  9. package/docs/cross-repo-discovery-phase1.md +55 -0
  10. package/docs/discovery-index-contract.md +60 -0
  11. package/docs/discovery-plane-operator-guide.md +101 -0
  12. package/docs/env-reference.md +30 -0
  13. package/docs/fleet-run-manifest.md +50 -0
  14. package/docs/miner-goal-spec.md +76 -0
  15. package/docs/miner-selfimprove-calibration.md +105 -0
  16. package/docs/observability.md +60 -0
  17. package/docs/operations-runbook.md +209 -0
  18. package/docs/repo-agnostic-capability-audit.md +129 -0
  19. package/docs/unattended-scheduling.md +107 -0
  20. package/expected-engine.version +1 -0
  21. package/lib/ams-policy.d.ts +20 -0
  22. package/lib/ams-policy.js +68 -0
  23. package/lib/attempt-cli.d.ts +99 -0
  24. package/lib/attempt-cli.js +523 -0
  25. package/lib/attempt-input-builder.d.ts +24 -0
  26. package/lib/attempt-input-builder.js +91 -0
  27. package/lib/attempt-log.d.ts +37 -0
  28. package/lib/attempt-log.js +177 -0
  29. package/lib/attempt-runner.d.ts +59 -0
  30. package/lib/attempt-runner.js +164 -0
  31. package/lib/attempt-worktree.d.ts +31 -0
  32. package/lib/attempt-worktree.js +94 -0
  33. package/lib/calibration-cli.d.ts +1 -0
  34. package/lib/calibration-cli.js +92 -0
  35. package/lib/calibration-run.d.ts +161 -0
  36. package/lib/calibration-run.js +232 -0
  37. package/lib/calibration-types.d.ts +46 -0
  38. package/lib/calibration-types.js +74 -0
  39. package/lib/calibration.d.ts +24 -0
  40. package/lib/calibration.js +99 -0
  41. package/lib/ci-poller.d.ts +33 -0
  42. package/lib/ci-poller.js +234 -0
  43. package/lib/claim-adjudication.d.ts +21 -0
  44. package/lib/claim-adjudication.js +36 -0
  45. package/lib/claim-conflict-resolver.d.ts +29 -0
  46. package/lib/claim-conflict-resolver.js +101 -0
  47. package/lib/claim-ledger-cli.d.ts +59 -0
  48. package/lib/claim-ledger-cli.js +320 -0
  49. package/lib/claim-ledger-expiry.d.ts +20 -0
  50. package/lib/claim-ledger-expiry.js +41 -0
  51. package/lib/claim-ledger.d.ts +63 -0
  52. package/lib/claim-ledger.js +303 -0
  53. package/lib/cli-error.d.ts +3 -0
  54. package/lib/cli-error.js +27 -0
  55. package/lib/cli.d.ts +3 -0
  56. package/lib/cli.js +72 -0
  57. package/lib/coding-agent-construction.d.ts +17 -0
  58. package/lib/coding-agent-construction.js +103 -0
  59. package/lib/coding-agent-house-rules.d.ts +24 -0
  60. package/lib/coding-agent-house-rules.js +64 -0
  61. package/lib/coding-task-spec.d.ts +44 -0
  62. package/lib/coding-task-spec.js +183 -0
  63. package/lib/config-precedence.d.ts +25 -0
  64. package/lib/config-precedence.js +38 -0
  65. package/lib/deny-check.d.ts +11 -0
  66. package/lib/deny-check.js +76 -0
  67. package/lib/deny-hook-synthesis.d.ts +93 -0
  68. package/lib/deny-hook-synthesis.js +399 -0
  69. package/lib/deny-hooks.d.ts +28 -0
  70. package/lib/deny-hooks.js +155 -0
  71. package/lib/deployment-docs-audit.d.ts +41 -0
  72. package/lib/deployment-docs-audit.js +115 -0
  73. package/lib/discover-cli.d.ts +94 -0
  74. package/lib/discover-cli.js +284 -0
  75. package/lib/discovery-throttle.d.ts +8 -0
  76. package/lib/discovery-throttle.js +33 -0
  77. package/lib/env-file-indirection.d.ts +4 -0
  78. package/lib/env-file-indirection.js +45 -0
  79. package/lib/event-ledger-cli.d.ts +82 -0
  80. package/lib/event-ledger-cli.js +266 -0
  81. package/lib/event-ledger.d.ts +37 -0
  82. package/lib/event-ledger.js +210 -0
  83. package/lib/execute-local-write.d.ts +14 -0
  84. package/lib/execute-local-write.js +50 -0
  85. package/lib/feasibility-cli.d.ts +25 -0
  86. package/lib/feasibility-cli.js +80 -0
  87. package/lib/forge-config.d.ts +17 -0
  88. package/lib/forge-config.js +37 -0
  89. package/lib/governor-action-mode.d.ts +26 -0
  90. package/lib/governor-action-mode.js +47 -0
  91. package/lib/governor-chokepoint-persisted.d.ts +18 -0
  92. package/lib/governor-chokepoint-persisted.js +46 -0
  93. package/lib/governor-chokepoint.d.ts +14 -0
  94. package/lib/governor-chokepoint.js +52 -0
  95. package/lib/governor-kill-switch.d.ts +26 -0
  96. package/lib/governor-kill-switch.js +47 -0
  97. package/lib/governor-ledger-cli.d.ts +32 -0
  98. package/lib/governor-ledger-cli.js +155 -0
  99. package/lib/governor-ledger.d.ts +46 -0
  100. package/lib/governor-ledger.js +203 -0
  101. package/lib/governor-open-pr.d.ts +21 -0
  102. package/lib/governor-open-pr.js +27 -0
  103. package/lib/governor-pause-cli.d.ts +23 -0
  104. package/lib/governor-pause-cli.js +171 -0
  105. package/lib/governor-run-halt.d.ts +37 -0
  106. package/lib/governor-run-halt.js +58 -0
  107. package/lib/governor-state.d.ts +63 -0
  108. package/lib/governor-state.js +363 -0
  109. package/lib/governor-write-rate-limit.d.ts +30 -0
  110. package/lib/governor-write-rate-limit.js +64 -0
  111. package/lib/harness-submission-trigger.d.ts +69 -0
  112. package/lib/harness-submission-trigger.js +138 -0
  113. package/lib/http-retry.d.ts +12 -0
  114. package/lib/http-retry.js +52 -0
  115. package/lib/laptop-init.d.ts +53 -0
  116. package/lib/laptop-init.js +332 -0
  117. package/lib/live-issue-snapshot.d.ts +16 -0
  118. package/lib/live-issue-snapshot.js +117 -0
  119. package/lib/local-store.d.ts +18 -0
  120. package/lib/local-store.js +62 -0
  121. package/lib/logger.d.ts +58 -0
  122. package/lib/logger.js +167 -0
  123. package/lib/loop-cli.d.ts +66 -0
  124. package/lib/loop-cli.js +540 -0
  125. package/lib/loop-closure.d.ts +35 -0
  126. package/lib/loop-closure.js +66 -0
  127. package/lib/loop-reentry.d.ts +51 -0
  128. package/lib/loop-reentry.js +124 -0
  129. package/lib/manage-poll.d.ts +79 -0
  130. package/lib/manage-poll.js +254 -0
  131. package/lib/manage-status.d.ts +70 -0
  132. package/lib/manage-status.js +241 -0
  133. package/lib/metrics-cli.d.ts +9 -0
  134. package/lib/metrics-cli.js +50 -0
  135. package/lib/migrate-cli.d.ts +24 -0
  136. package/lib/migrate-cli.js +111 -0
  137. package/lib/miner-goal-spec.d.ts +12 -0
  138. package/lib/miner-goal-spec.js +67 -0
  139. package/lib/opportunity-fanout.d.ts +86 -0
  140. package/lib/opportunity-fanout.js +597 -0
  141. package/lib/opportunity-ranker.d.ts +36 -0
  142. package/lib/opportunity-ranker.js +118 -0
  143. package/lib/orb-export.d.ts +54 -0
  144. package/lib/orb-export.js +206 -0
  145. package/lib/plan-store-cli.d.ts +37 -0
  146. package/lib/plan-store-cli.js +151 -0
  147. package/lib/plan-store.d.ts +51 -0
  148. package/lib/plan-store.js +231 -0
  149. package/lib/policy-doc-cache.d.ts +25 -0
  150. package/lib/policy-doc-cache.js +79 -0
  151. package/lib/policy-verdict-cache.d.ts +35 -0
  152. package/lib/policy-verdict-cache.js +106 -0
  153. package/lib/portfolio-dashboard.d.ts +30 -0
  154. package/lib/portfolio-dashboard.js +105 -0
  155. package/lib/portfolio-discovery.d.ts +29 -0
  156. package/lib/portfolio-discovery.js +100 -0
  157. package/lib/portfolio-queue-cli.d.ts +87 -0
  158. package/lib/portfolio-queue-cli.js +509 -0
  159. package/lib/portfolio-queue-expiry.d.ts +20 -0
  160. package/lib/portfolio-queue-expiry.js +51 -0
  161. package/lib/portfolio-queue-manager.d.ts +50 -0
  162. package/lib/portfolio-queue-manager.js +134 -0
  163. package/lib/portfolio-queue.d.ts +62 -0
  164. package/lib/portfolio-queue.js +351 -0
  165. package/lib/pr-disposition-poller.d.ts +26 -0
  166. package/lib/pr-disposition-poller.js +163 -0
  167. package/lib/pr-number-parse.d.ts +4 -0
  168. package/lib/pr-number-parse.js +23 -0
  169. package/lib/pr-outcome.d.ts +41 -0
  170. package/lib/pr-outcome.js +90 -0
  171. package/lib/prediction-ledger.d.ts +47 -0
  172. package/lib/prediction-ledger.js +222 -0
  173. package/lib/pretooluse-hook.d.ts +31 -0
  174. package/lib/pretooluse-hook.js +93 -0
  175. package/lib/process-lifecycle.d.ts +31 -0
  176. package/lib/process-lifecycle.js +106 -0
  177. package/lib/purge-cli.d.ts +44 -0
  178. package/lib/purge-cli.js +198 -0
  179. package/lib/rejection-signal.d.ts +6 -0
  180. package/lib/rejection-signal.js +101 -0
  181. package/lib/rejection-state-machine.d.ts +37 -0
  182. package/lib/rejection-state-machine.js +81 -0
  183. package/lib/rejection-templates.d.ts +12 -0
  184. package/lib/rejection-templates.js +71 -0
  185. package/lib/replay-objective-anchor.d.ts +79 -0
  186. package/lib/replay-objective-anchor.js +179 -0
  187. package/lib/replay-snapshot.d.ts +41 -0
  188. package/lib/replay-snapshot.js +288 -0
  189. package/lib/replay-task-generation.d.ts +126 -0
  190. package/lib/replay-task-generation.js +223 -0
  191. package/lib/repo-clone.d.ts +19 -0
  192. package/lib/repo-clone.js +102 -0
  193. package/lib/run-state-cli.d.ts +27 -0
  194. package/lib/run-state-cli.js +154 -0
  195. package/lib/run-state.d.ts +37 -0
  196. package/lib/run-state.js +150 -0
  197. package/lib/schema-version.d.ts +17 -0
  198. package/lib/schema-version.js +71 -0
  199. package/lib/self-review-context.d.ts +28 -0
  200. package/lib/self-review-context.js +339 -0
  201. package/lib/slop-assessment.d.ts +3 -0
  202. package/lib/slop-assessment.js +20 -0
  203. package/lib/stack-detection.d.ts +41 -0
  204. package/lib/stack-detection.js +248 -0
  205. package/lib/status.d.ts +64 -0
  206. package/lib/status.js +441 -0
  207. package/lib/store-maintenance.d.ts +31 -0
  208. package/lib/store-maintenance.js +183 -0
  209. package/lib/submission-freshness-check.d.ts +32 -0
  210. package/lib/submission-freshness-check.js +93 -0
  211. package/lib/update-check.d.ts +36 -0
  212. package/lib/update-check.js +161 -0
  213. package/lib/version.d.ts +3 -0
  214. package/lib/version.js +10 -0
  215. package/lib/worktree-allocator.d.ts +39 -0
  216. package/lib/worktree-allocator.js +262 -0
  217. package/package.json +50 -0
  218. package/schema/miner-goal-spec.schema.json +111 -0
@@ -0,0 +1,100 @@
1
+ /** Local orchestration: materialize ranked fan-out rows into the portfolio queue (#2292). */
2
+
3
+ function normalizeMinRankScore(minRankScore) {
4
+ if (minRankScore === undefined || minRankScore === null) return 0;
5
+ if (typeof minRankScore !== "number" || !Number.isFinite(minRankScore) || minRankScore < 0) {
6
+ throw new Error("invalid_min_rank_score");
7
+ }
8
+ return minRankScore;
9
+ }
10
+
11
+ function normalizeRankedIssue(issue) {
12
+ if (!issue || typeof issue !== "object") return null;
13
+ const repoFullName = typeof issue.repoFullName === "string" ? issue.repoFullName.trim() : "";
14
+ const [owner, repo, extra] = repoFullName.split("/");
15
+ if (!owner || !repo || extra !== undefined) return null;
16
+ if (!Number.isInteger(issue.issueNumber) || issue.issueNumber <= 0) return null;
17
+ if (typeof issue.rankScore !== "number" || !Number.isFinite(issue.rankScore) || issue.rankScore < 0) {
18
+ return null;
19
+ }
20
+ const title = typeof issue.title === "string" ? issue.title.trim() : "";
21
+ if (!title) return null;
22
+ const labels = Array.isArray(issue.labels)
23
+ ? issue.labels.filter((label) => typeof label === "string" && label.trim()).map((label) => label.trim())
24
+ : [];
25
+ return {
26
+ repoFullName: `${owner}/${repo}`,
27
+ issueNumber: issue.issueNumber,
28
+ title,
29
+ labels,
30
+ rankScore: issue.rankScore,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Enqueue ranked discovery rows into the local portfolio backlog. Uses each row's `rankScore` as queue priority
36
+ * (the #2292 placeholder field). Optionally appends `discovered_issue` audit events when an event ledger is supplied.
37
+ * Never calls GitHub — callers rank locally first via `rankCandidateIssues`.
38
+ */
39
+ export function enqueueRankedDiscovery(rankedIssues, options = {}) {
40
+ if (!Array.isArray(rankedIssues)) throw new Error("invalid_ranked_issues");
41
+ const queueStore = options.queueStore;
42
+ if (!queueStore || typeof queueStore.enqueue !== "function") throw new Error("invalid_queue_store");
43
+
44
+ let eventLedger = null;
45
+ if (options.eventLedger !== undefined) {
46
+ eventLedger = options.eventLedger;
47
+ if (!eventLedger || typeof eventLedger.appendEvent !== "function") {
48
+ throw new Error("invalid_event_ledger");
49
+ }
50
+ }
51
+
52
+ const minRankScore = normalizeMinRankScore(options.minRankScore);
53
+ // #5563: threaded through from the caller's already-resolved forge host, so a non-default (GitHub Enterprise)
54
+ // tenant's ranked issues land in the queue scoped to their own host instead of colliding with a same-named
55
+ // owner/repo on github.com. Omitted/nullish falls through to the queue store's own github.com default.
56
+ const apiBaseUrl = options.apiBaseUrl;
57
+
58
+ const summary = {
59
+ enqueued: 0,
60
+ skippedBelowMinRank: 0,
61
+ skippedInvalid: 0,
62
+ eventsAppended: 0,
63
+ };
64
+
65
+ for (const issue of rankedIssues) {
66
+ const normalized = normalizeRankedIssue(issue);
67
+ if (!normalized) {
68
+ summary.skippedInvalid += 1;
69
+ continue;
70
+ }
71
+ if (normalized.rankScore < minRankScore) {
72
+ summary.skippedBelowMinRank += 1;
73
+ continue;
74
+ }
75
+
76
+ queueStore.enqueue({
77
+ repoFullName: normalized.repoFullName,
78
+ identifier: `issue:${normalized.issueNumber}`,
79
+ priority: normalized.rankScore,
80
+ apiBaseUrl,
81
+ });
82
+ summary.enqueued += 1;
83
+
84
+ if (eventLedger) {
85
+ eventLedger.appendEvent({
86
+ type: "discovered_issue",
87
+ repoFullName: normalized.repoFullName,
88
+ payload: {
89
+ issueNumber: normalized.issueNumber,
90
+ rankScore: normalized.rankScore,
91
+ title: normalized.title,
92
+ labels: normalized.labels,
93
+ },
94
+ });
95
+ summary.eventsAppended += 1;
96
+ }
97
+ }
98
+
99
+ return summary;
100
+ }
@@ -0,0 +1,87 @@
1
+ import type { PortfolioQueueStore, QueueEntry } from "./portfolio-queue.js";
2
+ import type { PortfolioQueueManager } from "./portfolio-queue-manager.js";
3
+
4
+ export type ParsedQueueListArgs =
5
+ | {
6
+ json: boolean;
7
+ repoFullName: string | null;
8
+ }
9
+ | { error: string };
10
+
11
+ export type ParsedQueueNextArgs =
12
+ | { json: boolean; dryRun: boolean; globalWipCap: number | undefined; perRepoWipCap: number | undefined }
13
+ | { error: string };
14
+
15
+ export type QueueClaimTarget = { repoFullName: string; identifier: string; apiBaseUrl: string };
16
+
17
+ export function selectNextEligibleTarget(
18
+ entries: Array<{ repoFullName: string; identifier: string; apiBaseUrl: string; status: string }>,
19
+ caps: { globalWipCap: number; perRepoWipCap: number } | null,
20
+ ): QueueClaimTarget[];
21
+
22
+ export type ParsedQueueDoneArgs =
23
+ | {
24
+ repoFullName: string;
25
+ identifier: string;
26
+ dryRun: boolean;
27
+ json: boolean;
28
+ apiBaseUrl: string | undefined;
29
+ }
30
+ | { error: string };
31
+
32
+ export function parseQueueListArgs(args: string[]): ParsedQueueListArgs;
33
+
34
+ export function parseQueueNextArgs(args: string[]): ParsedQueueNextArgs;
35
+
36
+ export function parseQueueDoneArgs(args: string[]): ParsedQueueDoneArgs;
37
+
38
+ export function parseQueueReleaseArgs(args: string[]): ParsedQueueDoneArgs;
39
+
40
+ export function parseQueueRequeueArgs(args: string[]): ParsedQueueDoneArgs;
41
+
42
+ export type ParsedQueueClaimBatchArgs =
43
+ | { json: boolean; dryRun: boolean; globalWipCap: number; perRepoWipCap: number }
44
+ | { error: string };
45
+
46
+ export function parseQueueClaimBatchArgs(args: string[]): ParsedQueueClaimBatchArgs;
47
+
48
+ export function renderQueueTable(entries: QueueEntry[]): string;
49
+
50
+ export function runQueueList(
51
+ args: string[],
52
+ options?: { initPortfolioQueue?: () => PortfolioQueueStore },
53
+ ): number;
54
+
55
+ export function runQueueNext(
56
+ args: string[],
57
+ options?: { initPortfolioQueue?: () => PortfolioQueueStore },
58
+ ): number;
59
+
60
+ export function runQueueDone(
61
+ args: string[],
62
+ options?: { initPortfolioQueue?: () => PortfolioQueueStore },
63
+ ): number;
64
+
65
+ export function runQueueRelease(
66
+ args: string[],
67
+ options?: { initPortfolioQueue?: () => PortfolioQueueStore },
68
+ ): number;
69
+
70
+ export function runQueueRequeue(
71
+ args: string[],
72
+ options?: { initPortfolioQueue?: () => PortfolioQueueStore },
73
+ ): number;
74
+
75
+ export function runQueueClaimBatch(
76
+ args: string[],
77
+ options?: { initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager },
78
+ ): number;
79
+
80
+ export function runQueueCli(
81
+ subcommand: string | undefined,
82
+ args: string[],
83
+ options?: {
84
+ initPortfolioQueue?: () => PortfolioQueueStore;
85
+ initPortfolioQueueManager?: (opts: unknown) => PortfolioQueueManager;
86
+ },
87
+ ): number;
@@ -0,0 +1,509 @@
1
+ import { initPortfolioQueueStore } from "./portfolio-queue.js";
2
+ import { initPortfolioQueueManager } from "./portfolio-queue-manager.js";
3
+ import { runPortfolioDashboard } from "./portfolio-dashboard.js";
4
+ import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
5
+
6
+ const QUEUE_LIST_USAGE = "Usage: gittensory-miner queue list [--repo <owner/repo>] [--json]";
7
+ const QUEUE_NEXT_USAGE =
8
+ "Usage: gittensory-miner queue next [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]";
9
+ const QUEUE_DONE_USAGE =
10
+ "Usage: gittensory-miner queue done <owner/repo> <identifier> [--api-base-url <url>] [--dry-run] [--json]";
11
+ const QUEUE_RELEASE_USAGE =
12
+ "Usage: gittensory-miner queue release <owner/repo> <identifier> [--api-base-url <url>] [--dry-run] [--json]";
13
+ const QUEUE_REQUEUE_USAGE =
14
+ "Usage: gittensory-miner queue requeue <owner/repo> <identifier> [--api-base-url <url>] [--dry-run] [--json]";
15
+ const QUEUE_CLAIM_BATCH_USAGE =
16
+ "Usage: gittensory-miner queue claim-batch [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]";
17
+
18
+ function parseRepoArg(value, usage) {
19
+ if (!value) return { error: usage };
20
+ const trimmed = value.trim();
21
+ const [owner, repo, extra] = trimmed.split("/");
22
+ if (!owner || !repo || extra !== undefined) {
23
+ return { error: "Repository must be in owner/repo form." };
24
+ }
25
+ return { repoFullName: `${owner}/${repo}` };
26
+ }
27
+
28
+ function parseJsonFlag(args) {
29
+ const options = { json: false, dryRun: false };
30
+ const positional = [];
31
+
32
+ for (const token of args) {
33
+ if (token === "--json") {
34
+ options.json = true;
35
+ continue;
36
+ }
37
+ // #4847: reports what a real mutation would do and returns before opening the portfolio queue at all.
38
+ if (token === "--dry-run") {
39
+ options.dryRun = true;
40
+ continue;
41
+ }
42
+ if (token.startsWith("-")) {
43
+ return { error: `Unknown option: ${token}` };
44
+ }
45
+ positional.push(token);
46
+ }
47
+
48
+ return { positional, ...options };
49
+ }
50
+
51
+ export function parseQueueListArgs(args) {
52
+ const options = { json: false, repoFullName: null };
53
+ const positional = [];
54
+
55
+ for (let index = 0; index < args.length; index += 1) {
56
+ const token = args[index];
57
+ if (token === "--json") {
58
+ options.json = true;
59
+ continue;
60
+ }
61
+ if (token === "--repo") {
62
+ const repoArg = args[index + 1];
63
+ if (!repoArg || repoArg.startsWith("-")) {
64
+ return { error: QUEUE_LIST_USAGE };
65
+ }
66
+ const repo = parseRepoArg(repoArg, QUEUE_LIST_USAGE);
67
+ if ("error" in repo) return repo;
68
+ options.repoFullName = repo.repoFullName;
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (token.startsWith("-")) {
73
+ return { error: `Unknown option: ${token}` };
74
+ }
75
+ positional.push(token);
76
+ }
77
+
78
+ if (positional.length > 0) {
79
+ return { error: QUEUE_LIST_USAGE };
80
+ }
81
+
82
+ return options;
83
+ }
84
+
85
+ // #4850: --global-wip/--per-repo-wip are OMITTED (undefined) by default -- queue next stays uncapped, byte-
86
+ // identical to its pre-#4850 behavior, unless an operator explicitly opts in. Mirrors queue claim-batch's own
87
+ // flag names (portfolio-queue-manager.js's WIP-cap-aware claimer), but claim-batch's OWN default of 1/1 is not
88
+ // reused here: claim-batch's whole purpose is cap enforcement, while queue next has always been a plain
89
+ // highest-priority dequeue and must not silently start capping existing callers that never asked for it.
90
+ export function parseQueueNextArgs(args) {
91
+ const options = { json: false, dryRun: false, globalWipCap: undefined, perRepoWipCap: undefined };
92
+ const positional = [];
93
+
94
+ for (let index = 0; index < args.length; index += 1) {
95
+ const token = args[index];
96
+ if (token === "--json") {
97
+ options.json = true;
98
+ continue;
99
+ }
100
+ if (token === "--dry-run") {
101
+ options.dryRun = true;
102
+ continue;
103
+ }
104
+ if (token === "--global-wip" || token === "--per-repo-wip") {
105
+ const value = Number(args[index + 1]);
106
+ if (args[index + 1] === undefined || !Number.isFinite(value) || value < 0) {
107
+ return { error: QUEUE_NEXT_USAGE };
108
+ }
109
+ if (token === "--global-wip") options.globalWipCap = value;
110
+ else options.perRepoWipCap = value;
111
+ index += 1;
112
+ continue;
113
+ }
114
+ if (token.startsWith("-")) {
115
+ return { error: `Unknown option: ${token}` };
116
+ }
117
+ positional.push(token);
118
+ }
119
+
120
+ if (positional.length > 0) {
121
+ return { error: QUEUE_NEXT_USAGE };
122
+ }
123
+ return options;
124
+ }
125
+
126
+ /**
127
+ * Pick at most one atomically-claimable target from the store's already-priority-ordered active rows (queued
128
+ * AND in_progress interleaved, exactly `batchClaim`'s own `entries` shape). `caps` of `null` replicates the
129
+ * pre-#4850 behavior: the single highest-priority queued row, unconditionally. When caps are set, refuses to
130
+ * select anything once the global or the target row's own per-repo in-progress count has reached its cap --
131
+ * "stops claiming once the cap is reached" (#4850), not a diversifying batch selection (that remains
132
+ * claim-batch's job via the engine's own `nextEligibleItems`).
133
+ * @param {Array<{ repoFullName: string, identifier: string, apiBaseUrl: string, status: string }>} entries
134
+ * @param {{ globalWipCap: number, perRepoWipCap: number } | null} caps
135
+ */
136
+ export function selectNextEligibleTarget(entries, caps) {
137
+ const topQueued = entries.find((entry) => entry.status === "queued");
138
+ if (!topQueued) return [];
139
+ if (!caps) {
140
+ return [{ repoFullName: topQueued.repoFullName, identifier: topQueued.identifier, apiBaseUrl: topQueued.apiBaseUrl }];
141
+ }
142
+ const globalActiveCount = entries.filter((entry) => entry.status === "in_progress").length;
143
+ if (globalActiveCount >= caps.globalWipCap) return [];
144
+ const repoActiveCount = entries.filter(
145
+ (entry) => entry.status === "in_progress" && entry.repoFullName === topQueued.repoFullName,
146
+ ).length;
147
+ if (repoActiveCount >= caps.perRepoWipCap) return [];
148
+ return [{ repoFullName: topQueued.repoFullName, identifier: topQueued.identifier, apiBaseUrl: topQueued.apiBaseUrl }];
149
+ }
150
+
151
+ /** Shared `<owner/repo> <identifier> [--api-base-url <url>] [--json]` parse for the item-targeting subcommands
152
+ * (done/release/requeue). `usage` is the command-specific message surfaced on a malformed argv. */
153
+ function parseRepoIdentifierArgs(args, usage) {
154
+ const options = { json: false, dryRun: false, apiBaseUrl: undefined };
155
+ const positional = [];
156
+
157
+ for (let index = 0; index < args.length; index += 1) {
158
+ const token = args[index];
159
+ if (token === "--json") {
160
+ options.json = true;
161
+ continue;
162
+ }
163
+ // #4847: reports what a real mutation would do and returns before opening the portfolio queue at all.
164
+ if (token === "--dry-run") {
165
+ options.dryRun = true;
166
+ continue;
167
+ }
168
+ // #5563: scope the target to a non-default forge host, so it doesn't collide with (or get confused for) a
169
+ // same-named repo on the default github.com host.
170
+ if (token === "--api-base-url") {
171
+ const value = args[index + 1];
172
+ if (!value || value.startsWith("-")) {
173
+ return { error: usage };
174
+ }
175
+ options.apiBaseUrl = value;
176
+ index += 1;
177
+ continue;
178
+ }
179
+ if (token.startsWith("-")) {
180
+ return { error: `Unknown option: ${token}` };
181
+ }
182
+ positional.push(token);
183
+ }
184
+
185
+ if (positional.length !== 2) {
186
+ return { error: usage };
187
+ }
188
+
189
+ const repo = parseRepoArg(positional[0], usage);
190
+ if ("error" in repo) return repo;
191
+
192
+ const identifier = positional[1]?.trim();
193
+ if (!identifier) {
194
+ return { error: usage };
195
+ }
196
+
197
+ return {
198
+ repoFullName: repo.repoFullName,
199
+ identifier,
200
+ dryRun: options.dryRun,
201
+ json: options.json,
202
+ apiBaseUrl: options.apiBaseUrl,
203
+ };
204
+ }
205
+
206
+ export function parseQueueDoneArgs(args) {
207
+ return parseRepoIdentifierArgs(args, QUEUE_DONE_USAGE);
208
+ }
209
+
210
+ export function parseQueueReleaseArgs(args) {
211
+ return parseRepoIdentifierArgs(args, QUEUE_RELEASE_USAGE);
212
+ }
213
+
214
+ export function parseQueueRequeueArgs(args) {
215
+ return parseRepoIdentifierArgs(args, QUEUE_REQUEUE_USAGE);
216
+ }
217
+
218
+ function display(value) {
219
+ if (value === null || value === undefined) return "-";
220
+ return String(value);
221
+ }
222
+
223
+ export function renderQueueTable(entries) {
224
+ if (!Array.isArray(entries) || entries.length === 0) return "no portfolio queue entries";
225
+ const header = [
226
+ "repo".padEnd(24),
227
+ "identifier".padEnd(16),
228
+ "status".padEnd(12),
229
+ "pri".padStart(4),
230
+ "enqueued-at".padEnd(24),
231
+ ].join(" ");
232
+ const lines = entries.map((entry) =>
233
+ [
234
+ entry.repoFullName.padEnd(24),
235
+ entry.identifier.padEnd(16),
236
+ entry.status.padEnd(12),
237
+ display(entry.priority).padStart(4),
238
+ display(entry.enqueuedAt).padEnd(24),
239
+ ].join(" "),
240
+ );
241
+ return [header, ...lines].join("\n");
242
+ }
243
+
244
+ function withPortfolioQueue(options, run) {
245
+ const ownsStore = options.initPortfolioQueue === undefined;
246
+ const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
247
+ try {
248
+ return run(portfolioQueue);
249
+ } finally {
250
+ if (ownsStore) portfolioQueue.close();
251
+ }
252
+ }
253
+
254
+ export function runQueueList(args, options = {}) {
255
+ const parsed = parseQueueListArgs(args);
256
+ if ("error" in parsed) {
257
+ return reportCliFailure(argsWantJson(args), parsed.error);
258
+ }
259
+
260
+ try {
261
+ return withPortfolioQueue(options, (portfolioQueue) => {
262
+ const entries = portfolioQueue.listQueue(parsed.repoFullName);
263
+ if (parsed.json) {
264
+ console.log(JSON.stringify({ entries }, null, 2));
265
+ } else {
266
+ console.log(renderQueueTable(entries));
267
+ }
268
+ return 0;
269
+ });
270
+ } catch (error) {
271
+ return reportCliFailure(parsed.json, describeCliError(error));
272
+ }
273
+ }
274
+
275
+ export function runQueueNext(args, options = {}) {
276
+ const parsed = parseQueueNextArgs(args);
277
+ if ("error" in parsed) {
278
+ return reportCliFailure(argsWantJson(args), parsed.error);
279
+ }
280
+
281
+ const capsRequested = parsed.globalWipCap !== undefined || parsed.perRepoWipCap !== undefined;
282
+ if (parsed.dryRun) {
283
+ const dryRunResult = capsRequested
284
+ ? { outcome: "dry_run", globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap }
285
+ : { outcome: "dry_run" };
286
+ if (parsed.json) {
287
+ console.log(JSON.stringify(dryRunResult, null, 2));
288
+ } else if (capsRequested) {
289
+ console.log(
290
+ `DRY RUN: would dequeue the highest-priority queued item within WIP caps (global-wip: ${parsed.globalWipCap ?? "unset"}, per-repo-wip: ${parsed.perRepoWipCap ?? "unset"}). No portfolio-queue write was made.`,
291
+ );
292
+ } else {
293
+ console.log("DRY RUN: would dequeue the highest-priority queued item. No portfolio-queue write was made.");
294
+ }
295
+ return 0;
296
+ }
297
+
298
+ try {
299
+ return withPortfolioQueue(options, (portfolioQueue) => {
300
+ let entry;
301
+ if (capsRequested) {
302
+ // Unset dimensions stay genuinely uncapped (Infinity), not silently defaulted to 1 like claim-batch.
303
+ const caps = {
304
+ globalWipCap: parsed.globalWipCap ?? Number.POSITIVE_INFINITY,
305
+ perRepoWipCap: parsed.perRepoWipCap ?? Number.POSITIVE_INFINITY,
306
+ };
307
+ const claimed = portfolioQueue.batchClaim((entries) => selectNextEligibleTarget(entries, caps));
308
+ entry = claimed[0] ?? null;
309
+ } else {
310
+ entry = portfolioQueue.dequeueNext();
311
+ }
312
+ if (parsed.json) {
313
+ console.log(JSON.stringify({ entry }, null, 2));
314
+ } else {
315
+ console.log(entry ? entry.identifier : "none");
316
+ }
317
+ return 0;
318
+ });
319
+ } catch (error) {
320
+ return reportCliFailure(parsed.json, describeCliError(error));
321
+ }
322
+ }
323
+
324
+ export function runQueueDone(args, options = {}) {
325
+ const parsed = parseQueueDoneArgs(args);
326
+ if ("error" in parsed) {
327
+ return reportCliFailure(argsWantJson(args), parsed.error);
328
+ }
329
+
330
+ if (parsed.dryRun) {
331
+ const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, identifier: parsed.identifier };
332
+ if (parsed.json) {
333
+ console.log(JSON.stringify(dryRunResult, null, 2));
334
+ } else {
335
+ console.log(`DRY RUN: would mark ${parsed.repoFullName} ${parsed.identifier} done. No portfolio-queue write was made.`);
336
+ }
337
+ return 0;
338
+ }
339
+
340
+ try {
341
+ return withPortfolioQueue(options, (portfolioQueue) => {
342
+ const entry = portfolioQueue.markDone(parsed.repoFullName, parsed.identifier, parsed.apiBaseUrl);
343
+ if (!entry) {
344
+ return reportCliFailure(parsed.json, "queue_entry_not_found");
345
+ }
346
+ if (parsed.json) {
347
+ console.log(JSON.stringify({ entry }, null, 2));
348
+ } else {
349
+ console.log(entry.status);
350
+ }
351
+ return 0;
352
+ });
353
+ } catch (error) {
354
+ return reportCliFailure(parsed.json, describeCliError(error));
355
+ }
356
+ }
357
+
358
+ /** `release <owner/repo> <identifier>`: manually give up a CLAIMED (in_progress) item, returning it to the queue
359
+ * (the manual counterpart to the automated stuck-lease sweep). Exit 2 when there is no in-flight item to release. */
360
+ export function runQueueRelease(args, options = {}) {
361
+ const parsed = parseQueueReleaseArgs(args);
362
+ if ("error" in parsed) {
363
+ return reportCliFailure(argsWantJson(args), parsed.error);
364
+ }
365
+
366
+ if (parsed.dryRun) {
367
+ const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, identifier: parsed.identifier };
368
+ if (parsed.json) {
369
+ console.log(JSON.stringify(dryRunResult, null, 2));
370
+ } else {
371
+ console.log(`DRY RUN: would release ${parsed.repoFullName} ${parsed.identifier} back to the queue. No portfolio-queue write was made.`);
372
+ }
373
+ return 0;
374
+ }
375
+
376
+ try {
377
+ return withPortfolioQueue(options, (portfolioQueue) => {
378
+ const entry = portfolioQueue.reclaimStuckItem(parsed.repoFullName, parsed.identifier, parsed.apiBaseUrl);
379
+ if (!entry) {
380
+ return reportCliFailure(parsed.json, "queue_entry_not_in_progress");
381
+ }
382
+ if (parsed.json) {
383
+ console.log(JSON.stringify({ entry }, null, 2));
384
+ } else {
385
+ console.log(entry.status);
386
+ }
387
+ return 0;
388
+ });
389
+ } catch (error) {
390
+ return reportCliFailure(parsed.json, describeCliError(error));
391
+ }
392
+ }
393
+
394
+ /** `requeue <owner/repo> <identifier>`: manually put a COMPLETED (done) item back on the queue so it is picked up
395
+ * again, keeping its original FIFO position. Exit 2 when there is no done item to requeue (already queued,
396
+ * in-flight — release it instead — or absent). */
397
+ export function runQueueRequeue(args, options = {}) {
398
+ const parsed = parseQueueRequeueArgs(args);
399
+ if ("error" in parsed) {
400
+ return reportCliFailure(argsWantJson(args), parsed.error);
401
+ }
402
+
403
+ if (parsed.dryRun) {
404
+ const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, identifier: parsed.identifier };
405
+ if (parsed.json) {
406
+ console.log(JSON.stringify(dryRunResult, null, 2));
407
+ } else {
408
+ console.log(`DRY RUN: would requeue ${parsed.repoFullName} ${parsed.identifier}. No portfolio-queue write was made.`);
409
+ }
410
+ return 0;
411
+ }
412
+
413
+ try {
414
+ return withPortfolioQueue(options, (portfolioQueue) => {
415
+ const entry = portfolioQueue.requeueItem(parsed.repoFullName, parsed.identifier, parsed.apiBaseUrl);
416
+ if (!entry) {
417
+ return reportCliFailure(parsed.json, "queue_entry_not_requeuable");
418
+ }
419
+ if (parsed.json) {
420
+ console.log(JSON.stringify({ entry }, null, 2));
421
+ } else {
422
+ console.log(entry.status);
423
+ }
424
+ return 0;
425
+ });
426
+ } catch (error) {
427
+ return reportCliFailure(parsed.json, describeCliError(error));
428
+ }
429
+ }
430
+
431
+ export function parseQueueClaimBatchArgs(args) {
432
+ const options = { json: false, dryRun: false, globalWipCap: 1, perRepoWipCap: 1 };
433
+ for (let index = 0; index < args.length; index += 1) {
434
+ const token = args[index];
435
+ if (token === "--json") {
436
+ options.json = true;
437
+ continue;
438
+ }
439
+ if (token === "--dry-run") {
440
+ options.dryRun = true;
441
+ continue;
442
+ }
443
+ if (token === "--global-wip" || token === "--per-repo-wip") {
444
+ const value = Number(args[index + 1]);
445
+ if (args[index + 1] === undefined || !Number.isFinite(value) || value < 0) {
446
+ return { error: QUEUE_CLAIM_BATCH_USAGE };
447
+ }
448
+ if (token === "--global-wip") options.globalWipCap = value;
449
+ else options.perRepoWipCap = value;
450
+ index += 1;
451
+ continue;
452
+ }
453
+ return { error: QUEUE_CLAIM_BATCH_USAGE };
454
+ }
455
+ return options;
456
+ }
457
+
458
+ /** Claim the next caps-aware batch via the WIP-cap-aware batch claimer (portfolio-queue-manager.js), which also
459
+ * reclaims any leases orphaned by a crashed process first (#4833 wires the previously caller-less claimer). */
460
+ export function runQueueClaimBatch(args, options = {}) {
461
+ const parsed = parseQueueClaimBatchArgs(args);
462
+ if ("error" in parsed) {
463
+ return reportCliFailure(argsWantJson(args), parsed.error);
464
+ }
465
+
466
+ if (parsed.dryRun) {
467
+ const dryRunResult = { outcome: "dry_run", globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap };
468
+ if (parsed.json) {
469
+ console.log(JSON.stringify(dryRunResult, null, 2));
470
+ } else {
471
+ console.log(
472
+ `DRY RUN: would claim a batch (global-wip: ${parsed.globalWipCap}, per-repo-wip: ${parsed.perRepoWipCap}). No portfolio-queue write was made.`,
473
+ );
474
+ }
475
+ return 0;
476
+ }
477
+
478
+ // Open the manager INSIDE the try so a store open failure returns 2 instead of crashing; the finally guards the
479
+ // close with `?.` since the initializer may have thrown before assigning.
480
+ const ownsManager = options.initPortfolioQueueManager === undefined;
481
+ let manager;
482
+ try {
483
+ manager = (options.initPortfolioQueueManager ?? initPortfolioQueueManager)({
484
+ caps: { globalWipCap: parsed.globalWipCap, perRepoWipCap: parsed.perRepoWipCap },
485
+ });
486
+ const claimed = manager.claimNextBatch();
487
+ if (parsed.json) {
488
+ console.log(JSON.stringify({ claimed }, null, 2));
489
+ } else {
490
+ console.log(claimed.length === 0 ? "none" : claimed.map((entry) => entry.identifier).join("\n"));
491
+ }
492
+ return 0;
493
+ } catch (error) {
494
+ return reportCliFailure(parsed.json, describeCliError(error));
495
+ } finally {
496
+ if (ownsManager) manager?.close();
497
+ }
498
+ }
499
+
500
+ export function runQueueCli(subcommand, args, options = {}) {
501
+ if (subcommand === "list") return runQueueList(args, options);
502
+ if (subcommand === "next") return runQueueNext(args, options);
503
+ if (subcommand === "done") return runQueueDone(args, options);
504
+ if (subcommand === "release") return runQueueRelease(args, options);
505
+ if (subcommand === "requeue") return runQueueRequeue(args, options);
506
+ if (subcommand === "claim-batch") return runQueueClaimBatch(args, options);
507
+ if (subcommand === "dashboard") return runPortfolioDashboard(args, options);
508
+ return reportCliFailure(argsWantJson(args), `Unknown queue subcommand: ${subcommand ?? ""}. ${QUEUE_LIST_USAGE}`);
509
+ }