@martinloop/mcp 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +49 -104
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/prompts.d.ts +1 -1
  5. package/dist/resources.d.ts +1 -1
  6. package/dist/resources.js +2 -2
  7. package/dist/server-validation.d.ts +1 -0
  8. package/dist/server-validation.js +8 -0
  9. package/dist/server.js +18 -2
  10. package/dist/tools/doctor.d.ts +12 -1
  11. package/dist/tools/doctor.js +37 -6
  12. package/dist/tools/eval.js +3 -2
  13. package/dist/tools/get-run.d.ts +2 -0
  14. package/dist/tools/get-run.js +2 -1
  15. package/dist/tools/get-verification-results.d.ts +2 -0
  16. package/dist/tools/get-verification-results.js +2 -1
  17. package/dist/tools/pr-tools.js +2 -1
  18. package/dist/tools/preflight.d.ts +14 -1
  19. package/dist/tools/preflight.js +36 -5
  20. package/dist/tools/run-dossier.d.ts +2 -0
  21. package/dist/tools/run-dossier.js +4 -2
  22. package/dist/tools/run-loop.d.ts +3 -2
  23. package/dist/tools/run-loop.js +48 -28
  24. package/dist/tools/tool-errors.js +1 -1
  25. package/dist/tools/tool-support.d.ts +6 -3
  26. package/dist/tools/tool-support.js +12 -5
  27. package/dist/vendor/adapters/claude-cli.d.ts +25 -0
  28. package/dist/vendor/adapters/claude-cli.js +279 -19
  29. package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
  30. package/dist/vendor/adapters/cli-bridge.js +44 -3
  31. package/dist/vendor/adapters/codex-launcher.d.ts +44 -0
  32. package/dist/vendor/adapters/codex-launcher.js +247 -0
  33. package/dist/vendor/adapters/index.d.ts +3 -2
  34. package/dist/vendor/adapters/index.js +3 -2
  35. package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
  36. package/dist/vendor/adapters/openai-compatible.js +44 -19
  37. package/dist/vendor/adapters/runtime-support.d.ts +3 -0
  38. package/dist/vendor/adapters/runtime-support.js +8 -1
  39. package/dist/vendor/adapters/verifier-only.js +4 -3
  40. package/dist/vendor/contracts/index.d.ts +39 -0
  41. package/dist/vendor/contracts/index.js +2 -0
  42. package/dist/vendor/core/index.d.ts +23 -3
  43. package/dist/vendor/core/index.js +88 -15
  44. package/dist/vendor/core/persistence/index.d.ts +2 -0
  45. package/dist/vendor/core/persistence/index.js +1 -0
  46. package/dist/vendor/core/persistence/integrity.d.ts +38 -0
  47. package/dist/vendor/core/persistence/integrity.js +239 -0
  48. package/dist/vendor/core/persistence/store.d.ts +7 -0
  49. package/dist/vendor/core/persistence/store.js +25 -1
  50. package/dist/vendor/core/policy.d.ts +9 -0
  51. package/package.json +1 -1
  52. package/server.json +2 -2
@@ -13,7 +13,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
13
13
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
14
14
  export { compilePromptPacket } from "./compiler.js";
15
15
  // ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
16
- export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "./persistence/index.js";
16
+ export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
17
17
  export { compileAndPersistContext } from "./persistence/index.js";
18
18
  /**
19
19
  * Admission gate — must pass before any attempt is executed.
@@ -101,6 +101,7 @@ export async function runMartin(input) {
101
101
  projectId: input.projectId,
102
102
  task: input.task,
103
103
  budget: input.budget,
104
+ ...(input.receiptScope ? { receiptScope: input.receiptScope } : {}),
104
105
  ...(input.teamId ? { teamId: input.teamId } : {}),
105
106
  ...(input.metadata ? { metadata: input.metadata } : {})
106
107
  }, { now: now(), idFactory });
@@ -270,7 +271,7 @@ export async function runMartin(input) {
270
271
  // GATHER → ADMIT: run admission control before executing
271
272
  currentPhase = "ADMIT";
272
273
  // T05: Context Integrity Pre-gate — blocks authority inversion / injection before reasoning
273
- const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveRunsRoot(), loop.loopId), {
274
+ const contextPrecheck = await runContextIntegrityPrecheck(loop.loopId, loop.attempts.length + 1, runDir(resolveActiveRunsRoot(input.store), loop.loopId), {
274
275
  userPrompt: distilled.focus,
275
276
  history: loop.attempts.map(a => a.summary).join("\n")
276
277
  });
@@ -418,6 +419,7 @@ export async function runMartin(input) {
418
419
  const result = await executingAdapter.execute(request);
419
420
  const attemptCompletedAt = now();
420
421
  const compiledContext = compilePromptPacket(request);
422
+ const verification = normalizeVerificationOutcome(result);
421
423
  // PATCH → VERIFY
422
424
  currentPhase = "VERIFY";
423
425
  let failure = result.status === "failed"
@@ -444,7 +446,16 @@ export async function runMartin(input) {
444
446
  actualUsd: roundUsd(loop.cost.actualUsd + getUsageUsd(result.usage)),
445
447
  avoidedUsd: loop.cost.avoidedUsd,
446
448
  tokensIn: loop.cost.tokensIn + result.usage.tokensIn,
447
- tokensOut: loop.cost.tokensOut + result.usage.tokensOut
449
+ tokensOut: loop.cost.tokensOut + result.usage.tokensOut,
450
+ ...(result.usage.estimatedUsd !== undefined
451
+ ? {
452
+ estimatedUsd: roundUsd((loop.cost.estimatedUsd ?? loop.cost.actualUsd) + result.usage.estimatedUsd)
453
+ }
454
+ : {}),
455
+ provenance: getUsageProvenance(result.usage),
456
+ ...(result.usage.providerSettlement
457
+ ? { providerSettlement: result.usage.providerSettlement }
458
+ : {})
448
459
  },
449
460
  updatedAt: attemptCompletedAt
450
461
  };
@@ -503,11 +514,14 @@ export async function runMartin(input) {
503
514
  }
504
515
  loop = appendLoopEvent(loop, {
505
516
  type: "verification.completed",
506
- lifecycleState: result.verification.passed ? "completed" : "verifying",
517
+ lifecycleState: verification.passed ? "completed" : "verifying",
507
518
  payload: {
508
519
  attemptId,
509
- passed: result.verification.passed,
510
- summary: result.verification.summary
520
+ attemptIndex: currentAttemptIndex,
521
+ passed: verification.passed,
522
+ summary: verification.summary,
523
+ ...(verification.steps?.length ? { steps: verification.steps } : {}),
524
+ ...(verification.warnings?.length ? { warnings: verification.warnings } : {})
511
525
  }
512
526
  }, { now: attemptCompletedAt, idFactory });
513
527
  const costState = evaluateCostGovernor({
@@ -534,6 +548,7 @@ export async function runMartin(input) {
534
548
  });
535
549
  await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
536
550
  compiledContext,
551
+ verification,
537
552
  ...(rollbackBoundary ? { rollbackBoundary } : {})
538
553
  });
539
554
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
@@ -546,7 +561,13 @@ export async function runMartin(input) {
546
561
  kind: "verification.completed",
547
562
  runId: loop.loopId,
548
563
  attemptIndex: currentAttemptIndex,
549
- payload: { passed: result.verification.passed, summary: result.verification.summary }
564
+ payload: {
565
+ attemptId,
566
+ passed: verification.passed,
567
+ summary: verification.summary,
568
+ ...(verification.steps?.length ? { steps: verification.steps } : {}),
569
+ ...(verification.warnings?.length ? { warnings: verification.warnings } : {})
570
+ }
550
571
  }));
551
572
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
552
573
  kind: "budget.settled",
@@ -557,10 +578,13 @@ export async function runMartin(input) {
557
578
  estimatedUsd: result.usage.estimatedUsd,
558
579
  tokensIn: result.usage.tokensIn,
559
580
  tokensOut: result.usage.tokensOut,
581
+ cachedInputTokens: result.usage.cachedInputTokens,
582
+ reasoningTokensOut: result.usage.reasoningTokensOut,
560
583
  provenance: getUsageProvenance(result.usage),
561
584
  transport: getAdapterTransport(executingAdapter),
562
585
  providerId: executingAdapter.metadata.providerId,
563
586
  model: executingAdapter.metadata.model,
587
+ providerSettlement: result.usage.providerSettlement,
564
588
  patchCost: settlement.patchCost,
565
589
  verificationCost: settlement.verificationCost,
566
590
  varianceUsd: settlement.varianceUsd,
@@ -573,11 +597,13 @@ export async function runMartin(input) {
573
597
  // returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
574
598
  // a git repo) and silently return [], which would falsely trigger no_code_change.
575
599
  const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
600
+ const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
601
+ const patchTruthCountsEdits = !isVerifyOnly && !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
576
602
  if (isVerifyOnly && changedFiles.length > 0) {
577
603
  const patchDecision = evaluatePatchDecision({
578
- verificationPassed: result.verification.passed,
604
+ verificationPassed: verification.passed,
579
605
  previousVerifierScore,
580
- verifierScore: result.verification.passed ? 1 : 0,
606
+ verifierScore: verification.passed ? 1 : 0,
581
607
  scopeViolationCount: changedFiles.length,
582
608
  changedFileCount: changedFiles.length,
583
609
  diffNovelty: 1,
@@ -842,12 +868,12 @@ export async function runMartin(input) {
842
868
  let patchDecision;
843
869
  if (result.status === "completed") {
844
870
  patchDecision = evaluatePatchDecision({
845
- verificationPassed: result.verification.passed,
871
+ verificationPassed: verification.passed,
846
872
  previousVerifierScore,
847
- verifierScore: result.verification.passed ? 1 : 0,
873
+ verifierScore: verification.passed ? 1 : 0,
848
874
  groundingViolationCount: groundingScanResult?.violations.length ?? 0,
849
- changedFileCount: !isVerifyOnly && changedFileEvidenceAvailable ? changedFiles.length : undefined,
850
- diffNovelty: !isVerifyOnly && changedFileEvidenceAvailable ? (changedFiles.length > 0 ? 1 : 0) : undefined,
875
+ changedFileCount: patchTruthCountsEdits ? changedFiles.length : undefined,
876
+ diffNovelty: patchTruthCountsEdits ? (changedFiles.length > 0 ? 1 : 0) : undefined,
851
877
  diffStats: result.execution?.diffStats,
852
878
  costUsd: getUsageUsd(result.usage),
853
879
  summary: result.summary
@@ -897,10 +923,10 @@ export async function runMartin(input) {
897
923
  }
898
924
  else {
899
925
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
900
- kind: result.verification.passed ? "attempt.kept" : "attempt.discarded",
926
+ kind: verification.passed ? "attempt.kept" : "attempt.discarded",
901
927
  runId: loop.loopId,
902
928
  attemptIndex: currentAttemptIndex,
903
- payload: { reason: result.verification.summary }
929
+ payload: { reason: verification.summary }
904
930
  }));
905
931
  }
906
932
  }
@@ -1145,6 +1171,9 @@ function createBudgetSettlement(input) {
1145
1171
  usd: 0,
1146
1172
  provenance: "unavailable"
1147
1173
  },
1174
+ ...(input.usage.providerSettlement
1175
+ ? { providerSettlement: input.usage.providerSettlement }
1176
+ : {}),
1148
1177
  totalActualUsd,
1149
1178
  preflightEstimateUsd: input.estimate.estimatedAttemptCostUsd,
1150
1179
  varianceUsd: roundUsd(totalActualUsd - input.estimate.estimatedAttemptCostUsd),
@@ -1185,6 +1214,50 @@ function getLastVerifierScore(loop) {
1185
1214
  }
1186
1215
  return 0;
1187
1216
  }
1217
+ function truncateVerificationDetail(text, maxLength) {
1218
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
1219
+ }
1220
+ function normalizeVerificationOutcome(result) {
1221
+ const warnings = [...(result.verification.warnings ?? [])];
1222
+ const contradiction = detectVerificationContradiction(result);
1223
+ if (contradiction && !warnings.includes(contradiction)) {
1224
+ warnings.push(contradiction);
1225
+ }
1226
+ return {
1227
+ passed: result.verification.passed,
1228
+ summary: result.verification.summary,
1229
+ ...(result.verification.steps?.length ? { steps: result.verification.steps } : {}),
1230
+ ...(warnings.length ? { warnings } : {})
1231
+ };
1232
+ }
1233
+ function detectVerificationContradiction(result) {
1234
+ if (!result.verification.passed) {
1235
+ return undefined;
1236
+ }
1237
+ const sources = [result.summary, result.failure?.message]
1238
+ .filter((value) => typeof value === "string" && value.trim().length > 0);
1239
+ const contradictionPatterns = [
1240
+ /createprocessasuserw failed:\s*\d+/iu,
1241
+ /\bverifier not run\b/iu,
1242
+ /\bfailed before verifier\b/iu,
1243
+ /\bnever launched\b/iu,
1244
+ /\bfailed to launch\b/iu,
1245
+ /\bcould not launch\b/iu
1246
+ ];
1247
+ for (const source of sources) {
1248
+ const match = contradictionPatterns.find((pattern) => pattern.test(source));
1249
+ if (!match) {
1250
+ continue;
1251
+ }
1252
+ const excerpt = truncateVerificationDetail(source.trim().replace(/\s+/gu, " "), 220);
1253
+ return `Adapter output reported a tool-launch problem before MartinLoop ran its own verifier: ${excerpt}`;
1254
+ }
1255
+ return undefined;
1256
+ }
1257
+ function resolveActiveRunsRoot(store) {
1258
+ const configuredRunsRoot = store?.runsRoot?.trim();
1259
+ return configuredRunsRoot && configuredRunsRoot.length > 0 ? configuredRunsRoot : resolveRunsRoot();
1260
+ }
1188
1261
  function toPatchDecisionArtifact(decision) {
1189
1262
  return {
1190
1263
  decision: decision.decision,
@@ -1,5 +1,7 @@
1
1
  export { makeLedgerEvent } from "./ledger.js";
2
2
  export type { LedgerEvent, LedgerEventDraft, LedgerEventKind } from "./ledger.js";
3
+ export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
4
+ export type { ReceiptIntegrityChainEntry, StoredReceiptIntegrityMaterial } from "./integrity.js";
3
5
  export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
4
6
  export type { AttemptArtifacts, RunContract, RunStore } from "./store.js";
5
7
  export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
@@ -1,4 +1,5 @@
1
1
  export { makeLedgerEvent } from "./ledger.js";
2
+ export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
2
3
  export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
3
4
  export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
4
5
  export { compileAndPersistContext } from "./compiler.js";
@@ -0,0 +1,38 @@
1
+ import type { LoopRecord, ReceiptIntegritySummary, ReceiptScope } from "../../contracts/index.js";
2
+ export interface ReceiptIntegrityChainEntry {
3
+ index: number;
4
+ eventId?: string;
5
+ type?: string;
6
+ kind?: string;
7
+ timestamp?: string;
8
+ prevHash: string;
9
+ entryHash: string;
10
+ }
11
+ export interface StoredReceiptIntegrityMaterial {
12
+ schemaVersion: "martin.receipt-integrity.v1";
13
+ runId: string;
14
+ keyId: string;
15
+ signedAt: string;
16
+ scope?: ReceiptScope;
17
+ loopRecordSha256: string;
18
+ ledgerSha256: string;
19
+ ledgerHeadHash: string;
20
+ entryCount: number;
21
+ chain: ReceiptIntegrityChainEntry[];
22
+ signatureHmacSha256: string;
23
+ }
24
+ export declare function writeReceiptIntegrityMaterial(input: {
25
+ runId: string;
26
+ runsRoot: string;
27
+ loopRecord: LoopRecord;
28
+ ledgerEntries: unknown[];
29
+ scope?: ReceiptScope;
30
+ signedAt?: string;
31
+ }): Promise<StoredReceiptIntegrityMaterial>;
32
+ export declare function verifyReceiptIntegrityFromFiles(input: {
33
+ runId: string;
34
+ runsRoot: string;
35
+ loopRecordPath: string;
36
+ ledgerPath: string;
37
+ }): Promise<ReceiptIntegritySummary>;
38
+ export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
@@ -0,0 +1,239 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ const RECEIPT_INTEGRITY_SCHEMA_VERSION = "martin.receipt-integrity.v1";
6
+ const RECEIPT_INTEGRITY_KEY_DIR_MODE = 0o700;
7
+ const RECEIPT_INTEGRITY_KEY_FILE_MODE = 0o600;
8
+ export async function writeReceiptIntegrityMaterial(input) {
9
+ const signedAt = input.signedAt ?? new Date().toISOString();
10
+ const [key, keyId] = await ensureReceiptIntegrityKey(input.runsRoot, input.runId);
11
+ const chain = buildReceiptIntegrityChain(input.ledgerEntries);
12
+ const loopRecordRaw = serializeStoredJson(input.loopRecord);
13
+ const ledgerRaw = serializeStoredJsonl(input.ledgerEntries);
14
+ const materialBase = {
15
+ schemaVersion: RECEIPT_INTEGRITY_SCHEMA_VERSION,
16
+ runId: input.runId,
17
+ keyId,
18
+ signedAt,
19
+ ...(input.scope ? { scope: input.scope } : {}),
20
+ loopRecordSha256: sha256(loopRecordRaw),
21
+ ledgerSha256: sha256(ledgerRaw),
22
+ ledgerHeadHash: chain.at(-1)?.entryHash ?? "root",
23
+ entryCount: chain.length,
24
+ chain
25
+ };
26
+ const material = {
27
+ ...materialBase,
28
+ signatureHmacSha256: createReceiptIntegritySignature(key, materialBase)
29
+ };
30
+ await mkdir(join(input.runsRoot, input.runId), { recursive: true });
31
+ await writeFile(resolveReceiptIntegrityPath(input.runsRoot, input.runId), serializeStoredJson(material), "utf8");
32
+ return material;
33
+ }
34
+ export async function verifyReceiptIntegrityFromFiles(input) {
35
+ const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
36
+ const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
37
+ readFile(integrityPath, "utf8").catch(() => null),
38
+ readFile(input.loopRecordPath, "utf8").catch(() => null),
39
+ readFile(input.ledgerPath, "utf8").catch(() => null),
40
+ readReceiptIntegrityKey(input.runsRoot, input.runId).catch(() => null)
41
+ ]);
42
+ if (!rawMaterial || !rawLoopRecord || rawLedger === null || !key) {
43
+ return {
44
+ state: "unsigned",
45
+ reason: "Receipt integrity material is missing for this run.",
46
+ warnings: ["Receipts are local-only and unsigned; trust claims are unavailable."]
47
+ };
48
+ }
49
+ let material;
50
+ try {
51
+ material = JSON.parse(rawMaterial);
52
+ }
53
+ catch {
54
+ return {
55
+ state: "tamper_detected",
56
+ reason: "Receipt integrity metadata is unreadable."
57
+ };
58
+ }
59
+ const actualLoopRecordSha256 = sha256(rawLoopRecord);
60
+ if (actualLoopRecordSha256 !== material.loopRecordSha256) {
61
+ return {
62
+ state: "tamper_detected",
63
+ keyId: material.keyId,
64
+ signedAt: material.signedAt,
65
+ loopRecordSha256: material.loopRecordSha256,
66
+ ledgerSha256: material.ledgerSha256,
67
+ ledgerHeadHash: material.ledgerHeadHash,
68
+ entryCount: material.entryCount,
69
+ reason: "loop_record_hash_mismatch"
70
+ };
71
+ }
72
+ const actualLedgerSha256 = sha256(rawLedger);
73
+ if (actualLedgerSha256 !== material.ledgerSha256) {
74
+ return {
75
+ state: "tamper_detected",
76
+ keyId: material.keyId,
77
+ signedAt: material.signedAt,
78
+ loopRecordSha256: material.loopRecordSha256,
79
+ ledgerSha256: material.ledgerSha256,
80
+ ledgerHeadHash: material.ledgerHeadHash,
81
+ entryCount: material.entryCount,
82
+ reason: "ledger_hash_mismatch"
83
+ };
84
+ }
85
+ let parsedLedgerEntries;
86
+ try {
87
+ parsedLedgerEntries = rawLedger
88
+ .split(/\r?\n/u)
89
+ .map((line) => line.trim())
90
+ .filter(Boolean)
91
+ .map((line) => JSON.parse(line));
92
+ }
93
+ catch {
94
+ return {
95
+ state: "tamper_detected",
96
+ keyId: material.keyId,
97
+ signedAt: material.signedAt,
98
+ loopRecordSha256: material.loopRecordSha256,
99
+ ledgerSha256: material.ledgerSha256,
100
+ ledgerHeadHash: material.ledgerHeadHash,
101
+ entryCount: material.entryCount,
102
+ reason: "ledger_entry_parse_error"
103
+ };
104
+ }
105
+ const actualChain = buildReceiptIntegrityChain(parsedLedgerEntries);
106
+ if (actualChain.length !== material.chain.length) {
107
+ return {
108
+ state: "tamper_detected",
109
+ keyId: material.keyId,
110
+ signedAt: material.signedAt,
111
+ loopRecordSha256: material.loopRecordSha256,
112
+ ledgerSha256: material.ledgerSha256,
113
+ ledgerHeadHash: material.ledgerHeadHash,
114
+ entryCount: material.entryCount,
115
+ reason: "ledger_entry_count_mismatch"
116
+ };
117
+ }
118
+ for (let index = 0; index < actualChain.length; index += 1) {
119
+ const expected = material.chain[index];
120
+ const actual = actualChain[index];
121
+ if (!expected ||
122
+ !actual ||
123
+ expected.entryHash !== actual.entryHash ||
124
+ expected.prevHash !== actual.prevHash) {
125
+ return {
126
+ state: "tamper_detected",
127
+ keyId: material.keyId,
128
+ signedAt: material.signedAt,
129
+ loopRecordSha256: material.loopRecordSha256,
130
+ ledgerSha256: material.ledgerSha256,
131
+ ledgerHeadHash: material.ledgerHeadHash,
132
+ entryCount: material.entryCount,
133
+ reason: `ledger_chain_mismatch:${index}`
134
+ };
135
+ }
136
+ }
137
+ const signatureBase = {
138
+ schemaVersion: material.schemaVersion,
139
+ runId: material.runId,
140
+ keyId: material.keyId,
141
+ signedAt: material.signedAt,
142
+ ...(material.scope ? { scope: material.scope } : {}),
143
+ loopRecordSha256: material.loopRecordSha256,
144
+ ledgerSha256: material.ledgerSha256,
145
+ ledgerHeadHash: material.ledgerHeadHash,
146
+ entryCount: material.entryCount,
147
+ chain: material.chain
148
+ };
149
+ const expectedSignature = createReceiptIntegritySignature(key, signatureBase);
150
+ if (expectedSignature !== material.signatureHmacSha256) {
151
+ return {
152
+ state: "tamper_detected",
153
+ keyId: material.keyId,
154
+ signedAt: material.signedAt,
155
+ loopRecordSha256: material.loopRecordSha256,
156
+ ledgerSha256: material.ledgerSha256,
157
+ ledgerHeadHash: material.ledgerHeadHash,
158
+ entryCount: material.entryCount,
159
+ reason: "signature_mismatch"
160
+ };
161
+ }
162
+ return {
163
+ state: "verified",
164
+ keyId: material.keyId,
165
+ signedAt: material.signedAt,
166
+ loopRecordSha256: material.loopRecordSha256,
167
+ ledgerSha256: material.ledgerSha256,
168
+ ledgerHeadHash: material.ledgerHeadHash,
169
+ entryCount: material.entryCount
170
+ };
171
+ }
172
+ export function resolveReceiptIntegrityPath(runsRoot, runId) {
173
+ return join(runsRoot, runId, "receipt-integrity.json");
174
+ }
175
+ function buildReceiptIntegrityChain(entries) {
176
+ let previousHash = "root";
177
+ return entries.map((entry, index) => {
178
+ const normalizedEntry = JSON.stringify(entry);
179
+ const entryHash = sha256(`${previousHash}\n${normalizedEntry}`);
180
+ const candidate = entry;
181
+ const chainEntry = {
182
+ index,
183
+ prevHash: previousHash,
184
+ entryHash
185
+ };
186
+ if (typeof candidate.eventId === "string") {
187
+ chainEntry.eventId = candidate.eventId;
188
+ }
189
+ if (typeof candidate.type === "string") {
190
+ chainEntry.type = candidate.type;
191
+ }
192
+ if (typeof candidate.kind === "string") {
193
+ chainEntry.kind = candidate.kind;
194
+ }
195
+ if (typeof candidate.timestamp === "string") {
196
+ chainEntry.timestamp = candidate.timestamp;
197
+ }
198
+ previousHash = entryHash;
199
+ return chainEntry;
200
+ });
201
+ }
202
+ function createReceiptIntegritySignature(key, material) {
203
+ return createHmac("sha256", key).update(JSON.stringify(material)).digest("hex");
204
+ }
205
+ async function ensureReceiptIntegrityKey(runsRoot, runId) {
206
+ const keyPath = resolveReceiptIntegrityKeyPath(runsRoot, runId);
207
+ const existing = await readFile(keyPath, "utf8").catch(() => null);
208
+ if (existing) {
209
+ const trimmed = existing.trim();
210
+ return [trimmed, sha256(trimmed).slice(0, 16)];
211
+ }
212
+ const generated = randomBytes(32).toString("hex");
213
+ await mkdir(dirname(keyPath), { recursive: true, mode: RECEIPT_INTEGRITY_KEY_DIR_MODE });
214
+ await writeFile(keyPath, `${generated}\n`, {
215
+ encoding: "utf8",
216
+ mode: RECEIPT_INTEGRITY_KEY_FILE_MODE
217
+ });
218
+ return [generated, sha256(generated).slice(0, 16)];
219
+ }
220
+ async function readReceiptIntegrityKey(runsRoot, runId) {
221
+ const raw = await readFile(resolveReceiptIntegrityKeyPath(runsRoot, runId), "utf8");
222
+ return raw.trim();
223
+ }
224
+ function resolveReceiptIntegrityKeyPath(runsRoot, runId) {
225
+ const rootHash = sha256(runsRoot).slice(0, 16);
226
+ return join(homedir(), ".martin", "receipt-integrity", rootHash, `${runId}.key`);
227
+ }
228
+ function serializeStoredJson(value) {
229
+ return `${JSON.stringify(value, null, 2)}\n`;
230
+ }
231
+ function serializeStoredJsonl(entries) {
232
+ if (entries.length === 0) {
233
+ return "";
234
+ }
235
+ return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
236
+ }
237
+ function sha256(value) {
238
+ return createHash("sha256").update(value).digest("hex");
239
+ }
@@ -12,6 +12,8 @@ export interface RunContract {
12
12
  export interface AttemptArtifacts {
13
13
  /** Compiled PromptPacket written as compiled-context.json */
14
14
  compiledContext: unknown;
15
+ /** Structured verification evidence captured from the authoritative MartinLoop verifier (optional) */
16
+ verification?: unknown;
15
17
  /** Unified diff string from the patch (optional) */
16
18
  diff?: string;
17
19
  /** Raw verifier command output (optional) */
@@ -35,6 +37,11 @@ export interface AttemptArtifacts {
35
37
  * is durably written before the run proceeds to the next step.
36
38
  */
37
39
  export interface RunStore {
40
+ /**
41
+ * Optional runs root hint for filesystem-backed stores.
42
+ * Orchestration should prefer this over recomputing the default home store.
43
+ */
44
+ runsRoot?: string;
38
45
  /**
39
46
  * Write contract.json for a new run. Called once at run start.
40
47
  * The contract is immutable after this point.
@@ -1,7 +1,8 @@
1
1
  /// <reference types="node" />
2
- import { appendFile, mkdir, writeFile } from "node:fs/promises";
2
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { writeReceiptIntegrityMaterial } from "./integrity.js";
5
6
  // ─── FileRunStore implementation ─────────────────────────────────────────────
6
7
  export function resolveRunsRoot(env = process.env) {
7
8
  return env["MARTIN_RUNS_DIR"]?.trim() ??
@@ -31,6 +32,7 @@ export function artifactDir(runsRoot, runId, attemptIndex) {
31
32
  export function createFileRunStore(options = {}) {
32
33
  const runsRoot = options.runsRoot ?? resolveRunsRoot();
33
34
  return {
35
+ runsRoot,
34
36
  async initRun(contract) {
35
37
  const dir = runDir(runsRoot, contract.runId);
36
38
  await mkdir(dir, { recursive: true });
@@ -50,6 +52,9 @@ export function createFileRunStore(options = {}) {
50
52
  const dir = artifactDir(runsRoot, runId, attemptIndex);
51
53
  await mkdir(dir, { recursive: true });
52
54
  await writeJsonFile(join(dir, "compiled-context.json"), artifacts.compiledContext);
55
+ if (artifacts.verification !== undefined) {
56
+ await writeJsonFile(join(dir, "verification.json"), artifacts.verification);
57
+ }
53
58
  if (artifacts.diff !== undefined) {
54
59
  await writeFile(join(dir, "diff.patch"), artifacts.diff, "utf8");
55
60
  }
@@ -79,6 +84,25 @@ export function createFileRunStore(options = {}) {
79
84
  const dir = runDir(runsRoot, runId);
80
85
  await mkdir(dir, { recursive: true });
81
86
  await writeJsonFile(join(dir, "loop-record.json"), loop);
87
+ const ledgerRaw = await readFile(join(dir, "ledger.jsonl"), "utf8").catch(() => "");
88
+ const ledgerEntries = ledgerRaw
89
+ .split(/\r?\n/u)
90
+ .map((line) => line.trim())
91
+ .filter(Boolean)
92
+ .map((line) => JSON.parse(line));
93
+ await writeReceiptIntegrityMaterial({
94
+ runId,
95
+ runsRoot,
96
+ loopRecord: loop,
97
+ ledgerEntries,
98
+ scope: loop.receiptScope ??
99
+ {
100
+ ...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {}),
101
+ ...(loop.task.repoRoot ? { workingDirectory: loop.task.repoRoot } : {}),
102
+ runsRoot
103
+ },
104
+ signedAt: loop.updatedAt
105
+ });
82
106
  }
83
107
  };
84
108
  }
@@ -38,6 +38,15 @@ export interface MartinAdapterResultLike {
38
38
  verification: {
39
39
  passed: boolean;
40
40
  summary: string;
41
+ steps?: Array<{
42
+ command: string;
43
+ launched: boolean;
44
+ exitCode?: number;
45
+ timedOut: boolean;
46
+ fastFail?: boolean;
47
+ detail?: string;
48
+ }>;
49
+ warnings?: string[];
41
50
  };
42
51
  failure?: {
43
52
  message: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/Keesan12/martin-loop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.2.7",
10
+ "version": "0.3.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.2.7",
15
+ "version": "0.3.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }