@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,91 @@
|
|
|
1
|
+
import { isGlobalMinerKillSwitch, isGlobalMinerLiveModeOptIn } from "@loopover/engine";
|
|
2
|
+
|
|
3
|
+
// Pure composers for runMinerAttempt's real input (#5132, Wave 3.5 -- the final assembly). Everything here is
|
|
4
|
+
// a plain in/out transform over already-fetched/already-computed real data (coding-task-spec, #5239;
|
|
5
|
+
// self-review-context, #5145; worktree preparation, #5237/#5252; AmsPolicySpec, #5249) -- no fetching, no IO,
|
|
6
|
+
// same discipline as coding-task-spec.js's own composers.
|
|
7
|
+
//
|
|
8
|
+
// KNOWN, DOCUMENTED GAPS (not fabricated -- explicitly left as real, narrow follow-ups):
|
|
9
|
+
// - governor.convergenceInput is a first-attempt-shaped literal ({ attempts: 0, consecutiveFailures: 0,
|
|
10
|
+
// reenqueues: 0, reachedDone: false }), not a real per-issue query. attempt-log.js's schema has no
|
|
11
|
+
// repo+issue index (attemptId embeds a timestamp, so it's not a stable group key), and reenqueue counts
|
|
12
|
+
// aren't tracked ANYWHERE yet (non-convergence.ts's own header says that belongs on the portfolio-queue
|
|
13
|
+
// table once it grows attempt-history columns -- a real, separate schema change, not something to fake
|
|
14
|
+
// here). This literal is only ever an UNDER-estimate (reads "fresh, no prior failures" even on a real
|
|
15
|
+
// Nth attempt), which fails toward LETTING an attempt through, not blocking one -- documented, not silent.
|
|
16
|
+
// - governor.reputationHistory/selfPlagiarismCandidate/selfPlagiarismRecentSubmissions are omitted, which
|
|
17
|
+
// chokepoint.ts's own design treats as "skip that stage entirely" -- an honest absence, not a fabricated
|
|
18
|
+
// "clean" verdict.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Assemble the real Governor chokepoint context for one attempt. rateLimitBuckets/rateLimitBackoffAttempts/
|
|
22
|
+
* capUsage are deliberately omitted -- evaluateGovernorChokepointGatePersisted (#5134) auto-loads them from
|
|
23
|
+
* the persisted governor-state store when absent.
|
|
24
|
+
*
|
|
25
|
+
* `repoPaused` (#5392) is the caller's own resolved `MinerGoalSpec.killSwitch.paused` for the target repo
|
|
26
|
+
* (miner-goal-spec.js's resolveMinerGoalSpec) -- this composer stays pure and just threads whatever the
|
|
27
|
+
* caller already resolved through; passing nothing keeps the prior fails-open-on-that-axis-only behavior.
|
|
28
|
+
*
|
|
29
|
+
* @param {Record<string, string | undefined>} env
|
|
30
|
+
* @param {import("@loopover/engine").AmsPolicySpec} amsPolicySpec
|
|
31
|
+
* @param {boolean} [repoPaused]
|
|
32
|
+
* @returns {import("./attempt-runner.js").AttemptGovernorContext}
|
|
33
|
+
*/
|
|
34
|
+
export function buildAttemptGovernorContext(env, amsPolicySpec, repoPaused) {
|
|
35
|
+
return {
|
|
36
|
+
killSwitchGlobal: isGlobalMinerKillSwitch(env),
|
|
37
|
+
killSwitchRepoPaused: repoPaused,
|
|
38
|
+
liveModeGlobalOptIn: isGlobalMinerLiveModeOptIn(env),
|
|
39
|
+
capLimits: amsPolicySpec.capLimits,
|
|
40
|
+
convergenceInput: { attempts: 0, consecutiveFailures: 0, reenqueues: 0, reachedDone: false },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Assemble the real IterateLoopInput for one attempt from every already-computed real dependency. Pure --
|
|
46
|
+
* throws nothing itself (callers are expected to have already validated `codingTaskSpec.ready`).
|
|
47
|
+
*
|
|
48
|
+
* @param {{
|
|
49
|
+
* codingTaskSpec: Extract<import("./coding-task-spec.js").CodingTaskSpecResult, { ready: true }>,
|
|
50
|
+
* reviewContext: import("@loopover/engine").SelfReviewContext,
|
|
51
|
+
* worktreePath: string,
|
|
52
|
+
* attemptId: string,
|
|
53
|
+
* mode: import("@loopover/engine").CodingAgentExecutionMode,
|
|
54
|
+
* repoFullName: string,
|
|
55
|
+
* minerLogin: string,
|
|
56
|
+
* rejectionSignaled: boolean,
|
|
57
|
+
* amsPolicySpec: import("@loopover/engine").AmsPolicySpec,
|
|
58
|
+
* branchRef?: string,
|
|
59
|
+
* }} input
|
|
60
|
+
* @returns {import("@loopover/engine").IterateLoopInput}
|
|
61
|
+
*/
|
|
62
|
+
export function buildAttemptLoopInput(input) {
|
|
63
|
+
return {
|
|
64
|
+
attemptId: input.attemptId,
|
|
65
|
+
workingDirectory: input.worktreePath,
|
|
66
|
+
acceptanceCriteriaPath: input.codingTaskSpec.acceptanceCriteriaPath,
|
|
67
|
+
instructions: input.codingTaskSpec.instructions,
|
|
68
|
+
mode: input.mode,
|
|
69
|
+
maxIterations: input.amsPolicySpec.maxIterations,
|
|
70
|
+
maxTurnsPerIteration: input.amsPolicySpec.maxTurnsPerIteration,
|
|
71
|
+
// Real mid-attempt budget (#5395): the SAME Governor cap ceilings that already bound cross-cycle spend
|
|
72
|
+
// (loop-cli.js's after-the-fact governorState.saveCapUsage) now also bound this ONE attempt in progress,
|
|
73
|
+
// via the engine's real accumulateAttemptUsage/evaluateAttemptBudget -- a runaway attempt can no longer
|
|
74
|
+
// burn through the entire cross-cycle budget before anything reacts. No maxTokens: no driver reports a
|
|
75
|
+
// real per-iteration token count today, so that axis has no real ceiling to set (never fabricated).
|
|
76
|
+
budget: {
|
|
77
|
+
maxTurns: input.amsPolicySpec.capLimits.turns,
|
|
78
|
+
maxWallClockMs: input.amsPolicySpec.capLimits.elapsedMs,
|
|
79
|
+
maxCostUsd: input.amsPolicySpec.capLimits.budget,
|
|
80
|
+
},
|
|
81
|
+
repoFullName: input.repoFullName,
|
|
82
|
+
contributorLogin: input.minerLogin,
|
|
83
|
+
title: input.codingTaskSpec.title,
|
|
84
|
+
body: input.codingTaskSpec.body,
|
|
85
|
+
labels: input.codingTaskSpec.labels,
|
|
86
|
+
linkedIssues: input.codingTaskSpec.linkedIssues,
|
|
87
|
+
branchRef: input.branchRef,
|
|
88
|
+
reviewContext: input.reviewContext,
|
|
89
|
+
rejectionSignaled: input.rejectionSignaled,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AttemptLogEvent } from "@loopover/engine";
|
|
2
|
+
|
|
3
|
+
export type AttemptLogEntry = {
|
|
4
|
+
id: number;
|
|
5
|
+
seq: number;
|
|
6
|
+
eventType: string;
|
|
7
|
+
attemptId: string;
|
|
8
|
+
actionClass: string;
|
|
9
|
+
mode: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
payload: Record<string, unknown>;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ReadAttemptLogEventsFilter = {
|
|
16
|
+
attemptId?: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type AttemptLog = {
|
|
20
|
+
dbPath: string;
|
|
21
|
+
appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry;
|
|
22
|
+
readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[];
|
|
23
|
+
exportAttemptLogJsonl(attemptId: string): string;
|
|
24
|
+
close(): void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function resolveAttemptLogDbPath(env?: Record<string, string | undefined>): string;
|
|
28
|
+
|
|
29
|
+
export function initAttemptLog(dbPath?: string): AttemptLog;
|
|
30
|
+
|
|
31
|
+
export function appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry;
|
|
32
|
+
|
|
33
|
+
export function readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[];
|
|
34
|
+
|
|
35
|
+
export function exportAttemptLogJsonl(attemptId: string): string;
|
|
36
|
+
|
|
37
|
+
export function closeDefaultAttemptLog(): void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { formatAttemptLogJsonl, normalizeAttemptLogEvent } from "@loopover/engine";
|
|
2
|
+
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
|
|
3
|
+
|
|
4
|
+
// Append-only driver attempt log (#4294): a structured, attempt-scoped event trace for every CodingAgentDriver run
|
|
5
|
+
// (started, tool/edit, succeeded/failed/aborted). IMMUTABILITY INVARIANT: INSERT + SELECT only — rows are never
|
|
6
|
+
// rewritten or removed after append.
|
|
7
|
+
//
|
|
8
|
+
// Why a sibling store instead of extending event-ledger.js: event-ledger is the general miner-loop audit trail
|
|
9
|
+
// (discovered_issue, plan_built, pr_prepared, …) keyed by repo scope with a growing free-form type vocabulary.
|
|
10
|
+
// Attempt events are keyed by attempt_id, validated against the engine's fixed ATTEMPT_LOG_EVENT_TYPES, and are
|
|
11
|
+
// exported per attempt as JSONL — mixing both into one table would couple unrelated lifecycles and complicate the
|
|
12
|
+
// per-attempt dump path. This module mirrors governor-ledger.js: engine holds pure normalization, miner holds SQLite.
|
|
13
|
+
|
|
14
|
+
const defaultDbFileName = "attempt-log.sqlite3";
|
|
15
|
+
let defaultAttemptLog = null;
|
|
16
|
+
|
|
17
|
+
export function resolveAttemptLogDbPath(env = process.env) {
|
|
18
|
+
return resolveLocalStoreDbPath(defaultDbFileName, "GITTENSORY_MINER_ATTEMPT_LOG_DB", env);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeDbPath(dbPath) {
|
|
22
|
+
return normalizeLocalStoreDbPath(dbPath, resolveAttemptLogDbPath(), "invalid_attempt_log_db_path");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read-filter attempt scope: omitted/nullish → unscoped (all events); otherwise a non-empty attempt id. */
|
|
26
|
+
function normalizeReadAttemptIdFilter(attemptId) {
|
|
27
|
+
if (attemptId === undefined || attemptId === null) return undefined;
|
|
28
|
+
if (typeof attemptId !== "string") throw new Error("invalid_attempt_id");
|
|
29
|
+
const trimmed = attemptId.trim();
|
|
30
|
+
if (!trimmed) throw new Error("invalid_attempt_id");
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Export requires an explicit attempt id — JSONL dumps are always per attempt. */
|
|
35
|
+
function normalizeRequiredAttemptId(attemptId) {
|
|
36
|
+
const normalized = normalizeReadAttemptIdFilter(attemptId);
|
|
37
|
+
if (normalized === undefined) throw new Error("invalid_attempt_id");
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rowToEntry(row) {
|
|
42
|
+
let payload;
|
|
43
|
+
try {
|
|
44
|
+
payload = JSON.parse(row.payload_json);
|
|
45
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
46
|
+
throw new Error("corrupted_attempt_log_row");
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error("corrupted_attempt_log_row");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
seq: row.seq,
|
|
54
|
+
eventType: row.event_type,
|
|
55
|
+
attemptId: row.attempt_id,
|
|
56
|
+
actionClass: row.action_class,
|
|
57
|
+
mode: row.mode,
|
|
58
|
+
reason: row.reason,
|
|
59
|
+
payload,
|
|
60
|
+
createdAt: row.created_at,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rowToNormalized(row) {
|
|
65
|
+
return {
|
|
66
|
+
eventType: row.event_type,
|
|
67
|
+
attemptId: row.attempt_id,
|
|
68
|
+
actionClass: row.action_class,
|
|
69
|
+
mode: row.mode,
|
|
70
|
+
reason: row.reason,
|
|
71
|
+
payloadJson: row.payload_json,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Opens the append-only attempt log, creating the table on first use. `seq` is a monotonically increasing counter
|
|
77
|
+
* maintained by this module (next = current MAX(seq) + 1) with a UNIQUE(seq) constraint. Rows read back in seq ASC
|
|
78
|
+
* order. (#4294)
|
|
79
|
+
*/
|
|
80
|
+
export function initAttemptLog(dbPath = resolveAttemptLogDbPath()) {
|
|
81
|
+
const resolvedPath = normalizeDbPath(dbPath);
|
|
82
|
+
const db = openLocalStoreDb(resolvedPath);
|
|
83
|
+
db.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS attempt_log_events (
|
|
85
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
seq INTEGER NOT NULL UNIQUE,
|
|
87
|
+
attempt_id TEXT NOT NULL,
|
|
88
|
+
event_type TEXT NOT NULL,
|
|
89
|
+
action_class TEXT NOT NULL,
|
|
90
|
+
mode TEXT NOT NULL,
|
|
91
|
+
reason TEXT NOT NULL,
|
|
92
|
+
payload_json TEXT NOT NULL,
|
|
93
|
+
created_at TEXT NOT NULL
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
db.exec(
|
|
97
|
+
"CREATE INDEX IF NOT EXISTS idx_attempt_log_attempt ON attempt_log_events (attempt_id, seq)",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const nextSeqStatement = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS nextSeq FROM attempt_log_events");
|
|
101
|
+
const appendStatement = db.prepare(`
|
|
102
|
+
INSERT INTO attempt_log_events (
|
|
103
|
+
seq, attempt_id, event_type, action_class, mode, reason, payload_json, created_at
|
|
104
|
+
)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
|
+
`);
|
|
107
|
+
const getByIdStatement = db.prepare("SELECT * FROM attempt_log_events WHERE id = ?");
|
|
108
|
+
const readAllStatement = db.prepare("SELECT * FROM attempt_log_events ORDER BY seq ASC");
|
|
109
|
+
const readByAttemptStatement = db.prepare(
|
|
110
|
+
"SELECT * FROM attempt_log_events WHERE attempt_id = ? ORDER BY seq ASC",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
dbPath: resolvedPath,
|
|
115
|
+
appendAttemptLogEvent(event) {
|
|
116
|
+
const normalized = normalizeAttemptLogEvent(event);
|
|
117
|
+
const createdAt = new Date().toISOString();
|
|
118
|
+
db.exec("BEGIN IMMEDIATE");
|
|
119
|
+
try {
|
|
120
|
+
const { nextSeq } = nextSeqStatement.get();
|
|
121
|
+
const result = appendStatement.run(
|
|
122
|
+
nextSeq,
|
|
123
|
+
normalized.attemptId,
|
|
124
|
+
normalized.eventType,
|
|
125
|
+
normalized.actionClass,
|
|
126
|
+
normalized.mode,
|
|
127
|
+
normalized.reason,
|
|
128
|
+
normalized.payloadJson,
|
|
129
|
+
createdAt,
|
|
130
|
+
);
|
|
131
|
+
const entry = rowToEntry(getByIdStatement.get(Number(result.lastInsertRowid)));
|
|
132
|
+
db.exec("COMMIT");
|
|
133
|
+
return entry;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
db.exec("ROLLBACK");
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
readAttemptLogEvents(filter = {}) {
|
|
140
|
+
const attemptId = normalizeReadAttemptIdFilter(filter.attemptId);
|
|
141
|
+
const rows =
|
|
142
|
+
attemptId === undefined ? readAllStatement.all() : readByAttemptStatement.all(attemptId);
|
|
143
|
+
return rows.map(rowToEntry);
|
|
144
|
+
},
|
|
145
|
+
exportAttemptLogJsonl(attemptId) {
|
|
146
|
+
const scopedAttemptId = normalizeRequiredAttemptId(attemptId);
|
|
147
|
+
const rows = readByAttemptStatement.all(scopedAttemptId);
|
|
148
|
+
return formatAttemptLogJsonl(rows.map(rowToNormalized));
|
|
149
|
+
},
|
|
150
|
+
close() {
|
|
151
|
+
db.close();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getDefaultAttemptLog() {
|
|
157
|
+
defaultAttemptLog ??= initAttemptLog();
|
|
158
|
+
return defaultAttemptLog;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function appendAttemptLogEvent(event) {
|
|
162
|
+
return getDefaultAttemptLog().appendAttemptLogEvent(event);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function readAttemptLogEvents(filter) {
|
|
166
|
+
return getDefaultAttemptLog().readAttemptLogEvents(filter);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function exportAttemptLogJsonl(attemptId) {
|
|
170
|
+
return getDefaultAttemptLog().exportAttemptLogJsonl(attemptId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function closeDefaultAttemptLog() {
|
|
174
|
+
if (!defaultAttemptLog) return;
|
|
175
|
+
defaultAttemptLog.close();
|
|
176
|
+
defaultAttemptLog = null;
|
|
177
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodingAgentDriver,
|
|
3
|
+
GovernorDecision,
|
|
4
|
+
IterateLoopInput,
|
|
5
|
+
IterateLoopResult,
|
|
6
|
+
LocalWriteActionSpec,
|
|
7
|
+
} from "@loopover/engine";
|
|
8
|
+
import type { HarnessSubmissionDecision, HarnessSubmissionEventLedger } from "./harness-submission-trigger.js";
|
|
9
|
+
import type { SubmissionFreshnessClaimLedger, LiveIssueSnapshot, FreshnessAbortReason } from "./submission-freshness-check.js";
|
|
10
|
+
import type { GovernorChokepointInputPersisted } from "./governor-chokepoint-persisted.js";
|
|
11
|
+
import type { GovernorState } from "./governor-state.js";
|
|
12
|
+
|
|
13
|
+
export const ATTEMPT_OUTCOMES: readonly ["abandon", "stale", "blocked", "governed", "submitted"];
|
|
14
|
+
|
|
15
|
+
// rateLimitBuckets/rateLimitBackoffAttempts/capUsage are optional here (via GovernorChokepointInputPersisted,
|
|
16
|
+
// not the engine's own GovernorChokepointInput) so a caller can omit them and let evaluateGovernorChokepointGatePersisted
|
|
17
|
+
// (#5134) auto-supply real persisted state -- forcing them required at this layer would make every caller
|
|
18
|
+
// hand-thread honest-but-stale zero defaults on every invocation, silently defeating that persistence.
|
|
19
|
+
export type AttemptGovernorContext = Omit<GovernorChokepointInputPersisted, "actionClass" | "repoFullName" | "nowMs" | "wouldBeAction">;
|
|
20
|
+
|
|
21
|
+
export type AttemptInput = {
|
|
22
|
+
loopInput: IterateLoopInput;
|
|
23
|
+
issueNumber: number;
|
|
24
|
+
minerLogin: string;
|
|
25
|
+
base: string;
|
|
26
|
+
killSwitchScope: "global" | "repo" | "none";
|
|
27
|
+
slopThreshold: "clean" | "low" | "elevated" | "high";
|
|
28
|
+
submissionMode: "observe" | "enforce";
|
|
29
|
+
maxConsecutiveGateBlocks?: number;
|
|
30
|
+
draft?: boolean;
|
|
31
|
+
governor: AttemptGovernorContext;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type AttemptDeps = {
|
|
35
|
+
driver: CodingAgentDriver;
|
|
36
|
+
runSlopAssessment: (input: unknown) => unknown;
|
|
37
|
+
appendAttemptLogEvent: (event: unknown) => void;
|
|
38
|
+
claimLedger: SubmissionFreshnessClaimLedger;
|
|
39
|
+
fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<LiveIssueSnapshot | null>;
|
|
40
|
+
eventLedger: HarnessSubmissionEventLedger;
|
|
41
|
+
/** Injected governor-ledger append (mirrors evaluateGovernorChokepointGate's own `options.append`); omitted
|
|
42
|
+
* falls back to that function's own default (the real default governor ledger). */
|
|
43
|
+
governorLedgerAppend?: (event: unknown) => unknown;
|
|
44
|
+
/** Injected governor-state store (#5134); omitted falls back to evaluateGovernorChokepointGatePersisted's
|
|
45
|
+
* own default (opens + closes the real default governor-state store for this one call). */
|
|
46
|
+
governorState?: GovernorState;
|
|
47
|
+
sessionStartMs?: number;
|
|
48
|
+
nowMs: number;
|
|
49
|
+
executeLocalWrite: (spec: LocalWriteActionSpec) => Promise<unknown>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type AttemptResult =
|
|
53
|
+
| { outcome: "abandon"; loopResult: IterateLoopResult }
|
|
54
|
+
| { outcome: "stale"; reason: FreshnessAbortReason; loopResult: IterateLoopResult }
|
|
55
|
+
| { outcome: "blocked"; decision: HarnessSubmissionDecision; loopResult: IterateLoopResult }
|
|
56
|
+
| { outcome: "governed"; decision: GovernorDecision; loopResult: IterateLoopResult }
|
|
57
|
+
| { outcome: "submitted"; spec: LocalWriteActionSpec; execResult: unknown; loopResult: IterateLoopResult };
|
|
58
|
+
|
|
59
|
+
export function runMinerAttempt(input: AttemptInput, deps: AttemptDeps): Promise<AttemptResult>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { buildOpenPrSpec } from "@loopover/engine";
|
|
2
|
+
import { runIterateLoop } from "@loopover/engine";
|
|
3
|
+
import { checkSubmissionFreshness } from "./submission-freshness-check.js";
|
|
4
|
+
import { evaluateGovernorChokepointGatePersisted } from "./governor-chokepoint-persisted.js";
|
|
5
|
+
import { prepareOpenPrSubmission } from "./harness-submission-trigger.js";
|
|
6
|
+
|
|
7
|
+
// The real driving-loop entrypoint (#2337): the missing link between #2333's iterate-loop orchestrator and an
|
|
8
|
+
// actual, executed open_pr write. Composes, in order: runIterateLoop (create -> score -> self-review -> decide,
|
|
9
|
+
// #2333) -> on handoff, checkSubmissionFreshness (#3007) -> prepareOpenPrSubmission (#2336/#2337) -> the
|
|
10
|
+
// Governor chokepoint (#2340, which itself composes kill-switch, dry-run, rate-limit, budget caps, non-
|
|
11
|
+
// convergence, self-reputation-throttle, and self-plagiarism -- see chokepoint.ts's own module doc comment for
|
|
12
|
+
// the exact precedence ladder) -> on allowed:true, builds the REAL open_pr command via the now-shared
|
|
13
|
+
// buildOpenPrSpec (@loopover/engine, moved from root src/mcp/local-write-tools.ts) and executes it.
|
|
14
|
+
//
|
|
15
|
+
// WORKTREE LIFECYCLE IS NOT THIS MODULE'S JOB: runIterateLoop already takes a plain `workingDirectory` string
|
|
16
|
+
// (packages/gittensory-engine/src/miner/iterate-loop.ts's own IterateLoopInput), deliberately agnostic about
|
|
17
|
+
// where it came from. Allocating one is the caller's job, via the already-built slot allocator
|
|
18
|
+
// (worktree-allocator.js, #4297) -- this module composes the create/review/gate/submit sequence #2337 is
|
|
19
|
+
// actually about, not worktree allocation policy, which is a separate, already-solved concern.
|
|
20
|
+
//
|
|
21
|
+
// KNOWN, DELIBERATE GAP (not silently papered over -- was an injected-but-unwired seam before this module
|
|
22
|
+
// existed, and remains so here):
|
|
23
|
+
// - `deps.runSlopAssessment` has no production implementation anywhere in this package. The real slop scorer
|
|
24
|
+
// (src/signals/slop.ts, 518 lines, 5 sibling src/signals/** dependencies) is far larger and more
|
|
25
|
+
// interconnected than local-write-tools.ts was, so extracting it is separate, substantial scope -- this
|
|
26
|
+
// function requires a real one be injected rather than silently stubbing a result that would either always
|
|
27
|
+
// pass (unsafe) or always fail (useless).
|
|
28
|
+
//
|
|
29
|
+
// `input.governor`'s cross-attempt state (rate-limit buckets, backoff attempts, budget-cap usage) DOES now
|
|
30
|
+
// persist across separate process invocations (#5134, governor-state.js), via
|
|
31
|
+
// evaluateGovernorChokepointGatePersisted -- callers no longer need to hand-thread honest empty/zero defaults
|
|
32
|
+
// on every invocation; `capUsage` is loaded from that same store but its post-attempt save stays the caller's
|
|
33
|
+
// job (see governor-chokepoint-persisted.js's own header for why: nothing computes "the next capUsage" from a
|
|
34
|
+
// verdict, only the attempt's real outcome does). Reputation/self-plagiarism state also has real persistence
|
|
35
|
+
// primitives (governor-state.js) but isn't auto-loaded here yet -- `input.governor.reputationHistory`/
|
|
36
|
+
// `selfPlagiarismCandidate`/`selfPlagiarismRecentSubmissions` are still caller-supplied optional fields on
|
|
37
|
+
// GovernorChokepointInput, same as before.
|
|
38
|
+
|
|
39
|
+
/** True once the loop reaches handoff AND every downstream gate (freshness, submission, governor) allows. */
|
|
40
|
+
export const ATTEMPT_OUTCOMES = Object.freeze(["abandon", "stale", "blocked", "governed", "submitted"]);
|
|
41
|
+
|
|
42
|
+
function assertFn(value, name) {
|
|
43
|
+
if (typeof value !== "function") throw new Error(`invalid_${name}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertDeps(deps) {
|
|
47
|
+
if (!deps || typeof deps !== "object") throw new Error("invalid_attempt_deps");
|
|
48
|
+
assertFn(deps.runSlopAssessment, "run_slop_assessment");
|
|
49
|
+
assertFn(deps.appendAttemptLogEvent, "append_attempt_log_event");
|
|
50
|
+
assertFn(deps.fetchLiveIssueSnapshot, "fetch_live_issue_snapshot");
|
|
51
|
+
assertFn(deps.executeLocalWrite, "execute_local_write");
|
|
52
|
+
if (!deps.driver || typeof deps.driver.run !== "function") throw new Error("invalid_driver");
|
|
53
|
+
if (!deps.claimLedger || typeof deps.claimLedger.listClaims !== "function") throw new Error("invalid_claim_ledger");
|
|
54
|
+
if (!deps.eventLedger || typeof deps.eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger");
|
|
55
|
+
if (typeof deps.nowMs !== "number" || !Number.isFinite(deps.nowMs)) throw new Error("invalid_now_ms");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertInput(input) {
|
|
59
|
+
if (!input || typeof input !== "object") throw new Error("invalid_attempt_input");
|
|
60
|
+
if (!input.loopInput || typeof input.loopInput !== "object") throw new Error("invalid_loop_input");
|
|
61
|
+
if (!Number.isInteger(input.issueNumber) || input.issueNumber < 1) throw new Error("invalid_issue_number");
|
|
62
|
+
if (typeof input.minerLogin !== "string" || !input.minerLogin.trim()) throw new Error("invalid_miner_login");
|
|
63
|
+
if (typeof input.base !== "string" || !input.base.trim()) throw new Error("invalid_base");
|
|
64
|
+
if (!["global", "repo", "none"].includes(input.killSwitchScope)) throw new Error("invalid_kill_switch_scope");
|
|
65
|
+
if (!["clean", "low", "elevated", "high"].includes(input.slopThreshold)) throw new Error("invalid_slop_threshold");
|
|
66
|
+
if (!["observe", "enforce"].includes(input.submissionMode)) throw new Error("invalid_submission_mode");
|
|
67
|
+
if (!input.governor || typeof input.governor !== "object") throw new Error("invalid_governor_context");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run one full attempt end to end: iterate-loop -> (on handoff) freshness -> submission-gate -> Governor
|
|
72
|
+
* chokepoint -> (on allowed:true) build + execute the real open_pr command. Fails closed (throws) on malformed
|
|
73
|
+
* input/deps, mirroring every sibling module in this pipeline.
|
|
74
|
+
*
|
|
75
|
+
* @param {{
|
|
76
|
+
* loopInput: import("@loopover/engine").IterateLoopInput,
|
|
77
|
+
* issueNumber: number,
|
|
78
|
+
* minerLogin: string,
|
|
79
|
+
* base: string,
|
|
80
|
+
* killSwitchScope: "global"|"repo"|"none",
|
|
81
|
+
* slopThreshold: "clean"|"low"|"elevated"|"high",
|
|
82
|
+
* submissionMode: "observe"|"enforce",
|
|
83
|
+
* maxConsecutiveGateBlocks?: number,
|
|
84
|
+
* draft?: boolean,
|
|
85
|
+
* governor: Omit<import("@loopover/engine").GovernorChokepointInput, "actionClass"|"repoFullName"|"nowMs"|"wouldBeAction">,
|
|
86
|
+
* }} input
|
|
87
|
+
* @param {{
|
|
88
|
+
* driver: import("@loopover/engine").CodingAgentDriver,
|
|
89
|
+
* runSlopAssessment: Function,
|
|
90
|
+
* appendAttemptLogEvent: Function,
|
|
91
|
+
* claimLedger: object,
|
|
92
|
+
* fetchLiveIssueSnapshot: Function,
|
|
93
|
+
* eventLedger: object,
|
|
94
|
+
* governorLedgerAppend?: Function,
|
|
95
|
+
* governorState?: import("./governor-state.js").GovernorState,
|
|
96
|
+
* sessionStartMs?: number,
|
|
97
|
+
* nowMs: number,
|
|
98
|
+
* executeLocalWrite: (spec: import("@loopover/engine").LocalWriteActionSpec) => Promise<unknown>,
|
|
99
|
+
* }} deps
|
|
100
|
+
*/
|
|
101
|
+
export async function runMinerAttempt(input, deps) {
|
|
102
|
+
assertInput(input);
|
|
103
|
+
assertDeps(deps);
|
|
104
|
+
|
|
105
|
+
const loopResult = await runIterateLoop(input.loopInput, {
|
|
106
|
+
driver: deps.driver,
|
|
107
|
+
runSlopAssessment: deps.runSlopAssessment,
|
|
108
|
+
appendAttemptLogEvent: deps.appendAttemptLogEvent,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (loopResult.outcome === "abandon") {
|
|
112
|
+
return { outcome: "abandon", loopResult };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const handoffPacket = loopResult.handoffPacket;
|
|
116
|
+
|
|
117
|
+
const freshness = await checkSubmissionFreshness(
|
|
118
|
+
{ repoFullName: input.loopInput.repoFullName, issueNumber: input.issueNumber, minerLogin: input.minerLogin },
|
|
119
|
+
{ claimLedger: deps.claimLedger, fetchLiveIssueSnapshot: deps.fetchLiveIssueSnapshot, eventLedger: deps.eventLedger },
|
|
120
|
+
);
|
|
121
|
+
if (!freshness.fresh) {
|
|
122
|
+
return { outcome: "stale", reason: freshness.reason, loopResult };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const submission = await prepareOpenPrSubmission(
|
|
126
|
+
{
|
|
127
|
+
killSwitchScope: input.killSwitchScope,
|
|
128
|
+
repoFullName: input.loopInput.repoFullName,
|
|
129
|
+
handoffPacket,
|
|
130
|
+
slopThreshold: input.slopThreshold,
|
|
131
|
+
mode: input.submissionMode,
|
|
132
|
+
maxConsecutiveGateBlocks: input.maxConsecutiveGateBlocks,
|
|
133
|
+
base: input.base,
|
|
134
|
+
title: input.loopInput.title,
|
|
135
|
+
body: input.loopInput.body ?? "",
|
|
136
|
+
draft: input.draft,
|
|
137
|
+
},
|
|
138
|
+
{ eventLedger: deps.eventLedger, sessionStartMs: deps.sessionStartMs },
|
|
139
|
+
);
|
|
140
|
+
if (!submission.ready) {
|
|
141
|
+
return { outcome: "blocked", decision: submission.decision, loopResult };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const governed = evaluateGovernorChokepointGatePersisted(
|
|
145
|
+
{
|
|
146
|
+
actionClass: "open_pr",
|
|
147
|
+
repoFullName: input.loopInput.repoFullName,
|
|
148
|
+
nowMs: deps.nowMs,
|
|
149
|
+
wouldBeAction: submission.openPrInput,
|
|
150
|
+
...input.governor,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
...(deps.governorLedgerAppend ? { append: deps.governorLedgerAppend } : {}),
|
|
154
|
+
...(deps.governorState ? { governorState: deps.governorState } : {}),
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
if (!governed.decision.allowed) {
|
|
158
|
+
return { outcome: "governed", decision: governed.decision, loopResult };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const spec = buildOpenPrSpec(submission.openPrInput);
|
|
162
|
+
const execResult = await deps.executeLocalWrite(spec);
|
|
163
|
+
return { outcome: "submitted", spec, execResult, loopResult };
|
|
164
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { WorktreeExecFn } from "@loopover/engine";
|
|
2
|
+
import type { RunGitFn } from "./repo-clone.js";
|
|
3
|
+
|
|
4
|
+
export function createRealWorktreeExec(timeoutMs?: number): WorktreeExecFn;
|
|
5
|
+
|
|
6
|
+
export type PrepareAttemptWorktreeOptions = {
|
|
7
|
+
baseBranch?: string;
|
|
8
|
+
cloneBaseDir?: string;
|
|
9
|
+
env?: Record<string, string | undefined>;
|
|
10
|
+
exec?: WorktreeExecFn;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
remoteUrl?: string;
|
|
13
|
+
runGit?: RunGitFn;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PrepareAttemptWorktreeResult =
|
|
17
|
+
| { ok: true; worktreePath: string; branchName: string; repoPath: string }
|
|
18
|
+
| { ok: false; repoPath?: string; error: string };
|
|
19
|
+
|
|
20
|
+
export function prepareAttemptWorktree(
|
|
21
|
+
repoFullName: string,
|
|
22
|
+
attemptId: string,
|
|
23
|
+
options?: PrepareAttemptWorktreeOptions,
|
|
24
|
+
): Promise<PrepareAttemptWorktreeResult>;
|
|
25
|
+
|
|
26
|
+
export function cleanupAttemptWorktree(
|
|
27
|
+
repoPath: string,
|
|
28
|
+
worktreePath: string,
|
|
29
|
+
attemptOk: boolean,
|
|
30
|
+
options?: { exec?: WorktreeExecFn; timeoutMs?: number },
|
|
31
|
+
): Promise<{ ok: boolean; removed: boolean; error?: string }>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { addWorktree, removeWorktree, shouldRetainWorktree } from "@loopover/engine";
|
|
3
|
+
import { ensureRepoCloned } from "./repo-clone.js";
|
|
4
|
+
|
|
5
|
+
// Real attempt-worktree preparation (#5132, Wave 3.5 follow-up). Composes ensureRepoCloned (repo-clone.js,
|
|
6
|
+
// the missing base-clone-management step) with @loopover/engine's already-built, already-tested
|
|
7
|
+
// addWorktree/removeWorktree primitives -- which existed but were never called from this package, so
|
|
8
|
+
// `workingDirectory` handed to runIterateLoop was always just an empty directory with no real git repo in
|
|
9
|
+
// it. This is the caller that finally exercises them for real.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Real child_process-backed implementation of the engine's WorktreeExecFn contract. Resolves (never
|
|
15
|
+
* rejects) on error/timeout, mirroring coding-agent-construction.js's createRealCliSubprocessSpawn -- a
|
|
16
|
+
* failed `git worktree add`'s stderr is the diagnosable signal, not something to lose to an unhandled
|
|
17
|
+
* rejection.
|
|
18
|
+
*
|
|
19
|
+
* @returns {import("@loopover/engine").WorktreeExecFn}
|
|
20
|
+
*/
|
|
21
|
+
export function createRealWorktreeExec(timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
22
|
+
return (cmd, args, opts) =>
|
|
23
|
+
new Promise((resolve) => {
|
|
24
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
child.kill("SIGKILL");
|
|
29
|
+
resolve({ code: null, stdout, stderr: `${stderr}\ntimed_out_after_${timeoutMs}ms`.trim() });
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
child.stdout?.on("data", (chunk) => {
|
|
32
|
+
stdout += chunk.toString("utf8");
|
|
33
|
+
});
|
|
34
|
+
child.stderr?.on("data", (chunk) => {
|
|
35
|
+
stderr += chunk.toString("utf8");
|
|
36
|
+
});
|
|
37
|
+
child.on("error", (err) => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
resolve({ code: null, stdout, stderr: err.message });
|
|
40
|
+
});
|
|
41
|
+
child.on("close", (code) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve({ code, stdout, stderr });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Prepare a real, isolated git worktree for one attempt: ensure the target repo's base clone exists and is
|
|
50
|
+
* current, then create a fresh `git worktree` off it on a deterministically-named branch. Fails closed
|
|
51
|
+
* (`ok: false`) on any step's failure rather than handing back a half-prepared directory.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} repoFullName
|
|
54
|
+
* @param {string} attemptId
|
|
55
|
+
* @param {{
|
|
56
|
+
* baseBranch?: string, cloneBaseDir?: string, env?: Record<string, string | undefined>,
|
|
57
|
+
* exec?: import("@loopover/engine").WorktreeExecFn, timeoutMs?: number,
|
|
58
|
+
* remoteUrl?: string, runGit?: import("./repo-clone.js").RunGitFn,
|
|
59
|
+
* }} [options]
|
|
60
|
+
* @returns {Promise<{ ok: boolean, worktreePath?: string, branchName?: string, repoPath?: string, error?: string }>}
|
|
61
|
+
*/
|
|
62
|
+
export async function prepareAttemptWorktree(repoFullName, attemptId, options = {}) {
|
|
63
|
+
const cloneResult = await ensureRepoCloned(repoFullName, {
|
|
64
|
+
baseBranch: options.baseBranch,
|
|
65
|
+
cloneBaseDir: options.cloneBaseDir,
|
|
66
|
+
env: options.env,
|
|
67
|
+
timeoutMs: options.timeoutMs,
|
|
68
|
+
remoteUrl: options.remoteUrl,
|
|
69
|
+
runGit: options.runGit,
|
|
70
|
+
});
|
|
71
|
+
if (!cloneResult.ok) return { ok: false, error: cloneResult.error ?? "ensure_repo_cloned_failed" };
|
|
72
|
+
|
|
73
|
+
const exec = options.exec ?? createRealWorktreeExec(options.timeoutMs);
|
|
74
|
+
const baseBranch = typeof options.baseBranch === "string" && options.baseBranch.trim() ? options.baseBranch.trim() : "main";
|
|
75
|
+
const added = await addWorktree({ exec, repoPath: cloneResult.repoPath, baseBranch, attemptId });
|
|
76
|
+
if (!added.ok) return { ok: false, repoPath: cloneResult.repoPath, error: added.error ?? "git_worktree_add_failed" };
|
|
77
|
+
|
|
78
|
+
return { ok: true, worktreePath: added.plan.worktreePath, branchName: added.plan.branchName, repoPath: cloneResult.repoPath };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Tear down an attempt's worktree once the attempt concludes, per the engine's own retention policy: a
|
|
83
|
+
* failed attempt's worktree is RETAINED for post-mortem inspection, a succeeded one is removed.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} repoPath
|
|
86
|
+
* @param {string} worktreePath
|
|
87
|
+
* @param {boolean} attemptOk
|
|
88
|
+
* @param {{ exec?: import("@loopover/engine").WorktreeExecFn, timeoutMs?: number }} [options]
|
|
89
|
+
* @returns {Promise<{ ok: boolean, removed: boolean, error?: string }>}
|
|
90
|
+
*/
|
|
91
|
+
export async function cleanupAttemptWorktree(repoPath, worktreePath, attemptOk, options = {}) {
|
|
92
|
+
const exec = options.exec ?? createRealWorktreeExec(options.timeoutMs);
|
|
93
|
+
return removeWorktree({ exec, repoPath, worktreePath, retain: shouldRetainWorktree(attemptOk) });
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function runCalibrationCli(args?: string[], env?: Record<string, string | undefined>): number;
|