@martinloop/mcp 0.3.2 → 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.
Files changed (64) hide show
  1. package/README.md +112 -107
  2. package/dist/discovery-support.js +2 -2
  3. package/dist/prompts.d.ts +2 -1
  4. package/dist/resources.d.ts +2 -1
  5. package/dist/resources.js +147 -147
  6. package/dist/server-validation.js +5 -2
  7. package/dist/server.js +5 -4
  8. package/dist/tools/eval.js +29 -5
  9. package/dist/tools/execution-policy.d.ts +11 -0
  10. package/dist/tools/execution-policy.js +47 -0
  11. package/dist/tools/run-dossier.js +2 -2
  12. package/dist/tools/run-loop.d.ts +2 -1
  13. package/dist/tools/run-loop.js +15 -2
  14. package/dist/tools/run-store.js +166 -6
  15. package/dist/tools/tool-support.d.ts +3 -2
  16. package/dist/tools/tool-support.js +112 -18
  17. package/dist/tools/triage-runs.js +31 -6
  18. package/dist/vendor/adapters/claude-cli.d.ts +1 -1
  19. package/dist/vendor/adapters/claude-cli.js +71 -20
  20. package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
  21. package/dist/vendor/adapters/cli-bridge.js +91 -7
  22. package/dist/vendor/adapters/codex-launcher.js +65 -8
  23. package/dist/vendor/adapters/openai-compatible.d.ts +1 -1
  24. package/dist/vendor/adapters/openai-compatible.js +7 -7
  25. package/dist/vendor/adapters/verifier-only.js +1 -1
  26. package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
  27. package/dist/vendor/contracts/adapter-capabilities.js +35 -0
  28. package/dist/vendor/contracts/context-graph.d.ts +48 -0
  29. package/dist/vendor/contracts/context-graph.js +26 -0
  30. package/dist/vendor/contracts/execution-policy.d.ts +62 -0
  31. package/dist/vendor/contracts/execution-policy.js +34 -0
  32. package/dist/vendor/contracts/governance.d.ts +6 -3
  33. package/dist/vendor/contracts/identity.d.ts +29 -0
  34. package/dist/vendor/contracts/identity.js +30 -0
  35. package/dist/vendor/contracts/index.d.ts +13 -2
  36. package/dist/vendor/contracts/index.js +4 -0
  37. package/dist/vendor/contracts/observation.d.ts +21 -0
  38. package/dist/vendor/contracts/observation.js +27 -0
  39. package/dist/vendor/contracts/trajectory.d.ts +26 -0
  40. package/dist/vendor/contracts/trajectory.js +27 -0
  41. package/dist/vendor/contracts/verification.d.ts +28 -0
  42. package/dist/vendor/contracts/verification.js +7 -0
  43. package/dist/vendor/core/context-graph.d.ts +3 -0
  44. package/dist/vendor/core/context-graph.js +371 -0
  45. package/dist/vendor/core/context-integrity.js +1 -1
  46. package/dist/vendor/core/grounding.js +1 -1
  47. package/dist/vendor/core/identity.d.ts +20 -0
  48. package/dist/vendor/core/identity.js +86 -0
  49. package/dist/vendor/core/index.d.ts +7 -0
  50. package/dist/vendor/core/index.js +63 -9
  51. package/dist/vendor/core/leash.js +1 -1
  52. package/dist/vendor/core/persistence/integrity.d.ts +1 -0
  53. package/dist/vendor/core/persistence/integrity.js +7 -3
  54. package/dist/vendor/core/persistence/runs-reader.d.ts +14 -0
  55. package/dist/vendor/core/persistence/runs-reader.js +61 -4
  56. package/dist/vendor/core/policy-compiler.d.ts +2 -0
  57. package/dist/vendor/core/policy-compiler.js +195 -0
  58. package/dist/vendor/core/policy.d.ts +2 -0
  59. package/dist/vendor/core/policy.js +71 -4
  60. package/dist/vendor/core/rollback.js +32 -12
  61. package/dist/vendor/core/trajectory.d.ts +18 -0
  62. package/dist/vendor/core/trajectory.js +295 -0
  63. package/package.json +76 -75
  64. package/server.json +21 -21
@@ -5,6 +5,10 @@ import { dirname, join } from "node:path";
5
5
  const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
6
6
  const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
7
7
  const RECEIPT_INTEGRITY_KEY_FILE_MODE = 0o600;
8
+ export function resolveReceiptIntegrityRoot(env = process.env) {
9
+ return env["MARTIN_INTEGRITY_KEY_DIR"]?.trim() ??
10
+ join(homedir(), ".martin", "receipt-integrity");
11
+ }
8
12
  export async function writeReceiptIntegrityMaterial(input) {
9
13
  const signedAt = input.signedAt ?? new Date().toISOString();
10
14
  const keyMaterial = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
@@ -45,9 +49,9 @@ export async function verifyReceiptIntegrityFromFiles(input) {
45
49
  ]);
46
50
  if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
47
51
  return {
48
- state: "unsigned",
52
+ state: "material_missing",
49
53
  reason: "Receipt integrity material is missing for this run.",
50
- warnings: ["Receipts are local-only and unsigned; trust claims are unavailable."]
54
+ warnings: ["Receipt integrity material is incomplete; trust claims are unavailable."]
51
55
  };
52
56
  }
53
57
  let material;
@@ -232,7 +236,7 @@ async function readReceiptIntegrityKey(runsRoot, runId) {
232
236
  }
233
237
  function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
234
238
  const rootHash = sha256(runsRoot).slice(0, 16);
235
- return join(homedir(), ".martin", "receipt-integrity", rootHash, `${runId}.key`);
239
+ return join(resolveReceiptIntegrityRoot(), rootHash, `${runId}.key`);
236
240
  }
237
241
  function serializeStoredJson(value) {
238
242
  return `${JSON.stringify(value, null, 2)}\n`;
@@ -39,6 +39,19 @@ export interface LoopRunRecord {
39
39
  objective: string;
40
40
  };
41
41
  }
42
+ export interface LoopRecordsRollup {
43
+ generatedAt: string;
44
+ totalRuns: number;
45
+ statusBreakdown: Record<string, number>;
46
+ lifecycleBreakdown: Record<string, number>;
47
+ latestByLoopId: Record<string, {
48
+ status: string;
49
+ lifecycleState: string;
50
+ updatedAt: string;
51
+ costUsd: number;
52
+ attempts: number;
53
+ }>;
54
+ }
42
55
  export declare function readLoopRecordsFromFile(file: string): Promise<LoopRunRecord[]>;
43
56
  export declare function readLatestLoopRecordFromFile(file: string): Promise<LoopRunRecord | null>;
44
57
  /**
@@ -50,3 +63,4 @@ export declare function readAllLoopRecords(runsDir?: string): Promise<LoopRunRec
50
63
  * Returns the most recently updated loop record, or null if none exist.
51
64
  */
52
65
  export declare function readLatestLoopRecord(runsDir?: string): Promise<LoopRunRecord | null>;
66
+ export declare function buildLoopRecordsRollup(records: LoopRunRecord[]): LoopRecordsRollup;
@@ -45,13 +45,16 @@ export async function readAllLoopRecords(runsDir) {
45
45
  catch {
46
46
  return [];
47
47
  }
48
- const records = [];
48
+ const recordsByLoopId = new Map();
49
49
  const jsonlFiles = entries
50
50
  .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
51
51
  .map((entry) => entry.name);
52
52
  for (const file of jsonlFiles) {
53
53
  try {
54
- records.push(...(await readLoopRecordsFromFile(join(dir, file))));
54
+ const fromFile = await readLoopRecordsFromFile(join(dir, file));
55
+ for (const record of fromFile) {
56
+ ingestRecord(recordsByLoopId, record, "legacy_jsonl");
57
+ }
55
58
  }
56
59
  catch {
57
60
  // skip malformed files or lines
@@ -60,13 +63,16 @@ export async function readAllLoopRecords(runsDir) {
60
63
  const runDirectories = entries.filter((entry) => entry.isDirectory());
61
64
  for (const entry of runDirectories) {
62
65
  try {
63
- records.push(...(await readLoopRecordsFromFile(join(dir, entry.name, "loop-record.json"))));
66
+ const canonical = await readLoopRecordsFromFile(join(dir, entry.name, "loop-record.json"));
67
+ for (const record of canonical) {
68
+ ingestRecord(recordsByLoopId, record, "canonical_tree");
69
+ }
64
70
  }
65
71
  catch {
66
72
  // skip missing or malformed canonical records
67
73
  }
68
74
  }
69
- return records;
75
+ return [...recordsByLoopId.values()].map((entry) => entry.record);
70
76
  }
71
77
  /**
72
78
  * Returns the most recently updated loop record, or null if none exist.
@@ -81,3 +87,54 @@ export async function readLatestLoopRecord(runsDir) {
81
87
  return a > b ? r : latest;
82
88
  }, records[0]);
83
89
  }
90
+ export function buildLoopRecordsRollup(records) {
91
+ const statusBreakdown = {};
92
+ const lifecycleBreakdown = {};
93
+ const latestByLoopId = {};
94
+ for (const record of records) {
95
+ statusBreakdown[record.status] = (statusBreakdown[record.status] ?? 0) + 1;
96
+ lifecycleBreakdown[record.lifecycleState] = (lifecycleBreakdown[record.lifecycleState] ?? 0) + 1;
97
+ latestByLoopId[record.loopId] = {
98
+ status: record.status,
99
+ lifecycleState: record.lifecycleState,
100
+ updatedAt: record.updatedAt,
101
+ costUsd: record.cost.actualUsd,
102
+ attempts: record.attempts.length
103
+ };
104
+ }
105
+ return {
106
+ generatedAt: new Date().toISOString(),
107
+ totalRuns: records.length,
108
+ statusBreakdown,
109
+ lifecycleBreakdown,
110
+ latestByLoopId
111
+ };
112
+ }
113
+ function ingestRecord(recordsByLoopId, record, source) {
114
+ const existing = recordsByLoopId.get(record.loopId);
115
+ if (!existing) {
116
+ recordsByLoopId.set(record.loopId, { record, source });
117
+ return;
118
+ }
119
+ const candidateTimestamp = resolveRecordTimestamp(record);
120
+ const existingTimestamp = resolveRecordTimestamp(existing.record);
121
+ if (candidateTimestamp > existingTimestamp) {
122
+ recordsByLoopId.set(record.loopId, { record, source });
123
+ return;
124
+ }
125
+ if (candidateTimestamp === existingTimestamp &&
126
+ sourcePrecedence(source) > sourcePrecedence(existing.source)) {
127
+ recordsByLoopId.set(record.loopId, { record, source });
128
+ }
129
+ }
130
+ function resolveRecordTimestamp(record) {
131
+ const updated = Date.parse(record.updatedAt ?? "");
132
+ if (Number.isFinite(updated)) {
133
+ return updated;
134
+ }
135
+ const created = Date.parse(record.createdAt ?? "");
136
+ return Number.isFinite(created) ? created : 0;
137
+ }
138
+ function sourcePrecedence(source) {
139
+ return source === "canonical_tree" ? 2 : 1;
140
+ }
@@ -0,0 +1,2 @@
1
+ import { type ExecutionPolicy, type ExecutionPolicyCompileInput } from "../contracts/index.js";
2
+ export declare function compileExecutionPolicy(input: ExecutionPolicyCompileInput): ExecutionPolicy;
@@ -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
+ }
@@ -119,17 +119,24 @@ function readRepoState(repoRoot) {
119
119
  };
120
120
  }
121
121
  function readGitLines(repoRoot, args) {
122
- const result = spawnSync("git", args, {
123
- cwd: repoRoot,
124
- encoding: "utf8"
125
- });
126
- if (result.status !== 0 || typeof result.stdout !== "string") {
127
- return [];
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
+ }
128
138
  }
129
- return uniqueSorted(result.stdout
130
- .split(/\r?\n/u)
131
- .map((line) => normalizeRepoPath(line))
132
- .filter(Boolean));
139
+ return [];
133
140
  }
134
141
  function readGitScalar(repoRoot, args) {
135
142
  const result = spawnSync("git", args, {
@@ -175,9 +182,22 @@ function restoreTrackedFileFromHead(repoRoot, filePath) {
175
182
  cwd: repoRoot,
176
183
  encoding: "utf8"
177
184
  });
178
- if (result.status !== 0) {
179
- throw new Error(result.stderr?.trim() || `git restore failed for ${filePath}`);
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;
180
198
  }
199
+ throw new Error(result.stderr?.trim() ||
200
+ `git restore failed for ${filePath} (retry and checkout fallback also failed)`);
181
201
  }
182
202
  async function removeRepoPath(repoRoot, filePath) {
183
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 {};