@martinloop/mcp 0.3.3 → 0.3.4
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/README.md +112 -107
- package/dist/discovery-support.js +2 -2
- package/dist/resources.js +147 -147
- package/dist/server-validation.js +2 -0
- package/dist/server.js +1 -1
- package/dist/tools/eval.js +29 -5
- package/dist/tools/execution-policy.d.ts +11 -0
- package/dist/tools/execution-policy.js +47 -0
- package/dist/tools/run-dossier.js +2 -2
- package/dist/tools/run-loop.d.ts +1 -0
- package/dist/tools/run-loop.js +15 -2
- package/dist/tools/run-store.js +4 -4
- package/dist/tools/tool-support.d.ts +1 -1
- package/dist/tools/tool-support.js +111 -18
- package/dist/tools/triage-runs.js +31 -6
- package/dist/vendor/adapters/claude-cli.js +69 -19
- package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
- package/dist/vendor/adapters/cli-bridge.js +62 -2
- package/dist/vendor/adapters/codex-launcher.js +65 -8
- package/dist/vendor/adapters/openai-compatible.js +6 -6
- package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
- package/dist/vendor/contracts/adapter-capabilities.js +35 -0
- package/dist/vendor/contracts/context-graph.d.ts +48 -0
- package/dist/vendor/contracts/context-graph.js +26 -0
- package/dist/vendor/contracts/execution-policy.d.ts +62 -0
- package/dist/vendor/contracts/execution-policy.js +34 -0
- package/dist/vendor/contracts/governance.d.ts +6 -3
- package/dist/vendor/contracts/identity.d.ts +29 -0
- package/dist/vendor/contracts/identity.js +30 -0
- package/dist/vendor/contracts/index.d.ts +13 -2
- package/dist/vendor/contracts/index.js +4 -0
- package/dist/vendor/contracts/observation.d.ts +21 -0
- package/dist/vendor/contracts/observation.js +27 -0
- package/dist/vendor/contracts/trajectory.d.ts +26 -0
- package/dist/vendor/contracts/trajectory.js +27 -0
- package/dist/vendor/contracts/verification.d.ts +28 -0
- package/dist/vendor/contracts/verification.js +7 -0
- package/dist/vendor/core/context-graph.d.ts +3 -0
- package/dist/vendor/core/context-graph.js +371 -0
- package/dist/vendor/core/identity.d.ts +20 -0
- package/dist/vendor/core/identity.js +86 -0
- package/dist/vendor/core/index.d.ts +9 -2
- package/dist/vendor/core/index.js +67 -11
- package/dist/vendor/core/persistence/integrity.js +2 -2
- package/dist/vendor/core/persistence/store.js +0 -13
- package/dist/vendor/core/policy-compiler.d.ts +2 -0
- package/dist/vendor/core/policy-compiler.js +195 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +71 -4
- package/dist/vendor/core/rollback.d.ts +0 -4
- package/dist/vendor/core/rollback.js +32 -27
- package/dist/vendor/core/trajectory.d.ts +18 -0
- package/dist/vendor/core/trajectory.js +295 -0
- package/package.json +76 -75
- package/server.json +21 -21
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { appendLoopEvent, createLoopRecord } from "../contracts/index.js";
|
|
2
3
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
|
|
3
4
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
4
5
|
import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
|
|
5
|
-
import { captureRollbackBoundary,
|
|
6
|
+
import { captureRollbackBoundary, restoreRollbackBoundary } from "./rollback.js";
|
|
6
7
|
import { compilePromptPacket } from "./compiler.js";
|
|
7
8
|
import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
|
|
8
9
|
import { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
9
|
-
|
|
10
|
+
import { decideCircuitBreak } from "./trajectory.js";
|
|
11
|
+
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, restoreRollbackBoundary };
|
|
12
|
+
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
13
|
+
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
14
|
+
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
15
|
+
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
10
16
|
// ─── Context Integrity Pre-gate ──────────────────────────────────────────────
|
|
11
17
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
12
18
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
@@ -21,12 +27,35 @@ export { compileAndPersistContext } from "./persistence/index.js";
|
|
|
21
27
|
*/
|
|
22
28
|
export function evaluateAttemptPolicy(input) {
|
|
23
29
|
const { request, projectedUsd } = input;
|
|
30
|
+
const trajectoryAttempts = request.previousAttempts.map((attempt) => ({
|
|
31
|
+
index: attempt.index,
|
|
32
|
+
summary: attempt.summary,
|
|
33
|
+
failureClass: attempt.failureClass
|
|
34
|
+
}));
|
|
35
|
+
const circuitBreakDecision = decideCircuitBreak({
|
|
36
|
+
objective: request.context.objective,
|
|
37
|
+
verificationPlan: request.context.verificationPlan,
|
|
38
|
+
attempts: trajectoryAttempts,
|
|
39
|
+
remainingIterations: request.context.remainingIterations
|
|
40
|
+
});
|
|
41
|
+
const trajectoryAssessment = circuitBreakDecision.assessment;
|
|
42
|
+
if (circuitBreakDecision.shouldStop) {
|
|
43
|
+
return {
|
|
44
|
+
allowed: false,
|
|
45
|
+
reason: circuitBreakDecision.reason,
|
|
46
|
+
recommendedIntervention: circuitBreakDecision.recommendedIntervention,
|
|
47
|
+
trajectoryAssessment,
|
|
48
|
+
circuitBreakDecision
|
|
49
|
+
};
|
|
50
|
+
}
|
|
24
51
|
// Budget gate: reject if projected cost exceeds remaining
|
|
25
52
|
if (projectedUsd > request.context.remainingBudgetUsd) {
|
|
26
53
|
return {
|
|
27
54
|
allowed: false,
|
|
28
55
|
reason: `Projected cost $${projectedUsd} exceeds remaining budget $${request.context.remainingBudgetUsd}.`,
|
|
29
|
-
recommendedIntervention: "stop_loop"
|
|
56
|
+
recommendedIntervention: "stop_loop",
|
|
57
|
+
trajectoryAssessment,
|
|
58
|
+
circuitBreakDecision
|
|
30
59
|
};
|
|
31
60
|
}
|
|
32
61
|
// Iteration gate
|
|
@@ -34,7 +63,9 @@ export function evaluateAttemptPolicy(input) {
|
|
|
34
63
|
return {
|
|
35
64
|
allowed: false,
|
|
36
65
|
reason: "No remaining iterations in the budget.",
|
|
37
|
-
recommendedIntervention: "stop_loop"
|
|
66
|
+
recommendedIntervention: "stop_loop",
|
|
67
|
+
trajectoryAssessment,
|
|
68
|
+
circuitBreakDecision
|
|
38
69
|
};
|
|
39
70
|
}
|
|
40
71
|
// Oscillation detection: A/B/A pattern in failure classes
|
|
@@ -48,7 +79,9 @@ export function evaluateAttemptPolicy(input) {
|
|
|
48
79
|
return {
|
|
49
80
|
allowed: false,
|
|
50
81
|
reason: "Oscillating failure pattern detected. Escalating to human.",
|
|
51
|
-
recommendedIntervention: "escalate_human"
|
|
82
|
+
recommendedIntervention: "escalate_human",
|
|
83
|
+
trajectoryAssessment,
|
|
84
|
+
circuitBreakDecision
|
|
52
85
|
};
|
|
53
86
|
}
|
|
54
87
|
}
|
|
@@ -69,14 +102,18 @@ export function evaluateAttemptPolicy(input) {
|
|
|
69
102
|
return {
|
|
70
103
|
allowed: false,
|
|
71
104
|
reason: "Materially repetitive attempts detected. Escalating to human.",
|
|
72
|
-
recommendedIntervention: "escalate_human"
|
|
105
|
+
recommendedIntervention: "escalate_human",
|
|
106
|
+
trajectoryAssessment,
|
|
107
|
+
circuitBreakDecision
|
|
73
108
|
};
|
|
74
109
|
}
|
|
75
110
|
}
|
|
76
111
|
}
|
|
77
112
|
return {
|
|
78
113
|
allowed: true,
|
|
79
|
-
reason: "Attempt admitted."
|
|
114
|
+
reason: "Attempt admitted.",
|
|
115
|
+
trajectoryAssessment,
|
|
116
|
+
circuitBreakDecision
|
|
80
117
|
};
|
|
81
118
|
}
|
|
82
119
|
export function distillContext(loop, options = {}) {
|
|
@@ -452,7 +489,8 @@ export async function runMartin(input) {
|
|
|
452
489
|
...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
|
|
453
490
|
...(failure?.recommendedIntervention
|
|
454
491
|
? { intervention: failure.recommendedIntervention }
|
|
455
|
-
: {})
|
|
492
|
+
: {}),
|
|
493
|
+
...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
|
|
456
494
|
};
|
|
457
495
|
loop = {
|
|
458
496
|
...loop,
|
|
@@ -609,7 +647,7 @@ export async function runMartin(input) {
|
|
|
609
647
|
}));
|
|
610
648
|
}
|
|
611
649
|
const changedFiles = tracksWorkspaceMutations
|
|
612
|
-
? resolveChangedFiles(result, request.context.repoRoot
|
|
650
|
+
? resolveChangedFiles(result, request.context.repoRoot)
|
|
613
651
|
: [];
|
|
614
652
|
// Evidence is only reliable when the adapter explicitly reported files OR git actually
|
|
615
653
|
// returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
|
|
@@ -1152,11 +1190,29 @@ function mergeCostProvenance(previous, current) {
|
|
|
1152
1190
|
}
|
|
1153
1191
|
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1154
1192
|
}
|
|
1155
|
-
function resolveChangedFiles(result, repoRoot
|
|
1193
|
+
function resolveChangedFiles(result, repoRoot) {
|
|
1156
1194
|
if (result.execution?.changedFiles !== undefined) {
|
|
1157
1195
|
return result.execution.changedFiles;
|
|
1158
1196
|
}
|
|
1159
|
-
|
|
1197
|
+
if (!repoRoot) {
|
|
1198
|
+
return [];
|
|
1199
|
+
}
|
|
1200
|
+
try {
|
|
1201
|
+
const diff = spawnSync("git", ["diff", "--name-only", "HEAD", "--", "."], {
|
|
1202
|
+
cwd: repoRoot,
|
|
1203
|
+
encoding: "utf8"
|
|
1204
|
+
});
|
|
1205
|
+
if (diff.status !== 0 || typeof diff.stdout !== "string") {
|
|
1206
|
+
return [];
|
|
1207
|
+
}
|
|
1208
|
+
return diff.stdout
|
|
1209
|
+
.split(/\r?\n/u)
|
|
1210
|
+
.map((entry) => entry.trim())
|
|
1211
|
+
.filter(Boolean);
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
return [];
|
|
1215
|
+
}
|
|
1160
1216
|
}
|
|
1161
1217
|
function buildPatchDiff(result, changedFiles) {
|
|
1162
1218
|
// Use structured diff stats to build a minimal diff header if no raw diff is available
|
|
@@ -49,9 +49,9 @@ export async function verifyReceiptIntegrityFromFiles(input) {
|
|
|
49
49
|
]);
|
|
50
50
|
if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
|
|
51
51
|
return {
|
|
52
|
-
state: "
|
|
52
|
+
state: "material_missing",
|
|
53
53
|
reason: "Receipt integrity material is missing for this run.",
|
|
54
|
-
warnings: ["
|
|
54
|
+
warnings: ["Receipt integrity material is incomplete; trust claims are unavailable."]
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
let material;
|
|
@@ -4,7 +4,6 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { writeReceiptIntegrityMaterial } from "./integrity.js";
|
|
6
6
|
// ─── FileRunStore implementation ─────────────────────────────────────────────
|
|
7
|
-
const RUN_INDEX_FILENAME = "run-index.ndjson";
|
|
8
7
|
export function resolveRunsRoot(env = process.env) {
|
|
9
8
|
return env["MARTIN_RUNS_DIR"]?.trim() ??
|
|
10
9
|
join(homedir(), ".martin", "runs");
|
|
@@ -85,7 +84,6 @@ export function createFileRunStore(options = {}) {
|
|
|
85
84
|
const dir = runDir(runsRoot, runId);
|
|
86
85
|
await mkdir(dir, { recursive: true });
|
|
87
86
|
await writeJsonFile(join(dir, "loop-record.json"), loop);
|
|
88
|
-
await appendRunIndexRecord(runsRoot, loop);
|
|
89
87
|
const ledgerRaw = await readFile(join(dir, "ledger.jsonl"), "utf8").catch(() => "");
|
|
90
88
|
const ledgerEntries = ledgerRaw
|
|
91
89
|
.split(/\r?\n/u)
|
|
@@ -112,14 +110,3 @@ export function createFileRunStore(options = {}) {
|
|
|
112
110
|
async function writeJsonFile(path, value) {
|
|
113
111
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
114
112
|
}
|
|
115
|
-
async function appendRunIndexRecord(runsRoot, loop) {
|
|
116
|
-
const line = JSON.stringify({
|
|
117
|
-
loopId: loop.loopId,
|
|
118
|
-
workspaceId: loop.workspaceId,
|
|
119
|
-
projectId: loop.projectId,
|
|
120
|
-
status: loop.status,
|
|
121
|
-
lifecycleState: loop.lifecycleState,
|
|
122
|
-
updatedAt: loop.updatedAt
|
|
123
|
-
});
|
|
124
|
-
await appendFile(join(runsRoot, RUN_INDEX_FILENAME), `${line}\n`, "utf8");
|
|
125
|
-
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { cloneExecutionPolicy } from "../contracts/index.js";
|
|
2
|
+
export function compileExecutionPolicy(input) {
|
|
3
|
+
const provenance = [];
|
|
4
|
+
const budget = {
|
|
5
|
+
maxUsd: resolveBudgetField("maxUsd", input, provenance),
|
|
6
|
+
softLimitUsd: resolveBudgetField("softLimitUsd", input, provenance),
|
|
7
|
+
maxIterations: resolveBudgetField("maxIterations", input, provenance),
|
|
8
|
+
maxTokens: resolveBudgetField("maxTokens", input, provenance)
|
|
9
|
+
};
|
|
10
|
+
if (budget.softLimitUsd >= budget.maxUsd) {
|
|
11
|
+
budget.softLimitUsd = Math.round(budget.maxUsd * 0.75 * 100) / 100;
|
|
12
|
+
provenance.push({
|
|
13
|
+
field: "budget.softLimitUsd",
|
|
14
|
+
source: "config",
|
|
15
|
+
value: String(budget.softLimitUsd)
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const governance = {
|
|
19
|
+
policyProfile: resolveEnumField({
|
|
20
|
+
field: "governance.policyProfile",
|
|
21
|
+
defaultsTo: input.defaults.policyProfile,
|
|
22
|
+
configValue: input.config?.policyProfile,
|
|
23
|
+
requestValue: input.request.policyProfile,
|
|
24
|
+
allowed: ["strict", "balanced", "overnight", "debug"],
|
|
25
|
+
provenance
|
|
26
|
+
}),
|
|
27
|
+
telemetryDestination: resolveEnumField({
|
|
28
|
+
field: "governance.telemetryDestination",
|
|
29
|
+
defaultsTo: input.defaults.telemetryDestination,
|
|
30
|
+
configValue: input.config?.governance?.telemetryDestination,
|
|
31
|
+
requestValue: input.request.telemetryDestination,
|
|
32
|
+
allowed: ["local-only", "control-plane"],
|
|
33
|
+
provenance
|
|
34
|
+
}),
|
|
35
|
+
destructiveActionPolicy: resolveEnumField({
|
|
36
|
+
field: "governance.destructiveActionPolicy",
|
|
37
|
+
defaultsTo: input.defaults.destructiveActionPolicy,
|
|
38
|
+
configValue: input.config?.governance?.destructiveActionPolicy,
|
|
39
|
+
allowed: ["never", "approval", "allowed"],
|
|
40
|
+
provenance
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
const verificationPlan = resolveVerificationPlan(input, provenance);
|
|
44
|
+
const task = {
|
|
45
|
+
verificationPlan
|
|
46
|
+
};
|
|
47
|
+
if (input.request.mutationMode) {
|
|
48
|
+
task.mutationMode = input.request.mutationMode;
|
|
49
|
+
provenance.push({
|
|
50
|
+
field: "task.mutationMode",
|
|
51
|
+
source: "request",
|
|
52
|
+
value: input.request.mutationMode
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (input.request.repoRoot) {
|
|
56
|
+
task.repoRoot = input.request.repoRoot;
|
|
57
|
+
provenance.push({
|
|
58
|
+
field: "task.repoRoot",
|
|
59
|
+
source: "request",
|
|
60
|
+
value: input.request.repoRoot
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if ((input.request.allowedPaths?.length ?? 0) > 0) {
|
|
64
|
+
const allowedPaths = input.request.allowedPaths ?? [];
|
|
65
|
+
task.allowedPaths = [...allowedPaths];
|
|
66
|
+
provenance.push({
|
|
67
|
+
field: "task.allowedPaths",
|
|
68
|
+
source: "request",
|
|
69
|
+
value: allowedPaths.join(", ")
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if ((input.request.deniedPaths?.length ?? 0) > 0) {
|
|
73
|
+
const deniedPaths = input.request.deniedPaths ?? [];
|
|
74
|
+
task.deniedPaths = [...deniedPaths];
|
|
75
|
+
provenance.push({
|
|
76
|
+
field: "task.deniedPaths",
|
|
77
|
+
source: "request",
|
|
78
|
+
value: deniedPaths.join(", ")
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if ((input.request.acceptanceCriteria?.length ?? 0) > 0) {
|
|
82
|
+
const acceptanceCriteria = input.request.acceptanceCriteria ?? [];
|
|
83
|
+
task.acceptanceCriteria = [...acceptanceCriteria];
|
|
84
|
+
provenance.push({
|
|
85
|
+
field: "task.acceptanceCriteria",
|
|
86
|
+
source: "request",
|
|
87
|
+
value: acceptanceCriteria.join(", ")
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (input.request.approvalPolicy) {
|
|
91
|
+
task.approvalPolicy = { ...input.request.approvalPolicy };
|
|
92
|
+
provenance.push({
|
|
93
|
+
field: "task.approvalPolicy",
|
|
94
|
+
source: "request",
|
|
95
|
+
value: JSON.stringify(input.request.approvalPolicy)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return cloneExecutionPolicy({
|
|
99
|
+
configPath: input.configPath,
|
|
100
|
+
budget,
|
|
101
|
+
governance,
|
|
102
|
+
task,
|
|
103
|
+
provenance
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function resolveBudgetField(field, input, provenance) {
|
|
107
|
+
if (input.request.budgetOverrides?.[field]) {
|
|
108
|
+
const value = input.request.budget[field];
|
|
109
|
+
provenance.push({
|
|
110
|
+
field: `budget.${String(field)}`,
|
|
111
|
+
source: "request",
|
|
112
|
+
value: String(value)
|
|
113
|
+
});
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
const configValue = input.config?.budget?.[field];
|
|
117
|
+
if (typeof configValue === "number" && Number.isFinite(configValue)) {
|
|
118
|
+
provenance.push({
|
|
119
|
+
field: `budget.${String(field)}`,
|
|
120
|
+
source: "config",
|
|
121
|
+
value: String(configValue)
|
|
122
|
+
});
|
|
123
|
+
return configValue;
|
|
124
|
+
}
|
|
125
|
+
const value = input.defaults.budget[field];
|
|
126
|
+
provenance.push({
|
|
127
|
+
field: `budget.${String(field)}`,
|
|
128
|
+
source: "default",
|
|
129
|
+
value: String(value)
|
|
130
|
+
});
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
function resolveVerificationPlan(input, provenance) {
|
|
134
|
+
const requestVerificationPlan = normalizeStringList(input.request.verificationPlan);
|
|
135
|
+
if (requestVerificationPlan.length > 0) {
|
|
136
|
+
provenance.push({
|
|
137
|
+
field: "task.verificationPlan",
|
|
138
|
+
source: "request",
|
|
139
|
+
value: requestVerificationPlan.join(", ")
|
|
140
|
+
});
|
|
141
|
+
return requestVerificationPlan;
|
|
142
|
+
}
|
|
143
|
+
const configVerificationPlan = normalizeStringList(input.config?.governance?.verifierRules);
|
|
144
|
+
if (configVerificationPlan.length > 0) {
|
|
145
|
+
provenance.push({
|
|
146
|
+
field: "task.verificationPlan",
|
|
147
|
+
source: "config",
|
|
148
|
+
value: configVerificationPlan.join(", ")
|
|
149
|
+
});
|
|
150
|
+
return configVerificationPlan;
|
|
151
|
+
}
|
|
152
|
+
const defaultVerificationPlan = normalizeStringList(input.defaults.verifierRules);
|
|
153
|
+
provenance.push({
|
|
154
|
+
field: "task.verificationPlan",
|
|
155
|
+
source: "default",
|
|
156
|
+
value: defaultVerificationPlan.join(", ")
|
|
157
|
+
});
|
|
158
|
+
return defaultVerificationPlan;
|
|
159
|
+
}
|
|
160
|
+
function resolveEnumField(input) {
|
|
161
|
+
const normalizedRequest = normalizeEnumCandidate(input.requestValue, input.allowed);
|
|
162
|
+
if (normalizedRequest) {
|
|
163
|
+
input.provenance.push({
|
|
164
|
+
field: input.field,
|
|
165
|
+
source: "request",
|
|
166
|
+
value: normalizedRequest
|
|
167
|
+
});
|
|
168
|
+
return normalizedRequest;
|
|
169
|
+
}
|
|
170
|
+
const normalizedConfig = normalizeEnumCandidate(input.configValue, input.allowed);
|
|
171
|
+
if (normalizedConfig) {
|
|
172
|
+
input.provenance.push({
|
|
173
|
+
field: input.field,
|
|
174
|
+
source: "config",
|
|
175
|
+
value: normalizedConfig
|
|
176
|
+
});
|
|
177
|
+
return normalizedConfig;
|
|
178
|
+
}
|
|
179
|
+
input.provenance.push({
|
|
180
|
+
field: input.field,
|
|
181
|
+
source: "default",
|
|
182
|
+
value: input.defaultsTo
|
|
183
|
+
});
|
|
184
|
+
return input.defaultsTo;
|
|
185
|
+
}
|
|
186
|
+
function normalizeEnumCandidate(value, allowed) {
|
|
187
|
+
if (typeof value !== "string") {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const trimmed = value.trim();
|
|
191
|
+
return allowed.find((candidate) => candidate === trimmed);
|
|
192
|
+
}
|
|
193
|
+
function normalizeStringList(values) {
|
|
194
|
+
return (values ?? []).map((value) => value.trim()).filter(Boolean);
|
|
195
|
+
}
|
|
@@ -4,6 +4,8 @@ export interface FailureAssessment {
|
|
|
4
4
|
rationale: string;
|
|
5
5
|
retryable: boolean;
|
|
6
6
|
recommendedIntervention: InterventionType;
|
|
7
|
+
/** Actionable diagnosis injected into the next attempt's prompt context. */
|
|
8
|
+
diagnosticHint?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface CostGovernorState {
|
|
9
11
|
pressure: "healthy" | "soft_limit" | "hard_limit";
|
|
@@ -78,7 +78,8 @@ export function classifyFailure(input) {
|
|
|
78
78
|
failureClass: "environment_mismatch",
|
|
79
79
|
rationale: "The adapter could not access the required local runtime or CLI tooling.",
|
|
80
80
|
retryable: false,
|
|
81
|
-
recommendedIntervention: "switch_adapter"
|
|
81
|
+
recommendedIntervention: "switch_adapter",
|
|
82
|
+
diagnosticHint: diagnoseEnvironmentIssue(message)
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
if (containsPositive(message, ["syntax error", "parse error", "typescript error"])) {
|
|
@@ -86,7 +87,8 @@ export function classifyFailure(input) {
|
|
|
86
87
|
failureClass: "syntax_error",
|
|
87
88
|
rationale: "The attempt failed on a parser or compiler-style issue.",
|
|
88
89
|
retryable: true,
|
|
89
|
-
recommendedIntervention: "compress_context"
|
|
90
|
+
recommendedIntervention: "compress_context",
|
|
91
|
+
diagnosticHint: "Check for malformed edits — the verifier found a syntax or parse error in the changed files."
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
if (containsPositive(message, [
|
|
@@ -104,7 +106,8 @@ export function classifyFailure(input) {
|
|
|
104
106
|
failureClass: "repo_grounding_failure",
|
|
105
107
|
rationale: "The attempt referenced modules, symbols, or files that are not grounded in the repo or approved docs.",
|
|
106
108
|
retryable: true,
|
|
107
|
-
recommendedIntervention: repeatedFailure === "repo_grounding_failure" ? "tighten_task" : "run_verifier"
|
|
109
|
+
recommendedIntervention: repeatedFailure === "repo_grounding_failure" ? "tighten_task" : "run_verifier",
|
|
110
|
+
diagnosticHint: diagnoseGroundingIssue(message)
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
113
|
if (containsPositive(message, [
|
|
@@ -118,7 +121,8 @@ export function classifyFailure(input) {
|
|
|
118
121
|
failureClass: "verification_failure",
|
|
119
122
|
rationale: "The proposed fix did not satisfy the verification gate.",
|
|
120
123
|
retryable: true,
|
|
121
|
-
recommendedIntervention: repeatedFailure === "verification_failure" ? "tighten_task" : "run_verifier"
|
|
124
|
+
recommendedIntervention: repeatedFailure === "verification_failure" ? "tighten_task" : "run_verifier",
|
|
125
|
+
diagnosticHint: diagnoseVerificationIssue(message)
|
|
122
126
|
};
|
|
123
127
|
}
|
|
124
128
|
if (containsPositive(message, [
|
|
@@ -622,3 +626,66 @@ function computeDiffRiskScore(input) {
|
|
|
622
626
|
function roundScore(value) {
|
|
623
627
|
return Math.round(value * 100) / 100;
|
|
624
628
|
}
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// Diagnostic hint generators
|
|
631
|
+
//
|
|
632
|
+
// These extract actionable context from failure messages so the next attempt
|
|
633
|
+
// gets a concrete diagnosis, not just "verification failed."
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
const KNOWN_TOOLS = {
|
|
636
|
+
bun: "npm install -g bun",
|
|
637
|
+
pnpm: "npm install -g pnpm",
|
|
638
|
+
yarn: "npm install -g yarn",
|
|
639
|
+
eslint: "npm install eslint",
|
|
640
|
+
vitest: "npm install vitest",
|
|
641
|
+
jest: "npm install jest",
|
|
642
|
+
tsc: "npm install typescript",
|
|
643
|
+
prettier: "npm install prettier",
|
|
644
|
+
biome: "npm install @biomejs/biome"
|
|
645
|
+
};
|
|
646
|
+
function diagnoseEnvironmentIssue(message) {
|
|
647
|
+
// Try to extract the missing command name
|
|
648
|
+
const cmdMatch = message.match(/(?:command not found|enoent)[:\s]*['"]?(\w[\w.-]*)/i)
|
|
649
|
+
?? message.match(/['"](\w[\w.-]*)['"].*(?:not found|enoent)/i);
|
|
650
|
+
const missingCmd = cmdMatch?.[1];
|
|
651
|
+
if (missingCmd && KNOWN_TOOLS[missingCmd.toLowerCase()]) {
|
|
652
|
+
return `The verifier needs "${missingCmd}" which is not installed. Fix: ${KNOWN_TOOLS[missingCmd.toLowerCase()]}`;
|
|
653
|
+
}
|
|
654
|
+
if (missingCmd) {
|
|
655
|
+
return `The verifier could not find "${missingCmd}". Ensure it is installed in the workspace before retrying.`;
|
|
656
|
+
}
|
|
657
|
+
return "A required tool or command was not found in the environment. Check that all dependencies referenced by the verify command are installed.";
|
|
658
|
+
}
|
|
659
|
+
function diagnoseGroundingIssue(message) {
|
|
660
|
+
// Extract the missing module/symbol name
|
|
661
|
+
const moduleMatch = message.match(/cannot find module ['"]([^'"]+)['"]/i)
|
|
662
|
+
?? message.match(/module not found[:\s]*['"]?([^\s'"]+)/i);
|
|
663
|
+
const nameMatch = message.match(/cannot find name ['"]?(\w+)/i);
|
|
664
|
+
if (moduleMatch?.[1]) {
|
|
665
|
+
const mod = moduleMatch[1];
|
|
666
|
+
const isRelative = mod.startsWith(".") || mod.startsWith("/");
|
|
667
|
+
if (isRelative) {
|
|
668
|
+
return `Import "${mod}" points to a file that does not exist. Check the path and filename.`;
|
|
669
|
+
}
|
|
670
|
+
return `Module "${mod}" is not installed. Run npm install or check package.json dependencies.`;
|
|
671
|
+
}
|
|
672
|
+
if (nameMatch?.[1]) {
|
|
673
|
+
return `Symbol "${nameMatch[1]}" is not defined. Check that the import exists and the name is spelled correctly.`;
|
|
674
|
+
}
|
|
675
|
+
return "The code references modules or symbols that do not exist in the repository. Verify imports and file paths.";
|
|
676
|
+
}
|
|
677
|
+
function diagnoseVerificationIssue(message) {
|
|
678
|
+
// Count assertion failures for severity signal
|
|
679
|
+
const assertionCount = (message.match(/assertionerror/gi) ?? []).length;
|
|
680
|
+
if (assertionCount > 3) {
|
|
681
|
+
return `${String(assertionCount)} assertion failures detected — the change may have introduced a regression. Focus on the specific test expectations that broke.`;
|
|
682
|
+
}
|
|
683
|
+
// Check for common patterns
|
|
684
|
+
if (/timeout/i.test(message) && !/budget/i.test(message)) {
|
|
685
|
+
return "Tests timed out. The change may have introduced an infinite loop or unresolved async operation.";
|
|
686
|
+
}
|
|
687
|
+
if (/exit code (?!0)\d+/i.test(message) || /exited with code/i.test(message)) {
|
|
688
|
+
return "The verifier command exited non-zero. Read the output above to identify which specific check failed and why.";
|
|
689
|
+
}
|
|
690
|
+
return "Verification did not pass. Review the verifier output for the specific failing assertion or check.";
|
|
691
|
+
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { PatchDecision, RollbackBoundaryArtifact, RollbackOutcomeArtifact } from "../contracts/index.js";
|
|
2
|
-
export declare function listAttemptChangedFilesSinceBoundary(input: {
|
|
3
|
-
repoRoot?: string;
|
|
4
|
-
boundary?: RollbackBoundaryArtifact;
|
|
5
|
-
}): string[];
|
|
6
2
|
export declare function captureRollbackBoundary(input: {
|
|
7
3
|
repoRoot?: string;
|
|
8
4
|
capturedAt: string;
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
|
-
export function listAttemptChangedFilesSinceBoundary(input) {
|
|
5
|
-
if (!input.repoRoot) {
|
|
6
|
-
return [];
|
|
7
|
-
}
|
|
8
|
-
const repoState = readRepoState(input.repoRoot);
|
|
9
|
-
if (!input.boundary) {
|
|
10
|
-
return uniqueSorted([...repoState.trackedDirtyFiles, ...repoState.untrackedFiles]);
|
|
11
|
-
}
|
|
12
|
-
const baselineTracked = new Set(input.boundary.trackedDirtyFiles);
|
|
13
|
-
const baselineUntracked = new Set(input.boundary.untrackedFiles);
|
|
14
|
-
return uniqueSorted([
|
|
15
|
-
...repoState.trackedDirtyFiles.filter((filePath) => !baselineTracked.has(filePath)),
|
|
16
|
-
...repoState.untrackedFiles.filter((filePath) => !baselineUntracked.has(filePath))
|
|
17
|
-
]);
|
|
18
|
-
}
|
|
19
4
|
export async function captureRollbackBoundary(input) {
|
|
20
5
|
if (!input.repoRoot) {
|
|
21
6
|
return undefined;
|
|
@@ -134,17 +119,24 @@ function readRepoState(repoRoot) {
|
|
|
134
119
|
};
|
|
135
120
|
}
|
|
136
121
|
function readGitLines(repoRoot, args) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
123
|
+
const result = spawnSync("git", args, {
|
|
124
|
+
cwd: repoRoot,
|
|
125
|
+
encoding: "utf8"
|
|
126
|
+
});
|
|
127
|
+
if (result.status === 0 && typeof result.stdout === "string") {
|
|
128
|
+
return uniqueSorted(result.stdout
|
|
129
|
+
.split(/\r?\n/u)
|
|
130
|
+
.map((line) => normalizeRepoPath(line))
|
|
131
|
+
.filter(Boolean));
|
|
132
|
+
}
|
|
133
|
+
// Retry once after a short delay — handles git lock-file contention
|
|
134
|
+
// that occurs when another process holds .git/index.lock.
|
|
135
|
+
if (attempt === 0 && result.status !== 0) {
|
|
136
|
+
spawnSync("sleep", ["0.5"], { encoding: "utf8" });
|
|
137
|
+
}
|
|
143
138
|
}
|
|
144
|
-
return
|
|
145
|
-
.split(/\r?\n/u)
|
|
146
|
-
.map((line) => normalizeRepoPath(line))
|
|
147
|
-
.filter(Boolean));
|
|
139
|
+
return [];
|
|
148
140
|
}
|
|
149
141
|
function readGitScalar(repoRoot, args) {
|
|
150
142
|
const result = spawnSync("git", args, {
|
|
@@ -190,9 +182,22 @@ function restoreTrackedFileFromHead(repoRoot, filePath) {
|
|
|
190
182
|
cwd: repoRoot,
|
|
191
183
|
encoding: "utf8"
|
|
192
184
|
});
|
|
193
|
-
if (result.status
|
|
194
|
-
|
|
185
|
+
if (result.status === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Retry once after a short delay (git lock-file contention)
|
|
189
|
+
spawnSync("sleep", ["0.5"], { encoding: "utf8" });
|
|
190
|
+
const retry = spawnSync("git", ["restore", "--staged", "--worktree", "--source=HEAD", "--", filePath], { cwd: repoRoot, encoding: "utf8" });
|
|
191
|
+
if (retry.status === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Fallback: try git checkout which works on older git versions
|
|
195
|
+
const fallback = spawnSync("git", ["checkout", "HEAD", "--", filePath], { cwd: repoRoot, encoding: "utf8" });
|
|
196
|
+
if (fallback.status === 0) {
|
|
197
|
+
return;
|
|
195
198
|
}
|
|
199
|
+
throw new Error(result.stderr?.trim() ||
|
|
200
|
+
`git restore failed for ${filePath} (retry and checkout fallback also failed)`);
|
|
196
201
|
}
|
|
197
202
|
async function removeRepoPath(repoRoot, filePath) {
|
|
198
203
|
await rm(resolveRepoPath(repoRoot, filePath), { recursive: true, force: true });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type CircuitBreakDecision, type TrajectoryAssessment } from "../contracts/index.js";
|
|
2
|
+
export declare function assessTrajectory(input: {
|
|
3
|
+
objective: string;
|
|
4
|
+
verificationPlan?: string[];
|
|
5
|
+
attempts: TrajectoryAttemptLike[];
|
|
6
|
+
}): TrajectoryAssessment;
|
|
7
|
+
export declare function decideCircuitBreak(input: {
|
|
8
|
+
objective: string;
|
|
9
|
+
verificationPlan?: string[];
|
|
10
|
+
attempts: TrajectoryAttemptLike[];
|
|
11
|
+
remainingIterations?: number;
|
|
12
|
+
}): CircuitBreakDecision;
|
|
13
|
+
interface TrajectoryAttemptLike {
|
|
14
|
+
index: number;
|
|
15
|
+
summary?: string;
|
|
16
|
+
failureClass?: string;
|
|
17
|
+
}
|
|
18
|
+
export {};
|