@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,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js";
|
|
8
|
+
import {
|
|
9
|
+
collectEventLedgerAuditFeed,
|
|
10
|
+
normalizeAuditFeedMcpFilter,
|
|
11
|
+
} from "../lib/event-ledger-cli.js";
|
|
12
|
+
import { initEventLedger } from "../lib/event-ledger.js";
|
|
13
|
+
import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js";
|
|
14
|
+
import { initPortfolioQueueStore } from "../lib/portfolio-queue.js";
|
|
15
|
+
import { initRunStateStore } from "../lib/run-state.js";
|
|
16
|
+
import { PLAN_STATUSES, openPlanStore } from "../lib/plan-store.js";
|
|
17
|
+
import { initGovernorLedger } from "../lib/governor-ledger.js";
|
|
18
|
+
import { collectStatus, runDoctorChecks } from "../lib/status.js";
|
|
19
|
+
|
|
20
|
+
// MCP stdio server for @loopover/miner (scaffold #5153). Mirrors the packages/gittensory-mcp
|
|
21
|
+
// harness (MCP SDK server + stdio transport). Tools:
|
|
22
|
+
// - gittensory_miner_ping (#5153): trivial static health check, reads no AMS state.
|
|
23
|
+
// - gittensory_miner_get_portfolio_dashboard (#5155): read-only per-repo backlog dashboard, wrapping the
|
|
24
|
+
// existing collectPortfolioDashboard aggregator (no new logic; same data as `queue dashboard --json`).
|
|
25
|
+
// - gittensory_miner_list_claims (#5156): read-only listing of the local claim ledger (optional repo/status
|
|
26
|
+
// filter passed through to listClaims); exposes no claim/release mutation.
|
|
27
|
+
// - gittensory_miner_get_audit_feed (#5158): read-only metadata-only event-ledger audit feed via
|
|
28
|
+
// collectEventLedgerAuditFeed() (same filters as `ledger list`; never returns payload_json).
|
|
29
|
+
// - gittensory_miner_get_run_state (#5160): read-only per-repo run-state via run-state.js's getRunState/
|
|
30
|
+
// listRunStates (read-only analog of ORB's gittensory_get_automation_state; no state-set mutation).
|
|
31
|
+
// - gittensory_miner_list_plans / gittensory_miner_get_plan (#5161): read-only access to the persisted
|
|
32
|
+
// plan store via plan-store.js's listPlans/loadPlan (distinct from ORB's stateless gittensory_plan_status).
|
|
33
|
+
// - gittensory_miner_get_governor_decisions (#5159): read-only governor decision-log projection via
|
|
34
|
+
// governor-ledger.js's readGovernorDecisions -- an explicit named-column read that excludes payload_json.
|
|
35
|
+
// - gittensory_miner_status (#5154): read-only status + doctor diagnostics via status.js's collectStatus/
|
|
36
|
+
// runDoctorChecks (names/booleans/paths only -- never any env-var value, token, key, or credential).
|
|
37
|
+
|
|
38
|
+
// Read the version from this package's own package.json (always shipped) rather than a hand-synced
|
|
39
|
+
// literal, so a release bump never has a second place to forget -- same approach as the mcp harness.
|
|
40
|
+
const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
41
|
+
|
|
42
|
+
/** Optional filters accepted by gittensory_miner_get_audit_feed (#5158). */
|
|
43
|
+
const auditFeedInputSchema = {
|
|
44
|
+
repoFullName: z.string().min(1).optional(),
|
|
45
|
+
since: z.number().int().nonnegative().optional(),
|
|
46
|
+
type: z.string().min(1).optional(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */
|
|
50
|
+
export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`,
|
|
54
|
+
* `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`,
|
|
55
|
+
* `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default
|
|
56
|
+
* to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its
|
|
57
|
+
* store only when invoked and closes any store it opened.
|
|
58
|
+
*/
|
|
59
|
+
export function createMinerMcpServer(options = {}) {
|
|
60
|
+
const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version });
|
|
61
|
+
server.registerTool(
|
|
62
|
+
"gittensory_miner_ping",
|
|
63
|
+
{
|
|
64
|
+
description:
|
|
65
|
+
"Health check for the gittensory-miner MCP server. Returns a static status object confirming the " +
|
|
66
|
+
"server is reachable. Reads no AMS state and takes no arguments.",
|
|
67
|
+
inputSchema: {},
|
|
68
|
+
},
|
|
69
|
+
async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] }),
|
|
70
|
+
);
|
|
71
|
+
server.registerTool(
|
|
72
|
+
"gittensory_miner_get_portfolio_dashboard",
|
|
73
|
+
{
|
|
74
|
+
description:
|
|
75
|
+
"Read-only per-repo portfolio-queue backlog dashboard: status counts (queued/in_progress/done), totals, " +
|
|
76
|
+
"and the oldest-queued age in ms. Wraps the existing collectPortfolioDashboard aggregator (no new logic) " +
|
|
77
|
+
"-- the same data `gittensory-miner queue dashboard --json` prints locally. Takes no arguments; mutates nothing.",
|
|
78
|
+
inputSchema: {},
|
|
79
|
+
},
|
|
80
|
+
async () => {
|
|
81
|
+
const ownsQueue = options.initPortfolioQueue === undefined;
|
|
82
|
+
const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
|
|
83
|
+
try {
|
|
84
|
+
const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: options.nowMs ?? Date.now() });
|
|
85
|
+
return { content: [{ type: "text", text: JSON.stringify(summary) }] };
|
|
86
|
+
} finally {
|
|
87
|
+
if (ownsQueue) portfolioQueue.close();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
server.registerTool(
|
|
92
|
+
"gittensory_miner_list_claims",
|
|
93
|
+
{
|
|
94
|
+
description:
|
|
95
|
+
"Read-only listing of the local claim ledger: which issues this miner has claimed (repo, issue number, " +
|
|
96
|
+
"status, claimed-at, note). Optional repoFullName/status filters pass through to the existing listClaims " +
|
|
97
|
+
"query. Exposes no claim/release mutation and no conflict-resolution logic.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
repoFullName: z.string().optional(),
|
|
100
|
+
status: z.enum(CLAIM_STATUSES).optional(),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
async ({ repoFullName, status }) => {
|
|
104
|
+
const ownsLedger = options.openClaimLedger === undefined;
|
|
105
|
+
const ledger = (options.openClaimLedger ?? openClaimLedger)();
|
|
106
|
+
try {
|
|
107
|
+
const filter = {};
|
|
108
|
+
if (repoFullName !== undefined) filter.repoFullName = repoFullName;
|
|
109
|
+
if (status !== undefined) filter.status = status;
|
|
110
|
+
return { content: [{ type: "text", text: JSON.stringify(ledger.listClaims(filter)) }] };
|
|
111
|
+
} finally {
|
|
112
|
+
if (ownsLedger) ledger.close();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
server.registerTool(
|
|
117
|
+
"gittensory_miner_get_audit_feed",
|
|
118
|
+
{
|
|
119
|
+
description:
|
|
120
|
+
"Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " +
|
|
121
|
+
"outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " +
|
|
122
|
+
"the same read filters as `gittensory-miner ledger list` (--repo, --since, --type). Never returns " +
|
|
123
|
+
"payload_json or other raw ledger columns; never writes to the ledger.",
|
|
124
|
+
inputSchema: auditFeedInputSchema,
|
|
125
|
+
},
|
|
126
|
+
async (input) => {
|
|
127
|
+
const ownsLedger = options.initEventLedger === undefined;
|
|
128
|
+
const eventLedger = (options.initEventLedger ?? initEventLedger)();
|
|
129
|
+
try {
|
|
130
|
+
const filter = normalizeAuditFeedMcpFilter(input ?? {});
|
|
131
|
+
const feed = collectEventLedgerAuditFeed(eventLedger, filter);
|
|
132
|
+
return { content: [{ type: "text", text: JSON.stringify(feed) }] };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: JSON.stringify({
|
|
139
|
+
error: error instanceof Error ? error.message : String(error),
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
} finally {
|
|
146
|
+
if (ownsLedger) eventLedger.close();
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
server.registerTool(
|
|
151
|
+
"gittensory_miner_get_run_state",
|
|
152
|
+
{
|
|
153
|
+
description:
|
|
154
|
+
"Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single " +
|
|
155
|
+
"repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The " +
|
|
156
|
+
"read-only analog of ORB's gittensory_get_automation_state; adds no state-set or mutation capability.",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
repoFullName: z.string().min(1).optional(),
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async ({ repoFullName }) => {
|
|
162
|
+
const ownsStore = options.initRunStateStore === undefined;
|
|
163
|
+
const store = (options.initRunStateStore ?? initRunStateStore)();
|
|
164
|
+
try {
|
|
165
|
+
const result =
|
|
166
|
+
repoFullName === undefined
|
|
167
|
+
? { states: store.listRunStates() }
|
|
168
|
+
: { repoFullName, state: store.getRunState(repoFullName) };
|
|
169
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
170
|
+
} finally {
|
|
171
|
+
if (ownsStore) store.close();
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
server.registerTool(
|
|
176
|
+
"gittensory_miner_list_plans",
|
|
177
|
+
{
|
|
178
|
+
description:
|
|
179
|
+
"Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally " +
|
|
180
|
+
"filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: " +
|
|
181
|
+
"this is the store-backed AMS plan store; it is distinct from ORB's stateless gittensory_plan_status " +
|
|
182
|
+
"tool, which reads the caller's in-memory plan object rather than any persisted store.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
status: z.enum(PLAN_STATUSES).optional(),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
async ({ status }) => {
|
|
188
|
+
const ownsStore = options.openPlanStore === undefined;
|
|
189
|
+
const store = (options.openPlanStore ?? openPlanStore)();
|
|
190
|
+
try {
|
|
191
|
+
const filter = {};
|
|
192
|
+
if (status !== undefined) filter.status = status;
|
|
193
|
+
return { content: [{ type: "text", text: JSON.stringify(store.listPlans(filter)) }] };
|
|
194
|
+
} finally {
|
|
195
|
+
if (ownsStore) store.close();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
server.registerTool(
|
|
200
|
+
"gittensory_miner_get_plan",
|
|
201
|
+
{
|
|
202
|
+
description:
|
|
203
|
+
"Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an " +
|
|
204
|
+
"explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- " +
|
|
205
|
+
"no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless " +
|
|
206
|
+
"gittensory_plan_status tool.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
planId: z.string().min(1),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
async ({ planId }) => {
|
|
212
|
+
const ownsStore = options.openPlanStore === undefined;
|
|
213
|
+
const store = (options.openPlanStore ?? openPlanStore)();
|
|
214
|
+
try {
|
|
215
|
+
const plan = store.loadPlan(planId);
|
|
216
|
+
const result = plan === null ? { planId, found: false } : { found: true, plan };
|
|
217
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
218
|
+
} finally {
|
|
219
|
+
if (ownsStore) store.close();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
server.registerTool(
|
|
224
|
+
"gittensory_miner_get_governor_decisions",
|
|
225
|
+
{
|
|
226
|
+
description:
|
|
227
|
+
"Read-only projection of the governor decision log: id, ts, eventType, repoFullName, actionClass, " +
|
|
228
|
+
"decision, reason per row. This projection INTENTIONALLY EXCLUDES the internal/sensitive payload column " +
|
|
229
|
+
"(reputation / self-plagiarism / budget state) by construction -- governor-ledger.js reads it with an " +
|
|
230
|
+
"explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger " +
|
|
231
|
+
"supports natively). Read-only; never writes to the ledger.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
repoFullName: z.string().min(1).optional(),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
async ({ repoFullName }) => {
|
|
237
|
+
const ownsLedger = options.initGovernorLedger === undefined;
|
|
238
|
+
const ledger = (options.initGovernorLedger ?? initGovernorLedger)();
|
|
239
|
+
try {
|
|
240
|
+
const filter = {};
|
|
241
|
+
if (repoFullName !== undefined) filter.repoFullName = repoFullName;
|
|
242
|
+
return { content: [{ type: "text", text: JSON.stringify(ledger.readGovernorDecisions(filter)) }] };
|
|
243
|
+
} finally {
|
|
244
|
+
if (ownsLedger) ledger.close();
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
server.registerTool(
|
|
249
|
+
"gittensory_miner_status",
|
|
250
|
+
{
|
|
251
|
+
description:
|
|
252
|
+
"Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " +
|
|
253
|
+
"(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " +
|
|
254
|
+
"name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " +
|
|
255
|
+
"`gittensory-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " +
|
|
256
|
+
"collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " +
|
|
257
|
+
"any env-var value, token, key, or credential. Read-only; no writes or state changes.",
|
|
258
|
+
inputSchema: {},
|
|
259
|
+
},
|
|
260
|
+
async () => {
|
|
261
|
+
const status = (options.collectStatus ?? collectStatus)();
|
|
262
|
+
const doctor = (options.runDoctorChecks ?? runDoctorChecks)();
|
|
263
|
+
return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] };
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
return server;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Start the stdio transport only when executed directly as the bin, not when imported by a test.
|
|
270
|
+
// realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches.
|
|
271
|
+
const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
|
|
272
|
+
if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) {
|
|
273
|
+
createMinerMcpServer()
|
|
274
|
+
.connect(new StdioServerTransport())
|
|
275
|
+
.catch((error) => {
|
|
276
|
+
console.error(error);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runAttempt } from "../lib/attempt-cli.js";
|
|
3
|
+
import { printHelp, printVersion, runCli } from "../lib/cli.js";
|
|
4
|
+
import { configureLogger, extractLogOptions } from "../lib/logger.js";
|
|
5
|
+
import { runDenyCheck } from "../lib/deny-check.js";
|
|
6
|
+
import { runDiscover } from "../lib/discover-cli.js";
|
|
7
|
+
import { runFeasibilityCli } from "../lib/feasibility-cli.js";
|
|
8
|
+
import { runGovernorCli } from "../lib/governor-ledger-cli.js";
|
|
9
|
+
import { runLedgerCli } from "../lib/event-ledger-cli.js";
|
|
10
|
+
import { runCalibrationCli } from "../lib/calibration-cli.js";
|
|
11
|
+
import { runLoop } from "../lib/loop-cli.js";
|
|
12
|
+
import { runManagePoll } from "../lib/manage-poll.js";
|
|
13
|
+
import { runManageStatus } from "../lib/manage-status.js";
|
|
14
|
+
import { runMetrics } from "../lib/metrics-cli.js";
|
|
15
|
+
import { runPlanCli } from "../lib/plan-store-cli.js";
|
|
16
|
+
import { runClaimCli } from "../lib/claim-ledger-cli.js";
|
|
17
|
+
import { runPurge } from "../lib/purge-cli.js";
|
|
18
|
+
import { runQueueCli } from "../lib/portfolio-queue-cli.js";
|
|
19
|
+
import { runOrbExportCli } from "../lib/orb-export.js";
|
|
20
|
+
import { installCliSignalHandlers } from "../lib/process-lifecycle.js";
|
|
21
|
+
import { runStateCli } from "../lib/run-state-cli.js";
|
|
22
|
+
import { runInit } from "../lib/laptop-init.js";
|
|
23
|
+
import { loadMinerFileSecrets } from "../lib/env-file-indirection.js";
|
|
24
|
+
import { runMigrate } from "../lib/migrate-cli.js";
|
|
25
|
+
import { runDoctor, runStatus } from "../lib/status.js";
|
|
26
|
+
import {
|
|
27
|
+
awaitOpportunisticUpdateCheck,
|
|
28
|
+
resolveUpgradeCommand,
|
|
29
|
+
startUpdateCheck,
|
|
30
|
+
} from "../lib/update-check.js";
|
|
31
|
+
import { resolveMinerVersion } from "../lib/version.js";
|
|
32
|
+
|
|
33
|
+
// Resolve any `<NAME>_FILE` secret-mount vars (GITHUB_TOKEN_FILE, etc.) into their plain counterparts FIRST,
|
|
34
|
+
// before anything else reads process.env -- every subcommand below (and the coding-agent driver construction
|
|
35
|
+
// deeper in the call graph) reads plain env vars, so this single early pass is all that's needed for the whole
|
|
36
|
+
// CLI (#5178). A broken secret mount fails the process fast and loud with a clear message, instead of an
|
|
37
|
+
// uncaught-exception stack trace or a silent empty credential surfacing as a confusing GitHub 401 later.
|
|
38
|
+
try {
|
|
39
|
+
loadMinerFileSecrets();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Register signal + crash handlers once, before any command runs, so an interrupted run closes its open ledgers
|
|
46
|
+
// cleanly instead of dying mid-write (#4826). Covers every subcommand below, including the local ones.
|
|
47
|
+
installCliSignalHandlers();
|
|
48
|
+
|
|
49
|
+
// Peel the global logging flags (--quiet/--verbose/--log-level) off the front of argv and configure the
|
|
50
|
+
// process-wide logger once (#4835), so every command below shares one level-aware logger without re-parsing
|
|
51
|
+
// them; the stripped `cliArgs` is what the command dispatch sees.
|
|
52
|
+
const { options: logOptions, rest: cliArgs } = extractLogOptions(process.argv.slice(2));
|
|
53
|
+
configureLogger({ ...logOptions, env: process.env });
|
|
54
|
+
|
|
55
|
+
// `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls.
|
|
56
|
+
// `init` stays local by default and only makes a network call when the operator explicitly passes
|
|
57
|
+
// `--verify-token`.
|
|
58
|
+
// Dispatch the local commands BEFORE the opportunistic npm-registry update check is even started, so they can
|
|
59
|
+
// never reach that network path (the update check runs for the remaining commands below).
|
|
60
|
+
if (cliArgs[0] === "init") {
|
|
61
|
+
process.exit(await runInit(cliArgs.slice(1)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (cliArgs[0] === "status") {
|
|
65
|
+
process.exit(runStatus(cliArgs.slice(1)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (cliArgs[0] === "doctor") {
|
|
69
|
+
process.exit(runDoctor(cliArgs.slice(1)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// `migrate` is strictly local + offline like `status`/`doctor` (it only opens the local SQLite stores), so it is
|
|
73
|
+
// dispatched here too, before the opportunistic npm-registry update check is ever started.
|
|
74
|
+
if (cliArgs[0] === "migrate") {
|
|
75
|
+
process.exit(runMigrate(cliArgs.slice(1)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// `metrics` is strictly local + offline like `status`/`doctor` (it reads only the local prediction ledger), so it
|
|
79
|
+
// is dispatched here, before the opportunistic npm-registry update check is ever started.
|
|
80
|
+
if (cliArgs[0] === "metrics") {
|
|
81
|
+
process.exit(runMetrics(cliArgs.slice(1)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (cliArgs[0] === "manage" && cliArgs[1] === "status") {
|
|
85
|
+
process.exit(runManageStatus(cliArgs.slice(2)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cliArgs[0] === "queue") {
|
|
89
|
+
process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (cliArgs[0] === "orb" && cliArgs[1] === "export") {
|
|
93
|
+
process.exit(runOrbExportCli(cliArgs.slice(2)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (cliArgs[0] === "claim") {
|
|
97
|
+
process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cliArgs[0] === "ledger") {
|
|
101
|
+
process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (cliArgs[0] === "calibration") {
|
|
105
|
+
process.exit(runCalibrationCli(cliArgs.slice(1)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (cliArgs[0] === "plan") {
|
|
109
|
+
process.exit(runPlanCli(cliArgs[1], cliArgs.slice(2)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (cliArgs[0] === "governor") {
|
|
113
|
+
process.exit(await runGovernorCli(cliArgs[1], cliArgs.slice(2)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (cliArgs[0] === "feasibility") {
|
|
117
|
+
process.exit(runFeasibilityCli(cliArgs.slice(1)));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `purge` (#5564) is strictly local + offline like `queue`/`claim`/`governor` above -- it only opens the local
|
|
121
|
+
// SQLite stores, so it is dispatched here too, before the opportunistic npm-registry update check ever starts.
|
|
122
|
+
if (cliArgs[0] === "purge") {
|
|
123
|
+
process.exit(runPurge(cliArgs.slice(1)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const packageName = "@loopover/miner";
|
|
127
|
+
const packageVersion = resolveMinerVersion(process.env);
|
|
128
|
+
const upgradeCommand = resolveUpgradeCommand(packageName);
|
|
129
|
+
|
|
130
|
+
const updateCheck = startUpdateCheck(cliArgs, {
|
|
131
|
+
packageName,
|
|
132
|
+
packageVersion,
|
|
133
|
+
upgradeCommand,
|
|
134
|
+
env: process.env,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
cliArgs.length === 0 ||
|
|
139
|
+
cliArgs.includes("--help") ||
|
|
140
|
+
cliArgs.includes("-h") ||
|
|
141
|
+
cliArgs[0] === "help"
|
|
142
|
+
) {
|
|
143
|
+
printHelp({ packageName });
|
|
144
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
cliArgs.includes("--version") ||
|
|
150
|
+
cliArgs.includes("-v") ||
|
|
151
|
+
cliArgs[0] === "version"
|
|
152
|
+
) {
|
|
153
|
+
printVersion({ packageName, packageVersion });
|
|
154
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cliArgs[0] === "hooks" && cliArgs[1] === "check") {
|
|
159
|
+
const exitCode = runDenyCheck(cliArgs.slice(2));
|
|
160
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
161
|
+
process.exit(exitCode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (cliArgs[0] === "state") {
|
|
165
|
+
const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2));
|
|
166
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
167
|
+
process.exit(exitCode);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (cliArgs[0] === "manage" && cliArgs[1] === "poll") {
|
|
171
|
+
const exitCode = await runManagePoll(cliArgs.slice(2));
|
|
172
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
173
|
+
process.exit(exitCode);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (cliArgs[0] === "discover") {
|
|
177
|
+
const exitCode = await runDiscover(cliArgs.slice(1));
|
|
178
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
179
|
+
process.exit(exitCode);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (cliArgs[0] === "attempt") {
|
|
183
|
+
const exitCode = await runAttempt(cliArgs.slice(1));
|
|
184
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
185
|
+
process.exit(exitCode);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (cliArgs[0] === "loop") {
|
|
189
|
+
const exitCode = await runLoop(cliArgs.slice(1));
|
|
190
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
191
|
+
process.exit(exitCode);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const exitCode = runCli(cliArgs, { packageName });
|
|
195
|
+
await awaitOpportunisticUpdateCheck(updateCheck);
|
|
196
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# CodingAgentDriver — the miner's coding-agent seam
|
|
2
|
+
|
|
3
|
+
`CodingAgentDriver` is the single interface a Gittensory miner runs a coding agent through. It lets the miner drive
|
|
4
|
+
**either** a local CLI subprocess (e.g. `claude` / `codex`) **or** an in-process Agent SDK `query()` loop behind one
|
|
5
|
+
provider-agnostic contract, so the rest of the miner — planning, attempt logging, metering, gate polling — never
|
|
6
|
+
has to know which backend actually did the work.
|
|
7
|
+
|
|
8
|
+
The interface itself lives in `@loopover/engine`
|
|
9
|
+
([`packages/gittensory-engine/src/miner/coding-agent-driver.ts`](../../gittensory-engine/src/miner/coding-agent-driver.ts));
|
|
10
|
+
the orchestration around it (mode gating, invocation, the factory) lives in the sibling modules described below.
|
|
11
|
+
|
|
12
|
+
> **See also:** [Observing your miner](observability.md) — point Grafana at the miner's local SQLite ledgers
|
|
13
|
+
> (attempt log, prediction ledger) to see what the driver actually did.
|
|
14
|
+
|
|
15
|
+
## Why a seam, and why this shape
|
|
16
|
+
|
|
17
|
+
The design deliberately mirrors the review stack's `SelfHostAi` (`src/selfhost/ai.ts`) rather than inventing a new
|
|
18
|
+
pattern: a single `run()` method, provider-agnostic task/result types, and **injected dependencies** (spawn fn,
|
|
19
|
+
clock, filesystem) on the concrete implementations rather than hardcoded globals. That injection is what keeps every
|
|
20
|
+
driver unit-testable without real IO — the same reason `SelfHostAi` takes an injected `SpawnFn`.
|
|
21
|
+
|
|
22
|
+
The interface defines only the contract. Implementations MAY perform real IO; they never make GitHub writes or
|
|
23
|
+
autonomous continue/stop decisions — the task handed to a driver is already scoped.
|
|
24
|
+
|
|
25
|
+
## The contract
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type CodingAgentDriverTask = {
|
|
29
|
+
attemptId: string; // stable id for this attempt (keys the attempt log)
|
|
30
|
+
workingDirectory: string; // the ONLY directory a driver may edit (see worktree isolation below)
|
|
31
|
+
acceptanceCriteriaPath: string; // path to the immutable acceptance-criteria file written before the run
|
|
32
|
+
instructions: string; // the metadata-only prompt (no source contents)
|
|
33
|
+
maxTurns: number; // hard cap on agent iterations for this attempt
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type CodingAgentDriverResult = {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
changedFiles: readonly string[];
|
|
39
|
+
summary: string;
|
|
40
|
+
transcript?: string; // opaque provider transcript for operator inspection
|
|
41
|
+
turnsUsed?: number;
|
|
42
|
+
error?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
interface CodingAgentDriver {
|
|
46
|
+
run(task: CodingAgentDriverTask): Promise<CodingAgentDriverResult>;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Two reference implementations ship today for tests: `createFakeCodingAgentDriver` (records the last task, no IO) and
|
|
51
|
+
`createNoopCodingAgentDriver` (default-OFF stub). The two real backends — a CLI-subprocess driver (#4266) and an
|
|
52
|
+
Agent-SDK driver (#4267) — register in `createCodingAgentDriver` as `claude-cli`, `codex-cli`, and `agent-sdk`
|
|
53
|
+
(`CODING_AGENT_DRIVER_NAMES`: `["noop", "claude-cli", "codex-cli", "agent-sdk"]`).
|
|
54
|
+
|
|
55
|
+
## The surrounding primitives
|
|
56
|
+
|
|
57
|
+
A driver never runs in isolation. The neighborhood it plugs into:
|
|
58
|
+
|
|
59
|
+
| Concern | Module | What it provides |
|
|
60
|
+
| ------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| Execution mode | `coding-agent-mode.ts` | `paused` / `dry_run` / `live` with deny-toward-safety precedence; `codingAgentModeExecutes(mode)` is the single "should this attempt actually spawn?" boolean. A `dry_run` is a pure no-op at the driver boundary. |
|
|
62
|
+
| Invocation | `coding-agent-invoke.ts` | `invokeCodingAgentDriver(driver, task, mode?, log?)` — gates on the mode, calls `driver.run`, and streams lifecycle events to an `AttemptLogSink`. |
|
|
63
|
+
| Factory | `driver-factory.ts` | `createCodingAgentDriver(options)` resolves a driver by configured name; `resolveConfiguredCodingAgentDriverNames` / `isConfiguredCodingAgentDriver` deny unknown names by default; `runCodingAgentAttempt(options)` is the top-level "resolve + invoke" convenience. |
|
|
64
|
+
| Attempt log | `attempt-log.ts` (#4294) | `ATTEMPT_LOG_EVENT_TYPES` + `normalizeAttemptLogEvent` + `createAttemptLogBuffer` + `formatAttemptLogJsonl` — an append-only, JSONL-exportable event trace per attempt, independent of any driver's own transcript. Durable persistence: `packages/gittensory-miner/lib/attempt-log.js` (sibling SQLite store; imports the engine normalizer). |
|
|
65
|
+
| Metering | `attempt-metering.ts` (#4311) | `accumulateAttemptUsage` / `meterAttemptUsage` / `evaluateAttemptBudget` over `AttemptBudgetAxis` (`tokens` / `turns` / `wallClockMs` / `costUsd`). |
|
|
66
|
+
| Acceptance criteria | (#4271) | The immutable criteria file at `task.acceptanceCriteriaPath`, written before the driver starts. |
|
|
67
|
+
| Worktree isolation | (#4269) | Each attempt's `task.workingDirectory` is a dedicated git worktree; a driver must never edit outside it. |
|
|
68
|
+
|
|
69
|
+
## Authoring a third driver
|
|
70
|
+
|
|
71
|
+
To add a driver beyond the CLI-subprocess and Agent-SDK backends:
|
|
72
|
+
|
|
73
|
+
1. **Implement the interface.** Export a `create<Name>CodingAgentDriver(deps)` factory returning a `CodingAgentDriver`.
|
|
74
|
+
Take every side-effecting dependency (spawn fn, `query()` client, clock, fs) as an **injected argument** — do not
|
|
75
|
+
reach for globals — so the contract suite can drive it with fakes (mirrors `SpawnFn` injection in `SelfHostAi`).
|
|
76
|
+
2. **Honor the task scoping.** Only edit inside `task.workingDirectory`; stop at `task.maxTurns`; read the acceptance
|
|
77
|
+
criteria from `task.acceptanceCriteriaPath`; never make a GitHub write.
|
|
78
|
+
3. **Return the result shape faithfully.** Set `ok` from whether acceptance was met, list `changedFiles`, and surface
|
|
79
|
+
failures via `error` (a clean failure) rather than throwing — the invoker records either outcome.
|
|
80
|
+
4. **Register it in the factory.** Add the name to `CODING_AGENT_DRIVER_NAMES` and wire `createCodingAgentDriver`, so
|
|
81
|
+
`resolveConfiguredCodingAgentDriverNames` can select it from config (unknown names stay denied by default).
|
|
82
|
+
5. **Get covered by the parity/contract suite** (#4296): the suite runs the same scenario fixtures — a clean success,
|
|
83
|
+
a clean failure, a budget/timeout, and a malformed acceptance-criteria input — against every driver with an
|
|
84
|
+
injected backend, asserting identical SHAPE and edge-case handling (not identical output, which is inherently
|
|
85
|
+
non-deterministic across backends).
|
|
86
|
+
|
|
87
|
+
## A worked attempt lifecycle (the real production path)
|
|
88
|
+
|
|
89
|
+
`runCodingAgentAttempt`/`invokeCodingAgentDriver` (driver-factory.ts/coding-agent-invoke.ts) are real, tested,
|
|
90
|
+
and remain available as a lower-level composable, but **production does not call either of them today**. The
|
|
91
|
+
real construction + invocation path, as actually wired into `packages/gittensory-miner`, is:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
constructProductionCodingAgentDriver(env) (packages/gittensory-miner/lib/coding-agent-construction.js)
|
|
95
|
+
└─ createCodingAgentDriver({ providerName, spawn, ... }) (driver-factory.ts) → the configured CodingAgentDriver
|
|
96
|
+
│ (house-rule hooks attached by default for agent-sdk via buildHouseRulesAgentSdkHooks -- see below)
|
|
97
|
+
└─ becomes IterateLoopDeps.driver, handed to runIterateLoop (iterate-loop.ts)
|
|
98
|
+
|
|
99
|
+
runIterateLoop(input, deps) (packages/gittensory-engine/src/miner/iterate-loop.ts)
|
|
100
|
+
└─ per iteration, runDriverSafely(input, deps, task):
|
|
101
|
+
├─ if !codingAgentModeExecutes(input.mode): → invokeCodingAgentDriver(deps.driver, mode, task, {...})
|
|
102
|
+
│ (paused/dry_run only -- records a shadow/no-op attempt-log event, never spawns the real driver)
|
|
103
|
+
└─ else (live): → deps.driver.run(task) directly, NOT via invokeCodingAgentDriver
|
|
104
|
+
→ edits inside task.workingDirectory only, ≤ task.maxTurns
|
|
105
|
+
├─ evaluateSelfReviewOutcome(...) → self-review the resulting diff
|
|
106
|
+
├─ decideNextActionWithReason(state) → continue | handoff | abandon
|
|
107
|
+
└─ logDecision(...) records ONE attempt-log event per iteration (continue/handoff/abandon), not a
|
|
108
|
+
raw driver-start/succeed/fail pair the way invokeCodingAgentDriver's own wrapper would
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The attempt log (JSONL) is the durable, provider-independent record of what happened, independent of whichever
|
|
112
|
+
backend's own transcript, and the input to the miner's manage-phase and self-improve loops.
|
|
113
|
+
|
|
114
|
+
`packages/gittensory-miner/lib/coding-agent-house-rules.js`'s `runHouseRulesEnforcedCodingAgentAttempt` (a
|
|
115
|
+
drop-in wrapper over `runCodingAgentAttempt`) exists for a caller that wants the alternate, non-iterate-loop
|
|
116
|
+
path with house-rule enforcement built in by default -- no such caller exists in production today either; the
|
|
117
|
+
real house-rule enforcement for the live `agent-sdk` provider happens via `buildHouseRulesAgentSdkHooks`,
|
|
118
|
+
attached directly in `constructProductionCodingAgentDriver`.
|
|
119
|
+
|
|
120
|
+
**Metering:** `attempt-metering.ts`'s `accumulateAttemptUsage`/`evaluateAttemptBudget` are wired into
|
|
121
|
+
`iterate-loop.ts` for real ([#5395](https://github.com/JSONbored/gittensory/issues/5395)) -- every iteration
|
|
122
|
+
accumulates real `turns`/`costUsd`/`wallClockMs` (`tokens` stays an honest 0; no driver reports a real
|
|
123
|
+
per-iteration token count today) into a running `AttemptMeterTotals`, and `runIterateLoop`'s optional
|
|
124
|
+
`input.budget: AttemptBudget` is evaluated against it each iteration via the SAME `costCeilingReached` signal
|
|
125
|
+
`iterate-policy.ts` already exposed for the (now-removed) turns-only `maxTotalTurns` ceiling -- a genuine
|
|
126
|
+
mid-attempt abort, not just a between-cycle cap. `packages/gittensory-miner/lib/attempt-input-builder.js`'s
|
|
127
|
+
`buildAttemptLoopInput` sets `budget` from the SAME `AmsPolicySpec.capLimits` the Governor's cross-cycle
|
|
128
|
+
`GovernorCapUsage` already uses (`packages/gittensory-miner/lib/loop-cli.js`'s `governorState.saveCapUsage`
|
|
129
|
+
between loop cycles) -- one attempt can no longer burn through the entire cross-cycle budget before anything
|
|
130
|
+
reacts. `wallClockMs` uses a real injected clock (`IterateLoopDeps.nowMs`, defaulting to `Date.now`) measured
|
|
131
|
+
around each iteration's driver invocation. The result's `finalMeterTotals`/`budgetBreaches` fields surface
|
|
132
|
+
which axis (if any) triggered an abandon, for an operator reading the attempt log back.
|
|
133
|
+
|
|
134
|
+
## Related docs
|
|
135
|
+
|
|
136
|
+
- [`operations-runbook.md`](operations-runbook.md) — SQLite `busy_timeout` concurrency, corruption recovery, multi-process collisions, post-upgrade migration ([#4875](https://github.com/JSONbored/gittensory/issues/4875)).
|
|
137
|
+
- [`env-reference.md`](env-reference.md) — env vars including ledger path overrides.
|
|
138
|
+
- [`../DEPLOYMENT.md`](../DEPLOYMENT.md) — laptop vs fleet deployment and state directory layout.
|
|
139
|
+
- [`miner-goal-spec.md`](miner-goal-spec.md) — per-repo `.gittensory-miner.yml` targeting policy.
|
|
140
|
+
- [`../README.md#mcp-server`](../README.md#mcp-server) — the `gittensory-miner-mcp` read-only tool surface for querying this driver's resolved status (provider, model env-var name, CLI presence) and the rest of AMS's local state over MCP.
|