@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.
- package/DEPLOYMENT.md +156 -0
- package/Dockerfile +38 -0
- package/README.md +269 -0
- package/bin/gittensory-miner-mcp.d.ts +64 -0
- package/bin/gittensory-miner-mcp.js +279 -0
- package/bin/gittensory-miner.js +196 -0
- package/docs/coding-agent-driver.md +140 -0
- package/docs/config-precedence.md +96 -0
- package/docs/cross-repo-discovery-phase1.md +55 -0
- package/docs/discovery-index-contract.md +60 -0
- package/docs/discovery-plane-operator-guide.md +101 -0
- package/docs/env-reference.md +30 -0
- package/docs/fleet-run-manifest.md +50 -0
- package/docs/miner-goal-spec.md +76 -0
- package/docs/miner-selfimprove-calibration.md +105 -0
- package/docs/observability.md +60 -0
- package/docs/operations-runbook.md +209 -0
- package/docs/repo-agnostic-capability-audit.md +129 -0
- package/docs/unattended-scheduling.md +107 -0
- package/expected-engine.version +1 -0
- package/lib/ams-policy.d.ts +20 -0
- package/lib/ams-policy.js +68 -0
- package/lib/attempt-cli.d.ts +99 -0
- package/lib/attempt-cli.js +523 -0
- package/lib/attempt-input-builder.d.ts +24 -0
- package/lib/attempt-input-builder.js +91 -0
- package/lib/attempt-log.d.ts +37 -0
- package/lib/attempt-log.js +177 -0
- package/lib/attempt-runner.d.ts +59 -0
- package/lib/attempt-runner.js +164 -0
- package/lib/attempt-worktree.d.ts +31 -0
- package/lib/attempt-worktree.js +94 -0
- package/lib/calibration-cli.d.ts +1 -0
- package/lib/calibration-cli.js +92 -0
- package/lib/calibration-run.d.ts +161 -0
- package/lib/calibration-run.js +232 -0
- package/lib/calibration-types.d.ts +46 -0
- package/lib/calibration-types.js +74 -0
- package/lib/calibration.d.ts +24 -0
- package/lib/calibration.js +99 -0
- package/lib/ci-poller.d.ts +33 -0
- package/lib/ci-poller.js +234 -0
- package/lib/claim-adjudication.d.ts +21 -0
- package/lib/claim-adjudication.js +36 -0
- package/lib/claim-conflict-resolver.d.ts +29 -0
- package/lib/claim-conflict-resolver.js +101 -0
- package/lib/claim-ledger-cli.d.ts +59 -0
- package/lib/claim-ledger-cli.js +320 -0
- package/lib/claim-ledger-expiry.d.ts +20 -0
- package/lib/claim-ledger-expiry.js +41 -0
- package/lib/claim-ledger.d.ts +63 -0
- package/lib/claim-ledger.js +303 -0
- package/lib/cli-error.d.ts +3 -0
- package/lib/cli-error.js +27 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +72 -0
- package/lib/coding-agent-construction.d.ts +17 -0
- package/lib/coding-agent-construction.js +103 -0
- package/lib/coding-agent-house-rules.d.ts +24 -0
- package/lib/coding-agent-house-rules.js +64 -0
- package/lib/coding-task-spec.d.ts +44 -0
- package/lib/coding-task-spec.js +183 -0
- package/lib/config-precedence.d.ts +25 -0
- package/lib/config-precedence.js +38 -0
- package/lib/deny-check.d.ts +11 -0
- package/lib/deny-check.js +76 -0
- package/lib/deny-hook-synthesis.d.ts +93 -0
- package/lib/deny-hook-synthesis.js +399 -0
- package/lib/deny-hooks.d.ts +28 -0
- package/lib/deny-hooks.js +155 -0
- package/lib/deployment-docs-audit.d.ts +41 -0
- package/lib/deployment-docs-audit.js +115 -0
- package/lib/discover-cli.d.ts +94 -0
- package/lib/discover-cli.js +284 -0
- package/lib/discovery-throttle.d.ts +8 -0
- package/lib/discovery-throttle.js +33 -0
- package/lib/env-file-indirection.d.ts +4 -0
- package/lib/env-file-indirection.js +45 -0
- package/lib/event-ledger-cli.d.ts +82 -0
- package/lib/event-ledger-cli.js +266 -0
- package/lib/event-ledger.d.ts +37 -0
- package/lib/event-ledger.js +210 -0
- package/lib/execute-local-write.d.ts +14 -0
- package/lib/execute-local-write.js +50 -0
- package/lib/feasibility-cli.d.ts +25 -0
- package/lib/feasibility-cli.js +80 -0
- package/lib/forge-config.d.ts +17 -0
- package/lib/forge-config.js +37 -0
- package/lib/governor-action-mode.d.ts +26 -0
- package/lib/governor-action-mode.js +47 -0
- package/lib/governor-chokepoint-persisted.d.ts +18 -0
- package/lib/governor-chokepoint-persisted.js +46 -0
- package/lib/governor-chokepoint.d.ts +14 -0
- package/lib/governor-chokepoint.js +52 -0
- package/lib/governor-kill-switch.d.ts +26 -0
- package/lib/governor-kill-switch.js +47 -0
- package/lib/governor-ledger-cli.d.ts +32 -0
- package/lib/governor-ledger-cli.js +155 -0
- package/lib/governor-ledger.d.ts +46 -0
- package/lib/governor-ledger.js +203 -0
- package/lib/governor-open-pr.d.ts +21 -0
- package/lib/governor-open-pr.js +27 -0
- package/lib/governor-pause-cli.d.ts +23 -0
- package/lib/governor-pause-cli.js +171 -0
- package/lib/governor-run-halt.d.ts +37 -0
- package/lib/governor-run-halt.js +58 -0
- package/lib/governor-state.d.ts +63 -0
- package/lib/governor-state.js +363 -0
- package/lib/governor-write-rate-limit.d.ts +30 -0
- package/lib/governor-write-rate-limit.js +64 -0
- package/lib/harness-submission-trigger.d.ts +69 -0
- package/lib/harness-submission-trigger.js +138 -0
- package/lib/http-retry.d.ts +12 -0
- package/lib/http-retry.js +52 -0
- package/lib/laptop-init.d.ts +53 -0
- package/lib/laptop-init.js +332 -0
- package/lib/live-issue-snapshot.d.ts +16 -0
- package/lib/live-issue-snapshot.js +117 -0
- package/lib/local-store.d.ts +18 -0
- package/lib/local-store.js +62 -0
- package/lib/logger.d.ts +58 -0
- package/lib/logger.js +167 -0
- package/lib/loop-cli.d.ts +66 -0
- package/lib/loop-cli.js +540 -0
- package/lib/loop-closure.d.ts +35 -0
- package/lib/loop-closure.js +66 -0
- package/lib/loop-reentry.d.ts +51 -0
- package/lib/loop-reentry.js +124 -0
- package/lib/manage-poll.d.ts +79 -0
- package/lib/manage-poll.js +254 -0
- package/lib/manage-status.d.ts +70 -0
- package/lib/manage-status.js +241 -0
- package/lib/metrics-cli.d.ts +9 -0
- package/lib/metrics-cli.js +50 -0
- package/lib/migrate-cli.d.ts +24 -0
- package/lib/migrate-cli.js +111 -0
- package/lib/miner-goal-spec.d.ts +12 -0
- package/lib/miner-goal-spec.js +67 -0
- package/lib/opportunity-fanout.d.ts +86 -0
- package/lib/opportunity-fanout.js +597 -0
- package/lib/opportunity-ranker.d.ts +36 -0
- package/lib/opportunity-ranker.js +118 -0
- package/lib/orb-export.d.ts +54 -0
- package/lib/orb-export.js +206 -0
- package/lib/plan-store-cli.d.ts +37 -0
- package/lib/plan-store-cli.js +151 -0
- package/lib/plan-store.d.ts +51 -0
- package/lib/plan-store.js +231 -0
- package/lib/policy-doc-cache.d.ts +25 -0
- package/lib/policy-doc-cache.js +79 -0
- package/lib/policy-verdict-cache.d.ts +35 -0
- package/lib/policy-verdict-cache.js +106 -0
- package/lib/portfolio-dashboard.d.ts +30 -0
- package/lib/portfolio-dashboard.js +105 -0
- package/lib/portfolio-discovery.d.ts +29 -0
- package/lib/portfolio-discovery.js +100 -0
- package/lib/portfolio-queue-cli.d.ts +87 -0
- package/lib/portfolio-queue-cli.js +509 -0
- package/lib/portfolio-queue-expiry.d.ts +20 -0
- package/lib/portfolio-queue-expiry.js +51 -0
- package/lib/portfolio-queue-manager.d.ts +50 -0
- package/lib/portfolio-queue-manager.js +134 -0
- package/lib/portfolio-queue.d.ts +62 -0
- package/lib/portfolio-queue.js +351 -0
- package/lib/pr-disposition-poller.d.ts +26 -0
- package/lib/pr-disposition-poller.js +163 -0
- package/lib/pr-number-parse.d.ts +4 -0
- package/lib/pr-number-parse.js +23 -0
- package/lib/pr-outcome.d.ts +41 -0
- package/lib/pr-outcome.js +90 -0
- package/lib/prediction-ledger.d.ts +47 -0
- package/lib/prediction-ledger.js +222 -0
- package/lib/pretooluse-hook.d.ts +31 -0
- package/lib/pretooluse-hook.js +93 -0
- package/lib/process-lifecycle.d.ts +31 -0
- package/lib/process-lifecycle.js +106 -0
- package/lib/purge-cli.d.ts +44 -0
- package/lib/purge-cli.js +198 -0
- package/lib/rejection-signal.d.ts +6 -0
- package/lib/rejection-signal.js +101 -0
- package/lib/rejection-state-machine.d.ts +37 -0
- package/lib/rejection-state-machine.js +81 -0
- package/lib/rejection-templates.d.ts +12 -0
- package/lib/rejection-templates.js +71 -0
- package/lib/replay-objective-anchor.d.ts +79 -0
- package/lib/replay-objective-anchor.js +179 -0
- package/lib/replay-snapshot.d.ts +41 -0
- package/lib/replay-snapshot.js +288 -0
- package/lib/replay-task-generation.d.ts +126 -0
- package/lib/replay-task-generation.js +223 -0
- package/lib/repo-clone.d.ts +19 -0
- package/lib/repo-clone.js +102 -0
- package/lib/run-state-cli.d.ts +27 -0
- package/lib/run-state-cli.js +154 -0
- package/lib/run-state.d.ts +37 -0
- package/lib/run-state.js +150 -0
- package/lib/schema-version.d.ts +17 -0
- package/lib/schema-version.js +71 -0
- package/lib/self-review-context.d.ts +28 -0
- package/lib/self-review-context.js +339 -0
- package/lib/slop-assessment.d.ts +3 -0
- package/lib/slop-assessment.js +20 -0
- package/lib/stack-detection.d.ts +41 -0
- package/lib/stack-detection.js +248 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +441 -0
- package/lib/store-maintenance.d.ts +31 -0
- package/lib/store-maintenance.js +183 -0
- package/lib/submission-freshness-check.d.ts +32 -0
- package/lib/submission-freshness-check.js +93 -0
- package/lib/update-check.d.ts +36 -0
- package/lib/update-check.js +161 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.js +10 -0
- package/lib/worktree-allocator.d.ts +39 -0
- package/lib/worktree-allocator.js +262 -0
- package/package.json +50 -0
- 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;
|