@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
@@ -0,0 +1,371 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { extname, join, posix, relative, resolve } from "node:path";
3
+ import { cloneContextGraphSnapshot } from "../contracts/index.js";
4
+ const TEXT_EXTENSIONS = new Set([
5
+ ".ts",
6
+ ".tsx",
7
+ ".js",
8
+ ".jsx",
9
+ ".mjs",
10
+ ".cjs",
11
+ ".json",
12
+ ".md",
13
+ ".yaml",
14
+ ".yml",
15
+ ".txt",
16
+ ".py",
17
+ ".go",
18
+ ".rs",
19
+ ".java",
20
+ ".sh"
21
+ ]);
22
+ const IGNORED_DIRS = new Set([
23
+ ".git",
24
+ "node_modules",
25
+ "dist",
26
+ "build",
27
+ ".next",
28
+ ".turbo",
29
+ "coverage",
30
+ ".pnpm-store",
31
+ ".npm-cache"
32
+ ]);
33
+ export async function buildContextGraphSnapshot(repoRoot, options = {}) {
34
+ const normalizedRoot = resolve(repoRoot);
35
+ const maxFiles = options.maxFiles ?? 600;
36
+ const maxFileBytes = options.maxFileBytes ?? 96_000;
37
+ const nodes = [];
38
+ const state = { count: 0, truncated: false };
39
+ await walkContextGraph(normalizedRoot, normalizedRoot, nodes, state, { maxFiles, maxFileBytes });
40
+ const edges = buildContextGraphEdges(nodes);
41
+ return cloneContextGraphSnapshot({
42
+ schemaVersion: "martin.context-graph.v1",
43
+ repoRoot: normalizedRoot,
44
+ createdAt: new Date().toISOString(),
45
+ nodeCount: nodes.length,
46
+ edgeCount: edges.length,
47
+ truncated: state.truncated,
48
+ nodes,
49
+ edges
50
+ });
51
+ }
52
+ export function queryContextGraph(snapshot, query) {
53
+ const terms = normalizeQueryTerms(query);
54
+ const pathPrefix = query.pathPrefix?.replace(/\\/gu, "/").replace(/^\.\/+/u, "");
55
+ const limit = query.limit ?? 8;
56
+ return snapshot.nodes
57
+ .filter((node) => {
58
+ if (query.kinds && !query.kinds.includes(node.kind)) {
59
+ return false;
60
+ }
61
+ if (pathPrefix && !node.path.startsWith(pathPrefix)) {
62
+ return false;
63
+ }
64
+ return true;
65
+ })
66
+ .map((node) => scoreContextNode(node, terms))
67
+ .filter((hit) => hit !== undefined)
68
+ .sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
69
+ .slice(0, limit);
70
+ }
71
+ async function walkContextGraph(repoRoot, currentDir, nodes, state, options) {
72
+ if (state.count >= options.maxFiles) {
73
+ state.truncated = true;
74
+ return;
75
+ }
76
+ const entries = await readdir(currentDir, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (state.count >= options.maxFiles) {
79
+ state.truncated = true;
80
+ break;
81
+ }
82
+ const absPath = join(currentDir, entry.name);
83
+ if (entry.isDirectory()) {
84
+ if (!IGNORED_DIRS.has(entry.name)) {
85
+ await walkContextGraph(repoRoot, absPath, nodes, state, options);
86
+ }
87
+ continue;
88
+ }
89
+ if (!entry.isFile()) {
90
+ continue;
91
+ }
92
+ const extension = extname(entry.name).toLowerCase();
93
+ if (!TEXT_EXTENSIONS.has(extension)) {
94
+ continue;
95
+ }
96
+ const content = await readFile(absPath, "utf8").catch(() => undefined);
97
+ if (!content || content.length > options.maxFileBytes) {
98
+ continue;
99
+ }
100
+ const relPath = relative(repoRoot, absPath).replace(/\\/gu, "/");
101
+ nodes.push(createContextNode(relPath, content));
102
+ state.count += 1;
103
+ }
104
+ }
105
+ function createContextNode(path, content) {
106
+ const kind = classifyContextNodeKind(path);
107
+ return {
108
+ nodeId: path,
109
+ path,
110
+ kind,
111
+ ...(inferLanguage(path) ? { language: inferLanguage(path) } : {}),
112
+ sizeBytes: Buffer.byteLength(content, "utf8"),
113
+ headings: extractHeadings(path, content),
114
+ symbols: extractSymbols(content),
115
+ keywords: extractKeywords(path, content),
116
+ imports: extractImports(path, content)
117
+ };
118
+ }
119
+ function buildContextGraphEdges(nodes) {
120
+ const paths = new Set(nodes.map((node) => node.path));
121
+ const edges = [];
122
+ for (const node of nodes) {
123
+ for (const importTarget of node.imports) {
124
+ const resolvedTarget = resolveContextImport(node.path, importTarget, paths);
125
+ if (resolvedTarget) {
126
+ edges.push({
127
+ fromNodeId: node.nodeId,
128
+ toNodeId: resolvedTarget,
129
+ kind: "imports"
130
+ });
131
+ }
132
+ }
133
+ if (node.kind === "document") {
134
+ const documentTargets = node.keywords
135
+ .map((keyword) => nodes.find((candidate) => candidate.path !== node.path && candidate.path.includes(keyword)))
136
+ .filter((candidate) => candidate !== undefined)
137
+ .slice(0, 3);
138
+ for (const target of documentTargets) {
139
+ edges.push({
140
+ fromNodeId: node.nodeId,
141
+ toNodeId: target.nodeId,
142
+ kind: "documents"
143
+ });
144
+ }
145
+ }
146
+ }
147
+ return dedupeEdges(edges);
148
+ }
149
+ function scoreContextNode(node, terms) {
150
+ if (terms.length === 0) {
151
+ return {
152
+ nodeId: node.nodeId,
153
+ path: node.path,
154
+ kind: node.kind,
155
+ score: 1,
156
+ matchedTerms: [],
157
+ reasons: ["default_match"]
158
+ };
159
+ }
160
+ let score = 0;
161
+ const matchedTerms = new Set();
162
+ const reasons = new Set();
163
+ for (const term of terms) {
164
+ if (node.path.toLowerCase().includes(term)) {
165
+ score += 6;
166
+ matchedTerms.add(term);
167
+ reasons.add("path");
168
+ }
169
+ if (node.keywords.includes(term)) {
170
+ score += 3;
171
+ matchedTerms.add(term);
172
+ reasons.add("keyword");
173
+ }
174
+ if (node.symbols.some((symbol) => symbol.toLowerCase().includes(term))) {
175
+ score += 5;
176
+ matchedTerms.add(term);
177
+ reasons.add("symbol");
178
+ }
179
+ if (node.headings.some((heading) => heading.toLowerCase().includes(term))) {
180
+ score += 4;
181
+ matchedTerms.add(term);
182
+ reasons.add("heading");
183
+ }
184
+ if (node.imports.some((importPath) => importPath.toLowerCase().includes(term))) {
185
+ score += 2;
186
+ matchedTerms.add(term);
187
+ reasons.add("import");
188
+ }
189
+ }
190
+ if (score === 0) {
191
+ return undefined;
192
+ }
193
+ return {
194
+ nodeId: node.nodeId,
195
+ path: node.path,
196
+ kind: node.kind,
197
+ score,
198
+ matchedTerms: [...matchedTerms],
199
+ reasons: [...reasons]
200
+ };
201
+ }
202
+ function normalizeQueryTerms(query) {
203
+ const rawTerms = [
204
+ ...(query.text ? tokenize(query.text) : []),
205
+ ...((query.terms ?? []).flatMap((term) => tokenize(term)))
206
+ ];
207
+ return [...new Set(rawTerms)];
208
+ }
209
+ function classifyContextNodeKind(path) {
210
+ const extension = extname(path).toLowerCase();
211
+ if (path.endsWith("package.json") || path.endsWith("tsconfig.json")) {
212
+ return "manifest";
213
+ }
214
+ if (extension === ".md" || extension === ".txt") {
215
+ return "document";
216
+ }
217
+ if (extension === ".sh") {
218
+ return "script";
219
+ }
220
+ if (extension === ".json" || extension === ".yaml" || extension === ".yml") {
221
+ return "config";
222
+ }
223
+ return "source_file";
224
+ }
225
+ function inferLanguage(path) {
226
+ const extension = extname(path).toLowerCase();
227
+ switch (extension) {
228
+ case ".ts":
229
+ case ".tsx":
230
+ return "typescript";
231
+ case ".js":
232
+ case ".jsx":
233
+ case ".mjs":
234
+ case ".cjs":
235
+ return "javascript";
236
+ case ".md":
237
+ return "markdown";
238
+ case ".json":
239
+ return "json";
240
+ case ".yaml":
241
+ case ".yml":
242
+ return "yaml";
243
+ case ".py":
244
+ return "python";
245
+ case ".sh":
246
+ return "shell";
247
+ default:
248
+ return undefined;
249
+ }
250
+ }
251
+ function extractHeadings(path, content) {
252
+ if (extname(path).toLowerCase() !== ".md") {
253
+ return [];
254
+ }
255
+ return content
256
+ .split(/\r?\n/gu)
257
+ .map((line) => line.trim())
258
+ .filter((line) => line.startsWith("#"))
259
+ .map((line) => line.replace(/^#+\s*/u, "").trim())
260
+ .filter(Boolean)
261
+ .slice(0, 12);
262
+ }
263
+ function extractSymbols(content) {
264
+ const out = new Set();
265
+ const patterns = [
266
+ /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)/gu,
267
+ /export\s+class\s+([A-Za-z0-9_]+)/gu,
268
+ /export\s+const\s+([A-Za-z0-9_]+)/gu,
269
+ /function\s+([A-Za-z0-9_]+)/gu,
270
+ /class\s+([A-Za-z0-9_]+)/gu,
271
+ /interface\s+([A-Za-z0-9_]+)/gu,
272
+ /type\s+([A-Za-z0-9_]+)/gu
273
+ ];
274
+ for (const pattern of patterns) {
275
+ for (const match of content.matchAll(pattern)) {
276
+ if (match[1]) {
277
+ out.add(match[1]);
278
+ }
279
+ }
280
+ }
281
+ return [...out].slice(0, 16);
282
+ }
283
+ function extractKeywords(path, content) {
284
+ const out = new Set();
285
+ for (const part of path.split(/[\/._-]+/u)) {
286
+ if (part.length >= 3) {
287
+ out.add(part.toLowerCase());
288
+ }
289
+ }
290
+ for (const token of tokenize(content.split(/\r?\n/gu).slice(0, 16).join(" "))) {
291
+ out.add(token);
292
+ }
293
+ return [...out].slice(0, 24);
294
+ }
295
+ function extractImports(path, content) {
296
+ if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(extname(path).toLowerCase())) {
297
+ return [];
298
+ }
299
+ const imports = new Set();
300
+ const patterns = [
301
+ /from\s+["']([^"']+)["']/gu,
302
+ /require\(\s*["']([^"']+)["']\s*\)/gu,
303
+ /import\(\s*["']([^"']+)["']\s*\)/gu
304
+ ];
305
+ for (const pattern of patterns) {
306
+ for (const match of content.matchAll(pattern)) {
307
+ const candidate = match[1]?.trim();
308
+ if (candidate) {
309
+ imports.add(candidate);
310
+ }
311
+ }
312
+ }
313
+ return [...imports].slice(0, 24);
314
+ }
315
+ function resolveContextImport(fromPath, importPath, paths) {
316
+ if (!importPath.startsWith(".")) {
317
+ return undefined;
318
+ }
319
+ const fromDirectory = fromPath.includes("/") ? fromPath.slice(0, fromPath.lastIndexOf("/")) : "";
320
+ const base = posix.normalize(posix.join(fromDirectory, importPath)).replace(/^\.\//u, "");
321
+ const extensionlessBase = extname(base) ? base.slice(0, -extname(base).length) : base;
322
+ const candidates = [
323
+ base,
324
+ extensionlessBase,
325
+ `${extensionlessBase}.ts`,
326
+ `${extensionlessBase}.tsx`,
327
+ `${extensionlessBase}.js`,
328
+ `${extensionlessBase}.jsx`,
329
+ `${extensionlessBase}.md`,
330
+ `${base}.ts`,
331
+ `${base}.tsx`,
332
+ `${base}.js`,
333
+ `${base}.jsx`,
334
+ `${base}.md`,
335
+ `${base}/index.ts`,
336
+ `${base}/index.tsx`,
337
+ `${base}/index.js`
338
+ ];
339
+ return candidates.find((candidate) => paths.has(candidate));
340
+ }
341
+ function dedupeEdges(edges) {
342
+ const seen = new Set();
343
+ return edges.filter((edge) => {
344
+ const key = `${edge.kind}:${edge.fromNodeId}:${edge.toNodeId}`;
345
+ if (seen.has(key)) {
346
+ return false;
347
+ }
348
+ seen.add(key);
349
+ return true;
350
+ });
351
+ }
352
+ function tokenize(input) {
353
+ const normalized = input.replace(/([a-z0-9])([A-Z])/gu, "$1 $2").toLowerCase();
354
+ const stopWords = new Set([
355
+ "the",
356
+ "and",
357
+ "for",
358
+ "with",
359
+ "from",
360
+ "that",
361
+ "this",
362
+ "into",
363
+ "only",
364
+ "keep",
365
+ "make",
366
+ "loop",
367
+ "then",
368
+ "when"
369
+ ]);
370
+ return [...new Set(normalized.match(/[a-z0-9_]{3,}/gu) ?? [])].filter((term) => !stopWords.has(term));
371
+ }
@@ -21,7 +21,7 @@ const POISON_PATTERNS = [
21
21
  */
22
22
  const IDENTITY_REDEFINITION_PATTERNS = [
23
23
  /\byou(?:'re|\s+are)\s+now\s+(?:a|an|the)\b(?!\s+(?:martin\s+loop|ai\s+coding\s+agent))/i,
24
- /\byou(?:'re|\s+are)\s+no\s+longer\s+(?!.*\b(?:martin\s+loop|an?\s+ai)\b)/i,
24
+ /\byou(?:'re|\s+are)\s+no\s+longer\s+(?!(?:martin\s+loop|an?\s+ai)\b)/i,
25
25
  /\bforget\s+(?:that\s+)?you(?:'re|\s+are)\s+martin\s+loop\b/i,
26
26
  /\b(?:pretend|imagine)\s+(?:that\s+)?you(?:'re|\s+are)\b/i,
27
27
  /\bact\s+as\s+(?:if\s+you(?:'re|\s+are)\s+)?(?:a|an)\s+(?:different|new|unrestricted|jailbroken)\b/i,
@@ -13,7 +13,7 @@ const IGNORED_DIRS = new Set([
13
13
  const MAX_FILE_BYTES = 64_000;
14
14
  const MAX_FILES = 500;
15
15
  export function resolveGroundingRoot(env = process.env) {
16
- return env["MARTIN_GROUNDING_DIR"]?.trim() ??
16
+ return env["MARTIN_GROUNDING_DIR"]?.trim() ||
17
17
  join(homedir(), ".martin", "grounding");
18
18
  }
19
19
  export async function loadOrBuildRepoGroundingIndex(repoRoot) {
@@ -0,0 +1,20 @@
1
+ import { type IdentityAttestation, type IdentityClaims, type IdentityToken } from "../contracts/index.js";
2
+ export interface LocalIdentityAuthority {
3
+ keyId: string;
4
+ algorithm: "ed25519";
5
+ privateKeyPem: string;
6
+ publicKeyPem: string;
7
+ }
8
+ export declare function createLocalIdentityAuthority(input?: {
9
+ keyId?: string;
10
+ }): LocalIdentityAuthority;
11
+ export declare function issueIdentityToken(input: {
12
+ authority: Pick<LocalIdentityAuthority, "keyId" | "algorithm" | "privateKeyPem">;
13
+ claims: IdentityClaims;
14
+ }): IdentityToken;
15
+ export declare function verifyIdentityToken(input: {
16
+ token: IdentityToken;
17
+ publicKeyPem: string;
18
+ now?: string;
19
+ }): IdentityAttestation;
20
+ export declare function hasIdentityScope(token: IdentityToken, requiredScope: string): boolean;
@@ -0,0 +1,86 @@
1
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
2
+ import { cloneIdentityAttestation, cloneIdentityClaims, cloneIdentityToken } from "../contracts/index.js";
3
+ export function createLocalIdentityAuthority(input = {}) {
4
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
5
+ return {
6
+ keyId: input.keyId ?? `martin-local-${Date.now().toString(36)}`,
7
+ algorithm: "ed25519",
8
+ privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
9
+ publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString()
10
+ };
11
+ }
12
+ export function issueIdentityToken(input) {
13
+ const claims = cloneIdentityClaims(input.claims);
14
+ const signer = createPrivateKey(input.authority.privateKeyPem);
15
+ const payload = serializeIdentityClaims(claims);
16
+ const signature = sign(null, payload, signer).toString("base64");
17
+ return cloneIdentityToken({
18
+ schemaVersion: "martin.identity.v1",
19
+ algorithm: input.authority.algorithm,
20
+ keyId: input.authority.keyId,
21
+ claims,
22
+ signature
23
+ });
24
+ }
25
+ export function verifyIdentityToken(input) {
26
+ const token = cloneIdentityToken(input.token);
27
+ const verifier = createPublicKey(input.publicKeyPem);
28
+ const payload = serializeIdentityClaims(token.claims);
29
+ const verifiedAt = input.now ?? new Date().toISOString();
30
+ if (token.schemaVersion !== "martin.identity.v1") {
31
+ return cloneIdentityAttestation({
32
+ valid: false,
33
+ verifiedAt,
34
+ reason: `Unsupported identity schema version '${token.schemaVersion}'.`,
35
+ token
36
+ });
37
+ }
38
+ const signatureValid = verify(null, payload, verifier, Buffer.from(token.signature, "base64"));
39
+ if (!signatureValid) {
40
+ return cloneIdentityAttestation({
41
+ valid: false,
42
+ verifiedAt,
43
+ reason: "Identity signature verification failed.",
44
+ token
45
+ });
46
+ }
47
+ const expiresAt = token.claims.expiresAt ? Date.parse(token.claims.expiresAt) : undefined;
48
+ if (expiresAt !== undefined && Number.isFinite(expiresAt) && expiresAt <= Date.parse(verifiedAt)) {
49
+ return cloneIdentityAttestation({
50
+ valid: false,
51
+ verifiedAt,
52
+ reason: "Identity token is expired.",
53
+ token
54
+ });
55
+ }
56
+ return cloneIdentityAttestation({
57
+ valid: true,
58
+ verifiedAt,
59
+ reason: `Identity token for role '${token.claims.role}' verified successfully.`,
60
+ token
61
+ });
62
+ }
63
+ export function hasIdentityScope(token, requiredScope) {
64
+ const required = requiredScope.trim().toLowerCase();
65
+ if (required.length === 0) {
66
+ return false;
67
+ }
68
+ return token.claims.scopes.some((scope) => scope.trim().toLowerCase() === required);
69
+ }
70
+ function serializeIdentityClaims(claims) {
71
+ return Buffer.from(JSON.stringify(claims, sortKeys), "utf8");
72
+ }
73
+ function sortKeys(_, value) {
74
+ if (Array.isArray(value)) {
75
+ return value;
76
+ }
77
+ if (value && typeof value === "object") {
78
+ return Object.keys(value)
79
+ .sort()
80
+ .reduce((sorted, key) => {
81
+ sorted[key] = value[key];
82
+ return sorted;
83
+ }, {});
84
+ }
85
+ return value;
86
+ }
@@ -9,6 +9,11 @@ export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluate
9
9
  export type { BudgetPreflightDecision, BudgetPreflightInput, CostGovernorState, EvidenceVectorInput, EvaluatedPatchDecision, ExitDecision, FailureAssessment, PatchDecisionInput, RecoveryDecision, RecoveryRecipe } from "./policy.js";
10
10
  export type { ResolvedExecutionProfile, SafetyLeashDecision, SafetyViolation } from "./leash.js";
11
11
  export type { GroundingScanResult, GroundingViolation, GroundingViolationKind, RepoGroundingHit, RepoGroundingIndex } from "./grounding.js";
12
+ export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
13
+ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
14
+ export type { LocalIdentityAuthority } from "./identity.js";
15
+ export { compileExecutionPolicy } from "./policy-compiler.js";
16
+ export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
12
17
  export { runContextIntegrityPrecheck } from "./context-integrity.js";
13
18
  export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
14
19
  export { compilePromptPacket } from "./compiler.js";
@@ -132,6 +137,8 @@ export interface AttemptPolicyDecision {
132
137
  allowed: boolean;
133
138
  reason: string;
134
139
  recommendedIntervention?: InterventionType;
140
+ trajectoryAssessment?: import("../contracts/index.js").TrajectoryAssessment;
141
+ circuitBreakDecision?: import("../contracts/index.js").CircuitBreakDecision;
135
142
  }
136
143
  /**
137
144
  * Admission gate — must pass before any attempt is executed.
@@ -7,7 +7,12 @@ import { captureRollbackBoundary, restoreRollbackBoundary } from "./rollback.js"
7
7
  import { compilePromptPacket } from "./compiler.js";
8
8
  import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
9
9
  import { runContextIntegrityPrecheck } from "./context-integrity.js";
10
+ import { decideCircuitBreak } from "./trajectory.js";
10
11
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, restoreRollbackBoundary };
12
+ export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
13
+ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
14
+ export { compileExecutionPolicy } from "./policy-compiler.js";
15
+ export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
11
16
  // ─── Context Integrity Pre-gate ──────────────────────────────────────────────
12
17
  export { runContextIntegrityPrecheck } from "./context-integrity.js";
13
18
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
@@ -22,12 +27,35 @@ export { compileAndPersistContext } from "./persistence/index.js";
22
27
  */
23
28
  export function evaluateAttemptPolicy(input) {
24
29
  const { request, projectedUsd } = input;
30
+ const trajectoryAttempts = request.previousAttempts.map((attempt) => ({
31
+ index: attempt.index,
32
+ summary: attempt.summary,
33
+ failureClass: attempt.failureClass
34
+ }));
35
+ const circuitBreakDecision = decideCircuitBreak({
36
+ objective: request.context.objective,
37
+ verificationPlan: request.context.verificationPlan,
38
+ attempts: trajectoryAttempts,
39
+ remainingIterations: request.context.remainingIterations
40
+ });
41
+ const trajectoryAssessment = circuitBreakDecision.assessment;
42
+ if (circuitBreakDecision.shouldStop) {
43
+ return {
44
+ allowed: false,
45
+ reason: circuitBreakDecision.reason,
46
+ recommendedIntervention: circuitBreakDecision.recommendedIntervention,
47
+ trajectoryAssessment,
48
+ circuitBreakDecision
49
+ };
50
+ }
25
51
  // Budget gate: reject if projected cost exceeds remaining
26
52
  if (projectedUsd > request.context.remainingBudgetUsd) {
27
53
  return {
28
54
  allowed: false,
29
55
  reason: `Projected cost $${projectedUsd} exceeds remaining budget $${request.context.remainingBudgetUsd}.`,
30
- recommendedIntervention: "stop_loop"
56
+ recommendedIntervention: "stop_loop",
57
+ trajectoryAssessment,
58
+ circuitBreakDecision
31
59
  };
32
60
  }
33
61
  // Iteration gate
@@ -35,7 +63,9 @@ export function evaluateAttemptPolicy(input) {
35
63
  return {
36
64
  allowed: false,
37
65
  reason: "No remaining iterations in the budget.",
38
- recommendedIntervention: "stop_loop"
66
+ recommendedIntervention: "stop_loop",
67
+ trajectoryAssessment,
68
+ circuitBreakDecision
39
69
  };
40
70
  }
41
71
  // Oscillation detection: A/B/A pattern in failure classes
@@ -49,7 +79,9 @@ export function evaluateAttemptPolicy(input) {
49
79
  return {
50
80
  allowed: false,
51
81
  reason: "Oscillating failure pattern detected. Escalating to human.",
52
- recommendedIntervention: "escalate_human"
82
+ recommendedIntervention: "escalate_human",
83
+ trajectoryAssessment,
84
+ circuitBreakDecision
53
85
  };
54
86
  }
55
87
  }
@@ -70,14 +102,18 @@ export function evaluateAttemptPolicy(input) {
70
102
  return {
71
103
  allowed: false,
72
104
  reason: "Materially repetitive attempts detected. Escalating to human.",
73
- recommendedIntervention: "escalate_human"
105
+ recommendedIntervention: "escalate_human",
106
+ trajectoryAssessment,
107
+ circuitBreakDecision
74
108
  };
75
109
  }
76
110
  }
77
111
  }
78
112
  return {
79
113
  allowed: true,
80
- reason: "Attempt admitted."
114
+ reason: "Attempt admitted.",
115
+ trajectoryAssessment,
116
+ circuitBreakDecision
81
117
  };
82
118
  }
83
119
  export function distillContext(loop, options = {}) {
@@ -278,7 +314,7 @@ export async function runMartin(input) {
278
314
  // re-enters subsequent prompts, matching the documented "tool output / test output" scope.
279
315
  const priorVerifierOutput = loop.events
280
316
  .filter((event) => event.type === "verification.completed")
281
- .flatMap((event) => event.payload.steps ?? [])
317
+ .flatMap((event) => event.payload?.steps ?? [])
282
318
  .map((step) => step.detail)
283
319
  .filter((detail) => Boolean(detail))
284
320
  .join("\n---\n");
@@ -453,7 +489,8 @@ export async function runMartin(input) {
453
489
  ...(failure?.failureClass ? { failureClass: failure.failureClass } : {}),
454
490
  ...(failure?.recommendedIntervention
455
491
  ? { intervention: failure.recommendedIntervention }
456
- : {})
492
+ : {}),
493
+ ...(failure?.diagnosticHint ? { diagnosticHint: failure.diagnosticHint } : {})
457
494
  };
458
495
  loop = {
459
496
  ...loop,
@@ -468,7 +505,7 @@ export async function runMartin(input) {
468
505
  estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
469
506
  }
470
507
  : {}),
471
- provenance: getUsageProvenance(result.usage),
508
+ provenance: mergeCostProvenance(loop.cost.provenance, getUsageProvenance(result.usage)),
472
509
  ...(result.usage.providerSettlement
473
510
  ? { providerSettlement: result.usage.providerSettlement }
474
511
  : {})
@@ -552,7 +589,7 @@ export async function runMartin(input) {
552
589
  actualUsd: loop.cost.actualUsd,
553
590
  remainingBudgetUsd: costState.remainingBudgetUsd,
554
591
  pressure: costState.pressure,
555
- provenance: getUsageProvenance(result.usage)
592
+ provenance: loop.cost.provenance
556
593
  }
557
594
  }, { now: now(), idFactory });
558
595
  if (input.store) {
@@ -1136,6 +1173,23 @@ function getUsageProvenance(usage) {
1136
1173
  }
1137
1174
  return "actual";
1138
1175
  }
1176
+ const COST_PROVENANCE_RANK = {
1177
+ unavailable: 0,
1178
+ estimated: 1,
1179
+ actual: 2
1180
+ };
1181
+ /**
1182
+ * Aggregates cost provenance across attempts. The cumulative loop provenance
1183
+ * can only be as trustworthy as its weakest attempt: if any attempt's cost was
1184
+ * estimated or unavailable, the cumulative total must reflect that, even if a
1185
+ * later attempt reports an authoritative actual cost.
1186
+ */
1187
+ function mergeCostProvenance(previous, current) {
1188
+ if (previous === undefined) {
1189
+ return current;
1190
+ }
1191
+ return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
1192
+ }
1139
1193
  function resolveChangedFiles(result, repoRoot) {
1140
1194
  if (result.execution?.changedFiles !== undefined) {
1141
1195
  return result.execution.changedFiles;
@@ -39,7 +39,7 @@ const BLOCKED_PATTERNS = [
39
39
  */
40
40
  function commandContainsDestructiveRemoval(command) {
41
41
  const normalized = command.replace(/\$\{?IFS\}?/giu, " ").toLowerCase();
42
- const rmInvocation = /(?:^|[\s;&|`(])(?:\/(?:usr\/(?:local\/)?)?s?bin\/)?rm\s+([^\n;|`]+)/giu;
42
+ const rmInvocation = /(?:^|[\s;&|`(])(?:[^\s;&|`(]+\/)?rm\s+([^\n;|`]+)/giu;
43
43
  let match;
44
44
  while ((match = rmInvocation.exec(normalized)) !== null) {
45
45
  const args = match[1] ?? "";
@@ -21,6 +21,7 @@ export interface StoredReceiptIntegrityMaterial {
21
21
  chain: ReceiptIntegrityChainEntry[];
22
22
  signatureHmacSha256: string;
23
23
  }
24
+ export declare function resolveReceiptIntegrityRoot(env?: NodeJS.ProcessEnv): string;
24
25
  export declare function writeReceiptIntegrityMaterial(input: {
25
26
  runId: string;
26
27
  runsRoot: string;