martin-loop 0.1.5 → 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 (274) hide show
  1. package/CODE_OF_CONDUCT.md +32 -0
  2. package/LICENSE +21 -21
  3. package/README.md +307 -398
  4. package/demo/seeded-workspace/README.md +35 -35
  5. package/demo/seeded-workspace/TASKS.md +29 -29
  6. package/demo/seeded-workspace/martin.config.yaml +11 -11
  7. package/demo/seeded-workspace/package.json +8 -8
  8. package/demo/seeded-workspace/src/invoice-summary.js +11 -11
  9. package/demo/seeded-workspace/test/invoice-summary.test.js +20 -20
  10. package/dist/bin/martin-loop.js +0 -0
  11. package/dist/vendor/adapters/counter.d.ts +1 -0
  12. package/dist/vendor/adapters/counter.js +4 -0
  13. package/dist/vendor/adapters/git-baseline.d.ts +50 -0
  14. package/dist/vendor/adapters/git-baseline.js +233 -0
  15. package/dist/vendor/adapters/openrouter-adapter.d.ts +15 -0
  16. package/dist/vendor/adapters/openrouter-adapter.js +302 -0
  17. package/dist/vendor/adapters/usage.d.ts +48 -0
  18. package/dist/vendor/adapters/usage.js +66 -0
  19. package/dist/vendor/cli/bin/exit.d.ts +12 -0
  20. package/dist/vendor/cli/bin/exit.js +28 -0
  21. package/dist/vendor/cli/commands/analyze.d.ts +5 -0
  22. package/dist/vendor/cli/commands/analyze.js +58 -0
  23. package/dist/vendor/cli/commands/audit-log-verify.d.ts +34 -0
  24. package/dist/vendor/cli/commands/audit-log-verify.js +99 -0
  25. package/dist/vendor/cli/commands/audit.d.ts +8 -0
  26. package/dist/vendor/cli/commands/audit.js +199 -0
  27. package/dist/vendor/cli/commands/corpus.d.ts +5 -0
  28. package/dist/vendor/cli/commands/corpus.js +60 -0
  29. package/dist/vendor/cli/commands/doctor.d.ts +8 -0
  30. package/dist/vendor/cli/commands/doctor.js +219 -0
  31. package/dist/vendor/cli/commands/explain.d.ts +17 -0
  32. package/dist/vendor/cli/commands/explain.js +176 -0
  33. package/dist/vendor/cli/commands/export.d.ts +5 -0
  34. package/dist/vendor/cli/commands/export.js +60 -0
  35. package/dist/vendor/cli/commands/governance.d.ts +8 -0
  36. package/dist/vendor/cli/commands/governance.js +95 -0
  37. package/dist/vendor/cli/commands/improve.d.ts +18 -0
  38. package/dist/vendor/cli/commands/improve.js +396 -0
  39. package/dist/vendor/cli/commands/init.d.ts +8 -0
  40. package/dist/vendor/cli/commands/init.js +281 -0
  41. package/dist/vendor/cli/commands/migration.d.ts +8 -0
  42. package/dist/vendor/cli/commands/migration.js +67 -0
  43. package/dist/vendor/cli/commands/prior.d.ts +23 -0
  44. package/dist/vendor/cli/commands/prior.js +145 -0
  45. package/dist/vendor/cli/commands/resume.d.ts +21 -0
  46. package/dist/vendor/cli/commands/resume.js +73 -0
  47. package/dist/vendor/cli/commands/verify.d.ts +6 -0
  48. package/dist/vendor/cli/commands/verify.js +43 -0
  49. package/dist/vendor/cli/research/public-corpus.d.ts +43 -0
  50. package/dist/vendor/cli/research/public-corpus.js +151 -0
  51. package/dist/vendor/cli/ui/error-card.d.ts +38 -0
  52. package/dist/vendor/cli/ui/error-card.js +103 -0
  53. package/dist/vendor/cli/ui/mission-brief.d.ts +41 -0
  54. package/dist/vendor/cli/ui/mission-brief.js +173 -0
  55. package/dist/vendor/cli/ui/summary-card.d.ts +34 -0
  56. package/dist/vendor/cli/ui/summary-card.js +102 -0
  57. package/dist/vendor/contracts/audit.d.ts +46 -0
  58. package/dist/vendor/contracts/audit.js +360 -0
  59. package/dist/vendor/contracts/post-phase15.d.ts +240 -0
  60. package/dist/vendor/contracts/post-phase15.js +166 -0
  61. package/dist/vendor/core/agent/mandates.d.ts +46 -0
  62. package/dist/vendor/core/agent/mandates.js +178 -0
  63. package/dist/vendor/core/agent/receipts.d.ts +38 -0
  64. package/dist/vendor/core/agent/receipts.js +131 -0
  65. package/dist/vendor/core/agent/signing.d.ts +17 -0
  66. package/dist/vendor/core/agent/signing.js +91 -0
  67. package/dist/vendor/core/attestation/sign.d.ts +25 -0
  68. package/dist/vendor/core/attestation/sign.js +216 -0
  69. package/dist/vendor/core/autonomy/autonomous-promotion.d.ts +120 -0
  70. package/dist/vendor/core/autonomy/autonomous-promotion.js +346 -0
  71. package/dist/vendor/core/autonomy/envelope-v2.d.ts +29 -0
  72. package/dist/vendor/core/autonomy/envelope-v2.js +60 -0
  73. package/dist/vendor/core/autonomy/envelope.d.ts +17 -0
  74. package/dist/vendor/core/autonomy/envelope.js +27 -0
  75. package/dist/vendor/core/autonomy/escalation-ledger.d.ts +20 -0
  76. package/dist/vendor/core/autonomy/escalation-ledger.js +18 -0
  77. package/dist/vendor/core/autonomy/resume.d.ts +15 -0
  78. package/dist/vendor/core/autonomy/resume.js +23 -0
  79. package/dist/vendor/core/circuit/circuit-breaker.d.ts +60 -0
  80. package/dist/vendor/core/circuit/circuit-breaker.js +143 -0
  81. package/dist/vendor/core/context-distillation.d.ts +3 -0
  82. package/dist/vendor/core/context-distillation.js +44 -0
  83. package/dist/vendor/core/context-flow/compile-context.d.ts +8 -0
  84. package/dist/vendor/core/context-flow/compile-context.js +111 -0
  85. package/dist/vendor/core/context-flow/entities.d.ts +2 -0
  86. package/dist/vendor/core/context-flow/entities.js +44 -0
  87. package/dist/vendor/core/context-flow/evaluate-policy.d.ts +2 -0
  88. package/dist/vendor/core/context-flow/evaluate-policy.js +42 -0
  89. package/dist/vendor/core/context-flow/index.d.ts +11 -0
  90. package/dist/vendor/core/context-flow/index.js +24 -0
  91. package/dist/vendor/core/context-flow/labels.d.ts +3 -0
  92. package/dist/vendor/core/context-flow/labels.js +17 -0
  93. package/dist/vendor/core/context-flow/normalizer.d.ts +9 -0
  94. package/dist/vendor/core/context-flow/normalizer.js +69 -0
  95. package/dist/vendor/core/context-flow/profiles.d.ts +33 -0
  96. package/dist/vendor/core/context-flow/profiles.js +36 -0
  97. package/dist/vendor/core/context-flow/redaction.d.ts +1 -0
  98. package/dist/vendor/core/context-flow/redaction.js +6 -0
  99. package/dist/vendor/core/context-flow/sensitivity.d.ts +2 -0
  100. package/dist/vendor/core/context-flow/sensitivity.js +27 -0
  101. package/dist/vendor/core/context-flow/sync-preview.d.ts +2 -0
  102. package/dist/vendor/core/context-flow/sync-preview.js +22 -0
  103. package/dist/vendor/core/context-flow/token-estimator.d.ts +3 -0
  104. package/dist/vendor/core/context-flow/token-estimator.js +13 -0
  105. package/dist/vendor/core/context-flow/types.d.ts +91 -0
  106. package/dist/vendor/core/context-flow/types.js +2 -0
  107. package/dist/vendor/core/context-utility.d.ts +47 -0
  108. package/dist/vendor/core/context-utility.js +405 -0
  109. package/dist/vendor/core/cost/pipeline.d.ts +92 -0
  110. package/dist/vendor/core/cost/pipeline.js +141 -0
  111. package/dist/vendor/core/cost/tagged-cost.d.ts +27 -0
  112. package/dist/vendor/core/cost/tagged-cost.js +55 -0
  113. package/dist/vendor/core/cost-governor.d.ts +2 -0
  114. package/dist/vendor/core/cost-governor.js +50 -0
  115. package/dist/vendor/core/cve/cve-check.d.ts +80 -0
  116. package/dist/vendor/core/cve/cve-check.js +172 -0
  117. package/dist/vendor/core/digital-twin/index.d.ts +27 -0
  118. package/dist/vendor/core/digital-twin/index.js +90 -0
  119. package/dist/vendor/core/drift/drift-graph.d.ts +47 -0
  120. package/dist/vendor/core/drift/drift-graph.js +100 -0
  121. package/dist/vendor/core/drift/objective-lock.d.ts +69 -0
  122. package/dist/vendor/core/drift/objective-lock.js +88 -0
  123. package/dist/vendor/core/drift/scope.d.ts +46 -0
  124. package/dist/vendor/core/drift/scope.js +102 -0
  125. package/dist/vendor/core/drift/signature-lock.d.ts +48 -0
  126. package/dist/vendor/core/drift/signature-lock.js +202 -0
  127. package/dist/vendor/core/drift/stale-proof-gate.d.ts +21 -0
  128. package/dist/vendor/core/drift/stale-proof-gate.js +19 -0
  129. package/dist/vendor/core/eval/known-bad-world-runner.d.ts +24 -0
  130. package/dist/vendor/core/eval/known-bad-world-runner.js +256 -0
  131. package/dist/vendor/core/evidence/claim-audit.d.ts +18 -0
  132. package/dist/vendor/core/evidence/claim-audit.js +89 -0
  133. package/dist/vendor/core/exit-intelligence.d.ts +2 -0
  134. package/dist/vendor/core/exit-intelligence.js +58 -0
  135. package/dist/vendor/core/explain/formatter.d.ts +42 -0
  136. package/dist/vendor/core/explain/formatter.js +171 -0
  137. package/dist/vendor/core/explain/timeline.d.ts +29 -0
  138. package/dist/vendor/core/explain/timeline.js +213 -0
  139. package/dist/vendor/core/failure-taxonomy.d.ts +2 -0
  140. package/dist/vendor/core/failure-taxonomy.js +76 -0
  141. package/dist/vendor/core/gateway/index.d.ts +10 -0
  142. package/dist/vendor/core/gateway/index.js +12 -0
  143. package/dist/vendor/core/gateway/registry.d.ts +40 -0
  144. package/dist/vendor/core/gateway/registry.js +97 -0
  145. package/dist/vendor/core/gateway/transport.d.ts +31 -0
  146. package/dist/vendor/core/gateway/transport.js +82 -0
  147. package/dist/vendor/core/gateway/vault.d.ts +19 -0
  148. package/dist/vendor/core/gateway/vault.js +29 -0
  149. package/dist/vendor/core/graph/adapters.d.ts +43 -0
  150. package/dist/vendor/core/graph/adapters.js +91 -0
  151. package/dist/vendor/core/graph/hotspots.d.ts +22 -0
  152. package/dist/vendor/core/graph/hotspots.js +30 -0
  153. package/dist/vendor/core/graph/index.d.ts +1 -0
  154. package/dist/vendor/core/graph/index.js +2 -0
  155. package/dist/vendor/core/honey/honey-tokens.d.ts +32 -0
  156. package/dist/vendor/core/honey/honey-tokens.js +44 -0
  157. package/dist/vendor/core/index.d.ts +2 -2
  158. package/dist/vendor/core/index.js +38 -12
  159. package/dist/vendor/core/learning/bayesian-update.d.ts +31 -0
  160. package/dist/vendor/core/learning/bayesian-update.js +60 -0
  161. package/dist/vendor/core/learning/prior-sets.d.ts +42 -0
  162. package/dist/vendor/core/learning/prior-sets.js +111 -0
  163. package/dist/vendor/core/learning/promotion-gate.d.ts +17 -0
  164. package/dist/vendor/core/learning/promotion-gate.js +23 -0
  165. package/dist/vendor/core/leash/blast-radius.d.ts +42 -0
  166. package/dist/vendor/core/leash/blast-radius.js +156 -0
  167. package/dist/vendor/core/leash/policy-leash.d.ts +31 -0
  168. package/dist/vendor/core/leash/policy-leash.js +117 -0
  169. package/dist/vendor/core/memo/memo.d.ts +63 -0
  170. package/dist/vendor/core/memo/memo.js +97 -0
  171. package/dist/vendor/core/memory/learning-pipeline.d.ts +154 -0
  172. package/dist/vendor/core/memory/learning-pipeline.js +391 -0
  173. package/dist/vendor/core/memory/palace.d.ts +84 -0
  174. package/dist/vendor/core/memory/palace.js +379 -0
  175. package/dist/vendor/core/merge/ast-merge.d.ts +22 -0
  176. package/dist/vendor/core/merge/ast-merge.js +350 -0
  177. package/dist/vendor/core/merge/text-merge.d.ts +12 -0
  178. package/dist/vendor/core/merge/text-merge.js +182 -0
  179. package/dist/vendor/core/otel/tracer.d.ts +45 -0
  180. package/dist/vendor/core/otel/tracer.js +116 -0
  181. package/dist/vendor/core/parallel/parallel-attempts.d.ts +28 -0
  182. package/dist/vendor/core/parallel/parallel-attempts.js +41 -0
  183. package/dist/vendor/core/parallel/scorer.d.ts +24 -0
  184. package/dist/vendor/core/parallel/scorer.js +65 -0
  185. package/dist/vendor/core/pattern-detection.d.ts +64 -0
  186. package/dist/vendor/core/pattern-detection.js +108 -0
  187. package/dist/vendor/core/persistence/checkpoint.d.ts +44 -0
  188. package/dist/vendor/core/persistence/checkpoint.js +156 -0
  189. package/dist/vendor/core/persistence/cleanup.d.ts +22 -0
  190. package/dist/vendor/core/persistence/cleanup.js +131 -0
  191. package/dist/vendor/core/persistence/index.d.ts +2 -0
  192. package/dist/vendor/core/persistence/index.js +1 -0
  193. package/dist/vendor/core/persistence/runs-reader.d.ts +52 -0
  194. package/dist/vendor/core/persistence/runs-reader.js +84 -0
  195. package/dist/vendor/core/persistence/store.d.ts +6 -1
  196. package/dist/vendor/core/persistence/store.js +5 -0
  197. package/dist/vendor/core/policy/file-touch-quota.d.ts +60 -0
  198. package/dist/vendor/core/policy/file-touch-quota.js +105 -0
  199. package/dist/vendor/core/policy/policy-loader.d.ts +30 -0
  200. package/dist/vendor/core/policy/policy-loader.js +170 -0
  201. package/dist/vendor/core/policy/policy-schema.d.ts +55 -0
  202. package/dist/vendor/core/policy/policy-schema.js +78 -0
  203. package/dist/vendor/core/probe/probe.d.ts +49 -0
  204. package/dist/vendor/core/probe/probe.js +115 -0
  205. package/dist/vendor/core/proof/patch-proof.d.ts +58 -0
  206. package/dist/vendor/core/proof/patch-proof.js +84 -0
  207. package/dist/vendor/core/proof/semantic-probe.d.ts +25 -0
  208. package/dist/vendor/core/proof/semantic-probe.js +82 -0
  209. package/dist/vendor/core/recovery/failure-mode-runner.d.ts +29 -0
  210. package/dist/vendor/core/recovery/failure-mode-runner.js +39 -0
  211. package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
  212. package/dist/vendor/core/red-blue/red-phase.js +141 -0
  213. package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
  214. package/dist/vendor/core/red-blue/risk-tiers.js +33 -0
  215. package/dist/vendor/core/replay/replay.d.ts +85 -0
  216. package/dist/vendor/core/replay/replay.js +109 -0
  217. package/dist/vendor/core/router/engine.d.ts +54 -0
  218. package/dist/vendor/core/router/engine.js +131 -0
  219. package/dist/vendor/core/router/index.d.ts +1 -0
  220. package/dist/vendor/core/router/index.js +2 -0
  221. package/dist/vendor/core/router/trust-calibration.d.ts +57 -0
  222. package/dist/vendor/core/router/trust-calibration.js +127 -0
  223. package/dist/vendor/core/run-martin.d.ts +2 -0
  224. package/dist/vendor/core/run-martin.js +287 -0
  225. package/dist/vendor/core/security/cve-scanner.d.ts +62 -0
  226. package/dist/vendor/core/security/cve-scanner.js +178 -0
  227. package/dist/vendor/core/sentinel/efficiency-sentinel.d.ts +29 -0
  228. package/dist/vendor/core/sentinel/efficiency-sentinel.js +30 -0
  229. package/dist/vendor/core/sentinel/progress-guard.d.ts +35 -0
  230. package/dist/vendor/core/sentinel/progress-guard.js +46 -0
  231. package/dist/vendor/core/siem/siem-emitter.d.ts +49 -0
  232. package/dist/vendor/core/siem/siem-emitter.js +157 -0
  233. package/dist/vendor/core/strategy/attempt-brief.d.ts +22 -0
  234. package/dist/vendor/core/strategy/attempt-brief.js +89 -0
  235. package/dist/vendor/core/summarize/diff-summary.d.ts +35 -0
  236. package/dist/vendor/core/summarize/diff-summary.js +204 -0
  237. package/dist/vendor/core/surface-signals.d.ts +21 -0
  238. package/dist/vendor/core/surface-signals.js +139 -0
  239. package/dist/vendor/core/truth/truth-wall.d.ts +51 -0
  240. package/dist/vendor/core/truth/truth-wall.js +69 -0
  241. package/dist/vendor/core/truth-spine.d.ts +26 -0
  242. package/dist/vendor/core/truth-spine.js +62 -0
  243. package/dist/vendor/core/types.d.ts +115 -0
  244. package/dist/vendor/core/types.js +2 -0
  245. package/dist/vendor/core/verification/tiered-verify.d.ts +17 -0
  246. package/dist/vendor/core/verification/tiered-verify.js +29 -0
  247. package/dist/vendor/core/verifier-pyramid.d.ts +32 -0
  248. package/dist/vendor/core/verifier-pyramid.js +111 -0
  249. package/dist/vendor/core/workflow-artifacts.d.ts +99 -0
  250. package/dist/vendor/core/workflow-artifacts.js +668 -0
  251. package/dist/vendor/core/wrap/supervised-run.d.ts +96 -0
  252. package/dist/vendor/core/wrap/supervised-run.js +178 -0
  253. package/docs/assets/cli-animated.svg +139 -0
  254. package/docs/assets/cli-static.svg +34 -0
  255. package/docs/assets/github-hero-v2.svg +23 -0
  256. package/docs/assets/martin-raplph.png.jpg +0 -0
  257. package/docs/assets/martinloop-logo.png +0 -0
  258. package/docs/assets/nvidia-inception-program-light.png +0 -0
  259. package/docs/assets/nvidia-inception-program.png +0 -0
  260. package/docs/assets/phase3c-sidesidebyside-demo.html +228 -0
  261. package/docs/assets/side-by-side.svg +134 -0
  262. package/docs/oss/CLAUDE-CODE-WALKTHROUGH.md +142 -142
  263. package/docs/oss/EXAMPLES.md +134 -134
  264. package/docs/oss/OSS-BOUNDARY-REPORT.json +1 -1
  265. package/docs/oss/OSS-BOUNDARY-REPORT.md +1 -1
  266. package/docs/oss/QUICKSTART.md +170 -165
  267. package/docs/oss/RALPH-LOOP-SAFETY.md +113 -113
  268. package/docs/oss/README.md +96 -96
  269. package/docs/oss/RELEASE-SURFACE-REPORT.json +2 -1
  270. package/docs/oss/RELEASE-SURFACE-REPORT.md +2 -1
  271. package/package.json +130 -58
  272. package/docs/distribution/DIRECTORY-SUBMISSIONS.md +0 -89
  273. package/docs/distribution/INTEGRATION-OUTREACH.md +0 -61
  274. package/docs/distribution/UNDER-3-CHALLENGE.md +0 -65
@@ -0,0 +1,30 @@
1
+ // ─── Types ────────────────────────────────────────────────────────────────────
2
+ // ─── Detection ────────────────────────────────────────────────────────────────
3
+ /**
4
+ * T14 — Efficiency Anomaly Detection
5
+ *
6
+ * Detection logic:
7
+ * - IF baseline.samples < 20 → DO NOT FIRE (cold-start guard)
8
+ * - IF actualCost < (baseline.p25 * 0.75) → fire T14 anomaly
9
+ * - T14 is ALWAYS warn-only (action: "logged") — never hard-rejects a run
10
+ */
11
+ export function checkEfficiencyAnomaly(input) {
12
+ // Cold-start guard: never fire before 20 samples
13
+ if (input.baseline.samples < 20) {
14
+ return null;
15
+ }
16
+ const threshold = input.baseline.p25CostUsd * 0.75;
17
+ if (input.actualCostUsd >= threshold) {
18
+ return null;
19
+ }
20
+ const deviationPct = ((input.baseline.p25CostUsd - input.actualCostUsd) / input.baseline.p25CostUsd) * 100;
21
+ return {
22
+ trapId: "T14",
23
+ runId: input.runId,
24
+ actualCostUsd: input.actualCostUsd,
25
+ p25BaselineCostUsd: input.baseline.p25CostUsd,
26
+ deviationPct,
27
+ action: "logged"
28
+ };
29
+ }
30
+ //# sourceMappingURL=efficiency-sentinel.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Stagnation detection — monotonic progress guard.
3
+ *
4
+ * Detects when an agent loop is spinning without making forward progress:
5
+ * the last N attempts all produced the same failure classification and
6
+ * the same intervention response. When stagnation is detected the caller
7
+ * should exit the loop with lifecycleState "stagnation_detected".
8
+ *
9
+ * This prevents the "$500 infinite loop" scenario where Claude Code
10
+ * enters a retry cycle for hours without human awareness.
11
+ */
12
+ import type { LoopAttempt } from "../../contracts/index.js";
13
+ export interface StagnationInput {
14
+ readonly attempts: LoopAttempt[];
15
+ /** Number of consecutive attempts to inspect. Defaults to 3. */
16
+ readonly windowSize?: number;
17
+ }
18
+ export interface StagnationResult {
19
+ readonly stagnant: boolean;
20
+ readonly reason?: string;
21
+ readonly windowSize: number;
22
+ }
23
+ /**
24
+ * Return `stagnant: true` when the last `windowSize` attempts all share:
25
+ * - the same `adapterId` (same adapter/model — not a recovery escalation)
26
+ * - the same `failureClass` value (including both-undefined)
27
+ * - the same `intervention` value (including both-undefined)
28
+ *
29
+ * Requiring the same adapterId prevents false positives when the runtime
30
+ * is legitimately cycling through different recovery paths (which leads to
31
+ * `diminishing_returns`, not stagnation).
32
+ *
33
+ * When fewer attempts than `windowSize` exist, always returns `stagnant: false`.
34
+ */
35
+ export declare function detectStagnation(input: StagnationInput): StagnationResult;
@@ -0,0 +1,46 @@
1
+ // ---------------------------------------------------------------------------
2
+ // detectStagnation
3
+ // ---------------------------------------------------------------------------
4
+ /**
5
+ * Return `stagnant: true` when the last `windowSize` attempts all share:
6
+ * - the same `adapterId` (same adapter/model — not a recovery escalation)
7
+ * - the same `failureClass` value (including both-undefined)
8
+ * - the same `intervention` value (including both-undefined)
9
+ *
10
+ * Requiring the same adapterId prevents false positives when the runtime
11
+ * is legitimately cycling through different recovery paths (which leads to
12
+ * `diminishing_returns`, not stagnation).
13
+ *
14
+ * When fewer attempts than `windowSize` exist, always returns `stagnant: false`.
15
+ */
16
+ export function detectStagnation(input) {
17
+ const windowSize = input.windowSize ?? 3;
18
+ if (input.attempts.length < windowSize) {
19
+ return { stagnant: false, windowSize };
20
+ }
21
+ // Take the last `windowSize` attempts
22
+ const window = input.attempts.slice(-windowSize);
23
+ const first = window[0];
24
+ const referenceAdapter = first.adapterId;
25
+ const referenceClass = first.failureClass;
26
+ const referenceIntervention = first.intervention;
27
+ for (const attempt of window) {
28
+ // Different adapter = legitimate recovery escalation, not stagnation
29
+ if (attempt.adapterId !== referenceAdapter) {
30
+ return { stagnant: false, windowSize };
31
+ }
32
+ if (attempt.failureClass !== referenceClass) {
33
+ return { stagnant: false, windowSize };
34
+ }
35
+ if (attempt.intervention !== referenceIntervention) {
36
+ return { stagnant: false, windowSize };
37
+ }
38
+ }
39
+ const classLabel = referenceClass ?? "(none)";
40
+ const interventionLabel = referenceIntervention ?? "(none)";
41
+ const reason = `Stagnation detected: last ${windowSize} attempts on adapter "${referenceAdapter}" all produced ` +
42
+ `failureClass="${classLabel}" with intervention="${interventionLabel}". ` +
43
+ `Loop is not making forward progress.`;
44
+ return { stagnant: true, reason, windowSize };
45
+ }
46
+ //# sourceMappingURL=progress-guard.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * siem-emitter.ts — SLICE-16
3
+ *
4
+ * SIEM integration: emits Martin Loop gate events in OCSF or CEF format.
5
+ *
6
+ * Events are batched (up to 50 events or 5s) then POSTed to the configured
7
+ * endpoint. On transport failure the events are written to
8
+ * ~/.martin/siem-queue/ for later retry via `martin siem drain`.
9
+ *
10
+ * Configuration (policy file or env):
11
+ * siem.endpoint — HTTP POST target (required to enable)
12
+ * siem.format — "ocsf" | "cef" (default: "ocsf")
13
+ * siem.apiKey — Bearer token for endpoint auth (optional)
14
+ */
15
+ export type SiemEventType = "attempt_started" | "attempt_completed" | "leash_blocked" | "policy_violated" | "budget_exceeded" | "patch_accepted" | "patch_rejected" | "rollback_triggered";
16
+ export type SiemFormat = "ocsf" | "cef";
17
+ export interface SiemEventInput {
18
+ type: SiemEventType;
19
+ loopId: string;
20
+ attemptId?: string;
21
+ message: string;
22
+ severity?: "info" | "low" | "medium" | "high" | "critical";
23
+ metadata?: Record<string, string | number | boolean>;
24
+ }
25
+ export declare function formatOcsf(event: SiemEventInput, timestamp: number): object;
26
+ export declare function formatCef(event: SiemEventInput, timestamp: number): string;
27
+ export interface SiemConfig {
28
+ endpoint: string;
29
+ format: SiemFormat;
30
+ apiKey?: string;
31
+ flushIntervalMs?: number;
32
+ maxBatchSize?: number;
33
+ }
34
+ export declare class SiemEmitter {
35
+ private readonly config;
36
+ private queue;
37
+ private flushTimer;
38
+ private fetchFn;
39
+ constructor(config: SiemConfig, fetchFn?: typeof fetch);
40
+ emit(event: SiemEventInput): void;
41
+ flush(): Promise<{
42
+ sent: number;
43
+ queued: number;
44
+ }>;
45
+ /** Returns all pending (unflushed) events. Used in tests. */
46
+ pendingCount(): number;
47
+ destroy(): void;
48
+ }
49
+ export declare function createSiemEmitter(config: Partial<SiemConfig> | undefined, fetchFn?: typeof fetch): SiemEmitter | null;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * siem-emitter.ts — SLICE-16
3
+ *
4
+ * SIEM integration: emits Martin Loop gate events in OCSF or CEF format.
5
+ *
6
+ * Events are batched (up to 50 events or 5s) then POSTed to the configured
7
+ * endpoint. On transport failure the events are written to
8
+ * ~/.martin/siem-queue/ for later retry via `martin siem drain`.
9
+ *
10
+ * Configuration (policy file or env):
11
+ * siem.endpoint — HTTP POST target (required to enable)
12
+ * siem.format — "ocsf" | "cef" (default: "ocsf")
13
+ * siem.apiKey — Bearer token for endpoint auth (optional)
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ // ---------------------------------------------------------------------------
17
+ // OCSF formatter
18
+ // ---------------------------------------------------------------------------
19
+ const OCSF_SEVERITY_MAP = {
20
+ info: 1, low: 2, medium: 4, high: 6, critical: 8
21
+ };
22
+ const OCSF_ACTIVITY_MAP = {
23
+ attempt_started: 1,
24
+ attempt_completed: 2,
25
+ leash_blocked: 3,
26
+ policy_violated: 4,
27
+ budget_exceeded: 5,
28
+ patch_accepted: 6,
29
+ patch_rejected: 7,
30
+ rollback_triggered: 8
31
+ };
32
+ export function formatOcsf(event, timestamp) {
33
+ return {
34
+ class_uid: 300101,
35
+ category_uid: 3,
36
+ type_uid: 300101,
37
+ time: timestamp,
38
+ severity_id: OCSF_SEVERITY_MAP[event.severity ?? "info"] ?? 1,
39
+ actor: { user: { name: "martin-loop" } },
40
+ metadata: {
41
+ version: "1.0.0",
42
+ product: { name: "Martin Loop" },
43
+ uid: createHash("sha256")
44
+ .update(`${event.loopId}:${event.attemptId ?? ""}:${timestamp}`)
45
+ .digest("hex")
46
+ .slice(0, 16)
47
+ },
48
+ activity_id: OCSF_ACTIVITY_MAP[event.type] ?? 0,
49
+ message: event.message,
50
+ unmapped: {
51
+ event_type: event.type,
52
+ loop_id: event.loopId,
53
+ attempt_id: event.attemptId ?? "",
54
+ ...(event.metadata ?? {})
55
+ }
56
+ };
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // CEF formatter
60
+ // ---------------------------------------------------------------------------
61
+ const CEF_SEVERITY_MAP = {
62
+ info: 0, low: 2, medium: 5, high: 7, critical: 10
63
+ };
64
+ function cefEscape(val) {
65
+ return val.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/=/g, "\\=");
66
+ }
67
+ export function formatCef(event, timestamp) {
68
+ const sev = CEF_SEVERITY_MAP[event.severity ?? "info"] ?? 0;
69
+ const ext = [
70
+ `rt=${timestamp}`,
71
+ `loopId=${cefEscape(event.loopId)}`,
72
+ `attemptId=${cefEscape(event.attemptId ?? "")}`,
73
+ `msg=${cefEscape(event.message)}`
74
+ ];
75
+ if (event.metadata) {
76
+ for (const [k, v] of Object.entries(event.metadata)) {
77
+ ext.push(`${cefEscape(String(k))}=${cefEscape(String(v))}`);
78
+ }
79
+ }
80
+ return `CEF:0|MartinLoop|martin-loop|1.0|${event.type}|${event.message}|${sev}| ${ext.join(" ")}`;
81
+ }
82
+ export class SiemEmitter {
83
+ config;
84
+ queue = [];
85
+ flushTimer = null;
86
+ fetchFn;
87
+ constructor(config, fetchFn) {
88
+ this.config = {
89
+ endpoint: config.endpoint,
90
+ format: config.format,
91
+ apiKey: config.apiKey,
92
+ flushIntervalMs: config.flushIntervalMs ?? 5000,
93
+ maxBatchSize: config.maxBatchSize ?? 50
94
+ };
95
+ this.fetchFn = fetchFn ?? fetch;
96
+ }
97
+ emit(event) {
98
+ this.queue.push({ event, timestamp: Date.now() });
99
+ if (this.queue.length >= this.config.maxBatchSize) {
100
+ void this.flush();
101
+ return;
102
+ }
103
+ if (this.config.flushIntervalMs > 0 && !this.flushTimer) {
104
+ this.flushTimer = setTimeout(() => {
105
+ void this.flush();
106
+ }, this.config.flushIntervalMs);
107
+ }
108
+ }
109
+ async flush() {
110
+ if (this.flushTimer) {
111
+ clearTimeout(this.flushTimer);
112
+ this.flushTimer = null;
113
+ }
114
+ const batch = this.queue.splice(0, this.config.maxBatchSize);
115
+ if (batch.length === 0)
116
+ return { sent: 0, queued: 0 };
117
+ const formatted = batch.map(({ event, timestamp }) => this.config.format === "cef"
118
+ ? formatCef(event, timestamp)
119
+ : formatOcsf(event, timestamp));
120
+ const headers = { "Content-Type": "application/json" };
121
+ if (this.config.apiKey)
122
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
123
+ try {
124
+ const res = await this.fetchFn(this.config.endpoint, {
125
+ method: "POST",
126
+ headers,
127
+ body: JSON.stringify({ events: formatted }),
128
+ signal: AbortSignal.timeout(10_000)
129
+ });
130
+ if (!res.ok)
131
+ throw new Error(`SIEM endpoint returned HTTP ${res.status}`);
132
+ return { sent: batch.length, queued: 0 };
133
+ }
134
+ catch {
135
+ // On failure: keep events in front of queue for retry
136
+ this.queue.unshift(...batch);
137
+ return { sent: 0, queued: this.queue.length };
138
+ }
139
+ }
140
+ /** Returns all pending (unflushed) events. Used in tests. */
141
+ pendingCount() {
142
+ return this.queue.length;
143
+ }
144
+ destroy() {
145
+ if (this.flushTimer)
146
+ clearTimeout(this.flushTimer);
147
+ }
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // Factory — returns null when siem.endpoint is not configured
151
+ // ---------------------------------------------------------------------------
152
+ export function createSiemEmitter(config, fetchFn) {
153
+ if (!config?.endpoint)
154
+ return null;
155
+ return new SiemEmitter({ endpoint: config.endpoint, format: config.format ?? "ocsf", ...config }, fetchFn);
156
+ }
157
+ //# sourceMappingURL=siem-emitter.js.map
@@ -0,0 +1,22 @@
1
+ import type { LoopAttempt, LoopTask } from "../../contracts/index.js";
2
+ export interface AttemptFocusInput {
3
+ task: Pick<LoopTask, "objective" | "acceptanceCriteria">;
4
+ attempts: LoopAttempt[];
5
+ remainingBudgetUsd: number;
6
+ }
7
+ export interface RetryEconomicsInput {
8
+ previousAttempts: LoopAttempt[];
9
+ projectedUsd: number;
10
+ remainingBudgetUsd: number;
11
+ remainingIterations: number;
12
+ novelRecoveryPath?: boolean;
13
+ }
14
+ export interface RetryEconomicsDecision {
15
+ allowRetry: boolean;
16
+ reason?: string;
17
+ repeatedVerifierSignature?: string;
18
+ }
19
+ export declare function buildAttemptFocus(input: AttemptFocusInput): string;
20
+ export declare function evaluateRetryEconomics(input: RetryEconomicsInput): RetryEconomicsDecision;
21
+ export declare function detectRepeatedVerifierSignature(attempts: Pick<LoopAttempt, "verifierSummary">[], window?: number): string | undefined;
22
+ export declare function normalizeVerifierSignature(summary?: string): string | undefined;
@@ -0,0 +1,89 @@
1
+ export function buildAttemptFocus(input) {
2
+ const parts = [`Primary objective: ${input.task.objective}.`];
3
+ const acceptanceCriteria = input.task.acceptanceCriteria ?? [];
4
+ const lastAttempt = input.attempts.at(-1);
5
+ const repeatedSignature = detectRepeatedVerifierSignature(input.attempts);
6
+ if (acceptanceCriteria.length > 0) {
7
+ parts.push(`Acceptance criteria: ${acceptanceCriteria.join("; ")}.`);
8
+ }
9
+ if (lastAttempt?.failureClass) {
10
+ parts.push(`Last failure class: ${lastAttempt.failureClass}. Resolve that exact gap before widening scope.`);
11
+ }
12
+ if (lastAttempt?.summary) {
13
+ parts.push(`Do not repeat this failed pattern: ${truncateSentence(lastAttempt.summary, 160)}.`);
14
+ }
15
+ if (repeatedSignature) {
16
+ parts.push(`Repeated verifier signal: ${repeatedSignature}. Target the smallest change that clears this exact failure.`);
17
+ }
18
+ else if (lastAttempt?.verifierSummary) {
19
+ parts.push(`Verifier focus: ${normalizeVerifierSignature(lastAttempt.verifierSummary)}.`);
20
+ }
21
+ if (input.remainingBudgetUsd <= 2) {
22
+ parts.push("Budget is tight. Prefer one narrow, verifier-led fix over a broad rewrite.");
23
+ }
24
+ return parts.join(" ");
25
+ }
26
+ export function evaluateRetryEconomics(input) {
27
+ if (input.novelRecoveryPath) {
28
+ return { allowRetry: true };
29
+ }
30
+ const lastTwo = input.previousAttempts.slice(-2);
31
+ const repeatedFailureClass = lastTwo.length === 2 &&
32
+ lastTwo[0]?.failureClass &&
33
+ lastTwo[0].failureClass === lastTwo[1]?.failureClass;
34
+ const repeatedVerifierSignature = detectRepeatedVerifierSignature(input.previousAttempts);
35
+ const remainingAfterRetry = roundUsd(input.remainingBudgetUsd - input.projectedUsd);
36
+ const tightBudget = remainingAfterRetry < Math.max(input.projectedUsd * 0.5, 0.25);
37
+ if (repeatedFailureClass && repeatedVerifierSignature && tightBudget && input.remainingIterations <= 2) {
38
+ return {
39
+ allowRetry: false,
40
+ reason: `Repeated verifier signature "${repeatedVerifierSignature}" under tight budget. ` +
41
+ "Escalate instead of paying for another near-identical retry.",
42
+ repeatedVerifierSignature
43
+ };
44
+ }
45
+ return {
46
+ allowRetry: true,
47
+ ...(repeatedVerifierSignature ? { repeatedVerifierSignature } : {})
48
+ };
49
+ }
50
+ export function detectRepeatedVerifierSignature(attempts, window = 2) {
51
+ const signatures = attempts
52
+ .map((attempt) => normalizeVerifierSignature(attempt.verifierSummary))
53
+ .filter((signature) => Boolean(signature));
54
+ if (signatures.length < window) {
55
+ return undefined;
56
+ }
57
+ const recent = signatures.slice(-window);
58
+ const first = recent[0];
59
+ if (!first) {
60
+ return undefined;
61
+ }
62
+ return recent.every((signature) => signature === first) ? first : undefined;
63
+ }
64
+ export function normalizeVerifierSignature(summary) {
65
+ if (!summary) {
66
+ return undefined;
67
+ }
68
+ const normalized = summary
69
+ .toLowerCase()
70
+ .replace(/\s+/g, " ")
71
+ .replace(/\b\d+\b/g, "#")
72
+ .replace(/[^a-z0-9# :._-]/g, "")
73
+ .trim();
74
+ if (!normalized) {
75
+ return undefined;
76
+ }
77
+ const firstClause = normalized.split(/(?:;|\.|,|\n)/u)[0]?.trim();
78
+ return firstClause ? truncateSentence(firstClause, 96) : undefined;
79
+ }
80
+ function truncateSentence(value, maxLength) {
81
+ if (value.length <= maxLength) {
82
+ return value;
83
+ }
84
+ return `${value.slice(0, maxLength - 1).trimEnd()}...`;
85
+ }
86
+ function roundUsd(value) {
87
+ return Math.round(value * 100) / 100;
88
+ }
89
+ //# sourceMappingURL=attempt-brief.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * diff-summary.ts
3
+ *
4
+ * Produces a one-line human-readable summary of a unified diff relative to the
5
+ * loop objective. Uses a cheap haiku-tier LLM call when an adapter is available;
6
+ * falls back to a deterministic heuristic when no adapter is configured or the
7
+ * model call fails.
8
+ *
9
+ * Quality gate: the generated one-liner MUST contain at least one concrete
10
+ * identifier (file name, function name, symbol) extracted from the diff itself.
11
+ * If the model output fails that gate the fallback is used instead.
12
+ */
13
+ export interface DiffSummaryInput {
14
+ /** The raw unified diff text (output of git diff or similar). */
15
+ diff: string;
16
+ /** The original loop objective supplied by the user. */
17
+ objective: string;
18
+ }
19
+ export interface DiffSummary {
20
+ /** Single-line summary suitable for a commit message or PR description. */
21
+ oneLiner: string;
22
+ /** How the summary was produced. */
23
+ method: "model" | "heuristic";
24
+ /** Concrete identifier extracted from the diff that validates the summary. */
25
+ anchorIdentifier: string;
26
+ }
27
+ /**
28
+ * Summarize a unified diff in the context of a loop objective.
29
+ *
30
+ * @param input Diff text + objective string.
31
+ * @param callModelFn Optional async function that accepts a prompt string and
32
+ * returns a model response string. When omitted, or when
33
+ * the call throws, the deterministic heuristic is used.
34
+ */
35
+ export declare function summarizeDiff(input: DiffSummaryInput, callModelFn?: (prompt: string) => Promise<string>): Promise<DiffSummary>;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * diff-summary.ts
3
+ *
4
+ * Produces a one-line human-readable summary of a unified diff relative to the
5
+ * loop objective. Uses a cheap haiku-tier LLM call when an adapter is available;
6
+ * falls back to a deterministic heuristic when no adapter is configured or the
7
+ * model call fails.
8
+ *
9
+ * Quality gate: the generated one-liner MUST contain at least one concrete
10
+ * identifier (file name, function name, symbol) extracted from the diff itself.
11
+ * If the model output fails that gate the fallback is used instead.
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // Public entry point
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Summarize a unified diff in the context of a loop objective.
18
+ *
19
+ * @param input Diff text + objective string.
20
+ * @param callModelFn Optional async function that accepts a prompt string and
21
+ * returns a model response string. When omitted, or when
22
+ * the call throws, the deterministic heuristic is used.
23
+ */
24
+ export async function summarizeDiff(input, callModelFn) {
25
+ const anchor = extractAnchorIdentifier(input.diff);
26
+ if (callModelFn && input.diff.trim().length > 0) {
27
+ try {
28
+ const raw = await callModelFn(buildPrompt(input, anchor));
29
+ const cleaned = cleanModelOutput(raw);
30
+ if (isQualityGatePassing(cleaned, anchor)) {
31
+ return { oneLiner: cleaned, method: "model", anchorIdentifier: anchor };
32
+ }
33
+ // Model output failed quality gate — fall through to heuristic
34
+ }
35
+ catch {
36
+ // Non-fatal — fall through to heuristic
37
+ }
38
+ }
39
+ return {
40
+ oneLiner: buildHeuristicSummary(input, anchor),
41
+ method: "heuristic",
42
+ anchorIdentifier: anchor
43
+ };
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Prompt construction
47
+ // ---------------------------------------------------------------------------
48
+ function buildPrompt(input, anchor) {
49
+ // Keep the diff excerpt short — we only need enough for the model to name
50
+ // the primary change. Hard cap at 3000 chars to stay within haiku context.
51
+ const diffExcerpt = input.diff.length > 3000
52
+ ? `${input.diff.slice(0, 2900)}\n... (truncated)`
53
+ : input.diff;
54
+ return [
55
+ "You are a precise technical writer generating a single-line git commit summary.",
56
+ "",
57
+ `Objective: ${input.objective}`,
58
+ "",
59
+ "Diff:",
60
+ "```",
61
+ diffExcerpt,
62
+ "```",
63
+ "",
64
+ `The summary MUST mention "${anchor}" or a closely related identifier.`,
65
+ "Respond with ONLY the one-line summary — no preamble, no punctuation at the end.",
66
+ "Keep it under 72 characters.",
67
+ "Format: <verb in imperative mood> <what changed> in <where>",
68
+ "Example: add retry guard to fetchWithTimeout in http-client.ts"
69
+ ].join("\n");
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Quality gate
73
+ // ---------------------------------------------------------------------------
74
+ /**
75
+ * Returns true when the model output contains the anchor identifier or a
76
+ * plausible stem of it (e.g. "retry" from "retryCount").
77
+ */
78
+ function isQualityGatePassing(summary, anchor) {
79
+ if (summary.trim().length === 0)
80
+ return false;
81
+ if (summary.length > 120)
82
+ return false; // unreasonably long
83
+ const lowerSummary = summary.toLowerCase();
84
+ const lowerAnchor = anchor.toLowerCase();
85
+ // Direct match
86
+ if (lowerSummary.includes(lowerAnchor))
87
+ return true;
88
+ // Stem match: try the first 6+ chars of the anchor
89
+ const stem = lowerAnchor.slice(0, Math.max(6, Math.floor(lowerAnchor.length * 0.6)));
90
+ if (stem.length >= 4 && lowerSummary.includes(stem))
91
+ return true;
92
+ return false;
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Deterministic heuristic
96
+ // ---------------------------------------------------------------------------
97
+ /**
98
+ * Builds a summary without a model call by inspecting the diff structure.
99
+ * Always passes the quality gate because we embed the anchor directly.
100
+ */
101
+ function buildHeuristicSummary(input, anchor) {
102
+ const stats = parseDiffStats(input.diff);
103
+ const verb = selectVerb(stats);
104
+ const filesChanged = stats.files.length;
105
+ if (filesChanged === 0) {
106
+ return `${verb} changes related to ${anchor}`;
107
+ }
108
+ if (filesChanged === 1) {
109
+ const file = stats.files[0] ?? "change";
110
+ const location = stripPath(file);
111
+ return `${verb} ${anchor} in ${location}`;
112
+ }
113
+ const primary = stripPath(stats.files[0] ?? "change");
114
+ return `${verb} ${anchor} across ${filesChanged} files (primary: ${primary})`;
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Anchor identifier extraction
118
+ // ---------------------------------------------------------------------------
119
+ /**
120
+ * Extracts the most informative concrete identifier from a unified diff.
121
+ *
122
+ * Priority order:
123
+ * 1. First function/method name from a hunk header (@@...@@ <name>)
124
+ * 2. First added symbol-looking token from a + line
125
+ * 3. First changed file base name (no extension)
126
+ * 4. Literal "change" as a safe fallback
127
+ */
128
+ function extractAnchorIdentifier(diff) {
129
+ // 1. Hunk header function hint:
130
+ // @@ -l,n +l,n @@ function validateToken(
131
+ const hunkHeaderFn = diff.match(/@@[^@]+@@\s+(?:(?:export|async)\s+)*(?:function\s+)?(\w[\w.]*)\s*\(/u);
132
+ if (hunkHeaderFn?.[1] && hunkHeaderFn[1].length >= 3) {
133
+ return hunkHeaderFn[1];
134
+ }
135
+ // 2. First added function/method-like declaration. This intentionally
136
+ // precedes local const extraction so a method body's first local variable
137
+ // does not become the summary anchor.
138
+ const addedCallable = diff.match(/^\+[^+].*?\b(?:export\s+)?(?:async\s+)?(?:function\s+)?(\w[\w]*)\s*\(/mu);
139
+ if (addedCallable?.[1] && addedCallable[1].length >= 3) {
140
+ return addedCallable[1];
141
+ }
142
+ // 3. First export/const/class/type on an added line
143
+ const addedSymbol = diff.match(/^\+[^+].*?\b(?:export\s+)?(?:const|let|var|class|interface|type)\s+(\w[\w]*)/mu);
144
+ if (addedSymbol?.[1] && addedSymbol[1].length >= 3) {
145
+ return addedSymbol[1];
146
+ }
147
+ // 4. First changed file name from diff --git a/<path> b/<path>
148
+ const fileMatch = diff.match(/^diff --git a\/(.+?) b\//mu);
149
+ if (fileMatch?.[1]) {
150
+ const base = fileMatch[1].split("/").pop() ?? fileMatch[1];
151
+ return base.replace(/\.\w+$/, ""); // strip extension
152
+ }
153
+ // 5. First +++ b/ file name
154
+ const plusFile = diff.match(/^\+\+\+ b\/(.+)$/mu);
155
+ if (plusFile?.[1]) {
156
+ const base = plusFile[1].split("/").pop() ?? plusFile[1];
157
+ return base.replace(/\.\w+$/, "");
158
+ }
159
+ return "change";
160
+ }
161
+ function parseDiffStats(diff) {
162
+ const files = [];
163
+ let addedLines = 0;
164
+ let removedLines = 0;
165
+ for (const line of diff.split("\n")) {
166
+ if (line.startsWith("diff --git ")) {
167
+ const match = line.match(/^diff --git a\/(.+?) b\//u);
168
+ if (match?.[1])
169
+ files.push(match[1]);
170
+ }
171
+ else if (line.startsWith("+") && !line.startsWith("+++")) {
172
+ addedLines += 1;
173
+ }
174
+ else if (line.startsWith("-") && !line.startsWith("---")) {
175
+ removedLines += 1;
176
+ }
177
+ }
178
+ return { files, addedLines, removedLines };
179
+ }
180
+ function selectVerb(stats) {
181
+ if (stats.addedLines > 0 && stats.removedLines === 0)
182
+ return "add";
183
+ if (stats.removedLines > 0 && stats.addedLines === 0)
184
+ return "remove";
185
+ if (stats.addedLines > stats.removedLines * 3)
186
+ return "add";
187
+ if (stats.removedLines > stats.addedLines * 3)
188
+ return "remove";
189
+ return "update";
190
+ }
191
+ function stripPath(filePath) {
192
+ return filePath.split("/").pop() ?? filePath;
193
+ }
194
+ // ---------------------------------------------------------------------------
195
+ // Model output cleanup
196
+ // ---------------------------------------------------------------------------
197
+ function cleanModelOutput(raw) {
198
+ return raw
199
+ .trim()
200
+ .replace(/^["']|["']$/g, "") // strip surrounding quotes
201
+ .replace(/\.+$/, "") // strip trailing dots
202
+ .replace(/\s+/g, " "); // collapse whitespace
203
+ }
204
+ //# sourceMappingURL=diff-summary.js.map