@martinloop/mcp 0.3.9 → 0.5.1

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 (113) hide show
  1. package/README.md +82 -141
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +1 -1
  4. package/dist/resources.d.ts +1 -2
  5. package/dist/resources.js +62 -98
  6. package/dist/server-validation.d.ts +1 -1
  7. package/dist/server-validation.js +16 -11
  8. package/dist/server.d.ts +23 -0
  9. package/dist/server.js +192 -15
  10. package/dist/tools/doctor.js +5 -4
  11. package/dist/tools/get-status.d.ts +2 -0
  12. package/dist/tools/get-status.js +1 -0
  13. package/dist/tools/preflight.d.ts +2 -0
  14. package/dist/tools/preflight.js +43 -3
  15. package/dist/tools/run-dossier.d.ts +3 -1
  16. package/dist/tools/run-dossier.js +40 -2
  17. package/dist/tools/run-loop.d.ts +5 -3
  18. package/dist/tools/run-loop.js +15 -9
  19. package/dist/tools/tool-errors.js +1 -1
  20. package/dist/tools/tool-response.js +9 -2
  21. package/dist/tools/tool-support.d.ts +4 -2
  22. package/dist/tools/tool-support.js +6 -15
  23. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  24. package/dist/vendor/adapters/claude-cli.js +97 -69
  25. package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
  26. package/dist/vendor/adapters/cli-bridge.js +54 -13
  27. package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
  28. package/dist/vendor/adapters/codex-launcher.js +118 -19
  29. package/dist/vendor/adapters/index.d.ts +2 -4
  30. package/dist/vendor/adapters/index.js +2 -4
  31. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  32. package/dist/vendor/adapters/openai-compatible.js +80 -15
  33. package/dist/vendor/adapters/verifier-only.js +9 -4
  34. package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
  35. package/dist/vendor/contracts/context-handoff.d.ts +100 -0
  36. package/dist/vendor/contracts/context-handoff.js +11 -0
  37. package/dist/vendor/contracts/context-manifest.d.ts +195 -0
  38. package/dist/vendor/contracts/context-manifest.js +17 -0
  39. package/dist/vendor/contracts/context-shadow.d.ts +75 -0
  40. package/dist/vendor/contracts/context-shadow.js +8 -0
  41. package/dist/vendor/contracts/delivery.d.ts +47 -0
  42. package/dist/vendor/contracts/delivery.js +18 -0
  43. package/dist/vendor/contracts/exits.d.ts +126 -0
  44. package/dist/vendor/contracts/exits.js +32 -0
  45. package/dist/vendor/contracts/index.d.ts +55 -6
  46. package/dist/vendor/contracts/index.js +24 -0
  47. package/dist/vendor/contracts/mission.d.ts +102 -0
  48. package/dist/vendor/contracts/mission.js +64 -0
  49. package/dist/vendor/contracts/operator.d.ts +3 -1
  50. package/dist/vendor/contracts/operator.js +2 -1
  51. package/dist/vendor/contracts/verified-handoff.d.ts +106 -0
  52. package/dist/vendor/contracts/verified-handoff.js +40 -0
  53. package/dist/vendor/core/context-chain-gate.d.ts +89 -0
  54. package/dist/vendor/core/context-chain-gate.js +173 -0
  55. package/dist/vendor/core/context-compiler.d.ts +53 -0
  56. package/dist/vendor/core/context-compiler.js +261 -0
  57. package/dist/vendor/core/context-handoff.d.ts +42 -0
  58. package/dist/vendor/core/context-handoff.js +167 -0
  59. package/dist/vendor/core/context-shadow.d.ts +39 -0
  60. package/dist/vendor/core/context-shadow.js +200 -0
  61. package/dist/vendor/core/delivery/index.d.ts +6 -0
  62. package/dist/vendor/core/delivery/index.js +4 -0
  63. package/dist/vendor/core/delivery/message-client.d.ts +18 -0
  64. package/dist/vendor/core/delivery/message-client.js +38 -0
  65. package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
  66. package/dist/vendor/core/delivery/message-ledger.js +97 -0
  67. package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
  68. package/dist/vendor/core/delivery/message-schema.js +111 -0
  69. package/dist/vendor/core/delivery/update-check.d.ts +19 -0
  70. package/dist/vendor/core/delivery/update-check.js +107 -0
  71. package/dist/vendor/core/exit-signal.d.ts +88 -0
  72. package/dist/vendor/core/exit-signal.js +339 -0
  73. package/dist/vendor/core/exits.d.ts +41 -0
  74. package/dist/vendor/core/exits.js +258 -0
  75. package/dist/vendor/core/grounding.d.ts +2 -1
  76. package/dist/vendor/core/grounding.js +40 -5
  77. package/dist/vendor/core/index.d.ts +47 -6
  78. package/dist/vendor/core/index.js +1102 -670
  79. package/dist/vendor/core/mission/aggregate.d.ts +27 -0
  80. package/dist/vendor/core/mission/aggregate.js +42 -0
  81. package/dist/vendor/core/mission/index.d.ts +2 -0
  82. package/dist/vendor/core/mission/index.js +1 -0
  83. package/dist/vendor/core/persistence/compiler.d.ts +8 -0
  84. package/dist/vendor/core/persistence/compiler.js +156 -0
  85. package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
  86. package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
  87. package/dist/vendor/core/persistence/index.d.ts +4 -0
  88. package/dist/vendor/core/persistence/index.js +2 -0
  89. package/dist/vendor/core/persistence/ledger.d.ts +1 -1
  90. package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
  91. package/dist/vendor/core/persistence/mission-store.js +281 -0
  92. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  93. package/dist/vendor/core/policy.d.ts +2 -0
  94. package/dist/vendor/core/policy.js +11 -8
  95. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  96. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  97. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  98. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  99. package/dist/vendor/core/rollback.js +15 -1
  100. package/dist/vendor/core/routing.d.ts +0 -42
  101. package/dist/vendor/core/routing.js +4 -154
  102. package/dist/vendor/core/savings.d.ts +13 -0
  103. package/dist/vendor/core/savings.js +22 -0
  104. package/dist/vendor/core/termination-store.d.ts +15 -0
  105. package/dist/vendor/core/termination-store.js +44 -0
  106. package/dist/vendor/core/verified-handoff.d.ts +72 -0
  107. package/dist/vendor/core/verified-handoff.js +238 -0
  108. package/package.json +4 -10
  109. package/server.json +3 -3
  110. package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
  111. package/dist/vendor/adapters/stub-agent-cli.js +0 -40
  112. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  113. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -14,7 +14,7 @@
14
14
  * Usage:
15
15
  * # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
16
16
  * MARTIN_OPENAI_API_KEY=sk-...
17
- * MARTIN_OPENAI_MODEL=gpt-4.1-mini
17
+ * MARTIN_OPENAI_MODEL=<provider-model-id>
18
18
  * martin-loop run "fix the bug" --engine openai
19
19
  *
20
20
  * # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
@@ -69,6 +69,37 @@ function estimateCost(model, inputChars, outputChars) {
69
69
  const actualUsd = (tokensIn / 1000) * pricing.inputPer1K + (tokensOut / 1000) * pricing.outputPer1K;
70
70
  return { tokensIn, tokensOut, actualUsd };
71
71
  }
72
+ function normalizeOpenAiCompatibleUsage(input) {
73
+ const hasKnownPricing = KNOWN_MODEL_PRICING[input.model] !== undefined;
74
+ const pricing = KNOWN_MODEL_PRICING[input.model] ?? {
75
+ inputPer1K: FALLBACK_INPUT_PER_1K,
76
+ outputPer1K: FALLBACK_OUTPUT_PER_1K
77
+ };
78
+ const actualUsd = (input.tokensIn / 1000) * pricing.inputPer1K +
79
+ (input.tokensOut / 1000) * pricing.outputPer1K;
80
+ const provenance = input.usageWasFullyProviderReported && hasKnownPricing ? "calculated" : "estimated";
81
+ return normalizeUsage({
82
+ actualUsd,
83
+ ...(provenance === "estimated" ? { estimatedUsd: actualUsd } : {}),
84
+ tokensIn: input.tokensIn,
85
+ tokensOut: input.tokensOut,
86
+ provenance,
87
+ providerSettlement: {
88
+ providerId: "openai-compatible",
89
+ model: input.model,
90
+ transport: "http",
91
+ source: input.usageWasFullyProviderReported ? "openai_compatible_json" : "estimated_fallback",
92
+ inputTokens: input.tokensIn,
93
+ outputTokens: input.tokensOut,
94
+ billingMode: input.billingMode,
95
+ modelSource: input.modelSource,
96
+ pricingSource: hasKnownPricing ? "static_catalog" : "blended_fallback",
97
+ pricingVersion: "embedded-v1",
98
+ rawUsageAvailable: input.usageWasFullyProviderReported,
99
+ settledAt: new Date().toISOString()
100
+ }
101
+ });
102
+ }
72
103
  // ---------------------------------------------------------------------------
73
104
  // Prompt builder
74
105
  // ---------------------------------------------------------------------------
@@ -80,12 +111,11 @@ Follow these rules exactly:
80
111
  - Be precise, minimal, and test-backed in all changes.
81
112
  - State what you changed and why at the end of your response.`;
82
113
  export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
83
- export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
84
114
  export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
85
115
  const apiKey = env["MARTIN_OPENAI_API_KEY"] ?? "";
86
116
  return {
87
117
  baseUrl: env["MARTIN_OPENAI_BASE_URL"] ?? DEFAULT_OPENAI_BASE_URL,
88
- model: env["MARTIN_OPENAI_MODEL"] ?? DEFAULT_OPENAI_MODEL,
118
+ ...(env["MARTIN_OPENAI_MODEL"] ? { model: env["MARTIN_OPENAI_MODEL"] } : {}),
89
119
  apiKey,
90
120
  apiKeyConfigured: apiKey.length > 0,
91
121
  authPosture: apiKey.length > 0 ? "api_key" : "anonymous_or_local"
@@ -126,7 +156,16 @@ export function createOpenAiCompatibleAdapter(options) {
126
156
  const runtimeConfig = resolveOpenAiCompatibleRuntimeConfig();
127
157
  const baseUrl = (options.baseUrl ?? runtimeConfig.baseUrl).replace(/\/$/, "");
128
158
  const model = options.model ?? runtimeConfig.model;
159
+ if (!model) {
160
+ throw new Error("MODEL_CONFIGURATION_REQUIRED: set --model or MARTIN_OPENAI_MODEL for direct OpenAI-compatible execution.");
161
+ }
129
162
  const apiKey = options.apiKey ?? runtimeConfig.apiKey;
163
+ const modelSource = options.model ? "explicit_override" : "provider_configured";
164
+ const billingMode = /^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])/iu.test(baseUrl)
165
+ ? "local_unmetered"
166
+ : apiKey
167
+ ? "metered_api"
168
+ : "unknown";
130
169
  return {
131
170
  adapterId: `openai-compatible:${model}`,
132
171
  kind: "direct-provider",
@@ -172,10 +211,12 @@ export function createOpenAiCompatibleAdapter(options) {
172
211
  // indicate a permanent configuration problem.
173
212
  const MAX_RETRIES = 3;
174
213
  const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
214
+ const NON_RETRYABLE_STATUS = new Set([400, 401, 403]);
175
215
  const endpoint = `${baseUrl}/v1/chat/completions`;
176
216
  let responseText = "";
177
217
  let tokensIn = estimated.tokensIn;
178
218
  let tokensOut = 0;
219
+ let usageWasFullyProviderReported = false;
179
220
  const headers = { "Content-Type": "application/json" };
180
221
  if (apiKey)
181
222
  headers["Authorization"] = `Bearer ${apiKey}`;
@@ -207,6 +248,17 @@ export function createOpenAiCompatibleAdapter(options) {
207
248
  const body = (await res.json());
208
249
  if (!res.ok || body.error) {
209
250
  const errMsg = body.error?.message ?? `HTTP ${res.status}`;
251
+ // Fail immediately on non-retryable errors (auth, bad request)
252
+ if (NON_RETRYABLE_STATUS.has(res.status)) {
253
+ return {
254
+ status: "failed",
255
+ summary: `${model} API error: ${errMsg}`,
256
+ usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
257
+ verification: { passed: false, summary: "API call failed before verifier." },
258
+ failure: { message: errMsg, classHint: "infrastructure_error" }
259
+ };
260
+ }
261
+ // Retry on transient errors
210
262
  if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES - 1) {
211
263
  lastError = errMsg;
212
264
  // Exponential backoff: 1s, 2s, 4s
@@ -223,8 +275,13 @@ export function createOpenAiCompatibleAdapter(options) {
223
275
  }
224
276
  responseText = body.choices?.[0]?.message?.content ?? "";
225
277
  if (body.usage) {
226
- tokensIn = body.usage.prompt_tokens ?? tokensIn;
227
- tokensOut = body.usage.completion_tokens ?? 0;
278
+ const providerPromptTokens = body.usage.prompt_tokens;
279
+ const providerCompletionTokens = body.usage.completion_tokens;
280
+ usageWasFullyProviderReported =
281
+ typeof providerPromptTokens === "number" &&
282
+ typeof providerCompletionTokens === "number";
283
+ tokensIn = providerPromptTokens ?? tokensIn;
284
+ tokensOut = providerCompletionTokens ?? Math.ceil(responseText.length / CHARS_PER_TOKEN);
228
285
  }
229
286
  else {
230
287
  tokensOut = Math.ceil(responseText.length / CHARS_PER_TOKEN);
@@ -267,33 +324,41 @@ export function createOpenAiCompatibleAdapter(options) {
267
324
  return {
268
325
  status: "failed",
269
326
  summary: `${model} returned an empty response.`,
270
- usage: normalizeUsage({ actualUsd: 0, tokensIn, tokensOut: 0, provenance: "actual" }),
327
+ usage: normalizeOpenAiCompatibleUsage({
328
+ model,
329
+ tokensIn,
330
+ tokensOut: 0,
331
+ usageWasFullyProviderReported,
332
+ modelSource,
333
+ billingMode
334
+ }),
271
335
  verification: { passed: false, summary: "Empty response — nothing to verify." },
272
336
  failure: { message: "empty_response" }
273
337
  };
274
338
  }
275
339
  // Run verification
276
- const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
340
+ const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, undefined, {
341
+ runId: request.loopId,
342
+ workspaceId: request.workspaceId,
343
+ cwd: workingDirectory,
344
+ });
277
345
  const execution = {
278
346
  changedFiles: hasVerificationSteps
279
347
  ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
280
348
  : []
281
349
  };
282
- const pricing = KNOWN_MODEL_PRICING[model] ?? {
283
- inputPer1K: FALLBACK_INPUT_PER_1K,
284
- outputPer1K: FALLBACK_OUTPUT_PER_1K
285
- };
286
- const actualUsd = (tokensIn / 1000) * pricing.inputPer1K + (tokensOut / 1000) * pricing.outputPer1K;
287
350
  return {
288
351
  status: verification.passed ? "completed" : "failed",
289
352
  summary: verification.passed
290
353
  ? `${model} completed the task. Verifier passed.`
291
354
  : `${model} completed but verifier failed: ${verification.summary}`,
292
- usage: normalizeUsage({
293
- actualUsd,
355
+ usage: normalizeOpenAiCompatibleUsage({
356
+ model,
294
357
  tokensIn,
295
358
  tokensOut,
296
- provenance: "actual"
359
+ usageWasFullyProviderReported,
360
+ modelSource,
361
+ billingMode
297
362
  }),
298
363
  verification,
299
364
  execution,
@@ -13,18 +13,23 @@ export function createVerifierOnlyAdapter(options = {}) {
13
13
  transport: "cli",
14
14
  capabilities: createAdapterCapabilities({
15
15
  usageSettlement: true,
16
- diffArtifacts: true
16
+ diffArtifacts: true,
17
+ workspaceMutations: false
17
18
  })
18
19
  },
19
20
  async execute(request) {
20
21
  const shouldTrackVerifierWrites = request.context.verificationPlan.length > 0 ||
21
22
  (request.context.verificationStack?.length ?? 0) > 0;
22
23
  const baselineChangedFiles = shouldTrackVerifierWrites
23
- ? new Set(await readGitChangedFiles(workingDirectory, 5_000, options.spawnImpl))
24
+ ? new Set(await readGitChangedFiles(workingDirectory, 5_000))
24
25
  : new Set();
25
- const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl);
26
+ const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl, {
27
+ runId: request.loopId,
28
+ workspaceId: request.workspaceId,
29
+ cwd: workingDirectory
30
+ });
26
31
  const changedFiles = shouldTrackVerifierWrites
27
- ? (await readGitChangedFiles(workingDirectory, 5_000, options.spawnImpl)).filter((file) => !baselineChangedFiles.has(file))
32
+ ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
28
33
  : [];
29
34
  const execution = { changedFiles };
30
35
  if (verification.passed) {
@@ -1,5 +1,5 @@
1
1
  export type DiffVisibilityLevel = "none" | "git" | "adapter_reported";
2
- export type VerifierCompatibility = "full" | "verify_only" | "unsupported";
2
+ export type VerifierCompatibility = "full" | "proof" | "unsupported";
3
3
  export type SandboxExpectation = "host_process" | "workspace_write" | "provider_managed" | "not_applicable";
4
4
  export type LaunchReadiness = "path_lookup" | "configured_endpoint" | "built_in";
5
5
  export type AdapterUsageSettlement = "actual" | "estimated" | "unavailable";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Context Handoff — A-CTX-2 contracts.
3
+ *
4
+ * Chain verification circuit breaker for upstream-to-downstream context handoff.
5
+ *
6
+ * Rule: a downstream agent MUST NOT execute before verifyContextHandoff returns ok=true.
7
+ * Healthy handoffs are silent. Broken handoffs stop execution and return one reason code
8
+ * plus one recovery instruction. Never silently discard excluded or unverifiable context.
9
+ */
10
+ /**
11
+ * Overall integrity state of the upstream chain.
12
+ * "verified" — all upstream receipts and artifacts check out
13
+ * "evidence_boundary" — upstream integrity could not be established (no receipt)
14
+ * "tamper_detected" — receipt hash or artifact hash does not match
15
+ * "incomplete" — required fields or artifacts are absent
16
+ * "unsupported_schema"— handoff schemaVersion is not recognised
17
+ */
18
+ export type ChainIntegrityState = "verified" | "evidence_boundary" | "tamper_detected" | "incomplete" | "unsupported_schema";
19
+ export type HandoffClaimState = "verified" | "unverified" | "rejected" | "unknown";
20
+ /** A single verifiable statement carried across the handoff boundary. */
21
+ export interface ContextHandoffClaim {
22
+ claimId: string;
23
+ statement: string;
24
+ evidenceRefs: string[];
25
+ verificationState: HandoffClaimState;
26
+ }
27
+ /** A content-addressed artifact that must be present and hash-stable across the boundary. */
28
+ export interface ContextHandoffArtifact {
29
+ path?: string;
30
+ sha256: string;
31
+ required: boolean;
32
+ label?: string;
33
+ }
34
+ export declare const HANDOFF_SCHEMA_VERSION: "martin.handoff.v1";
35
+ /**
36
+ * Immutable record produced by the upstream agent and verified before the
37
+ * downstream agent executes.
38
+ *
39
+ * upstreamIntegrity propagates from the parent handoff — it can never be
40
+ * upgraded by downstream success. A "tamper_detected" upstream remains
41
+ * "tamper_detected" regardless of what the current agent does.
42
+ */
43
+ export interface ContextHandoffReceipt {
44
+ schemaVersion: typeof HANDOFF_SCHEMA_VERSION;
45
+ handoffId: string;
46
+ chainId: string;
47
+ missionId?: string;
48
+ producerRunId: string;
49
+ /** SHA-256 of the upstream run's canonical receipt file. */
50
+ producerReceiptHash: string;
51
+ /** Parent handoff IDs for multi-hop chain lineage. */
52
+ parentHandoffIds?: string[];
53
+ claims: ContextHandoffClaim[];
54
+ artifacts: ContextHandoffArtifact[];
55
+ /** Free-text assumptions that have not yet been verified at handoff time. */
56
+ unresolvedAssumptions: string[];
57
+ upstreamIntegrity: "verified" | "evidence_boundary" | "tamper_detected" | "incomplete";
58
+ createdAt: string;
59
+ }
60
+ /**
61
+ * Safe ledger record for a context object that was denied before reaching
62
+ * executable context.
63
+ *
64
+ * MUST NOT contain the secret value or raw denied content — only safe
65
+ * identity, hashes, and reason codes.
66
+ */
67
+ export interface ContextExclusionDecision {
68
+ decision: "excluded";
69
+ objectId: string;
70
+ reasonCode: "secret_detected" | "policy_denied" | "authority_boundary" | "integrity_unverified";
71
+ reason: string;
72
+ /** SHA-256 of the denied content — never the content itself. */
73
+ sourceHash?: string;
74
+ excludedAt: string;
75
+ }
76
+ export interface ContextHandoffVerification {
77
+ ok: boolean;
78
+ integrity: ChainIntegrityState;
79
+ reasons: Array<{
80
+ code: string;
81
+ message: string;
82
+ claimId?: string;
83
+ artifactSha256?: string;
84
+ }>;
85
+ }
86
+ /**
87
+ * Decision produced by decideContextCircuitBreak.
88
+ *
89
+ * Healthy: { shouldStop: false, silent: true }
90
+ * Broken: { shouldStop: true, silent: false, reasonCode, message, nextAction }
91
+ */
92
+ export interface ContextCircuitBreakResult {
93
+ shouldStop: boolean;
94
+ /** true = healthy, no human-visible output; false = must surface to caller */
95
+ silent: boolean;
96
+ verification: ContextHandoffVerification;
97
+ reasonCode?: string;
98
+ message?: string;
99
+ nextAction?: string;
100
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Context Handoff — A-CTX-2 contracts.
3
+ *
4
+ * Chain verification circuit breaker for upstream-to-downstream context handoff.
5
+ *
6
+ * Rule: a downstream agent MUST NOT execute before verifyContextHandoff returns ok=true.
7
+ * Healthy handoffs are silent. Broken handoffs stop execution and return one reason code
8
+ * plus one recovery instruction. Never silently discard excluded or unverifiable context.
9
+ */
10
+ // ─── Receipt ──────────────────────────────────────────────────────────────────
11
+ export const HANDOFF_SCHEMA_VERSION = "martin.handoff.v1";
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Context Runtime — A-CTX-1 contracts.
3
+ *
4
+ * Defines the governed context layer: objects, budgets, manifests, ledger
5
+ * entries, fault protocol, and continuation checkpoints.
6
+ *
7
+ * Non-negotiable invariants (enforced in implementation, not type system):
8
+ * - No `confidence` float anywhere — use UsageEvidence enums only
9
+ * - No `outcome_delta` field anywhere — permanently banned
10
+ * - `nowMs` is always a captured input; never Date.now() inside a compiler
11
+ * - Secret scan hit → deny persistence entirely; reason-coded ledger entry only
12
+ * - Fault budget accumulates by taskId, NOT by runId
13
+ */
14
+ /**
15
+ * Evidence quality for a metric or measurement.
16
+ *
17
+ * "observed" = distinctive substring from the object appears verbatim in model output
18
+ * "inferred" = weak kind correlation, narrow and documented per kind
19
+ * "unknown" = default; metrics MUST be withheld, never reported as zero
20
+ */
21
+ export type UsageEvidence = "observed" | "inferred" | "unknown";
22
+ export type ContextKind = "mission" | "policy" | "security" | "task" | "continuation" | "working_diff" | "diagnostic" | "source" | "test" | "decision" | "todo" | "receipt_summary" | "tool_result";
23
+ export type ContextPriority = "required" | "high" | "normal" | "low";
24
+ export type ContextTrust = "authoritative" | "workspace" | "external" | "untrusted";
25
+ export type ContextSensitivity = "public" | "workspace" | "restricted" | "secret";
26
+ /** A typed, hashed, provenance-tracked object eligible for working-set inclusion. */
27
+ export interface ContextObject {
28
+ /** Stable identifier across revisions. */
29
+ id: string;
30
+ kind: ContextKind;
31
+ priority: ContextPriority;
32
+ trust: ContextTrust;
33
+ sensitivity: ContextSensitivity;
34
+ /** File path, receipt hash, or MCP URI — where the object originates. */
35
+ sourceRef: string;
36
+ /** SHA-256 of raw content. */
37
+ contentHash: string;
38
+ /** Git SHA or equivalent revision of the source file, if known. */
39
+ sourceRevisionHash?: string;
40
+ /** Pre-render token estimate used during selection. */
41
+ estimatedTokens: number;
42
+ /** Set iff the object was manually pinned; counts against pinnedTokensMax. */
43
+ pinnedBy?: string;
44
+ tags?: string[];
45
+ metadata?: Record<string, unknown>;
46
+ }
47
+ /**
48
+ * Token budget for one context compilation pass.
49
+ *
50
+ * pinnedTokensMax REQUIRED: caps manual-pin tokens to prevent deterministic
51
+ * selector starvation. Any pin that would exceed this cap must be rejected.
52
+ */
53
+ export interface ContextBudget {
54
+ modelWindowTokens: number;
55
+ systemReserveTokens: number;
56
+ outputReserveTokens: number;
57
+ toolReserveTokens: number;
58
+ overflowReserveTokens: number;
59
+ maxWorkingSetTokens: number;
60
+ /** Hard cap on manually-pinned token allocation. */
61
+ pinnedTokensMax: number;
62
+ }
63
+ /**
64
+ * The compiler's per-object inclusion/exclusion record.
65
+ *
66
+ * "included_truncated" is DISTINCT from "included" — truncation must be
67
+ * explicit in every receipt. Never use "included" for a truncated object.
68
+ */
69
+ export interface ContextCandidateDecision {
70
+ objectId: string;
71
+ decision: "included" | "included_truncated" | "excluded" | "deferred" | "fault_loaded";
72
+ /**
73
+ * Only present when decision === "included_truncated".
74
+ * Records the pre-truncation token count for receipts.
75
+ */
76
+ truncatedFromTokens?: number;
77
+ /** Machine-readable reason code for this decision. */
78
+ reason: string;
79
+ /** Actual post-render token count after adapter recount. Omitted pre-render. */
80
+ renderedTokens?: number;
81
+ }
82
+ export declare const CONTEXT_MANIFEST_VERSION: "context-manifest/1";
83
+ /**
84
+ * Immutable record of one context compilation pass.
85
+ *
86
+ * Determinism invariant:
87
+ * same inputs + policyHash + adapterVersion + nowMs → same manifestHash
88
+ *
89
+ * nowMs MUST be an explicit captured input — never Date.now() inside the
90
+ * compiler or rankContextObject.
91
+ */
92
+ export interface ContextManifest {
93
+ schemaVersion: typeof CONTEXT_MANIFEST_VERSION;
94
+ /** SHA-256 of (decisions + budget + policyHash + adapterVersion + nowMs). */
95
+ manifestHash: string;
96
+ taskId: string;
97
+ runId: string;
98
+ /** Explicit input — never Date.now() inside the compiler. */
99
+ nowMs: number;
100
+ adapterVersion: string;
101
+ policyHash: string;
102
+ budget: ContextBudget;
103
+ decisions: ContextCandidateDecision[];
104
+ totalRenderedTokens: number;
105
+ /** Render pass count; must be ≤ MAX_RENDER_PASSES (5). */
106
+ renderPasses: number;
107
+ compilationDurationMs: number;
108
+ }
109
+ export declare const CONTEXT_LEDGER_VERSION: "context-ledger/1";
110
+ /**
111
+ * Fourth ledger entry type — on the SAME receipt chain as cost and verification.
112
+ *
113
+ * FORBIDDEN: confidence field, outcome_delta field — both permanently banned.
114
+ * overflowCount and faultBudgetUsed accumulate by taskId, NOT by runId.
115
+ */
116
+ export interface ContextLedgerEntry {
117
+ schemaVersion: typeof CONTEXT_LEDGER_VERSION;
118
+ entryType: "context";
119
+ taskId: string;
120
+ runId: string;
121
+ manifestHash: string;
122
+ totalIncluded: number;
123
+ totalExcluded: number;
124
+ totalFaultLoaded: number;
125
+ /** Cumulative overflow count — resets per task, never per run. */
126
+ overflowCount: number;
127
+ /** Cumulative fault budget consumed — resets per task, never per run. */
128
+ faultBudgetUsed: number;
129
+ adapterVersion: string;
130
+ }
131
+ /** Request to demand-load a missing context object on fault. */
132
+ export interface ContextFaultRequest {
133
+ objectId: string;
134
+ taskId: string;
135
+ runId: string;
136
+ reason: string;
137
+ }
138
+ /** Result of a context fault resolution attempt. */
139
+ export interface ContextFaultResult {
140
+ objectId: string;
141
+ status: "loaded" | "unavailable" | "secret_blocked" | "budget_exceeded";
142
+ /** Post-render token count when status === "loaded". */
143
+ renderedTokens?: number;
144
+ ledgerEventEmitted: boolean;
145
+ }
146
+ /** Minimal task item tracked within a ContinuationCheckpoint. */
147
+ export interface TaskItem {
148
+ id: string;
149
+ description: string;
150
+ status: "pending" | "in_progress" | "completed" | "failed";
151
+ createdAtMs: number;
152
+ completedAtMs?: number;
153
+ }
154
+ /**
155
+ * Durable task state preserved outside the provider transcript.
156
+ *
157
+ * Write discipline (all enforced in CheckpointStore):
158
+ * - All writes MUST go through CheckpointStore.withTaskLock (CAS-locked).
159
+ * - Reject if store.read(taskId).sequenceNumber !== checkpoint.sequenceNumber - 1.
160
+ * - All array fields MUST be bounded before persistence.
161
+ * - Old completed entries MUST be compacted into completedSummaryHash.
162
+ *
163
+ * Testing requirement: two real concurrent Node processes, not jest mocks.
164
+ */
165
+ export interface ContinuationCheckpoint {
166
+ schemaVersion: "checkpoint/1";
167
+ taskId: string;
168
+ /** CAS lock key: reject write if stored sequenceNumber !== this - 1. */
169
+ sequenceNumber: number;
170
+ /** SHA-256 of the previous checkpoint — integrity chain. */
171
+ parentHash: string;
172
+ /** Old completed[] compacted into a hash-referenced summary to bound size. */
173
+ completedSummaryHash?: string;
174
+ pending: TaskItem[];
175
+ inProgress: TaskItem[];
176
+ decisions: ContextCandidateDecision[];
177
+ createdAtMs: number;
178
+ }
179
+ /**
180
+ * Governs what may enter the working set, how much, and why.
181
+ * policyHash must be included in every manifestHash computation.
182
+ */
183
+ export interface ContextPolicy {
184
+ policyHash: string;
185
+ /** Sensitivity levels that are unconditionally excluded. */
186
+ deniedSensitivities: ContextSensitivity[];
187
+ /** Trust levels that are unconditionally excluded. */
188
+ deniedTrustLevels: ContextTrust[];
189
+ /** Behaviour when required objects alone exceed budget. */
190
+ requiredOverBudgetAction: "explicit_escalation" | "fail_closed";
191
+ /** Maximum allowed compiler wall-clock duration in ms. */
192
+ maxCompilerDurationMs: number;
193
+ /** Maximum overhead ratio: compilationDurationMs / totalRenderedTokens. */
194
+ maxOverheadRatio: number;
195
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Context Runtime — A-CTX-1 contracts.
3
+ *
4
+ * Defines the governed context layer: objects, budgets, manifests, ledger
5
+ * entries, fault protocol, and continuation checkpoints.
6
+ *
7
+ * Non-negotiable invariants (enforced in implementation, not type system):
8
+ * - No `confidence` float anywhere — use UsageEvidence enums only
9
+ * - No `outcome_delta` field anywhere — permanently banned
10
+ * - `nowMs` is always a captured input; never Date.now() inside a compiler
11
+ * - Secret scan hit → deny persistence entirely; reason-coded ledger entry only
12
+ * - Fault budget accumulates by taskId, NOT by runId
13
+ */
14
+ // ─── ContextManifest ─────────────────────────────────────────────────────────
15
+ export const CONTEXT_MANIFEST_VERSION = "context-manifest/1";
16
+ // ─── ContextLedgerEntry ───────────────────────────────────────────────────────
17
+ export const CONTEXT_LEDGER_VERSION = "context-ledger/1";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Context Shadow — A-CTX-0 schema contracts.
3
+ *
4
+ * Shadow mode observes the existing prompt without altering it.
5
+ * No source text is retained in manifests or receipts — only hashes and counts.
6
+ */
7
+ export declare const CONTEXT_SHADOW_MANIFEST_VERSION: "context-shadow-manifest/1";
8
+ export declare const CONTEXT_C5_VERSION: "context-c5/1";
9
+ export type ContextEvidence = "observed" | "configured" | "inferred" | "unknown";
10
+ export type ContextShadowSegmentKind = "system" | "mission" | "task" | "workspace" | "diagnostic" | "tool" | "other";
11
+ export interface ContextShadowSegmentInput {
12
+ segmentId: string;
13
+ kind: ContextShadowSegmentKind;
14
+ required: boolean;
15
+ text: string;
16
+ }
17
+ export interface ContextShadowDecisionV1 {
18
+ segmentId: string;
19
+ kind: string;
20
+ /** SHA-256 of segment.text — never the source text itself. */
21
+ contentHash: string;
22
+ estimatedTokens: number;
23
+ /** Always true in shadow mode — actual prompt is never altered. */
24
+ actuallyIncluded: true;
25
+ /** What the shadow compiler would have decided if it controlled selection. */
26
+ proposedDecision: "included" | "excluded";
27
+ reason: "required" | "within_shadow_budget" | "shadow_budget_exhausted" | "required_over_budget";
28
+ }
29
+ export interface ContextShadowManifestV1 {
30
+ schemaVersion: typeof CONTEXT_SHADOW_MANIFEST_VERSION;
31
+ manifestId: string;
32
+ /** SHA-256 of the canonical JSON of the manifest (excluding manifestId and manifestHash). */
33
+ manifestHash: string;
34
+ /** SHA-256 of the canonical compiler input, for cross-run determinism checks. */
35
+ compilerInputHash: string;
36
+ runId: string;
37
+ taskId?: string;
38
+ adapter: string;
39
+ model?: string;
40
+ capturedAt: string;
41
+ mode: "shadow";
42
+ modelWindowTokens: number | null;
43
+ modelWindowEvidence: ContextEvidence;
44
+ shadowBudgetTokens: number;
45
+ /** SHA-256 of framed source segments — confirms prompt identity without storing content. */
46
+ actualPromptHash: string;
47
+ /** Estimated token count of all segments (actual prompt). */
48
+ actualEstimatedTokens: number;
49
+ /** Estimated token count of shadow-selected segments only. */
50
+ proposedEstimatedTokens: number;
51
+ /** True if required segments alone exceed shadowBudgetTokens. */
52
+ requiredOverBudget: boolean;
53
+ decisions: ContextShadowDecisionV1[];
54
+ }
55
+ export interface ContextC5EnvelopeV1 {
56
+ schemaVersion: typeof CONTEXT_C5_VERSION;
57
+ resource: "context";
58
+ event: "shadow_compiled";
59
+ runId: string;
60
+ taskId?: string;
61
+ manifestId: string;
62
+ manifestHash: string;
63
+ adapter: string;
64
+ model?: string;
65
+ mode: "shadow";
66
+ actualEstimatedTokens: number;
67
+ proposedEstimatedTokens: number;
68
+ candidateCount: number;
69
+ includedCount: number;
70
+ excludedCount: number;
71
+ requiredOverBudget: boolean;
72
+ modelWindowTokens: number | null;
73
+ modelWindowEvidence: ContextEvidence;
74
+ createdAt: string;
75
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Context Shadow — A-CTX-0 schema contracts.
3
+ *
4
+ * Shadow mode observes the existing prompt without altering it.
5
+ * No source text is retained in manifests or receipts — only hashes and counts.
6
+ */
7
+ export const CONTEXT_SHADOW_MANIFEST_VERSION = "context-shadow-manifest/1";
8
+ export const CONTEXT_C5_VERSION = "context-c5/1";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * R4 Delivery — M1 Contract
3
+ *
4
+ * Shared with Lane B (Control Plane). Do not modify unilaterally.
5
+ * The server selects one message; the client never downloads the catalog.
6
+ */
7
+ export declare const DELIVERY_MESSAGE_SCHEMA_VERSION: "delivery-message/1";
8
+ export declare const DELIVERY_RECORD_SCHEMA_VERSION: "delivery-record/1";
9
+ export declare const MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION: "martin-message-selection/1";
10
+ export declare const ALLOWED_ACTION_TYPES: readonly ["upgrade_cli", "upgrade_mcp", "submit_feedback", "open_release_notes", "dismiss", "view_spend_report", "view_run_explain"];
11
+ export type ActionType = (typeof ALLOWED_ACTION_TYPES)[number];
12
+ export type MessageKind = "update" | "feedback_request" | "milestone";
13
+ export interface DeliveryMessage {
14
+ schemaVersion: typeof DELIVERY_MESSAGE_SCHEMA_VERSION;
15
+ id: string;
16
+ revision: number;
17
+ kind: MessageKind;
18
+ title: string;
19
+ body: string;
20
+ action: {
21
+ type: ActionType;
22
+ url?: string;
23
+ targetVersion?: string;
24
+ };
25
+ expiresAt: string;
26
+ cooldownHours: number;
27
+ }
28
+ export interface MessageSelectionResponse {
29
+ schemaVersion: typeof MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION;
30
+ message?: DeliveryMessage;
31
+ }
32
+ /** Local cooldown/dismissal state — persisted in .martin/delivery-record.json */
33
+ export interface DeliveryRecord {
34
+ schemaVersion: typeof DELIVERY_RECORD_SCHEMA_VERSION;
35
+ lastMessageId?: string;
36
+ lastShownAtEpochMs?: number;
37
+ dismissedIds: string[];
38
+ cooldownUntilEpochMs?: number;
39
+ cachedMessage?: DeliveryMessage;
40
+ cachedAtEpochMs?: number;
41
+ }
42
+ /** Structured update field for --json and MCP responses */
43
+ export interface UpdateAvailableField {
44
+ targetVersion: string;
45
+ kind: "cli" | "mcp";
46
+ message?: string;
47
+ }