martin-loop 0.1.4 → 1.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 (286) hide show
  1. package/CODE_OF_CONDUCT.md +32 -0
  2. package/README.md +172 -227
  3. package/demo/seeded-workspace/README.md +35 -0
  4. package/demo/seeded-workspace/TASKS.md +29 -0
  5. package/demo/seeded-workspace/martin.config.yaml +11 -0
  6. package/demo/seeded-workspace/package.json +8 -0
  7. package/demo/seeded-workspace/src/invoice-summary.js +11 -0
  8. package/demo/seeded-workspace/test/invoice-summary.test.js +20 -0
  9. package/dist/bin/martin-loop.js +0 -0
  10. package/dist/vendor/adapters/claude-cli.d.ts +19 -4
  11. package/dist/vendor/adapters/claude-cli.js +55 -24
  12. package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
  13. package/dist/vendor/adapters/cli-bridge.js +154 -28
  14. package/dist/vendor/adapters/counter.d.ts +1 -0
  15. package/dist/vendor/adapters/counter.js +4 -0
  16. package/dist/vendor/adapters/git-baseline.d.ts +50 -0
  17. package/dist/vendor/adapters/git-baseline.js +233 -0
  18. package/dist/vendor/adapters/index.d.ts +1 -0
  19. package/dist/vendor/adapters/index.js +1 -0
  20. package/dist/vendor/adapters/openrouter-adapter.d.ts +15 -0
  21. package/dist/vendor/adapters/openrouter-adapter.js +302 -0
  22. package/dist/vendor/adapters/usage.d.ts +48 -0
  23. package/dist/vendor/adapters/usage.js +66 -0
  24. package/dist/vendor/adapters/verifier-only.d.ts +7 -0
  25. package/dist/vendor/adapters/verifier-only.js +57 -0
  26. package/dist/vendor/cli/bin/exit.d.ts +12 -0
  27. package/dist/vendor/cli/bin/exit.js +28 -0
  28. package/dist/vendor/cli/commands/analyze.d.ts +5 -0
  29. package/dist/vendor/cli/commands/analyze.js +58 -0
  30. package/dist/vendor/cli/commands/audit-log-verify.d.ts +34 -0
  31. package/dist/vendor/cli/commands/audit-log-verify.js +99 -0
  32. package/dist/vendor/cli/commands/audit.d.ts +8 -0
  33. package/dist/vendor/cli/commands/audit.js +199 -0
  34. package/dist/vendor/cli/commands/corpus.d.ts +5 -0
  35. package/dist/vendor/cli/commands/corpus.js +60 -0
  36. package/dist/vendor/cli/commands/doctor.d.ts +8 -0
  37. package/dist/vendor/cli/commands/doctor.js +219 -0
  38. package/dist/vendor/cli/commands/explain.d.ts +17 -0
  39. package/dist/vendor/cli/commands/explain.js +176 -0
  40. package/dist/vendor/cli/commands/export.d.ts +5 -0
  41. package/dist/vendor/cli/commands/export.js +60 -0
  42. package/dist/vendor/cli/commands/governance.d.ts +8 -0
  43. package/dist/vendor/cli/commands/governance.js +95 -0
  44. package/dist/vendor/cli/commands/improve.d.ts +18 -0
  45. package/dist/vendor/cli/commands/improve.js +396 -0
  46. package/dist/vendor/cli/commands/init.d.ts +8 -0
  47. package/dist/vendor/cli/commands/init.js +281 -0
  48. package/dist/vendor/cli/commands/migration.d.ts +8 -0
  49. package/dist/vendor/cli/commands/migration.js +67 -0
  50. package/dist/vendor/cli/commands/prior.d.ts +23 -0
  51. package/dist/vendor/cli/commands/prior.js +145 -0
  52. package/dist/vendor/cli/commands/resume.d.ts +21 -0
  53. package/dist/vendor/cli/commands/resume.js +73 -0
  54. package/dist/vendor/cli/commands/verify.d.ts +6 -0
  55. package/dist/vendor/cli/commands/verify.js +43 -0
  56. package/dist/vendor/cli/index.d.ts +6 -1
  57. package/dist/vendor/cli/index.js +124 -7
  58. package/dist/vendor/cli/research/public-corpus.d.ts +43 -0
  59. package/dist/vendor/cli/research/public-corpus.js +151 -0
  60. package/dist/vendor/cli/ui/error-card.d.ts +38 -0
  61. package/dist/vendor/cli/ui/error-card.js +103 -0
  62. package/dist/vendor/cli/ui/mission-brief.d.ts +41 -0
  63. package/dist/vendor/cli/ui/mission-brief.js +173 -0
  64. package/dist/vendor/cli/ui/summary-card.d.ts +34 -0
  65. package/dist/vendor/cli/ui/summary-card.js +102 -0
  66. package/dist/vendor/contracts/audit.d.ts +46 -0
  67. package/dist/vendor/contracts/audit.js +360 -0
  68. package/dist/vendor/contracts/index.d.ts +3 -1
  69. package/dist/vendor/contracts/post-phase15.d.ts +240 -0
  70. package/dist/vendor/contracts/post-phase15.js +166 -0
  71. package/dist/vendor/core/agent/mandates.d.ts +46 -0
  72. package/dist/vendor/core/agent/mandates.js +178 -0
  73. package/dist/vendor/core/agent/receipts.d.ts +38 -0
  74. package/dist/vendor/core/agent/receipts.js +131 -0
  75. package/dist/vendor/core/agent/signing.d.ts +17 -0
  76. package/dist/vendor/core/agent/signing.js +91 -0
  77. package/dist/vendor/core/attestation/sign.d.ts +25 -0
  78. package/dist/vendor/core/attestation/sign.js +216 -0
  79. package/dist/vendor/core/autonomy/autonomous-promotion.d.ts +120 -0
  80. package/dist/vendor/core/autonomy/autonomous-promotion.js +346 -0
  81. package/dist/vendor/core/autonomy/envelope-v2.d.ts +29 -0
  82. package/dist/vendor/core/autonomy/envelope-v2.js +60 -0
  83. package/dist/vendor/core/autonomy/envelope.d.ts +17 -0
  84. package/dist/vendor/core/autonomy/envelope.js +27 -0
  85. package/dist/vendor/core/autonomy/escalation-ledger.d.ts +20 -0
  86. package/dist/vendor/core/autonomy/escalation-ledger.js +18 -0
  87. package/dist/vendor/core/autonomy/resume.d.ts +15 -0
  88. package/dist/vendor/core/autonomy/resume.js +23 -0
  89. package/dist/vendor/core/circuit/circuit-breaker.d.ts +60 -0
  90. package/dist/vendor/core/circuit/circuit-breaker.js +143 -0
  91. package/dist/vendor/core/compiler.d.ts +2 -0
  92. package/dist/vendor/core/compiler.js +10 -4
  93. package/dist/vendor/core/context-distillation.d.ts +3 -0
  94. package/dist/vendor/core/context-distillation.js +44 -0
  95. package/dist/vendor/core/context-flow/compile-context.d.ts +8 -0
  96. package/dist/vendor/core/context-flow/compile-context.js +111 -0
  97. package/dist/vendor/core/context-flow/entities.d.ts +2 -0
  98. package/dist/vendor/core/context-flow/entities.js +44 -0
  99. package/dist/vendor/core/context-flow/evaluate-policy.d.ts +2 -0
  100. package/dist/vendor/core/context-flow/evaluate-policy.js +42 -0
  101. package/dist/vendor/core/context-flow/index.d.ts +11 -0
  102. package/dist/vendor/core/context-flow/index.js +24 -0
  103. package/dist/vendor/core/context-flow/labels.d.ts +3 -0
  104. package/dist/vendor/core/context-flow/labels.js +17 -0
  105. package/dist/vendor/core/context-flow/normalizer.d.ts +9 -0
  106. package/dist/vendor/core/context-flow/normalizer.js +69 -0
  107. package/dist/vendor/core/context-flow/profiles.d.ts +33 -0
  108. package/dist/vendor/core/context-flow/profiles.js +36 -0
  109. package/dist/vendor/core/context-flow/redaction.d.ts +1 -0
  110. package/dist/vendor/core/context-flow/redaction.js +6 -0
  111. package/dist/vendor/core/context-flow/sensitivity.d.ts +2 -0
  112. package/dist/vendor/core/context-flow/sensitivity.js +27 -0
  113. package/dist/vendor/core/context-flow/sync-preview.d.ts +2 -0
  114. package/dist/vendor/core/context-flow/sync-preview.js +22 -0
  115. package/dist/vendor/core/context-flow/token-estimator.d.ts +3 -0
  116. package/dist/vendor/core/context-flow/token-estimator.js +13 -0
  117. package/dist/vendor/core/context-flow/types.d.ts +91 -0
  118. package/dist/vendor/core/context-flow/types.js +2 -0
  119. package/dist/vendor/core/context-integrity.d.ts +26 -0
  120. package/dist/vendor/core/context-integrity.js +56 -0
  121. package/dist/vendor/core/context-utility.d.ts +47 -0
  122. package/dist/vendor/core/context-utility.js +405 -0
  123. package/dist/vendor/core/cost/pipeline.d.ts +92 -0
  124. package/dist/vendor/core/cost/pipeline.js +141 -0
  125. package/dist/vendor/core/cost/tagged-cost.d.ts +27 -0
  126. package/dist/vendor/core/cost/tagged-cost.js +55 -0
  127. package/dist/vendor/core/cost-governor.d.ts +2 -0
  128. package/dist/vendor/core/cost-governor.js +50 -0
  129. package/dist/vendor/core/cve/cve-check.d.ts +80 -0
  130. package/dist/vendor/core/cve/cve-check.js +172 -0
  131. package/dist/vendor/core/digital-twin/index.d.ts +27 -0
  132. package/dist/vendor/core/digital-twin/index.js +90 -0
  133. package/dist/vendor/core/drift/drift-graph.d.ts +47 -0
  134. package/dist/vendor/core/drift/drift-graph.js +100 -0
  135. package/dist/vendor/core/drift/objective-lock.d.ts +69 -0
  136. package/dist/vendor/core/drift/objective-lock.js +88 -0
  137. package/dist/vendor/core/drift/scope.d.ts +46 -0
  138. package/dist/vendor/core/drift/scope.js +102 -0
  139. package/dist/vendor/core/drift/signature-lock.d.ts +48 -0
  140. package/dist/vendor/core/drift/signature-lock.js +202 -0
  141. package/dist/vendor/core/drift/stale-proof-gate.d.ts +21 -0
  142. package/dist/vendor/core/drift/stale-proof-gate.js +19 -0
  143. package/dist/vendor/core/eval/known-bad-world-runner.d.ts +24 -0
  144. package/dist/vendor/core/eval/known-bad-world-runner.js +256 -0
  145. package/dist/vendor/core/evidence/claim-audit.d.ts +18 -0
  146. package/dist/vendor/core/evidence/claim-audit.js +89 -0
  147. package/dist/vendor/core/exit-intelligence.d.ts +2 -0
  148. package/dist/vendor/core/exit-intelligence.js +58 -0
  149. package/dist/vendor/core/explain/formatter.d.ts +42 -0
  150. package/dist/vendor/core/explain/formatter.js +171 -0
  151. package/dist/vendor/core/explain/timeline.d.ts +29 -0
  152. package/dist/vendor/core/explain/timeline.js +213 -0
  153. package/dist/vendor/core/failure-taxonomy.d.ts +2 -0
  154. package/dist/vendor/core/failure-taxonomy.js +76 -0
  155. package/dist/vendor/core/gateway/index.d.ts +10 -0
  156. package/dist/vendor/core/gateway/index.js +12 -0
  157. package/dist/vendor/core/gateway/registry.d.ts +40 -0
  158. package/dist/vendor/core/gateway/registry.js +97 -0
  159. package/dist/vendor/core/gateway/transport.d.ts +31 -0
  160. package/dist/vendor/core/gateway/transport.js +82 -0
  161. package/dist/vendor/core/gateway/vault.d.ts +19 -0
  162. package/dist/vendor/core/gateway/vault.js +29 -0
  163. package/dist/vendor/core/graph/adapters.d.ts +43 -0
  164. package/dist/vendor/core/graph/adapters.js +91 -0
  165. package/dist/vendor/core/graph/hotspots.d.ts +22 -0
  166. package/dist/vendor/core/graph/hotspots.js +30 -0
  167. package/dist/vendor/core/graph/index.d.ts +1 -0
  168. package/dist/vendor/core/graph/index.js +2 -0
  169. package/dist/vendor/core/honey/honey-tokens.d.ts +32 -0
  170. package/dist/vendor/core/honey/honey-tokens.js +44 -0
  171. package/dist/vendor/core/index.d.ts +7 -4
  172. package/dist/vendor/core/index.js +222 -64
  173. package/dist/vendor/core/learning/bayesian-update.d.ts +31 -0
  174. package/dist/vendor/core/learning/bayesian-update.js +60 -0
  175. package/dist/vendor/core/learning/prior-sets.d.ts +42 -0
  176. package/dist/vendor/core/learning/prior-sets.js +111 -0
  177. package/dist/vendor/core/learning/promotion-gate.d.ts +17 -0
  178. package/dist/vendor/core/learning/promotion-gate.js +23 -0
  179. package/dist/vendor/core/leash/blast-radius.d.ts +42 -0
  180. package/dist/vendor/core/leash/blast-radius.js +156 -0
  181. package/dist/vendor/core/leash/policy-leash.d.ts +31 -0
  182. package/dist/vendor/core/leash/policy-leash.js +117 -0
  183. package/dist/vendor/core/memo/memo.d.ts +63 -0
  184. package/dist/vendor/core/memo/memo.js +97 -0
  185. package/dist/vendor/core/memory/learning-pipeline.d.ts +154 -0
  186. package/dist/vendor/core/memory/learning-pipeline.js +391 -0
  187. package/dist/vendor/core/memory/palace.d.ts +84 -0
  188. package/dist/vendor/core/memory/palace.js +379 -0
  189. package/dist/vendor/core/merge/ast-merge.d.ts +22 -0
  190. package/dist/vendor/core/merge/ast-merge.js +350 -0
  191. package/dist/vendor/core/merge/text-merge.d.ts +12 -0
  192. package/dist/vendor/core/merge/text-merge.js +182 -0
  193. package/dist/vendor/core/otel/tracer.d.ts +45 -0
  194. package/dist/vendor/core/otel/tracer.js +116 -0
  195. package/dist/vendor/core/parallel/parallel-attempts.d.ts +28 -0
  196. package/dist/vendor/core/parallel/parallel-attempts.js +41 -0
  197. package/dist/vendor/core/parallel/scorer.d.ts +24 -0
  198. package/dist/vendor/core/parallel/scorer.js +65 -0
  199. package/dist/vendor/core/pattern-detection.d.ts +64 -0
  200. package/dist/vendor/core/pattern-detection.js +108 -0
  201. package/dist/vendor/core/persistence/checkpoint.d.ts +44 -0
  202. package/dist/vendor/core/persistence/checkpoint.js +156 -0
  203. package/dist/vendor/core/persistence/cleanup.d.ts +22 -0
  204. package/dist/vendor/core/persistence/cleanup.js +131 -0
  205. package/dist/vendor/core/persistence/index.d.ts +2 -0
  206. package/dist/vendor/core/persistence/index.js +1 -0
  207. package/dist/vendor/core/persistence/runs-reader.d.ts +52 -0
  208. package/dist/vendor/core/persistence/runs-reader.js +84 -0
  209. package/dist/vendor/core/persistence/store.d.ts +6 -1
  210. package/dist/vendor/core/persistence/store.js +5 -0
  211. package/dist/vendor/core/policy/file-touch-quota.d.ts +60 -0
  212. package/dist/vendor/core/policy/file-touch-quota.js +105 -0
  213. package/dist/vendor/core/policy/policy-loader.d.ts +30 -0
  214. package/dist/vendor/core/policy/policy-loader.js +170 -0
  215. package/dist/vendor/core/policy/policy-schema.d.ts +55 -0
  216. package/dist/vendor/core/policy/policy-schema.js +78 -0
  217. package/dist/vendor/core/policy.d.ts +6 -0
  218. package/dist/vendor/core/probe/probe.d.ts +49 -0
  219. package/dist/vendor/core/probe/probe.js +115 -0
  220. package/dist/vendor/core/proof/patch-proof.d.ts +58 -0
  221. package/dist/vendor/core/proof/patch-proof.js +84 -0
  222. package/dist/vendor/core/proof/semantic-probe.d.ts +25 -0
  223. package/dist/vendor/core/proof/semantic-probe.js +82 -0
  224. package/dist/vendor/core/recovery/failure-mode-runner.d.ts +29 -0
  225. package/dist/vendor/core/recovery/failure-mode-runner.js +39 -0
  226. package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
  227. package/dist/vendor/core/red-blue/red-phase.js +141 -0
  228. package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
  229. package/dist/vendor/core/red-blue/risk-tiers.js +33 -0
  230. package/dist/vendor/core/replay/replay.d.ts +85 -0
  231. package/dist/vendor/core/replay/replay.js +109 -0
  232. package/dist/vendor/core/router/engine.d.ts +54 -0
  233. package/dist/vendor/core/router/engine.js +131 -0
  234. package/dist/vendor/core/router/index.d.ts +1 -0
  235. package/dist/vendor/core/router/index.js +2 -0
  236. package/dist/vendor/core/router/trust-calibration.d.ts +57 -0
  237. package/dist/vendor/core/router/trust-calibration.js +127 -0
  238. package/dist/vendor/core/run-martin.d.ts +2 -0
  239. package/dist/vendor/core/run-martin.js +287 -0
  240. package/dist/vendor/core/security/cve-scanner.d.ts +62 -0
  241. package/dist/vendor/core/security/cve-scanner.js +178 -0
  242. package/dist/vendor/core/sentinel/efficiency-sentinel.d.ts +29 -0
  243. package/dist/vendor/core/sentinel/efficiency-sentinel.js +30 -0
  244. package/dist/vendor/core/sentinel/progress-guard.d.ts +35 -0
  245. package/dist/vendor/core/sentinel/progress-guard.js +46 -0
  246. package/dist/vendor/core/siem/siem-emitter.d.ts +49 -0
  247. package/dist/vendor/core/siem/siem-emitter.js +157 -0
  248. package/dist/vendor/core/strategy/attempt-brief.d.ts +22 -0
  249. package/dist/vendor/core/strategy/attempt-brief.js +89 -0
  250. package/dist/vendor/core/summarize/diff-summary.d.ts +35 -0
  251. package/dist/vendor/core/summarize/diff-summary.js +204 -0
  252. package/dist/vendor/core/surface-signals.d.ts +21 -0
  253. package/dist/vendor/core/surface-signals.js +139 -0
  254. package/dist/vendor/core/truth/truth-wall.d.ts +51 -0
  255. package/dist/vendor/core/truth/truth-wall.js +69 -0
  256. package/dist/vendor/core/truth-spine.d.ts +26 -0
  257. package/dist/vendor/core/truth-spine.js +62 -0
  258. package/dist/vendor/core/types.d.ts +115 -0
  259. package/dist/vendor/core/types.js +2 -0
  260. package/dist/vendor/core/verification/tiered-verify.d.ts +17 -0
  261. package/dist/vendor/core/verification/tiered-verify.js +29 -0
  262. package/dist/vendor/core/verifier-pyramid.d.ts +32 -0
  263. package/dist/vendor/core/verifier-pyramid.js +111 -0
  264. package/dist/vendor/core/workflow-artifacts.d.ts +99 -0
  265. package/dist/vendor/core/workflow-artifacts.js +668 -0
  266. package/dist/vendor/core/wrap/supervised-run.d.ts +96 -0
  267. package/dist/vendor/core/wrap/supervised-run.js +178 -0
  268. package/docs/assets/cli-animated.svg +139 -0
  269. package/docs/assets/cli-static.svg +34 -0
  270. package/docs/assets/github-hero-v2.svg +23 -0
  271. package/docs/assets/martin-raplph.png.jpg +0 -0
  272. package/docs/assets/martinloop-logo.png +0 -0
  273. package/docs/assets/nvidia-inception-program-light.png +0 -0
  274. package/docs/assets/nvidia-inception-program.png +0 -0
  275. package/docs/assets/phase3c-sidesidebyside-demo.html +228 -0
  276. package/docs/assets/side-by-side.svg +134 -0
  277. package/docs/oss/CLAUDE-CODE-WALKTHROUGH.md +142 -0
  278. package/docs/oss/EXAMPLES.md +9 -1
  279. package/docs/oss/OSS-BOUNDARY-REPORT.json +109 -113
  280. package/docs/oss/OSS-BOUNDARY-REPORT.md +48 -48
  281. package/docs/oss/QUICKSTART.md +39 -4
  282. package/docs/oss/RALPH-LOOP-SAFETY.md +113 -0
  283. package/docs/oss/README.md +7 -4
  284. package/docs/oss/RELEASE-SURFACE-REPORT.json +46 -45
  285. package/docs/oss/RELEASE-SURFACE-REPORT.md +36 -35
  286. package/package.json +129 -49
@@ -0,0 +1,233 @@
1
+ import { createHash } from "node:crypto";
2
+ import { runSubprocess } from "./cli-bridge.js";
3
+ import { diffStatsFromNumstat } from "./runtime-support.js";
4
+ export async function captureBaseline(options) {
5
+ const [startHeadSha, startTrackedStatus, startUntrackedSet] = await Promise.all([
6
+ readRequiredScalar(options.repoRoot, ["rev-parse", "HEAD"], options.timeoutMs, options.spawnImpl),
7
+ readOptionalStdout(options.repoRoot, ["status", "--porcelain=v1", "--untracked-files=no"], options.timeoutMs, options.spawnImpl),
8
+ readLines(options.repoRoot, ["ls-files", "--others", "--exclude-standard"], options.timeoutMs, options.spawnImpl)
9
+ ]);
10
+ const startTrackedSet = parsePorcelainTrackedPaths(startTrackedStatus);
11
+ const startTrackedDiffSha256 = await collectPathDiffHashes({
12
+ repoRoot: options.repoRoot,
13
+ baseRef: startHeadSha,
14
+ paths: startTrackedSet,
15
+ timeoutMs: options.timeoutMs,
16
+ spawnImpl: options.spawnImpl
17
+ });
18
+ return {
19
+ startHeadSha,
20
+ startTrackedStatus,
21
+ startTrackedSet,
22
+ startTrackedDiffSha256,
23
+ startUntrackedSet,
24
+ startTimestamp: new Date().toISOString(),
25
+ worktreeClean: startTrackedStatus.trim().length === 0 && startUntrackedSet.length === 0,
26
+ pilotRepoClass: options.pilotRepoClass ?? null
27
+ };
28
+ }
29
+ export async function normalizeAdapterGitState(options) {
30
+ const endHeadShaBeforeNormalization = await readRequiredScalar(options.repoRoot, ["rev-parse", "HEAD"], options.timeoutMs, options.spawnImpl);
31
+ const normalizationApplied = endHeadShaBeforeNormalization !== options.baseline.startHeadSha;
32
+ if (!normalizationApplied) {
33
+ return {
34
+ startHeadSha: options.baseline.startHeadSha,
35
+ endHeadShaBeforeNormalization,
36
+ normalizationApplied: false,
37
+ normalizationMode: "none",
38
+ normalizationSucceeded: true,
39
+ headAfterNormalization: endHeadShaBeforeNormalization
40
+ };
41
+ }
42
+ const resetResult = await runSubprocess("git", ["reset", "--mixed", options.baseline.startHeadSha], {
43
+ cwd: options.repoRoot,
44
+ timeoutMs: options.timeoutMs,
45
+ spawnImpl: options.spawnImpl
46
+ });
47
+ const headAfterNormalization = resetResult.exitCode === 0
48
+ ? await readRequiredScalar(options.repoRoot, ["rev-parse", "HEAD"], options.timeoutMs, options.spawnImpl)
49
+ : endHeadShaBeforeNormalization;
50
+ return {
51
+ startHeadSha: options.baseline.startHeadSha,
52
+ endHeadShaBeforeNormalization,
53
+ normalizationApplied: true,
54
+ normalizationMode: "reset_mixed_to_start_head",
55
+ normalizationSucceeded: resetResult.exitCode === 0 && headAfterNormalization === options.baseline.startHeadSha,
56
+ headAfterNormalization
57
+ };
58
+ }
59
+ export async function collectScopeSurface(options) {
60
+ const [nameStatusResult, numstatResult, endUntrackedSet] = await Promise.all([
61
+ runSubprocess("git", ["diff", "--name-status", "--no-renames", options.baseline.startHeadSha], {
62
+ cwd: options.repoRoot,
63
+ timeoutMs: options.timeoutMs,
64
+ spawnImpl: options.spawnImpl
65
+ }),
66
+ runSubprocess("git", ["diff", "--numstat", "--no-renames", options.baseline.startHeadSha], {
67
+ cwd: options.repoRoot,
68
+ timeoutMs: options.timeoutMs,
69
+ spawnImpl: options.spawnImpl
70
+ }),
71
+ readLines(options.repoRoot, ["ls-files", "--others", "--exclude-standard"], options.timeoutMs, options.spawnImpl)
72
+ ]);
73
+ const trackedChangedPaths = [];
74
+ const deletedPaths = [];
75
+ const changedBaselinePaths = [];
76
+ const seenTracked = new Set();
77
+ const seenDeleted = new Set();
78
+ const baselineTracked = new Set(options.baseline.startTrackedSet.map(normalizeGitPath));
79
+ const candidateTrackedPaths = [];
80
+ if (nameStatusResult.exitCode === 0) {
81
+ for (const rawLine of splitLines(nameStatusResult.stdout)) {
82
+ const parts = rawLine.split(/\t+/u).map((entry) => normalizeGitPath(entry));
83
+ const status = parts[0]?.trim() ?? "";
84
+ const candidatePath = parts.at(-1);
85
+ if (!status || !candidatePath) {
86
+ continue;
87
+ }
88
+ if (status.startsWith("D")) {
89
+ candidateTrackedPaths.push({ path: candidatePath, deleted: true });
90
+ continue;
91
+ }
92
+ candidateTrackedPaths.push({ path: candidatePath, deleted: false });
93
+ }
94
+ }
95
+ const endTrackedDiffSha256 = await collectPathDiffHashes({
96
+ repoRoot: options.repoRoot,
97
+ baseRef: options.baseline.startHeadSha,
98
+ paths: candidateTrackedPaths.map((candidate) => candidate.path),
99
+ timeoutMs: options.timeoutMs,
100
+ spawnImpl: options.spawnImpl
101
+ });
102
+ for (const candidate of candidateTrackedPaths) {
103
+ const baselineHash = options.baseline.startTrackedDiffSha256[candidate.path];
104
+ const endHash = endTrackedDiffSha256[candidate.path];
105
+ const changedSinceBaseline = !baselineTracked.has(candidate.path) ||
106
+ baselineHash === undefined ||
107
+ endHash === undefined ||
108
+ baselineHash !== endHash;
109
+ if (!changedSinceBaseline) {
110
+ continue;
111
+ }
112
+ if (baselineTracked.has(candidate.path)) {
113
+ changedBaselinePaths.push(candidate.path);
114
+ }
115
+ if (candidate.deleted) {
116
+ if (!seenDeleted.has(candidate.path)) {
117
+ seenDeleted.add(candidate.path);
118
+ deletedPaths.push(candidate.path);
119
+ }
120
+ continue;
121
+ }
122
+ if (!seenTracked.has(candidate.path)) {
123
+ seenTracked.add(candidate.path);
124
+ trackedChangedPaths.push(candidate.path);
125
+ }
126
+ }
127
+ const startingUntracked = new Set(options.baseline.startUntrackedSet.map(normalizeGitPath));
128
+ const newUntrackedPaths = endUntrackedSet
129
+ .map(normalizeGitPath)
130
+ .filter((entry) => !startingUntracked.has(entry));
131
+ const allChangedPaths = uniquePaths([
132
+ ...trackedChangedPaths,
133
+ ...deletedPaths,
134
+ ...newUntrackedPaths
135
+ ]);
136
+ const diffStats = allChangedPaths.length > 0
137
+ ? await collectDiffStats({
138
+ repoRoot: options.repoRoot,
139
+ baseRef: options.baseline.startHeadSha,
140
+ paths: allChangedPaths,
141
+ fallbackStdout: numstatResult.stdout,
142
+ fallbackExitCode: numstatResult.exitCode,
143
+ timeoutMs: options.timeoutMs,
144
+ spawnImpl: options.spawnImpl
145
+ })
146
+ : undefined;
147
+ return {
148
+ trackedChangedPaths,
149
+ deletedPaths,
150
+ newUntrackedPaths,
151
+ allChangedPaths,
152
+ changedBaselinePaths: uniquePaths(changedBaselinePaths),
153
+ baselineWasClean: options.baseline.worktreeClean,
154
+ noCodeChange: allChangedPaths.length === 0,
155
+ ...(diffStats ? { diffStats } : {})
156
+ };
157
+ }
158
+ function uniquePaths(paths) {
159
+ return [...new Set(paths.map(normalizeGitPath).filter(Boolean))];
160
+ }
161
+ function splitLines(stdout) {
162
+ return stdout
163
+ .split(/\r?\n/u)
164
+ .map((line) => line.trim())
165
+ .filter(Boolean);
166
+ }
167
+ function parsePorcelainTrackedPaths(status) {
168
+ const paths = [];
169
+ for (const line of status.split(/\r?\n/u).filter(Boolean)) {
170
+ const code = line.slice(0, 2);
171
+ if (code === "??" || code === "!!") {
172
+ continue;
173
+ }
174
+ const rawPath = line.slice(3).trim();
175
+ if (!rawPath) {
176
+ continue;
177
+ }
178
+ for (const path of rawPath.split(" -> ")) {
179
+ paths.push(path);
180
+ }
181
+ }
182
+ return uniquePaths(paths);
183
+ }
184
+ async function collectPathDiffHashes(options) {
185
+ const hashes = {};
186
+ for (const path of uniquePaths(options.paths)) {
187
+ const result = await runSubprocess("git", ["diff", "--binary", options.baseRef, "--", path], {
188
+ cwd: options.repoRoot,
189
+ timeoutMs: options.timeoutMs,
190
+ spawnImpl: options.spawnImpl
191
+ });
192
+ hashes[path] = sha256Text(JSON.stringify({
193
+ stdout: result.stdout,
194
+ stderr: result.exitCode === 0 ? "" : result.stderr,
195
+ exitCode: result.exitCode
196
+ }));
197
+ }
198
+ return hashes;
199
+ }
200
+ async function collectDiffStats(options) {
201
+ const result = await runSubprocess("git", ["diff", "--numstat", "--no-renames", options.baseRef, "--", ...uniquePaths(options.paths)], {
202
+ cwd: options.repoRoot,
203
+ timeoutMs: options.timeoutMs,
204
+ spawnImpl: options.spawnImpl
205
+ });
206
+ if (result.exitCode === 0) {
207
+ return diffStatsFromNumstat(result.stdout);
208
+ }
209
+ return options.fallbackExitCode === 0 ? diffStatsFromNumstat(options.fallbackStdout) : undefined;
210
+ }
211
+ function normalizeGitPath(path) {
212
+ return path.replace(/\\/gu, "/").trim();
213
+ }
214
+ function sha256Text(value) {
215
+ return createHash("sha256").update(value).digest("hex");
216
+ }
217
+ async function readRequiredScalar(repoRoot, args, timeoutMs, spawnImpl) {
218
+ const result = await runSubprocess("git", args, { cwd: repoRoot, timeoutMs, spawnImpl });
219
+ if (result.exitCode !== 0) {
220
+ const detail = result.stderr.trim() || result.stdout.trim() || args.join(" ");
221
+ throw new Error(`Git baseline command failed: ${detail}`);
222
+ }
223
+ return result.stdout.trim();
224
+ }
225
+ async function readOptionalStdout(repoRoot, args, timeoutMs, spawnImpl) {
226
+ const result = await runSubprocess("git", args, { cwd: repoRoot, timeoutMs, spawnImpl });
227
+ return result.exitCode === 0 ? result.stdout : "";
228
+ }
229
+ async function readLines(repoRoot, args, timeoutMs, spawnImpl) {
230
+ const result = await runSubprocess("git", args, { cwd: repoRoot, timeoutMs, spawnImpl });
231
+ return result.exitCode === 0 ? splitLines(result.stdout) : [];
232
+ }
233
+ //# sourceMappingURL=git-baseline.js.map
@@ -2,4 +2,5 @@ export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "
2
2
  export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
3
3
  export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions } from "./stub-agent-cli.js";
4
4
  export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
5
+ export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
5
6
  export type { SpawnLike, SubprocessResult, VerificationOutcome } from "./cli-bridge.js";
@@ -2,4 +2,5 @@ export { createDirectProviderAdapter } from "./direct-provider.js";
2
2
  export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
3
3
  export { createStubAgentCliAdapter } from "./stub-agent-cli.js";
4
4
  export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter } from "./claude-cli.js";
5
+ export { createVerifierOnlyAdapter } from "./verifier-only.js";
5
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * OpenRouter Adapter — real LLM-backed coding agent via OpenRouter API.
3
+ *
4
+ * Uses function-calling (tool use) to give the LLM read_file, write_file,
5
+ * and run_command tools. Loops until verification passes or max turns reached.
6
+ *
7
+ * Compatible with any OpenRouter model that supports function calling:
8
+ * openai/gpt-4o, openai/gpt-4o-mini, anthropic/claude-3.5-sonnet, etc.
9
+ */
10
+ import type { MartinAdapter } from "../core/index.js";
11
+ export declare function createOpenRouterAdapter(options: {
12
+ apiKey: string;
13
+ model?: string;
14
+ maxTurns?: number;
15
+ }): MartinAdapter;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * OpenRouter Adapter — real LLM-backed coding agent via OpenRouter API.
3
+ *
4
+ * Uses function-calling (tool use) to give the LLM read_file, write_file,
5
+ * and run_command tools. Loops until verification passes or max turns reached.
6
+ *
7
+ * Compatible with any OpenRouter model that supports function calling:
8
+ * openai/gpt-4o, openai/gpt-4o-mini, anthropic/claude-3.5-sonnet, etc.
9
+ */
10
+ import { execFile, exec } from "node:child_process";
11
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
12
+ import { join, dirname, resolve } from "node:path";
13
+ import { promisify } from "node:util";
14
+ const execAsync = promisify(exec);
15
+ const execFileAsync = promisify(execFile);
16
+ const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
17
+ const MAX_TOOL_TURNS = 20;
18
+ // ─── Tool definitions ─────────────────────────────────────────────────────────
19
+ const TOOLS = [
20
+ {
21
+ type: "function",
22
+ function: {
23
+ name: "read_file",
24
+ description: "Read the contents of a file in the repository.",
25
+ parameters: {
26
+ type: "object",
27
+ properties: {
28
+ path: { type: "string", description: "File path relative to repo root" }
29
+ },
30
+ required: ["path"]
31
+ }
32
+ }
33
+ },
34
+ {
35
+ type: "function",
36
+ function: {
37
+ name: "write_file",
38
+ description: "Write content to a file. Creates parent directories as needed.",
39
+ parameters: {
40
+ type: "object",
41
+ properties: {
42
+ path: { type: "string", description: "File path relative to repo root" },
43
+ content: { type: "string", description: "File content to write" }
44
+ },
45
+ required: ["path", "content"]
46
+ }
47
+ }
48
+ },
49
+ {
50
+ type: "function",
51
+ function: {
52
+ name: "run_command",
53
+ description: "Run a shell command in the repo root. Returns stdout + stderr.",
54
+ parameters: {
55
+ type: "object",
56
+ properties: {
57
+ command: { type: "string", description: "Shell command to execute" }
58
+ },
59
+ required: ["command"]
60
+ }
61
+ }
62
+ },
63
+ {
64
+ type: "function",
65
+ function: {
66
+ name: "task_complete",
67
+ description: "Signal that the task is complete and all acceptance criteria are met.",
68
+ parameters: {
69
+ type: "object",
70
+ properties: {
71
+ summary: { type: "string", description: "Summary of what was accomplished" }
72
+ },
73
+ required: ["summary"]
74
+ }
75
+ }
76
+ }
77
+ ];
78
+ // ─── Tool executor ─────────────────────────────────────────────────────────────
79
+ async function executeTool(name, args, repoRoot) {
80
+ try {
81
+ switch (name) {
82
+ case "read_file": {
83
+ const fullPath = resolve(join(repoRoot, args["path"] ?? ""));
84
+ // Prevent path traversal outside repoRoot
85
+ if (!fullPath.startsWith(resolve(repoRoot))) {
86
+ return `BLOCKED: Cannot read files outside repo root`;
87
+ }
88
+ const content = await readFile(fullPath, "utf8").catch(e => `ERROR: ${e.message}`);
89
+ return content;
90
+ }
91
+ case "write_file": {
92
+ const fullPath = resolve(join(repoRoot, args["path"] ?? ""));
93
+ const resolvedRoot = resolve(repoRoot);
94
+ // Prevent path traversal outside repoRoot
95
+ if (!fullPath.startsWith(resolvedRoot)) {
96
+ return `BLOCKED: Cannot write files outside repo root (attempted: ${args["path"]})`;
97
+ }
98
+ // Block writes to dependency management files
99
+ const DENIED_WRITE_PATTERNS = /\b(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/;
100
+ if (DENIED_WRITE_PATTERNS.test(fullPath)) {
101
+ return `BLOCKED: Cannot modify dependency files (${args["path"]}). Use existing types from the source code.`;
102
+ }
103
+ await mkdir(dirname(fullPath), { recursive: true });
104
+ await writeFile(fullPath, args["content"] ?? "", "utf8");
105
+ return `OK: wrote ${args["path"]}`;
106
+ }
107
+ case "run_command": {
108
+ const cmd = args["command"] ?? "";
109
+ // Block package installation commands — they modify dependency files and trigger the safety leash
110
+ const BLOCKED_PATTERNS = /\b(npm|pnpm|yarn|bun)\s+(install|add|i\b|ci\b)/i;
111
+ if (BLOCKED_PATTERNS.test(cmd)) {
112
+ return "BLOCKED: Package installation commands are not allowed. Use existing imports and types from the source file.";
113
+ }
114
+ const { stdout, stderr } = await execAsync(cmd, { cwd: repoRoot, timeout: 30_000 }).catch(e => ({
115
+ stdout: "",
116
+ stderr: e.message
117
+ }));
118
+ return [stdout, stderr].filter(Boolean).join("\n").slice(0, 4000);
119
+ }
120
+ case "task_complete":
121
+ return `COMPLETE: ${args["summary"]}`;
122
+ default:
123
+ return `ERROR: unknown tool ${name}`;
124
+ }
125
+ }
126
+ catch (e) {
127
+ return `ERROR: ${e.message}`;
128
+ }
129
+ }
130
+ // ─── Verification runner ──────────────────────────────────────────────────────
131
+ async function runVerification(verificationPlan, repoRoot) {
132
+ const results = [];
133
+ let allPassed = true;
134
+ for (const cmd of verificationPlan) {
135
+ try {
136
+ const { stdout, stderr } = await execAsync(cmd, { cwd: repoRoot, timeout: 60_000 });
137
+ const out = [stdout, stderr].filter(Boolean).join(" ").slice(0, 500);
138
+ results.push(`✓ ${cmd}: ${out || "OK"}`);
139
+ }
140
+ catch (e) {
141
+ allPassed = false;
142
+ results.push(`✗ ${cmd}: ${e.stderr?.slice(0, 300) ?? e.message.slice(0, 300)}`);
143
+ }
144
+ }
145
+ return { passed: allPassed, summary: results.join("\n") };
146
+ }
147
+ // ─── Main adapter function ────────────────────────────────────────────────────
148
+ export function createOpenRouterAdapter(options) {
149
+ const model = options.model ?? "openai/gpt-4o-mini";
150
+ const maxTurns = options.maxTurns ?? MAX_TOOL_TURNS;
151
+ return {
152
+ adapterId: `openrouter:${model}`,
153
+ kind: "direct-provider",
154
+ label: `OpenRouter (${model})`,
155
+ metadata: {
156
+ providerId: "openrouter",
157
+ model,
158
+ transport: "routed_http",
159
+ capabilities: {
160
+ usageSettlement: true,
161
+ structuredErrors: true,
162
+ diffArtifacts: false
163
+ }
164
+ },
165
+ withModel(newModel) {
166
+ return createOpenRouterAdapter({ ...options, model: newModel });
167
+ },
168
+ async execute(request) {
169
+ const repoRoot = request.context.repoRoot ?? process.cwd();
170
+ const startTime = Date.now();
171
+ let tokensIn = 0;
172
+ let tokensOut = 0;
173
+ const systemPrompt = [
174
+ "You are an expert software engineer completing coding tasks.",
175
+ "You have tools to read files, write files, and run commands.",
176
+ "Your goal: complete the task described in the user message.",
177
+ "Strategy:",
178
+ "1. Read relevant existing files first to understand the codebase",
179
+ "2. Make the required changes using write_file",
180
+ "3. Run verification commands to confirm success",
181
+ "4. Call task_complete when all acceptance criteria are met",
182
+ "",
183
+ "IMPORTANT: Only modify files explicitly required by the task.",
184
+ "Do not modify: leash.ts, policy.ts, grounding.ts, contracts/src/index.ts",
185
+ "",
186
+ `Repo root: ${repoRoot}`,
187
+ `Remaining budget: $${request.context.remainingBudgetUsd.toFixed(2)}`,
188
+ `Remaining iterations: ${request.context.remainingIterations}`
189
+ ].join("\n");
190
+ const userMessage = [
191
+ `# Task: ${request.context.taskTitle}`,
192
+ "",
193
+ `## Objective`,
194
+ request.context.objective,
195
+ "",
196
+ ...(request.context.verificationPlan.length > 0 ? [
197
+ "## Verification (must pass)",
198
+ ...request.context.verificationPlan.map(v => `- ${v}`)
199
+ ] : []),
200
+ ...(request.context.acceptanceCriteria?.length ? [
201
+ "",
202
+ "## Acceptance Criteria",
203
+ ...request.context.acceptanceCriteria.map(c => `- ${c}`)
204
+ ] : []),
205
+ ...(request.previousAttempts.length > 0 ? [
206
+ "",
207
+ `## Previous attempts (${request.previousAttempts.length} failed)`,
208
+ request.previousAttempts.slice(-2).map(a => `- ${a.summary ?? "failed"}`).join("\n")
209
+ ] : []),
210
+ "",
211
+ request.context.focus
212
+ ].join("\n");
213
+ const messages = [
214
+ { role: "system", content: systemPrompt },
215
+ { role: "user", content: userMessage }
216
+ ];
217
+ let completionSummary = "Agent did not complete.";
218
+ let taskCompleted = false;
219
+ for (let turn = 0; turn < maxTurns; turn++) {
220
+ const body = JSON.stringify({
221
+ model,
222
+ messages,
223
+ tools: TOOLS,
224
+ tool_choice: "auto",
225
+ max_tokens: 4096,
226
+ temperature: 0.1
227
+ });
228
+ const res = await fetch(OPENROUTER_URL, {
229
+ method: "POST",
230
+ headers: {
231
+ "Content-Type": "application/json",
232
+ "Authorization": `Bearer ${options.apiKey}`,
233
+ "HTTP-Referer": "https://martin-loop.dev",
234
+ "X-Title": "Martin Loop Agent"
235
+ },
236
+ body
237
+ }).catch(e => { throw new Error(`OpenRouter fetch failed: ${e.message}`); });
238
+ if (!res.ok) {
239
+ const text = await res.text().catch(() => "");
240
+ throw new Error(`OpenRouter API error ${res.status}: ${text.slice(0, 200)}`);
241
+ }
242
+ const data = await res.json();
243
+ tokensIn += data.usage?.prompt_tokens ?? 0;
244
+ tokensOut += data.usage?.completion_tokens ?? 0;
245
+ const choice = data.choices[0];
246
+ if (!choice)
247
+ break;
248
+ const assistantMsg = choice.message;
249
+ messages.push({ role: "assistant", content: assistantMsg.content, tool_calls: assistantMsg.tool_calls });
250
+ if (choice.finish_reason === "stop" || !assistantMsg.tool_calls?.length) {
251
+ completionSummary = assistantMsg.content ?? "Task complete.";
252
+ break;
253
+ }
254
+ // Execute tool calls
255
+ for (const toolCall of assistantMsg.tool_calls ?? []) {
256
+ let args = {};
257
+ try {
258
+ args = JSON.parse(toolCall.function.arguments);
259
+ }
260
+ catch {
261
+ args = {};
262
+ }
263
+ const result = await executeTool(toolCall.function.name, args, repoRoot);
264
+ if (toolCall.function.name === "task_complete") {
265
+ completionSummary = args["summary"] ?? "Task completed successfully.";
266
+ taskCompleted = true;
267
+ }
268
+ messages.push({
269
+ role: "tool",
270
+ tool_call_id: toolCall.id,
271
+ name: toolCall.function.name,
272
+ content: result
273
+ });
274
+ }
275
+ if (taskCompleted)
276
+ break;
277
+ }
278
+ // Run verification
279
+ const verification = await runVerification(request.context.verificationPlan, repoRoot);
280
+ // Compute cost (gpt-4o-mini pricing: $0.15/1M input, $0.60/1M output)
281
+ const COST_PER_1K = { input: 0.00015, output: 0.0006 };
282
+ const costUsd = (tokensIn / 1000) * COST_PER_1K.input + (tokensOut / 1000) * COST_PER_1K.output;
283
+ return {
284
+ status: verification.passed ? "completed" : "failed",
285
+ summary: completionSummary,
286
+ usage: {
287
+ actualUsd: Math.round(costUsd * 100000) / 100000,
288
+ tokensIn,
289
+ tokensOut,
290
+ provenance: "actual"
291
+ },
292
+ verification,
293
+ ...(verification.passed ? {} : {
294
+ failure: {
295
+ message: `Verification failed: ${verification.summary.slice(0, 300)}`
296
+ }
297
+ })
298
+ };
299
+ }
300
+ };
301
+ }
302
+ //# sourceMappingURL=openrouter-adapter.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Token usage extraction and cost calculation for Claude CLI JSON output.
3
+ *
4
+ * Extracted from claude-cli.ts so it can be unit-tested without spawning
5
+ * a real subprocess. All exports are stable internal APIs.
6
+ */
7
+ import type { MartinAdapterResult } from "../core/index.js";
8
+ export declare const BLENDED_INPUT_COST_PER_1K = 0.003;
9
+ export declare const BLENDED_OUTPUT_COST_PER_1K = 0.012;
10
+ /** Per-model cost overrides. Falls back to blended average when model unknown. */
11
+ export declare const MODEL_PRICING: Record<string, {
12
+ inputPer1K: number;
13
+ outputPer1K: number;
14
+ }>;
15
+ export interface ClaudeJsonOutput {
16
+ type: string;
17
+ subtype?: string;
18
+ result?: string;
19
+ error?: string;
20
+ usage?: {
21
+ inputTokens?: number;
22
+ outputTokens?: number;
23
+ cacheReadInputTokens?: number;
24
+ cacheCreationInputTokens?: number;
25
+ /**
26
+ * Extended-thinking output tokens from Claude CLI when extended thinking is
27
+ * enabled. Billed at the same per-token rate as regular output tokens but
28
+ * tracked separately for cost transparency.
29
+ */
30
+ thinkingOutputTokens?: number;
31
+ };
32
+ }
33
+ /**
34
+ * Parse a Claude CLI JSON output block into a normalised MartinUsage object.
35
+ *
36
+ * Thinking tokens (when present) are:
37
+ * - stored separately as `thinkingTokensOut`
38
+ * - billed at the same output token rate and included in `actualUsd`
39
+ * - NOT added to `tokensOut` (regular output tokens stay clean)
40
+ */
41
+ export declare function extractUsage(parsed: ClaudeJsonOutput | undefined, modelLabel: string | undefined): MartinAdapterResult["usage"] & {
42
+ thinkingTokensOut: number;
43
+ };
44
+ /**
45
+ * Alias exported for unit tests that import directly from this module.
46
+ * Identical to `extractUsage` — no test-only behaviour.
47
+ */
48
+ export declare const extractUsageForTest: typeof extractUsage;
@@ -0,0 +1,66 @@
1
+ import { normalizeUsage } from "./runtime-support.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Model pricing
4
+ // ---------------------------------------------------------------------------
5
+ export const BLENDED_INPUT_COST_PER_1K = 0.003; // $/1K input tokens
6
+ export const BLENDED_OUTPUT_COST_PER_1K = 0.012; // $/1K output tokens
7
+ /** Per-model cost overrides. Falls back to blended average when model unknown. */
8
+ export const MODEL_PRICING = {
9
+ "claude-opus-4-6": { inputPer1K: 0.015, outputPer1K: 0.075 },
10
+ "claude-sonnet-4-6": { inputPer1K: 0.003, outputPer1K: 0.015 },
11
+ "claude-haiku-4-5-20251001": { inputPer1K: 0.00025, outputPer1K: 0.00125 },
12
+ // Legacy short names
13
+ "claude-haiku-4-5": { inputPer1K: 0.00025, outputPer1K: 0.00125 },
14
+ "claude-opus": { inputPer1K: 0.015, outputPer1K: 0.075 },
15
+ "claude-sonnet": { inputPer1K: 0.003, outputPer1K: 0.015 },
16
+ "claude-haiku": { inputPer1K: 0.00025, outputPer1K: 0.00125 },
17
+ };
18
+ // ---------------------------------------------------------------------------
19
+ // Usage extraction
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Parse a Claude CLI JSON output block into a normalised MartinUsage object.
23
+ *
24
+ * Thinking tokens (when present) are:
25
+ * - stored separately as `thinkingTokensOut`
26
+ * - billed at the same output token rate and included in `actualUsd`
27
+ * - NOT added to `tokensOut` (regular output tokens stay clean)
28
+ */
29
+ export function extractUsage(parsed, modelLabel) {
30
+ if (!parsed?.usage) {
31
+ return {
32
+ ...normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
33
+ thinkingTokensOut: 0
34
+ };
35
+ }
36
+ const rawIn = parsed.usage.inputTokens ?? 0;
37
+ const rawOut = parsed.usage.outputTokens ?? 0;
38
+ const cacheRead = parsed.usage.cacheReadInputTokens ?? 0;
39
+ const cacheCreate = parsed.usage.cacheCreationInputTokens ?? 0;
40
+ const thinkingOut = parsed.usage.thinkingOutputTokens ?? 0;
41
+ // tokensIn = all input-side tokens (prompt + cache hits + cache writes)
42
+ const tokensIn = rawIn + cacheRead + cacheCreate;
43
+ // tokensOut = regular completion tokens only (thinking tracked separately)
44
+ const tokensOut = rawOut;
45
+ const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
46
+ { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
47
+ // Thinking tokens billed at output token rate per Anthropic billing docs
48
+ const actualUsd = (tokensIn / 1000) * pricing.inputPer1K +
49
+ (tokensOut / 1000) * pricing.outputPer1K +
50
+ (thinkingOut / 1000) * pricing.outputPer1K;
51
+ return {
52
+ ...normalizeUsage({
53
+ actualUsd: Number(actualUsd.toFixed(6)),
54
+ tokensIn,
55
+ tokensOut,
56
+ provenance: "actual"
57
+ }),
58
+ thinkingTokensOut: thinkingOut
59
+ };
60
+ }
61
+ /**
62
+ * Alias exported for unit tests that import directly from this module.
63
+ * Identical to `extractUsage` — no test-only behaviour.
64
+ */
65
+ export const extractUsageForTest = extractUsage;
66
+ //# sourceMappingURL=usage.js.map