@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,303 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
|
|
3
|
+
import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
|
|
4
|
+
import { applySchemaMigrations } from "./schema-version.js";
|
|
5
|
+
import { CLAIM_LEDGER_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";
|
|
6
|
+
|
|
7
|
+
// The miner's local soft-claim ledger (#2314): a 100% client-side record of "I'm working on issue #N in repo X",
|
|
8
|
+
// so Phase 2's soft-claim adjudication (sibling issues) has somewhere to persist claims. Schema + CRUD only — no
|
|
9
|
+
// adjudication logic, no network calls, no autonomous writes. The database only lives on this machine; this module
|
|
10
|
+
// never uploads, syncs, or phones home. Mirrors the package's existing local-store pattern (run-state.js,
|
|
11
|
+
// portfolio-queue.js, event-ledger.js) — plain JS + node:sqlite, not the hosted Worker's shared D1 `migrations/`.
|
|
12
|
+
|
|
13
|
+
export const CLAIM_STATUSES = Object.freeze(["active", "released", "expired"]);
|
|
14
|
+
|
|
15
|
+
const defaultDbFileName = "claim-ledger.sqlite3";
|
|
16
|
+
let defaultClaimLedger = null;
|
|
17
|
+
|
|
18
|
+
export function resolveClaimLedgerDbPath(env = process.env) {
|
|
19
|
+
return resolveLocalStoreDbPath(defaultDbFileName, "GITTENSORY_MINER_CLAIM_LEDGER_DB", env);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeDbPath(dbPath) {
|
|
23
|
+
return normalizeLocalStoreDbPath(dbPath, resolveClaimLedgerDbPath(), "invalid_claim_ledger_db_path");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeRepoFullName(repoFullName) {
|
|
27
|
+
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
|
|
28
|
+
const [owner, repo, extra] = repoFullName.trim().split("/");
|
|
29
|
+
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
|
|
30
|
+
return `${owner}/${repo}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeIssueNumber(issueNumber) {
|
|
34
|
+
if (!Number.isInteger(issueNumber) || issueNumber < 1) throw new Error("invalid_issue_number");
|
|
35
|
+
return issueNumber;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563).
|
|
39
|
+
* Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */
|
|
40
|
+
function normalizeApiBaseUrl(apiBaseUrl) {
|
|
41
|
+
if (apiBaseUrl === undefined || apiBaseUrl === null) return DEFAULT_FORGE_CONFIG.apiBaseUrl;
|
|
42
|
+
if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) throw new Error("invalid_api_base_url");
|
|
43
|
+
return apiBaseUrl.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Optional free-text note: omitted/nullish → null; a string is kept as-is; anything else is rejected. */
|
|
47
|
+
function normalizeNote(note) {
|
|
48
|
+
if (note === undefined || note === null) return null;
|
|
49
|
+
if (typeof note !== "string") throw new Error("invalid_note");
|
|
50
|
+
return note;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rowToClaim(row) {
|
|
54
|
+
return {
|
|
55
|
+
id: row.id,
|
|
56
|
+
apiBaseUrl: row.api_base_url,
|
|
57
|
+
repoFullName: row.repo_full_name,
|
|
58
|
+
issueNumber: row.issue_number,
|
|
59
|
+
claimedAt: row.claimed_at,
|
|
60
|
+
status: row.status,
|
|
61
|
+
note: row.note,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// v1 -> v2 (#5563): scope the UNIQUE constraint by (api_base_url, repo_full_name, issue_number) instead of bare
|
|
66
|
+
// (repo_full_name, issue_number) -- two different forge hosts serving a same-named repo/issue must not collide
|
|
67
|
+
// in this ledger. SQLite cannot ALTER a UNIQUE constraint in place, so this rebuilds the table: create the new
|
|
68
|
+
// shape, copy every existing row with the pre-#4784 implicit single-forge default backfilled, drop the old
|
|
69
|
+
// table, rename the new one in. Runs inside applySchemaMigrations' own transaction, so a mid-rebuild failure
|
|
70
|
+
// leaves the file at v1 and retries cleanly on next open.
|
|
71
|
+
function addApiBaseUrlScope(db) {
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TABLE miner_claims_v2 (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
api_base_url TEXT NOT NULL,
|
|
76
|
+
repo_full_name TEXT NOT NULL,
|
|
77
|
+
issue_number INTEGER NOT NULL,
|
|
78
|
+
claimed_at TEXT NOT NULL,
|
|
79
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'released', 'expired')),
|
|
80
|
+
note TEXT,
|
|
81
|
+
UNIQUE (api_base_url, repo_full_name, issue_number)
|
|
82
|
+
)
|
|
83
|
+
`);
|
|
84
|
+
// OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized `status`,
|
|
85
|
+
// e.g. from a hand-edited or otherwise corrupted file) would violate the CHECK constraint above and abort the
|
|
86
|
+
// whole migration. Skipping it here is consistent with that same fail-closed posture, rather than turning one
|
|
87
|
+
// bad row into a permanently unmigratable file.
|
|
88
|
+
db.prepare(
|
|
89
|
+
`INSERT OR IGNORE INTO miner_claims_v2 (id, api_base_url, repo_full_name, issue_number, claimed_at, status, note)
|
|
90
|
+
SELECT id, ?, repo_full_name, issue_number, claimed_at, status, note FROM miner_claims`,
|
|
91
|
+
).run(DEFAULT_FORGE_CONFIG.apiBaseUrl);
|
|
92
|
+
db.exec("DROP TABLE miner_claims");
|
|
93
|
+
db.exec("ALTER TABLE miner_claims_v2 RENAME TO miner_claims");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Opens the local claim ledger, creating the table on first use. `UNIQUE(api_base_url, repo_full_name,
|
|
98
|
+
* issue_number)` keeps ONE row per claimed issue per forge host, and `recordClaim` is a single atomic
|
|
99
|
+
* INSERT…ON CONFLICT statement (no read-then-write), so concurrent claims cannot duplicate a row. (#2314, #5563)
|
|
100
|
+
*/
|
|
101
|
+
export function openClaimLedger(dbPath = resolveClaimLedgerDbPath()) {
|
|
102
|
+
const resolvedPath = normalizeDbPath(dbPath);
|
|
103
|
+
const db = openLocalStoreDb(resolvedPath);
|
|
104
|
+
// LOCAL bookkeeping only: this table records which issues this miner instance has soft-claimed on this
|
|
105
|
+
// machine. It does NOT adjudicate contested duplicates — sibling miners claiming the same issue are
|
|
106
|
+
// resolved elsewhere via `isDuplicateClusterWinnerByClaim` from `@loopover/engine` (#3355).
|
|
107
|
+
db.exec(`
|
|
108
|
+
CREATE TABLE IF NOT EXISTS miner_claims (
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
repo_full_name TEXT NOT NULL,
|
|
111
|
+
issue_number INTEGER NOT NULL,
|
|
112
|
+
claimed_at TEXT NOT NULL,
|
|
113
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'released', 'expired')),
|
|
114
|
+
note TEXT,
|
|
115
|
+
UNIQUE (repo_full_name, issue_number)
|
|
116
|
+
)
|
|
117
|
+
`);
|
|
118
|
+
// Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations.
|
|
119
|
+
applySchemaMigrations(db, [addApiBaseUrlScope]);
|
|
120
|
+
|
|
121
|
+
// Idempotent claim in ONE atomic statement: insert a new active claim, or — only if the existing row is NOT
|
|
122
|
+
// already active — re-activate it (a released/expired claim can be re-claimed). The `WHERE status <> 'active'`
|
|
123
|
+
// guard makes re-claiming an already-active issue a true no-op (no row churn), never a duplicate row.
|
|
124
|
+
const recordStatement = db.prepare(`
|
|
125
|
+
INSERT INTO miner_claims (api_base_url, repo_full_name, issue_number, claimed_at, status, note)
|
|
126
|
+
VALUES (?, ?, ?, ?, 'active', ?)
|
|
127
|
+
ON CONFLICT(api_base_url, repo_full_name, issue_number) DO UPDATE SET
|
|
128
|
+
claimed_at = excluded.claimed_at,
|
|
129
|
+
note = excluded.note,
|
|
130
|
+
status = 'active'
|
|
131
|
+
WHERE miner_claims.status <> 'active'
|
|
132
|
+
`);
|
|
133
|
+
const getStatement = db.prepare(
|
|
134
|
+
"SELECT * FROM miner_claims WHERE api_base_url = ? AND repo_full_name = ? AND issue_number = ?",
|
|
135
|
+
);
|
|
136
|
+
// RETURNING (matching portfolio-queue.js's own claim/release statements) makes the "nothing to release/expire"
|
|
137
|
+
// case observable directly from ONE atomic statement, rather than a separate post-UPDATE SELECT whose "row
|
|
138
|
+
// went missing" branch would be structurally unreachable (nothing else runs between the UPDATE and a SELECT
|
|
139
|
+
// on the same key within one synchronous call).
|
|
140
|
+
const releaseStatement = db.prepare(
|
|
141
|
+
"UPDATE miner_claims SET status = 'released' WHERE api_base_url = ? AND repo_full_name = ? AND issue_number = ? AND status = 'active' RETURNING *",
|
|
142
|
+
);
|
|
143
|
+
const expireStatement = db.prepare(
|
|
144
|
+
"UPDATE miner_claims SET status = 'expired' WHERE api_base_url = ? AND repo_full_name = ? AND issue_number = ? AND status = 'active' RETURNING *",
|
|
145
|
+
);
|
|
146
|
+
const listAllStatement = db.prepare("SELECT * FROM miner_claims ORDER BY id ASC");
|
|
147
|
+
const listRepoStatement = db.prepare(
|
|
148
|
+
"SELECT * FROM miner_claims WHERE repo_full_name = ? ORDER BY id ASC",
|
|
149
|
+
);
|
|
150
|
+
const listStatusStatement = db.prepare(
|
|
151
|
+
"SELECT * FROM miner_claims WHERE status = ? ORDER BY id ASC",
|
|
152
|
+
);
|
|
153
|
+
const listRepoStatusStatement = db.prepare(
|
|
154
|
+
"SELECT * FROM miner_claims WHERE repo_full_name = ? AND status = ? ORDER BY id ASC",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
function normalizeListRepoFilter(repoFullName) {
|
|
158
|
+
if (repoFullName === undefined || repoFullName === null) return undefined;
|
|
159
|
+
return normalizeRepoFullName(repoFullName);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeStatusFilter(status) {
|
|
163
|
+
if (status === undefined || status === null) return undefined;
|
|
164
|
+
if (!CLAIM_STATUSES.includes(status)) throw new Error("invalid_status");
|
|
165
|
+
return status;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ledger = {
|
|
169
|
+
dbPath: resolvedPath,
|
|
170
|
+
recordClaim(claim) {
|
|
171
|
+
const apiBaseUrl = normalizeApiBaseUrl(claim?.apiBaseUrl);
|
|
172
|
+
const repoFullName = normalizeRepoFullName(claim?.repoFullName);
|
|
173
|
+
const issueNumber = normalizeIssueNumber(claim?.issueNumber);
|
|
174
|
+
const note = normalizeNote(claim?.note);
|
|
175
|
+
const claimedAt = new Date().toISOString();
|
|
176
|
+
recordStatement.run(apiBaseUrl, repoFullName, issueNumber, claimedAt, note);
|
|
177
|
+
return rowToClaim(getStatement.get(apiBaseUrl, repoFullName, issueNumber));
|
|
178
|
+
},
|
|
179
|
+
releaseClaim(repoFullName, issueNumber, apiBaseUrl) {
|
|
180
|
+
const normalizedForge = normalizeApiBaseUrl(apiBaseUrl);
|
|
181
|
+
const normalizedRepo = normalizeRepoFullName(repoFullName);
|
|
182
|
+
const normalizedIssue = normalizeIssueNumber(issueNumber);
|
|
183
|
+
const row = releaseStatement.get(normalizedForge, normalizedRepo, normalizedIssue);
|
|
184
|
+
return row ? rowToClaim(row) : null;
|
|
185
|
+
},
|
|
186
|
+
expireClaim(repoFullName, issueNumber, apiBaseUrl) {
|
|
187
|
+
const normalizedForge = normalizeApiBaseUrl(apiBaseUrl);
|
|
188
|
+
const normalizedRepo = normalizeRepoFullName(repoFullName);
|
|
189
|
+
const normalizedIssue = normalizeIssueNumber(issueNumber);
|
|
190
|
+
const row = expireStatement.get(normalizedForge, normalizedRepo, normalizedIssue);
|
|
191
|
+
return row ? rowToClaim(row) : null;
|
|
192
|
+
},
|
|
193
|
+
listClaims(filter = {}) {
|
|
194
|
+
const repoFullName = normalizeListRepoFilter(filter.repoFullName);
|
|
195
|
+
const status = normalizeStatusFilter(filter.status);
|
|
196
|
+
|
|
197
|
+
let rows;
|
|
198
|
+
if (repoFullName !== undefined && status !== undefined) {
|
|
199
|
+
rows = listRepoStatusStatement.all(repoFullName, status);
|
|
200
|
+
} else if (repoFullName !== undefined) {
|
|
201
|
+
rows = listRepoStatement.all(repoFullName);
|
|
202
|
+
} else if (status !== undefined) {
|
|
203
|
+
rows = listStatusStatement.all(status);
|
|
204
|
+
} else {
|
|
205
|
+
rows = listAllStatement.all();
|
|
206
|
+
}
|
|
207
|
+
return rows.map(rowToClaim);
|
|
208
|
+
},
|
|
209
|
+
claimIssue(repoFullName, issueNumber, note, apiBaseUrl) {
|
|
210
|
+
return ledger.recordClaim({ repoFullName, issueNumber, note, apiBaseUrl });
|
|
211
|
+
},
|
|
212
|
+
listActiveClaims(repoFullName) {
|
|
213
|
+
const filter = { status: "active" };
|
|
214
|
+
if (repoFullName !== undefined) filter.repoFullName = repoFullName;
|
|
215
|
+
return ledger.listClaims(filter);
|
|
216
|
+
},
|
|
217
|
+
// Explicit, operator-invoked right-to-be-forgotten purge (#5564) — never runs automatically. Distinct from
|
|
218
|
+
// this store's normal claim/release/expire lifecycle: deletes every row for a repo outright.
|
|
219
|
+
purgeByRepo(repoFullName) {
|
|
220
|
+
return purgeStoreByRepo(db, CLAIM_LEDGER_PURGE_SPEC, normalizeRepoFullName(repoFullName));
|
|
221
|
+
},
|
|
222
|
+
close() {
|
|
223
|
+
db.close();
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
return ledger;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Strictly read-only ledger access for advisory-only callers (#5157) that must never write anything --
|
|
231
|
+
* not even the schema-creation DDL and schema-version stamp {@link openClaimLedger} always runs on open.
|
|
232
|
+
* Opens the DB file in SQLite's own `readonly` mode (driver-enforced: an attempted write throws, this isn't
|
|
233
|
+
* just a by-convention guarantee) and touches the filesystem in no other way -- no `mkdirSync`/`chmodSync`,
|
|
234
|
+
* no `CREATE TABLE IF NOT EXISTS`, no migrations. The caller MUST only call this against a path it has
|
|
235
|
+
* already confirmed exists (e.g. via `existsSync`); a read-only connection to a nonexistent file throws.
|
|
236
|
+
* Throws if the expected table is missing too (a file exists at this path but isn't a real claim ledger) --
|
|
237
|
+
* callers should treat that identically to any other open/query failure.
|
|
238
|
+
*/
|
|
239
|
+
export function openClaimLedgerReadOnly(dbPath) {
|
|
240
|
+
const resolvedPath = normalizeDbPath(dbPath);
|
|
241
|
+
// `readOnly` (camelCase) -- node:sqlite silently IGNORES `readonly` (lowercase) as an unrecognized option
|
|
242
|
+
// and opens read-write anyway, defeating the entire point of this function. Verified empirically: a write
|
|
243
|
+
// via a `{ readonly: true }` connection succeeds with no error.
|
|
244
|
+
const db = new DatabaseSync(resolvedPath, { readOnly: true });
|
|
245
|
+
let listActiveStatement;
|
|
246
|
+
try {
|
|
247
|
+
listActiveStatement = db.prepare(
|
|
248
|
+
"SELECT * FROM miner_claims WHERE repo_full_name = ? AND status = 'active' ORDER BY id ASC",
|
|
249
|
+
);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// The table doesn't exist (a file exists at this path but isn't a real claim ledger) -- close the
|
|
252
|
+
// connection we already opened before rethrowing, so this never leaks a file handle.
|
|
253
|
+
db.close();
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
dbPath: resolvedPath,
|
|
258
|
+
listActiveClaims(repoFullName) {
|
|
259
|
+
const normalizedRepo = normalizeRepoFullName(repoFullName);
|
|
260
|
+
return listActiveStatement.all(normalizedRepo).map(rowToClaim);
|
|
261
|
+
},
|
|
262
|
+
close() {
|
|
263
|
+
db.close();
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getDefaultClaimLedger() {
|
|
269
|
+
defaultClaimLedger ??= openClaimLedger();
|
|
270
|
+
return defaultClaimLedger;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function recordClaim(claim) {
|
|
274
|
+
return getDefaultClaimLedger().recordClaim(claim);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function releaseClaim(repoFullName, issueNumber, apiBaseUrl) {
|
|
278
|
+
return getDefaultClaimLedger().releaseClaim(repoFullName, issueNumber, apiBaseUrl);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function expireClaim(repoFullName, issueNumber, apiBaseUrl) {
|
|
282
|
+
return getDefaultClaimLedger().expireClaim(repoFullName, issueNumber, apiBaseUrl);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function listClaims(filter) {
|
|
286
|
+
return getDefaultClaimLedger().listClaims(filter);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Foundation-phase alias for `recordClaim({ repoFullName, issueNumber, note, apiBaseUrl })`. (#3351) */
|
|
290
|
+
export function claimIssue(repoFullName, issueNumber, note, apiBaseUrl) {
|
|
291
|
+
return getDefaultClaimLedger().claimIssue(repoFullName, issueNumber, note, apiBaseUrl);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** List only `active` claims, optionally scoped to one repo. (#3351) */
|
|
295
|
+
export function listActiveClaims(repoFullName) {
|
|
296
|
+
return getDefaultClaimLedger().listActiveClaims(repoFullName);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function closeDefaultClaimLedger() {
|
|
300
|
+
if (!defaultClaimLedger) return;
|
|
301
|
+
defaultClaimLedger.close();
|
|
302
|
+
defaultClaimLedger = null;
|
|
303
|
+
}
|
package/lib/cli-error.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Shared CLI failure output (#4836): when `--json` is set, emit a parseable `{ ok: false, error }` object on
|
|
2
|
+
* stdout (matching each command's success-path JSON stream); otherwise log plain text to stderr. */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {boolean} wantsJson
|
|
6
|
+
* @param {string} message
|
|
7
|
+
* @param {number} [exitCode]
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function reportCliFailure(wantsJson, message, exitCode = 2) {
|
|
11
|
+
if (wantsJson) {
|
|
12
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
13
|
+
} else {
|
|
14
|
+
console.error(message);
|
|
15
|
+
}
|
|
16
|
+
return exitCode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** True when argv includes `--json` (used on parse-error paths before a full parse result exists). */
|
|
20
|
+
export function argsWantJson(args) {
|
|
21
|
+
return args.includes("--json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Normalize a thrown value to a safe error string for CLI output. */
|
|
25
|
+
export function describeCliError(error) {
|
|
26
|
+
return error instanceof Error ? error.message : String(error);
|
|
27
|
+
}
|
package/lib/cli.d.ts
ADDED
package/lib/cli.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { argsWantJson, reportCliFailure } from "./cli-error.js";
|
|
2
|
+
|
|
3
|
+
export function printVersion(input) {
|
|
4
|
+
console.log(`${input.packageName}/${input.packageVersion} (node ${process.version})`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function printHelp(input) {
|
|
8
|
+
console.log(
|
|
9
|
+
[
|
|
10
|
+
input.packageName,
|
|
11
|
+
"",
|
|
12
|
+
"Foundation CLI for the local Gittensory miner runtime.",
|
|
13
|
+
"",
|
|
14
|
+
"Usage:",
|
|
15
|
+
" gittensory-miner --help",
|
|
16
|
+
" gittensory-miner --version",
|
|
17
|
+
" gittensory-miner help",
|
|
18
|
+
" gittensory-miner version",
|
|
19
|
+
" gittensory-miner init [--json] [--verify-token] Bootstrap laptop-mode local SQLite state",
|
|
20
|
+
" gittensory-miner status [--json] Show installed versions + local state paths",
|
|
21
|
+
" gittensory-miner doctor [--json] Check this laptop is set up correctly",
|
|
22
|
+
" gittensory-miner migrate [--json] Apply pending schema migrations to existing local stores",
|
|
23
|
+
" gittensory-miner metrics Print prediction-calibration counters in Prometheus text format",
|
|
24
|
+
" gittensory-miner manage status [--json] Show managed PR rows from local portfolio + ledger",
|
|
25
|
+
" gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--dry-run] [--json]",
|
|
26
|
+
" gittensory-miner discover <owner/repo> [<owner/repo>...] [--dry-run] [--json]",
|
|
27
|
+
" gittensory-miner discover --search <query> [--dry-run] [--json] Fan out, rank, and enqueue candidates",
|
|
28
|
+
" gittensory-miner attempt <owner/repo> <issue#> --miner-login <login> [--base <branch>] [--live] [--dry-run] [--json]",
|
|
29
|
+
" gittensory-miner loop <owner/repo> [<owner/repo>...] --miner-login <login> [--base <branch>] [--live] [--dry-run]",
|
|
30
|
+
" gittensory-miner loop --search <query> --miner-login <login> [--max-cycles <n>] [--cycle-delay-ms <ms>] [--dry-run] [--json]",
|
|
31
|
+
" Autonomous discover->claim->attempt->reenter loop",
|
|
32
|
+
" gittensory-miner queue list [--repo <owner/repo>] [--json] List portfolio backlog rows",
|
|
33
|
+
" gittensory-miner queue next [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]",
|
|
34
|
+
" Claim the highest-priority queued item, optionally WIP-cap-aware",
|
|
35
|
+
" gittensory-miner queue claim-batch [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]",
|
|
36
|
+
" gittensory-miner queue done <owner/repo> <identifier> [--dry-run] [--json]",
|
|
37
|
+
" gittensory-miner queue release <owner/repo> <identifier> [--dry-run] [--json] Return a claimed item to the queue",
|
|
38
|
+
" gittensory-miner queue requeue <owner/repo> <identifier> [--dry-run] [--json] Put a completed item back on the queue",
|
|
39
|
+
" gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--dry-run] [--json]",
|
|
40
|
+
" gittensory-miner claim release <owner/repo> <issue#> [--dry-run] [--json]",
|
|
41
|
+
" gittensory-miner claim list [--repo <owner/repo>] [--status active|released|expired] [--json]",
|
|
42
|
+
" gittensory-miner ledger list [--repo <owner/repo>] [--since <seq>] [--type <eventType>] [--json]",
|
|
43
|
+
" gittensory-miner ledger metrics Print event-ledger counters in Prometheus text format",
|
|
44
|
+
" gittensory-miner plan list [--status pending|running|completed|failed] [--json]",
|
|
45
|
+
" gittensory-miner plan show <planId> [--json]",
|
|
46
|
+
" gittensory-miner governor list [--repo <owner/repo>] [--type allowed|denied|throttled|kill_switch] [--json]",
|
|
47
|
+
" gittensory-miner governor pause [--reason <text>] [--dry-run] [--json] Stop the loop before its next cycle",
|
|
48
|
+
" gittensory-miner governor resume [--dry-run] [--json] Let a paused loop continue",
|
|
49
|
+
" gittensory-miner governor status [--json] Show whether the governor is paused",
|
|
50
|
+
" gittensory-miner calibration [--json] Report predicted-vs-realized gate accuracy",
|
|
51
|
+
" gittensory-miner feasibility <claimStatus> <duplicateClusterRisk> <issueStatus> [--not-found] [--json]",
|
|
52
|
+
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
|
|
53
|
+
" gittensory-miner state get <owner/repo> [--json]",
|
|
54
|
+
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--dry-run] [--json]",
|
|
55
|
+
" gittensory-miner orb export [--enable] [--dry-run] [--json] Build the opt-in anonymized telemetry batch",
|
|
56
|
+
" gittensory-miner purge --repo <owner/repo> [--dry-run] [--json]",
|
|
57
|
+
" Right-to-be-forgotten: delete a repo's rows from every local store",
|
|
58
|
+
"",
|
|
59
|
+
"Options:",
|
|
60
|
+
" --no-update-check Skip the npm registry version nudge (also GITTENSORY_MINER_NO_UPDATE_CHECK=1)",
|
|
61
|
+
" --quiet Log only warnings and errors (also GITTENSORY_MINER_LOG_LEVEL=error)",
|
|
62
|
+
" --verbose Log debug-level diagnostics (also GITTENSORY_MINER_LOG_LEVEL=debug)",
|
|
63
|
+
" --log-level <lvl> Set the log level explicitly: silent|error|warn|info|debug",
|
|
64
|
+
].join("\n"),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function runCli(cliArgs, input) {
|
|
69
|
+
const command = cliArgs[0] ?? "";
|
|
70
|
+
const message = `Unknown command: ${command}. Run ${input.packageName} --help.`;
|
|
71
|
+
return reportCliFailure(argsWantJson(cliArgs), message, 1);
|
|
72
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentSdkHooks, AgentSdkQueryFn, CliSubprocessSpawnFn, CodingAgentDriver } from "@loopover/engine";
|
|
2
|
+
|
|
3
|
+
export function createRealCliSubprocessSpawn(): CliSubprocessSpawnFn;
|
|
4
|
+
|
|
5
|
+
export type ConstructProductionCodingAgentDriverOptions = {
|
|
6
|
+
spawn?: CliSubprocessSpawnFn;
|
|
7
|
+
query?: AgentSdkQueryFn;
|
|
8
|
+
hooks?: AgentSdkHooks;
|
|
9
|
+
listChangedFiles?: (cwd: string) => Promise<string[]>;
|
|
10
|
+
houseRulesConfig?: unknown;
|
|
11
|
+
houseRulesOptions?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function constructProductionCodingAgentDriver(
|
|
15
|
+
env: Record<string, string | undefined>,
|
|
16
|
+
options?: ConstructProductionCodingAgentDriverOptions,
|
|
17
|
+
): CodingAgentDriver;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Production coding-agent driver construction (#5131, Wave 3.5 follow-up to #2337/#2343). Closes the gap
|
|
2
|
+
// coding-agent-house-rules.js's own header names explicitly: "nothing in this package constructs a
|
|
3
|
+
// coding-agent driver in production yet ... that is separate, larger follow-up work." This module IS that
|
|
4
|
+
// call site -- it provides a real `child_process`-backed spawn (mirroring src/selfhost/ai.ts's `defaultSpawn`,
|
|
5
|
+
// simplified to the engine's smaller `CliSubprocessSpawnFn` contract: no `firstOutputTimeoutMs`/`input`, since
|
|
6
|
+
// those are reviewer-CLI-specific concerns this driver doesn't share) and resolves + constructs a real
|
|
7
|
+
// `CodingAgentDriver` from `MINER_CODING_AGENT_PROVIDER`, with house-rule enforcement (#2343) wired in by
|
|
8
|
+
// default via `buildHouseRulesAgentSdkHooks` -- a caller never has to remember to attach it by hand.
|
|
9
|
+
|
|
10
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
11
|
+
import { createCodingAgentDriver, resolveFirstConfiguredCodingAgentDriverName } from "@loopover/engine";
|
|
12
|
+
import { buildHouseRulesAgentSdkHooks } from "./coding-agent-house-rules.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Real `child_process.spawn`-backed implementation of the engine's `CliSubprocessSpawnFn` contract. Captures
|
|
16
|
+
* stdout/stderr and RESOLVES (never rejects) on timeout or spawn error, so the caller always sees whatever
|
|
17
|
+
* output accumulated rather than an unhandled rejection -- mirrors `src/selfhost/ai.ts`'s `defaultSpawn`'s own
|
|
18
|
+
* resolve-not-reject rationale (a killed/errored subprocess's partial output may hold the real diagnosable
|
|
19
|
+
* error, e.g. an auth failure line on stderr).
|
|
20
|
+
*
|
|
21
|
+
* @returns {import("@loopover/engine").CliSubprocessSpawnFn}
|
|
22
|
+
*/
|
|
23
|
+
export function createRealCliSubprocessSpawn() {
|
|
24
|
+
return (cmd, args, opts) =>
|
|
25
|
+
new Promise((resolve) => {
|
|
26
|
+
const child = nodeSpawn(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
// Unlike src/selfhost/ai.ts's defaultSpawn (a fixed ~120s default, genuinely untestable without a real
|
|
30
|
+
// wait), `opts.timeoutMs` here is always CALLER-supplied per CliSubprocessSpawnFn's contract -- a test can
|
|
31
|
+
// pass a short value against a genuinely long-lived child, so this path is exercised directly rather than
|
|
32
|
+
// v8-ignored. No "already settled" guard is needed: Promise resolution is idempotent (a second `resolve()`
|
|
33
|
+
// is a no-op) and clearing an already-fired timer is a harmless no-op too, so `close`/`error` firing after
|
|
34
|
+
// the timeout already resolved is safe without extra bookkeeping.
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
child.kill("SIGKILL");
|
|
37
|
+
resolve({ stdout, code: null, stderr, timedOut: true });
|
|
38
|
+
}, opts.timeoutMs);
|
|
39
|
+
child.stdout?.on("data", (chunk) => {
|
|
40
|
+
stdout += chunk.toString("utf8");
|
|
41
|
+
});
|
|
42
|
+
child.stderr?.on("data", (chunk) => {
|
|
43
|
+
stderr += chunk.toString("utf8");
|
|
44
|
+
});
|
|
45
|
+
child.on("error", (err) => {
|
|
46
|
+
// A spawn-level error (e.g. ENOENT) fires before the child ever produces output, so `stderr` is always
|
|
47
|
+
// "" here in practice; Node guarantees this listener receives a real Error with `.message` (the
|
|
48
|
+
// documented contract for ChildProcess's own "error" event), so no optional chaining/fallback is needed.
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolve({ stdout, code: null, stderr: err.message });
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
resolve({ stdout, code, stderr });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve `MINER_CODING_AGENT_PROVIDER` from `env` and construct a REAL, production `CodingAgentDriver` —
|
|
61
|
+
* house-rule-enforced by default (#2343) via `buildHouseRulesAgentSdkHooks`, matching the same
|
|
62
|
+
* automatic-enforcement guarantee `runHouseRulesEnforcedCodingAgentAttempt` gives task-level callers, but at
|
|
63
|
+
* the raw driver-construction level `attempt-runner.js`'s `deps.driver` actually needs.
|
|
64
|
+
*
|
|
65
|
+
* The default only applies to `agent-sdk`, the one provider with a real hook-registration surface. CLI
|
|
66
|
+
* subprocess providers (`claude-cli`/`codex-cli`) have none, and the engine's `createCliProvider` fails closed
|
|
67
|
+
* if `hooks` is supplied at all (driver-factory.ts) -- filling the default for them here would make every CLI
|
|
68
|
+
* construction throw. An explicitly-supplied `options.hooks` always wins and is forwarded as-is, so a caller
|
|
69
|
+
* that deliberately asks a CLI provider to enforce hooks still gets that same fail-closed rejection.
|
|
70
|
+
*
|
|
71
|
+
* Fails closed (throws) when no provider is configured, or when a CLI provider is selected without a real
|
|
72
|
+
* spawn available — never silently falls back to a driver that can never run.
|
|
73
|
+
*
|
|
74
|
+
* @param {Record<string, string | undefined>} env
|
|
75
|
+
* @param {{
|
|
76
|
+
* spawn?: import("@loopover/engine").CliSubprocessSpawnFn,
|
|
77
|
+
* query?: import("@loopover/engine").AgentSdkQueryFn,
|
|
78
|
+
* hooks?: import("@loopover/engine").AgentSdkHooks,
|
|
79
|
+
* listChangedFiles?: (cwd: string) => Promise<string[]>,
|
|
80
|
+
* houseRulesConfig?: unknown,
|
|
81
|
+
* houseRulesOptions?: unknown,
|
|
82
|
+
* }} [options]
|
|
83
|
+
* @returns {import("@loopover/engine").CodingAgentDriver}
|
|
84
|
+
*/
|
|
85
|
+
export function constructProductionCodingAgentDriver(env, options = {}) {
|
|
86
|
+
const providerName = resolveFirstConfiguredCodingAgentDriverName(env);
|
|
87
|
+
if (!providerName) {
|
|
88
|
+
throw new Error("unconfigured_coding_agent_driver:no_provider_in_MINER_CODING_AGENT_PROVIDER");
|
|
89
|
+
}
|
|
90
|
+
const hooks =
|
|
91
|
+
options.hooks ??
|
|
92
|
+
(providerName.trim().toLowerCase() === "agent-sdk"
|
|
93
|
+
? buildHouseRulesAgentSdkHooks(options.houseRulesConfig, options.houseRulesOptions)
|
|
94
|
+
: undefined);
|
|
95
|
+
return createCodingAgentDriver({
|
|
96
|
+
providerName,
|
|
97
|
+
env,
|
|
98
|
+
spawn: options.spawn ?? createRealCliSubprocessSpawn(),
|
|
99
|
+
...(options.query !== undefined ? { query: options.query } : {}),
|
|
100
|
+
...(hooks !== undefined ? { hooks } : {}),
|
|
101
|
+
...(options.listChangedFiles !== undefined ? { listChangedFiles: options.listChangedFiles } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentSdkHooks, CodingAgentDriverResult, CodingAgentExecutionMode, LintGuardResult, RunCodingAgentAttemptOptions } from "@loopover/engine";
|
|
2
|
+
import type { DenyRule } from "./deny-hooks.js";
|
|
3
|
+
import type { appendGovernorEvent } from "./governor-ledger.js";
|
|
4
|
+
|
|
5
|
+
export type HouseRulesConfig = {
|
|
6
|
+
rules?: readonly DenyRule[];
|
|
7
|
+
repoFullName?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type HouseRulesOptions = {
|
|
11
|
+
append?: typeof appendGovernorEvent;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The concrete shape {@link buildHouseRulesAgentSdkHooks} returns -- a single PreToolUse matcher group
|
|
15
|
+
* holding the one house-rules callback. Structurally assignable to the engine's opaque `AgentSdkHooks`. */
|
|
16
|
+
export type HouseRulesAgentSdkHooks = AgentSdkHooks & {
|
|
17
|
+
PreToolUse: Array<{ hooks: Array<(input: unknown, toolUseId?: string, context?: unknown) => Promise<Record<string, unknown>>> }>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function buildHouseRulesAgentSdkHooks(config?: HouseRulesConfig, options?: HouseRulesOptions): HouseRulesAgentSdkHooks;
|
|
21
|
+
|
|
22
|
+
export function runHouseRulesEnforcedCodingAgentAttempt(
|
|
23
|
+
options: RunCodingAgentAttemptOptions & { houseRulesConfig?: HouseRulesConfig; houseRulesOptions?: HouseRulesOptions },
|
|
24
|
+
): Promise<{ mode: CodingAgentExecutionMode; result: CodingAgentDriverResult & { lintGuard?: LintGuardResult } }>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// House-rules-enforced coding-agent construction (#2343 follow-up). buildHouseRulesPreToolUseHook
|
|
2
|
+
// (pretooluse-hook.js) is the LIVE PreToolUse interception point, but the engine package's
|
|
3
|
+
// createCodingAgentDriver / runCodingAgentAttempt (driver-factory.ts) cannot import it directly -- the
|
|
4
|
+
// dependency only ever flows gittensory-miner -> @loopover/engine, never the reverse (the engine
|
|
5
|
+
// package is portable and cannot depend on the miner CLI package). This module is the missing miner-side
|
|
6
|
+
// glue: it wraps runCodingAgentAttempt so the `agent-sdk` provider gets house-rule enforcement by DEFAULT --
|
|
7
|
+
// a future real call site does not need to remember to attach it itself, closing the exact gap
|
|
8
|
+
// buildHouseRulesPreToolUseHook's own doc comment already anticipated ("the live interception wiring itself").
|
|
9
|
+
//
|
|
10
|
+
// This does not build a CLI entrypoint. UPDATE (#5135/#5396): a real production call site now exists --
|
|
11
|
+
// packages/gittensory-miner/lib/coding-agent-construction.js's `constructProductionCodingAgentDriver` calls
|
|
12
|
+
// `createCodingAgentDriver` directly -- but it does NOT go through `runCodingAgentAttempt` or this module's
|
|
13
|
+
// own `runHouseRulesEnforcedCodingAgentAttempt` wrapper below; the real invocation path is
|
|
14
|
+
// `constructProductionCodingAgentDriver` -> the resulting driver becomes `IterateLoopDeps.driver` -> consumed
|
|
15
|
+
// by `runIterateLoop` (packages/gittensory-engine/src/miner/iterate-loop.ts). House-rule enforcement for the
|
|
16
|
+
// live `agent-sdk` provider is instead attached directly there, via this module's own
|
|
17
|
+
// `buildHouseRulesAgentSdkHooks` export (called from `constructProductionCodingAgentDriver` -- so the
|
|
18
|
+
// DEFAULT-attachment guarantee below still holds, just through a different real call site than originally
|
|
19
|
+
// anticipated). `runHouseRulesEnforcedCodingAgentAttempt` itself remains real, tested, and callable, but has
|
|
20
|
+
// no production caller today -- it's a lower-level composable for a hypothetical caller that wants the
|
|
21
|
+
// non-iterate-loop path, not something this package currently needs.
|
|
22
|
+
|
|
23
|
+
import { runCodingAgentAttempt } from "@loopover/engine";
|
|
24
|
+
import { buildHouseRulesPreToolUseHook } from "./pretooluse-hook.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrap {@link buildHouseRulesPreToolUseHook}'s callback into the Claude Agent SDK's own `hooks.PreToolUse`
|
|
28
|
+
* registration shape (an array of matcher groups, each holding an array of hook callbacks) -- the exact
|
|
29
|
+
* contract `agent-sdk-driver.ts`'s own doc comment names as "#2343's stated attachment point", and the shape
|
|
30
|
+
* `packages/gittensory-engine/test/agent-sdk-driver.test.ts` asserts is forwarded to the SDK verbatim.
|
|
31
|
+
*
|
|
32
|
+
* @param {Parameters<typeof buildHouseRulesPreToolUseHook>[0]} [config]
|
|
33
|
+
* @param {Parameters<typeof buildHouseRulesPreToolUseHook>[1]} [options]
|
|
34
|
+
* @returns {{ PreToolUse: Array<{ hooks: Array<ReturnType<typeof buildHouseRulesPreToolUseHook>> }> }}
|
|
35
|
+
*/
|
|
36
|
+
export function buildHouseRulesAgentSdkHooks(config = {}, options = {}) {
|
|
37
|
+
return { PreToolUse: [{ hooks: [buildHouseRulesPreToolUseHook(config, options)] }] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drop-in replacement for the engine's `runCodingAgentAttempt` that defaults `hooks` to
|
|
42
|
+
* {@link buildHouseRulesAgentSdkHooks} for the `agent-sdk` provider, so house-rule enforcement (#2343) is ON
|
|
43
|
+
* by default rather than opt-in. An explicitly-supplied `hooks` option always wins (e.g. a test injecting its
|
|
44
|
+
* own hook double, or a caller composing additional hooks of its own) -- this only fills the gap when the
|
|
45
|
+
* caller omitted it entirely. CLI providers (`claude-cli`, `codex-cli`) have no hook-registration surface, and
|
|
46
|
+
* the engine fails closed if `hooks` is supplied to them at all -- so the default is scoped to `agent-sdk`
|
|
47
|
+
* only; a CLI attempt with no explicit `hooks` gets none (today's inert no-op), while one that explicitly
|
|
48
|
+
* supplies `hooks` still gets the engine's real fail-closed rejection instead of a silently unenforced run.
|
|
49
|
+
*
|
|
50
|
+
* @param {Parameters<typeof runCodingAgentAttempt>[0] & {
|
|
51
|
+
* houseRulesConfig?: Parameters<typeof buildHouseRulesPreToolUseHook>[0],
|
|
52
|
+
* houseRulesOptions?: Parameters<typeof buildHouseRulesPreToolUseHook>[1],
|
|
53
|
+
* }} options
|
|
54
|
+
* @returns {ReturnType<typeof runCodingAgentAttempt>}
|
|
55
|
+
*/
|
|
56
|
+
export function runHouseRulesEnforcedCodingAgentAttempt(options) {
|
|
57
|
+
const { houseRulesConfig, houseRulesOptions, ...attemptOptions } = options;
|
|
58
|
+
const hooks =
|
|
59
|
+
attemptOptions.hooks ??
|
|
60
|
+
(attemptOptions.providerName.trim().toLowerCase() === "agent-sdk"
|
|
61
|
+
? buildHouseRulesAgentSdkHooks(houseRulesConfig, houseRulesOptions)
|
|
62
|
+
: undefined);
|
|
63
|
+
return runCodingAgentAttempt({ ...attemptOptions, ...(hooks !== undefined ? { hooks } : {}) });
|
|
64
|
+
}
|