@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,99 @@
1
+ import type { CodingAgentExecutionMode, FeasibilityVerdict, LocalWriteActionSpec } from "@loopover/engine";
2
+ import type { AttemptDeps, AttemptResult as RunMinerAttemptResult, runMinerAttempt } from "./attempt-runner.js";
3
+ import type { ClaimLedger } from "./claim-ledger.js";
4
+ import type { EventLedger } from "./event-ledger.js";
5
+ import type { AttemptLog } from "./attempt-log.js";
6
+ import type { GovernorLedger } from "./governor-ledger.js";
7
+ import type { WorktreeAllocator } from "./worktree-allocator.js";
8
+ import type { resolveRejectionSignaled } from "./rejection-signal.js";
9
+ import type { SelfReviewContextFetch, fetchSelfReviewContext } from "./self-review-context.js";
10
+ import type { cleanupAttemptWorktree, prepareAttemptWorktree } from "./attempt-worktree.js";
11
+ import type { buildCodingTaskSpec } from "./coding-task-spec.js";
12
+ import type { resolveAmsPolicy } from "./ams-policy.js";
13
+ import type { checkMinerKillSwitch } from "./governor-kill-switch.js";
14
+ import type { resolveMinerGoalSpec } from "./miner-goal-spec.js";
15
+ import type { ClaimConflictResult, resolveClaimConflict } from "./claim-conflict-resolver.js";
16
+
17
+ type CommonAttemptResultFields = {
18
+ repoFullName: string;
19
+ issueNumber: number;
20
+ minerLogin: string;
21
+ base: string;
22
+ mode: CodingAgentExecutionMode;
23
+ attemptId: string;
24
+ };
25
+
26
+ /** The result runAttempt reports at every real return point, threaded to `options.onResult` (in addition to
27
+ * the plain exit-code return runAttempt itself still returns, unchanged, so bin/gittensory-miner.js's own
28
+ * `process.exit(exitCode)` usage never breaks) -- the loop orchestrator's real caller for this data. */
29
+ export type AttemptCliResult =
30
+ | (CommonAttemptResultFields & { outcome: "dry_run" })
31
+ | (CommonAttemptResultFields & { outcome: "blocked_rejection_signaled"; reason: string })
32
+ | (CommonAttemptResultFields & { outcome: "blocked_worktree_preparation_failed"; reason: string })
33
+ | (CommonAttemptResultFields & {
34
+ outcome: "blocked_infeasible";
35
+ reason: string;
36
+ verdict: FeasibilityVerdict;
37
+ avoidReasons: string[];
38
+ raiseReasons: string[];
39
+ })
40
+ | (CommonAttemptResultFields & {
41
+ outcome: `attempt_${RunMinerAttemptResult["outcome"]}`;
42
+ submissionMode: "observe" | "enforce";
43
+ totalTurnsUsed: number;
44
+ totalCostUsd: number;
45
+ iterationsUsed: number;
46
+ reason?: string;
47
+ decision?: unknown;
48
+ spec?: LocalWriteActionSpec;
49
+ execResult?: unknown;
50
+ claimConflict?: ClaimConflictResult;
51
+ });
52
+
53
+ export type ParsedAttemptArgs =
54
+ | { error: string }
55
+ | {
56
+ repoFullName: string;
57
+ issueNumber: number;
58
+ minerLogin: string;
59
+ base: string;
60
+ live: boolean;
61
+ dryRun: boolean;
62
+ json: boolean;
63
+ };
64
+
65
+ export function parseAttemptArgs(args: string[]): ParsedAttemptArgs;
66
+
67
+ export function buildAttemptDeps(
68
+ env: Record<string, string | undefined>,
69
+ ledgers: { claimLedger: ClaimLedger; eventLedger: EventLedger; attemptLog: AttemptLog; governorLedger: GovernorLedger; nowMs: number },
70
+ ): AttemptDeps;
71
+
72
+ export type RunAttemptOptions = {
73
+ env?: Record<string, string | undefined>;
74
+ nowMs?: number;
75
+ attemptId?: string;
76
+ resolveCodingAgentModeFromConfig?: (config: { env?: Record<string, string | undefined> }) => CodingAgentExecutionMode;
77
+ openWorktreeAllocator?: () => WorktreeAllocator;
78
+ openClaimLedger?: () => ClaimLedger;
79
+ initEventLedger?: () => EventLedger;
80
+ initAttemptLog?: () => AttemptLog;
81
+ initGovernorLedger?: () => GovernorLedger;
82
+ buildAttemptDeps?: typeof buildAttemptDeps;
83
+ resolveRejectionSignaled?: typeof resolveRejectionSignaled;
84
+ fetchImpl?: SelfReviewContextFetch;
85
+ prepareAttemptWorktree?: typeof prepareAttemptWorktree;
86
+ cleanupAttemptWorktree?: typeof cleanupAttemptWorktree;
87
+ fetchSelfReviewContext?: typeof fetchSelfReviewContext;
88
+ buildCodingTaskSpec?: typeof buildCodingTaskSpec;
89
+ resolveAmsPolicy?: typeof resolveAmsPolicy;
90
+ checkMinerKillSwitch?: typeof checkMinerKillSwitch;
91
+ resolveMinerGoalSpec?: typeof resolveMinerGoalSpec;
92
+ runMinerAttempt?: typeof runMinerAttempt;
93
+ resolveClaimConflict?: typeof resolveClaimConflict;
94
+ /** Invoked with the real structured result at every return point, in addition to (never instead of) the
95
+ * plain exit-code return -- the loop orchestrator's real hook into what actually happened. */
96
+ onResult?: (result: AttemptCliResult) => void;
97
+ };
98
+
99
+ export function runAttempt(args: string[], options?: RunAttemptOptions): Promise<number>;
@@ -0,0 +1,523 @@
1
+ // CLI dispatch for the real attempt pipeline (#5132, Wave 3.5 -- the final assembly). Wires bin/gittensory-miner.js's
2
+ // `attempt` subcommand to real infrastructure end to end: worktree allocation + real git preparation
3
+ // (worktree-allocator.js + attempt-worktree.js), the four ledgers (claim/event/attempt-log/governor), the
4
+ // real coding-agent driver (#5131) and slop assessor (#5133), a live SelfReviewContext fetch (#5145), a real
5
+ // coding-task spec (#5239), the operator's AmsPolicySpec execution policy (#5249), rejectionSignaled (#5241),
6
+ // a real runMinerAttempt call -- the first point in this epic where a real coding agent actually runs, not
7
+ // just checks-and-reports-blocked -- and, only on a real "submitted" outcome, a real post-submission
8
+ // claim-conflict resolution (#4848, claim-conflict-resolver.js) for the narrow race window
9
+ // checkSubmissionFreshness cannot see (two miners submitting almost simultaneously).
10
+ //
11
+ // KNOWN, DOCUMENTED GAPS (not fabricated -- see attempt-input-builder.js's own header for the full list):
12
+ // governor.convergenceInput is an honest first-attempt-shaped literal, not a real per-issue attempt-history
13
+ // query (attempt-log.js's schema has no repo+issue index, and reenqueue counts aren't tracked anywhere yet).
14
+
15
+ import { resolveCodingAgentModeFromConfig } from "@loopover/engine";
16
+ import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
17
+ import { constructProductionCodingAgentDriver } from "./coding-agent-construction.js";
18
+ import { runSlopAssessment } from "./slop-assessment.js";
19
+ import { fetchLiveIssueSnapshot } from "./live-issue-snapshot.js";
20
+ import { executeLocalWrite } from "./execute-local-write.js";
21
+ import { openClaimLedger } from "./claim-ledger.js";
22
+ import { resolveMinerGoalSpec } from "./miner-goal-spec.js";
23
+ import { resolveClaimConflict } from "./claim-conflict-resolver.js";
24
+ import { parsePrNumberFromExecResult } from "./pr-number-parse.js";
25
+ import { initEventLedger } from "./event-ledger.js";
26
+ import { initAttemptLog } from "./attempt-log.js";
27
+ import { initGovernorLedger } from "./governor-ledger.js";
28
+ import { openWorktreeAllocator } from "./worktree-allocator.js";
29
+ import { resolveRejectionSignaled } from "./rejection-signal.js";
30
+ import { cleanupAttemptWorktree, prepareAttemptWorktree } from "./attempt-worktree.js";
31
+ import { fetchSelfReviewContext } from "./self-review-context.js";
32
+ import { buildCodingTaskSpec } from "./coding-task-spec.js";
33
+ import { resolveAmsPolicy } from "./ams-policy.js";
34
+ import { checkMinerKillSwitch } from "./governor-kill-switch.js";
35
+ import { buildAttemptGovernorContext, buildAttemptLoopInput } from "./attempt-input-builder.js";
36
+ import { runMinerAttempt } from "./attempt-runner.js";
37
+
38
+ const ATTEMPT_USAGE =
39
+ "Usage: gittensory-miner attempt <owner/repo> <issue#> --miner-login <login> [--base <branch>] [--live] [--dry-run] [--json]";
40
+
41
+ function parseRepoTarget(value) {
42
+ const trimmed = typeof value === "string" ? value.trim() : "";
43
+ const [owner, repo, extra] = trimmed.split("/");
44
+ if (!owner || !repo || extra !== undefined) return null;
45
+ return `${owner}/${repo}`;
46
+ }
47
+
48
+ export function parseAttemptArgs(args) {
49
+ const options = { json: false, minerLogin: null, base: "main", live: false, dryRun: false };
50
+ const positional = [];
51
+
52
+ for (let index = 0; index < args.length; index += 1) {
53
+ const token = args[index];
54
+ if (token === "--json") {
55
+ options.json = true;
56
+ continue;
57
+ }
58
+ // Opt-in only: resolveCodingAgentModeFromConfig's own default (no agentDryRun override) is "live", not
59
+ // "dry_run" -- so #5132's "dry-run is default" acceptance criteria (#2342) has to be enforced HERE, by
60
+ // requiring an explicit --live flag before this command will ever request live mode.
61
+ if (token === "--live") {
62
+ options.live = true;
63
+ continue;
64
+ }
65
+ // #4847: distinct from --live's absence above -- --live only ever gated the coding-agent DRIVER's mode,
66
+ // but a non---live run still opened every store and made real worktree/claim/ledger writes. --dry-run
67
+ // short-circuits BEFORE any of that infrastructure is even opened, guaranteeing zero writes rather than
68
+ // merely skipping the driver.
69
+ if (token === "--dry-run") {
70
+ options.dryRun = true;
71
+ continue;
72
+ }
73
+ if (token === "--miner-login") {
74
+ const value = args[index + 1];
75
+ if (!value || value.startsWith("-")) return { error: ATTEMPT_USAGE };
76
+ options.minerLogin = value;
77
+ index += 1;
78
+ continue;
79
+ }
80
+ if (token === "--base") {
81
+ const value = args[index + 1];
82
+ if (!value || value.startsWith("-")) return { error: ATTEMPT_USAGE };
83
+ options.base = value;
84
+ index += 1;
85
+ continue;
86
+ }
87
+ if (token.startsWith("-")) return { error: `Unknown option: ${token}` };
88
+ positional.push(token);
89
+ }
90
+
91
+ if (positional.length !== 2) return { error: ATTEMPT_USAGE };
92
+ const repoFullName = parseRepoTarget(positional[0]);
93
+ if (!repoFullName) return { error: `Repository must be in owner/repo form: ${positional[0]}` };
94
+ const issueNumber = Number(positional[1]);
95
+ if (!Number.isInteger(issueNumber) || issueNumber < 1) {
96
+ return { error: `Issue number must be a positive integer: ${positional[1]}` };
97
+ }
98
+ if (!options.minerLogin) return { error: `--miner-login is required. ${ATTEMPT_USAGE}` };
99
+
100
+ return {
101
+ repoFullName,
102
+ issueNumber,
103
+ minerLogin: options.minerLogin,
104
+ base: options.base,
105
+ live: options.live,
106
+ dryRun: options.dryRun,
107
+ json: options.json,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Assemble a real AttemptDeps object: every field wired to a genuine implementation (the #5131 driver, the
113
+ * #5133 slop assessor, the four real ledgers passed in, and the fetchLiveIssueSnapshot/executeLocalWrite
114
+ * built alongside this file). Throws if the coding-agent driver is unconfigured (fails closed, matching
115
+ * constructProductionCodingAgentDriver's own contract) -- callers should report that clearly rather than
116
+ * silently falling back to a driver that could never run.
117
+ *
118
+ * @param {Record<string, string | undefined>} env
119
+ * @param {{
120
+ * claimLedger: import("./claim-ledger.js").ClaimLedger,
121
+ * eventLedger: import("./event-ledger.js").EventLedger,
122
+ * attemptLog: import("./attempt-log.js").AttemptLog,
123
+ * governorLedger: import("./governor-ledger.js").GovernorLedger,
124
+ * nowMs: number,
125
+ * }} ledgers
126
+ * @returns {import("./attempt-runner.js").AttemptDeps}
127
+ */
128
+ export function buildAttemptDeps(env, ledgers) {
129
+ return {
130
+ driver: constructProductionCodingAgentDriver(env),
131
+ runSlopAssessment: (input) => runSlopAssessment(input),
132
+ appendAttemptLogEvent: (event) => ledgers.attemptLog.appendAttemptLogEvent(event),
133
+ claimLedger: ledgers.claimLedger,
134
+ fetchLiveIssueSnapshot: (repoFullName, issueNumber) => fetchLiveIssueSnapshot(repoFullName, issueNumber, { githubToken: env.GITHUB_TOKEN }),
135
+ eventLedger: ledgers.eventLedger,
136
+ governorLedgerAppend: (event) => ledgers.governorLedger.appendGovernorEvent(event),
137
+ nowMs: ledgers.nowMs,
138
+ executeLocalWrite: (spec) => executeLocalWrite(spec),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Run the `attempt` CLI subcommand end to end: resolveRejectionSignaled (before consuming a worktree slot) ->
144
+ * acquire a concurrency slot -> assemble real AttemptDeps -> prepare a REAL git worktree -> fetch a real
145
+ * SelfReviewContext -> build a real coding-task spec (blocks on an infeasible verdict) -> resolve the real
146
+ * AmsPolicySpec execution policy -> assemble the real IterateLoopInput + Governor context -> call
147
+ * runMinerAttempt for real. The worktree is cleaned up (or retained, per the real outcome) in `finally`.
148
+ * See this file's header for the documented gaps (real convergence history).
149
+ */
150
+ export async function runAttempt(args, options = {}) {
151
+ const parsed = parseAttemptArgs(args);
152
+ if ("error" in parsed) {
153
+ return reportCliFailure(argsWantJson(args), parsed.error);
154
+ }
155
+
156
+ const env = options.env ?? process.env;
157
+ const nowMs = options.nowMs ?? Date.now();
158
+ const resolveMode = options.resolveCodingAgentModeFromConfig ?? resolveCodingAgentModeFromConfig;
159
+ const mode = resolveMode({ env, agentDryRun: !parsed.live });
160
+
161
+ if (mode === "paused") {
162
+ return reportCliFailure(
163
+ parsed.json,
164
+ `Coding-agent execution is globally paused (MINER_CODING_AGENT_PAUSED). Not running attempt for ${parsed.repoFullName}#${parsed.issueNumber}.`,
165
+ 3,
166
+ );
167
+ }
168
+
169
+ const attemptId = options.attemptId ?? `${parsed.repoFullName.replace("/", "_")}-${parsed.issueNumber}-${nowMs}`;
170
+
171
+ // #4847: reports what a real run would do and returns BEFORE any store (allocator/claim/event/attempt-log/
172
+ // governor ledger) is even opened, so this is a provable zero-write path -- not just "opened but didn't
173
+ // write to" the local stores, and nowhere near the real worktree clone, claim, or coding-agent driver.
174
+ if (parsed.dryRun) {
175
+ const dryRunResult = {
176
+ outcome: "dry_run",
177
+ repoFullName: parsed.repoFullName,
178
+ issueNumber: parsed.issueNumber,
179
+ minerLogin: parsed.minerLogin,
180
+ base: parsed.base,
181
+ mode,
182
+ attemptId,
183
+ };
184
+ if (parsed.json) {
185
+ console.log(JSON.stringify(dryRunResult, null, 2));
186
+ } else {
187
+ console.log(
188
+ `DRY RUN: would attempt ${parsed.repoFullName}#${parsed.issueNumber} for ${parsed.minerLogin} (mode: ${mode}, base: ${parsed.base}). No worktree, claim, or ledger writes were made.`,
189
+ );
190
+ }
191
+ options.onResult?.(dryRunResult);
192
+ return 0;
193
+ }
194
+
195
+ let allocator = null;
196
+ let claimLedger = null;
197
+ let eventLedger = null;
198
+ let attemptLog = null;
199
+ let governorLedger = null;
200
+ let allocation = null;
201
+ let worktreeResult = null;
202
+ let claimedIssue = false;
203
+
204
+ try {
205
+ allocator = (options.openWorktreeAllocator ?? openWorktreeAllocator)();
206
+ claimLedger = (options.openClaimLedger ?? openClaimLedger)();
207
+ eventLedger = (options.initEventLedger ?? initEventLedger)();
208
+ attemptLog = (options.initAttemptLog ?? initAttemptLog)();
209
+ governorLedger = (options.initGovernorLedger ?? initGovernorLedger)();
210
+
211
+ // Checked before acquiring a worktree slot: a banned repo should never consume one. This resolves the
212
+ // first of rejectionSignaled's two documented triggers (an explicit AI-usage-policy ban, #5132 follow-up)
213
+ // -- the second (a prior own-submission rejection on this exact repo) remains a documented gap, see
214
+ // rejection-signal.js's own header for why.
215
+ const resolveRejection = options.resolveRejectionSignaled ?? resolveRejectionSignaled;
216
+ const rejectionSignaled = await resolveRejection(parsed.repoFullName, { fetchImpl: options.fetchImpl });
217
+ if (rejectionSignaled) {
218
+ const reason = "ai_usage_policy_ban";
219
+ attemptLog.appendAttemptLogEvent({
220
+ eventType: "attempt_aborted",
221
+ attemptId,
222
+ actionClass: "open_pr",
223
+ mode,
224
+ reason,
225
+ payload: { repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber },
226
+ });
227
+ eventLedger.appendEvent({
228
+ type: "attempt_blocked",
229
+ repoFullName: parsed.repoFullName,
230
+ payload: { issueNumber: parsed.issueNumber, reason },
231
+ });
232
+ const rejectedResult = {
233
+ outcome: "blocked_rejection_signaled",
234
+ reason,
235
+ repoFullName: parsed.repoFullName,
236
+ issueNumber: parsed.issueNumber,
237
+ minerLogin: parsed.minerLogin,
238
+ base: parsed.base,
239
+ mode,
240
+ attemptId,
241
+ };
242
+ if (parsed.json) {
243
+ console.log(JSON.stringify(rejectedResult, null, 2));
244
+ } else {
245
+ console.error(
246
+ `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this repo's AI-usage policy bans automated/AI-authored contributions.`,
247
+ );
248
+ }
249
+ options.onResult?.(rejectedResult);
250
+ return 5;
251
+ }
252
+
253
+ allocation = allocator.acquire(attemptId, parsed.repoFullName);
254
+
255
+ let deps;
256
+ try {
257
+ const buildDeps = options.buildAttemptDeps ?? buildAttemptDeps;
258
+ deps = buildDeps(env, { claimLedger, eventLedger, attemptLog, governorLedger, nowMs });
259
+ } catch (error) {
260
+ const reason = describeCliError(error);
261
+ return reportCliFailure(
262
+ parsed.json,
263
+ `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: ${reason}`,
264
+ 3,
265
+ );
266
+ }
267
+
268
+ // Real worktree preparation (repo-clone.js + attempt-worktree.js, #5237): the allocator above only
269
+ // reserves a concurrency SLOT (worktree-allocator.js's own `slot-N` placeholder dirs never receive real
270
+ // git content) -- this is the step that actually clones/fetches the target repo and creates a real
271
+ // `git worktree` for this attempt. Its own path, NOT the allocator's slot path, is the real
272
+ // workingDirectory a future runMinerAttempt call must use.
273
+ const prepareWorktree = options.prepareAttemptWorktree ?? prepareAttemptWorktree;
274
+ worktreeResult = await prepareWorktree(parsed.repoFullName, attemptId, { baseBranch: parsed.base, env });
275
+ if (!worktreeResult.ok) {
276
+ const reason = worktreeResult.error;
277
+ attemptLog.appendAttemptLogEvent({
278
+ eventType: "attempt_aborted",
279
+ attemptId,
280
+ actionClass: "open_pr",
281
+ mode,
282
+ reason,
283
+ payload: { repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber },
284
+ });
285
+ eventLedger.appendEvent({
286
+ type: "attempt_blocked",
287
+ repoFullName: parsed.repoFullName,
288
+ payload: { issueNumber: parsed.issueNumber, reason },
289
+ });
290
+ const worktreeFailureResult = {
291
+ outcome: "blocked_worktree_preparation_failed",
292
+ reason,
293
+ repoFullName: parsed.repoFullName,
294
+ issueNumber: parsed.issueNumber,
295
+ minerLogin: parsed.minerLogin,
296
+ base: parsed.base,
297
+ mode,
298
+ attemptId,
299
+ };
300
+ if (parsed.json) {
301
+ console.log(JSON.stringify(worktreeFailureResult, null, 2));
302
+ } else {
303
+ console.error(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: real worktree preparation failed: ${reason}`);
304
+ }
305
+ options.onResult?.(worktreeFailureResult);
306
+ return 6;
307
+ }
308
+
309
+ // Real SelfReviewContext (#5145): issue/PR/manifest data at live-gate fidelity for the target repo.
310
+ const fetchReviewContext = options.fetchSelfReviewContext ?? fetchSelfReviewContext;
311
+ const reviewContext = await fetchReviewContext(parsed.repoFullName, {
312
+ githubToken: env.GITHUB_TOKEN,
313
+ contributorLogin: parsed.minerLogin,
314
+ linkedIssues: [parsed.issueNumber],
315
+ });
316
+
317
+ // The target issue's own real record, when present in the fetched context. When absent (e.g. already
318
+ // closed, or genuinely not found), buildCodingTaskSpec's own feasibility check reports target_not_found
319
+ // and this placeholder's empty title/body are never surfaced anywhere -- not fabricated content, just an
320
+ // inert shape for a verdict that immediately blocks.
321
+ const targetIssue = reviewContext.issues.find((candidate) => candidate.number === parsed.issueNumber) ?? {
322
+ number: parsed.issueNumber,
323
+ title: "",
324
+ body: null,
325
+ labels: [],
326
+ };
327
+
328
+ const buildTaskSpec = options.buildCodingTaskSpec ?? buildCodingTaskSpec;
329
+ const codingTaskSpec = buildTaskSpec({
330
+ repoFullName: parsed.repoFullName,
331
+ issue: targetIssue,
332
+ context: { issues: reviewContext.issues, pullRequests: reviewContext.pullRequests },
333
+ claimLedger,
334
+ workingDirectory: worktreeResult.worktreePath,
335
+ });
336
+
337
+ if (!codingTaskSpec.ready) {
338
+ const reason = `infeasible_${codingTaskSpec.verdict}`;
339
+ attemptLog.appendAttemptLogEvent({
340
+ eventType: "attempt_aborted",
341
+ attemptId,
342
+ actionClass: "open_pr",
343
+ mode,
344
+ reason,
345
+ payload: { repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, feasibility: codingTaskSpec.feasibility },
346
+ });
347
+ eventLedger.appendEvent({
348
+ type: "attempt_blocked",
349
+ repoFullName: parsed.repoFullName,
350
+ payload: { issueNumber: parsed.issueNumber, reason },
351
+ });
352
+ const infeasibleResult = {
353
+ outcome: "blocked_infeasible",
354
+ reason,
355
+ verdict: codingTaskSpec.verdict,
356
+ avoidReasons: codingTaskSpec.feasibility.avoidReasons,
357
+ raiseReasons: codingTaskSpec.feasibility.raiseReasons,
358
+ repoFullName: parsed.repoFullName,
359
+ issueNumber: parsed.issueNumber,
360
+ minerLogin: parsed.minerLogin,
361
+ base: parsed.base,
362
+ mode,
363
+ attemptId,
364
+ };
365
+ if (parsed.json) {
366
+ console.log(JSON.stringify(infeasibleResult, null, 2));
367
+ } else {
368
+ console.error(
369
+ `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: feasibility verdict "${codingTaskSpec.verdict}" (${[...codingTaskSpec.feasibility.avoidReasons, ...codingTaskSpec.feasibility.raiseReasons].join(", ")}).`,
370
+ );
371
+ }
372
+ options.onResult?.(infeasibleResult);
373
+ return 4;
374
+ }
375
+
376
+ const amsPolicy = await (options.resolveAmsPolicy ?? resolveAmsPolicy)(parsed.repoFullName, { env });
377
+
378
+ // Real per-repo pause (#5392): read straight from the already-cloned worktree's own .gittensory-miner.yml
379
+ // (resolveMinerGoalSpec never throws -- a missing/malformed file degrades to killSwitch.paused: false, so
380
+ // this can't fail this attempt on its own). Threaded into BOTH checkMinerKillSwitch (killSwitchScope, used
381
+ // by the freshness/submission gate) and the governor context (killSwitchRepoPaused, used by the Governor
382
+ // chokepoint) -- the same two places the GLOBAL kill switch already reaches.
383
+ const resolveGoalSpec = options.resolveMinerGoalSpec ?? resolveMinerGoalSpec;
384
+ const minerGoalSpec = resolveGoalSpec(worktreeResult.repoPath);
385
+ const repoPaused = minerGoalSpec.spec.killSwitch.paused;
386
+
387
+ const checkKillSwitch = options.checkMinerKillSwitch ?? checkMinerKillSwitch;
388
+ const killSwitchScope = checkKillSwitch({ env, repoPaused }).scope;
389
+
390
+ const loopInput = buildAttemptLoopInput({
391
+ codingTaskSpec,
392
+ reviewContext,
393
+ worktreePath: worktreeResult.worktreePath,
394
+ attemptId,
395
+ mode,
396
+ repoFullName: parsed.repoFullName,
397
+ minerLogin: parsed.minerLogin,
398
+ rejectionSignaled: false,
399
+ amsPolicySpec: amsPolicy.spec,
400
+ branchRef: worktreeResult.branchName,
401
+ });
402
+ const governor = buildAttemptGovernorContext(env, amsPolicy.spec, repoPaused);
403
+
404
+ // Real soft-claim (#5393): recorded once we've committed to a real attempt (past feasibility), so a
405
+ // sibling miner process on this machine sees it via claimLedger.listClaims/listActiveClaims while this
406
+ // attempt is in flight. Released in `finally` on every terminal outcome -- mirrors the worktree
407
+ // allocation slot's own acquire-then-always-release pattern below. The real claimedAt this returns is
408
+ // ALSO this miner's own claim-time for the post-submission conflict check further down (#4848).
409
+ const claimRecord = claimLedger.claimIssue(parsed.repoFullName, parsed.issueNumber, `attempt:${attemptId}`);
410
+ claimedIssue = true;
411
+
412
+ const runAttemptPipeline = options.runMinerAttempt ?? runMinerAttempt;
413
+ const result = await runAttemptPipeline(
414
+ {
415
+ loopInput,
416
+ issueNumber: parsed.issueNumber,
417
+ minerLogin: parsed.minerLogin,
418
+ base: parsed.base,
419
+ killSwitchScope,
420
+ slopThreshold: amsPolicy.spec.slopThreshold,
421
+ submissionMode: amsPolicy.spec.submissionMode,
422
+ governor,
423
+ },
424
+ deps,
425
+ );
426
+
427
+ worktreeResult.attemptOk = result.outcome === "submitted";
428
+
429
+ // Real claim-conflict resolution (#4848): only meaningful once a real PR exists, so this only ever runs
430
+ // on a real "submitted" outcome. checkSubmissionFreshness (inside runMinerAttempt) already caught the
431
+ // common pre-submission case; this closes the narrower TOCTOU window where two miners raced past that
432
+ // check almost simultaneously -- see claim-conflict-resolver.js's own header for why the adjudicator
433
+ // can only run POST-submission (it needs a real PR number on both sides of the election).
434
+ let claimConflict;
435
+ if (result.outcome === "submitted") {
436
+ const selfPrNumber = parsePrNumberFromExecResult(result.execResult, parsed.repoFullName);
437
+ if (selfPrNumber !== null) {
438
+ const resolveConflict = options.resolveClaimConflict ?? resolveClaimConflict;
439
+ claimConflict = await resolveConflict(
440
+ {
441
+ repoFullName: parsed.repoFullName,
442
+ issueNumber: parsed.issueNumber,
443
+ selfPrNumber,
444
+ selfClaimedAt: claimRecord.claimedAt,
445
+ minerLogin: parsed.minerLogin,
446
+ },
447
+ { fetchLiveIssueSnapshot: deps.fetchLiveIssueSnapshot, executeLocalWrite: deps.executeLocalWrite },
448
+ );
449
+ }
450
+ }
451
+
452
+ const finalResult = {
453
+ outcome: `attempt_${result.outcome}`,
454
+ repoFullName: parsed.repoFullName,
455
+ issueNumber: parsed.issueNumber,
456
+ minerLogin: parsed.minerLogin,
457
+ base: parsed.base,
458
+ mode,
459
+ attemptId,
460
+ submissionMode: amsPolicy.spec.submissionMode,
461
+ // Every runMinerAttempt outcome carries a real loopResult (#5135's loop needs its genuine turn-usage and
462
+ // cost to save real GovernorCapUsage via governor-state.js's saveCapUsage -- nothing else in the codebase
463
+ // calls it yet). Surfaced flat rather than the whole loopResult object, matching this result's own
464
+ // shallow shape. costUsd is real only for the agent-sdk provider (its own SDK result message reports
465
+ // total_cost_usd); CLI-subprocess providers (claude-cli/codex-cli) report no cost signal today, so this
466
+ // is 0 for those -- an honest absence, not a fabricated number.
467
+ totalTurnsUsed: result.loopResult.totalTurnsUsed,
468
+ totalCostUsd: result.loopResult.totalCostUsd,
469
+ iterationsUsed: result.loopResult.iterationsUsed,
470
+ ...("reason" in result ? { reason: result.reason } : {}),
471
+ ...("decision" in result ? { decision: result.decision } : {}),
472
+ ...("spec" in result ? { spec: result.spec } : {}),
473
+ ...("execResult" in result ? { execResult: result.execResult } : {}),
474
+ // Present only on a real "submitted" outcome whose PR number was recoverable from execResult -- omitted
475
+ // (not fabricated as "checked: false") on every other outcome, and on a submitted outcome where the new
476
+ // PR's number genuinely couldn't be parsed (an honest gap, not silently swallowed).
477
+ ...(claimConflict !== undefined ? { claimConflict } : {}),
478
+ };
479
+
480
+ if (parsed.json) {
481
+ console.log(JSON.stringify(finalResult, null, 2));
482
+ } else {
483
+ console.log(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} finished with outcome: ${result.outcome}.`);
484
+ }
485
+ options.onResult?.(finalResult);
486
+
487
+ switch (result.outcome) {
488
+ case "submitted":
489
+ return 0;
490
+ case "abandon":
491
+ return 7;
492
+ case "stale":
493
+ return 8;
494
+ case "blocked":
495
+ return 9;
496
+ case "governed":
497
+ return 10;
498
+ default:
499
+ return 2;
500
+ }
501
+ } catch (error) {
502
+ return reportCliFailure(parsed.json, describeCliError(error));
503
+ } finally {
504
+ // worktreeResult.attemptOk is set to the REAL runMinerAttempt outcome (submitted = true) once that call
505
+ // happens; every earlier blocked path (rejection/worktree-prep-failure/infeasible) never sets it, since
506
+ // nothing ran in the worktree to postmortem -- those default to `true` (nothing to retain), matching
507
+ // cleanupAttemptWorktree's own retention policy (a failed REAL attempt is what gets retained).
508
+ if (worktreeResult?.ok) {
509
+ const cleanupWorktree = options.cleanupAttemptWorktree ?? cleanupAttemptWorktree;
510
+ await cleanupWorktree(worktreeResult.repoPath, worktreeResult.worktreePath, worktreeResult.attemptOk ?? true);
511
+ }
512
+ // Every terminal outcome past the claim point (submitted/abandon/stale/blocked/governed, or an
513
+ // unexpected throw) releases the soft-claim -- a claim that outlives its own attempt process would
514
+ // wrongly tell a sibling miner this issue is still in flight.
515
+ if (claimedIssue && claimLedger) claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber);
516
+ if (allocation && allocator) allocator.release(attemptId);
517
+ allocator?.close();
518
+ claimLedger?.close();
519
+ eventLedger?.close();
520
+ attemptLog?.close();
521
+ governorLedger?.close();
522
+ }
523
+ }
@@ -0,0 +1,24 @@
1
+ import type { AmsPolicySpec, CodingAgentExecutionMode, IterateLoopInput, SelfReviewContext } from "@loopover/engine";
2
+ import type { AttemptGovernorContext } from "./attempt-runner.js";
3
+ import type { CodingTaskSpecResult } from "./coding-task-spec.js";
4
+
5
+ export function buildAttemptGovernorContext(
6
+ env: Record<string, string | undefined>,
7
+ amsPolicySpec: AmsPolicySpec,
8
+ repoPaused?: boolean,
9
+ ): AttemptGovernorContext;
10
+
11
+ export type BuildAttemptLoopInputInput = {
12
+ codingTaskSpec: Extract<CodingTaskSpecResult, { ready: true }>;
13
+ reviewContext: SelfReviewContext;
14
+ worktreePath: string;
15
+ attemptId: string;
16
+ mode: CodingAgentExecutionMode;
17
+ repoFullName: string;
18
+ minerLogin: string;
19
+ rejectionSignaled: boolean;
20
+ amsPolicySpec: AmsPolicySpec;
21
+ branchRef?: string;
22
+ };
23
+
24
+ export function buildAttemptLoopInput(input: BuildAttemptLoopInputInput): IterateLoopInput;