@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,183 @@
|
|
|
1
|
+
// Local-store maintenance for the miner (#4834): SQLite integrity checks + append-only ledger retention.
|
|
2
|
+
//
|
|
3
|
+
// Three independent, side-effect-light helpers used by `doctor`, the ledgers, and `purge-cli.js`:
|
|
4
|
+
// 1. checkStoreIntegrity — run `PRAGMA integrity_check` on one store file and report health, so `doctor` can
|
|
5
|
+
// flag a corrupted store instead of only probing a single one with `SELECT 1`.
|
|
6
|
+
// 2. resolveLedgerRetentionPolicy / pruneLedgerByRetention — an opt-in, age- and/or size-based retention
|
|
7
|
+
// policy for the unbounded append-only ledgers (event, governor, prediction), which otherwise grow forever.
|
|
8
|
+
// OFF by default: retention only runs when an operator sets the env opt-in.
|
|
9
|
+
// 3. purgeStoreByRepo — an explicit, operator-invoked delete of every row for one repo (#5564, right-to-be-
|
|
10
|
+
// forgotten). Distinct from retention pruning: never runs automatically, always caller-initiated via
|
|
11
|
+
// `purge-cli.js`, and always reports how many rows it removed so a purge is never silent.
|
|
12
|
+
// Pure control flow over injected inputs (a DB handle, an env object, a caller-supplied clock) — no network, and
|
|
13
|
+
// no internal clock read in the prune path so it stays deterministic and unit-testable.
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { DatabaseSync } from "node:sqlite";
|
|
16
|
+
|
|
17
|
+
/** Env opt-ins for ledger retention (unset ⇒ retention disabled). */
|
|
18
|
+
export const LEDGER_RETENTION_DAYS_ENV = "GITTENSORY_MINER_LEDGER_RETENTION_DAYS";
|
|
19
|
+
export const LEDGER_RETENTION_MAX_ROWS_ENV = "GITTENSORY_MINER_LEDGER_RETENTION_MAX_ROWS";
|
|
20
|
+
|
|
21
|
+
/** Fixed retention specs for the three append-only ledgers. These identifiers are INTERNAL constants — never
|
|
22
|
+
* caller/user text — and are validated as plain identifiers before interpolation as defence in depth. */
|
|
23
|
+
export const EVENT_LEDGER_RETENTION_SPEC = { table: "miner_event_ledger", timestampColumn: "created_at", orderColumn: "id" };
|
|
24
|
+
export const GOVERNOR_LEDGER_RETENTION_SPEC = { table: "governor_events", timestampColumn: "ts", orderColumn: "id" };
|
|
25
|
+
export const PREDICTION_LEDGER_RETENTION_SPEC = { table: "predictions", timestampColumn: "ts", orderColumn: "id" };
|
|
26
|
+
|
|
27
|
+
/** Fixed purge specs (#5564) for the four stores whose rows are directly scoped by a `repoColumn`. Same
|
|
28
|
+
* internal-constant-only discipline as the retention specs above. `attempt-log.js` is deliberately absent: its
|
|
29
|
+
* payload is a free-form `Record<string, unknown>` with no dedicated repo column, so a precise per-repo purge
|
|
30
|
+
* isn't possible there without risking false matches — `purge-cli.js` reports it as not-purgeable instead. */
|
|
31
|
+
export const CLAIM_LEDGER_PURGE_SPEC = { table: "miner_claims", repoColumn: "repo_full_name" };
|
|
32
|
+
export const EVENT_LEDGER_PURGE_SPEC = { table: "miner_event_ledger", repoColumn: "repo_full_name" };
|
|
33
|
+
export const GOVERNOR_LEDGER_PURGE_SPEC = { table: "governor_events", repoColumn: "repo_full_name" };
|
|
34
|
+
export const PREDICTION_LEDGER_PURGE_SPEC = { table: "predictions", repoColumn: "repo_full_name" };
|
|
35
|
+
|
|
36
|
+
const SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
37
|
+
|
|
38
|
+
/** A readable message for a caught value, whether or not it is an Error. */
|
|
39
|
+
export function describeError(error) {
|
|
40
|
+
return error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Classify raw `PRAGMA integrity_check` rows. A healthy database yields a single `"ok"` row; a corrupt one yields
|
|
45
|
+
* one row per problem. Pure — extracted so both the healthy and problem paths are testable without a genuinely
|
|
46
|
+
* corrupt file (which SQLite typically refuses to open at all, i.e. the catch path below).
|
|
47
|
+
* @param {Array<{ integrity_check?: unknown }>} rows
|
|
48
|
+
* @returns {{ ok: boolean, note: string }}
|
|
49
|
+
*/
|
|
50
|
+
export function classifyIntegrityRows(rows) {
|
|
51
|
+
const problems = rows.map((row) => String(row.integrity_check)).filter((value) => value !== "ok");
|
|
52
|
+
return problems.length === 0 ? { ok: true, note: "ok" } : { ok: false, note: problems.join("; ") };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run `PRAGMA integrity_check` on a single store file. A store that does not exist yet is healthy by absence
|
|
57
|
+
* (nothing to corrupt). Never throws: a store that cannot be opened or read is reported as not-ok, so one bad
|
|
58
|
+
* store cannot abort the whole doctor sweep. Opens the connection driver-enforced read-only -- `readOnly`
|
|
59
|
+
* (camelCase) is the only option key node:sqlite recognizes for this; the lowercase `readonly` is silently
|
|
60
|
+
* ignored and opens read-write instead (the exact gotcha claim-ledger.js's own openClaimLedgerReadOnly already
|
|
61
|
+
* documents), which would defeat the read-only guarantee this function's own docs claim.
|
|
62
|
+
* @param {string} name - the check label (e.g. "event-ledger").
|
|
63
|
+
* @param {string} dbPath - the store file path.
|
|
64
|
+
* @returns {{ name: string, ok: boolean, detail: string }}
|
|
65
|
+
*/
|
|
66
|
+
export function checkStoreIntegrity(name, dbPath) {
|
|
67
|
+
if (!existsSync(dbPath)) {
|
|
68
|
+
return { name, ok: true, detail: `${dbPath}: not created yet` };
|
|
69
|
+
}
|
|
70
|
+
let db;
|
|
71
|
+
try {
|
|
72
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
73
|
+
const { ok, note } = classifyIntegrityRows(db.prepare("PRAGMA integrity_check").all());
|
|
74
|
+
return { name, ok, detail: `${dbPath}: ${note}` };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { name, ok: false, detail: `${dbPath}: ${describeError(error)}` };
|
|
77
|
+
} finally {
|
|
78
|
+
db?.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Coerce an env value to a positive integer, or null (unset/blank/zero/negative/non-finite ⇒ null ⇒ disabled).
|
|
83
|
+
* Floors BEFORE the positivity test, so a fractional value below 1 (e.g. "0.5") floors to 0 and disables the
|
|
84
|
+
* bound rather than becoming a dangerous 0 that would prune the whole ledger. */
|
|
85
|
+
function positiveIntOrNull(raw) {
|
|
86
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") return null;
|
|
87
|
+
const numeric = Math.floor(Number(raw));
|
|
88
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the opt-in ledger retention policy from an env object. OFF by default: returns null unless at least
|
|
93
|
+
* one bound is set to a positive value. A zero/negative/non-numeric value is treated as unset. When set, returns
|
|
94
|
+
* `{ maxAgeMs? }` (from a day count) and/or `{ maxRows? }`.
|
|
95
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
96
|
+
* @returns {{ maxAgeMs?: number, maxRows?: number } | null}
|
|
97
|
+
*/
|
|
98
|
+
export function resolveLedgerRetentionPolicy(env = process.env) {
|
|
99
|
+
const maxAgeDays = positiveIntOrNull(env[LEDGER_RETENTION_DAYS_ENV]);
|
|
100
|
+
const maxRows = positiveIntOrNull(env[LEDGER_RETENTION_MAX_ROWS_ENV]);
|
|
101
|
+
if (maxAgeDays === null && maxRows === null) return null;
|
|
102
|
+
const policy = {};
|
|
103
|
+
if (maxAgeDays !== null) policy.maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
104
|
+
if (maxRows !== null) policy.maxRows = maxRows;
|
|
105
|
+
return policy;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Prune one append-only ledger per a resolved retention policy: delete rows older than the age bound AND rows
|
|
110
|
+
* beyond the row-count bound (keeping the newest `maxRows` by `orderColumn`), atomically. A null policy is a
|
|
111
|
+
* no-op. `nowMs` is caller-supplied (no internal clock). Timestamp columns are UTC ISO-8601 strings, which sort
|
|
112
|
+
* lexicographically in chronological order, so a string comparison against the ISO cutoff selects older rows.
|
|
113
|
+
* @param {import("node:sqlite").DatabaseSync} db
|
|
114
|
+
* @param {{ table: string, timestampColumn: string, orderColumn: string }} spec
|
|
115
|
+
* @param {{ maxAgeMs?: number, maxRows?: number } | null} policy
|
|
116
|
+
* @param {number} nowMs
|
|
117
|
+
* @returns {number} rows deleted
|
|
118
|
+
*/
|
|
119
|
+
export function pruneLedgerByRetention(db, spec, policy, nowMs) {
|
|
120
|
+
if (!policy) return 0;
|
|
121
|
+
for (const identifier of [spec.table, spec.timestampColumn, spec.orderColumn]) {
|
|
122
|
+
if (!SQL_IDENTIFIER.test(identifier)) throw new Error(`unsafe SQL identifier: ${identifier}`);
|
|
123
|
+
}
|
|
124
|
+
let deleted = 0;
|
|
125
|
+
db.exec("BEGIN");
|
|
126
|
+
try {
|
|
127
|
+
// Both bounds are guarded to be strictly positive as defence in depth: a 0 age would prune everything older
|
|
128
|
+
// than `now`, and a 0 row-cap makes `LIMIT 0` match no rows so `NOT IN (empty)` would delete the whole ledger.
|
|
129
|
+
if (policy.maxAgeMs !== undefined && policy.maxAgeMs > 0) {
|
|
130
|
+
const cutoff = new Date(nowMs - policy.maxAgeMs).toISOString();
|
|
131
|
+
const info = db.prepare(`DELETE FROM ${spec.table} WHERE ${spec.timestampColumn} < ?`).run(cutoff);
|
|
132
|
+
deleted += Number(info.changes);
|
|
133
|
+
}
|
|
134
|
+
if (policy.maxRows !== undefined && policy.maxRows >= 1) {
|
|
135
|
+
const info = db
|
|
136
|
+
.prepare(
|
|
137
|
+
`DELETE FROM ${spec.table} WHERE ${spec.orderColumn} NOT IN ` +
|
|
138
|
+
`(SELECT ${spec.orderColumn} FROM ${spec.table} ORDER BY ${spec.orderColumn} DESC LIMIT ?)`,
|
|
139
|
+
)
|
|
140
|
+
.run(policy.maxRows);
|
|
141
|
+
deleted += Number(info.changes);
|
|
142
|
+
}
|
|
143
|
+
db.exec("COMMIT");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
db.exec("ROLLBACK");
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return deleted;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Delete every row for one repo from a store (#5564). Unlike `pruneLedgerByRetention`, this never runs
|
|
153
|
+
* automatically — it exists solely so `purge-cli.js` can give an operator a real right-to-be-forgotten path.
|
|
154
|
+
* `repoFullName` is caller-normalized (owner/repo) before reaching here; this function only guards the SQL
|
|
155
|
+
* identifiers, matching `pruneLedgerByRetention`'s own defence-in-depth discipline.
|
|
156
|
+
* @param {import("node:sqlite").DatabaseSync} db
|
|
157
|
+
* @param {{ table: string, repoColumn: string }} spec
|
|
158
|
+
* @param {string} repoFullName
|
|
159
|
+
* @returns {number} rows deleted
|
|
160
|
+
*/
|
|
161
|
+
export function purgeStoreByRepo(db, spec, repoFullName) {
|
|
162
|
+
for (const identifier of [spec.table, spec.repoColumn]) {
|
|
163
|
+
if (!SQL_IDENTIFIER.test(identifier)) throw new Error(`unsafe SQL identifier: ${identifier}`);
|
|
164
|
+
}
|
|
165
|
+
const info = db.prepare(`DELETE FROM ${spec.table} WHERE ${spec.repoColumn} = ?`).run(repoFullName);
|
|
166
|
+
return Number(info.changes);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Count rows for one repo in a store without deleting anything (#5564) — the read-only counterpart to
|
|
171
|
+
* `purgeStoreByRepo`, used by `purge-cli.js --dry-run` to report what a real purge would remove.
|
|
172
|
+
* @param {import("node:sqlite").DatabaseSync} db
|
|
173
|
+
* @param {{ table: string, repoColumn: string }} spec
|
|
174
|
+
* @param {string} repoFullName
|
|
175
|
+
* @returns {number} matching row count
|
|
176
|
+
*/
|
|
177
|
+
export function countStoreByRepo(db, spec, repoFullName) {
|
|
178
|
+
for (const identifier of [spec.table, spec.repoColumn]) {
|
|
179
|
+
if (!SQL_IDENTIFIER.test(identifier)) throw new Error(`unsafe SQL identifier: ${identifier}`);
|
|
180
|
+
}
|
|
181
|
+
const row = db.prepare(`SELECT COUNT(*) AS count FROM ${spec.table} WHERE ${spec.repoColumn} = ?`).get(repoFullName);
|
|
182
|
+
return Number(row.count);
|
|
183
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const SUBMISSION_FRESHNESS_ABORT_EVENT: "submission_freshness_abort";
|
|
2
|
+
|
|
3
|
+
export type FreshnessAbortReason = "issue_closed" | "already_addressed" | "claim_superseded" | "live_state_unavailable";
|
|
4
|
+
|
|
5
|
+
export type SubmissionFreshnessCandidate = {
|
|
6
|
+
repoFullName: string;
|
|
7
|
+
issueNumber: number;
|
|
8
|
+
minerLogin: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type LiveIssueSnapshot = {
|
|
12
|
+
state: "open" | "closed";
|
|
13
|
+
referencingPrs: Array<{ number: number; state: "open" | "closed" | "merged"; authorLogin: string; createdAt: string | null }>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SubmissionFreshnessClaimLedger = {
|
|
17
|
+
listClaims(filter: { repoFullName?: string; status?: string }): Array<{ repoFullName: string; issueNumber: number; status: string }>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SubmissionFreshnessEventLedger = {
|
|
21
|
+
appendEvent(event: { type: string; repoFullName?: string; payload: Record<string, unknown> }): unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SubmissionFreshnessDeps = {
|
|
25
|
+
claimLedger: SubmissionFreshnessClaimLedger;
|
|
26
|
+
fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<LiveIssueSnapshot | null>;
|
|
27
|
+
eventLedger: SubmissionFreshnessEventLedger;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type SubmissionFreshnessResult = { fresh: true } | { fresh: false; reason: FreshnessAbortReason };
|
|
31
|
+
|
|
32
|
+
export function checkSubmissionFreshness(candidate: SubmissionFreshnessCandidate, deps: SubmissionFreshnessDeps): Promise<SubmissionFreshnessResult>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Late-binding freshness check before open_pr fires (#3007). A soft-claim made at the start of a long
|
|
2
|
+
// create/iterate loop can go stale by the time a candidate reaches submission: the target issue may have been
|
|
3
|
+
// closed, already fixed by another author, or the miner's own claim may have been released/expired in the
|
|
4
|
+
// interim. This is a FINAL, read-only check immediately before open_pr spec construction -- complementing, not
|
|
5
|
+
// replacing, the claim-time check (src/miner/soft-claim.ts) -- so a stale submission never reaches the Governor
|
|
6
|
+
// chokepoint (governor-chokepoint.js) as a live write attempt: check freshness first, THEN prepareOpenPrSubmission
|
|
7
|
+
// (harness-submission-trigger.js), THEN the Governor. These are separate, sequentially-composed units, not nested
|
|
8
|
+
// calls -- a future real call site wires them together in that order.
|
|
9
|
+
//
|
|
10
|
+
// READ-ONLY BY CONTRACT: never writes anything except its own abort-reason audit event (on staleness only, not
|
|
11
|
+
// on every check -- mirrors this issue's own "log the abort reason" wording, not a per-decision audit trail).
|
|
12
|
+
// The live-state fetch is an injected dependency so this stays testable without real network I/O and agnostic
|
|
13
|
+
// to HOW the caller sources issue/PR state (raw GitHub API, gittensory's own cached MCP data, etc.).
|
|
14
|
+
//
|
|
15
|
+
// FAIL CLOSED: an unreachable/failed live-state fetch is treated as stale (aborts), never as "no evidence of
|
|
16
|
+
// staleness, so proceed" -- mirrors this package's fail-closed convention elsewhere (harness-submission-
|
|
17
|
+
// trigger.js's predicted_gate_unavailable/slop_assessment_unavailable, iterate-loop.ts's ambiguous-on-error).
|
|
18
|
+
//
|
|
19
|
+
// NOT a rejection outcome: staleness is caught BEFORE any PR exists, so it is not the same lifecycle event as
|
|
20
|
+
// rejection-state-machine.js's DISENGAGED_OUTCOME (which handles an EXISTING PR a maintainer closed). "No PR,
|
|
21
|
+
// no noisy failure" here just means: return a quiet not-fresh result, same shape as any other blocked gate
|
|
22
|
+
// decision in this package -- never throw, never surface anything to the target repo.
|
|
23
|
+
|
|
24
|
+
export const SUBMISSION_FRESHNESS_ABORT_EVENT = "submission_freshness_abort";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate whether a submission candidate's live repo state is still fresh enough to proceed toward open_pr.
|
|
28
|
+
* Checks the miner's own claim-ledger status first (local, free) before spending a network round-trip on the
|
|
29
|
+
* live issue/PR snapshot. Fails closed (throws) on a malformed candidate or missing dependency.
|
|
30
|
+
*
|
|
31
|
+
* @param {{ repoFullName: string, issueNumber: number, minerLogin: string }} candidate
|
|
32
|
+
* @param {{
|
|
33
|
+
* claimLedger: { listClaims(filter: { repoFullName?: string, status?: string }): Array<{ repoFullName: string, issueNumber: number, status: string }> },
|
|
34
|
+
* fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<{ state: "open"|"closed", referencingPrs: Array<{ number: number, state: "open"|"closed"|"merged", authorLogin: string }> } | null>,
|
|
35
|
+
* eventLedger: { appendEvent(event: { type: string, repoFullName?: string, payload: Record<string, unknown> }): unknown },
|
|
36
|
+
* }} deps
|
|
37
|
+
*/
|
|
38
|
+
export async function checkSubmissionFreshness(candidate, deps) {
|
|
39
|
+
if (!candidate || typeof candidate !== "object") throw new Error("invalid_freshness_candidate");
|
|
40
|
+
const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : "";
|
|
41
|
+
if (!repoFullName) throw new Error("invalid_repo_full_name");
|
|
42
|
+
if (!Number.isInteger(candidate.issueNumber) || candidate.issueNumber < 1) throw new Error("invalid_issue_number");
|
|
43
|
+
const minerLogin = typeof candidate.minerLogin === "string" ? candidate.minerLogin.trim() : "";
|
|
44
|
+
if (!minerLogin) throw new Error("invalid_miner_login");
|
|
45
|
+
|
|
46
|
+
if (!deps || typeof deps !== "object") throw new Error("invalid_freshness_deps");
|
|
47
|
+
const { claimLedger, fetchLiveIssueSnapshot, eventLedger } = deps;
|
|
48
|
+
if (!claimLedger || typeof claimLedger.listClaims !== "function") throw new Error("invalid_claim_ledger");
|
|
49
|
+
if (typeof fetchLiveIssueSnapshot !== "function") throw new Error("invalid_live_state_fetcher");
|
|
50
|
+
if (!eventLedger || typeof eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger");
|
|
51
|
+
|
|
52
|
+
const claim = claimLedger.listClaims({ repoFullName }).find((c) => c.issueNumber === candidate.issueNumber);
|
|
53
|
+
if (!claim || claim.status !== "active") {
|
|
54
|
+
return abort(eventLedger, repoFullName, candidate.issueNumber, "claim_superseded");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let snapshot;
|
|
58
|
+
try {
|
|
59
|
+
snapshot = await fetchLiveIssueSnapshot(repoFullName, candidate.issueNumber);
|
|
60
|
+
} catch {
|
|
61
|
+
snapshot = null;
|
|
62
|
+
}
|
|
63
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
64
|
+
return abort(eventLedger, repoFullName, candidate.issueNumber, "live_state_unavailable");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (snapshot.state === "closed") {
|
|
68
|
+
return abort(eventLedger, repoFullName, candidate.issueNumber, "issue_closed");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// GitHub logins are case-insensitive for identity purposes (the same account can be echoed back with
|
|
72
|
+
// different casing by different API responses), so a strict `!==` would misclassify the miner's own
|
|
73
|
+
// referencing PR as "another author" whenever the casing happens to differ -- compare case-normalized.
|
|
74
|
+
const minerLoginKey = minerLogin.toLowerCase();
|
|
75
|
+
const referencingPrs = Array.isArray(snapshot.referencingPrs) ? snapshot.referencingPrs : [];
|
|
76
|
+
const addressedByAnotherAuthor = referencingPrs.some(
|
|
77
|
+
(pr) => typeof pr.authorLogin === "string" && pr.authorLogin.trim().toLowerCase() !== minerLoginKey && (pr.state === "merged" || pr.state === "open"),
|
|
78
|
+
);
|
|
79
|
+
if (addressedByAnotherAuthor) {
|
|
80
|
+
return abort(eventLedger, repoFullName, candidate.issueNumber, "already_addressed");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { fresh: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function abort(eventLedger, repoFullName, issueNumber, reason) {
|
|
87
|
+
eventLedger.appendEvent({
|
|
88
|
+
type: SUBMISSION_FRESHNESS_ABORT_EVENT,
|
|
89
|
+
repoFullName,
|
|
90
|
+
payload: { issueNumber, reason },
|
|
91
|
+
});
|
|
92
|
+
return { fresh: false, reason };
|
|
93
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function resolveNpmRegistryUrl(
|
|
2
|
+
env?: Record<string, string | undefined>,
|
|
3
|
+
): string;
|
|
4
|
+
export function resolveUpgradeCommand(packageName?: string): string;
|
|
5
|
+
export function shouldSkipUpdateCheck(
|
|
6
|
+
cliArgs: string[],
|
|
7
|
+
env?: Record<string, string | undefined>,
|
|
8
|
+
): boolean;
|
|
9
|
+
export function compareSemver(a: string, b: string): -1 | 0 | 1 | null;
|
|
10
|
+
export function fetchLatestPackageVersion(input: {
|
|
11
|
+
packageName: string;
|
|
12
|
+
npmRegistryUrl: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
export function maybePrintUpdateNudge(input: {
|
|
16
|
+
packageName: string;
|
|
17
|
+
packageVersion: string;
|
|
18
|
+
npmRegistryUrl: string;
|
|
19
|
+
upgradeCommand: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
export function startUpdateCheck(
|
|
23
|
+
cliArgs: string[],
|
|
24
|
+
input: {
|
|
25
|
+
packageName: string;
|
|
26
|
+
packageVersion: string;
|
|
27
|
+
upgradeCommand?: string;
|
|
28
|
+
env?: Record<string, string | undefined>;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
},
|
|
31
|
+
): Promise<void>;
|
|
32
|
+
export const updateCheckExitGraceMs: number;
|
|
33
|
+
export function awaitOpportunisticUpdateCheck(
|
|
34
|
+
updateCheck: Promise<void>,
|
|
35
|
+
graceMs?: number,
|
|
36
|
+
): Promise<void>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const defaultPackageName = "@loopover/miner";
|
|
2
|
+
const defaultNpmRegistryUrl = "https://registry.npmjs.org";
|
|
3
|
+
|
|
4
|
+
function isLocalRegistryHost(hostname) {
|
|
5
|
+
const normalized = hostname.toLowerCase().replace(/\.$/, "");
|
|
6
|
+
return (
|
|
7
|
+
normalized === "localhost" ||
|
|
8
|
+
normalized === "127.0.0.1" ||
|
|
9
|
+
normalized === "::1" ||
|
|
10
|
+
normalized === "[::1]"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveNpmRegistryUrl(env = process.env) {
|
|
15
|
+
const raw = env.GITTENSORY_NPM_REGISTRY_URL?.trim();
|
|
16
|
+
if (!raw) return defaultNpmRegistryUrl;
|
|
17
|
+
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return defaultNpmRegistryUrl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (url.username || url.password || url.search || url.hash || !url.hostname) {
|
|
26
|
+
return defaultNpmRegistryUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const local = isLocalRegistryHost(url.hostname);
|
|
30
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
31
|
+
return defaultNpmRegistryUrl;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
|
|
35
|
+
return `${url.origin}${path}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveUpgradeCommand(packageName = defaultPackageName) {
|
|
39
|
+
return `npm install -g ${packageName}@latest`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function shouldSkipUpdateCheck(cliArgs, env = process.env) {
|
|
43
|
+
if (/^(1|true|yes)$/i.test(env.GITTENSORY_MINER_NO_UPDATE_CHECK ?? ""))
|
|
44
|
+
return true;
|
|
45
|
+
return cliArgs.includes("--no-update-check");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseSemver(version) {
|
|
49
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(
|
|
50
|
+
String(version ?? "").trim(),
|
|
51
|
+
);
|
|
52
|
+
if (!match) return null;
|
|
53
|
+
return {
|
|
54
|
+
major: Number(match[1]),
|
|
55
|
+
minor: Number(match[2]),
|
|
56
|
+
patch: Number(match[3]),
|
|
57
|
+
prerelease: match[4] ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Numeric identifiers are compared as decimal strings, not via Number(), which loses precision beyond
|
|
62
|
+
// Number.MAX_SAFE_INTEGER (2^53-1): two distinct digit strings past that width can round to the SAME float,
|
|
63
|
+
// making Number(leftId) !== Number(rightId) wrongly report them as equal (mirrors the same fix already applied
|
|
64
|
+
// to compareMcpSemver's comparePrerelease in src/services/mcp-compatibility.ts, #3049). With no leading zeros
|
|
65
|
+
// (semver's own numeric-identifier rule), a longer digit string is the larger number, and equal-length strings
|
|
66
|
+
// compare lexicographically.
|
|
67
|
+
function comparePrerelease(a, b) {
|
|
68
|
+
const left = a.split(".");
|
|
69
|
+
const right = b.split(".");
|
|
70
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
71
|
+
const leftId = left[index];
|
|
72
|
+
const rightId = right[index];
|
|
73
|
+
if (leftId === undefined) return -1;
|
|
74
|
+
if (rightId === undefined) return 1;
|
|
75
|
+
const leftNumeric = /^\d+$/.test(leftId);
|
|
76
|
+
const rightNumeric = /^\d+$/.test(rightId);
|
|
77
|
+
if (leftNumeric && rightNumeric) {
|
|
78
|
+
if (leftId.length !== rightId.length) return leftId.length < rightId.length ? -1 : 1;
|
|
79
|
+
if (leftId !== rightId) return leftId < rightId ? -1 : 1;
|
|
80
|
+
} else if (leftNumeric !== rightNumeric) {
|
|
81
|
+
return leftNumeric ? -1 : 1;
|
|
82
|
+
} else if (leftId !== rightId) {
|
|
83
|
+
return leftId < rightId ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function compareSemver(a, b) {
|
|
90
|
+
const left = parseSemver(a);
|
|
91
|
+
const right = parseSemver(b);
|
|
92
|
+
if (!left || !right) return null;
|
|
93
|
+
for (const part of ["major", "minor", "patch"]) {
|
|
94
|
+
if (left[part] !== right[part]) return left[part] < right[part] ? -1 : 1;
|
|
95
|
+
}
|
|
96
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
97
|
+
if (left.prerelease === null) return 1;
|
|
98
|
+
if (right.prerelease === null) return -1;
|
|
99
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function fetchLatestPackageVersion(input) {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs ?? 5000);
|
|
105
|
+
const registrySlug = input.packageName.startsWith("@")
|
|
106
|
+
? input.packageName.replace("/", "%2F")
|
|
107
|
+
: input.packageName;
|
|
108
|
+
const registryPath = `${input.npmRegistryUrl}/${registrySlug}/latest`;
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(registryPath, {
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
headers: { accept: "application/json" },
|
|
113
|
+
});
|
|
114
|
+
const payload = await response.json().catch(() => ({}));
|
|
115
|
+
if (!response.ok || typeof payload.version !== "string")
|
|
116
|
+
throw new Error("npm_latest_version_unavailable");
|
|
117
|
+
return payload.version;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Non-blocking startup nudge: prints one upgrade line when local is behind npm latest.
|
|
124
|
+
// Mirrors packages/gittensory-mcp/bin/gittensory-mcp.js packageVersion/npmRegistryUrl/upgradeCommand (#2331).
|
|
125
|
+
export async function maybePrintUpdateNudge(input) {
|
|
126
|
+
try {
|
|
127
|
+
const latestVersion = await fetchLatestPackageVersion(input);
|
|
128
|
+
const comparison = compareSemver(input.packageVersion, latestVersion);
|
|
129
|
+
if (comparison !== null && comparison < 0) {
|
|
130
|
+
process.stderr.write(`${input.upgradeCommand}\n`);
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Offline or unreachable registry — never block or fail the CLI.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function startUpdateCheck(cliArgs, input) {
|
|
138
|
+
if (shouldSkipUpdateCheck(cliArgs, input.env)) return Promise.resolve();
|
|
139
|
+
return maybePrintUpdateNudge({
|
|
140
|
+
packageName: input.packageName,
|
|
141
|
+
packageVersion: input.packageVersion,
|
|
142
|
+
npmRegistryUrl: resolveNpmRegistryUrl(input.env),
|
|
143
|
+
upgradeCommand:
|
|
144
|
+
input.upgradeCommand ?? resolveUpgradeCommand(input.packageName),
|
|
145
|
+
timeoutMs: input.timeoutMs,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const updateCheckExitGraceMs = 250;
|
|
150
|
+
|
|
151
|
+
// After command output is printed, give a fast registry response time to emit the nudge
|
|
152
|
+
// without waiting for the full lookup timeout on slow/offline registries.
|
|
153
|
+
export async function awaitOpportunisticUpdateCheck(
|
|
154
|
+
updateCheck,
|
|
155
|
+
graceMs = updateCheckExitGraceMs,
|
|
156
|
+
) {
|
|
157
|
+
await Promise.race([
|
|
158
|
+
updateCheck.catch(() => undefined),
|
|
159
|
+
new Promise((resolve) => setTimeout(resolve, graceMs)),
|
|
160
|
+
]);
|
|
161
|
+
}
|
package/lib/version.d.ts
ADDED
package/lib/version.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ownPackageJson from "../package.json" with { type: "json" };
|
|
2
|
+
|
|
3
|
+
/** Package.json semver at import time — the laptop npm-install default. */
|
|
4
|
+
export const MINER_PACKAGE_VERSION = ownPackageJson.version;
|
|
5
|
+
|
|
6
|
+
/** Resolved miner release id: `GITTENSORY_MINER_VERSION` wins when set (fleet Docker image builds). */
|
|
7
|
+
export function resolveMinerVersion(env = process.env) {
|
|
8
|
+
const override = typeof env.GITTENSORY_MINER_VERSION === "string" ? env.GITTENSORY_MINER_VERSION.trim() : "";
|
|
9
|
+
return override || MINER_PACKAGE_VERSION;
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type WorktreeAllocation = {
|
|
2
|
+
slotIndex: number;
|
|
3
|
+
worktreePath: string;
|
|
4
|
+
attemptId: string | null;
|
|
5
|
+
repoFullName: string | null;
|
|
6
|
+
status: "free" | "active";
|
|
7
|
+
ownerPid: number | null;
|
|
8
|
+
allocatedAt: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type WorktreeAllocator = {
|
|
12
|
+
dbPath: string;
|
|
13
|
+
worktreeBaseDir: string;
|
|
14
|
+
maxConcurrency: number;
|
|
15
|
+
processPid: number;
|
|
16
|
+
acquire(attemptId: string, repoFullName: string): WorktreeAllocation;
|
|
17
|
+
release(attemptId: string): WorktreeAllocation | null;
|
|
18
|
+
listSlots(): WorktreeAllocation[];
|
|
19
|
+
close(): void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function resolveWorktreeAllocatorDbPath(env?: Record<string, string | undefined>): string;
|
|
23
|
+
|
|
24
|
+
export function resolveWorktreeBaseDir(env?: Record<string, string | undefined>): string;
|
|
25
|
+
|
|
26
|
+
export function isProcessAlive(pid: number): boolean;
|
|
27
|
+
|
|
28
|
+
export function openWorktreeAllocator(options?: {
|
|
29
|
+
dbPath?: string;
|
|
30
|
+
worktreeBaseDir?: string;
|
|
31
|
+
maxConcurrency?: number;
|
|
32
|
+
processPid?: number;
|
|
33
|
+
}): WorktreeAllocator;
|
|
34
|
+
|
|
35
|
+
export function acquireWorktree(attemptId: string, repoFullName: string): WorktreeAllocation;
|
|
36
|
+
|
|
37
|
+
export function releaseWorktree(attemptId: string): WorktreeAllocation | null;
|
|
38
|
+
|
|
39
|
+
export function closeDefaultWorktreeAllocator(): void;
|