@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,241 @@
1
+ import { initEventLedger } from "./event-ledger.js";
2
+ import { initPortfolioQueueStore } from "./portfolio-queue.js";
3
+ import { initRunStateStore } from "./run-state.js";
4
+ import { argsWantJson, reportCliFailure } from "./cli-error.js";
5
+
6
+ /** Event vocabulary for manage-phase PR snapshots written by manage poll. (#2325) */
7
+ export const MANAGE_PR_UPDATE_EVENT = "manage_pr_update";
8
+ export const MANAGED_PR_IDENTIFIER_PREFIX = "pr:";
9
+
10
+ export function parseManagedPrIdentifier(identifier) {
11
+ if (typeof identifier !== "string") return null;
12
+ const match = identifier.match(/^pr:(\d+)$/);
13
+ if (!match) return null;
14
+ const prNumber = Number(match[1]);
15
+ return Number.isInteger(prNumber) && prNumber > 0 ? prNumber : null;
16
+ }
17
+
18
+ export function formatManagedPrIdentifier(prNumber) {
19
+ if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("invalid_pr_number");
20
+ return `${MANAGED_PR_IDENTIFIER_PREFIX}${prNumber}`;
21
+ }
22
+
23
+ function optionalString(value) {
24
+ if (value === undefined || value === null) return null;
25
+ if (typeof value !== "string") return null;
26
+ const trimmed = value.trim();
27
+ return trimmed || null;
28
+ }
29
+
30
+ function normalizeManageUpdatePayload(payload) {
31
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
32
+ if (!Number.isInteger(payload.prNumber) || payload.prNumber <= 0) return null;
33
+ return {
34
+ prNumber: payload.prNumber,
35
+ branch: optionalString(payload.branch),
36
+ ciState: optionalString(payload.ciState),
37
+ gateVerdict: optionalString(payload.gateVerdict),
38
+ outcome: optionalString(payload.outcome),
39
+ lastPolledAt: optionalString(payload.lastPolledAt),
40
+ };
41
+ }
42
+
43
+ /** Index the latest manage snapshot per repo/PR from ascending ledger events. Pure. */
44
+ export function indexLatestManageUpdates(events) {
45
+ const latest = new Map();
46
+ for (const event of Array.isArray(events) ? events : []) {
47
+ if (event?.type !== MANAGE_PR_UPDATE_EVENT) continue;
48
+ if (typeof event.repoFullName !== "string" || !event.repoFullName.trim()) continue;
49
+ const normalized = normalizeManageUpdatePayload(event.payload);
50
+ if (!normalized) continue;
51
+ const key = `${event.repoFullName}:${normalized.prNumber}`;
52
+ latest.set(key, { ...normalized, repoFullName: event.repoFullName });
53
+ }
54
+ return latest;
55
+ }
56
+
57
+ /**
58
+ * Aggregate managed PR rows from the local portfolio queue and append-only event ledger. Read-only — never calls
59
+ * GitHub or mutates local stores. (#2325)
60
+ */
61
+ export function collectManageStatus(sources) {
62
+ const portfolioQueue = sources?.portfolioQueue;
63
+ const eventLedger = sources?.eventLedger;
64
+ if (!portfolioQueue || typeof portfolioQueue.listQueue !== "function") {
65
+ throw new Error("invalid_portfolio_queue");
66
+ }
67
+ if (!eventLedger || typeof eventLedger.readEvents !== "function") {
68
+ throw new Error("invalid_event_ledger");
69
+ }
70
+
71
+ const rowsByKey = new Map();
72
+ for (const entry of portfolioQueue.listQueue(null)) {
73
+ const prNumber = parseManagedPrIdentifier(entry.identifier);
74
+ if (prNumber === null) continue;
75
+ const key = `${entry.repoFullName}:${prNumber}`;
76
+ rowsByKey.set(key, {
77
+ repoFullName: entry.repoFullName,
78
+ prNumber,
79
+ branch: null,
80
+ ciState: null,
81
+ gateVerdict: null,
82
+ outcome: null,
83
+ lastPolledAt: null,
84
+ queueStatus: entry.status,
85
+ priority: entry.priority,
86
+ });
87
+ }
88
+
89
+ for (const [key, update] of indexLatestManageUpdates(eventLedger.readEvents())) {
90
+ const existing = rowsByKey.get(key);
91
+ rowsByKey.set(key, {
92
+ repoFullName: update.repoFullName,
93
+ prNumber: update.prNumber,
94
+ branch: update.branch,
95
+ ciState: update.ciState,
96
+ gateVerdict: update.gateVerdict,
97
+ outcome: update.outcome,
98
+ lastPolledAt: update.lastPolledAt,
99
+ queueStatus: existing?.queueStatus ?? null,
100
+ priority: existing?.priority ?? null,
101
+ });
102
+ }
103
+
104
+ return [...rowsByKey.values()].sort((left, right) => {
105
+ const repoCmp = left.repoFullName.localeCompare(right.repoFullName);
106
+ if (repoCmp !== 0) return repoCmp;
107
+ return left.prNumber - right.prNumber;
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Fold each tracked repo's current discover/plan/prepare run state alongside its managed PR rows into one
113
+ * "run portfolio" row per repo (#4279). `collectManageStatus` alone is PR-scoped only and never surfaces the
114
+ * run-state signal, so a repo actively discovering/planning with zero PRs yet is otherwise invisible. A repo
115
+ * appears here if it has EITHER a recorded run state OR at least one managed PR row.
116
+ */
117
+ export function collectRunPortfolio(sources) {
118
+ const runStateStore = sources?.runStateStore;
119
+ if (!runStateStore || typeof runStateStore.listRunStates !== "function") {
120
+ throw new Error("invalid_run_state_store");
121
+ }
122
+ const prsByRepo = new Map();
123
+ for (const row of collectManageStatus(sources)) {
124
+ const list = prsByRepo.get(row.repoFullName) ?? [];
125
+ list.push(row);
126
+ prsByRepo.set(row.repoFullName, list);
127
+ }
128
+ // NOTE (#5563): keyed by repoFullName alone, not apiBaseUrl -- this dashboard fold predates multi-forge run
129
+ // states and produces exactly ONE row per repo name. If the same repo name has a recorded run state on two
130
+ // different hosts, only one (the later entry in listRunStates' order) survives here; the other's row is still
131
+ // intact in the store, just not surfaced in this particular view. Safe (no data loss, no write), just a display
132
+ // limitation -- broadening this fold to be host-aware is a separate, larger dashboard-shape change.
133
+ const runStateByRepo = new Map(runStateStore.listRunStates().map((entry) => [entry.repoFullName, entry]));
134
+
135
+ const repoFullNames = new Set([...prsByRepo.keys(), ...runStateByRepo.keys()]);
136
+ return [...repoFullNames].sort((left, right) => left.localeCompare(right)).map((repoFullName) => {
137
+ const prs = prsByRepo.get(repoFullName) ?? [];
138
+ const runState = runStateByRepo.get(repoFullName);
139
+ return {
140
+ repoFullName,
141
+ runState: runState?.state ?? null,
142
+ runStateUpdatedAt: runState?.updatedAt ?? null,
143
+ prCount: prs.length,
144
+ prs,
145
+ };
146
+ });
147
+ }
148
+
149
+ function display(value) {
150
+ if (value === null || value === undefined) return "-";
151
+ return String(value);
152
+ }
153
+
154
+ export function renderManageStatusTable(rows) {
155
+ if (!Array.isArray(rows) || rows.length === 0) return "no managed pull requests";
156
+ const header = [
157
+ "repo".padEnd(24),
158
+ "pr".padStart(4),
159
+ "branch".padEnd(16),
160
+ "ci".padEnd(10),
161
+ "gate".padEnd(10),
162
+ "outcome".padEnd(10),
163
+ "last-polled".padEnd(20),
164
+ "queue".padEnd(12),
165
+ "pri".padStart(4),
166
+ ].join(" ");
167
+ const lines = rows.map((row) =>
168
+ [
169
+ row.repoFullName.padEnd(24),
170
+ String(row.prNumber).padStart(4),
171
+ display(row.branch).padEnd(16),
172
+ display(row.ciState).padEnd(10),
173
+ display(row.gateVerdict).padEnd(10),
174
+ display(row.outcome).padEnd(10),
175
+ display(row.lastPolledAt).padEnd(20),
176
+ display(row.queueStatus).padEnd(12),
177
+ display(row.priority).padStart(4),
178
+ ].join(" "),
179
+ );
180
+ return [header, ...lines].join("\n");
181
+ }
182
+
183
+ /** One row per tracked repo (run state + PR count), the compact companion to {@link renderManageStatusTable}'s
184
+ * per-PR detail (#4279). */
185
+ export function renderRunPortfolioTable(portfolio) {
186
+ if (!Array.isArray(portfolio) || portfolio.length === 0) return "no tracked repos";
187
+ const header = [
188
+ "repo".padEnd(24),
189
+ "run-state".padEnd(12),
190
+ "updated".padEnd(20),
191
+ "prs".padStart(4),
192
+ ].join(" ");
193
+ const lines = portfolio.map((entry) =>
194
+ [
195
+ entry.repoFullName.padEnd(24),
196
+ display(entry.runState).padEnd(12),
197
+ display(entry.runStateUpdatedAt).padEnd(20),
198
+ String(entry.prCount).padStart(4),
199
+ ].join(" "),
200
+ );
201
+ return [header, ...lines].join("\n");
202
+ }
203
+
204
+ export function parseManageStatusArgs(args = []) {
205
+ for (const token of args) {
206
+ if (token === "--json") continue;
207
+ if (token.startsWith("-")) return { error: `Unknown option: ${token}` };
208
+ return { error: "Usage: gittensory-miner manage status [--json]" };
209
+ }
210
+ return { json: args.includes("--json") };
211
+ }
212
+
213
+ export function runManageStatus(args = [], options = {}) {
214
+ const parsed = parseManageStatusArgs(args);
215
+ if ("error" in parsed) {
216
+ return reportCliFailure(argsWantJson(args), parsed.error);
217
+ }
218
+
219
+ const ownsPortfolioQueue = options.initPortfolioQueue === undefined;
220
+ const ownsEventLedger = options.initEventLedger === undefined;
221
+ const ownsRunStateStore = options.initRunStateStore === undefined;
222
+ const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
223
+ const eventLedger = (options.initEventLedger ?? initEventLedger)();
224
+ const runStateStore = (options.initRunStateStore ?? initRunStateStore)();
225
+ try {
226
+ const rows = collectManageStatus({ portfolioQueue, eventLedger });
227
+ const runPortfolio = collectRunPortfolio({ portfolioQueue, eventLedger, runStateStore });
228
+ if (parsed.json) {
229
+ // Additive only (#4279): `rows` keeps its existing shape unchanged; `runPortfolio` is a new key so an
230
+ // existing consumer parsing this JSON for `rows` alone sees byte-identical output.
231
+ console.log(JSON.stringify({ rows, runPortfolio }, null, 2));
232
+ } else {
233
+ console.log(`${renderManageStatusTable(rows)}\n\n${renderRunPortfolioTable(runPortfolio)}`);
234
+ }
235
+ return 0;
236
+ } finally {
237
+ if (ownsPortfolioQueue) portfolioQueue.close();
238
+ if (ownsEventLedger) eventLedger.close();
239
+ if (ownsRunStateStore) runStateStore.close();
240
+ }
241
+ }
@@ -0,0 +1,9 @@
1
+ import type { MinerPredictionMetricRow } from "@loopover/engine";
2
+ import type { PredictionLedger } from "./prediction-ledger.js";
3
+
4
+ export function collectPredictionMetricRows(ledger: PredictionLedger): MinerPredictionMetricRow[];
5
+
6
+ export function runMetrics(
7
+ args: string[],
8
+ options?: { initPredictionLedger?: () => PredictionLedger },
9
+ ): number;
@@ -0,0 +1,50 @@
1
+ import { renderMinerPredictionMetrics } from "@loopover/engine";
2
+ import { initPredictionLedger } from "./prediction-ledger.js";
3
+ import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
4
+
5
+ // `metrics` (#4838): render the miner's prediction-calibration counters as Prometheus text-exposition to stdout,
6
+ // for a scrape wrapper or cron redirect. The counters are produced by the engine's already-built
7
+ // renderMinerPredictionMetrics (packages/gittensory-engine/src/miner-prediction-metrics.ts) -- this command only
8
+ // reads the local prediction ledger and feeds it in, never touching the renderer itself. Strictly local + offline:
9
+ // no network, no writes.
10
+
11
+ const METRICS_USAGE = "Usage: gittensory-miner metrics";
12
+
13
+ /**
14
+ * Project prediction-ledger rows onto the engine renderer's metric-row shape -- the predicted `conclusion` only.
15
+ * The realized-outcome pairing (`correct`) is intentionally left unset: the miner has no outcome-join yet, so the
16
+ * correct/incorrect counters stay zero and only `predictions_total{conclusion}` moves -- exactly how the renderer
17
+ * is designed to degrade before outcome-pairing exists (see its header comment).
18
+ */
19
+ export function collectPredictionMetricRows(ledger) {
20
+ return ledger.readPredictions().map((entry) => ({ conclusion: entry.conclusion }));
21
+ }
22
+
23
+ // Open the local prediction ledger (or a test-injected one) for the duration of `run`, closing it only when we
24
+ // opened it -- an injected ledger is owned by the caller. Mirrors event-ledger-cli.js's withEventLedger.
25
+ function withPredictionLedger(options, run) {
26
+ const ownsLedger = options.initPredictionLedger === undefined;
27
+ const ledger = (options.initPredictionLedger ?? initPredictionLedger)();
28
+ try {
29
+ return run(ledger);
30
+ } finally {
31
+ if (ownsLedger) ledger.close();
32
+ }
33
+ }
34
+
35
+ export function runMetrics(args, options = {}) {
36
+ if (args.length > 0) {
37
+ return reportCliFailure(argsWantJson(args), METRICS_USAGE);
38
+ }
39
+
40
+ try {
41
+ return withPredictionLedger(options, (ledger) => {
42
+ // renderMinerPredictionMetrics returns a newline-terminated document; console.log re-adds the terminator, so
43
+ // trim it to emit exactly one trailing newline.
44
+ console.log(renderMinerPredictionMetrics(collectPredictionMetricRows(ledger)).trimEnd());
45
+ return 0;
46
+ });
47
+ } catch (error) {
48
+ return reportCliFailure(argsWantJson(args), describeCliError(error));
49
+ }
50
+ }
@@ -0,0 +1,24 @@
1
+ export type MigrateStatus = "skipped" | "up-to-date" | "migrated" | "failed";
2
+
3
+ export type MigrateResult = {
4
+ name: string;
5
+ dbPath: string;
6
+ ok: boolean;
7
+ status: MigrateStatus;
8
+ detail: string;
9
+ versionBefore: number | null;
10
+ versionAfter: number | null;
11
+ };
12
+
13
+ export type MigrateStoreDescriptor = {
14
+ name: string;
15
+ resolveDbPath: (env?: Record<string, string | undefined>) => string;
16
+ open: (dbPath: string) => { close: () => void };
17
+ };
18
+
19
+ export function runMigrateChecks(
20
+ env?: Record<string, string | undefined>,
21
+ stores?: MigrateStoreDescriptor[],
22
+ ): MigrateResult[];
23
+
24
+ export function runMigrate(args?: string[], env?: Record<string, string | undefined>): number;
@@ -0,0 +1,111 @@
1
+ // Proactive schema-migration runner for the miner's local SQLite stores (#4871). Every store already applies
2
+ // its own pending migrations (schema-version.js's applySchemaMigrations) as a side effect of being opened by
3
+ // whatever command happens to touch it first -- this command instead lets an operator PROACTIVELY bring every
4
+ // known store's EXISTING on-disk file up to date in one pass (e.g. right after upgrading, or before starting a
5
+ // fleet), without needing to guess which command happens to touch which store first. Mirrors status.js's
6
+ // storeIntegrityChecks [name, resolve*DbPath(env)] store list exactly (same seven stores `doctor` already
7
+ // covers), but actually OPENS each store (rather than a read-only integrity probe) so its real open/init
8
+ // function's migration path runs for real. A store file that does not exist yet is skipped, not created --
9
+ // "migrate" brings existing files up to date; it is not another way to bootstrap fresh state (that's `init`).
10
+ import { existsSync } from "node:fs";
11
+ import { DatabaseSync } from "node:sqlite";
12
+ import { readSchemaVersion } from "./schema-version.js";
13
+ import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js";
14
+ import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js";
15
+ import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js";
16
+ import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
17
+ import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js";
18
+ import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js";
19
+ import { openPlanStore, resolvePlanStoreDbPath } from "./plan-store.js";
20
+
21
+ const STORES = [
22
+ { name: "event-ledger", resolveDbPath: resolveEventLedgerDbPath, open: initEventLedger },
23
+ { name: "governor-ledger", resolveDbPath: resolveGovernorLedgerDbPath, open: initGovernorLedger },
24
+ { name: "prediction-ledger", resolveDbPath: resolvePredictionLedgerDbPath, open: initPredictionLedger },
25
+ { name: "portfolio-queue", resolveDbPath: resolvePortfolioQueueDbPath, open: initPortfolioQueueStore },
26
+ { name: "claim-ledger", resolveDbPath: resolveClaimLedgerDbPath, open: openClaimLedger },
27
+ { name: "run-state", resolveDbPath: resolveRunStateDbPath, open: initRunStateStore },
28
+ { name: "plan-store", resolveDbPath: resolvePlanStoreDbPath, open: openPlanStore },
29
+ ];
30
+
31
+ /** Read a store file's stamped schema version without ever creating it -- matches checkStoreIntegrity's
32
+ * "not created yet" convention: an absent file has nothing to report a version for. */
33
+ function peekSchemaVersion(dbPath) {
34
+ const db = new DatabaseSync(dbPath, { readOnly: true });
35
+ try {
36
+ return readSchemaVersion(db);
37
+ } finally {
38
+ db.close();
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Bring one store's EXISTING on-disk schema up to date. Never throws: a store that fails to open/migrate is
44
+ * reported as a failed result so one bad store cannot abort the whole sweep, matching doctor's per-store
45
+ * isolation. A store file that does not exist yet is reported as a clean skip (nothing to migrate), never
46
+ * created as a side effect of running this command.
47
+ * @returns {{ name: string, dbPath: string, ok: boolean, status: "skipped"|"up-to-date"|"migrated"|"failed", detail: string, versionBefore: number|null, versionAfter: number|null }}
48
+ */
49
+ function migrateStore({ name, resolveDbPath, open }, env) {
50
+ const dbPath = resolveDbPath(env);
51
+ if (!existsSync(dbPath)) {
52
+ return {
53
+ name,
54
+ dbPath,
55
+ ok: true,
56
+ status: "skipped",
57
+ detail: "not created yet",
58
+ versionBefore: null,
59
+ versionAfter: null,
60
+ };
61
+ }
62
+ // versionBefore is read INSIDE the same try as the migration itself: a corrupted file can throw on this very
63
+ // first read (a store that can't even be opened has no readable version either), and that must still surface
64
+ // as one failed store result rather than an uncaught exception aborting the whole sweep.
65
+ let versionBefore = null;
66
+ try {
67
+ versionBefore = peekSchemaVersion(dbPath);
68
+ const store = open(dbPath);
69
+ store.close();
70
+ const versionAfter = peekSchemaVersion(dbPath);
71
+ return {
72
+ name,
73
+ dbPath,
74
+ ok: true,
75
+ status: versionAfter > versionBefore ? "migrated" : "up-to-date",
76
+ detail: `v${versionBefore} -> v${versionAfter}`,
77
+ versionBefore,
78
+ versionAfter,
79
+ };
80
+ } catch (error) {
81
+ return {
82
+ name,
83
+ dbPath,
84
+ ok: false,
85
+ status: "failed",
86
+ detail: error instanceof Error ? error.message : String(error),
87
+ versionBefore,
88
+ versionAfter: versionBefore,
89
+ };
90
+ }
91
+ }
92
+
93
+ /** `stores` is injectable so tests can exercise a store descriptor's failure paths (e.g. a non-Error throw)
94
+ * without depending on real node:sqlite error shapes; defaults to the real seven-store list. */
95
+ export function runMigrateChecks(env = process.env, stores = STORES) {
96
+ return stores.map((store) => migrateStore(store, env));
97
+ }
98
+
99
+ export function runMigrate(args = [], env = process.env) {
100
+ const results = runMigrateChecks(env);
101
+ const failed = results.filter((result) => !result.ok);
102
+ if (args.includes("--json")) {
103
+ console.log(JSON.stringify({ ok: failed.length === 0, stores: results }, null, 2));
104
+ } else {
105
+ for (const result of results) {
106
+ console.log(`${result.ok ? result.status.padEnd(10) : "FAIL "} ${result.name}: ${result.detail}`);
107
+ }
108
+ if (failed.length > 0) console.error(`migrate: ${failed.length} store(s) failed`);
109
+ }
110
+ return failed.length === 0 ? 0 : 1;
111
+ }
@@ -0,0 +1,12 @@
1
+ import type { ParsedMinerGoalSpec } from "@loopover/engine";
2
+
3
+ export function resolveMinerGoalSpec(
4
+ repoPath: string,
5
+ options?: {
6
+ existsSync?: (path: string) => boolean;
7
+ openSync?: (path: string, flags: number) => number;
8
+ fstatSync?: (fd: number) => import("node:fs").Stats;
9
+ readSync?: (fd: number, buffer: Buffer, offset: number, length: number, position: number | null) => number;
10
+ closeSync?: (fd: number) => void;
11
+ },
12
+ ): ParsedMinerGoalSpec;
@@ -0,0 +1,67 @@
1
+ import { closeSync, constants, existsSync, fstatSync, openSync, readSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { discoverMinerGoalSpecPath, parseMinerGoalSpecContent } from "@loopover/engine";
4
+
5
+ const MAX_MINER_GOAL_SPEC_BYTES = 32_768;
6
+
7
+ // Real local .gittensory-miner.yml resolver (#5132, Wave 3.5 follow-up). MinerGoalSpec's own discovery
8
+ // helper (discoverMinerGoalSpecPath, packages/gittensory-engine) is deliberately IO-free -- the caller
9
+ // injects the existence check. Unlike self-review-context.js/rejection-signal.js/ams-policy.js, which fetch
10
+ // their target repo's files live over raw.githubusercontent.com BEFORE any clone exists, this resolver reads
11
+ // the ALREADY-CLONED repo on disk (attempt-worktree.js's prepareAttemptWorktree runs first in the real
12
+ // attempt-cli.js flow) -- no extra network round trip needed for a file that's already sitting in the
13
+ // worktree.
14
+
15
+ // Same convention as packages/gittensory-mcp/bin/gittensory-mcp.js's readCliTextFile: O_NOFOLLOW on open
16
+ // atomically rejects a symlinked path (no separate pre-open lstat -- that would be a check-then-open race, since
17
+ // a symlink can be swapped in between the lstat and the open). Bounds the READ itself, not just fstat's
18
+ // reported size, since a regular file can still grow between fstatSync and the read below.
19
+ function readRegularUtf8File(path, options) {
20
+ const openImpl = options.openSync ?? openSync;
21
+ const fstatImpl = options.fstatSync ?? fstatSync;
22
+ const readImpl = options.readSync ?? readSync;
23
+ const closeImpl = options.closeSync ?? closeSync;
24
+
25
+ const fd = openImpl(path, constants.O_RDONLY | constants.O_NOFOLLOW);
26
+ try {
27
+ const stats = fstatImpl(fd);
28
+ if (!stats.isFile() || stats.size > MAX_MINER_GOAL_SPEC_BYTES) return null;
29
+ const buffer = Buffer.alloc(MAX_MINER_GOAL_SPEC_BYTES + 1);
30
+ let bytesRead = 0;
31
+ while (bytesRead < buffer.length) {
32
+ const n = readImpl(fd, buffer, bytesRead, buffer.length - bytesRead, null);
33
+ if (n === 0) break;
34
+ bytesRead += n;
35
+ }
36
+ if (bytesRead > MAX_MINER_GOAL_SPEC_BYTES) return null;
37
+ return buffer.subarray(0, bytesRead).toString("utf8");
38
+ } finally {
39
+ closeImpl(fd);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Resolve the real, parsed MinerGoalSpec for an already-cloned repo at `repoPath`, trying each
45
+ * MINER_GOAL_SPEC_FILENAMES candidate in the documented discovery order. Never throws: a missing file, an
46
+ * unreadable file, or malformed content all degrade to the tolerant parser's own absent/safe-default result.
47
+ *
48
+ * Injected filesystem operations receive the FULL joined path (same convention as `node:fs`'s own
49
+ * functions), not a repoPath-relative candidate.
50
+ *
51
+ * @param {string} repoPath
52
+ * @param {{ existsSync?: (path: string) => boolean, openSync?: (path: string, flags: number) => number, fstatSync?: (fd: number) => import("node:fs").Stats, readSync?: (fd: number, buffer: Buffer, offset: number, length: number, position: number | null) => number, closeSync?: (fd: number) => void }} [options]
53
+ * @returns {import("@loopover/engine").ParsedMinerGoalSpec}
54
+ */
55
+ export function resolveMinerGoalSpec(repoPath, options = {}) {
56
+ const existsImpl = options.existsSync ?? existsSync;
57
+
58
+ const relativePath = discoverMinerGoalSpecPath((candidate) => existsImpl(join(repoPath, candidate)));
59
+ if (!relativePath) return parseMinerGoalSpecContent(null);
60
+
61
+ try {
62
+ const content = readRegularUtf8File(join(repoPath, relativePath), options);
63
+ return parseMinerGoalSpecContent(content);
64
+ } catch {
65
+ return parseMinerGoalSpecContent(null);
66
+ }
67
+ }
@@ -0,0 +1,86 @@
1
+ import type { ForgeConfig } from "./forge-config.js";
2
+ import type { PolicyDocCache } from "./policy-doc-cache.js";
3
+ import type { PolicyVerdictCache } from "./policy-verdict-cache.js";
4
+
5
+ export type FanoutTarget = {
6
+ owner: string;
7
+ repo: string;
8
+ };
9
+
10
+ /** Options shared by every fan-out entry point. `apiBaseUrl` is the legacy top-level forge-host override (it still
11
+ * wins over `forge.apiBaseUrl`); `forge` (#4784) carries the rest of the per-tenant forge knobs. `policyDocCache`,
12
+ * when supplied, lets discovery revalidate each repo's policy docs with a conditional GET instead of a full
13
+ * refetch (#4842). `policyVerdictCache`, when supplied, lets discovery reuse an already-resolved verdict once its
14
+ * deciding doc's ETag is confirmed unchanged, instead of re-resolving it (#4843). */
15
+ export type FanoutOptions = {
16
+ apiBaseUrl?: string;
17
+ forge?: Partial<ForgeConfig>;
18
+ concurrency?: number;
19
+ rateLimitLowWaterMark?: number;
20
+ rateLimitHighWaterMark?: number;
21
+ perPage?: number;
22
+ maxPages?: number;
23
+ sleepFn?: (ms: number) => Promise<unknown>;
24
+ policyDocCache?: PolicyDocCache | null;
25
+ policyVerdictCache?: PolicyVerdictCache | null;
26
+ };
27
+
28
+ export type RawCandidateIssue = {
29
+ owner: string;
30
+ repo: string;
31
+ repoFullName: string;
32
+ issueNumber: number;
33
+ title: string;
34
+ labels: string[];
35
+ commentsCount: number;
36
+ createdAt: string | null;
37
+ updatedAt: string | null;
38
+ htmlUrl: string | null;
39
+ aiPolicyAllowed: true;
40
+ aiPolicySource: "AI-USAGE.md" | "CONTRIBUTING.md" | "none";
41
+ };
42
+
43
+ export type CandidateIssueWarning = {
44
+ repoFullName: string;
45
+ stage: string;
46
+ message: string;
47
+ };
48
+
49
+ export type CandidateIssueSummary = {
50
+ issues: RawCandidateIssue[];
51
+ rateLimitRemaining: number | null;
52
+ rateLimitResetAt: string | null;
53
+ warnings: CandidateIssueWarning[];
54
+ };
55
+
56
+ export function mapWithConcurrency<T, R>(
57
+ items: T[],
58
+ maxConcurrency: number,
59
+ worker: (item: T, index: number) => Promise<R>,
60
+ resolveLimit: () => number,
61
+ sleepFn?: (ms: number) => Promise<unknown>,
62
+ ): Promise<R[]>;
63
+
64
+ export function fetchCandidateIssuesWithSummary(
65
+ targets: FanoutTarget[],
66
+ githubToken: string,
67
+ options?: FanoutOptions,
68
+ ): Promise<CandidateIssueSummary>;
69
+
70
+ export function fetchCandidateIssues(
71
+ targets: FanoutTarget[],
72
+ githubToken: string,
73
+ options?: FanoutOptions,
74
+ ): Promise<RawCandidateIssue[]>;
75
+
76
+ export function searchCandidateIssuesWithSummary(
77
+ searchQuery: string,
78
+ githubToken: string,
79
+ options?: FanoutOptions,
80
+ ): Promise<CandidateIssueSummary>;
81
+
82
+ export function searchCandidateIssues(
83
+ searchQuery: string,
84
+ githubToken: string,
85
+ options?: FanoutOptions,
86
+ ): Promise<RawCandidateIssue[]>;