@martinloop/mcp 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -52
- package/dist/arcade/capabilities.d.ts +36 -0
- package/dist/arcade/capabilities.js +37 -0
- package/dist/arcade/resource.d.ts +31 -0
- package/dist/arcade/resource.js +52 -0
- package/dist/arcade/status.d.ts +23 -0
- package/dist/arcade/status.js +25 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +3 -3
- package/dist/resources.js +1 -13
- package/dist/server.js +241 -75
- package/dist/tools/execution-policy.js +1 -1
- package/dist/tools/get-status.d.ts +2 -4
- package/dist/tools/get-status.js +6 -3
- package/dist/tools/preflight.d.ts +1 -1
- package/dist/tools/run-loop.d.ts +2 -3
- package/dist/tools/run-loop.js +4 -3
- package/dist/tools/run-store.js +1 -1
- package/dist/tools/tool-support.d.ts +3 -2
- package/dist/tools/tool-support.js +9 -2
- package/dist/tools/workflow-governance.d.ts +1 -1
- package/dist/tools/workflow-governance.js +1 -1
- package/dist/vendor/adapters/claude-cli.js +2 -1
- package/dist/vendor/adapters/cli-bridge.js +7 -1
- package/dist/vendor/adapters/index.d.ts +1 -0
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/adapters/openai-compatible.js +6 -3
- package/dist/vendor/adapters/stub-agent-cli.d.ts +9 -0
- package/dist/vendor/adapters/stub-agent-cli.js +43 -0
- package/dist/vendor/adapters/stub-direct-provider.d.ts +8 -0
- package/dist/vendor/adapters/stub-direct-provider.js +16 -0
- package/dist/vendor/adapters/verifier-only.js +1 -1
- package/dist/vendor/contracts/execution-policy.js +1 -1
- package/dist/vendor/contracts/exits.d.ts +1 -1
- package/dist/vendor/contracts/governance.d.ts +1 -1
- package/dist/vendor/contracts/index.d.ts +1 -1
- package/dist/vendor/contracts/index.js +1 -2
- package/dist/vendor/contracts/operator.d.ts +1 -1
- package/dist/vendor/contracts/operator.js +1 -2
- package/dist/vendor/core/compiler.d.ts +2 -2
- package/dist/vendor/core/compiler.js +3 -1
- package/dist/vendor/core/exits.js +9 -4
- package/dist/vendor/core/grounding.d.ts +1 -0
- package/dist/vendor/core/grounding.js +28 -1
- package/dist/vendor/core/index.d.ts +2 -2
- package/dist/vendor/core/index.js +11 -64
- package/dist/vendor/core/policy-compiler.js +17 -1
- package/dist/vendor/core/policy.d.ts +2 -1
- package/dist/vendor/core/policy.js +15 -5
- package/dist/vendor/core/rollback.d.ts +3 -0
- package/dist/vendor/core/rollback.js +33 -20
- package/dist/vendor/core/verified-handoff.js +25 -6
- package/dist/vendor/presentation/motion.js +0 -4
- package/dist/vendor/presentation/verified-handoff.js +10 -13
- package/dist/workflow-state.js +1 -1
- package/package.json +20 -11
- package/server.json +3 -3
package/dist/tools/run-store.js
CHANGED
|
@@ -641,7 +641,7 @@ function validateInspectableLoopRecord(value) {
|
|
|
641
641
|
!isFiniteNumber(budget["maxUsd"]) ||
|
|
642
642
|
!isFiniteNumber(budget["softLimitUsd"]) ||
|
|
643
643
|
!Number.isInteger(budget["maxIterations"]) ||
|
|
644
|
-
!Number.isInteger(budget["maxTokens"]) ||
|
|
644
|
+
(budget["maxTokens"] !== undefined && !Number.isInteger(budget["maxTokens"])) ||
|
|
645
645
|
!isRecord(cost) ||
|
|
646
646
|
!isFiniteNumber(cost["actualUsd"]) ||
|
|
647
647
|
!isFiniteNumber(cost["tokensIn"]) ||
|
|
@@ -6,7 +6,8 @@ export interface InspectableLoopAttempt extends LoopAttemptRecord {
|
|
|
6
6
|
attemptId?: string;
|
|
7
7
|
summary?: string;
|
|
8
8
|
}
|
|
9
|
-
export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" | "task"> {
|
|
9
|
+
export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" | "task" | "budget"> {
|
|
10
|
+
budget: LoopBudget;
|
|
10
11
|
attempts: InspectableLoopAttempt[];
|
|
11
12
|
task?: LoopTask;
|
|
12
13
|
artifacts?: LoopArtifact[];
|
|
@@ -33,7 +34,7 @@ export interface LoopPreview {
|
|
|
33
34
|
shouldStop: boolean;
|
|
34
35
|
remainingBudgetUsd: number;
|
|
35
36
|
remainingIterations: number;
|
|
36
|
-
remainingTokens
|
|
37
|
+
remainingTokens?: number;
|
|
37
38
|
lastAttempt?: AttemptSummary;
|
|
38
39
|
routingEconomics?: Record<string, unknown>;
|
|
39
40
|
}
|
|
@@ -157,7 +157,9 @@ export function buildLoopPreview(loop) {
|
|
|
157
157
|
shouldStop: costState.shouldStop,
|
|
158
158
|
remainingBudgetUsd: costState.remainingBudgetUsd,
|
|
159
159
|
remainingIterations: costState.remainingIterations,
|
|
160
|
-
|
|
160
|
+
...(costState.remainingTokens !== undefined
|
|
161
|
+
? { remainingTokens: costState.remainingTokens }
|
|
162
|
+
: {}),
|
|
161
163
|
...(lastAttempt ? { lastAttempt: buildAttemptSummary(lastAttempt) } : {}),
|
|
162
164
|
...(loop.routingEconomics ? { routingEconomics: buildRoutingEconomicsSummary(loop.routingEconomics) } : {})
|
|
163
165
|
};
|
|
@@ -291,7 +293,12 @@ export function buildCostSnapshot(cost) {
|
|
|
291
293
|
};
|
|
292
294
|
}
|
|
293
295
|
export function buildBudgetSnapshot(budget) {
|
|
294
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
maxUsd: budget.maxUsd,
|
|
298
|
+
softLimitUsd: budget.softLimitUsd,
|
|
299
|
+
maxIterations: budget.maxIterations,
|
|
300
|
+
...(budget.maxTokens !== undefined ? { maxTokens: budget.maxTokens } : {})
|
|
301
|
+
};
|
|
295
302
|
}
|
|
296
303
|
export function buildSuggestedResourceUris(loopId) {
|
|
297
304
|
return ["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://runs/${loopId}`, `martin://runs/${loopId}/dossier`, `martin://runs/${loopId}/verification`, "martin://guides/mcp-usage", "martin://guides/agent-start", "martin://guides/publish-readiness"];
|
|
@@ -271,7 +271,7 @@ export function normalizeLoopBudget(overrides = {}) {
|
|
|
271
271
|
maxUsd,
|
|
272
272
|
softLimitUsd,
|
|
273
273
|
maxIterations: overrides.maxIterations ?? DEFAULT_BUDGET.maxIterations,
|
|
274
|
-
maxTokens: overrides.maxTokens
|
|
274
|
+
...(overrides.maxTokens !== undefined ? { maxTokens: overrides.maxTokens } : {})
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
export function assessRunRisk(input) {
|
|
@@ -749,6 +749,7 @@ export function createAgentCliAdapter(options) {
|
|
|
749
749
|
workspaceId: request.workspaceId,
|
|
750
750
|
cwd: workingDirectory,
|
|
751
751
|
});
|
|
752
|
+
const verificationConfigured = verification.binding.commands.length > 0;
|
|
752
753
|
// Check for zero-diff (agent ran but made no file changes)
|
|
753
754
|
const postRunChangedFiles = gitRepoRoot
|
|
754
755
|
? await readGitChangedFiles(gitRepoRoot, 5_000, options.spawnImpl)
|
|
@@ -791,7 +792,7 @@ export function createAgentCliAdapter(options) {
|
|
|
791
792
|
}
|
|
792
793
|
// Derive structural classHint from evidence, not keyword scanning
|
|
793
794
|
const structuralHint = inferStructuralClassHint(agentText, verification.summary, agentResult.exitCode, request.context.objective);
|
|
794
|
-
if (verification.passed) {
|
|
795
|
+
if (verification.passed || !verificationConfigured) {
|
|
795
796
|
return {
|
|
796
797
|
status: "completed",
|
|
797
798
|
summary,
|
|
@@ -154,7 +154,13 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
|
|
|
154
154
|
commands: steps.map((step) => step.command),
|
|
155
155
|
};
|
|
156
156
|
if (steps.length === 0) {
|
|
157
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
passed: false,
|
|
159
|
+
summary: "No verification commands specified; execution is not VERIFIED.",
|
|
160
|
+
steps: [],
|
|
161
|
+
warnings: ["Execution completed without verifier evidence."],
|
|
162
|
+
binding: executionBinding,
|
|
163
|
+
};
|
|
158
164
|
}
|
|
159
165
|
const failedSteps = [];
|
|
160
166
|
const stepOutcomes = [];
|
|
@@ -4,5 +4,6 @@ export { createCodexCliAdapter, type CodexCliAdapterOptions } from "./codex-cli.
|
|
|
4
4
|
export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
|
|
5
5
|
export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
|
|
6
6
|
/** @internal Test-only compatibility export. Public package builders strip this surface. */
|
|
7
|
+
export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions, createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-agent-cli.js";
|
|
7
8
|
export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexApprovalCapability, type CodexAutonomyResolution, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexFlagScope, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, type CodexPromptTransport, type CodexSandboxCapability, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
|
|
8
9
|
export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
|
|
@@ -4,5 +4,6 @@ export { createCodexCliAdapter } from "./codex-cli.js";
|
|
|
4
4
|
export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
|
|
5
5
|
export { createVerifierOnlyAdapter } from "./verifier-only.js";
|
|
6
6
|
/** @internal Test-only compatibility export. Public package builders strip this surface. */
|
|
7
|
+
export { createStubAgentCliAdapter, createStubDirectProviderAdapter } from "./stub-agent-cli.js";
|
|
7
8
|
export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
|
|
8
9
|
export { createSpawnPlan } from "./cli-bridge.js";
|
|
@@ -347,11 +347,14 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
347
347
|
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
348
348
|
: []
|
|
349
349
|
};
|
|
350
|
+
const verificationConfigured = verification.binding.commands.length > 0;
|
|
350
351
|
return {
|
|
351
|
-
status: verification.passed ? "completed" : "failed",
|
|
352
|
+
status: verification.passed || !verificationConfigured ? "completed" : "failed",
|
|
352
353
|
summary: verification.passed
|
|
353
354
|
? `${model} completed the task. Verifier passed.`
|
|
354
|
-
:
|
|
355
|
+
: !verificationConfigured
|
|
356
|
+
? `${model} completed the task without verifier evidence; outcome is not VERIFIED.`
|
|
357
|
+
: `${model} completed but verifier failed: ${verification.summary}`,
|
|
355
358
|
usage: normalizeOpenAiCompatibleUsage({
|
|
356
359
|
model,
|
|
357
360
|
tokensIn,
|
|
@@ -362,7 +365,7 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
362
365
|
}),
|
|
363
366
|
verification,
|
|
364
367
|
execution,
|
|
365
|
-
...(verification.passed ? {} : {
|
|
368
|
+
...(verification.passed || !verificationConfigured ? {} : {
|
|
366
369
|
failure: { message: verification.summary }
|
|
367
370
|
})
|
|
368
371
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MartinAdapter, MartinAdapterRequest, MartinAdapterResult } from "../core/index.js";
|
|
2
|
+
export interface StubAgentCliAdapterOptions {
|
|
3
|
+
command: string[];
|
|
4
|
+
profile?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
responder?: (request: MartinAdapterRequest) => Promise<MartinAdapterResult> | MartinAdapterResult;
|
|
7
|
+
}
|
|
8
|
+
export declare function createStubAgentCliAdapter(options: StubAgentCliAdapterOptions): MartinAdapter;
|
|
9
|
+
export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createAdapterCapabilities } from "./runtime-support.js";
|
|
2
|
+
export function createStubAgentCliAdapter(options) {
|
|
3
|
+
const metadata = {
|
|
4
|
+
command: options.command.join(" "),
|
|
5
|
+
providerId: "agent-cli",
|
|
6
|
+
model: options.profile ?? options.command[0] ?? "default",
|
|
7
|
+
transport: "cli",
|
|
8
|
+
capabilities: createAdapterCapabilities(),
|
|
9
|
+
...(options.profile ? { profile: options.profile } : {})
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
adapterId: `agent-cli:stub:${options.command.join(":")}`,
|
|
13
|
+
kind: "agent-cli",
|
|
14
|
+
label: options.label ?? `Stub agent CLI (${options.command.join(" ")})`,
|
|
15
|
+
metadata,
|
|
16
|
+
async execute(request) {
|
|
17
|
+
if (options.responder) {
|
|
18
|
+
return await options.responder(request);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
status: "failed",
|
|
22
|
+
summary: `Stub agent CLI ${options.command.join(" ")} did not execute a live session.`,
|
|
23
|
+
usage: {
|
|
24
|
+
actualUsd: 0,
|
|
25
|
+
tokensIn: 0,
|
|
26
|
+
tokensOut: 0,
|
|
27
|
+
provenance: "unavailable"
|
|
28
|
+
},
|
|
29
|
+
verification: {
|
|
30
|
+
passed: false,
|
|
31
|
+
summary: "No CLI execution was attempted."
|
|
32
|
+
},
|
|
33
|
+
failure: {
|
|
34
|
+
message: `Agent CLI ${options.command.join(" ")} is not configured for live execution.`,
|
|
35
|
+
classHint: "environment_mismatch"
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Internal test-compatibility re-export. Public builders remove the entire
|
|
42
|
+
// stub compatibility surface before constructing distributable packages.
|
|
43
|
+
export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MartinAdapter, MartinAdapterRequest, MartinAdapterResult } from "../core/index.js";
|
|
2
|
+
export interface StubDirectProviderAdapterOptions {
|
|
3
|
+
providerId: string;
|
|
4
|
+
model: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
responder?: (request: MartinAdapterRequest) => Promise<MartinAdapterResult> | MartinAdapterResult;
|
|
7
|
+
}
|
|
8
|
+
export declare function createStubDirectProviderAdapter(options: StubDirectProviderAdapterOptions): MartinAdapter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createDirectProviderAdapter } from "./direct-provider.js";
|
|
2
|
+
export function createStubDirectProviderAdapter(options) {
|
|
3
|
+
const adapter = createDirectProviderAdapter({
|
|
4
|
+
providerId: options.providerId,
|
|
5
|
+
model: options.model,
|
|
6
|
+
label: options.label ?? `Stub direct provider (${options.providerId}/${options.model})`,
|
|
7
|
+
capabilities: {
|
|
8
|
+
workspaceMutations: false
|
|
9
|
+
},
|
|
10
|
+
responder: options.responder
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
...adapter,
|
|
14
|
+
adapterId: `direct:stub:${options.providerId}:${options.model}`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -26,7 +26,7 @@ export function createVerifierOnlyAdapter(options = {}) {
|
|
|
26
26
|
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl, {
|
|
27
27
|
runId: request.loopId,
|
|
28
28
|
workspaceId: request.workspaceId,
|
|
29
|
-
cwd: workingDirectory
|
|
29
|
+
cwd: workingDirectory,
|
|
30
30
|
});
|
|
31
31
|
const changedFiles = shouldTrackVerifierWrites
|
|
32
32
|
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
@@ -14,7 +14,7 @@ export function cloneExecutionPolicy(policy) {
|
|
|
14
14
|
maxUsd: policy.budget.maxUsd,
|
|
15
15
|
softLimitUsd: policy.budget.softLimitUsd,
|
|
16
16
|
maxIterations: policy.budget.maxIterations,
|
|
17
|
-
maxTokens: policy.budget.maxTokens
|
|
17
|
+
...(policy.budget.maxTokens !== undefined ? { maxTokens: policy.budget.maxTokens } : {})
|
|
18
18
|
},
|
|
19
19
|
governance: {
|
|
20
20
|
policyProfile: policy.governance.policyProfile,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MARTIN_ERROR_CATEGORIES: readonly ["invalid_input", "environment", "auth", "not_found", "store_unreadable", "verification_failed", "policy_blocked", "budget_exit", "transient"
|
|
1
|
+
export declare const MARTIN_ERROR_CATEGORIES: readonly ["invalid_input", "environment", "auth", "not_found", "store_unreadable", "verification_failed", "policy_blocked", "budget_exit", "transient"];
|
|
2
2
|
export type MartinErrorCategory = (typeof MARTIN_ERROR_CATEGORIES)[number];
|
|
3
3
|
export type MartinOutputMode = "human" | "json" | "quiet";
|
|
4
4
|
export interface MartinRunSelector {
|
|
@@ -20,7 +20,7 @@ export interface CompilerAdapterRequest {
|
|
|
20
20
|
focus: string;
|
|
21
21
|
remainingBudgetUsd: number;
|
|
22
22
|
remainingIterations: number;
|
|
23
|
-
remainingTokens
|
|
23
|
+
remainingTokens?: number;
|
|
24
24
|
};
|
|
25
25
|
previousAttempts: LoopAttempt[];
|
|
26
26
|
}
|
|
@@ -41,7 +41,7 @@ export interface PromptPacket {
|
|
|
41
41
|
budgetEnvelope: {
|
|
42
42
|
remainingBudgetUsd: number;
|
|
43
43
|
remainingIterations: number;
|
|
44
|
-
remainingTokens
|
|
44
|
+
remainingTokens?: number;
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
@@ -41,7 +41,9 @@ export function compilePromptPacket(request) {
|
|
|
41
41
|
budgetEnvelope: {
|
|
42
42
|
remainingBudgetUsd: request.context.remainingBudgetUsd,
|
|
43
43
|
remainingIterations: request.context.remainingIterations,
|
|
44
|
-
|
|
44
|
+
...(request.context.remainingTokens !== undefined
|
|
45
|
+
? { remainingTokens: request.context.remainingTokens }
|
|
46
|
+
: {})
|
|
45
47
|
}
|
|
46
48
|
};
|
|
47
49
|
}
|
|
@@ -26,7 +26,10 @@ export function createDefaultExitPolicy(budget, overrides = {}) {
|
|
|
26
26
|
schemaVersion: "exit-policy/1",
|
|
27
27
|
goal: { verifierRequired: true, minimumScore: 1 },
|
|
28
28
|
turns: { max: budget.maxIterations },
|
|
29
|
-
budget: {
|
|
29
|
+
budget: {
|
|
30
|
+
maxUsd: budget.maxUsd,
|
|
31
|
+
...(budget.maxTokens !== undefined ? { maxTokens: budget.maxTokens } : {})
|
|
32
|
+
},
|
|
30
33
|
wallClock: { maxElapsedMs: 30 * 60 * 1000 },
|
|
31
34
|
progress: { windowSize: 3, unchangedStateLimit: 3 },
|
|
32
35
|
errors: { maxConsecutive: 3 },
|
|
@@ -49,7 +52,9 @@ export function createDefaultExitPolicy(budget, overrides = {}) {
|
|
|
49
52
|
export function validateExitPolicy(policy) {
|
|
50
53
|
assertFinitePositive("turns.max", policy.turns.max);
|
|
51
54
|
assertFinitePositive("budget.maxUsd", policy.budget.maxUsd);
|
|
52
|
-
|
|
55
|
+
if (policy.budget.maxTokens !== undefined) {
|
|
56
|
+
assertFinitePositive("budget.maxTokens", policy.budget.maxTokens);
|
|
57
|
+
}
|
|
53
58
|
assertFinitePositive("wallClock.maxElapsedMs", policy.wallClock.maxElapsedMs);
|
|
54
59
|
assertFinitePositive("progress.windowSize", policy.progress.windowSize);
|
|
55
60
|
assertFinitePositive("progress.unchangedStateLimit", policy.progress.unchangedStateLimit);
|
|
@@ -117,13 +122,13 @@ export function evaluateExitPolicy(policy, snapshot) {
|
|
|
117
122
|
}
|
|
118
123
|
// 5. Budget cap
|
|
119
124
|
const usdExceeded = snapshot.actualUsd >= policy.budget.maxUsd;
|
|
120
|
-
const tokensExceeded = snapshot.tokensUsed >= policy.budget.maxTokens;
|
|
125
|
+
const tokensExceeded = policy.budget.maxTokens !== undefined && snapshot.tokensUsed >= policy.budget.maxTokens;
|
|
121
126
|
if (usdExceeded || tokensExceeded) {
|
|
122
127
|
push("budget_cap", "Run token or dollar budget reached.", {
|
|
123
128
|
actualUsd: snapshot.actualUsd,
|
|
124
129
|
maxUsd: policy.budget.maxUsd,
|
|
125
130
|
tokensUsed: snapshot.tokensUsed,
|
|
126
|
-
maxTokens: policy.budget.maxTokens,
|
|
131
|
+
...(policy.budget.maxTokens !== undefined ? { maxTokens: policy.budget.maxTokens } : {}),
|
|
127
132
|
usdExceeded,
|
|
128
133
|
tokensExceeded
|
|
129
134
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { extname, join, relative } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
5
8
|
const TEXT_EXTENSIONS = new Set([
|
|
6
9
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
7
10
|
".json", ".md", ".yaml", ".yml", ".py", ".go",
|
|
@@ -42,21 +45,42 @@ export async function loadOrBuildRepoGroundingIndex(repoRoot) {
|
|
|
42
45
|
export async function buildRepoGroundingIndex(repoRoot, repositoryFingerprint) {
|
|
43
46
|
const files = [];
|
|
44
47
|
const discovered = await walk(repoRoot, repoRoot, files, { count: 0 });
|
|
48
|
+
const trackedPaths = await loadTrackedPaths(repoRoot);
|
|
45
49
|
return {
|
|
46
50
|
schemaVersion: "martin.grounding.v1",
|
|
47
51
|
repoRoot,
|
|
48
52
|
createdAt: new Date().toISOString(),
|
|
49
53
|
fileCount: discovered.count,
|
|
50
54
|
files,
|
|
55
|
+
...(trackedPaths.length > 0 ? { trackedPaths } : {}),
|
|
51
56
|
repositoryFingerprint: repositoryFingerprint ?? await computeRepositoryFingerprint(repoRoot)
|
|
52
57
|
};
|
|
53
58
|
}
|
|
54
59
|
async function computeRepositoryFingerprint(repoRoot) {
|
|
55
60
|
const entries = [];
|
|
56
61
|
await collectFingerprintEntries(repoRoot, repoRoot, entries, { count: 0 });
|
|
62
|
+
for (const trackedPath of await loadTrackedPaths(repoRoot)) {
|
|
63
|
+
entries.push(`tracked:${trackedPath}`);
|
|
64
|
+
}
|
|
57
65
|
entries.sort();
|
|
58
66
|
return createHash("sha256").update(entries.join("\n")).digest("hex");
|
|
59
67
|
}
|
|
68
|
+
async function loadTrackedPaths(repoRoot) {
|
|
69
|
+
try {
|
|
70
|
+
const { stdout } = await execFileAsync("git", ["-C", repoRoot, "ls-files", "-z"], {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
windowsHide: true
|
|
73
|
+
});
|
|
74
|
+
return stdout
|
|
75
|
+
.split("\0")
|
|
76
|
+
.map((filePath) => filePath.trim().replace(/\\/g, "/"))
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.sort();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
60
84
|
async function collectFingerprintEntries(repoRoot, currentDir, entries, state) {
|
|
61
85
|
if (state.count >= MAX_FILES)
|
|
62
86
|
return;
|
|
@@ -193,7 +217,10 @@ function tokenize(input) {
|
|
|
193
217
|
export function scanPatchForGroundingViolations(diff, index, options = {}) {
|
|
194
218
|
const violations = [];
|
|
195
219
|
const resolvedFiles = [];
|
|
196
|
-
const indexedPaths = new Set(
|
|
220
|
+
const indexedPaths = new Set([
|
|
221
|
+
...index.files.map((file) => file.path),
|
|
222
|
+
...(index.trackedPaths ?? [])
|
|
223
|
+
]);
|
|
197
224
|
const indexedSymbols = new Set(index.files.flatMap((file) => file.symbols.map((symbol) => symbol.toLowerCase())));
|
|
198
225
|
const referencedFiles = new Set();
|
|
199
226
|
const addedFiles = new Set();
|
|
@@ -75,7 +75,7 @@ export interface MartinAdapterRequest {
|
|
|
75
75
|
focus: string;
|
|
76
76
|
remainingBudgetUsd: number;
|
|
77
77
|
remainingIterations: number;
|
|
78
|
-
remainingTokens
|
|
78
|
+
remainingTokens?: number;
|
|
79
79
|
};
|
|
80
80
|
previousAttempts: LoopAttempt[];
|
|
81
81
|
/** Abort signal propagated from the harness — adapters should honour it. */
|
|
@@ -165,7 +165,7 @@ export interface DistilledContext {
|
|
|
165
165
|
constraints: {
|
|
166
166
|
remainingBudgetUsd: number;
|
|
167
167
|
remainingIterations: number;
|
|
168
|
-
remainingTokens
|
|
168
|
+
remainingTokens?: number;
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
export interface AttemptPolicyDecision {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
1
|
import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, appendLoopEvent, createLoopRecord } from "../contracts/index.js";
|
|
4
2
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
|
|
5
3
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
@@ -154,7 +152,9 @@ export function distillContext(loop, options = {}) {
|
|
|
154
152
|
constraints: {
|
|
155
153
|
remainingBudgetUsd: roundUsd(loop.budget.maxUsd - loop.cost.actualUsd),
|
|
156
154
|
remainingIterations: Math.max(loop.budget.maxIterations - loop.attempts.length, 0),
|
|
157
|
-
|
|
155
|
+
...(loop.budget.maxTokens !== undefined
|
|
156
|
+
? { remainingTokens: Math.max(loop.budget.maxTokens - loop.cost.tokensIn - loop.cost.tokensOut, 0) }
|
|
157
|
+
: {})
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
}
|
|
@@ -796,6 +796,7 @@ export async function runMartin(input) {
|
|
|
796
796
|
const attemptCompletedAt = now();
|
|
797
797
|
const compiledContext = compilePromptPacket(request);
|
|
798
798
|
const expectedVerifierBinding = resolveExpectedVerifierBinding(request);
|
|
799
|
+
const verifierConfigured = expectedVerifierBinding.commands.length > 0;
|
|
799
800
|
const verification = normalizeVerificationOutcome(result, expectedVerifierBinding);
|
|
800
801
|
// PATCH → VERIFY
|
|
801
802
|
currentPhase = "VERIFY";
|
|
@@ -1135,17 +1136,12 @@ export async function runMartin(input) {
|
|
|
1135
1136
|
// Uses the task's repoRoot to build/load the grounding index, then scans any diff
|
|
1136
1137
|
let groundingScanResult;
|
|
1137
1138
|
const patchDiff = buildPatchDiff(result, changedFiles);
|
|
1138
|
-
|
|
1139
|
-
const hasReportedOrObservedChanges = changedFiles.length > 0 || (result.execution?.changedFiles?.length ?? 0) > 0;
|
|
1140
|
-
const groundingEvidenceRequired = result.status === "completed" && workspaceMutationCapable && hasReportedOrObservedChanges;
|
|
1141
|
-
let groundingEvidenceAvailable = !groundingEvidenceRequired;
|
|
1142
|
-
if (groundingEvidenceRequired && patchDiff && input.task.repoRoot) {
|
|
1139
|
+
if (patchDiff && input.task.repoRoot) {
|
|
1143
1140
|
try {
|
|
1144
1141
|
const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
|
|
1145
|
-
groundingScanResult =
|
|
1142
|
+
groundingScanResult = scanPatchForGroundingViolations(patchDiff, groundingIndex, {
|
|
1146
1143
|
allowedPaths: input.task.allowedPaths
|
|
1147
|
-
})
|
|
1148
|
-
groundingEvidenceAvailable = true;
|
|
1144
|
+
});
|
|
1149
1145
|
if (input.store && groundingScanResult.violations.length > 0) {
|
|
1150
1146
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1151
1147
|
kind: "grounding.violations_found",
|
|
@@ -1161,22 +1157,10 @@ export async function runMartin(input) {
|
|
|
1161
1157
|
}
|
|
1162
1158
|
}
|
|
1163
1159
|
catch {
|
|
1164
|
-
//
|
|
1165
|
-
// grounding inputs follow the same recover-forward trust contract.
|
|
1160
|
+
// Grounding scan is best-effort — never fail the loop because of a scan error
|
|
1166
1161
|
}
|
|
1167
1162
|
}
|
|
1168
|
-
|
|
1169
|
-
// The completed work remains useful, but unavailable grounding evidence
|
|
1170
|
-
// cannot be converted into a zero-violation governance claim.
|
|
1171
|
-
loop = {
|
|
1172
|
-
...loop,
|
|
1173
|
-
metadata: {
|
|
1174
|
-
...loop.metadata,
|
|
1175
|
-
groundingEvidenceStatus: "unavailable"
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && groundingEvidenceAvailable;
|
|
1163
|
+
const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && verifierConfigured;
|
|
1180
1164
|
let patchDecision;
|
|
1181
1165
|
if (shouldEvaluatePatchTruth) {
|
|
1182
1166
|
patchDecision = evaluatePatchDecision({
|
|
@@ -1237,7 +1221,7 @@ export async function runMartin(input) {
|
|
|
1237
1221
|
}
|
|
1238
1222
|
else {
|
|
1239
1223
|
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1240
|
-
kind: verification.passed ? "attempt.kept" : "attempt.discarded",
|
|
1224
|
+
kind: verification.passed || !verifierConfigured ? "attempt.kept" : "attempt.discarded",
|
|
1241
1225
|
runId: loop.loopId,
|
|
1242
1226
|
attemptIndex: currentAttemptIndex,
|
|
1243
1227
|
payload: { reason: verification.summary }
|
|
@@ -1314,6 +1298,7 @@ export async function runMartin(input) {
|
|
|
1314
1298
|
lastResult: effectiveResult,
|
|
1315
1299
|
lastFailure: failure,
|
|
1316
1300
|
costState,
|
|
1301
|
+
verificationRequired: verifierConfigured,
|
|
1317
1302
|
canSwitchAdapter: failure?.recommendedIntervention === "switch_adapter" &&
|
|
1318
1303
|
adapterChain[currentAdapterIndex] !== undefined &&
|
|
1319
1304
|
currentAdapter.adapterId !== executingAdapter.adapterId
|
|
@@ -1578,9 +1563,6 @@ function mergeCostProvenance(previous, current) {
|
|
|
1578
1563
|
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1579
1564
|
}
|
|
1580
1565
|
function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
1581
|
-
if (repoRoot && rollbackBoundary?.headRef) {
|
|
1582
|
-
return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1583
|
-
}
|
|
1584
1566
|
if (result.execution?.changedFiles !== undefined) {
|
|
1585
1567
|
return result.execution.changedFiles;
|
|
1586
1568
|
}
|
|
@@ -1601,41 +1583,6 @@ function buildPatchDiff(result, changedFiles) {
|
|
|
1601
1583
|
}
|
|
1602
1584
|
return undefined;
|
|
1603
1585
|
}
|
|
1604
|
-
function reconcileExistingGroundingFiles(scan, repoRoot, boundary) {
|
|
1605
|
-
const resolvedFiles = new Set(scan.resolvedFiles);
|
|
1606
|
-
const violations = scan.violations.filter((violation) => {
|
|
1607
|
-
if (violation.kind !== "file_not_found") {
|
|
1608
|
-
return true;
|
|
1609
|
-
}
|
|
1610
|
-
const absolutePath = resolve(repoRoot, violation.reference);
|
|
1611
|
-
const relativePath = relative(repoRoot, absolutePath);
|
|
1612
|
-
const isInsideRepo = relativePath.length > 0 && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
1613
|
-
if (!isInsideRepo || !isTrackedRepoFileAtBoundary(repoRoot, violation.reference, boundary)) {
|
|
1614
|
-
return true;
|
|
1615
|
-
}
|
|
1616
|
-
resolvedFiles.add(violation.reference);
|
|
1617
|
-
return false;
|
|
1618
|
-
});
|
|
1619
|
-
return {
|
|
1620
|
-
...scan,
|
|
1621
|
-
violations,
|
|
1622
|
-
resolvedFiles: [...resolvedFiles]
|
|
1623
|
-
};
|
|
1624
|
-
}
|
|
1625
|
-
function isTrackedRepoFileAtBoundary(repoRoot, filePath, boundary) {
|
|
1626
|
-
if (!boundary) {
|
|
1627
|
-
return false;
|
|
1628
|
-
}
|
|
1629
|
-
const normalizedPath = filePath.replace(/\\/gu, "/");
|
|
1630
|
-
if (boundary.snapshots.some((snapshot) => snapshot.existed && snapshot.path === normalizedPath)) {
|
|
1631
|
-
return true;
|
|
1632
|
-
}
|
|
1633
|
-
if (!boundary.headRef) {
|
|
1634
|
-
return false;
|
|
1635
|
-
}
|
|
1636
|
-
const result = spawnSync("git", ["-C", repoRoot, "cat-file", "-e", `${boundary.headRef}:${normalizedPath}`], { encoding: "utf8", stdio: "ignore", windowsHide: true });
|
|
1637
|
-
return result.status === 0;
|
|
1638
|
-
}
|
|
1639
1586
|
function createBudgetSettlement(input) {
|
|
1640
1587
|
const totalActualUsd = getUsageUsd(input.usage);
|
|
1641
1588
|
return {
|