@martinloop/mcp 0.3.4 → 0.3.5

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.
@@ -1,10 +1,10 @@
1
- export declare const MARTIN_TOOL_NAMES: readonly ["martin_run", "martin_inspect", "martin_status", "martin_doctor", "martin_plan", "martin_preflight", "martin_logs", "martin_pause", "martin_cancel", "martin_continue", "martin_list_runs", "martin_triage_runs", "martin_get_run", "martin_get_attempt", "martin_get_verification_results", "martin_run_dossier", "martin_dossier", "martin_eval", "martin_pr_summary", "martin_create_pr", "martin_review_pr"];
2
- export declare const MARTIN_STARTER_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_run", "martin_triage_runs", "martin_dossier"];
3
- export declare const MARTIN_MINIMAL_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_list_runs", "martin_triage_runs", "martin_dossier"];
4
- export declare const MARTIN_DIAGNOSTIC_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_logs", "martin_list_runs", "martin_triage_runs", "martin_get_run", "martin_get_attempt", "martin_get_verification_results", "martin_dossier", "martin_eval"];
1
+ export declare const MARTIN_TOOL_NAMES: readonly ["martin_run", "martin_inspect", "martin_status", "martin_doctor", "martin_plan", "martin_preflight", "martin_estimate", "martin_logs", "martin_pause", "martin_cancel", "martin_continue", "martin_list_runs", "martin_triage_runs", "martin_get_run", "martin_get_attempt", "martin_get_verification_results", "martin_run_dossier", "martin_dossier", "martin_eval", "martin_pr_summary", "martin_create_pr", "martin_review_pr"];
2
+ export declare const MARTIN_STARTER_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_estimate", "martin_run", "martin_triage_runs", "martin_dossier"];
3
+ export declare const MARTIN_MINIMAL_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_estimate", "martin_list_runs", "martin_triage_runs", "martin_dossier"];
4
+ export declare const MARTIN_DIAGNOSTIC_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_estimate", "martin_logs", "martin_list_runs", "martin_triage_runs", "martin_get_run", "martin_get_attempt", "martin_get_verification_results", "martin_dossier", "martin_eval"];
5
5
  export declare const MARTIN_GITHUB_REVIEW_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_dossier", "martin_eval", "martin_pr_summary", "martin_create_pr", "martin_review_pr"];
6
- export declare const MARTIN_PAID_REMOTE_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_run", "martin_list_runs", "martin_triage_runs", "martin_get_run", "martin_get_verification_results", "martin_dossier", "martin_eval"];
7
- export declare const MARTIN_RESOURCE_URIS: readonly ["martin://server/health", "martin://runs/recent", "martin://runs/triage", "martin://runs/latest", "martin://runs/latest/summary", "martin://runs/latest/proof-card", "martin://runs/latest/budget-status", "martin://runs/latest/verifier-evidence", "martin://runs/latest/rollback-evidence", "martin://policies/current", "martin://repo/risk-map", "martin://verifiers/results", "martin://agent/next-step", "martin://guides/mcp-usage", "martin://guides/agent-start", "martin://guides/command-map", "martin://guides/ide-onboarding", "martin://guides/operating-rules", "martin://guides/publish-readiness"];
6
+ export declare const MARTIN_PAID_REMOTE_TOOL_NAMES: readonly ["martin_doctor", "martin_plan", "martin_preflight", "martin_estimate", "martin_run", "martin_list_runs", "martin_triage_runs", "martin_get_run", "martin_get_verification_results", "martin_dossier", "martin_eval"];
7
+ export declare const MARTIN_RESOURCE_URIS: readonly ["martin://server/health", "martin://runs/recent", "martin://runs/triage", "martin://runs/latest", "martin://runs/latest/summary", "martin://runs/latest/proof-card", "martin://runs/latest/budget-status", "martin://runs/latest/verifier-evidence", "martin://runs/latest/rollback-evidence", "martin://policies/current", "martin://repo/risk-map", "martin://verifiers/results", "martin://agent/next-step", "martin://agent/governance-status", "martin://guides/mcp-usage", "martin://guides/agent-start", "martin://guides/command-map", "martin://guides/ide-onboarding", "martin://guides/operating-rules", "martin://guides/publish-readiness"];
8
8
  export declare const MARTIN_RESOURCE_TEMPLATE_URIS: readonly ["martin://runs/{loopId}", "martin://runs/{loopId}/dossier", "martin://runs/{loopId}/attempts/{attemptIndex}", "martin://runs/{loopId}/verification"];
9
9
  export declare const MARTIN_PROMPT_NAMES: readonly ["martin_start", "martin_preflight", "martin_triage", "martin_resume", "martin_prove", "martin_release_check", "martin_governed_coding_kickoff", "martin_debug_failed_run", "martin_publish_readiness_review", "martin_triage_run_store", "safe_bug_fix", "write_tests_first", "small_refactor", "security_review", "pr_review", "release_check"];
10
10
  export interface MartinDiscoveryMetadata {
@@ -6,6 +6,7 @@ export const MARTIN_TOOL_NAMES = [
6
6
  "martin_doctor",
7
7
  "martin_plan",
8
8
  "martin_preflight",
9
+ "martin_estimate",
9
10
  "martin_logs",
10
11
  "martin_pause",
11
12
  "martin_cancel",
@@ -26,6 +27,7 @@ export const MARTIN_STARTER_TOOL_NAMES = [
26
27
  "martin_doctor",
27
28
  "martin_plan",
28
29
  "martin_preflight",
30
+ "martin_estimate",
29
31
  "martin_run",
30
32
  "martin_triage_runs",
31
33
  "martin_dossier"
@@ -34,6 +36,7 @@ export const MARTIN_MINIMAL_TOOL_NAMES = [
34
36
  "martin_doctor",
35
37
  "martin_plan",
36
38
  "martin_preflight",
39
+ "martin_estimate",
37
40
  "martin_list_runs",
38
41
  "martin_triage_runs",
39
42
  "martin_dossier"
@@ -42,6 +45,7 @@ export const MARTIN_DIAGNOSTIC_TOOL_NAMES = [
42
45
  "martin_doctor",
43
46
  "martin_plan",
44
47
  "martin_preflight",
48
+ "martin_estimate",
45
49
  "martin_logs",
46
50
  "martin_list_runs",
47
51
  "martin_triage_runs",
@@ -65,6 +69,7 @@ export const MARTIN_PAID_REMOTE_TOOL_NAMES = [
65
69
  "martin_doctor",
66
70
  "martin_plan",
67
71
  "martin_preflight",
72
+ "martin_estimate",
68
73
  "martin_run",
69
74
  "martin_list_runs",
70
75
  "martin_triage_runs",
@@ -87,6 +92,7 @@ export const MARTIN_RESOURCE_URIS = [
87
92
  "martin://repo/risk-map",
88
93
  "martin://verifiers/results",
89
94
  "martin://agent/next-step",
95
+ "martin://agent/governance-status",
90
96
  "martin://guides/mcp-usage",
91
97
  "martin://guides/agent-start",
92
98
  "martin://guides/command-map",
@@ -20,6 +20,7 @@ export declare const MARTIN_STATIC_RESOURCE_URIS: {
20
20
  readonly ideOnboardingGuide: "martin://guides/ide-onboarding";
21
21
  readonly operatingRulesGuide: "martin://guides/operating-rules";
22
22
  readonly publishReadinessGuide: "martin://guides/publish-readiness";
23
+ readonly governanceStatus: "martin://agent/governance-status";
23
24
  };
24
25
  export declare const MARTIN_RESOURCE_TEMPLATES: ResourceTemplate[];
25
26
  export declare const MARTIN_STATIC_RESOURCES: Resource[];
package/dist/resources.js CHANGED
@@ -5,6 +5,7 @@ import { martinDoctorTool } from "./tools/doctor.js";
5
5
  import { martinTriageRunsTool } from "./tools/triage-runs.js";
6
6
  import { martinRunDossierTool } from "./tools/run-dossier.js";
7
7
  import { inspectRepoSignals, buildPolicyPackDefinition, buildRepoRiskMap } from "./tools/workflow-governance.js";
8
+ import { readWorkflowState } from "./workflow-state.js";
8
9
  import { buildAttemptSnapshot, buildPersistedLoopPreview, buildVerificationHistorySnapshot, parseAttemptIndex, resolveMartinDiscoveryContext, toPrettyJson } from "./discovery-support.js";
9
10
  import { loadDetailedLoopRecord, readLedgerEvents } from "./tools/run-store.js";
10
11
  import { invalidArgumentsError, MartinToolError } from "./tools/tool-errors.js";
@@ -27,7 +28,8 @@ export const MARTIN_STATIC_RESOURCE_URIS = {
27
28
  commandMapGuide: "martin://guides/command-map",
28
29
  ideOnboardingGuide: "martin://guides/ide-onboarding",
29
30
  operatingRulesGuide: "martin://guides/operating-rules",
30
- publishReadinessGuide: "martin://guides/publish-readiness"
31
+ publishReadinessGuide: "martin://guides/publish-readiness",
32
+ governanceStatus: "martin://agent/governance-status"
31
33
  };
32
34
  export const MARTIN_RESOURCE_TEMPLATES = [
33
35
  {
@@ -188,6 +190,13 @@ export const MARTIN_STATIC_RESOURCES = [
188
190
  title: "Martin Publish Readiness Guide",
189
191
  description: "Checklist-oriented guide for MCP publish readiness reviews.",
190
192
  mimeType: "text/markdown"
193
+ },
194
+ {
195
+ uri: MARTIN_STATIC_RESOURCE_URIS.governanceStatus,
196
+ name: "martin_governance_status",
197
+ title: "Martin Governance Status",
198
+ description: "Proactive governance status: whether work is governed, budget remaining, unreceipted runs, and recommended next action. Read this before starting agent work.",
199
+ mimeType: "application/json"
191
200
  }
192
201
  ];
193
202
  export function listMartinResources() {
@@ -237,6 +246,8 @@ export async function readMartinResource(input) {
237
246
  return jsonResource(input.uri, withDiscoveryMetadata(buildRepoRiskMap(inspectRepoSignals(context.workingDirectory)), context.runsRoot));
238
247
  case MARTIN_STATIC_RESOURCE_URIS.verifierResults:
239
248
  return jsonResource(input.uri, withDiscoveryMetadata(await buildLatestVerifierEvidenceResource(context.runsRoot), context.runsRoot));
249
+ case MARTIN_STATIC_RESOURCE_URIS.governanceStatus:
250
+ return jsonResource(input.uri, withDiscoveryMetadata(await buildGovernanceStatusResource(context.runsRoot, context.workingDirectory), context.runsRoot));
240
251
  case MARTIN_STATIC_RESOURCE_URIS.agentNextStep:
241
252
  return jsonResource(input.uri, withDiscoveryMetadata(await buildAgentNextStepResource(context.runsRoot), context.runsRoot));
242
253
  case MARTIN_STATIC_RESOURCE_URIS.mcpUsageGuide:
@@ -565,6 +576,57 @@ function compactEmptyState(kind, runsRoot, warnings) {
565
576
  warnings
566
577
  };
567
578
  }
579
+ async function buildGovernanceStatusResource(runsRoot, workingDirectory) {
580
+ const state = await readWorkflowState(runsRoot);
581
+ const mcpState = state.mcp ?? {};
582
+ const hasDoctor = Boolean(mcpState.doctor);
583
+ const hasPlan = Boolean(mcpState.plan);
584
+ const hasPreflight = Boolean(mcpState.preflight);
585
+ const governed = hasDoctor && hasPlan && hasPreflight;
586
+ const latest = await loadLatestRunForCompactResource(runsRoot);
587
+ const budgetRemaining = (!latest.empty && latest.detail)
588
+ ? buildPersistedLoopPreview(latest.detail.loop).remainingBudgetUsd
589
+ : null;
590
+ let unreceiptedRuns = 0;
591
+ try {
592
+ const inspection = await inspectLoopTool({ runsDir: runsRoot });
593
+ const total = inspection.loopCount ?? 0;
594
+ const completed = (inspection.statusBreakdown?.completed ?? 0) + (inspection.statusBreakdown?.failed ?? 0);
595
+ unreceiptedRuns = Math.max(0, total - completed);
596
+ }
597
+ catch {
598
+ // No runs yet
599
+ }
600
+ const missingSteps = [];
601
+ if (!hasDoctor)
602
+ missingSteps.push("martin_doctor");
603
+ if (!hasPlan)
604
+ missingSteps.push("martin_plan");
605
+ if (!hasPreflight)
606
+ missingSteps.push("martin_preflight");
607
+ const recommendedAction = !hasDoctor
608
+ ? "Run martin_doctor to confirm environment before any work."
609
+ : !hasPlan
610
+ ? "Run martin_plan with your objective to scope the task."
611
+ : !hasPreflight
612
+ ? "Run martin_preflight to validate the run contract before execution."
613
+ : "Governance complete. You may proceed with martin_run.";
614
+ return {
615
+ kind: "governance-status",
616
+ governed,
617
+ workflowReceipts: {
618
+ doctor: hasDoctor ? { recordedAt: mcpState.doctor.recordedAt } : null,
619
+ plan: hasPlan ? { recordedAt: mcpState.plan.recordedAt } : null,
620
+ preflight: hasPreflight ? { recordedAt: mcpState.preflight.recordedAt } : null
621
+ },
622
+ missingSteps,
623
+ budgetRemaining,
624
+ unreceiptedRuns,
625
+ recommendedAction,
626
+ requiredSequence: ["martin_doctor", "martin_plan", "martin_preflight", "martin_run", "martin_dossier"],
627
+ warning: governed ? undefined : "This session is NOT governed. Complete the required sequence before making changes."
628
+ };
629
+ }
568
630
  function buildCurrentPoliciesResource(workingDirectory) {
569
631
  const signals = inspectRepoSignals(workingDirectory);
570
632
  const recommended = buildPolicyPackDefinition(undefined, signals);
package/dist/server.js CHANGED
@@ -43,6 +43,7 @@ import { createRunControlReceipt } from "./tools/run-controls.js";
43
43
  import { martinTriageRunsTool } from "./tools/triage-runs.js";
44
44
  import { runLoopTool } from "./tools/run-loop.js";
45
45
  import { MARTIN_ENGINE_VALUES } from "./tools/tool-support.js";
46
+ import { classifyRoute } from "./vendor/core/index.js";
46
47
  import { createToolErrorResult, createToolSuccessResult } from "./tools/tool-response.js";
47
48
  import { MartinToolError, toToolFailure } from "./tools/tool-errors.js";
48
49
  import { normalizeLoopBudget } from "./tools/workflow-governance.js";
@@ -1370,6 +1371,59 @@ export function createMartinMcpServer(serverInfo) {
1370
1371
  oneOf: [{ required: ["file"] }, { required: ["loopId"] }, { required: ["latest"] }]
1371
1372
  },
1372
1373
  outputSchema: prReviewOutputSchema
1374
+ },
1375
+ {
1376
+ name: "martin_estimate",
1377
+ description: "Estimate the cost, recommended route, and Pre Work Burn for an objective without spending anything. Use before martin_run to understand what a task will cost.",
1378
+ annotations: {
1379
+ readOnlyHint: true,
1380
+ idempotentHint: true
1381
+ },
1382
+ inputSchema: {
1383
+ type: "object",
1384
+ additionalProperties: false,
1385
+ properties: {
1386
+ objective: {
1387
+ type: "string",
1388
+ description: "The coding task to estimate."
1389
+ },
1390
+ engine: {
1391
+ type: "string",
1392
+ enum: [...MARTIN_ENGINE_VALUES],
1393
+ description: "Which agent CLI would be used. Defaults to claude."
1394
+ },
1395
+ budgetUsd: {
1396
+ type: "number",
1397
+ exclusiveMinimum: 0,
1398
+ description: "Budget ceiling for estimation. Defaults to 5."
1399
+ },
1400
+ fileScope: {
1401
+ type: "array",
1402
+ items: { type: "string" },
1403
+ description: "Optional file paths to scope the estimate."
1404
+ }
1405
+ },
1406
+ required: ["objective"]
1407
+ },
1408
+ outputSchema: {
1409
+ type: "object",
1410
+ additionalProperties: true,
1411
+ properties: {
1412
+ objective: { type: "string" },
1413
+ engine: { type: "string" },
1414
+ budgetUsd: { type: "number" },
1415
+ selectedMode: { type: "string" },
1416
+ confidence: { type: "number" },
1417
+ expectedCostUsd: { type: "number" },
1418
+ expectedPreworkBurnPct: { type: "number" },
1419
+ reason: { type: "array", items: { type: "string" } },
1420
+ blockedSteps: { type: "array", items: { type: "string" } },
1421
+ compressed: { type: "boolean" },
1422
+ compressionSummary: { type: "string" },
1423
+ recommendedBudgetUsd: { type: "number" }
1424
+ },
1425
+ required: ["objective", "engine", "budgetUsd", "selectedMode", "confidence", "expectedCostUsd", "expectedPreworkBurnPct", "reason"]
1426
+ }
1373
1427
  }
1374
1428
  ]
1375
1429
  }));
@@ -1580,6 +1634,38 @@ export function createMartinMcpServer(serverInfo) {
1580
1634
  const output = await martinReviewPrTool(input);
1581
1635
  return createToolSuccessResult(output, `PR review verdict for ${output.loopId}: ${output.verdict}.`);
1582
1636
  }
1637
+ if (name === "martin_estimate") {
1638
+ const input = args;
1639
+ const objective = input.objective;
1640
+ const engine = input.engine ?? "claude";
1641
+ const budgetUsd = input.budgetUsd ?? 5;
1642
+ const fileScope = input.fileScope ?? [];
1643
+ const route = classifyRoute({
1644
+ objective,
1645
+ verificationPlan: [],
1646
+ budgetUsd,
1647
+ allowedPaths: fileScope,
1648
+ scopedFileCount: fileScope.length > 0 ? fileScope.length : undefined
1649
+ });
1650
+ const recommendedBudgetUsd = route.selectedMode === "direct"
1651
+ ? Math.max(2, Math.round(route.expectedCostUsd * 3 * 100) / 100)
1652
+ : Math.max(5, Math.round(route.expectedCostUsd * 2 * 100) / 100);
1653
+ const output = {
1654
+ objective,
1655
+ engine,
1656
+ budgetUsd,
1657
+ selectedMode: route.selectedMode,
1658
+ confidence: route.confidence,
1659
+ expectedCostUsd: route.expectedCostUsd,
1660
+ expectedPreworkBurnPct: route.expectedPreworkBurnPct,
1661
+ reason: route.reason,
1662
+ blockedSteps: route.blockedSteps,
1663
+ compressed: route.compressed,
1664
+ ...(route.compressionSummary ? { compressionSummary: route.compressionSummary } : {}),
1665
+ recommendedBudgetUsd
1666
+ };
1667
+ return createToolSuccessResult(output, `Estimate: ${route.selectedMode} route, ~$${route.expectedCostUsd.toFixed(2)} expected cost, ${route.expectedPreworkBurnPct}% pre-work burn. Recommended budget: $${recommendedBudgetUsd.toFixed(2)}.`);
1668
+ }
1583
1669
  return createToolErrorResult(toToolFailure(new Error(`Unknown tool: ${name}`)));
1584
1670
  }
1585
1671
  catch (error) {
@@ -16,17 +16,17 @@ async function attachReceiptIntegrity(detail) {
16
16
  loopRecordPath: detail.canonicalLoopRecordPath,
17
17
  ledgerPath
18
18
  }).catch(() => ({
19
- state: "material_missing",
19
+ state: "unsigned",
20
20
  reason: "Receipt integrity verification could not be completed."
21
21
  }))
22
22
  : detail.canonicalLoopRecordPath && detail.canonicalRunDirectory
23
23
  ? ({
24
- state: "material_missing",
24
+ state: "unsigned",
25
25
  reason: "Receipt integrity material is incomplete for this canonical run."
26
26
  })
27
27
  : ({
28
- state: "selector_noncanonical",
29
- reason: "Receipt integrity is only available for canonical run selectors."
28
+ state: "unsigned",
29
+ reason: "Receipt integrity is only available for canonical run directories."
30
30
  });
31
31
  const receiptScope = resolveReceiptScope(detail.loop, detail.runsRoot);
32
32
  return {
@@ -14,6 +14,7 @@ export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" |
14
14
  metadata?: Record<string, string>;
15
15
  receiptIntegrity?: ReceiptIntegritySummary;
16
16
  receiptScope?: ReceiptScope;
17
+ routingEconomics?: import("../vendor/contracts/index.js").RoutingEconomics;
17
18
  }
18
19
  export interface LoopPreview {
19
20
  loopId: string;
@@ -32,6 +33,7 @@ export interface LoopPreview {
32
33
  remainingIterations: number;
33
34
  remainingTokens: number;
34
35
  lastAttempt?: AttemptSummary;
36
+ routingEconomics?: Record<string, unknown>;
35
37
  }
36
38
  export interface AttemptArtifactFiles {
37
39
  directory: string;
@@ -214,7 +214,22 @@ export function buildLoopPreview(loop) {
214
214
  remainingBudgetUsd: costState.remainingBudgetUsd,
215
215
  remainingIterations: costState.remainingIterations,
216
216
  remainingTokens: costState.remainingTokens,
217
- ...(lastAttempt ? { lastAttempt: buildAttemptSummary(lastAttempt) } : {})
217
+ ...(lastAttempt ? { lastAttempt: buildAttemptSummary(lastAttempt) } : {}),
218
+ ...(loop.routingEconomics ? { routingEconomics: buildRoutingEconomicsSummary(loop.routingEconomics) } : {})
219
+ };
220
+ }
221
+ function buildRoutingEconomicsSummary(economics) {
222
+ return {
223
+ preworkBurnPct: economics.preworkBurnPct,
224
+ preworkCostUsd: economics.preworkCostUsd,
225
+ executionCostUsd: economics.executionCostUsd,
226
+ totalCostUsd: economics.totalCostUsd,
227
+ ...(economics.timeToFirstDeltaMs !== undefined ? { timeToFirstDeltaMs: economics.timeToFirstDeltaMs } : {}),
228
+ ...(economics.costPerAcceptedChange !== undefined ? { costPerAcceptedChange: economics.costPerAcceptedChange } : {}),
229
+ ...(economics.routeRecommendation ? {
230
+ routeRecommendation: economics.routeRecommendation,
231
+ routeRecommendationReason: economics.routeRecommendationReason
232
+ } : {})
218
233
  };
219
234
  }
220
235
  export function buildAttemptSummary(attempt, artifacts) {
@@ -12,7 +12,7 @@
12
12
  * MCP tools and integration tests use the same factories.
13
13
  */
14
14
  import { readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
15
- import { buildCodexExecArgs } from "./codex-launcher.js";
15
+ import { buildCodexExecArgs, DEFAULT_CODEX_CHATGPT_MODEL } from "./codex-launcher.js";
16
16
  import { createAdapterCapabilities, normalizeStructuredErrors, normalizeUsage } from "./runtime-support.js";
17
17
  // ---------------------------------------------------------------------------
18
18
  // Cost estimation
@@ -253,6 +253,10 @@ function createStreamingUsageInspector(capUsd, modelLabel) {
253
253
  const bytesCeiling = blendedCostPerChar > 0
254
254
  ? Math.ceil((capUsd / blendedCostPerChar) * 2)
255
255
  : 100_000_000;
256
+ // Time-based fallback: if we receive data but no usage events for this long,
257
+ // estimate spend from byte volume and enforce the cap. Prevents the inspector
258
+ // from going blind when Claude changes its stream-json event format.
259
+ const USAGE_BLIND_TIMEOUT_MS = 30_000;
256
260
  let buffer = "";
257
261
  let cumulativeUsd = 0;
258
262
  let tokensIn = 0;
@@ -260,6 +264,7 @@ function createStreamingUsageInspector(capUsd, modelLabel) {
260
264
  let turns = 0;
261
265
  let totalBytes = 0;
262
266
  let usageEventSeen = false;
267
+ let firstChunkAt;
263
268
  let finalResult;
264
269
  const checkBudgetExceeded = (terminate) => {
265
270
  if (capUsd > 0 && cumulativeUsd > effectiveCapUsd) {
@@ -324,6 +329,7 @@ function createStreamingUsageInspector(capUsd, modelLabel) {
324
329
  const chunkStr = chunk.toString("utf8");
325
330
  totalBytes += chunk.byteLength;
326
331
  buffer += chunkStr;
332
+ firstChunkAt ??= Date.now();
327
333
  // Token-count ceiling fallback: if we've ingested a lot of bytes but
328
334
  // never seen a single usage event, the event format may have changed
329
335
  // and the inspector is silently blind. Terminate as a last resort.
@@ -333,6 +339,23 @@ function createStreamingUsageInspector(capUsd, modelLabel) {
333
339
  `Subprocess terminated as a fallback budget guard for cap $${capUsd.toFixed(4)}.`);
334
340
  return;
335
341
  }
342
+ // Time-based fallback: if we've been receiving data for 30+ seconds
343
+ // without a single usage event, estimate spend from byte volume and
344
+ // enforce the cap. This catches cases where Claude's event format
345
+ // changed but bytes are still flowing.
346
+ if (!usageEventSeen &&
347
+ capUsd > 0 &&
348
+ firstChunkAt !== undefined &&
349
+ Date.now() - firstChunkAt > USAGE_BLIND_TIMEOUT_MS &&
350
+ totalBytes > 10_000) {
351
+ const estimatedUsd = totalBytes * blendedCostPerChar;
352
+ if (estimatedUsd > effectiveCapUsd) {
353
+ terminate(`No usage events received after ${String(Math.round((Date.now() - firstChunkAt) / 1000))}s ` +
354
+ `(${String(totalBytes)} bytes). Estimated cost ~$${estimatedUsd.toFixed(4)} exceeds cap ` +
355
+ `$${capUsd.toFixed(4)}. Subprocess terminated to prevent unmetered spend.`);
356
+ return;
357
+ }
358
+ }
336
359
  let newlineIndex = buffer.indexOf("\n");
337
360
  while (newlineIndex !== -1) {
338
361
  const line = buffer.slice(0, newlineIndex);
@@ -773,6 +796,11 @@ export function createClaudeCliAdapter(options = {}) {
773
796
  "--verbose",
774
797
  "--print",
775
798
  "--dangerously-skip-permissions",
799
+ // Prevent the child Claude process from loading the parent's MCP servers.
800
+ // Without this, a MartinLoop MCP server in the user's config gets spawned
801
+ // inside the governed subprocess, causing conflicts and "MCP server being
802
+ // overwritten" errors.
803
+ "--strict-mcp-config",
776
804
  ...modelArgs,
777
805
  ...extraArgs
778
806
  ],
@@ -797,6 +825,7 @@ export function createCodexCliAdapter(options = {}) {
797
825
  const sandbox = options.sandbox ?? "workspace-write";
798
826
  const workingDirectory = options.workingDirectory ?? process.cwd();
799
827
  const command = options.command ?? "codex";
828
+ const launchModel = options.model ?? DEFAULT_CODEX_CHATGPT_MODEL;
800
829
  return createAgentCliAdapter({
801
830
  command,
802
831
  adapterIdSuffix: "codex",
@@ -810,7 +839,7 @@ export function createCodexCliAdapter(options = {}) {
810
839
  argsBuilder: () => buildCodexExecArgs({
811
840
  workingDirectory,
812
841
  sandbox,
813
- ...(options.model ? { model: options.model } : {}),
842
+ model: launchModel,
814
843
  extraArgs,
815
844
  mode: "prompt"
816
845
  }),
@@ -71,6 +71,16 @@ export declare function readGitExecutionArtifacts(repoRoot: string, timeoutMs: n
71
71
  diffStats?: ReturnType<typeof diffStatsFromNumstat>;
72
72
  }>;
73
73
  export declare function readGitChangedFiles(repoRoot: string, timeoutMs: number, spawnImpl?: SpawnLike): Promise<string[]>;
74
+ /**
75
+ * Detects whether a meaningful workspace delta has occurred by checking
76
+ * git status for changed files that aren't MartinLoop metadata, lockfiles,
77
+ * or cache artifacts. Returns the first meaningful changed file if found.
78
+ */
79
+ export declare function detectFirstDelta(repoRoot: string, timeoutMs: number, spawnImpl?: SpawnLike): Promise<{
80
+ detected: boolean;
81
+ filePath?: string;
82
+ changeType?: "create" | "modify" | "delete";
83
+ }>;
74
84
  export declare function resolveGitRepositoryRoot(workingDirectory: string): string | undefined;
75
85
  export interface SpawnPlan {
76
86
  command: string;
@@ -236,6 +236,36 @@ export async function readGitChangedFiles(repoRoot, timeoutMs, spawnImpl) {
236
236
  }
237
237
  return parsePorcelainEntries(statusResult.stdout).filter((entry) => typeof entry === "string" && entry.length > 0);
238
238
  }
239
+ // Ignore paths that are not meaningful code changes for first-delta detection
240
+ const FIRST_DELTA_IGNORE = [
241
+ /^\.martin\//u,
242
+ /^PROGRESS\.md$/u,
243
+ /\.lock$/u,
244
+ /^node_modules\//u,
245
+ /^\.git\//u,
246
+ /^\.cache\//u,
247
+ ];
248
+ /**
249
+ * Detects whether a meaningful workspace delta has occurred by checking
250
+ * git status for changed files that aren't MartinLoop metadata, lockfiles,
251
+ * or cache artifacts. Returns the first meaningful changed file if found.
252
+ */
253
+ export async function detectFirstDelta(repoRoot, timeoutMs, spawnImpl) {
254
+ const changedFiles = await readGitChangedFiles(repoRoot, timeoutMs, spawnImpl);
255
+ for (const file of changedFiles) {
256
+ if (FIRST_DELTA_IGNORE.some((pattern) => pattern.test(file))) {
257
+ continue;
258
+ }
259
+ // Classify: new file = create, deleted = delete, else modify
260
+ const diffResult = await runSubprocess("git", ["status", "--porcelain", "--", file], { cwd: repoRoot, timeoutMs: 3000, spawnImpl });
261
+ const status = diffResult.stdout.trim().slice(0, 2);
262
+ const changeType = status.includes("?") ? "create"
263
+ : status.includes("D") ? "delete"
264
+ : "modify";
265
+ return { detected: true, filePath: file, changeType };
266
+ }
267
+ return { detected: false };
268
+ }
239
269
  export function resolveGitRepositoryRoot(workingDirectory) {
240
270
  const resolvedWorkingDirectory = resolve(workingDirectory);
241
271
  const cached = gitRepositoryRootCache.get(resolvedWorkingDirectory);
@@ -43,6 +43,7 @@ export interface CodexExecArgsOptions {
43
43
  mode?: "prompt" | "probe";
44
44
  }
45
45
  type SpawnSyncLike = typeof spawnSync;
46
+ export declare const DEFAULT_CODEX_CHATGPT_MODEL = "gpt-5.4";
46
47
  export interface CodexProbeCandidateResult {
47
48
  path: string;
48
49
  installKind: CodexInstallKind;
@@ -72,5 +73,6 @@ export declare function probeCodexLaunch(input: {
72
73
  env?: NodeJS.ProcessEnv;
73
74
  platform?: NodeJS.Platform;
74
75
  spawnSyncImpl?: SpawnSyncLike;
76
+ model?: string;
75
77
  }): CodexLaunchProbeResult;
76
78
  export {};
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
4
  const codexLaunchProbeCache = new Map();
5
+ export const DEFAULT_CODEX_CHATGPT_MODEL = "gpt-5.4";
5
6
  const CODEX_LAUNCH_PROBE_PROMPT = [
6
7
  "You are validating MartinLoop Codex host readiness.",
7
8
  "Do not edit files.",
@@ -14,7 +15,8 @@ function buildProbeCacheKey(input) {
14
15
  return JSON.stringify({
15
16
  workingDirectory: resolve(input.workingDirectory),
16
17
  platform: input.platform,
17
- candidatePaths: input.candidatePaths
18
+ candidatePaths: input.candidatePaths,
19
+ model: input.model ?? DEFAULT_CODEX_CHATGPT_MODEL
18
20
  });
19
21
  }
20
22
  function isInsideGitRepository(workingDirectory) {
@@ -167,6 +169,39 @@ function detectInvocationMode(resolvedPath, platform) {
167
169
  }
168
170
  function classifyProbeFailure(stderr, stdout, diagnosis) {
169
171
  const combined = `${stderr}\n${stdout}`;
172
+ if (/MARTIN_REMOTE_TOKEN/iu.test(combined) ||
173
+ /AuthRequired\(AuthRequiredError/iu.test(combined) ||
174
+ /www_authenticate_header/iu.test(combined)) {
175
+ const warnings = [...diagnosis.warnings];
176
+ const configCollisionWarning = "Codex inherited auth-sensitive MCP or plugin state from the operator's default user config.";
177
+ if (!warnings.includes(configCollisionWarning)) {
178
+ warnings.push(configCollisionWarning);
179
+ }
180
+ return {
181
+ summary: "Codex inherited auth-sensitive MCP or plugin state from the operator's default user config. Governed Codex runs should launch with `exec --ignore-user-config` so missing remote tokens or third-party auth do not abort MartinLoop work.",
182
+ diagnosis: {
183
+ ...diagnosis,
184
+ warnings,
185
+ remediation: "Retry the governed Codex launch with `exec --ignore-user-config`, or remove auth-sensitive MCP/plugin dependencies from the default Codex user config before running MartinLoop."
186
+ }
187
+ };
188
+ }
189
+ if (/not supported when using Codex with a ChatGPT account/iu.test(combined) ||
190
+ /gpt-5\.3-codex/iu.test(combined)) {
191
+ const warnings = [...diagnosis.warnings];
192
+ const unsupportedModelWarning = "Codex tried to use a model that is not supported for ChatGPT-account authentication.";
193
+ if (!warnings.includes(unsupportedModelWarning)) {
194
+ warnings.push(unsupportedModelWarning);
195
+ }
196
+ return {
197
+ summary: `Codex launched with a model that is not supported for ChatGPT-account authentication. Use an explicit supported model such as \`${DEFAULT_CODEX_CHATGPT_MODEL}\` for governed Codex work.`,
198
+ diagnosis: {
199
+ ...diagnosis,
200
+ warnings,
201
+ remediation: `Override the Codex launch model to a ChatGPT-account-supported option such as \`${DEFAULT_CODEX_CHATGPT_MODEL}\` before running governed Codex work.`
202
+ }
203
+ };
204
+ }
170
205
  if (/@openai\/codex-linux-x64/iu.test(combined) ||
171
206
  /cannot find module ['"]@openai\/codex-linux-x64['"]/iu.test(combined)) {
172
207
  const warnings = [...diagnosis.warnings];
@@ -205,6 +240,24 @@ function classifyProbeFailure(stderr, stdout, diagnosis) {
205
240
  }
206
241
  };
207
242
  }
243
+ if (/writing is blocked by read-only sandbox/iu.test(combined) ||
244
+ /read-only filesystem sandbox/iu.test(combined) ||
245
+ /approval is disabled/iu.test(combined)) {
246
+ const warnings = [...diagnosis.warnings];
247
+ const readOnlySandboxWarning = "Codex stayed in a read-only or approval-disabled sandbox even though MartinLoop requested workspace-write.";
248
+ if (!warnings.includes(readOnlySandboxWarning)) {
249
+ warnings.push(readOnlySandboxWarning);
250
+ }
251
+ return {
252
+ summary: "Codex stayed in a read-only or approval-disabled sandbox even though MartinLoop requested workspace-write.",
253
+ diagnosis: {
254
+ ...diagnosis,
255
+ sandboxCompatible: false,
256
+ warnings,
257
+ remediation: "Launch governed Codex runs with `codex exec --ignore-user-config --sandbox workspace-write`. If the session still reports a read-only sandbox, treat it as a host/runtime mismatch and repair or relocate the affected Codex workspace before resuming governed work."
258
+ }
259
+ };
260
+ }
208
261
  return {};
209
262
  }
210
263
  export function buildCodexExecArgs(options) {
@@ -213,6 +266,8 @@ export function buildCodexExecArgs(options) {
213
266
  const extraArgs = options.extraArgs ?? [];
214
267
  return [
215
268
  "exec",
269
+ // Governed MartinLoop runs should not inherit auth-sensitive default MCP/plugin state.
270
+ "--ignore-user-config",
216
271
  "--cd",
217
272
  options.workingDirectory,
218
273
  "--sandbox",
@@ -396,6 +451,7 @@ export function probeCodexLaunch(input) {
396
451
  });
397
452
  const args = buildCodexExecArgs({
398
453
  workingDirectory: input.workingDirectory,
454
+ model: input.model ?? DEFAULT_CODEX_CHATGPT_MODEL,
399
455
  mode: "probe"
400
456
  });
401
457
  if (!availability.available) {
@@ -431,7 +487,8 @@ export function probeCodexLaunch(input) {
431
487
  const cacheKey = buildProbeCacheKey({
432
488
  workingDirectory: input.workingDirectory,
433
489
  platform,
434
- candidatePaths
490
+ candidatePaths,
491
+ model: input.model
435
492
  });
436
493
  if (input.spawnSyncImpl === undefined) {
437
494
  const cached = codexLaunchProbeCache.get(cacheKey);
@@ -59,4 +59,20 @@ export interface ExecutionPolicyCompileInput {
59
59
  config?: ExecutionPolicyConfigInput;
60
60
  request: ExecutionPolicyRequestInput;
61
61
  }
62
+ export type RoutingMode = "direct" | "manager" | "adaptive" | "consensus";
63
+ export interface RoutingPolicy {
64
+ enabled: boolean;
65
+ mode: RoutingMode;
66
+ /** Max percentage of total budget that can be spent before first code change. */
67
+ maxPreworkBudgetPct: number;
68
+ /** Hard dollar cap on pre-work spend. */
69
+ maxPreworkCostUsd: number;
70
+ /** Max model calls allowed for manager/router/planner before worker starts. */
71
+ maxManagerCalls: number;
72
+ /** Skip orchestration entirely if task confidence is above this threshold (0-1). */
73
+ skipOrchestrationIfConfidenceAbove: number;
74
+ /** Force direct execution for tasks classified as simple. */
75
+ requireDirectForSimpleTasks: boolean;
76
+ }
77
+ export declare const DEFAULT_ROUTING_POLICY: RoutingPolicy;
62
78
  export declare function cloneExecutionPolicy(policy: ExecutionPolicy): ExecutionPolicy;
@@ -1,3 +1,12 @@
1
+ export const DEFAULT_ROUTING_POLICY = {
2
+ enabled: true,
3
+ mode: "adaptive",
4
+ maxPreworkBudgetPct: 25,
5
+ maxPreworkCostUsd: 2.0,
6
+ maxManagerCalls: 2,
7
+ skipOrchestrationIfConfidenceAbove: 0.85,
8
+ requireDirectForSimpleTasks: true
9
+ };
1
10
  export function cloneExecutionPolicy(policy) {
2
11
  return {
3
12
  configPath: policy.configPath,