@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,64 @@
1
+ export type MinerDriverStatus = {
2
+ provider: string | null;
3
+ modelEnvVar: string | null;
4
+ cliPresent: boolean | null;
5
+ };
6
+
7
+ export type MinerStatus = {
8
+ package: { name: string; version: string | null };
9
+ engine: { name: string; version: string | null };
10
+ node: string;
11
+ stateDir: string;
12
+ configFile: string | null;
13
+ driver: MinerDriverStatus;
14
+ };
15
+
16
+ export type DoctorCheck = {
17
+ name: string;
18
+ ok: boolean;
19
+ detail: string;
20
+ };
21
+
22
+ export function resolveMinerStateDir(env?: Record<string, string | undefined>): string;
23
+
24
+ export function collectStatus(env?: Record<string, string | undefined>, cwd?: string): MinerStatus;
25
+
26
+ export function runStatus(args?: string[], env?: Record<string, string | undefined>, cwd?: string): number;
27
+
28
+ export function checkConfigContent(cwd: string, readImpl?: (path: string, encoding: "utf8") => string): DoctorCheck;
29
+
30
+ export function checkGitHubTokenPresent(env?: Record<string, string | undefined>): DoctorCheck;
31
+
32
+ export function checkCodingAgentCredential(
33
+ env?: Record<string, string | undefined>,
34
+ resolveAuthPath?: (env: Record<string, string | undefined>) => string,
35
+ ): DoctorCheck;
36
+
37
+ export function runDoctorChecks(env?: Record<string, string | undefined>, cwd?: string): DoctorCheck[];
38
+
39
+ export function runDoctor(args?: string[], env?: Record<string, string | undefined>, cwd?: string): number;
40
+
41
+ export function readInstalledEnginePackageVersionFromPaths(
42
+ resolvedEntry: string,
43
+ workspacePkg: string,
44
+ deps?: { existsSync: (path: string) => boolean; readFileSync: (path: string, encoding: "utf8") => string },
45
+ ): string | null;
46
+
47
+ export function readInstalledEnginePackageVersion(): string | null;
48
+
49
+ export function readExpectedEnginePackageVersionFromPaths(
50
+ monorepoEnginePkg: string,
51
+ pinFile: string,
52
+ deps?: { existsSync: (path: string) => boolean; readFileSync: (path: string, encoding: "utf8") => string },
53
+ ): string | null;
54
+
55
+ export function readExpectedEnginePackageVersion(): string | null;
56
+
57
+ export function compareInstalledEngineVersion(installed: string, expected: string): -1 | 0 | 1;
58
+
59
+ export function buildEngineVersionSkewCheck(
60
+ readInstalled?: () => string | null,
61
+ readExpected?: () => string | null,
62
+ ): DoctorCheck;
63
+
64
+ export function buildEngineVersionDisplay(readInstalled?: () => string | null): string | null;
package/lib/status.js ADDED
@@ -0,0 +1,441 @@
1
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { CODING_AGENT_DRIVER_CONFIG_ENV, parseMinerGoalSpecContent, resolveFirstConfiguredCodingAgentDriverName } from "@loopover/engine";
6
+ import {
7
+ checkClaudeCliPresent,
8
+ checkCodexCliPresent,
9
+ checkDockerPresent,
10
+ checkLaptopStateSqlite,
11
+ findExecutableOnPath,
12
+ resolveCodexAuthPath,
13
+ } from "./laptop-init.js";
14
+ import { resolveMinerVersion } from "./version.js";
15
+ import { checkStoreIntegrity, describeError } from "./store-maintenance.js";
16
+ import { resolveEventLedgerDbPath } from "./event-ledger.js";
17
+ import { resolveGovernorLedgerDbPath } from "./governor-ledger.js";
18
+ import { resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
19
+ import { resolvePortfolioQueueDbPath } from "./portfolio-queue.js";
20
+ import { resolveClaimLedgerDbPath } from "./claim-ledger.js";
21
+ import { resolveRunStateDbPath } from "./run-state.js";
22
+ import { resolvePlanStoreDbPath } from "./plan-store.js";
23
+
24
+ // Slim laptop-mode CLI commands (#2288): `status` (what's installed + where local state lives) and `doctor` (is
25
+ // this laptop set up correctly). Both are read-only and 100% local — no repo-scanning, no coding-agent invocation,
26
+ // no GitHub writes, and no network calls of any kind. Later phases add the real discover/plan/manage loop.
27
+
28
+ // Lazy, not module-scope: mirrors the gittensory-engine repo-map.ts fix -- this file is CLI-only today, but
29
+ // an eager createRequire(import.meta.url)/import.meta.dirname at module scope would crash on import in any
30
+ // bundler context where import.meta is unavailable (e.g. if a future import chain pulls this into a Worker
31
+ // bundle, the way repo-map.ts was). Deferring construction to first real use keeps this import-safe.
32
+ let cachedRequire = null;
33
+ function requireFromHere() {
34
+ return (cachedRequire ??= createRequire(import.meta.url));
35
+ }
36
+ let cachedModuleDir = null;
37
+ function moduleDir() {
38
+ return (cachedModuleDir ??= import.meta.dirname);
39
+ }
40
+
41
+ const PACKAGE_NAME = "@loopover/miner";
42
+ const ENGINE_PACKAGE = "@loopover/engine";
43
+ // Config-file discovery order (mirrors the `.gittensory-miner.yml` precedence the goal-spec parser documents).
44
+ const CONFIG_FILE_CANDIDATES = Object.freeze([
45
+ ".gittensory-miner.yml",
46
+ ".github/gittensory-miner.yml",
47
+ ".gittensory-miner.json",
48
+ ".github/gittensory-miner.json",
49
+ ]);
50
+
51
+ /** The miner's local-state directory (holds the run-state / queue / ledger SQLite files). */
52
+ export function resolveMinerStateDir(env = process.env) {
53
+ const explicitConfigDir = typeof env.GITTENSORY_MINER_CONFIG_DIR === "string"
54
+ ? env.GITTENSORY_MINER_CONFIG_DIR.trim()
55
+ : "";
56
+ if (explicitConfigDir) return explicitConfigDir;
57
+
58
+ const configHome = typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()
59
+ ? env.XDG_CONFIG_HOME.trim()
60
+ : join(homedir(), ".config");
61
+ return join(configHome, "gittensory-miner");
62
+ }
63
+
64
+ /**
65
+ * The REAL installed @loopover/engine version, for `status`'s own display. Prefers `readInstalled`
66
+ * (the actually-resolved semver from node_modules/the monorepo workspace, the same real resolution `doctor`'s
67
+ * engine-version-skew check already relies on) -- a self-hoster asking "what's installed" wants the real
68
+ * answer, not the declared dependency RANGE ("*" in this monorepo, which tells them nothing). Falls back to
69
+ * the declared range only if real resolution genuinely comes up empty (the engine package's `exports` map
70
+ * blocks `require("<pkg>/package.json")` in some resolution orders, and its built `dist` may be absent
71
+ * depending on build order) -- still better than reporting nothing at all.
72
+ *
73
+ * Exported + injectable (mirrors `buildEngineVersionSkewCheck`'s own `readInstalled` param): real resolution
74
+ * succeeding is the only realistic case in a working install, so the fallback path needs a way to force it.
75
+ */
76
+ export function buildEngineVersionDisplay(readInstalled = readInstalledEnginePackageVersion) {
77
+ const installed = readInstalled();
78
+ if (installed) return installed;
79
+ try {
80
+ return requireFromHere()("../package.json").dependencies?.[ENGINE_PACKAGE] ?? null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function readEngineVersion() {
87
+ return buildEngineVersionDisplay();
88
+ }
89
+
90
+ export function readInstalledEnginePackageVersionFromPaths(
91
+ resolvedEntry,
92
+ workspacePkg,
93
+ deps = { existsSync, readFileSync },
94
+ ) {
95
+ try {
96
+ for (const pkgJson of [join(resolvedEntry, "..", "package.json"), join(resolvedEntry, "..", "..", "package.json")]) {
97
+ if (deps.existsSync(pkgJson)) {
98
+ const version = JSON.parse(deps.readFileSync(pkgJson, "utf8")).version;
99
+ if (version) return version;
100
+ }
101
+ }
102
+ } catch {
103
+ // fall through to monorepo workspace fallback
104
+ }
105
+ if (deps.existsSync(workspacePkg)) {
106
+ try {
107
+ return JSON.parse(deps.readFileSync(workspacePkg, "utf8")).version ?? null;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /** Installed @loopover/engine semver from node_modules (not the declared dependency range). */
116
+ export function readInstalledEnginePackageVersion() {
117
+ try {
118
+ return readInstalledEnginePackageVersionFromPaths(
119
+ requireFromHere().resolve(ENGINE_PACKAGE),
120
+ join(moduleDir(), "../../gittensory-engine/package.json"),
121
+ );
122
+ } catch {
123
+ const workspacePkg = join(moduleDir(), "../../gittensory-engine/package.json");
124
+ if (existsSync(workspacePkg)) {
125
+ try {
126
+ return JSON.parse(readFileSync(workspacePkg, "utf8")).version ?? null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ }
134
+
135
+ /** Expected minimum engine semver: monorepo engine package.json when present, else the shipped pin file. */
136
+ export function readExpectedEnginePackageVersionFromPaths(
137
+ monorepoEnginePkg,
138
+ pinFile,
139
+ deps = { existsSync, readFileSync },
140
+ ) {
141
+ if (deps.existsSync(monorepoEnginePkg)) {
142
+ try {
143
+ return JSON.parse(deps.readFileSync(monorepoEnginePkg, "utf8")).version ?? null;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ try {
149
+ const pinned = deps.readFileSync(pinFile, "utf8").trim();
150
+ return pinned || null;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ export function readExpectedEnginePackageVersion() {
157
+ return readExpectedEnginePackageVersionFromPaths(
158
+ join(moduleDir(), "../../gittensory-engine/package.json"),
159
+ join(moduleDir(), "../expected-engine.version"),
160
+ );
161
+ }
162
+
163
+ function parseSemverCore(version) {
164
+ const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)/);
165
+ if (!match) return null;
166
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
167
+ }
168
+
169
+ /** Returns -1 when installed is behind expected, 0 when equal, 1 when ahead. */
170
+ export function compareInstalledEngineVersion(installed, expected) {
171
+ const installedCore = parseSemverCore(installed);
172
+ const expectedCore = parseSemverCore(expected);
173
+ if (!installedCore || !expectedCore) return -1;
174
+ for (let index = 0; index < 3; index += 1) {
175
+ if (installedCore[index] < expectedCore[index]) return -1;
176
+ if (installedCore[index] > expectedCore[index]) return 1;
177
+ }
178
+ return 0;
179
+ }
180
+
181
+ export function buildEngineVersionSkewCheck(
182
+ readInstalled = readInstalledEnginePackageVersion,
183
+ readExpected = readExpectedEnginePackageVersion,
184
+ ) {
185
+ const installed = readInstalled();
186
+ const expected = readExpected();
187
+ if (!expected) {
188
+ return { name: "engine-version-skew", ok: true, detail: "expected engine version unavailable (skipped)" };
189
+ }
190
+ if (!installed) {
191
+ return {
192
+ name: "engine-version-skew",
193
+ ok: false,
194
+ detail: `${ENGINE_PACKAGE} not installed (cannot verify version skew)`,
195
+ };
196
+ }
197
+ const comparison = compareInstalledEngineVersion(installed, expected);
198
+ return {
199
+ name: "engine-version-skew",
200
+ ok: comparison >= 0,
201
+ detail:
202
+ comparison < 0
203
+ ? `installed ${installed} is behind expected ${expected}`
204
+ : `installed ${installed} (${comparison === 0 ? "matches" : "ahead of"} expected ${expected})`,
205
+ };
206
+ }
207
+
208
+ function checkEngineVersionSkew() {
209
+ return buildEngineVersionSkewCheck();
210
+ }
211
+
212
+ /** The minimum Node major version from the package's `engines.node` floor (e.g. ">=22.13.0" → 22). */
213
+ function requiredNodeMajor() {
214
+ const engines = requireFromHere()("../package.json").engines;
215
+ const match = typeof engines?.node === "string" ? engines.node.match(/(\d+)/) : null;
216
+ return match ? Number(match[1]) : 0;
217
+ }
218
+
219
+ function discoverConfigFile(cwd) {
220
+ for (const candidate of CONFIG_FILE_CANDIDATES) {
221
+ const path = join(cwd, candidate);
222
+ if (existsSync(path)) return path;
223
+ }
224
+ return null;
225
+ }
226
+
227
+ // CLI names driver-factory.ts's resolved provider values that actually spawn a local subprocess -- "noop" and
228
+ // "agent-sdk" have no separate CLI binary to check presence for, so cliPresent is null (not applicable) for them.
229
+ const PROVIDER_CLI_BINARY = Object.freeze({ "claude-cli": "claude", "codex-cli": "codex" });
230
+
231
+ /** The `driver` section of `status`/`status --json` (#5164): which coding-agent provider is configured, the
232
+ * NAME (never the value) of its model env var, and whether its CLI binary is on PATH. Reuses
233
+ * `resolveFirstConfiguredCodingAgentDriverName`/`CODING_AGENT_DRIVER_CONFIG_ENV` (the same resolution
234
+ * driver-factory.ts uses) and `findExecutableOnPath` (the same PATH scan the doctor CLI-presence checks use)
235
+ * rather than duplicating either. Never reads or returns an env var's actual value. */
236
+ function resolveDriverStatus(env) {
237
+ const provider = resolveFirstConfiguredCodingAgentDriverName(env) ?? null;
238
+ const modelEnvVar = provider ? (CODING_AGENT_DRIVER_CONFIG_ENV[provider]?.model ?? null) : null;
239
+ const cliBinary = provider ? (PROVIDER_CLI_BINARY[provider] ?? null) : null;
240
+ const cliPresent = cliBinary ? Boolean(findExecutableOnPath(cliBinary, env)) : null;
241
+ return { provider, modelEnvVar, cliPresent };
242
+ }
243
+
244
+ /** Gather the read-only status snapshot. Pure w.r.t. its (env, cwd) inputs — no writes, no network. */
245
+ export function collectStatus(env = process.env, cwd = process.cwd()) {
246
+ const stateDir = resolveMinerStateDir(env);
247
+ return {
248
+ package: { name: PACKAGE_NAME, version: resolveMinerVersion(env) },
249
+ engine: { name: ENGINE_PACKAGE, version: readEngineVersion() },
250
+ node: process.version,
251
+ stateDir,
252
+ configFile: discoverConfigFile(cwd),
253
+ driver: resolveDriverStatus(env),
254
+ };
255
+ }
256
+
257
+ function renderDriverLine(driver) {
258
+ if (!driver.provider) return "driver: none configured";
259
+ const cliText = driver.cliPresent === null ? "n/a" : driver.cliPresent ? "yes" : "no";
260
+ const modelText = driver.modelEnvVar ? `, model env: ${driver.modelEnvVar}` : "";
261
+ return `driver: ${driver.provider} (CLI present: ${cliText}${modelText})`;
262
+ }
263
+
264
+ function renderStatusText(status) {
265
+ return [
266
+ `${status.package.name} ${status.package.version ?? "unknown"} (node ${status.node})`,
267
+ `engine: ${status.engine.name} ${status.engine.version ?? "unresolved"}`,
268
+ `state dir: ${status.stateDir}`,
269
+ `config file: ${status.configFile ?? "none found"}`,
270
+ renderDriverLine(status.driver),
271
+ ].join("\n");
272
+ }
273
+
274
+ export function runStatus(args = [], env = process.env, cwd = process.cwd()) {
275
+ const status = collectStatus(env, cwd);
276
+ console.log(args.includes("--json") ? JSON.stringify(status, null, 2) : renderStatusText(status));
277
+ return 0;
278
+ }
279
+
280
+ function checkStateDirWritable(stateDir) {
281
+ const probe = join(stateDir, ".gittensory-miner-write-probe");
282
+ try {
283
+ // Creating the dir and writing (then removing) a probe file proves it is writable — the state dir must be
284
+ // creatable/writable for the local SQLite stores to work.
285
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
286
+ writeFileSync(probe, "");
287
+ rmSync(probe, { force: true });
288
+ return { name: "state-dir-writable", ok: true, detail: stateDir };
289
+ } catch (error) {
290
+ return {
291
+ name: "state-dir-writable",
292
+ ok: false,
293
+ detail: `${stateDir}: ${error instanceof Error ? error.message : "not writable"}`,
294
+ };
295
+ }
296
+ }
297
+
298
+ /** Per-store `PRAGMA integrity_check` sweep for `doctor` (#4834) — flags a corrupted store instead of probing
299
+ * only one with `SELECT 1`. A store file that does not exist yet is healthy by absence. */
300
+ function storeIntegrityChecks(env) {
301
+ const stores = [
302
+ ["event-ledger", resolveEventLedgerDbPath(env)],
303
+ ["governor-ledger", resolveGovernorLedgerDbPath(env)],
304
+ ["prediction-ledger", resolvePredictionLedgerDbPath(env)],
305
+ ["portfolio-queue", resolvePortfolioQueueDbPath(env)],
306
+ ["claim-ledger", resolveClaimLedgerDbPath(env)],
307
+ ["run-state", resolveRunStateDbPath(env)],
308
+ ["plan-store", resolvePlanStoreDbPath(env)],
309
+ ];
310
+ return stores.map(([name, dbPath]) => checkStoreIntegrity(`store-integrity:${name}`, dbPath));
311
+ }
312
+
313
+ /** Validate the discovered `.gittensory-miner` config's CONTENT (#4873), not just its path: parse it with the
314
+ * tolerant goal-spec parser and surface its warnings, so a malformed config is flagged by `doctor` rather than
315
+ * silently degrading to defaults. No config file is fine (defaults apply); a read failure is reported. `readImpl`
316
+ * is injectable for tests. */
317
+ export function checkConfigContent(cwd, readImpl = readFileSync) {
318
+ const configPath = discoverConfigFile(cwd);
319
+ if (!configPath) {
320
+ return { name: "config-content", ok: true, detail: "no .gittensory-miner config found (using defaults)" };
321
+ }
322
+ let warnings;
323
+ try {
324
+ warnings = parseMinerGoalSpecContent(readImpl(configPath, "utf8")).warnings;
325
+ } catch (error) {
326
+ return { name: "config-content", ok: false, detail: `${configPath}: ${describeError(error)}` };
327
+ }
328
+ return warnings.length === 0
329
+ ? { name: "config-content", ok: true, detail: `${configPath}: valid` }
330
+ : { name: "config-content", ok: false, detail: `${configPath}: ${warnings.join("; ")}` };
331
+ }
332
+
333
+ function nonEmptyEnv(value) {
334
+ return typeof value === "string" && value.length > 0;
335
+ }
336
+
337
+ /** `GITHUB_TOKEN` presence (#5170). A purely offline string check — `doctor` never calls GitHub — but a missing
338
+ * token fails every real attempt the moment it tries to push a branch or open a PR, so surface it up front
339
+ * rather than mid-run. Reports presence only; the token value itself is never included in the detail. */
340
+ export function checkGitHubTokenPresent(env = process.env) {
341
+ const present = nonEmptyEnv(env.GITHUB_TOKEN);
342
+ return {
343
+ name: "github-token",
344
+ ok: present,
345
+ detail: present
346
+ ? "GITHUB_TOKEN is set"
347
+ : "GITHUB_TOKEN is not set — attempts that push a branch or open a PR will fail",
348
+ };
349
+ }
350
+
351
+ /** Credential presence for the CONFIGURED coding-agent provider (#5170). Distinct from the CLI-present checks,
352
+ * which by design keep `ok: true` when only the credential is missing (#5165): this FAILS `doctor` when the
353
+ * resolved provider's credential is absent, so an operator learns before an attempt fails partway through.
354
+ * Fully offline — an env-var string check for the Claude backends, a file-readability check for codex — and it
355
+ * never prints the credential value, only the env-var names / file path. `resolveAuthPath` is injectable for
356
+ * tests, mirroring `checkCodexCliPresent`. */
357
+ export function checkCodingAgentCredential(env = process.env, resolveAuthPath = resolveCodexAuthPath) {
358
+ const provider = resolveFirstConfiguredCodingAgentDriverName(env) ?? null;
359
+ if (provider === null || provider === "noop") {
360
+ return {
361
+ name: "coding-agent-credential",
362
+ ok: true,
363
+ detail:
364
+ provider === "noop"
365
+ ? "noop driver needs no credential"
366
+ : "no coding-agent provider configured (skipped)",
367
+ };
368
+ }
369
+ if (provider === "claude-cli" || provider === "agent-sdk") {
370
+ // Both run the Claude backend (a `claude` subprocess vs the in-process Agent SDK) off the same subscription
371
+ // OAuth token the rest of the tree reads (CLAUDE_CODE_OAUTH_TOKEN; see createClaudeCodeAi in
372
+ // src/selfhost/ai.ts). The SDK additionally accepts a raw ANTHROPIC_API_KEY, so either satisfies the credential.
373
+ const present = nonEmptyEnv(env.CLAUDE_CODE_OAUTH_TOKEN) || nonEmptyEnv(env.ANTHROPIC_API_KEY);
374
+ return {
375
+ name: "coding-agent-credential",
376
+ ok: present,
377
+ detail: present
378
+ ? `${provider}: Claude credential is set`
379
+ : `${provider}: no Claude credential — set CLAUDE_CODE_OAUTH_TOKEN (or ANTHROPIC_API_KEY)`,
380
+ };
381
+ }
382
+ // codex-cli: the only remaining configured provider — its credential is a readable auth.json, the same
383
+ // read-only condition checkCodexCliPresent probes (reusing resolveCodexAuthPath so the location never drifts).
384
+ const authPath = resolveAuthPath(env);
385
+ let readable = false;
386
+ try {
387
+ accessSync(authPath, constants.R_OK);
388
+ readable = true;
389
+ } catch {
390
+ // missing or unreadable — codex would fail for lack of credentials at attempt time.
391
+ }
392
+ return {
393
+ name: "coding-agent-credential",
394
+ ok: readable,
395
+ detail: readable
396
+ ? `codex-cli: auth.json is readable at ${authPath}`
397
+ : `codex-cli: auth.json missing or unreadable at ${authPath} — run \`codex auth\``,
398
+ };
399
+ }
400
+
401
+ /** Run the doctor checks. Returns an array of { name, ok, detail }; only writes a transient probe in the state dir,
402
+ * never touches the network. */
403
+ export function runDoctorChecks(env = process.env, cwd = process.cwd()) {
404
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
405
+ const requiredMajor = requiredNodeMajor();
406
+ const engineVersion = readEngineVersion();
407
+ return [
408
+ {
409
+ name: "node-version",
410
+ ok: nodeMajor >= requiredMajor,
411
+ detail: `node ${process.version} (requires >= ${requiredMajor})`,
412
+ },
413
+ {
414
+ name: "engine-resolves",
415
+ ok: engineVersion !== null,
416
+ detail: engineVersion ? `${ENGINE_PACKAGE} ${engineVersion}` : `${ENGINE_PACKAGE} not resolvable`,
417
+ },
418
+ checkEngineVersionSkew(),
419
+ checkStateDirWritable(resolveMinerStateDir(env)),
420
+ checkLaptopStateSqlite(env),
421
+ checkDockerPresent(),
422
+ checkClaudeCliPresent({ env }),
423
+ checkCodexCliPresent({ env }),
424
+ checkGitHubTokenPresent(env),
425
+ checkCodingAgentCredential(env),
426
+ checkConfigContent(cwd),
427
+ ...storeIntegrityChecks(env),
428
+ ];
429
+ }
430
+
431
+ export function runDoctor(args = [], env = process.env, cwd = process.cwd()) {
432
+ const checks = runDoctorChecks(env, cwd);
433
+ const failed = checks.filter((check) => !check.ok);
434
+ if (args.includes("--json")) {
435
+ console.log(JSON.stringify({ ok: failed.length === 0, checks }, null, 2));
436
+ } else {
437
+ for (const check of checks) console.log(`${check.ok ? "ok " : "FAIL"} ${check.name}: ${check.detail}`);
438
+ if (failed.length > 0) console.error(`doctor: ${failed.length} check(s) failed`);
439
+ }
440
+ return failed.length === 0 ? 0 : 1;
441
+ }
@@ -0,0 +1,31 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+
3
+ export const LEDGER_RETENTION_DAYS_ENV: string;
4
+ export const LEDGER_RETENTION_MAX_ROWS_ENV: string;
5
+
6
+ export type LedgerRetentionSpec = { table: string; timestampColumn: string; orderColumn: string };
7
+ export const EVENT_LEDGER_RETENTION_SPEC: LedgerRetentionSpec;
8
+ export const GOVERNOR_LEDGER_RETENTION_SPEC: LedgerRetentionSpec;
9
+ export const PREDICTION_LEDGER_RETENTION_SPEC: LedgerRetentionSpec;
10
+
11
+ export type LedgerPurgeSpec = { table: string; repoColumn: string };
12
+ export const CLAIM_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
13
+ export const EVENT_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
14
+ export const GOVERNOR_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
15
+ export const PREDICTION_LEDGER_PURGE_SPEC: LedgerPurgeSpec;
16
+
17
+ export type StoreIntegrityResult = { name: string; ok: boolean; detail: string };
18
+ export type LedgerRetentionPolicy = { maxAgeMs?: number; maxRows?: number };
19
+
20
+ export function describeError(error: unknown): string;
21
+ export function classifyIntegrityRows(rows: Array<{ integrity_check?: unknown }>): { ok: boolean; note: string };
22
+ export function checkStoreIntegrity(name: string, dbPath: string): StoreIntegrityResult;
23
+ export function resolveLedgerRetentionPolicy(env?: Record<string, string | undefined>): LedgerRetentionPolicy | null;
24
+ export function pruneLedgerByRetention(
25
+ db: DatabaseSync,
26
+ spec: LedgerRetentionSpec,
27
+ policy: LedgerRetentionPolicy | null,
28
+ nowMs: number,
29
+ ): number;
30
+ export function purgeStoreByRepo(db: DatabaseSync, spec: LedgerPurgeSpec, repoFullName: string): number;
31
+ export function countStoreByRepo(db: DatabaseSync, spec: LedgerPurgeSpec, repoFullName: string): number;