@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,266 @@
1
+ import { initEventLedger } from "./event-ledger.js";
2
+ import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
3
+
4
+ const LEDGER_LIST_USAGE =
5
+ "Usage: gittensory-miner ledger list [--repo <owner/repo>] [--since <seq>] [--type <eventType>] [--json]";
6
+
7
+ function parseRepoArg(value, usage) {
8
+ if (!value) return { error: usage };
9
+ const trimmed = value.trim();
10
+ const [owner, repo, extra] = trimmed.split("/");
11
+ if (!owner || !repo || extra !== undefined) {
12
+ return { error: "Repository must be in owner/repo form." };
13
+ }
14
+ return { repoFullName: `${owner}/${repo}` };
15
+ }
16
+
17
+ function parseSinceArg(value) {
18
+ const since = Number(value);
19
+ if (!Number.isInteger(since) || since < 0) {
20
+ return { error: "since must be a non-negative integer seq cursor." };
21
+ }
22
+ return { since };
23
+ }
24
+
25
+ export function parseLedgerListArgs(args) {
26
+ const options = { json: false, repoFullName: null, since: null, type: null };
27
+ const positional = [];
28
+
29
+ for (let index = 0; index < args.length; index += 1) {
30
+ const token = args[index];
31
+ if (token === "--json") {
32
+ options.json = true;
33
+ continue;
34
+ }
35
+ if (token === "--repo") {
36
+ const repoArg = args[index + 1];
37
+ if (!repoArg || repoArg.startsWith("-")) return { error: LEDGER_LIST_USAGE };
38
+ const repo = parseRepoArg(repoArg, LEDGER_LIST_USAGE);
39
+ if ("error" in repo) return repo;
40
+ options.repoFullName = repo.repoFullName;
41
+ index += 1;
42
+ continue;
43
+ }
44
+ if (token === "--since") {
45
+ const sinceArg = args[index + 1];
46
+ if (!sinceArg || sinceArg.startsWith("--")) return { error: LEDGER_LIST_USAGE };
47
+ const parsedSince = parseSinceArg(sinceArg);
48
+ if ("error" in parsedSince) return parsedSince;
49
+ options.since = parsedSince.since;
50
+ index += 1;
51
+ continue;
52
+ }
53
+ if (token === "--type") {
54
+ const type = args[index + 1];
55
+ if (!type || type.startsWith("-")) return { error: LEDGER_LIST_USAGE };
56
+ options.type = type.trim();
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (token.startsWith("-")) return { error: `Unknown option: ${token}` };
61
+ positional.push(token);
62
+ }
63
+
64
+ if (positional.length > 0) return { error: LEDGER_LIST_USAGE };
65
+ return options;
66
+ }
67
+
68
+ export function filterLedgerEvents(events, options = {}) {
69
+ if (!Array.isArray(events)) return [];
70
+ const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null;
71
+ if (!type) return events;
72
+ return events.filter((entry) => entry.type === type);
73
+ }
74
+
75
+ /** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */
76
+ export const AUDIT_FEED_ENTRY_FIELDS = Object.freeze([
77
+ "eventType",
78
+ "repoFullName",
79
+ "outcome",
80
+ "actor",
81
+ "detail",
82
+ "createdAt",
83
+ ]);
84
+
85
+ function optionalMetadataString(value) {
86
+ if (typeof value !== "string") return null;
87
+ const trimmed = value.trim();
88
+ return trimmed || null;
89
+ }
90
+
91
+ /** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */
92
+ export function projectLedgerEventToAuditFeedEntry(entry) {
93
+ const payload =
94
+ entry?.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) ? entry.payload : {};
95
+ return {
96
+ eventType: entry.type,
97
+ repoFullName: entry.repoFullName,
98
+ outcome: optionalMetadataString(payload.outcome),
99
+ actor: optionalMetadataString(payload.actor),
100
+ detail: optionalMetadataString(payload.detail),
101
+ createdAt: entry.createdAt,
102
+ };
103
+ }
104
+
105
+ /** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */
106
+ export function normalizeAuditFeedMcpFilter(input = {}) {
107
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
108
+ throw new Error("filter must be an object");
109
+ }
110
+ const filter = { repoFullName: null, since: null, type: null };
111
+ if (input.repoFullName !== undefined && input.repoFullName !== null) {
112
+ const repo = parseRepoArg(String(input.repoFullName), "repoFullName must be in owner/repo form.");
113
+ if ("error" in repo) throw new Error(repo.error);
114
+ filter.repoFullName = repo.repoFullName;
115
+ }
116
+ if (input.since !== undefined && input.since !== null) {
117
+ const parsedSince = parseSinceArg(String(input.since));
118
+ if ("error" in parsedSince) throw new Error(parsedSince.error);
119
+ filter.since = parsedSince.since;
120
+ }
121
+ if (input.type !== undefined && input.type !== null) {
122
+ const trimmed = String(input.type).trim();
123
+ if (!trimmed) throw new Error("type must be a non-empty string.");
124
+ filter.type = trimmed;
125
+ }
126
+ return filter;
127
+ }
128
+
129
+ /** Read-only audit feed shared by the MCP audit-feed tool (#5158). */
130
+ export function collectEventLedgerAuditFeed(eventLedger, filter = {}) {
131
+ const events = filterLedgerEvents(
132
+ eventLedger.readEvents({
133
+ repoFullName: filter.repoFullName,
134
+ since: filter.since,
135
+ }),
136
+ { type: filter.type },
137
+ );
138
+ return {
139
+ ...(filter.repoFullName ? { repoFullName: filter.repoFullName } : {}),
140
+ events: events.map(projectLedgerEventToAuditFeedEntry),
141
+ };
142
+ }
143
+
144
+ function display(value) {
145
+ if (value === null || value === undefined) return "-";
146
+ return String(value);
147
+ }
148
+
149
+ export function renderLedgerTable(events) {
150
+ if (!Array.isArray(events) || events.length === 0) return "no event ledger entries";
151
+ const header = [
152
+ "seq".padStart(4),
153
+ "type".padEnd(20),
154
+ "repo".padEnd(24),
155
+ "created-at".padEnd(24),
156
+ ].join(" ");
157
+ const lines = events.map((entry) =>
158
+ [
159
+ String(entry.seq).padStart(4),
160
+ entry.type.padEnd(20),
161
+ display(entry.repoFullName).padEnd(24),
162
+ display(entry.createdAt).padEnd(24),
163
+ ].join(" "),
164
+ );
165
+ return [header, ...lines].join("\n");
166
+ }
167
+
168
+ const EVENT_LEDGER_METRICS_USAGE = "Usage: gittensory-miner ledger metrics";
169
+
170
+ // Prometheus metric name for the per-type event-ledger counter. Mirrors the `gittensory_miner_*_total` naming and
171
+ // the HELP/TYPE/label conventions of the engine's renderMinerPredictionMetrics
172
+ // (packages/gittensory-engine/src/miner-prediction-metrics.ts) rather than importing across the package boundary.
173
+ const MINER_EVENTS_TOTAL = "gittensory_miner_events_total";
174
+
175
+ /** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */
176
+ function escapeHelpText(help) {
177
+ return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
178
+ }
179
+
180
+ /** Prometheus label-value escaping — backslash, double-quote, newline — so an arbitrary event `type` string can
181
+ * never break the metric line (mirrors miner-prediction-metrics.ts's escapeLabelValue). */
182
+ function escapeLabelValue(value) {
183
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
184
+ }
185
+
186
+ /**
187
+ * Render event-ledger activity as Prometheus text-exposition counters: one `gittensory_miner_events_total{type}`
188
+ * series per event type, so a self-hoster's own Grafana/alerting can scrape ledger activity instead of polling
189
+ * `ledger list --json` (#4841). Pure + side-effect-free — the caller supplies the rows and prints the result;
190
+ * deterministic (series emitted in sorted type order); always emits HELP/TYPE so an empty ledger is still a
191
+ * well-formed exposition document.
192
+ */
193
+ export function renderEventLedgerMetrics(events) {
194
+ const totalByType = new Map();
195
+ for (const entry of events) {
196
+ totalByType.set(entry.type, (totalByType.get(entry.type) ?? 0) + 1);
197
+ }
198
+ const lines = [
199
+ `# HELP ${MINER_EVENTS_TOTAL} ${escapeHelpText("Event-ledger entries the miner has recorded, by event type.")}`,
200
+ `# TYPE ${MINER_EVENTS_TOTAL} counter`,
201
+ ];
202
+ for (const [type, count] of [...totalByType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
203
+ lines.push(`${MINER_EVENTS_TOTAL}{type="${escapeLabelValue(type)}"} ${count}`);
204
+ }
205
+ return `${lines.join("\n")}\n`;
206
+ }
207
+
208
+ function withEventLedger(options, run) {
209
+ const ownsLedger = options.initEventLedger === undefined;
210
+ const eventLedger = (options.initEventLedger ?? initEventLedger)();
211
+ try {
212
+ return run(eventLedger);
213
+ } finally {
214
+ if (ownsLedger) eventLedger.close();
215
+ }
216
+ }
217
+
218
+ export function runLedgerList(args, options = {}) {
219
+ const parsed = parseLedgerListArgs(args);
220
+ if ("error" in parsed) {
221
+ return reportCliFailure(argsWantJson(args), parsed.error);
222
+ }
223
+
224
+ try {
225
+ return withEventLedger(options, (eventLedger) => {
226
+ const events = filterLedgerEvents(
227
+ eventLedger.readEvents({
228
+ repoFullName: parsed.repoFullName,
229
+ since: parsed.since,
230
+ }),
231
+ { type: parsed.type },
232
+ );
233
+ if (parsed.json) {
234
+ console.log(JSON.stringify({ events }, null, 2));
235
+ } else {
236
+ console.log(renderLedgerTable(events));
237
+ }
238
+ return 0;
239
+ });
240
+ } catch (error) {
241
+ return reportCliFailure(parsed.json, describeCliError(error));
242
+ }
243
+ }
244
+
245
+ export function runLedgerMetrics(args, options = {}) {
246
+ if (args.length > 0) {
247
+ return reportCliFailure(argsWantJson(args), EVENT_LEDGER_METRICS_USAGE);
248
+ }
249
+
250
+ try {
251
+ return withEventLedger(options, (eventLedger) => {
252
+ // renderEventLedgerMetrics returns a newline-terminated document; console.log re-adds the terminator, so
253
+ // trim it to emit exactly one trailing newline (mirrors metrics-cli.js's runMetrics).
254
+ console.log(renderEventLedgerMetrics(eventLedger.readEvents()).trimEnd());
255
+ return 0;
256
+ });
257
+ } catch (error) {
258
+ return reportCliFailure(argsWantJson(args), describeCliError(error));
259
+ }
260
+ }
261
+
262
+ export function runLedgerCli(subcommand, args, options = {}) {
263
+ if (subcommand === "list") return runLedgerList(args, options);
264
+ if (subcommand === "metrics") return runLedgerMetrics(args, options);
265
+ return reportCliFailure(argsWantJson(args), `Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`);
266
+ }
@@ -0,0 +1,37 @@
1
+ export type LedgerEntry = {
2
+ id: number;
3
+ seq: number;
4
+ type: string;
5
+ repoFullName: string | null;
6
+ payload: Record<string, unknown>;
7
+ createdAt: string;
8
+ };
9
+
10
+ export type AppendEventInput = {
11
+ type: string;
12
+ repoFullName?: string;
13
+ payload: Record<string, unknown>;
14
+ };
15
+
16
+ export type ReadEventsFilter = {
17
+ repoFullName?: string | null;
18
+ since?: number | null;
19
+ };
20
+
21
+ export type EventLedger = {
22
+ dbPath: string;
23
+ appendEvent(event: AppendEventInput): LedgerEntry;
24
+ readEvents(filter?: ReadEventsFilter): LedgerEntry[];
25
+ purgeByRepo(repoFullName: string): number;
26
+ close(): void;
27
+ };
28
+
29
+ export function resolveEventLedgerDbPath(env?: Record<string, string | undefined>): string;
30
+
31
+ export function initEventLedger(dbPath?: string): EventLedger;
32
+
33
+ export function appendEvent(event: AppendEventInput): LedgerEntry;
34
+
35
+ export function readEvents(filter?: ReadEventsFilter): LedgerEntry[];
36
+
37
+ export function closeDefaultEventLedger(): void;
@@ -0,0 +1,210 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
3
+ import { applySchemaMigrations } from "./schema-version.js";
4
+ import {
5
+ EVENT_LEDGER_PURGE_SPEC,
6
+ EVENT_LEDGER_RETENTION_SPEC,
7
+ purgeStoreByRepo,
8
+ pruneLedgerByRetention,
9
+ resolveLedgerRetentionPolicy,
10
+ } from "./store-maintenance.js";
11
+
12
+ // The miner's local, append-only event ledger (#2290): an immutable audit trail of every significant miner-loop
13
+ // event (discovered_issue, plan_built, plan_step_completed, pr_prepared, … — a small fixed vocabulary for this
14
+ // foundation phase that grows in later phases), each stamped with a module-maintained monotonic `seq` and a
15
+ // timestamp. IMMUTABILITY INVARIANT: `appendEvent`/`readEvents` only ever issue INSERT and SELECT — they NEVER
16
+ // rewrite or remove a row, so a contributor auditing the miner's history later can trust it was not retroactively
17
+ // edited. Keep it that way: do not add mutation to the day-to-day append/read path. The two documented exceptions
18
+ // are opt-in retention pruning (#4834, automatic, age/size-bounded) and `purgeByRepo` (#5564, always explicit and
19
+ // operator-invoked, never automatic) — both are separate, clearly-labeled maintenance operations, not part of the
20
+ // ledger's normal operation. The database is 100% local; this module never uploads, syncs, or phones home with
21
+ // its contents. Mirrors the local-store pattern of run-state.js.
22
+
23
+ const defaultDbFileName = "event-ledger.sqlite3";
24
+ let defaultEventLedger = null;
25
+
26
+ export function resolveEventLedgerDbPath(env = process.env) {
27
+ return resolveLocalStoreDbPath(defaultDbFileName, "GITTENSORY_MINER_EVENT_LEDGER_DB", env);
28
+ }
29
+
30
+ function normalizeDbPath(dbPath) {
31
+ return normalizeLocalStoreDbPath(dbPath, resolveEventLedgerDbPath(), "invalid_event_ledger_db_path");
32
+ }
33
+
34
+ function normalizeEventType(type) {
35
+ if (typeof type !== "string") throw new Error("invalid_event_type");
36
+ const trimmed = type.trim();
37
+ if (!trimmed) throw new Error("invalid_event_type");
38
+ return trimmed;
39
+ }
40
+
41
+ /** Optional repo scope: omitted/nullish → null; otherwise a validated `owner/repo`. */
42
+ function normalizeOptionalRepoFullName(repoFullName) {
43
+ if (repoFullName === undefined || repoFullName === null) return null;
44
+ if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
45
+ const [owner, repo, extra] = repoFullName.trim().split("/");
46
+ if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
47
+ return `${owner}/${repo}`;
48
+ }
49
+
50
+ /** Optional seq cursor for polling: omitted → undefined; otherwise a non-negative integer last-seen seq. */
51
+ function normalizeOptionalSince(since) {
52
+ if (since === undefined || since === null) return undefined;
53
+ if (typeof since !== "number" || !Number.isInteger(since) || since < 0) {
54
+ throw new Error("invalid_since");
55
+ }
56
+ return since;
57
+ }
58
+
59
+ /** Read-filter repo scope: omitted/nullish → unscoped (all events); otherwise a validated `owner/repo`. */
60
+ function normalizeReadRepoFilter(repoFullName) {
61
+ if (repoFullName === undefined || repoFullName === null) return undefined;
62
+ return normalizeOptionalRepoFullName(repoFullName);
63
+ }
64
+
65
+ // Serialize an audit payload, enforcing that it round-trips through JSON VERBATIM. A plain JSON.stringify would
66
+ // silently drop `undefined`/function/symbol values and coerce `NaN`/`Infinity` to `null` (and throw on BigInt or a
67
+ // cycle), so a read-back would not equal the appended event. We reject any such lossy payload outright — an audit
68
+ // ledger must return exactly what was recorded.
69
+ function serializePayload(payload) {
70
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
71
+ throw new Error("invalid_payload");
72
+ }
73
+ let json;
74
+ try {
75
+ json = JSON.stringify(payload);
76
+ } catch {
77
+ throw new Error("invalid_payload"); // BigInt value or circular reference
78
+ }
79
+ if (!isDeepStrictEqual(JSON.parse(json), payload)) {
80
+ throw new Error("invalid_payload"); // a value JSON would drop or coerce (undefined/NaN/function/symbol/Date/…)
81
+ }
82
+ return json;
83
+ }
84
+
85
+ function rowToEntry(row) {
86
+ return {
87
+ id: row.id,
88
+ seq: row.seq,
89
+ type: row.event_type,
90
+ repoFullName: row.repo_full_name,
91
+ payload: JSON.parse(row.payload_json),
92
+ createdAt: row.created_at,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Opens the local append-only event ledger, creating the table on first use. `seq` is a monotonically increasing
98
+ * counter maintained by this module (next = current MAX(seq) + 1) rather than relying on `AUTOINCREMENT`'s
99
+ * reuse-after-vacuum behavior, so consumers get a stable ordering guarantee. Rows read back in `seq ASC` order.
100
+ * (#2290)
101
+ */
102
+ export function initEventLedger(dbPath = resolveEventLedgerDbPath()) {
103
+ const resolvedPath = normalizeDbPath(dbPath);
104
+ const db = openLocalStoreDb(resolvedPath);
105
+ // `UNIQUE(seq)` makes the monotonic-ordering guarantee an enforced invariant: a duplicate seq can never persist,
106
+ // even if the append path were ever changed.
107
+ db.exec(`
108
+ CREATE TABLE IF NOT EXISTS miner_event_ledger (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ seq INTEGER NOT NULL UNIQUE,
111
+ event_type TEXT NOT NULL,
112
+ repo_full_name TEXT,
113
+ payload_json TEXT NOT NULL,
114
+ created_at TEXT NOT NULL
115
+ )
116
+ `);
117
+ // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations (none yet).
118
+ applySchemaMigrations(db, []);
119
+ // Opt-in retention (#4834): prune aged/excess rows when an operator has enabled it; a no-op by default.
120
+ pruneLedgerByRetention(db, EVENT_LEDGER_RETENTION_SPEC, resolveLedgerRetentionPolicy(), Date.now());
121
+
122
+ const nextSeqStatement = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS nextSeq FROM miner_event_ledger");
123
+ const appendStatement = db.prepare(`
124
+ INSERT INTO miner_event_ledger (seq, event_type, repo_full_name, payload_json, created_at)
125
+ VALUES (?, ?, ?, ?, ?)
126
+ `);
127
+ const getByIdStatement = db.prepare("SELECT * FROM miner_event_ledger WHERE id = ?");
128
+ const readAllStatement = db.prepare("SELECT * FROM miner_event_ledger ORDER BY seq ASC");
129
+ const readByRepoStatement = db.prepare(
130
+ "SELECT * FROM miner_event_ledger WHERE repo_full_name = ? ORDER BY seq ASC",
131
+ );
132
+ const readSinceStatement = db.prepare(
133
+ "SELECT * FROM miner_event_ledger WHERE seq > ? ORDER BY seq ASC",
134
+ );
135
+ const readByRepoSinceStatement = db.prepare(
136
+ "SELECT * FROM miner_event_ledger WHERE repo_full_name = ? AND seq > ? ORDER BY seq ASC",
137
+ );
138
+
139
+ return {
140
+ dbPath: resolvedPath,
141
+ appendEvent(event) {
142
+ const type = normalizeEventType(event?.type);
143
+ const repoFullName = normalizeOptionalRepoFullName(event?.repoFullName);
144
+ const payloadJson = serializePayload(event?.payload);
145
+ const createdAt = new Date().toISOString();
146
+ // Serialize the read-then-write: BEGIN IMMEDIATE takes the write lock BEFORE reading MAX(seq), so two ledger
147
+ // instances on the same file cannot both compute the same next seq and corrupt the ordering guarantee.
148
+ db.exec("BEGIN IMMEDIATE");
149
+ try {
150
+ const { nextSeq } = nextSeqStatement.get();
151
+ const result = appendStatement.run(nextSeq, type, repoFullName, payloadJson, createdAt);
152
+ const entry = rowToEntry(getByIdStatement.get(Number(result.lastInsertRowid)));
153
+ db.exec("COMMIT");
154
+ return entry;
155
+ } catch (error) {
156
+ db.exec("ROLLBACK");
157
+ throw error;
158
+ }
159
+ },
160
+ readEvents(filter = {}) {
161
+ const repoFullName = normalizeReadRepoFilter(filter.repoFullName);
162
+ // `since` returns events with a seq STRICTLY greater than it — the "give me everything after the last seq I
163
+ // saw" polling shape.
164
+ const since = normalizeOptionalSince(filter.since);
165
+
166
+ let rows;
167
+ if (repoFullName !== undefined && since !== undefined) {
168
+ rows = readByRepoSinceStatement.all(repoFullName, since);
169
+ } else if (repoFullName !== undefined) {
170
+ rows = readByRepoStatement.all(repoFullName);
171
+ } else if (since !== undefined) {
172
+ rows = readSinceStatement.all(since);
173
+ } else {
174
+ rows = readAllStatement.all();
175
+ }
176
+ return rows.map(rowToEntry);
177
+ },
178
+ // Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. See the
179
+ // IMMUTABILITY INVARIANT note above: this is a deliberate, separate exception, not a normal ledger write.
180
+ // Requires a real repoFullName (unlike the optional filter above): a purge must never silently no-op on a
181
+ // missing/blank argument.
182
+ purgeByRepo(repoFullName) {
183
+ const normalized = normalizeOptionalRepoFullName(repoFullName);
184
+ if (normalized === null) throw new Error("invalid_repo_full_name");
185
+ return purgeStoreByRepo(db, EVENT_LEDGER_PURGE_SPEC, normalized);
186
+ },
187
+ close() {
188
+ db.close();
189
+ },
190
+ };
191
+ }
192
+
193
+ function getDefaultEventLedger() {
194
+ defaultEventLedger ??= initEventLedger();
195
+ return defaultEventLedger;
196
+ }
197
+
198
+ export function appendEvent(event) {
199
+ return getDefaultEventLedger().appendEvent(event);
200
+ }
201
+
202
+ export function readEvents(filter) {
203
+ return getDefaultEventLedger().readEvents(filter);
204
+ }
205
+
206
+ export function closeDefaultEventLedger() {
207
+ if (!defaultEventLedger) return;
208
+ defaultEventLedger.close();
209
+ defaultEventLedger = null;
210
+ }
@@ -0,0 +1,14 @@
1
+ import type { LocalWriteActionSpec } from "@loopover/engine";
2
+
3
+ export type ExecuteLocalWriteResult = {
4
+ action: string;
5
+ stdout: string;
6
+ stderr: string;
7
+ code: number | null;
8
+ timedOut: boolean;
9
+ };
10
+
11
+ export function executeLocalWrite(
12
+ spec: LocalWriteActionSpec,
13
+ options?: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number },
14
+ ): Promise<ExecuteLocalWriteResult>;
@@ -0,0 +1,50 @@
1
+ // Real executeLocalWrite implementation (#5132, Wave 3.5). Mirrors coding-agent-construction.js's
2
+ // createRealCliSubprocessSpawn pattern (real child_process, resolve-not-reject on error/timeout so a
3
+ // killed/errored process's partial output -- e.g. an auth failure line on stderr -- is never lost to an
4
+ // unhandled rejection) but for LocalWriteActionSpec.command: a single shell-safe string (built with
5
+ // packages/gittensory-engine/src/miner/local-write-tools.ts's own single-quote escaping), not the
6
+ // cmd/args-array CliSubprocessSpawnFn contract the coding-agent driver itself uses. Runs it via `sh -c` in
7
+ // the given working directory. Per local-write-tools.ts's own boundary comment, this always runs with
8
+ // whatever `gh`/`git` credentials are already configured in that environment -- gittensory never performs
9
+ // the write itself.
10
+
11
+ import { spawn } from "node:child_process";
12
+
13
+ const DEFAULT_TIMEOUT_MS = 120_000;
14
+
15
+ /**
16
+ * @param {import("@loopover/engine").LocalWriteActionSpec} spec
17
+ * @param {{ cwd?: string, env?: NodeJS.ProcessEnv, timeoutMs?: number }} [options]
18
+ * @returns {Promise<{ action: string, stdout: string, stderr: string, code: number | null, timedOut: boolean }>}
19
+ */
20
+ export function executeLocalWrite(spec, options = {}) {
21
+ const cwd = options.cwd ?? process.cwd();
22
+ const env = options.env ?? process.env;
23
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
24
+
25
+ return new Promise((resolve) => {
26
+ const child = spawn("sh", ["-c", spec.command], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
27
+ let stdout = "";
28
+ let stderr = "";
29
+ const timer = setTimeout(() => {
30
+ child.kill("SIGKILL");
31
+ resolve({ action: spec.action, stdout, stderr, code: null, timedOut: true });
32
+ }, timeoutMs);
33
+ child.stdout?.on("data", (chunk) => {
34
+ stdout += chunk.toString("utf8");
35
+ });
36
+ child.stderr?.on("data", (chunk) => {
37
+ stderr += chunk.toString("utf8");
38
+ });
39
+ child.on("error", (err) => {
40
+ // A spawn-level error (e.g. no `sh` on PATH) fires before the child ever produces output -- mirrors
41
+ // createRealCliSubprocessSpawn's own identical handling.
42
+ clearTimeout(timer);
43
+ resolve({ action: spec.action, stdout, stderr: err.message, code: null, timedOut: false });
44
+ });
45
+ child.on("close", (code) => {
46
+ clearTimeout(timer);
47
+ resolve({ action: spec.action, stdout, stderr, code, timedOut: false });
48
+ });
49
+ });
50
+ }
@@ -0,0 +1,25 @@
1
+ import type {
2
+ FeasibilityClaimStatus,
3
+ FeasibilityDuplicateClusterRisk,
4
+ FeasibilityGateInput,
5
+ FeasibilityGateResult,
6
+ FeasibilityIssueStatus,
7
+ } from "@loopover/engine";
8
+
9
+ export type ParsedFeasibilityArgs =
10
+ | {
11
+ claimStatus: FeasibilityClaimStatus;
12
+ duplicateClusterRisk: FeasibilityDuplicateClusterRisk;
13
+ issueStatus: FeasibilityIssueStatus;
14
+ found: boolean;
15
+ json: boolean;
16
+ }
17
+ | { error: string };
18
+
19
+ export type RunFeasibilityCliOptions = {
20
+ buildFeasibilityVerdict?: (input: FeasibilityGateInput) => FeasibilityGateResult;
21
+ };
22
+
23
+ export function parseFeasibilityArgs(args: string[]): ParsedFeasibilityArgs;
24
+
25
+ export function runFeasibilityCli(args: string[], options?: RunFeasibilityCliOptions): number;
@@ -0,0 +1,80 @@
1
+ /** `feasibility` CLI command (#4270): a thin parse -> execute -> render wrapper around the engine's pure
2
+ * `buildFeasibilityVerdict` composer. Purely local — no network, no filesystem — so it never needs the
3
+ * npm-registry update check other subcommands opt into. */
4
+ import { buildFeasibilityVerdict } from "@loopover/engine";
5
+ import { argsWantJson, reportCliFailure } from "./cli-error.js";
6
+
7
+ const CLAIM_STATUSES = ["unclaimed", "claimed", "solved", "unknown"];
8
+ const DUPLICATE_CLUSTER_RISKS = ["none", "low", "medium", "high"];
9
+ const ISSUE_STATUSES = ["ready", "needs_proof", "hold", "do_not_use", "duplicate", "invalid", "missing"];
10
+
11
+ const FEASIBILITY_USAGE =
12
+ "Usage: gittensory-miner feasibility <claimStatus> <duplicateClusterRisk> <issueStatus> [--not-found] [--json]\n" +
13
+ ` claimStatus: ${CLAIM_STATUSES.join("|")}\n` +
14
+ ` duplicateClusterRisk: ${DUPLICATE_CLUSTER_RISKS.join("|")}\n` +
15
+ ` issueStatus: ${ISSUE_STATUSES.join("|")}`;
16
+
17
+ export function parseFeasibilityArgs(args) {
18
+ const options = { json: false, found: true };
19
+ const positional = [];
20
+
21
+ for (const token of args) {
22
+ if (token === "--json") {
23
+ options.json = true;
24
+ continue;
25
+ }
26
+ if (token === "--not-found") {
27
+ options.found = false;
28
+ continue;
29
+ }
30
+ if (token.startsWith("-")) {
31
+ return { error: `Unknown option: ${token}` };
32
+ }
33
+ positional.push(token);
34
+ }
35
+
36
+ if (positional.length !== 3) {
37
+ return { error: FEASIBILITY_USAGE };
38
+ }
39
+
40
+ const [claimStatus, duplicateClusterRisk, issueStatus] = positional;
41
+ if (!CLAIM_STATUSES.includes(claimStatus)) {
42
+ return { error: `claimStatus must be one of: ${CLAIM_STATUSES.join(", ")}.` };
43
+ }
44
+ if (!DUPLICATE_CLUSTER_RISKS.includes(duplicateClusterRisk)) {
45
+ return { error: `duplicateClusterRisk must be one of: ${DUPLICATE_CLUSTER_RISKS.join(", ")}.` };
46
+ }
47
+ if (!ISSUE_STATUSES.includes(issueStatus)) {
48
+ return { error: `issueStatus must be one of: ${ISSUE_STATUSES.join(", ")}.` };
49
+ }
50
+
51
+ return {
52
+ claimStatus,
53
+ duplicateClusterRisk,
54
+ issueStatus,
55
+ found: options.found,
56
+ json: options.json,
57
+ };
58
+ }
59
+
60
+ export function runFeasibilityCli(args, options = {}) {
61
+ const parsed = parseFeasibilityArgs(args);
62
+ if ("error" in parsed) {
63
+ return reportCliFailure(argsWantJson(args), parsed.error);
64
+ }
65
+
66
+ const buildVerdict = options.buildFeasibilityVerdict ?? buildFeasibilityVerdict;
67
+ const verdict = buildVerdict({
68
+ found: parsed.found,
69
+ claimStatus: parsed.claimStatus,
70
+ duplicateClusterRisk: parsed.duplicateClusterRisk,
71
+ issueStatus: parsed.issueStatus,
72
+ });
73
+
74
+ if (parsed.json) {
75
+ console.log(JSON.stringify(verdict, null, 2));
76
+ } else {
77
+ console.log(`${verdict.verdict}: ${verdict.summary}`);
78
+ }
79
+ return 0;
80
+ }