@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,279 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js";
8
+ import {
9
+ collectEventLedgerAuditFeed,
10
+ normalizeAuditFeedMcpFilter,
11
+ } from "../lib/event-ledger-cli.js";
12
+ import { initEventLedger } from "../lib/event-ledger.js";
13
+ import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js";
14
+ import { initPortfolioQueueStore } from "../lib/portfolio-queue.js";
15
+ import { initRunStateStore } from "../lib/run-state.js";
16
+ import { PLAN_STATUSES, openPlanStore } from "../lib/plan-store.js";
17
+ import { initGovernorLedger } from "../lib/governor-ledger.js";
18
+ import { collectStatus, runDoctorChecks } from "../lib/status.js";
19
+
20
+ // MCP stdio server for @loopover/miner (scaffold #5153). Mirrors the packages/gittensory-mcp
21
+ // harness (MCP SDK server + stdio transport). Tools:
22
+ // - gittensory_miner_ping (#5153): trivial static health check, reads no AMS state.
23
+ // - gittensory_miner_get_portfolio_dashboard (#5155): read-only per-repo backlog dashboard, wrapping the
24
+ // existing collectPortfolioDashboard aggregator (no new logic; same data as `queue dashboard --json`).
25
+ // - gittensory_miner_list_claims (#5156): read-only listing of the local claim ledger (optional repo/status
26
+ // filter passed through to listClaims); exposes no claim/release mutation.
27
+ // - gittensory_miner_get_audit_feed (#5158): read-only metadata-only event-ledger audit feed via
28
+ // collectEventLedgerAuditFeed() (same filters as `ledger list`; never returns payload_json).
29
+ // - gittensory_miner_get_run_state (#5160): read-only per-repo run-state via run-state.js's getRunState/
30
+ // listRunStates (read-only analog of ORB's gittensory_get_automation_state; no state-set mutation).
31
+ // - gittensory_miner_list_plans / gittensory_miner_get_plan (#5161): read-only access to the persisted
32
+ // plan store via plan-store.js's listPlans/loadPlan (distinct from ORB's stateless gittensory_plan_status).
33
+ // - gittensory_miner_get_governor_decisions (#5159): read-only governor decision-log projection via
34
+ // governor-ledger.js's readGovernorDecisions -- an explicit named-column read that excludes payload_json.
35
+ // - gittensory_miner_status (#5154): read-only status + doctor diagnostics via status.js's collectStatus/
36
+ // runDoctorChecks (names/booleans/paths only -- never any env-var value, token, key, or credential).
37
+
38
+ // Read the version from this package's own package.json (always shipped) rather than a hand-synced
39
+ // literal, so a release bump never has a second place to forget -- same approach as the mcp harness.
40
+ const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
41
+
42
+ /** Optional filters accepted by gittensory_miner_get_audit_feed (#5158). */
43
+ const auditFeedInputSchema = {
44
+ repoFullName: z.string().min(1).optional(),
45
+ since: z.number().int().nonnegative().optional(),
46
+ type: z.string().min(1).optional(),
47
+ };
48
+
49
+ /** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */
50
+ export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" };
51
+
52
+ /**
53
+ * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`,
54
+ * `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`,
55
+ * `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default
56
+ * to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its
57
+ * store only when invoked and closes any store it opened.
58
+ */
59
+ export function createMinerMcpServer(options = {}) {
60
+ const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version });
61
+ server.registerTool(
62
+ "gittensory_miner_ping",
63
+ {
64
+ description:
65
+ "Health check for the gittensory-miner MCP server. Returns a static status object confirming the " +
66
+ "server is reachable. Reads no AMS state and takes no arguments.",
67
+ inputSchema: {},
68
+ },
69
+ async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] }),
70
+ );
71
+ server.registerTool(
72
+ "gittensory_miner_get_portfolio_dashboard",
73
+ {
74
+ description:
75
+ "Read-only per-repo portfolio-queue backlog dashboard: status counts (queued/in_progress/done), totals, " +
76
+ "and the oldest-queued age in ms. Wraps the existing collectPortfolioDashboard aggregator (no new logic) " +
77
+ "-- the same data `gittensory-miner queue dashboard --json` prints locally. Takes no arguments; mutates nothing.",
78
+ inputSchema: {},
79
+ },
80
+ async () => {
81
+ const ownsQueue = options.initPortfolioQueue === undefined;
82
+ const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
83
+ try {
84
+ const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: options.nowMs ?? Date.now() });
85
+ return { content: [{ type: "text", text: JSON.stringify(summary) }] };
86
+ } finally {
87
+ if (ownsQueue) portfolioQueue.close();
88
+ }
89
+ },
90
+ );
91
+ server.registerTool(
92
+ "gittensory_miner_list_claims",
93
+ {
94
+ description:
95
+ "Read-only listing of the local claim ledger: which issues this miner has claimed (repo, issue number, " +
96
+ "status, claimed-at, note). Optional repoFullName/status filters pass through to the existing listClaims " +
97
+ "query. Exposes no claim/release mutation and no conflict-resolution logic.",
98
+ inputSchema: {
99
+ repoFullName: z.string().optional(),
100
+ status: z.enum(CLAIM_STATUSES).optional(),
101
+ },
102
+ },
103
+ async ({ repoFullName, status }) => {
104
+ const ownsLedger = options.openClaimLedger === undefined;
105
+ const ledger = (options.openClaimLedger ?? openClaimLedger)();
106
+ try {
107
+ const filter = {};
108
+ if (repoFullName !== undefined) filter.repoFullName = repoFullName;
109
+ if (status !== undefined) filter.status = status;
110
+ return { content: [{ type: "text", text: JSON.stringify(ledger.listClaims(filter)) }] };
111
+ } finally {
112
+ if (ownsLedger) ledger.close();
113
+ }
114
+ },
115
+ );
116
+ server.registerTool(
117
+ "gittensory_miner_get_audit_feed",
118
+ {
119
+ description:
120
+ "Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " +
121
+ "outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " +
122
+ "the same read filters as `gittensory-miner ledger list` (--repo, --since, --type). Never returns " +
123
+ "payload_json or other raw ledger columns; never writes to the ledger.",
124
+ inputSchema: auditFeedInputSchema,
125
+ },
126
+ async (input) => {
127
+ const ownsLedger = options.initEventLedger === undefined;
128
+ const eventLedger = (options.initEventLedger ?? initEventLedger)();
129
+ try {
130
+ const filter = normalizeAuditFeedMcpFilter(input ?? {});
131
+ const feed = collectEventLedgerAuditFeed(eventLedger, filter);
132
+ return { content: [{ type: "text", text: JSON.stringify(feed) }] };
133
+ } catch (error) {
134
+ return {
135
+ content: [
136
+ {
137
+ type: "text",
138
+ text: JSON.stringify({
139
+ error: error instanceof Error ? error.message : String(error),
140
+ }),
141
+ },
142
+ ],
143
+ isError: true,
144
+ };
145
+ } finally {
146
+ if (ownsLedger) eventLedger.close();
147
+ }
148
+ },
149
+ );
150
+ server.registerTool(
151
+ "gittensory_miner_get_run_state",
152
+ {
153
+ description:
154
+ "Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single " +
155
+ "repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The " +
156
+ "read-only analog of ORB's gittensory_get_automation_state; adds no state-set or mutation capability.",
157
+ inputSchema: {
158
+ repoFullName: z.string().min(1).optional(),
159
+ },
160
+ },
161
+ async ({ repoFullName }) => {
162
+ const ownsStore = options.initRunStateStore === undefined;
163
+ const store = (options.initRunStateStore ?? initRunStateStore)();
164
+ try {
165
+ const result =
166
+ repoFullName === undefined
167
+ ? { states: store.listRunStates() }
168
+ : { repoFullName, state: store.getRunState(repoFullName) };
169
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
170
+ } finally {
171
+ if (ownsStore) store.close();
172
+ }
173
+ },
174
+ );
175
+ server.registerTool(
176
+ "gittensory_miner_list_plans",
177
+ {
178
+ description:
179
+ "Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally " +
180
+ "filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: " +
181
+ "this is the store-backed AMS plan store; it is distinct from ORB's stateless gittensory_plan_status " +
182
+ "tool, which reads the caller's in-memory plan object rather than any persisted store.",
183
+ inputSchema: {
184
+ status: z.enum(PLAN_STATUSES).optional(),
185
+ },
186
+ },
187
+ async ({ status }) => {
188
+ const ownsStore = options.openPlanStore === undefined;
189
+ const store = (options.openPlanStore ?? openPlanStore)();
190
+ try {
191
+ const filter = {};
192
+ if (status !== undefined) filter.status = status;
193
+ return { content: [{ type: "text", text: JSON.stringify(store.listPlans(filter)) }] };
194
+ } finally {
195
+ if (ownsStore) store.close();
196
+ }
197
+ },
198
+ );
199
+ server.registerTool(
200
+ "gittensory_miner_get_plan",
201
+ {
202
+ description:
203
+ "Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an " +
204
+ "explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- " +
205
+ "no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless " +
206
+ "gittensory_plan_status tool.",
207
+ inputSchema: {
208
+ planId: z.string().min(1),
209
+ },
210
+ },
211
+ async ({ planId }) => {
212
+ const ownsStore = options.openPlanStore === undefined;
213
+ const store = (options.openPlanStore ?? openPlanStore)();
214
+ try {
215
+ const plan = store.loadPlan(planId);
216
+ const result = plan === null ? { planId, found: false } : { found: true, plan };
217
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
218
+ } finally {
219
+ if (ownsStore) store.close();
220
+ }
221
+ },
222
+ );
223
+ server.registerTool(
224
+ "gittensory_miner_get_governor_decisions",
225
+ {
226
+ description:
227
+ "Read-only projection of the governor decision log: id, ts, eventType, repoFullName, actionClass, " +
228
+ "decision, reason per row. This projection INTENTIONALLY EXCLUDES the internal/sensitive payload column " +
229
+ "(reputation / self-plagiarism / budget state) by construction -- governor-ledger.js reads it with an " +
230
+ "explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger " +
231
+ "supports natively). Read-only; never writes to the ledger.",
232
+ inputSchema: {
233
+ repoFullName: z.string().min(1).optional(),
234
+ },
235
+ },
236
+ async ({ repoFullName }) => {
237
+ const ownsLedger = options.initGovernorLedger === undefined;
238
+ const ledger = (options.initGovernorLedger ?? initGovernorLedger)();
239
+ try {
240
+ const filter = {};
241
+ if (repoFullName !== undefined) filter.repoFullName = repoFullName;
242
+ return { content: [{ type: "text", text: JSON.stringify(ledger.readGovernorDecisions(filter)) }] };
243
+ } finally {
244
+ if (ownsLedger) ledger.close();
245
+ }
246
+ },
247
+ );
248
+ server.registerTool(
249
+ "gittensory_miner_status",
250
+ {
251
+ description:
252
+ "Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " +
253
+ "(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " +
254
+ "name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " +
255
+ "`gittensory-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " +
256
+ "collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " +
257
+ "any env-var value, token, key, or credential. Read-only; no writes or state changes.",
258
+ inputSchema: {},
259
+ },
260
+ async () => {
261
+ const status = (options.collectStatus ?? collectStatus)();
262
+ const doctor = (options.runDoctorChecks ?? runDoctorChecks)();
263
+ return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] };
264
+ },
265
+ );
266
+ return server;
267
+ }
268
+
269
+ // Start the stdio transport only when executed directly as the bin, not when imported by a test.
270
+ // realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches.
271
+ const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
272
+ if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) {
273
+ createMinerMcpServer()
274
+ .connect(new StdioServerTransport())
275
+ .catch((error) => {
276
+ console.error(error);
277
+ process.exit(1);
278
+ });
279
+ }
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ import { runAttempt } from "../lib/attempt-cli.js";
3
+ import { printHelp, printVersion, runCli } from "../lib/cli.js";
4
+ import { configureLogger, extractLogOptions } from "../lib/logger.js";
5
+ import { runDenyCheck } from "../lib/deny-check.js";
6
+ import { runDiscover } from "../lib/discover-cli.js";
7
+ import { runFeasibilityCli } from "../lib/feasibility-cli.js";
8
+ import { runGovernorCli } from "../lib/governor-ledger-cli.js";
9
+ import { runLedgerCli } from "../lib/event-ledger-cli.js";
10
+ import { runCalibrationCli } from "../lib/calibration-cli.js";
11
+ import { runLoop } from "../lib/loop-cli.js";
12
+ import { runManagePoll } from "../lib/manage-poll.js";
13
+ import { runManageStatus } from "../lib/manage-status.js";
14
+ import { runMetrics } from "../lib/metrics-cli.js";
15
+ import { runPlanCli } from "../lib/plan-store-cli.js";
16
+ import { runClaimCli } from "../lib/claim-ledger-cli.js";
17
+ import { runPurge } from "../lib/purge-cli.js";
18
+ import { runQueueCli } from "../lib/portfolio-queue-cli.js";
19
+ import { runOrbExportCli } from "../lib/orb-export.js";
20
+ import { installCliSignalHandlers } from "../lib/process-lifecycle.js";
21
+ import { runStateCli } from "../lib/run-state-cli.js";
22
+ import { runInit } from "../lib/laptop-init.js";
23
+ import { loadMinerFileSecrets } from "../lib/env-file-indirection.js";
24
+ import { runMigrate } from "../lib/migrate-cli.js";
25
+ import { runDoctor, runStatus } from "../lib/status.js";
26
+ import {
27
+ awaitOpportunisticUpdateCheck,
28
+ resolveUpgradeCommand,
29
+ startUpdateCheck,
30
+ } from "../lib/update-check.js";
31
+ import { resolveMinerVersion } from "../lib/version.js";
32
+
33
+ // Resolve any `<NAME>_FILE` secret-mount vars (GITHUB_TOKEN_FILE, etc.) into their plain counterparts FIRST,
34
+ // before anything else reads process.env -- every subcommand below (and the coding-agent driver construction
35
+ // deeper in the call graph) reads plain env vars, so this single early pass is all that's needed for the whole
36
+ // CLI (#5178). A broken secret mount fails the process fast and loud with a clear message, instead of an
37
+ // uncaught-exception stack trace or a silent empty credential surfacing as a confusing GitHub 401 later.
38
+ try {
39
+ loadMinerFileSecrets();
40
+ } catch (error) {
41
+ console.error(error instanceof Error ? error.message : String(error));
42
+ process.exit(1);
43
+ }
44
+
45
+ // Register signal + crash handlers once, before any command runs, so an interrupted run closes its open ledgers
46
+ // cleanly instead of dying mid-write (#4826). Covers every subcommand below, including the local ones.
47
+ installCliSignalHandlers();
48
+
49
+ // Peel the global logging flags (--quiet/--verbose/--log-level) off the front of argv and configure the
50
+ // process-wide logger once (#4835), so every command below shares one level-aware logger without re-parsing
51
+ // them; the stripped `cliArgs` is what the command dispatch sees.
52
+ const { options: logOptions, rest: cliArgs } = extractLogOptions(process.argv.slice(2));
53
+ configureLogger({ ...logOptions, env: process.env });
54
+
55
+ // `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls.
56
+ // `init` stays local by default and only makes a network call when the operator explicitly passes
57
+ // `--verify-token`.
58
+ // Dispatch the local commands BEFORE the opportunistic npm-registry update check is even started, so they can
59
+ // never reach that network path (the update check runs for the remaining commands below).
60
+ if (cliArgs[0] === "init") {
61
+ process.exit(await runInit(cliArgs.slice(1)));
62
+ }
63
+
64
+ if (cliArgs[0] === "status") {
65
+ process.exit(runStatus(cliArgs.slice(1)));
66
+ }
67
+
68
+ if (cliArgs[0] === "doctor") {
69
+ process.exit(runDoctor(cliArgs.slice(1)));
70
+ }
71
+
72
+ // `migrate` is strictly local + offline like `status`/`doctor` (it only opens the local SQLite stores), so it is
73
+ // dispatched here too, before the opportunistic npm-registry update check is ever started.
74
+ if (cliArgs[0] === "migrate") {
75
+ process.exit(runMigrate(cliArgs.slice(1)));
76
+ }
77
+
78
+ // `metrics` is strictly local + offline like `status`/`doctor` (it reads only the local prediction ledger), so it
79
+ // is dispatched here, before the opportunistic npm-registry update check is ever started.
80
+ if (cliArgs[0] === "metrics") {
81
+ process.exit(runMetrics(cliArgs.slice(1)));
82
+ }
83
+
84
+ if (cliArgs[0] === "manage" && cliArgs[1] === "status") {
85
+ process.exit(runManageStatus(cliArgs.slice(2)));
86
+ }
87
+
88
+ if (cliArgs[0] === "queue") {
89
+ process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2)));
90
+ }
91
+
92
+ if (cliArgs[0] === "orb" && cliArgs[1] === "export") {
93
+ process.exit(runOrbExportCli(cliArgs.slice(2)));
94
+ }
95
+
96
+ if (cliArgs[0] === "claim") {
97
+ process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2)));
98
+ }
99
+
100
+ if (cliArgs[0] === "ledger") {
101
+ process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2)));
102
+ }
103
+
104
+ if (cliArgs[0] === "calibration") {
105
+ process.exit(runCalibrationCli(cliArgs.slice(1)));
106
+ }
107
+
108
+ if (cliArgs[0] === "plan") {
109
+ process.exit(runPlanCli(cliArgs[1], cliArgs.slice(2)));
110
+ }
111
+
112
+ if (cliArgs[0] === "governor") {
113
+ process.exit(await runGovernorCli(cliArgs[1], cliArgs.slice(2)));
114
+ }
115
+
116
+ if (cliArgs[0] === "feasibility") {
117
+ process.exit(runFeasibilityCli(cliArgs.slice(1)));
118
+ }
119
+
120
+ // `purge` (#5564) is strictly local + offline like `queue`/`claim`/`governor` above -- it only opens the local
121
+ // SQLite stores, so it is dispatched here too, before the opportunistic npm-registry update check ever starts.
122
+ if (cliArgs[0] === "purge") {
123
+ process.exit(runPurge(cliArgs.slice(1)));
124
+ }
125
+
126
+ const packageName = "@loopover/miner";
127
+ const packageVersion = resolveMinerVersion(process.env);
128
+ const upgradeCommand = resolveUpgradeCommand(packageName);
129
+
130
+ const updateCheck = startUpdateCheck(cliArgs, {
131
+ packageName,
132
+ packageVersion,
133
+ upgradeCommand,
134
+ env: process.env,
135
+ });
136
+
137
+ if (
138
+ cliArgs.length === 0 ||
139
+ cliArgs.includes("--help") ||
140
+ cliArgs.includes("-h") ||
141
+ cliArgs[0] === "help"
142
+ ) {
143
+ printHelp({ packageName });
144
+ await awaitOpportunisticUpdateCheck(updateCheck);
145
+ process.exit(0);
146
+ }
147
+
148
+ if (
149
+ cliArgs.includes("--version") ||
150
+ cliArgs.includes("-v") ||
151
+ cliArgs[0] === "version"
152
+ ) {
153
+ printVersion({ packageName, packageVersion });
154
+ await awaitOpportunisticUpdateCheck(updateCheck);
155
+ process.exit(0);
156
+ }
157
+
158
+ if (cliArgs[0] === "hooks" && cliArgs[1] === "check") {
159
+ const exitCode = runDenyCheck(cliArgs.slice(2));
160
+ await awaitOpportunisticUpdateCheck(updateCheck);
161
+ process.exit(exitCode);
162
+ }
163
+
164
+ if (cliArgs[0] === "state") {
165
+ const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2));
166
+ await awaitOpportunisticUpdateCheck(updateCheck);
167
+ process.exit(exitCode);
168
+ }
169
+
170
+ if (cliArgs[0] === "manage" && cliArgs[1] === "poll") {
171
+ const exitCode = await runManagePoll(cliArgs.slice(2));
172
+ await awaitOpportunisticUpdateCheck(updateCheck);
173
+ process.exit(exitCode);
174
+ }
175
+
176
+ if (cliArgs[0] === "discover") {
177
+ const exitCode = await runDiscover(cliArgs.slice(1));
178
+ await awaitOpportunisticUpdateCheck(updateCheck);
179
+ process.exit(exitCode);
180
+ }
181
+
182
+ if (cliArgs[0] === "attempt") {
183
+ const exitCode = await runAttempt(cliArgs.slice(1));
184
+ await awaitOpportunisticUpdateCheck(updateCheck);
185
+ process.exit(exitCode);
186
+ }
187
+
188
+ if (cliArgs[0] === "loop") {
189
+ const exitCode = await runLoop(cliArgs.slice(1));
190
+ await awaitOpportunisticUpdateCheck(updateCheck);
191
+ process.exit(exitCode);
192
+ }
193
+
194
+ const exitCode = runCli(cliArgs, { packageName });
195
+ await awaitOpportunisticUpdateCheck(updateCheck);
196
+ process.exit(exitCode);
@@ -0,0 +1,140 @@
1
+ # CodingAgentDriver — the miner's coding-agent seam
2
+
3
+ `CodingAgentDriver` is the single interface a Gittensory miner runs a coding agent through. It lets the miner drive
4
+ **either** a local CLI subprocess (e.g. `claude` / `codex`) **or** an in-process Agent SDK `query()` loop behind one
5
+ provider-agnostic contract, so the rest of the miner — planning, attempt logging, metering, gate polling — never
6
+ has to know which backend actually did the work.
7
+
8
+ The interface itself lives in `@loopover/engine`
9
+ ([`packages/gittensory-engine/src/miner/coding-agent-driver.ts`](../../gittensory-engine/src/miner/coding-agent-driver.ts));
10
+ the orchestration around it (mode gating, invocation, the factory) lives in the sibling modules described below.
11
+
12
+ > **See also:** [Observing your miner](observability.md) — point Grafana at the miner's local SQLite ledgers
13
+ > (attempt log, prediction ledger) to see what the driver actually did.
14
+
15
+ ## Why a seam, and why this shape
16
+
17
+ The design deliberately mirrors the review stack's `SelfHostAi` (`src/selfhost/ai.ts`) rather than inventing a new
18
+ pattern: a single `run()` method, provider-agnostic task/result types, and **injected dependencies** (spawn fn,
19
+ clock, filesystem) on the concrete implementations rather than hardcoded globals. That injection is what keeps every
20
+ driver unit-testable without real IO — the same reason `SelfHostAi` takes an injected `SpawnFn`.
21
+
22
+ The interface defines only the contract. Implementations MAY perform real IO; they never make GitHub writes or
23
+ autonomous continue/stop decisions — the task handed to a driver is already scoped.
24
+
25
+ ## The contract
26
+
27
+ ```ts
28
+ type CodingAgentDriverTask = {
29
+ attemptId: string; // stable id for this attempt (keys the attempt log)
30
+ workingDirectory: string; // the ONLY directory a driver may edit (see worktree isolation below)
31
+ acceptanceCriteriaPath: string; // path to the immutable acceptance-criteria file written before the run
32
+ instructions: string; // the metadata-only prompt (no source contents)
33
+ maxTurns: number; // hard cap on agent iterations for this attempt
34
+ };
35
+
36
+ type CodingAgentDriverResult = {
37
+ ok: boolean;
38
+ changedFiles: readonly string[];
39
+ summary: string;
40
+ transcript?: string; // opaque provider transcript for operator inspection
41
+ turnsUsed?: number;
42
+ error?: string;
43
+ };
44
+
45
+ interface CodingAgentDriver {
46
+ run(task: CodingAgentDriverTask): Promise<CodingAgentDriverResult>;
47
+ }
48
+ ```
49
+
50
+ Two reference implementations ship today for tests: `createFakeCodingAgentDriver` (records the last task, no IO) and
51
+ `createNoopCodingAgentDriver` (default-OFF stub). The two real backends — a CLI-subprocess driver (#4266) and an
52
+ Agent-SDK driver (#4267) — register in `createCodingAgentDriver` as `claude-cli`, `codex-cli`, and `agent-sdk`
53
+ (`CODING_AGENT_DRIVER_NAMES`: `["noop", "claude-cli", "codex-cli", "agent-sdk"]`).
54
+
55
+ ## The surrounding primitives
56
+
57
+ A driver never runs in isolation. The neighborhood it plugs into:
58
+
59
+ | Concern | Module | What it provides |
60
+ | ------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
+ | Execution mode | `coding-agent-mode.ts` | `paused` / `dry_run` / `live` with deny-toward-safety precedence; `codingAgentModeExecutes(mode)` is the single "should this attempt actually spawn?" boolean. A `dry_run` is a pure no-op at the driver boundary. |
62
+ | Invocation | `coding-agent-invoke.ts` | `invokeCodingAgentDriver(driver, task, mode?, log?)` — gates on the mode, calls `driver.run`, and streams lifecycle events to an `AttemptLogSink`. |
63
+ | Factory | `driver-factory.ts` | `createCodingAgentDriver(options)` resolves a driver by configured name; `resolveConfiguredCodingAgentDriverNames` / `isConfiguredCodingAgentDriver` deny unknown names by default; `runCodingAgentAttempt(options)` is the top-level "resolve + invoke" convenience. |
64
+ | Attempt log | `attempt-log.ts` (#4294) | `ATTEMPT_LOG_EVENT_TYPES` + `normalizeAttemptLogEvent` + `createAttemptLogBuffer` + `formatAttemptLogJsonl` — an append-only, JSONL-exportable event trace per attempt, independent of any driver's own transcript. Durable persistence: `packages/gittensory-miner/lib/attempt-log.js` (sibling SQLite store; imports the engine normalizer). |
65
+ | Metering | `attempt-metering.ts` (#4311) | `accumulateAttemptUsage` / `meterAttemptUsage` / `evaluateAttemptBudget` over `AttemptBudgetAxis` (`tokens` / `turns` / `wallClockMs` / `costUsd`). |
66
+ | Acceptance criteria | (#4271) | The immutable criteria file at `task.acceptanceCriteriaPath`, written before the driver starts. |
67
+ | Worktree isolation | (#4269) | Each attempt's `task.workingDirectory` is a dedicated git worktree; a driver must never edit outside it. |
68
+
69
+ ## Authoring a third driver
70
+
71
+ To add a driver beyond the CLI-subprocess and Agent-SDK backends:
72
+
73
+ 1. **Implement the interface.** Export a `create<Name>CodingAgentDriver(deps)` factory returning a `CodingAgentDriver`.
74
+ Take every side-effecting dependency (spawn fn, `query()` client, clock, fs) as an **injected argument** — do not
75
+ reach for globals — so the contract suite can drive it with fakes (mirrors `SpawnFn` injection in `SelfHostAi`).
76
+ 2. **Honor the task scoping.** Only edit inside `task.workingDirectory`; stop at `task.maxTurns`; read the acceptance
77
+ criteria from `task.acceptanceCriteriaPath`; never make a GitHub write.
78
+ 3. **Return the result shape faithfully.** Set `ok` from whether acceptance was met, list `changedFiles`, and surface
79
+ failures via `error` (a clean failure) rather than throwing — the invoker records either outcome.
80
+ 4. **Register it in the factory.** Add the name to `CODING_AGENT_DRIVER_NAMES` and wire `createCodingAgentDriver`, so
81
+ `resolveConfiguredCodingAgentDriverNames` can select it from config (unknown names stay denied by default).
82
+ 5. **Get covered by the parity/contract suite** (#4296): the suite runs the same scenario fixtures — a clean success,
83
+ a clean failure, a budget/timeout, and a malformed acceptance-criteria input — against every driver with an
84
+ injected backend, asserting identical SHAPE and edge-case handling (not identical output, which is inherently
85
+ non-deterministic across backends).
86
+
87
+ ## A worked attempt lifecycle (the real production path)
88
+
89
+ `runCodingAgentAttempt`/`invokeCodingAgentDriver` (driver-factory.ts/coding-agent-invoke.ts) are real, tested,
90
+ and remain available as a lower-level composable, but **production does not call either of them today**. The
91
+ real construction + invocation path, as actually wired into `packages/gittensory-miner`, is:
92
+
93
+ ```
94
+ constructProductionCodingAgentDriver(env) (packages/gittensory-miner/lib/coding-agent-construction.js)
95
+ └─ createCodingAgentDriver({ providerName, spawn, ... }) (driver-factory.ts) → the configured CodingAgentDriver
96
+ │ (house-rule hooks attached by default for agent-sdk via buildHouseRulesAgentSdkHooks -- see below)
97
+ └─ becomes IterateLoopDeps.driver, handed to runIterateLoop (iterate-loop.ts)
98
+
99
+ runIterateLoop(input, deps) (packages/gittensory-engine/src/miner/iterate-loop.ts)
100
+ └─ per iteration, runDriverSafely(input, deps, task):
101
+ ├─ if !codingAgentModeExecutes(input.mode): → invokeCodingAgentDriver(deps.driver, mode, task, {...})
102
+ │ (paused/dry_run only -- records a shadow/no-op attempt-log event, never spawns the real driver)
103
+ └─ else (live): → deps.driver.run(task) directly, NOT via invokeCodingAgentDriver
104
+ → edits inside task.workingDirectory only, ≤ task.maxTurns
105
+ ├─ evaluateSelfReviewOutcome(...) → self-review the resulting diff
106
+ ├─ decideNextActionWithReason(state) → continue | handoff | abandon
107
+ └─ logDecision(...) records ONE attempt-log event per iteration (continue/handoff/abandon), not a
108
+ raw driver-start/succeed/fail pair the way invokeCodingAgentDriver's own wrapper would
109
+ ```
110
+
111
+ The attempt log (JSONL) is the durable, provider-independent record of what happened, independent of whichever
112
+ backend's own transcript, and the input to the miner's manage-phase and self-improve loops.
113
+
114
+ `packages/gittensory-miner/lib/coding-agent-house-rules.js`'s `runHouseRulesEnforcedCodingAgentAttempt` (a
115
+ drop-in wrapper over `runCodingAgentAttempt`) exists for a caller that wants the alternate, non-iterate-loop
116
+ path with house-rule enforcement built in by default -- no such caller exists in production today either; the
117
+ real house-rule enforcement for the live `agent-sdk` provider happens via `buildHouseRulesAgentSdkHooks`,
118
+ attached directly in `constructProductionCodingAgentDriver`.
119
+
120
+ **Metering:** `attempt-metering.ts`'s `accumulateAttemptUsage`/`evaluateAttemptBudget` are wired into
121
+ `iterate-loop.ts` for real ([#5395](https://github.com/JSONbored/gittensory/issues/5395)) -- every iteration
122
+ accumulates real `turns`/`costUsd`/`wallClockMs` (`tokens` stays an honest 0; no driver reports a real
123
+ per-iteration token count today) into a running `AttemptMeterTotals`, and `runIterateLoop`'s optional
124
+ `input.budget: AttemptBudget` is evaluated against it each iteration via the SAME `costCeilingReached` signal
125
+ `iterate-policy.ts` already exposed for the (now-removed) turns-only `maxTotalTurns` ceiling -- a genuine
126
+ mid-attempt abort, not just a between-cycle cap. `packages/gittensory-miner/lib/attempt-input-builder.js`'s
127
+ `buildAttemptLoopInput` sets `budget` from the SAME `AmsPolicySpec.capLimits` the Governor's cross-cycle
128
+ `GovernorCapUsage` already uses (`packages/gittensory-miner/lib/loop-cli.js`'s `governorState.saveCapUsage`
129
+ between loop cycles) -- one attempt can no longer burn through the entire cross-cycle budget before anything
130
+ reacts. `wallClockMs` uses a real injected clock (`IterateLoopDeps.nowMs`, defaulting to `Date.now`) measured
131
+ around each iteration's driver invocation. The result's `finalMeterTotals`/`budgetBreaches` fields surface
132
+ which axis (if any) triggered an abandon, for an operator reading the attempt log back.
133
+
134
+ ## Related docs
135
+
136
+ - [`operations-runbook.md`](operations-runbook.md) — SQLite `busy_timeout` concurrency, corruption recovery, multi-process collisions, post-upgrade migration ([#4875](https://github.com/JSONbored/gittensory/issues/4875)).
137
+ - [`env-reference.md`](env-reference.md) — env vars including ledger path overrides.
138
+ - [`../DEPLOYMENT.md`](../DEPLOYMENT.md) — laptop vs fleet deployment and state directory layout.
139
+ - [`miner-goal-spec.md`](miner-goal-spec.md) — per-repo `.gittensory-miner.yml` targeting policy.
140
+ - [`../README.md#mcp-server`](../README.md#mcp-server) — the `gittensory-miner-mcp` read-only tool surface for querying this driver's resolved status (provider, model env-var name, CLI presence) and the rest of AMS's local state over MCP.