@martinloop/mcp 0.3.6 → 0.3.8

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 (119) hide show
  1. package/LICENSE +216 -0
  2. package/README.md +105 -97
  3. package/dist/discovery-metadata.d.ts +1 -1
  4. package/dist/discovery-metadata.js +1 -1
  5. package/dist/package-version.d.ts +1 -1
  6. package/dist/package-version.js +1 -1
  7. package/dist/resources.d.ts +2 -1
  8. package/dist/resources.js +251 -197
  9. package/dist/server-validation.d.ts +1 -1
  10. package/dist/server-validation.js +11 -16
  11. package/dist/server.d.ts +0 -23
  12. package/dist/server.js +7 -152
  13. package/dist/tools/preflight.d.ts +2 -0
  14. package/dist/tools/preflight.js +42 -2
  15. package/dist/tools/run-loop.d.ts +2 -1
  16. package/dist/tools/run-loop.js +5 -1
  17. package/dist/tools/tool-support.js +12 -1
  18. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
  19. package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
  20. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
  21. package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
  22. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
  23. package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
  24. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
  25. package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
  26. package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
  27. package/dist/vendor/adapters/adapters/src/index.js +8 -0
  28. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
  29. package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
  30. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
  31. package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
  32. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
  33. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
  34. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
  35. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
  36. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
  37. package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
  38. package/dist/vendor/adapters/claude-cli.js +130 -68
  39. package/dist/vendor/adapters/codex-launcher.js +10 -1
  40. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
  41. package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
  42. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
  43. package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
  44. package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
  45. package/dist/vendor/adapters/contracts/src/governance.js +11 -0
  46. package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
  47. package/dist/vendor/adapters/contracts/src/identity.js +30 -0
  48. package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
  49. package/dist/vendor/adapters/contracts/src/index.js +225 -0
  50. package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
  51. package/dist/vendor/adapters/contracts/src/operator.js +11 -0
  52. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
  53. package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
  54. package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
  55. package/dist/vendor/adapters/core/src/compiler.js +52 -0
  56. package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
  57. package/dist/vendor/adapters/core/src/context-graph.js +371 -0
  58. package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
  59. package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
  60. package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
  61. package/dist/vendor/adapters/core/src/grounding.js +273 -0
  62. package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
  63. package/dist/vendor/adapters/core/src/identity.js +86 -0
  64. package/dist/vendor/adapters/core/src/index.d.ts +186 -0
  65. package/dist/vendor/adapters/core/src/index.js +1495 -0
  66. package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
  67. package/dist/vendor/adapters/core/src/leash.js +497 -0
  68. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
  69. package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
  70. package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
  71. package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
  72. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
  73. package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
  74. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
  75. package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
  76. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
  77. package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
  78. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
  79. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
  80. package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
  81. package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
  82. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
  83. package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
  84. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
  85. package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
  86. package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
  87. package/dist/vendor/adapters/core/src/policy.js +704 -0
  88. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
  89. package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
  90. package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
  91. package/dist/vendor/adapters/core/src/rollback.js +253 -0
  92. package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
  93. package/dist/vendor/adapters/core/src/routing.js +336 -0
  94. package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
  95. package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
  96. package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
  97. package/dist/vendor/adapters/core/src/trajectory.js +295 -0
  98. package/dist/vendor/adapters/openai-compatible.js +6 -6
  99. package/dist/vendor/adapters/verifier-only.js +2 -2
  100. package/dist/vendor/contracts/delivery.d.ts +47 -0
  101. package/dist/vendor/contracts/delivery.js +18 -0
  102. package/dist/vendor/contracts/index.d.ts +3 -1
  103. package/dist/vendor/contracts/index.js +2 -0
  104. package/dist/vendor/core/compiler.js +4 -9
  105. package/dist/vendor/core/delivery/index.d.ts +6 -0
  106. package/dist/vendor/core/delivery/index.js +4 -0
  107. package/dist/vendor/core/delivery/message-client.d.ts +18 -0
  108. package/dist/vendor/core/delivery/message-client.js +38 -0
  109. package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
  110. package/dist/vendor/core/delivery/message-ledger.js +97 -0
  111. package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
  112. package/dist/vendor/core/delivery/message-schema.js +111 -0
  113. package/dist/vendor/core/delivery/update-check.d.ts +19 -0
  114. package/dist/vendor/core/delivery/update-check.js +107 -0
  115. package/dist/vendor/core/index.d.ts +2 -0
  116. package/dist/vendor/core/index.js +3 -88
  117. package/dist/vendor/core/rollback.js +2 -3
  118. package/package.json +83 -76
  119. package/server.json +21 -21
@@ -0,0 +1,53 @@
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
+ export interface RepoStyleProfile {
18
+ /** Detected package manager */
19
+ packageManager: "pnpm" | "npm" | "yarn" | "bun" | "unknown";
20
+ /** Detected module system */
21
+ moduleSystem: "esm" | "cjs" | "mixed" | "unknown";
22
+ /** Detected test framework */
23
+ testFramework: "vitest" | "jest" | "mocha" | "node-test" | "unknown";
24
+ /** Primary language */
25
+ language: "typescript" | "javascript" | "mixed";
26
+ /** TypeScript strict mode enabled */
27
+ typescriptStrict: boolean;
28
+ /** Dominant naming convention for files */
29
+ fileNaming: "kebab-case" | "camelCase" | "PascalCase" | "snake_case" | "mixed";
30
+ /** Dominant naming convention for variables/functions */
31
+ codeNaming: "camelCase" | "snake_case" | "mixed";
32
+ /** Uses semicolons */
33
+ semicolons: boolean;
34
+ /** Single vs double quotes */
35
+ quotes: "single" | "double" | "mixed";
36
+ /** Indentation style */
37
+ indentation: "tabs" | "2-spaces" | "4-spaces" | "mixed";
38
+ /** Average lines per file (source files) */
39
+ avgLinesPerFile: number;
40
+ /** File organization pattern */
41
+ organization: "feature-folders" | "type-folders" | "flat" | "mixed";
42
+ /** Raw signals for prompt injection */
43
+ promptDirectives: string[];
44
+ }
45
+ /**
46
+ * Analyze a repository and return a style profile.
47
+ */
48
+ export declare function analyzeRepoStyle(rootDir: string): RepoStyleProfile;
49
+ /**
50
+ * Build prompt directives from the style profile.
51
+ * These get injected into the agent's prompt to match repo conventions.
52
+ */
53
+ export declare function buildPromptDirectives(profile: RepoStyleProfile): string[];
@@ -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
+ }
@@ -0,0 +1,15 @@
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
+ export declare function captureRollbackBoundary(input: {
7
+ repoRoot?: string;
8
+ capturedAt: string;
9
+ }): Promise<RollbackBoundaryArtifact | undefined>;
10
+ export declare function restoreRollbackBoundary(input: {
11
+ repoRoot?: string;
12
+ boundary?: RollbackBoundaryArtifact;
13
+ restoredAt: string;
14
+ decision: PatchDecision;
15
+ }): Promise<RollbackOutcomeArtifact | undefined>;
@@ -0,0 +1,253 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
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
+ export async function captureRollbackBoundary(input) {
20
+ if (!input.repoRoot) {
21
+ return undefined;
22
+ }
23
+ const repoState = readRepoState(input.repoRoot);
24
+ const snapshotPaths = uniqueSorted([
25
+ ...repoState.trackedDirtyFiles,
26
+ ...repoState.untrackedFiles
27
+ ]);
28
+ const snapshots = [];
29
+ for (const filePath of snapshotPaths) {
30
+ snapshots.push(await readRollbackSnapshot(input.repoRoot, filePath));
31
+ }
32
+ return {
33
+ strategy: "git_head_plus_snapshot",
34
+ capturedAt: input.capturedAt,
35
+ ...(readGitScalar(input.repoRoot, ["rev-parse", "HEAD"])
36
+ ? { headRef: readGitScalar(input.repoRoot, ["rev-parse", "HEAD"]) }
37
+ : {}),
38
+ trackedDirtyFiles: repoState.trackedDirtyFiles,
39
+ untrackedFiles: repoState.untrackedFiles,
40
+ snapshots
41
+ };
42
+ }
43
+ export async function restoreRollbackBoundary(input) {
44
+ if (!input.repoRoot) {
45
+ return undefined;
46
+ }
47
+ if (!input.boundary) {
48
+ return {
49
+ attempted: false,
50
+ status: "unavailable",
51
+ restoredAt: input.restoredAt,
52
+ decision: input.decision,
53
+ before: emptyRepoState(),
54
+ after: emptyRepoState(),
55
+ restoredFiles: [],
56
+ deletedFiles: [],
57
+ error: "Rollback boundary was unavailable for this attempt."
58
+ };
59
+ }
60
+ const before = readRepoState(input.repoRoot);
61
+ if (repoStateMatchesBoundary(before, input.boundary)) {
62
+ return {
63
+ attempted: false,
64
+ status: "not_required",
65
+ restoredAt: input.restoredAt,
66
+ decision: input.decision,
67
+ before,
68
+ after: before,
69
+ restoredFiles: [],
70
+ deletedFiles: []
71
+ };
72
+ }
73
+ const restoredFiles = new Set();
74
+ const deletedFiles = new Set();
75
+ try {
76
+ const baselineTracked = new Set(input.boundary.trackedDirtyFiles);
77
+ const baselineUntracked = new Set(input.boundary.untrackedFiles);
78
+ for (const filePath of before.trackedDirtyFiles) {
79
+ if (!baselineTracked.has(filePath)) {
80
+ restoreTrackedFileFromHead(input.repoRoot, filePath);
81
+ restoredFiles.add(filePath);
82
+ }
83
+ }
84
+ for (const filePath of before.untrackedFiles) {
85
+ if (!baselineUntracked.has(filePath)) {
86
+ await removeRepoPath(input.repoRoot, filePath);
87
+ deletedFiles.add(filePath);
88
+ }
89
+ }
90
+ for (const snapshot of input.boundary.snapshots) {
91
+ await restoreRollbackSnapshot(input.repoRoot, snapshot);
92
+ if (snapshot.existed) {
93
+ restoredFiles.add(snapshot.path);
94
+ }
95
+ else {
96
+ deletedFiles.add(snapshot.path);
97
+ }
98
+ }
99
+ const after = readRepoState(input.repoRoot);
100
+ const restored = repoStateMatchesBoundary(after, input.boundary);
101
+ return {
102
+ attempted: true,
103
+ status: restored ? "restored" : "failed",
104
+ restoredAt: input.restoredAt,
105
+ decision: input.decision,
106
+ before,
107
+ after,
108
+ restoredFiles: [...restoredFiles].sort(),
109
+ deletedFiles: [...deletedFiles].sort(),
110
+ ...(restored
111
+ ? {}
112
+ : { error: "Repo state still diverged from the recorded rollback boundary after restore." })
113
+ };
114
+ }
115
+ catch (error) {
116
+ const after = readRepoState(input.repoRoot);
117
+ return {
118
+ attempted: true,
119
+ status: "failed",
120
+ restoredAt: input.restoredAt,
121
+ decision: input.decision,
122
+ before,
123
+ after,
124
+ restoredFiles: [...restoredFiles].sort(),
125
+ deletedFiles: [...deletedFiles].sort(),
126
+ error: toErrorMessage(error)
127
+ };
128
+ }
129
+ }
130
+ function readRepoState(repoRoot) {
131
+ return {
132
+ trackedDirtyFiles: readGitLines(repoRoot, ["diff", "--name-only", "HEAD", "--", "."]),
133
+ untrackedFiles: readGitLines(repoRoot, ["ls-files", "--others", "--exclude-standard", "--", "."])
134
+ };
135
+ }
136
+ function readGitLines(repoRoot, args) {
137
+ for (let attempt = 0; attempt < 2; attempt++) {
138
+ const result = spawnSync("git", args, {
139
+ cwd: repoRoot,
140
+ encoding: "utf8"
141
+ });
142
+ if (result.status === 0 && typeof result.stdout === "string") {
143
+ return uniqueSorted(result.stdout
144
+ .split(/\r?\n/u)
145
+ .map((line) => normalizeRepoPath(line))
146
+ .filter(Boolean));
147
+ }
148
+ // Retry once after a short delay — handles git lock-file contention
149
+ // that occurs when another process holds .git/index.lock.
150
+ if (attempt === 0 && result.status !== 0) {
151
+ spawnSync("sleep", ["0.5"], { encoding: "utf8" });
152
+ }
153
+ }
154
+ return [];
155
+ }
156
+ function readGitScalar(repoRoot, args) {
157
+ const result = spawnSync("git", args, {
158
+ cwd: repoRoot,
159
+ encoding: "utf8"
160
+ });
161
+ if (result.status !== 0 || typeof result.stdout !== "string") {
162
+ return undefined;
163
+ }
164
+ const value = result.stdout.trim();
165
+ return value.length > 0 ? value : undefined;
166
+ }
167
+ async function readRollbackSnapshot(repoRoot, filePath) {
168
+ const absolutePath = resolveRepoPath(repoRoot, filePath);
169
+ try {
170
+ const contents = await readFile(absolutePath);
171
+ return {
172
+ path: normalizeRepoPath(filePath),
173
+ existed: true,
174
+ encoding: "base64",
175
+ contentBase64: contents.toString("base64")
176
+ };
177
+ }
178
+ catch {
179
+ return {
180
+ path: normalizeRepoPath(filePath),
181
+ existed: false,
182
+ encoding: "base64"
183
+ };
184
+ }
185
+ }
186
+ async function restoreRollbackSnapshot(repoRoot, snapshot) {
187
+ const absolutePath = resolveRepoPath(repoRoot, snapshot.path);
188
+ if (!snapshot.existed || !snapshot.contentBase64) {
189
+ await rm(absolutePath, { recursive: true, force: true });
190
+ return;
191
+ }
192
+ await mkdir(dirname(absolutePath), { recursive: true });
193
+ await writeFile(absolutePath, Buffer.from(snapshot.contentBase64, "base64"));
194
+ }
195
+ function restoreTrackedFileFromHead(repoRoot, filePath) {
196
+ const result = spawnSync("git", ["restore", "--staged", "--worktree", "--source=HEAD", "--", filePath], {
197
+ cwd: repoRoot,
198
+ encoding: "utf8"
199
+ });
200
+ if (result.status === 0) {
201
+ return;
202
+ }
203
+ // Retry once after a short delay (git lock-file contention)
204
+ spawnSync("sleep", ["0.5"], { encoding: "utf8" });
205
+ const retry = spawnSync("git", ["restore", "--staged", "--worktree", "--source=HEAD", "--", filePath], { cwd: repoRoot, encoding: "utf8" });
206
+ if (retry.status === 0) {
207
+ return;
208
+ }
209
+ // Fallback: try git checkout which works on older git versions
210
+ const fallback = spawnSync("git", ["checkout", "HEAD", "--", filePath], { cwd: repoRoot, encoding: "utf8" });
211
+ if (fallback.status === 0) {
212
+ return;
213
+ }
214
+ throw new Error(result.stderr?.trim() ||
215
+ `git restore failed for ${filePath} (retry and checkout fallback also failed)`);
216
+ }
217
+ async function removeRepoPath(repoRoot, filePath) {
218
+ await rm(resolveRepoPath(repoRoot, filePath), { recursive: true, force: true });
219
+ }
220
+ function resolveRepoPath(repoRoot, filePath) {
221
+ const resolvedRoot = resolve(repoRoot);
222
+ const resolvedPath = resolve(resolvedRoot, filePath);
223
+ const relativePath = relative(resolvedRoot, resolvedPath);
224
+ if (relativePath.startsWith("..") || relativePath === "") {
225
+ throw new Error(`Refusing to access a rollback path outside repo root: ${filePath}`);
226
+ }
227
+ return resolvedPath;
228
+ }
229
+ function repoStateMatchesBoundary(state, boundary) {
230
+ return arraysEqual(state.trackedDirtyFiles, boundary.trackedDirtyFiles) &&
231
+ arraysEqual(state.untrackedFiles, boundary.untrackedFiles);
232
+ }
233
+ function arraysEqual(left, right) {
234
+ if (left.length !== right.length) {
235
+ return false;
236
+ }
237
+ return left.every((value, index) => value === right[index]);
238
+ }
239
+ function uniqueSorted(values) {
240
+ return [...new Set(values.map((value) => normalizeRepoPath(value)).filter(Boolean))].sort();
241
+ }
242
+ function normalizeRepoPath(value) {
243
+ return value.trim().replace(/\\/gu, "/");
244
+ }
245
+ function emptyRepoState() {
246
+ return {
247
+ trackedDirtyFiles: [],
248
+ untrackedFiles: []
249
+ };
250
+ }
251
+ function toErrorMessage(error) {
252
+ return error instanceof Error ? error.message : String(error);
253
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Route classification and compression for governed agent runs.
3
+ *
4
+ * Decides whether a task needs manager/planner orchestration or can go
5
+ * directly to a worker, based on task complexity signals. Prevents
6
+ * wasted coordination cost (Pre Work Burn) on simple tasks.
7
+ */
8
+ import type { RoutingMode, RoutingPolicy } from "../../../contracts/index.js";
9
+ export interface RouteDecision {
10
+ selectedMode: RoutingMode;
11
+ confidence: number;
12
+ expectedCostUsd: number;
13
+ expectedPreworkBurnPct: number;
14
+ reason: string[];
15
+ blockedSteps: string[];
16
+ compressed: boolean;
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;
26
+ }
27
+ export interface RouteClassificationInput {
28
+ objective: string;
29
+ verificationPlan: string[];
30
+ repoRoot?: string;
31
+ allowedPaths?: string[];
32
+ deniedPaths?: string[];
33
+ budgetUsd: number;
34
+ policy?: Partial<RoutingPolicy>;
35
+ /** Number of files the task references or is scoped to. */
36
+ scopedFileCount?: number;
37
+ /** Historical success rate for similar tasks with direct execution (0-1). */
38
+ historicalDirectSuccessRate?: number;
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
+ };
74
+ export declare function classifyRoute(input: RouteClassificationInput): RouteDecision;
75
+ /**
76
+ * Checks whether a run's Pre Work Burn exceeds the routing policy limits.
77
+ * Returns a block reason if exceeded, undefined if within bounds.
78
+ */
79
+ export declare function evaluatePreworkBurnPolicy(preworkCostUsd: number, totalCostUsd: number, policy?: Partial<RoutingPolicy>): {
80
+ exceeded: boolean;
81
+ reason?: string;
82
+ };