@martinloop/mcp 0.2.5 → 0.3.0

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 (70) hide show
  1. package/README.md +40 -132
  2. package/dist/discovery-metadata.d.ts +10 -5
  3. package/dist/discovery-metadata.js +95 -5
  4. package/dist/package-version.d.ts +1 -1
  5. package/dist/package-version.js +1 -1
  6. package/dist/prompts.d.ts +1 -1
  7. package/dist/prompts.js +93 -1
  8. package/dist/resources.d.ts +9 -1
  9. package/dist/resources.js +247 -16
  10. package/dist/server-validation.d.ts +2 -1
  11. package/dist/server-validation.js +124 -0
  12. package/dist/server.js +379 -5
  13. package/dist/tools/doctor.d.ts +14 -1
  14. package/dist/tools/doctor.js +43 -8
  15. package/dist/tools/eval.d.ts +24 -0
  16. package/dist/tools/eval.js +66 -0
  17. package/dist/tools/get-run.d.ts +2 -0
  18. package/dist/tools/get-run.js +2 -1
  19. package/dist/tools/get-status.d.ts +8 -0
  20. package/dist/tools/get-status.js +18 -0
  21. package/dist/tools/get-verification-results.d.ts +2 -0
  22. package/dist/tools/get-verification-results.js +2 -1
  23. package/dist/tools/logs.d.ts +25 -0
  24. package/dist/tools/logs.js +49 -0
  25. package/dist/tools/plan.d.ts +20 -0
  26. package/dist/tools/plan.js +10 -0
  27. package/dist/tools/pr-tools.d.ts +31 -0
  28. package/dist/tools/pr-tools.js +112 -0
  29. package/dist/tools/preflight.d.ts +24 -1
  30. package/dist/tools/preflight.js +47 -7
  31. package/dist/tools/run-controls.d.ts +36 -0
  32. package/dist/tools/run-controls.js +88 -0
  33. package/dist/tools/run-dossier.d.ts +16 -0
  34. package/dist/tools/run-dossier.js +64 -2
  35. package/dist/tools/run-loop.d.ts +3 -2
  36. package/dist/tools/run-loop.js +52 -13
  37. package/dist/tools/tool-errors.d.ts +1 -1
  38. package/dist/tools/tool-errors.js +1 -1
  39. package/dist/tools/tool-support.d.ts +6 -3
  40. package/dist/tools/tool-support.js +37 -3
  41. package/dist/tools/workflow-governance.d.ts +133 -0
  42. package/dist/tools/workflow-governance.js +581 -0
  43. package/dist/vendor/adapters/claude-cli.d.ts +25 -0
  44. package/dist/vendor/adapters/claude-cli.js +279 -19
  45. package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
  46. package/dist/vendor/adapters/cli-bridge.js +58 -9
  47. package/dist/vendor/adapters/codex-launcher.d.ts +44 -0
  48. package/dist/vendor/adapters/codex-launcher.js +247 -0
  49. package/dist/vendor/adapters/index.d.ts +4 -2
  50. package/dist/vendor/adapters/index.js +4 -1
  51. package/dist/vendor/adapters/openai-compatible.d.ts +62 -0
  52. package/dist/vendor/adapters/openai-compatible.js +267 -0
  53. package/dist/vendor/adapters/runtime-support.d.ts +3 -0
  54. package/dist/vendor/adapters/runtime-support.js +8 -1
  55. package/dist/vendor/adapters/verifier-only.js +4 -3
  56. package/dist/vendor/contracts/index.d.ts +39 -0
  57. package/dist/vendor/contracts/index.js +2 -0
  58. package/dist/vendor/core/index.d.ts +23 -3
  59. package/dist/vendor/core/index.js +88 -15
  60. package/dist/vendor/core/persistence/index.d.ts +2 -0
  61. package/dist/vendor/core/persistence/index.js +1 -0
  62. package/dist/vendor/core/persistence/integrity.d.ts +38 -0
  63. package/dist/vendor/core/persistence/integrity.js +239 -0
  64. package/dist/vendor/core/persistence/store.d.ts +7 -0
  65. package/dist/vendor/core/persistence/store.js +25 -1
  66. package/dist/vendor/core/policy.d.ts +9 -0
  67. package/dist/workflow-state.d.ts +25 -0
  68. package/dist/workflow-state.js +102 -0
  69. package/package.json +3 -3
  70. package/server.json +2 -2
@@ -0,0 +1,581 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { DEFAULT_BUDGET } from "../vendor/contracts/index.js";
5
+ import { detectCliAvailability } from "./tool-support.js";
6
+ const HOST_COMMANDS = {
7
+ claude: "claude",
8
+ codex: "codex",
9
+ cursor: "cursor",
10
+ gemini: "gemini"
11
+ };
12
+ const POLICY_PACKS = {
13
+ "solo-founder": {
14
+ name: "solo-founder",
15
+ summary: "Fast local defaults with light scope controls and proof-first review.",
16
+ defaultAllowedPaths: ["src/**", "tests/**", "docs/**"],
17
+ defaultBlockedPaths: [".env", ".env.*", ".git/**", "node_modules/**", "dist/**"],
18
+ dossierExpectations: ["objective", "files touched", "verifier result", "next action"],
19
+ requireApprovalAtOrAbove: "high"
20
+ },
21
+ "startup-team": {
22
+ name: "startup-team",
23
+ summary: "Team-safe defaults with verifier expectations and branch hygiene.",
24
+ defaultAllowedPaths: ["src/**", "tests/**", "docs/**"],
25
+ defaultBlockedPaths: [
26
+ ".env",
27
+ ".env.*",
28
+ ".git/**",
29
+ "infra/**",
30
+ "deploy/**",
31
+ "node_modules/**"
32
+ ],
33
+ dossierExpectations: [
34
+ "objective",
35
+ "scope",
36
+ "commands run",
37
+ "verifier result",
38
+ "risk flags",
39
+ "review focus"
40
+ ],
41
+ requireApprovalAtOrAbove: "medium"
42
+ },
43
+ "enterprise-strict": {
44
+ name: "enterprise-strict",
45
+ summary: "Fail-closed local policy that blocks infra, dependency, and secret-risk paths by default.",
46
+ defaultAllowedPaths: ["src/**", "tests/**"],
47
+ defaultBlockedPaths: [
48
+ ".env",
49
+ ".env.*",
50
+ ".git/**",
51
+ "infra/**",
52
+ "deploy/**",
53
+ ".github/workflows/**",
54
+ "package.json",
55
+ "pnpm-lock.yaml",
56
+ "package-lock.json",
57
+ "yarn.lock"
58
+ ],
59
+ dossierExpectations: [
60
+ "objective",
61
+ "scope contract",
62
+ "tests",
63
+ "risk score",
64
+ "review blockers",
65
+ "rollback evidence"
66
+ ],
67
+ requireApprovalAtOrAbove: "medium"
68
+ },
69
+ "oss-maintainer": {
70
+ name: "oss-maintainer",
71
+ summary: "OSS-focused policy with docs, tests, and reviewable diff discipline.",
72
+ defaultAllowedPaths: ["packages/**", "tests/**", "docs/**", "README.md", "CHANGELOG.md"],
73
+ defaultBlockedPaths: [".env", ".env.*", ".git/**", "infra/**", "deploy/**"],
74
+ dossierExpectations: [
75
+ "objective",
76
+ "diff summary",
77
+ "tests",
78
+ "risk flags",
79
+ "release notes impact"
80
+ ],
81
+ requireApprovalAtOrAbove: "medium"
82
+ },
83
+ "security-sensitive": {
84
+ name: "security-sensitive",
85
+ summary: "Local security policy with strict approvals around auth, secrets, and privileged files.",
86
+ defaultAllowedPaths: ["src/**", "tests/**"],
87
+ defaultBlockedPaths: [
88
+ ".env",
89
+ ".env.*",
90
+ ".git/**",
91
+ "infra/**",
92
+ "deploy/**",
93
+ ".github/workflows/**",
94
+ "package.json",
95
+ "pnpm-lock.yaml",
96
+ "package-lock.json",
97
+ "yarn.lock",
98
+ "auth/**",
99
+ "payments/**",
100
+ "secrets/**"
101
+ ],
102
+ dossierExpectations: [
103
+ "objective",
104
+ "scope contract",
105
+ "security review",
106
+ "tests",
107
+ "risk score",
108
+ "human approval receipt"
109
+ ],
110
+ requireApprovalAtOrAbove: "medium"
111
+ }
112
+ };
113
+ export function inspectRepoSignals(workingDirectory) {
114
+ const packageScripts = readPackageScripts(workingDirectory);
115
+ const packageManager = detectPackageManager(workingDirectory);
116
+ const frameworks = detectFrameworks(workingDirectory, packageScripts);
117
+ const languages = detectLanguages(workingDirectory, frameworks);
118
+ const verifiers = detectVerifierCommands(packageScripts, packageManager);
119
+ return {
120
+ workingDirectory,
121
+ packageManager,
122
+ languages,
123
+ frameworks,
124
+ verifiers,
125
+ packageScripts,
126
+ git: detectGitState(workingDirectory),
127
+ sensitivePaths: detectSensitivePaths(workingDirectory),
128
+ availableHosts: {
129
+ claude: detectCliAvailability(HOST_COMMANDS.claude),
130
+ codex: detectCliAvailability(HOST_COMMANDS.codex),
131
+ cursor: detectCliAvailability(HOST_COMMANDS.cursor),
132
+ gemini: detectCliAvailability(HOST_COMMANDS.gemini)
133
+ }
134
+ };
135
+ }
136
+ export function buildReadinessReport(signals, runStore) {
137
+ const missingSafeguards = [];
138
+ if (!signals.git.available || !signals.git.isRepo) {
139
+ missingSafeguards.push("Git repository context is unavailable.");
140
+ }
141
+ if (signals.git.isRepo && !signals.git.clean) {
142
+ missingSafeguards.push("Working tree is dirty; agent edits may overlap uncommitted work.");
143
+ }
144
+ if (signals.verifiers.defaultPlan.length === 0) {
145
+ missingSafeguards.push("No test/lint/build verifier plan was detected.");
146
+ }
147
+ if (!runStore.exists) {
148
+ missingSafeguards.push("Martin runs root does not exist yet.");
149
+ }
150
+ if (!signals.git.branch || /^(main|master)$/u.test(signals.git.branch)) {
151
+ missingSafeguards.push("Work is not isolated on a feature branch.");
152
+ }
153
+ let score = 100;
154
+ score -= missingSafeguards.length * 12;
155
+ if (signals.frameworks.length === 0) {
156
+ score -= 8;
157
+ }
158
+ if (!signals.availableHosts.claude.available && !signals.availableHosts.codex.available) {
159
+ score -= 18;
160
+ }
161
+ score = Math.max(0, Math.min(100, score));
162
+ const level = score >= 80 ? "low" : score >= 55 ? "medium" : "high";
163
+ return {
164
+ score,
165
+ level,
166
+ missingSafeguards,
167
+ repo: {
168
+ git: signals.git,
169
+ packageManager: signals.packageManager,
170
+ languages: signals.languages,
171
+ frameworks: signals.frameworks
172
+ },
173
+ safeguards: {
174
+ verifierDetected: signals.verifiers.defaultPlan.length > 0,
175
+ repoScoped: signals.git.isRepo,
176
+ branchSafe: Boolean(signals.git.branch && !/^(main|master)$/u.test(signals.git.branch)),
177
+ runStoreHealthy: runStore.exists
178
+ },
179
+ availableHosts: signals.availableHosts
180
+ };
181
+ }
182
+ export function buildPolicyPackDefinition(policyPack, signals) {
183
+ const name = policyPack ?? inferPolicyPack(signals);
184
+ const base = POLICY_PACKS[name];
185
+ return {
186
+ ...base,
187
+ defaultVerifiers: signals.verifiers.defaultPlan.length > 0
188
+ ? signals.verifiers.defaultPlan
189
+ : fallbackVerifierPlan(signals.packageManager)
190
+ };
191
+ }
192
+ export function buildPlanProposal(workingDirectory, overrides) {
193
+ const signals = inspectRepoSignals(workingDirectory);
194
+ const policy = buildPolicyPackDefinition(overrides.policyPack, signals);
195
+ const scope = inferScopeFromObjective(overrides.objective, policy, overrides);
196
+ const estimatedBudget = buildBudget(overrides, signals);
197
+ const risk = assessRunRisk({
198
+ objective: overrides.objective,
199
+ context: overrides.context,
200
+ allowedPaths: scope.allowedPaths,
201
+ blockedPaths: scope.blockedPaths,
202
+ verifiers: selectVerifiers(policy, overrides.verificationPlan),
203
+ signals
204
+ });
205
+ const approvalRecommendation = risk.level === "high"
206
+ ? "required"
207
+ : risk.level === "medium"
208
+ ? "recommended"
209
+ : "not_required";
210
+ return {
211
+ objective: overrides.objective,
212
+ implementationSummary: buildImplementationSummary(overrides.objective, scope.allowedPaths),
213
+ proposedFileScope: scope,
214
+ proposedVerifiers: selectVerifiers(policy, overrides.verificationPlan),
215
+ estimatedBudget,
216
+ risk,
217
+ approvalRecommendation,
218
+ policyPack: policy,
219
+ nextSteps: [
220
+ "Review the proposed scope and risk reasons.",
221
+ "Run martin_preflight with the same objective, policy pack, and verifier plan.",
222
+ "Execute martin_run only after the run contract looks safe and reviewable."
223
+ ]
224
+ };
225
+ }
226
+ export function buildRunContract(workingDirectory, overrides) {
227
+ const plan = buildPlanProposal(workingDirectory, overrides);
228
+ return {
229
+ objective: overrides.objective,
230
+ ...(overrides.context ? { context: overrides.context } : {}),
231
+ allowedPaths: plan.proposedFileScope.allowedPaths,
232
+ blockedPaths: plan.proposedFileScope.blockedPaths,
233
+ budget: plan.estimatedBudget,
234
+ verifiers: plan.proposedVerifiers,
235
+ risk: plan.risk,
236
+ policyPack: plan.policyPack.name,
237
+ requiresApproval: plan.approvalRecommendation === "required" ||
238
+ shouldRequireApproval(plan.policyPack.requireApprovalAtOrAbove, plan.risk.level)
239
+ };
240
+ }
241
+ export function assessRunRisk(input) {
242
+ const reasons = [];
243
+ let score = 12;
244
+ const text = `${input.objective} ${input.context ?? ""}`.toLowerCase();
245
+ if (input.signals.git.isRepo && !input.signals.git.clean) {
246
+ score += 18;
247
+ reasons.push("Repository has uncommitted changes.");
248
+ }
249
+ if (input.verifiers.length === 0) {
250
+ score += 22;
251
+ reasons.push("No verifier commands are configured for this run.");
252
+ }
253
+ if (input.allowedPaths.length === 0) {
254
+ score += 15;
255
+ reasons.push("No edit allowlist was provided.");
256
+ }
257
+ if (/(auth|login|permission|secret|token|payment|billing|deploy|infra|migration)/u.test(text)) {
258
+ score += 26;
259
+ reasons.push("Objective touches sensitive auth, secret, payment, or deployment concerns.");
260
+ }
261
+ if (/(dependency|package\.json|lockfile|upgrade|install)/u.test(text)) {
262
+ score += 14;
263
+ reasons.push("Objective may require dependency or lockfile changes.");
264
+ }
265
+ if (input.blockedPaths.some((candidate) => /package\.json|lock|workflow|infra/u.test(candidate))) {
266
+ score += 6;
267
+ reasons.push("Policy pack blocks high-risk repo surfaces by default.");
268
+ }
269
+ if (input.signals.sensitivePaths.length > 0 && input.allowedPaths.some((candidate) => candidate === "**" || candidate === "*")) {
270
+ score += 10;
271
+ reasons.push("Wide edit scope overlaps with sensitive repo areas.");
272
+ }
273
+ score = Math.max(0, Math.min(100, score));
274
+ const level = score >= 70 ? "high" : score >= 40 ? "medium" : "low";
275
+ return {
276
+ score,
277
+ level,
278
+ reasons,
279
+ recommendedAction: level === "high"
280
+ ? "require_human_approval"
281
+ : level === "medium"
282
+ ? "review"
283
+ : "proceed"
284
+ };
285
+ }
286
+ export function buildRepoRiskMap(signals) {
287
+ return {
288
+ workingDirectory: signals.workingDirectory,
289
+ packageManager: signals.packageManager,
290
+ frameworks: signals.frameworks,
291
+ sensitivePaths: signals.sensitivePaths,
292
+ recommendedPolicyPack: inferPolicyPack(signals)
293
+ };
294
+ }
295
+ function detectPackageManager(workingDirectory) {
296
+ if (existsSync(path.join(workingDirectory, "pnpm-lock.yaml"))) {
297
+ return "pnpm";
298
+ }
299
+ if (existsSync(path.join(workingDirectory, "package-lock.json"))) {
300
+ return "npm";
301
+ }
302
+ if (existsSync(path.join(workingDirectory, "yarn.lock"))) {
303
+ return "yarn";
304
+ }
305
+ if (existsSync(path.join(workingDirectory, "bun.lockb")) || existsSync(path.join(workingDirectory, "bun.lock"))) {
306
+ return "bun";
307
+ }
308
+ return "unknown";
309
+ }
310
+ function readPackageScripts(workingDirectory) {
311
+ const packageJsonPath = path.join(workingDirectory, "package.json");
312
+ if (!existsSync(packageJsonPath)) {
313
+ return {};
314
+ }
315
+ try {
316
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
317
+ return parsed.scripts ?? {};
318
+ }
319
+ catch {
320
+ return {};
321
+ }
322
+ }
323
+ function detectFrameworks(workingDirectory, scripts) {
324
+ const packageJsonPath = path.join(workingDirectory, "package.json");
325
+ const frameworks = new Set();
326
+ if (existsSync(path.join(workingDirectory, "next.config.js")) || existsSync(path.join(workingDirectory, "next.config.mjs"))) {
327
+ frameworks.add("Next.js");
328
+ }
329
+ if (existsSync(path.join(workingDirectory, "vite.config.ts")) || existsSync(path.join(workingDirectory, "vite.config.js"))) {
330
+ frameworks.add("Vite");
331
+ }
332
+ if (existsSync(packageJsonPath)) {
333
+ try {
334
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
335
+ const deps = {
336
+ ...(parsed.dependencies ?? {}),
337
+ ...(parsed.devDependencies ?? {})
338
+ };
339
+ if ("next" in deps) {
340
+ frameworks.add("Next.js");
341
+ }
342
+ if ("react" in deps) {
343
+ frameworks.add("React");
344
+ }
345
+ if ("vitest" in deps) {
346
+ frameworks.add("Vitest");
347
+ }
348
+ if ("jest" in deps) {
349
+ frameworks.add("Jest");
350
+ }
351
+ if ("typescript" in deps || existsSync(path.join(workingDirectory, "tsconfig.json"))) {
352
+ frameworks.add("TypeScript");
353
+ }
354
+ }
355
+ catch {
356
+ // ignore malformed package metadata here; doctor surfaces the gap elsewhere
357
+ }
358
+ }
359
+ if (Object.keys(scripts).some((key) => key.startsWith("test"))) {
360
+ frameworks.add("Scripted verification");
361
+ }
362
+ return [...frameworks];
363
+ }
364
+ function detectLanguages(workingDirectory, frameworks) {
365
+ const languages = new Set();
366
+ if (existsSync(path.join(workingDirectory, "tsconfig.json")) || frameworks.includes("TypeScript")) {
367
+ languages.add("TypeScript");
368
+ }
369
+ if (existsSync(path.join(workingDirectory, "package.json"))) {
370
+ languages.add("JavaScript");
371
+ }
372
+ if (existsSync(path.join(workingDirectory, "pyproject.toml")) || existsSync(path.join(workingDirectory, "requirements.txt"))) {
373
+ languages.add("Python");
374
+ }
375
+ if (existsSync(path.join(workingDirectory, "go.mod"))) {
376
+ languages.add("Go");
377
+ }
378
+ if (existsSync(path.join(workingDirectory, "Cargo.toml"))) {
379
+ languages.add("Rust");
380
+ }
381
+ return [...languages];
382
+ }
383
+ function detectVerifierCommands(scripts, packageManager) {
384
+ const prefix = packageManager === "pnpm"
385
+ ? "pnpm"
386
+ : packageManager === "yarn"
387
+ ? "yarn"
388
+ : packageManager === "bun"
389
+ ? "bun run"
390
+ : "npm run";
391
+ const test = [];
392
+ const lint = [];
393
+ const build = [];
394
+ for (const key of Object.keys(scripts)) {
395
+ if (/^test($|:|-)/u.test(key)) {
396
+ test.push(commandForScript(prefix, key));
397
+ }
398
+ if (/^lint($|:|-)/u.test(key)) {
399
+ lint.push(commandForScript(prefix, key));
400
+ }
401
+ if (/^build($|:|-)/u.test(key)) {
402
+ build.push(commandForScript(prefix, key));
403
+ }
404
+ }
405
+ const defaultPlan = [...test.slice(0, 1), ...lint.slice(0, 1), ...build.slice(0, 1)];
406
+ return { test, lint, build, defaultPlan };
407
+ }
408
+ function detectGitState(workingDirectory) {
409
+ const availability = spawnSync("git", ["--version"], {
410
+ cwd: workingDirectory,
411
+ encoding: "utf8",
412
+ stdio: ["ignore", "pipe", "pipe"]
413
+ });
414
+ if (availability.status !== 0) {
415
+ return {
416
+ available: false,
417
+ isRepo: false,
418
+ clean: false
419
+ };
420
+ }
421
+ const isRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
422
+ cwd: workingDirectory,
423
+ encoding: "utf8",
424
+ stdio: ["ignore", "pipe", "pipe"]
425
+ });
426
+ if (isRepo.status !== 0 || !/true/u.test(isRepo.stdout ?? "")) {
427
+ return {
428
+ available: true,
429
+ isRepo: false,
430
+ clean: false
431
+ };
432
+ }
433
+ const branch = spawnSync("git", ["branch", "--show-current"], {
434
+ cwd: workingDirectory,
435
+ encoding: "utf8",
436
+ stdio: ["ignore", "pipe", "pipe"]
437
+ }).stdout.trim();
438
+ const status = spawnSync("git", ["status", "--porcelain", "--branch"], {
439
+ cwd: workingDirectory,
440
+ encoding: "utf8",
441
+ stdio: ["ignore", "pipe", "pipe"]
442
+ });
443
+ const statusLines = (status.stdout ?? "")
444
+ .split(/\r?\n/u)
445
+ .map((line) => line.trim())
446
+ .filter(Boolean);
447
+ const dirty = statusLines.some((line) => !line.startsWith("##"));
448
+ const header = statusLines.find((line) => line.startsWith("##"));
449
+ const upstream = header?.match(/\.\.\.([^\s[]+)/u)?.[1];
450
+ const ahead = parseCount(header, /ahead (\d+)/u);
451
+ const behind = parseCount(header, /behind (\d+)/u);
452
+ return {
453
+ available: true,
454
+ isRepo: true,
455
+ clean: !dirty,
456
+ ...(branch ? { branch } : {}),
457
+ ...(upstream ? { upstream } : {}),
458
+ ...(ahead !== undefined ? { ahead } : {}),
459
+ ...(behind !== undefined ? { behind } : {})
460
+ };
461
+ }
462
+ function detectSensitivePaths(workingDirectory) {
463
+ const candidates = [
464
+ ".env",
465
+ ".env.local",
466
+ "infra",
467
+ "deploy",
468
+ ".github/workflows",
469
+ "package.json",
470
+ "pnpm-lock.yaml",
471
+ "package-lock.json",
472
+ "yarn.lock",
473
+ "Dockerfile",
474
+ "docker-compose.yml",
475
+ "auth",
476
+ "payments"
477
+ ];
478
+ return candidates.filter((candidate) => existsSync(path.join(workingDirectory, candidate)));
479
+ }
480
+ function inferPolicyPack(signals) {
481
+ const sensitive = signals.sensitivePaths.join(" ").toLowerCase();
482
+ if (/(auth|payment|workflow|deploy|infra|docker)/u.test(sensitive)) {
483
+ return "security-sensitive";
484
+ }
485
+ if (/README|CHANGELOG/u.test(Object.keys(signals.packageScripts).join(" "))) {
486
+ return "oss-maintainer";
487
+ }
488
+ return signals.git.isRepo && signals.git.clean ? "startup-team" : "solo-founder";
489
+ }
490
+ function inferScopeFromObjective(objective, policy, overrides) {
491
+ if ((overrides.allowedPaths?.length ?? 0) > 0 || (overrides.deniedPaths?.length ?? 0) > 0) {
492
+ return {
493
+ allowedPaths: overrides.allowedPaths ?? [],
494
+ blockedPaths: overrides.deniedPaths ?? []
495
+ };
496
+ }
497
+ const normalized = objective.toLowerCase();
498
+ if (/(readme|docs|changelog|release notes)/u.test(normalized)) {
499
+ return {
500
+ allowedPaths: ["docs/**", "README.md", "CHANGELOG.md"],
501
+ blockedPaths: [...policy.defaultBlockedPaths, "src/**"]
502
+ };
503
+ }
504
+ if (/(test|verifier|spec|coverage)/u.test(normalized)) {
505
+ return {
506
+ allowedPaths: ["src/**", "tests/**"],
507
+ blockedPaths: policy.defaultBlockedPaths
508
+ };
509
+ }
510
+ if (/(mcp|cli|package)/u.test(normalized)) {
511
+ return {
512
+ allowedPaths: ["packages/**", "tests/**", "docs/**"],
513
+ blockedPaths: policy.defaultBlockedPaths
514
+ };
515
+ }
516
+ return {
517
+ allowedPaths: [...policy.defaultAllowedPaths],
518
+ blockedPaths: [...policy.defaultBlockedPaths]
519
+ };
520
+ }
521
+ function buildBudget(overrides, signals) {
522
+ const defaultCommands = signals.verifiers.defaultPlan.length > 0 ? 12 : 8;
523
+ return {
524
+ maxUsd: overrides.maxUsd ?? DEFAULT_BUDGET.maxUsd,
525
+ softLimitUsd: Math.min(overrides.maxUsd ?? DEFAULT_BUDGET.maxUsd, DEFAULT_BUDGET.softLimitUsd),
526
+ maxIterations: overrides.maxIterations ?? DEFAULT_BUDGET.maxIterations,
527
+ maxTokens: overrides.maxTokens ?? DEFAULT_BUDGET.maxTokens,
528
+ maxMinutes: overrides.maxMinutes ?? 20,
529
+ maxFilesChanged: overrides.maxFilesChanged ?? 8,
530
+ maxCommands: overrides.maxCommands ?? defaultCommands
531
+ };
532
+ }
533
+ function selectVerifiers(policy, explicitVerificationPlan) {
534
+ return explicitVerificationPlan && explicitVerificationPlan.length > 0
535
+ ? explicitVerificationPlan
536
+ : policy.defaultVerifiers;
537
+ }
538
+ function buildImplementationSummary(objective, allowedPaths) {
539
+ const scopeHint = allowedPaths.length > 0
540
+ ? `Limit work to ${allowedPaths.slice(0, 3).join(", ")}${allowedPaths.length > 3 ? " and adjacent tests/docs" : ""}.`
541
+ : "Establish a narrower file scope before execution.";
542
+ return `${objective.trim()} ${scopeHint}`.trim();
543
+ }
544
+ function commandForScript(prefix, name) {
545
+ if (prefix === "pnpm") {
546
+ return name === "test" ? "pnpm test" : `pnpm ${name}`;
547
+ }
548
+ if (prefix === "yarn") {
549
+ return name === "test" ? "yarn test" : `yarn ${name}`;
550
+ }
551
+ if (prefix === "bun run") {
552
+ return `bun run ${name}`;
553
+ }
554
+ return `npm run ${name}`;
555
+ }
556
+ function fallbackVerifierPlan(packageManager) {
557
+ switch (packageManager) {
558
+ case "pnpm":
559
+ return ["pnpm test", "pnpm lint", "pnpm build"];
560
+ case "yarn":
561
+ return ["yarn test", "yarn lint", "yarn build"];
562
+ case "bun":
563
+ return ["bun run test", "bun run lint", "bun run build"];
564
+ case "npm":
565
+ return ["npm test", "npm run lint", "npm run build"];
566
+ default:
567
+ return [];
568
+ }
569
+ }
570
+ function shouldRequireApproval(threshold, level) {
571
+ const ordering = ["low", "medium", "high"];
572
+ return ordering.indexOf(level) >= ordering.indexOf(threshold);
573
+ }
574
+ function parseCount(value, pattern) {
575
+ const match = value?.match(pattern)?.[1];
576
+ if (!match) {
577
+ return undefined;
578
+ }
579
+ const parsed = Number.parseInt(match, 10);
580
+ return Number.isFinite(parsed) ? parsed : undefined;
581
+ }
@@ -80,6 +80,21 @@ export interface CodexCliAdapterOptions {
80
80
  extraArgs?: string[];
81
81
  spawnImpl?: SpawnLike;
82
82
  }
83
+ export interface GeminiCliAdapterOptions {
84
+ workingDirectory?: string;
85
+ timeoutMs?: number;
86
+ verifyTimeoutMs?: number;
87
+ label?: string;
88
+ /** Override the model passed via --model flag. Defaults to the Gemini `flash` alias. */
89
+ model?: string;
90
+ /** Approval mode for headless Gemini runs. Defaults to yolo for autonomous execution. */
91
+ approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
92
+ /** Enable Gemini sandbox mode when the host is configured for it. Disabled by default. */
93
+ sandbox?: boolean;
94
+ /** Extra args appended after core args. */
95
+ extraArgs?: string[];
96
+ spawnImpl?: SpawnLike;
97
+ }
83
98
  export declare function createAgentCliAdapter(options: AgentCliAdapterOptions): MartinAdapter;
84
99
  /**
85
100
  * Spawns `claude --output-format json --print "<prompt>" --dangerously-skip-permissions [extraArgs]`.
@@ -102,3 +117,13 @@ export declare function createClaudeCliAdapter(options?: ClaudeCliAdapterOptions
102
117
  * npm install -g @openai/codex
103
118
  */
104
119
  export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): MartinAdapter;
120
+ /**
121
+ * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
122
+ *
123
+ * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
124
+ * which keeps large MartinLoop prompts off the command line on Windows.
125
+ *
126
+ * Requires the Gemini CLI to be installed and authenticated:
127
+ * npm install -g @google/gemini-cli
128
+ */
129
+ export declare function createGeminiCliAdapter(options?: GeminiCliAdapterOptions): MartinAdapter;