@martinloop/mcp 0.3.5 → 0.3.7
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 +137 -137
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +2 -0
- package/dist/prompts.js +19 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +125 -17
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +12 -0
- package/dist/server.js +18 -7
- package/dist/tools/doctor.js +6 -0
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +5 -1
- package/dist/tools/tool-support.d.ts +1 -1
- package/dist/tools/tool-support.js +2 -1
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
- package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
- package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/index.js +8 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
- package/dist/vendor/adapters/claude-cli.js +21 -20
- package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
- package/dist/vendor/adapters/cli-bridge.js +94 -8
- package/dist/vendor/adapters/codex-launcher.js +18 -7
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
- package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
- package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
- package/dist/vendor/adapters/contracts/src/governance.js +11 -0
- package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
- package/dist/vendor/adapters/contracts/src/identity.js +30 -0
- package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
- package/dist/vendor/adapters/contracts/src/index.js +225 -0
- package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
- package/dist/vendor/adapters/contracts/src/operator.js +11 -0
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
- package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
- package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
- package/dist/vendor/adapters/core/src/compiler.js +52 -0
- package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
- package/dist/vendor/adapters/core/src/context-graph.js +371 -0
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
- package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
- package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
- package/dist/vendor/adapters/core/src/grounding.js +273 -0
- package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
- package/dist/vendor/adapters/core/src/identity.js +86 -0
- package/dist/vendor/adapters/core/src/index.d.ts +186 -0
- package/dist/vendor/adapters/core/src/index.js +1495 -0
- package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
- package/dist/vendor/adapters/core/src/leash.js +497 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
- package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
- package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
- package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
- package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
- package/dist/vendor/adapters/core/src/policy.js +704 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
- package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
- package/dist/vendor/adapters/core/src/rollback.js +253 -0
- package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
- package/dist/vendor/adapters/core/src/routing.js +336 -0
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
- package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
- package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
- package/dist/vendor/adapters/core/src/trajectory.js +295 -0
- package/dist/vendor/adapters/openai-compatible.js +84 -51
- package/dist/vendor/adapters/verifier-only.js +2 -2
- package/dist/vendor/contracts/index.d.ts +2 -2
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/compiler.js +4 -9
- package/dist/vendor/core/index.d.ts +10 -2
- package/dist/vendor/core/index.js +32 -91
- package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/core/persistence/memory-store.js +115 -0
- package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/core/persistence/trace-store.js +125 -0
- package/dist/vendor/core/repo-analyzer.d.ts +53 -0
- package/dist/vendor/core/repo-analyzer.js +263 -0
- package/dist/vendor/core/rollback.js +2 -3
- package/dist/vendor/core/routing.d.ts +42 -0
- package/dist/vendor/core/routing.js +156 -5
- package/dist/vendor/core/test-integrity.d.ts +64 -0
- package/dist/vendor/core/test-integrity.js +197 -0
- package/dist/workflow-state.d.ts +1 -1
- package/dist/workflow-state.js +12 -4
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Style Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Scans a repository to detect coding conventions, patterns, and style
|
|
5
|
+
* so that governed agent runs produce code matching the existing codebase.
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Naming conventions (camelCase, snake_case, PascalCase, kebab-case)
|
|
9
|
+
* - Test framework (Vitest, Jest, Mocha, node:test)
|
|
10
|
+
* - Package manager (pnpm, npm, yarn, bun)
|
|
11
|
+
* - Module system (ESM, CJS, mixed)
|
|
12
|
+
* - File organization pattern (feature folders, type folders, flat)
|
|
13
|
+
* - TypeScript strictness level
|
|
14
|
+
* - Comment density
|
|
15
|
+
* - Formatting (tabs vs spaces, semicolons, quotes)
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
18
|
+
import { join, extname } from "node:path";
|
|
19
|
+
/**
|
|
20
|
+
* Analyze a repository and return a style profile.
|
|
21
|
+
*/
|
|
22
|
+
export function analyzeRepoStyle(rootDir) {
|
|
23
|
+
const profile = {
|
|
24
|
+
packageManager: detectPackageManager(rootDir),
|
|
25
|
+
moduleSystem: detectModuleSystem(rootDir),
|
|
26
|
+
testFramework: detectTestFramework(rootDir),
|
|
27
|
+
language: detectLanguage(rootDir),
|
|
28
|
+
typescriptStrict: detectTypeScriptStrict(rootDir),
|
|
29
|
+
fileNaming: "kebab-case",
|
|
30
|
+
codeNaming: "camelCase",
|
|
31
|
+
semicolons: true,
|
|
32
|
+
quotes: "double",
|
|
33
|
+
indentation: "2-spaces",
|
|
34
|
+
avgLinesPerFile: 0,
|
|
35
|
+
organization: "mixed",
|
|
36
|
+
promptDirectives: []
|
|
37
|
+
};
|
|
38
|
+
// Sample source files for style detection
|
|
39
|
+
const sourceFiles = collectSourceFiles(rootDir, 50);
|
|
40
|
+
if (sourceFiles.length > 0) {
|
|
41
|
+
const styleSignals = analyzeSourceFiles(sourceFiles);
|
|
42
|
+
profile.fileNaming = styleSignals.fileNaming;
|
|
43
|
+
profile.codeNaming = styleSignals.codeNaming;
|
|
44
|
+
profile.semicolons = styleSignals.semicolons;
|
|
45
|
+
profile.quotes = styleSignals.quotes;
|
|
46
|
+
profile.indentation = styleSignals.indentation;
|
|
47
|
+
profile.avgLinesPerFile = styleSignals.avgLinesPerFile;
|
|
48
|
+
}
|
|
49
|
+
profile.organization = detectOrganization(rootDir);
|
|
50
|
+
profile.promptDirectives = buildPromptDirectives(profile);
|
|
51
|
+
return profile;
|
|
52
|
+
}
|
|
53
|
+
function detectPackageManager(rootDir) {
|
|
54
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml")))
|
|
55
|
+
return "pnpm";
|
|
56
|
+
if (existsSync(join(rootDir, "bun.lockb")) || existsSync(join(rootDir, "bun.lock")))
|
|
57
|
+
return "bun";
|
|
58
|
+
if (existsSync(join(rootDir, "yarn.lock")))
|
|
59
|
+
return "yarn";
|
|
60
|
+
if (existsSync(join(rootDir, "package-lock.json")))
|
|
61
|
+
return "npm";
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
function detectModuleSystem(rootDir) {
|
|
65
|
+
const pkgPath = join(rootDir, "package.json");
|
|
66
|
+
if (!existsSync(pkgPath))
|
|
67
|
+
return "unknown";
|
|
68
|
+
try {
|
|
69
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
70
|
+
if (pkg.type === "module")
|
|
71
|
+
return "esm";
|
|
72
|
+
if (pkg.type === "commonjs")
|
|
73
|
+
return "cjs";
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
function detectTestFramework(rootDir) {
|
|
79
|
+
const pkgPath = join(rootDir, "package.json");
|
|
80
|
+
if (!existsSync(pkgPath))
|
|
81
|
+
return "unknown";
|
|
82
|
+
try {
|
|
83
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
84
|
+
if (raw.includes("vitest"))
|
|
85
|
+
return "vitest";
|
|
86
|
+
if (raw.includes("jest"))
|
|
87
|
+
return "jest";
|
|
88
|
+
if (raw.includes("mocha"))
|
|
89
|
+
return "mocha";
|
|
90
|
+
if (raw.includes("node:test"))
|
|
91
|
+
return "node-test";
|
|
92
|
+
}
|
|
93
|
+
catch { /* ignore */ }
|
|
94
|
+
if (existsSync(join(rootDir, "vitest.config.ts")) || existsSync(join(rootDir, "vitest.config.js")))
|
|
95
|
+
return "vitest";
|
|
96
|
+
if (existsSync(join(rootDir, "jest.config.ts")) || existsSync(join(rootDir, "jest.config.js")))
|
|
97
|
+
return "jest";
|
|
98
|
+
return "unknown";
|
|
99
|
+
}
|
|
100
|
+
function detectLanguage(rootDir) {
|
|
101
|
+
if (existsSync(join(rootDir, "tsconfig.json")) ||
|
|
102
|
+
existsSync(join(rootDir, "tsconfig.base.json")) ||
|
|
103
|
+
existsSync(join(rootDir, "tsconfig.build.json"))) {
|
|
104
|
+
return "typescript";
|
|
105
|
+
}
|
|
106
|
+
return "javascript";
|
|
107
|
+
}
|
|
108
|
+
function detectTypeScriptStrict(rootDir) {
|
|
109
|
+
const candidates = ["tsconfig.json", "tsconfig.base.json", "tsconfig.build.json"];
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
const tsConfigPath = join(rootDir, candidate);
|
|
112
|
+
if (!existsSync(tsConfigPath))
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
const raw = readFileSync(tsConfigPath, "utf8");
|
|
116
|
+
if (raw.includes('"strict": true') || raw.includes('"strict":true'))
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch { /* skip */ }
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
function collectSourceFiles(rootDir, limit) {
|
|
124
|
+
const results = [];
|
|
125
|
+
const extensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
126
|
+
function walk(dir, depth) {
|
|
127
|
+
if (depth > 5 || results.length >= limit)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (results.length >= limit)
|
|
133
|
+
return;
|
|
134
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
135
|
+
continue;
|
|
136
|
+
const fullPath = join(dir, entry.name);
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
walk(fullPath, depth + 1);
|
|
139
|
+
}
|
|
140
|
+
else if (entry.isFile() && extensions.has(extname(entry.name))) {
|
|
141
|
+
results.push(fullPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch { /* skip */ }
|
|
146
|
+
}
|
|
147
|
+
walk(rootDir, 0);
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
function analyzeSourceFiles(files) {
|
|
151
|
+
let semiCount = 0;
|
|
152
|
+
let noSemiCount = 0;
|
|
153
|
+
let singleQuoteCount = 0;
|
|
154
|
+
let doubleQuoteCount = 0;
|
|
155
|
+
let tabCount = 0;
|
|
156
|
+
let twoSpaceCount = 0;
|
|
157
|
+
let fourSpaceCount = 0;
|
|
158
|
+
let totalLines = 0;
|
|
159
|
+
for (const filePath of files) {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(filePath, "utf8");
|
|
162
|
+
const lines = content.split("\n");
|
|
163
|
+
totalLines += lines.length;
|
|
164
|
+
for (const line of lines.slice(0, 100)) {
|
|
165
|
+
if (line.trimEnd().endsWith(";"))
|
|
166
|
+
semiCount++;
|
|
167
|
+
else if (line.trim().length > 0)
|
|
168
|
+
noSemiCount++;
|
|
169
|
+
if (/'\w/.test(line))
|
|
170
|
+
singleQuoteCount++;
|
|
171
|
+
if (/"\w/.test(line))
|
|
172
|
+
doubleQuoteCount++;
|
|
173
|
+
if (line.startsWith("\t"))
|
|
174
|
+
tabCount++;
|
|
175
|
+
else if (line.startsWith(" ") && !line.startsWith(" "))
|
|
176
|
+
twoSpaceCount++;
|
|
177
|
+
else if (line.startsWith(" "))
|
|
178
|
+
fourSpaceCount++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch { /* skip */ }
|
|
182
|
+
}
|
|
183
|
+
const semicolons = semiCount > noSemiCount;
|
|
184
|
+
const quotes = singleQuoteCount > doubleQuoteCount * 2 ? "single" :
|
|
185
|
+
doubleQuoteCount > singleQuoteCount * 2 ? "double" : "mixed";
|
|
186
|
+
const indentation = tabCount > twoSpaceCount + fourSpaceCount ? "tabs" :
|
|
187
|
+
twoSpaceCount > fourSpaceCount * 2 ? "2-spaces" :
|
|
188
|
+
fourSpaceCount > twoSpaceCount * 2 ? "4-spaces" : "mixed";
|
|
189
|
+
return {
|
|
190
|
+
fileNaming: "kebab-case", // Default, hard to detect reliably without more files
|
|
191
|
+
codeNaming: "camelCase", // TypeScript default
|
|
192
|
+
semicolons,
|
|
193
|
+
quotes,
|
|
194
|
+
indentation,
|
|
195
|
+
avgLinesPerFile: files.length > 0 ? Math.round(totalLines / files.length) : 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function detectOrganization(rootDir) {
|
|
199
|
+
const srcDir = join(rootDir, "src");
|
|
200
|
+
if (!existsSync(srcDir))
|
|
201
|
+
return "flat";
|
|
202
|
+
try {
|
|
203
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
204
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
205
|
+
const hasFeatureDirs = dirs.some((d) => !["utils", "lib", "types", "helpers", "common", "shared", "__tests__", "tests"].includes(d));
|
|
206
|
+
const hasTypeDirs = dirs.some((d) => ["components", "hooks", "services", "models", "controllers", "routes", "middleware"].includes(d));
|
|
207
|
+
if (hasFeatureDirs && !hasTypeDirs)
|
|
208
|
+
return "feature-folders";
|
|
209
|
+
if (hasTypeDirs && !hasFeatureDirs)
|
|
210
|
+
return "type-folders";
|
|
211
|
+
if (hasFeatureDirs && hasTypeDirs)
|
|
212
|
+
return "mixed";
|
|
213
|
+
}
|
|
214
|
+
catch { /* skip */ }
|
|
215
|
+
return "flat";
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Build prompt directives from the style profile.
|
|
219
|
+
* These get injected into the agent's prompt to match repo conventions.
|
|
220
|
+
*/
|
|
221
|
+
export function buildPromptDirectives(profile) {
|
|
222
|
+
const directives = [];
|
|
223
|
+
if (profile.language === "typescript") {
|
|
224
|
+
directives.push("Write TypeScript, not JavaScript.");
|
|
225
|
+
if (profile.typescriptStrict) {
|
|
226
|
+
directives.push("TypeScript strict mode is enabled. Use explicit types, avoid any.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (profile.moduleSystem === "esm") {
|
|
230
|
+
directives.push("Use ES module imports (import/export), not require().");
|
|
231
|
+
}
|
|
232
|
+
else if (profile.moduleSystem === "cjs") {
|
|
233
|
+
directives.push("Use CommonJS (require/module.exports), not ES module imports.");
|
|
234
|
+
}
|
|
235
|
+
if (profile.testFramework !== "unknown") {
|
|
236
|
+
directives.push(`Use ${profile.testFramework} for tests. Do not introduce a different test framework.`);
|
|
237
|
+
}
|
|
238
|
+
if (profile.semicolons) {
|
|
239
|
+
directives.push("Use semicolons at the end of statements.");
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
directives.push("Do not use semicolons at the end of statements.");
|
|
243
|
+
}
|
|
244
|
+
if (profile.quotes === "single") {
|
|
245
|
+
directives.push("Use single quotes for strings.");
|
|
246
|
+
}
|
|
247
|
+
else if (profile.quotes === "double") {
|
|
248
|
+
directives.push("Use double quotes for strings.");
|
|
249
|
+
}
|
|
250
|
+
if (profile.indentation === "tabs") {
|
|
251
|
+
directives.push("Use tabs for indentation.");
|
|
252
|
+
}
|
|
253
|
+
else if (profile.indentation === "2-spaces") {
|
|
254
|
+
directives.push("Use 2-space indentation.");
|
|
255
|
+
}
|
|
256
|
+
else if (profile.indentation === "4-spaces") {
|
|
257
|
+
directives.push("Use 4-space indentation.");
|
|
258
|
+
}
|
|
259
|
+
if (profile.packageManager !== "unknown") {
|
|
260
|
+
directives.push(`This project uses ${profile.packageManager}. Do not use other package managers.`);
|
|
261
|
+
}
|
|
262
|
+
return directives;
|
|
263
|
+
}
|
|
@@ -29,12 +29,11 @@ export async function captureRollbackBoundary(input) {
|
|
|
29
29
|
for (const filePath of snapshotPaths) {
|
|
30
30
|
snapshots.push(await readRollbackSnapshot(input.repoRoot, filePath));
|
|
31
31
|
}
|
|
32
|
+
const headRef = readGitScalar(input.repoRoot, ["rev-parse", "HEAD"]);
|
|
32
33
|
return {
|
|
33
34
|
strategy: "git_head_plus_snapshot",
|
|
34
35
|
capturedAt: input.capturedAt,
|
|
35
|
-
...(
|
|
36
|
-
? { headRef: readGitScalar(input.repoRoot, ["rev-parse", "HEAD"]) }
|
|
37
|
-
: {}),
|
|
36
|
+
...(headRef ? { headRef } : {}),
|
|
38
37
|
trackedDirtyFiles: repoState.trackedDirtyFiles,
|
|
39
38
|
untrackedFiles: repoState.untrackedFiles,
|
|
40
39
|
snapshots
|
|
@@ -15,6 +15,14 @@ export interface RouteDecision {
|
|
|
15
15
|
blockedSteps: string[];
|
|
16
16
|
compressed: boolean;
|
|
17
17
|
compressionSummary?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Recommended model tier based on task complexity and route.
|
|
20
|
+
* Follows cost-aware routing: direct → haiku/sonnet, manager → sonnet,
|
|
21
|
+
* consensus → opus. Never use a more expensive model than the task needs.
|
|
22
|
+
*/
|
|
23
|
+
recommendedModelTier: "haiku" | "sonnet" | "opus";
|
|
24
|
+
/** Estimated cost saving vs always using sonnet */
|
|
25
|
+
estimatedSavingVsSonnetUsd: number;
|
|
18
26
|
}
|
|
19
27
|
export interface RouteClassificationInput {
|
|
20
28
|
objective: string;
|
|
@@ -29,6 +37,40 @@ export interface RouteClassificationInput {
|
|
|
29
37
|
/** Historical success rate for similar tasks with direct execution (0-1). */
|
|
30
38
|
historicalDirectSuccessRate?: number;
|
|
31
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a tier to a concrete model ID for a specific engine.
|
|
42
|
+
* Allows downstream adapters to switch models automatically.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveModelForTier(tier: RouteDecision["recommendedModelTier"], engine: "claude" | "codex" | "gemini" | "openai" | string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Available engine registry — describes what's installed and its capability.
|
|
47
|
+
* Populated by doctor/preflight from PATH and env var detection.
|
|
48
|
+
*/
|
|
49
|
+
export interface AvailableEngine {
|
|
50
|
+
id: string;
|
|
51
|
+
available: boolean;
|
|
52
|
+
/** Cost tier: cheap < mid < expensive */
|
|
53
|
+
costTier: "cheap" | "mid" | "expensive";
|
|
54
|
+
/** Capability tier: determines which task types this engine can handle */
|
|
55
|
+
capabilityTier: "haiku" | "sonnet" | "opus";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Select the best available engine for a given tier requirement.
|
|
59
|
+
*
|
|
60
|
+
* When multiple engines are available, picks the cheapest that meets
|
|
61
|
+
* the capability requirement. Falls back gracefully:
|
|
62
|
+
* - If required tier unavailable, upgrades to next tier of same engine
|
|
63
|
+
* - If only one engine family available, uses its best equivalent
|
|
64
|
+
*
|
|
65
|
+
* @param requiredTier - Minimum capability tier needed
|
|
66
|
+
* @param availableEngines - What's installed and authenticated
|
|
67
|
+
* @returns Best engine ID and concrete model to use
|
|
68
|
+
*/
|
|
69
|
+
export declare function selectBestEngine(requiredTier: RouteDecision["recommendedModelTier"], availableEngines: AvailableEngine[]): {
|
|
70
|
+
engineId: string;
|
|
71
|
+
model: string;
|
|
72
|
+
reasoning: string;
|
|
73
|
+
};
|
|
32
74
|
export declare function classifyRoute(input: RouteClassificationInput): RouteDecision;
|
|
33
75
|
/**
|
|
34
76
|
* Checks whether a run's Pre Work Burn exceeds the routing policy limits.
|
|
@@ -84,6 +84,144 @@ function calculateDirectConfidence(signals, input) {
|
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
85
|
// Public API
|
|
86
86
|
// ---------------------------------------------------------------------------
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Cost-aware model tier routing
|
|
89
|
+
//
|
|
90
|
+
// Engine-agnostic tier system — each engine maps tiers to real model IDs.
|
|
91
|
+
// Never pay for reasoning/orchestration on simple tasks.
|
|
92
|
+
//
|
|
93
|
+
// Tier → concrete model per engine:
|
|
94
|
+
// haiku → claude-haiku-4-5, gpt-4o-mini, gemini-2.5-flash, deepseek-chat, qwen3-8b, llama-3.1-8b
|
|
95
|
+
// sonnet → claude-sonnet-4-6, gpt-4o, gemini-2.5-pro, deepseek-r1, qwen3-32b, llama-3.3-70b
|
|
96
|
+
// opus → claude-opus-4-6, o3, gemini-2.5-ultra, deepseek-r1-671b, qwen3-235b, llama-3.1-405b
|
|
97
|
+
//
|
|
98
|
+
// Pricing reference (USD per 1M tokens, approx 2026):
|
|
99
|
+
// haiku: $0.80 input / $4.00 output (~1x baseline)
|
|
100
|
+
// sonnet: $3.00 input / $15.00 output (~4x haiku)
|
|
101
|
+
// opus: $15.00 input / $75.00 output (~19x haiku)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
const SONNET_AVG_COST_PER_USD_BUDGET = 0.35; // sonnet uses ~35% of budget on average
|
|
104
|
+
const HAIKU_AVG_COST_PER_USD_BUDGET = 0.10; // haiku uses ~10% — ~3.5x cheaper
|
|
105
|
+
function selectModelTier(mode) {
|
|
106
|
+
// Route already encodes complexity. Direct = passed the simplicity check → haiku.
|
|
107
|
+
if (mode === "direct")
|
|
108
|
+
return "haiku";
|
|
109
|
+
if (mode === "manager")
|
|
110
|
+
return "sonnet";
|
|
111
|
+
return "opus";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a tier to a concrete model ID for a specific engine.
|
|
115
|
+
* Allows downstream adapters to switch models automatically.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveModelForTier(tier, engine) {
|
|
118
|
+
const matrix = {
|
|
119
|
+
claude: {
|
|
120
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
121
|
+
sonnet: "claude-sonnet-4-6",
|
|
122
|
+
opus: "claude-opus-4-6"
|
|
123
|
+
},
|
|
124
|
+
codex: {
|
|
125
|
+
haiku: "gpt-4o-mini",
|
|
126
|
+
sonnet: "gpt-4.1",
|
|
127
|
+
opus: "o3"
|
|
128
|
+
},
|
|
129
|
+
gemini: {
|
|
130
|
+
haiku: "gemini-2.5-flash",
|
|
131
|
+
sonnet: "gemini-2.5-pro",
|
|
132
|
+
opus: "gemini-2.5-ultra"
|
|
133
|
+
},
|
|
134
|
+
openai: {
|
|
135
|
+
haiku: "gpt-4o-mini",
|
|
136
|
+
sonnet: "gpt-4o",
|
|
137
|
+
opus: "o3"
|
|
138
|
+
},
|
|
139
|
+
deepseek: {
|
|
140
|
+
haiku: "deepseek-chat",
|
|
141
|
+
sonnet: "deepseek-r1",
|
|
142
|
+
opus: "deepseek-r1"
|
|
143
|
+
},
|
|
144
|
+
qwen: {
|
|
145
|
+
haiku: "qwen/qwen3-8b",
|
|
146
|
+
sonnet: "qwen/qwen3-32b",
|
|
147
|
+
opus: "qwen/qwen3-235b-a22b"
|
|
148
|
+
},
|
|
149
|
+
llama: {
|
|
150
|
+
haiku: "meta-llama/llama-3.1-8b-instruct",
|
|
151
|
+
sonnet: "meta-llama/llama-3.3-70b-instruct",
|
|
152
|
+
opus: "meta-llama/llama-3.1-405b-instruct"
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const engineKey = engine.toLowerCase().split("-")[0] ?? engine;
|
|
156
|
+
const claudeMatrix = matrix.claude;
|
|
157
|
+
return matrix[engineKey]?.[tier] ?? claudeMatrix?.[tier] ?? "claude-sonnet-4-6";
|
|
158
|
+
}
|
|
159
|
+
function estimateSavingVsSonnet(tier, budgetUsd) {
|
|
160
|
+
if (tier === "sonnet")
|
|
161
|
+
return 0;
|
|
162
|
+
if (tier === "haiku")
|
|
163
|
+
return Math.round((SONNET_AVG_COST_PER_USD_BUDGET - HAIKU_AVG_COST_PER_USD_BUDGET) * budgetUsd * 100) / 100;
|
|
164
|
+
// opus costs more than sonnet — negative saving
|
|
165
|
+
return Math.round(-1 * SONNET_AVG_COST_PER_USD_BUDGET * budgetUsd * 0.5 * 100) / 100;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Select the best available engine for a given tier requirement.
|
|
169
|
+
*
|
|
170
|
+
* When multiple engines are available, picks the cheapest that meets
|
|
171
|
+
* the capability requirement. Falls back gracefully:
|
|
172
|
+
* - If required tier unavailable, upgrades to next tier of same engine
|
|
173
|
+
* - If only one engine family available, uses its best equivalent
|
|
174
|
+
*
|
|
175
|
+
* @param requiredTier - Minimum capability tier needed
|
|
176
|
+
* @param availableEngines - What's installed and authenticated
|
|
177
|
+
* @returns Best engine ID and concrete model to use
|
|
178
|
+
*/
|
|
179
|
+
export function selectBestEngine(requiredTier, availableEngines) {
|
|
180
|
+
const available = availableEngines.filter((e) => e.available);
|
|
181
|
+
if (available.length === 0) {
|
|
182
|
+
// Nothing detected — default to claude (user must install)
|
|
183
|
+
return {
|
|
184
|
+
engineId: "claude",
|
|
185
|
+
model: resolveModelForTier(requiredTier, "claude"),
|
|
186
|
+
reasoning: "No engines detected. Defaulting to claude. Run martin doctor for setup instructions."
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const tierRank = {
|
|
190
|
+
haiku: 0, sonnet: 1, opus: 2
|
|
191
|
+
};
|
|
192
|
+
const requiredRank = tierRank[requiredTier];
|
|
193
|
+
// Find the cheapest engine that meets or exceeds the required tier
|
|
194
|
+
const eligible = available
|
|
195
|
+
.filter((e) => tierRank[e.capabilityTier] >= requiredRank)
|
|
196
|
+
.sort((a, b) => {
|
|
197
|
+
// Sort by cost (cheap first), then by exact tier match
|
|
198
|
+
const costOrder = { cheap: 0, mid: 1, expensive: 2 };
|
|
199
|
+
if (costOrder[a.costTier] !== costOrder[b.costTier]) {
|
|
200
|
+
return costOrder[a.costTier] - costOrder[b.costTier];
|
|
201
|
+
}
|
|
202
|
+
// Prefer exact tier match over overqualified
|
|
203
|
+
const aExact = a.capabilityTier === requiredTier ? 0 : 1;
|
|
204
|
+
const bExact = b.capabilityTier === requiredTier ? 0 : 1;
|
|
205
|
+
return aExact - bExact;
|
|
206
|
+
});
|
|
207
|
+
if (eligible.length > 0) {
|
|
208
|
+
const best = eligible[0];
|
|
209
|
+
const model = resolveModelForTier(requiredTier, best.id);
|
|
210
|
+
return {
|
|
211
|
+
engineId: best.id,
|
|
212
|
+
model,
|
|
213
|
+
reasoning: `Selected ${best.id} (${model}) as cheapest available engine for ${requiredTier}-tier task.`
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// All available engines are below required tier — upgrade to best available
|
|
217
|
+
const best = available.sort((a, b) => tierRank[b.capabilityTier] - tierRank[a.capabilityTier])[0];
|
|
218
|
+
const model = resolveModelForTier(best.capabilityTier, best.id);
|
|
219
|
+
return {
|
|
220
|
+
engineId: best.id,
|
|
221
|
+
model,
|
|
222
|
+
reasoning: `No ${requiredTier}-tier engine available. Upgrading to ${best.id} (${model}) — best available.`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
87
225
|
export function classifyRoute(input) {
|
|
88
226
|
const policy = { ...DEFAULT_ROUTING_POLICY, ...input.policy };
|
|
89
227
|
const signals = extractComplexitySignals(input);
|
|
@@ -92,6 +230,7 @@ export function classifyRoute(input) {
|
|
|
92
230
|
const blockedSteps = [];
|
|
93
231
|
// Policy override: forced mode
|
|
94
232
|
if (policy.mode === "direct") {
|
|
233
|
+
const tier = selectModelTier("direct");
|
|
95
234
|
return {
|
|
96
235
|
selectedMode: "direct",
|
|
97
236
|
confidence: 1,
|
|
@@ -100,10 +239,13 @@ export function classifyRoute(input) {
|
|
|
100
239
|
reason: ["Policy forces direct execution mode."],
|
|
101
240
|
blockedSteps: ["manager", "planner", "router", "consensus"],
|
|
102
241
|
compressed: true,
|
|
103
|
-
compressionSummary: "Policy: direct mode — skipping all orchestration."
|
|
242
|
+
compressionSummary: "Policy: direct mode — skipping all orchestration.",
|
|
243
|
+
recommendedModelTier: tier,
|
|
244
|
+
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
104
245
|
};
|
|
105
246
|
}
|
|
106
247
|
if (policy.mode === "consensus") {
|
|
248
|
+
const tier = selectModelTier("consensus");
|
|
107
249
|
return {
|
|
108
250
|
selectedMode: "consensus",
|
|
109
251
|
confidence: 0.5,
|
|
@@ -111,7 +253,9 @@ export function classifyRoute(input) {
|
|
|
111
253
|
expectedPreworkBurnPct: 35,
|
|
112
254
|
reason: ["Policy forces consensus mode."],
|
|
113
255
|
blockedSteps: [],
|
|
114
|
-
compressed: false
|
|
256
|
+
compressed: false,
|
|
257
|
+
recommendedModelTier: tier,
|
|
258
|
+
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
115
259
|
};
|
|
116
260
|
}
|
|
117
261
|
// Adaptive routing
|
|
@@ -124,6 +268,7 @@ export function classifyRoute(input) {
|
|
|
124
268
|
if (signals.scopeRestricted)
|
|
125
269
|
reason.push("Path scope is restricted.");
|
|
126
270
|
blockedSteps.push("manager", "consensus");
|
|
271
|
+
const tier = selectModelTier("direct");
|
|
127
272
|
return {
|
|
128
273
|
selectedMode: "direct",
|
|
129
274
|
confidence: directConfidence,
|
|
@@ -132,7 +277,9 @@ export function classifyRoute(input) {
|
|
|
132
277
|
reason,
|
|
133
278
|
blockedSteps,
|
|
134
279
|
compressed: true,
|
|
135
|
-
compressionSummary: `Compressed from manager→planner→worker to direct worker. Estimated savings: ${String(Math.round(input.budgetUsd * 0.3 * 100) / 100)} USD
|
|
280
|
+
compressionSummary: `Compressed from manager→planner→worker to direct worker. Estimated savings: ${String(Math.round(input.budgetUsd * 0.3 * 100) / 100)} USD.`,
|
|
281
|
+
recommendedModelTier: tier,
|
|
282
|
+
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
136
283
|
};
|
|
137
284
|
}
|
|
138
285
|
// Complex task — needs orchestration
|
|
@@ -145,14 +292,18 @@ export function classifyRoute(input) {
|
|
|
145
292
|
if (signals.fileCount > 5)
|
|
146
293
|
reason.push(`Touches ${String(signals.fileCount)}+ files.`);
|
|
147
294
|
const needsConsensus = signals.hasSecurityKeywords && signals.hasMigrationKeywords;
|
|
295
|
+
const mode = needsConsensus ? "consensus" : "manager";
|
|
296
|
+
const tier = selectModelTier(mode);
|
|
148
297
|
return {
|
|
149
|
-
selectedMode:
|
|
298
|
+
selectedMode: mode,
|
|
150
299
|
confidence: directConfidence,
|
|
151
300
|
expectedCostUsd: input.budgetUsd * (needsConsensus ? 0.8 : 0.6),
|
|
152
301
|
expectedPreworkBurnPct: needsConsensus ? 40 : 25,
|
|
153
302
|
reason,
|
|
154
303
|
blockedSteps: needsConsensus ? [] : ["consensus"],
|
|
155
|
-
compressed: false
|
|
304
|
+
compressed: false,
|
|
305
|
+
recommendedModelTier: tier,
|
|
306
|
+
estimatedSavingVsSonnetUsd: estimateSavingVsSonnet(tier, input.budgetUsd)
|
|
156
307
|
};
|
|
157
308
|
}
|
|
158
309
|
function estimateDirectCost(budgetUsd) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Integrity Validator
|
|
3
|
+
*
|
|
4
|
+
* Detects circular test validation — the problem where AI agents generate
|
|
5
|
+
* tests that only pass because they test the agent's own output, not real
|
|
6
|
+
* behavior. A test that passes on both the pre-change and post-change
|
|
7
|
+
* codebase proves nothing about the change.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. Snapshot which test files existed before the agent ran
|
|
11
|
+
* 2. After the run, identify NEW test files the agent created
|
|
12
|
+
* 3. For each new test: check if it imports/references pre-existing code
|
|
13
|
+
* (real test) or only references agent-generated code (circular test)
|
|
14
|
+
* 4. Flag tests that don't exercise any pre-existing function
|
|
15
|
+
*
|
|
16
|
+
* This module provides the analysis primitives. The verification pipeline
|
|
17
|
+
* calls these during the post-run verification step.
|
|
18
|
+
*/
|
|
19
|
+
export interface TestIntegrityReport {
|
|
20
|
+
totalNewTests: number;
|
|
21
|
+
realTests: number;
|
|
22
|
+
suspiciousTests: number;
|
|
23
|
+
findings: TestIntegrityFinding[];
|
|
24
|
+
verdict: "pass" | "warn" | "fail";
|
|
25
|
+
summary: string;
|
|
26
|
+
}
|
|
27
|
+
export interface TestIntegrityFinding {
|
|
28
|
+
filePath: string;
|
|
29
|
+
issue: "no_pre_existing_imports" | "self_referential" | "trivial_assertion" | "empty_test";
|
|
30
|
+
severity: "high" | "medium" | "low";
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Detect test files in a directory tree.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findTestFiles(rootDir: string): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Check if a filename matches test file patterns.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isTestFile(filename: string, fullPath?: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Snapshot existing test files before an agent run.
|
|
43
|
+
*/
|
|
44
|
+
export declare function snapshotTestFiles(rootDir: string): Set<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Identify new test files created during an agent run.
|
|
47
|
+
*/
|
|
48
|
+
export declare function identifyNewTests(rootDir: string, preRunSnapshot: Set<string>): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Extract import paths from a TypeScript/JavaScript file.
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractImports(content: string): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Check if a test file contains only trivial assertions.
|
|
55
|
+
*/
|
|
56
|
+
export declare function hasTrivialAssertionsOnly(content: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Check if a test file imports any pre-existing source files.
|
|
59
|
+
*/
|
|
60
|
+
export declare function importsPreExistingCode(testContent: string, preRunSourceFiles: Set<string>): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Validate the integrity of tests created during a governed run.
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateTestIntegrity(rootDir: string, preRunTestSnapshot: Set<string>, preRunSourceFiles: Set<string>): TestIntegrityReport;
|