@xhulz/nina 0.32.0 → 0.34.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 (256) hide show
  1. package/README.md +9 -5
  2. package/package.json +1 -1
  3. package/releases/0.33.0/core/GAPS.md +125 -0
  4. package/releases/0.33.0/core/answered.json +29 -0
  5. package/releases/0.33.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  6. package/releases/0.33.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  7. package/releases/0.33.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  8. package/releases/0.33.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  9. package/releases/0.33.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  10. package/releases/0.33.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  11. package/releases/0.33.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  12. package/releases/0.33.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  13. package/releases/0.33.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  14. package/releases/0.33.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  15. package/releases/0.33.0/core/tree/.claude/agents/architect.md +127 -0
  16. package/releases/0.33.0/core/tree/.claude/agents/dba.md +110 -0
  17. package/releases/0.33.0/core/tree/.claude/agents/devops.md +114 -0
  18. package/releases/0.33.0/core/tree/.claude/agents/implementer.md +156 -0
  19. package/releases/0.33.0/core/tree/.claude/agents/integration-tester.md +168 -0
  20. package/releases/0.33.0/core/tree/.claude/agents/planner.md +132 -0
  21. package/releases/0.33.0/core/tree/.claude/agents/qa.md +148 -0
  22. package/releases/0.33.0/core/tree/.claude/agents/reviewer.md +183 -0
  23. package/releases/0.33.0/core/tree/.claude/agents/secops.md +121 -0
  24. package/releases/0.33.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  25. package/releases/0.33.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  26. package/releases/0.33.0/core/tree/.claude/agents-overview.md +64 -0
  27. package/releases/0.33.0/core/tree/.claude/frontend.md +10 -0
  28. package/releases/0.33.0/core/tree/.claude/graph.md +75 -0
  29. package/releases/0.33.0/core/tree/.claude/patterns.md +213 -0
  30. package/releases/0.33.0/core/tree/.claude/pills/README.md +146 -0
  31. package/releases/0.33.0/core/tree/.claude/pipeline.md +101 -0
  32. package/releases/0.33.0/core/tree/.claude/retrieval.md +103 -0
  33. package/releases/0.33.0/core/tree/.claude/router.md +198 -0
  34. package/releases/0.33.0/core/tree/.claude/templates/integration.md +45 -0
  35. package/releases/0.33.0/core/tree/CLAUDE.md +161 -0
  36. package/releases/0.33.0/core/tree/scripts/cost-watch.mjs +18 -0
  37. package/releases/0.33.0/core/tree/scripts/edit-guard.mjs +16 -0
  38. package/releases/0.33.0/core/tree/scripts/harness-check.mjs +71 -0
  39. package/releases/0.33.0/core/tree/scripts/loop-gate.mjs +19 -0
  40. package/releases/0.33.0/core/vocabulary.json +13 -0
  41. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  42. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  43. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  44. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  45. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  46. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  47. package/releases/0.33.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  48. package/releases/0.33.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  49. package/releases/0.33.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  50. package/releases/0.33.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  51. package/releases/0.33.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  52. package/releases/0.33.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  53. package/releases/0.33.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.33.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  55. package/releases/0.33.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  56. package/releases/0.33.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  57. package/releases/0.33.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  58. package/releases/0.33.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  59. package/releases/0.33.0/surfaces/db/tree/.claude/graph.md +8 -0
  60. package/releases/0.33.0/surfaces/db/tree/.claude/patterns.md +19 -0
  61. package/releases/0.33.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  62. package/releases/0.33.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  63. package/releases/0.33.0/surfaces/db/tree/.claude/router.md +4 -0
  64. package/releases/0.33.0/surfaces/db/tree/CLAUDE.md +17 -0
  65. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  66. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  67. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  68. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  69. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  70. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  71. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  72. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  73. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  74. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  75. package/releases/0.33.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  76. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  77. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  78. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  79. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  80. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  81. package/releases/0.33.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  82. package/releases/0.33.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  83. package/releases/0.33.0/surfaces/frontend/tree/.claude/router.md +14 -0
  84. package/releases/0.33.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  85. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  86. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  87. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  88. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  89. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  90. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  91. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  92. package/releases/0.33.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  93. package/releases/0.33.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  94. package/releases/0.33.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  95. package/releases/0.33.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  96. package/releases/0.33.0/surfaces/integrations/tree/.claude/router.md +6 -0
  97. package/releases/0.33.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  98. package/releases/0.33.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  99. package/releases/0.33.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  100. package/releases/0.33.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  101. package/releases/0.33.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  102. package/releases/0.33.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  103. package/releases/0.33.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  104. package/releases/0.33.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  105. package/releases/0.33.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  106. package/releases/0.33.0/surfaces/money/tree/.claude/graph.md +2 -0
  107. package/releases/0.33.0/surfaces/money/tree/.claude/patterns.md +49 -0
  108. package/releases/0.33.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  109. package/releases/0.33.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  110. package/releases/0.33.0/surfaces/money/tree/CLAUDE.md +11 -0
  111. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  112. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  113. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  114. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  115. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  116. package/releases/0.33.0/surfaces/pii/tree/CLAUDE.md +5 -0
  117. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  118. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  119. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  120. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  121. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  122. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  123. package/releases/0.33.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  124. package/releases/0.33.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  125. package/releases/0.34.0/core/GAPS.md +125 -0
  126. package/releases/0.34.0/core/answered.json +29 -0
  127. package/releases/0.34.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  128. package/releases/0.34.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  129. package/releases/0.34.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  130. package/releases/0.34.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  131. package/releases/0.34.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  132. package/releases/0.34.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  133. package/releases/0.34.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  134. package/releases/0.34.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  135. package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  136. package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  137. package/releases/0.34.0/core/tree/.claude/agents/architect.md +127 -0
  138. package/releases/0.34.0/core/tree/.claude/agents/dba.md +110 -0
  139. package/releases/0.34.0/core/tree/.claude/agents/devops.md +114 -0
  140. package/releases/0.34.0/core/tree/.claude/agents/implementer.md +156 -0
  141. package/releases/0.34.0/core/tree/.claude/agents/integration-tester.md +168 -0
  142. package/releases/0.34.0/core/tree/.claude/agents/planner.md +132 -0
  143. package/releases/0.34.0/core/tree/.claude/agents/qa.md +148 -0
  144. package/releases/0.34.0/core/tree/.claude/agents/reviewer.md +183 -0
  145. package/releases/0.34.0/core/tree/.claude/agents/secops.md +121 -0
  146. package/releases/0.34.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  147. package/releases/0.34.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  148. package/releases/0.34.0/core/tree/.claude/agents-overview.md +64 -0
  149. package/releases/0.34.0/core/tree/.claude/frontend.md +10 -0
  150. package/releases/0.34.0/core/tree/.claude/graph.md +75 -0
  151. package/releases/0.34.0/core/tree/.claude/patterns.md +213 -0
  152. package/releases/0.34.0/core/tree/.claude/pills/README.md +146 -0
  153. package/releases/0.34.0/core/tree/.claude/pipeline.md +101 -0
  154. package/releases/0.34.0/core/tree/.claude/retrieval.md +103 -0
  155. package/releases/0.34.0/core/tree/.claude/router.md +198 -0
  156. package/releases/0.34.0/core/tree/.claude/templates/integration.md +45 -0
  157. package/releases/0.34.0/core/tree/CLAUDE.md +161 -0
  158. package/releases/0.34.0/core/tree/scripts/cost-watch.mjs +18 -0
  159. package/releases/0.34.0/core/tree/scripts/edit-guard.mjs +16 -0
  160. package/releases/0.34.0/core/tree/scripts/harness-check.mjs +71 -0
  161. package/releases/0.34.0/core/tree/scripts/loop-gate.mjs +19 -0
  162. package/releases/0.34.0/core/vocabulary.json +13 -0
  163. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  164. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  165. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  166. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  167. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  168. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  169. package/releases/0.34.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  170. package/releases/0.34.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  171. package/releases/0.34.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  172. package/releases/0.34.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  173. package/releases/0.34.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  174. package/releases/0.34.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  175. package/releases/0.34.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  176. package/releases/0.34.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  177. package/releases/0.34.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  178. package/releases/0.34.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  179. package/releases/0.34.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  180. package/releases/0.34.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  181. package/releases/0.34.0/surfaces/db/tree/.claude/graph.md +8 -0
  182. package/releases/0.34.0/surfaces/db/tree/.claude/patterns.md +19 -0
  183. package/releases/0.34.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  184. package/releases/0.34.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  185. package/releases/0.34.0/surfaces/db/tree/.claude/router.md +4 -0
  186. package/releases/0.34.0/surfaces/db/tree/CLAUDE.md +17 -0
  187. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  188. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  189. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  190. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  191. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  192. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  193. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  194. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  195. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  196. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  197. package/releases/0.34.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  198. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  199. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  200. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  201. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  202. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  203. package/releases/0.34.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  204. package/releases/0.34.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  205. package/releases/0.34.0/surfaces/frontend/tree/.claude/router.md +14 -0
  206. package/releases/0.34.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  207. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  208. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  209. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  210. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  211. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  212. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  213. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  214. package/releases/0.34.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  215. package/releases/0.34.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  216. package/releases/0.34.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  217. package/releases/0.34.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  218. package/releases/0.34.0/surfaces/integrations/tree/.claude/router.md +6 -0
  219. package/releases/0.34.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  220. package/releases/0.34.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  221. package/releases/0.34.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  222. package/releases/0.34.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  223. package/releases/0.34.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  224. package/releases/0.34.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  225. package/releases/0.34.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  226. package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  227. package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  228. package/releases/0.34.0/surfaces/money/tree/.claude/graph.md +2 -0
  229. package/releases/0.34.0/surfaces/money/tree/.claude/patterns.md +49 -0
  230. package/releases/0.34.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  231. package/releases/0.34.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  232. package/releases/0.34.0/surfaces/money/tree/CLAUDE.md +11 -0
  233. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  234. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  235. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  236. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  237. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  238. package/releases/0.34.0/surfaces/pii/tree/CLAUDE.md +5 -0
  239. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  240. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  241. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  242. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  243. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  244. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  245. package/releases/0.34.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  246. package/releases/0.34.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  247. package/src/agentrun.mjs +2 -3
  248. package/src/commands/check.mjs +3 -1
  249. package/src/commands/export.mjs +9 -7
  250. package/src/commands/langfuse.mjs +25 -10
  251. package/src/commands/runs.mjs +7 -2
  252. package/src/commands/stats.mjs +37 -0
  253. package/src/commands/upgrade.mjs +12 -3
  254. package/src/expected.mjs +4 -3
  255. package/src/langfuse.mjs +31 -11
  256. package/src/transcripts.mjs +53 -1
@@ -25,7 +25,7 @@ import { spawn } from 'node:child_process';
25
25
  import { join } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { readRun, runFile } from '../agentrun.mjs';
28
- import { BATCH, SETTLE_MINUTES, postScore, postScores, postSpans, scoreOf, settled, spanOf, spansOf } from '../langfuse.mjs';
28
+ import { BATCH, SETTLE_MINUTES, contextMode, postScore, postScores, postSpans, scoreOf, settled, spanOf, spansOf } from '../langfuse.mjs';
29
29
  import { runOf, transcriptsOf } from '../transcripts.mjs';
30
30
  import { exportsDir, snapshotsDir } from '../paths.mjs';
31
31
  import { readConfig, statusPath, targetOf } from './langfuse.mjs';
@@ -214,7 +214,7 @@ export async function exportCommand(argv, ctx = {}) {
214
214
  const release = await lock(only);
215
215
  if (!release) return 0;
216
216
  try {
217
- const result = await exportProject({ slug: only, name: projectName(only), file: join(snapshotsDir(), `${only}.jsonl`), target, now: ctx.now, since: setting.since, quiet: true, content: setting.content });
217
+ const result = await exportProject({ slug: only, name: projectName(only), file: join(snapshotsDir(), `${only}.jsonl`), target, now: ctx.now, since: setting.since, quiet: true, content: contextMode(setting) });
218
218
  if (result.spans > 0 || result.scores > 0 || result.error) {
219
219
  // The same failure again is not news: it was reported once, and repeating it every turn it is
220
220
  // retried would be the noise the detectors exist to remove.
@@ -251,7 +251,7 @@ export async function exportCommand(argv, ctx = {}) {
251
251
  continue;
252
252
  }
253
253
  try {
254
- const content = Boolean(config.projects[slug]?.content);
254
+ const content = contextMode(config.projects[slug]);
255
255
  if ((await exportProject({ slug, name, file: join(dir, `${slug}.jsonl`), target, dry, now: ctx.now, content })).error) failed = true;
256
256
  } finally {
257
257
  await release();
@@ -265,7 +265,7 @@ export async function exportCommand(argv, ctx = {}) {
265
265
  *
266
266
  * @returns {Promise<{spans: number, scores: number, error: string|null}>} What went, and why the rest did not.
267
267
  */
268
- async function exportProject({ slug, name, file, target, dry, now, since = '', quiet = false, content = false }) {
268
+ async function exportProject({ slug, name, file, target, dry, now, since = '', quiet = false, content = null }) {
269
269
  const say = quiet ? () => {} : (line) => console.log(line);
270
270
  const records = await recordsOf(file);
271
271
  const sent = await sentOf(slug);
@@ -282,7 +282,8 @@ async function exportProject({ slug, name, file, target, dry, now, since = '', q
282
282
  const tail = () => (notes.length ? ` — ${notes.join('; ')}` : '');
283
283
 
284
284
  if (dry) {
285
- say(` ${name}: would send ${todo.spans.length} trace(s)${content ? " with each stage's context" : ''} and ${todo.scores.length} score(s) to ${target.host}${tail()}`);
285
+ const what = content === 'prompts' ? " with each stage's prompt and report" : content ? " with each stage's context" : '';
286
+ say(` ${name}: would send ${todo.spans.length} trace(s)${what} and ${todo.scores.length} score(s) to ${target.host}${tail()}`);
286
287
  if (todo.spans[0]) say(` e.g. ${JSON.stringify(spanOf(todo.spans[0], name))}`);
287
288
  return { spans: 0, scores: 0, error: null };
288
289
  }
@@ -319,11 +320,12 @@ async function exportProject({ slug, name, file, target, dry, now, since = '', q
319
320
  if (error) break;
320
321
  const run = content ? await readRun(runFile(transcriptsOf(slug), record), record.round ?? 1) : null;
321
322
  if (content && !run) bare += 1;
322
- let built = spansOf(record, name, run);
323
+ const sending = { prompts: content === 'prompts' };
324
+ let built = spansOf(record, name, run, {}, sending);
323
325
  let size = JSON.stringify(built).length;
324
326
  for (const limits of SMALLER) {
325
327
  if (size <= MAX_BODY) break;
326
- built = spansOf(record, name, run, limits);
328
+ built = spansOf(record, name, run, limits, sending);
327
329
  size = JSON.stringify(built).length;
328
330
  }
329
331
  // Cutting its texts bounds each one, not how many calls a stage made. One that still does not fit
@@ -2,14 +2,15 @@
2
2
  * `nina langfuse` — the keys, set once, and which projects send their runs to Langfuse on their own.
3
3
  *
4
4
  * nina langfuse login [--host <url>] ask for the keys, check them against Langfuse, keep them
5
- * nina langfuse on [--project <dir>] [--content] send this project's runs after every turn
5
+ * nina langfuse on [--project <dir>] [--content | --prompts] send this project's runs after every turn
6
6
  * nina langfuse off [--project <dir>]
7
7
  * nina langfuse status
8
8
  *
9
9
  * Once a project is on, nobody runs an export: the `lessons` detector every composed project runs
10
10
  * already snapshots the project each turn, and it starts the export in the background whenever a run
11
11
  * is ready to go. What it sends is metadata unless the project is turned on with `--content`, which adds
12
- * each stage's own context — see `src/langfuse.mjs`.
12
+ * each stage's own context, or with `--prompts`, which adds only what passed between the agents: the
13
+ * prompt each stage was given and the report it handed back — see `src/langfuse.mjs`.
13
14
  *
14
15
  * The keys live in `~/.nina/exports/langfuse.json`, readable by the owner only, beside the record of what
15
16
  * was sent; `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and `LANGFUSE_HOST` override them. They are asked
@@ -21,20 +22,26 @@
21
22
  import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
22
23
  import { join, resolve } from 'node:path';
23
24
  import { createInterface } from 'node:readline';
24
- import { DEFAULT_HOST, projectOf } from '../langfuse.mjs';
25
+ import { DEFAULT_HOST, contextMode, projectOf } from '../langfuse.mjs';
25
26
  import { exportsDir, slugFor } from '../paths.mjs';
26
27
  import { projectName } from './stats.mjs';
27
28
 
28
29
  /** Where the keys and the switches live. */
29
30
  export const configPath = () => join(exportsDir(), 'langfuse.json');
30
31
 
32
+ /** What a project sends, in words, by `contextMode`. */
33
+ const LABELS = { full: "with each stage's own context", prompts: "with each stage's prompt and report", none: 'as metadata only' };
34
+
35
+ /** The same, as `status` lists it. */
36
+ const SHORT = { full: 'with context', prompts: 'with prompts and reports', none: 'metadata only' };
37
+
31
38
  /** Where the background export leaves what it last did, per project. */
32
39
  export const statusPath = (slug) => join(exportsDir(), 'langfuse', `${slug}.status.json`);
33
40
 
34
41
  /**
35
42
  * The configuration on disk: keys, host, and the projects that are on.
36
43
  *
37
- * @returns {Promise<{publicKey?: string, secretKey?: string, host?: string, projects: Record<string, {content: boolean, since: string}>}>}
44
+ * @returns {Promise<{publicKey?: string, secretKey?: string, host?: string, projects: Record<string, {content: boolean|'prompts', since: string}>}>}
38
45
  */
39
46
  export async function readConfig() {
40
47
  try {
@@ -113,7 +120,7 @@ export async function langfuse(argv, ctx = {}) {
113
120
  }
114
121
  await writeConfig({ ...config, host, publicKey, secretKey });
115
122
  console.log(` ✓ the keys open "${opened.project}" at ${host}; kept in ${configPath()}, readable by you only`);
116
- console.log(' next, in each project that should send: `nina langfuse on` (add `--content` for each stage\'s own context)\n');
123
+ console.log(' next, in each project that should send: `nina langfuse on` (add `--prompts` for what passed between the agents, or `--content` for each stage\'s whole context)\n');
117
124
  return 0;
118
125
  }
119
126
 
@@ -123,13 +130,21 @@ export async function langfuse(argv, ctx = {}) {
123
130
  console.error(' no keys yet — `nina langfuse login` first\n');
124
131
  return 2;
125
132
  }
126
- const content = argv.includes('--content');
133
+ if (argv.includes('--content') && argv.includes('--prompts')) {
134
+ console.error(' --content already sends the prompts; pass one of the two\n');
135
+ return 2;
136
+ }
137
+ const content = argv.includes('--content') ? true : argv.includes('--prompts') ? 'prompts' : false;
127
138
  await writeConfig({ ...config, projects: { ...config.projects, [slug]: { content, since: new Date().toISOString() } } });
128
- console.log(` ✓ ${project} sends its runs to ${target.host} from now on, after every turn, ${content ? "with each stage's own context" : 'as metadata only'}`);
129
- if (content) {
139
+ console.log(` ✓ ${project} sends its runs to ${target.host} from now on, after every turn, ${LABELS[contextMode({ content }) ?? 'none']}`);
140
+ if (content === true) {
130
141
  console.log(" each stage's prompt, messages, tool calls and report go with it — code and whatever the stages read included;");
131
142
  console.log(' obvious secrets are masked, and nothing more is: turn it on only where that code may leave this machine');
132
143
  }
144
+ if (content === 'prompts') {
145
+ console.log(" each stage's prompt and report go with it, and nothing it read, wrote or called between them;");
146
+ console.log(' a prompt carries whatever the orchestrator put in it, diffs and code included, and only obvious secrets are masked');
147
+ }
133
148
  console.log(` the runs from before now stay here; \`nina export --langfuse --project ${slug}\` sends them, once`);
134
149
  console.log(' it runs from the `lessons` detector in this project\'s hooks — `nina check` says whether they are wired\n');
135
150
  return 0;
@@ -150,12 +165,12 @@ export async function langfuse(argv, ctx = {}) {
150
165
  for (const [name, setting] of on) {
151
166
  const last = JSON.parse(await readFile(statusPath(name), 'utf8').catch(() => 'null'));
152
167
  const said = !last ? 'nothing sent yet' : last.error ? `✗ ${last.at}: ${last.error}` : `last sent ${last.at}: ${last.spans} trace(s), ${last.scores} score(s)`;
153
- console.log(` ${projectName(name)}: ${setting.content ? 'with context' : 'metadata only'}, since ${setting.since.slice(0, 10)} — ${said}`);
168
+ console.log(` ${projectName(name)}: ${SHORT[contextMode(setting) ?? 'none']}, since ${setting.since.slice(0, 10)} — ${said}`);
154
169
  }
155
170
  console.log('');
156
171
  return 0;
157
172
  }
158
173
 
159
- console.error(' usage: nina langfuse login [--host <url>] | on [--project <dir>] [--content] | off [--project <dir>] | status\n');
174
+ console.error(' usage: nina langfuse login [--host <url>] | on [--project <dir>] [--content | --prompts] | off [--project <dir>] | status\n');
160
175
  return 2;
161
176
  }
@@ -32,7 +32,7 @@ const ENDS = {
32
32
  * stages first ran.
33
33
  *
34
34
  * @param {{runs: object[], end: string}} cycle
35
- * @returns {{start: string, end: string, minutes: number|null, cost: number, unpriced: number, name: string, how: string, stages: {role: string, runs: number, rounds: number, back: number}[], files: number|null}}
35
+ * @returns {{start: string, end: string, minutes: number|null, cost: number, unpriced: number, name: string, how: string, stages: {role: string, runs: number, rounds: number, back: number}[], files: number|null, peak: number|null}}
36
36
  */
37
37
  export function summarize(cycle) {
38
38
  const rounds = cycle.runs;
@@ -56,6 +56,7 @@ export function summarize(cycle) {
56
56
  stages.set(r.role, s);
57
57
  }
58
58
  const files = rounds.filter((r) => typeof r.files_touched === 'number').map((r) => r.files_touched);
59
+ const peaks = rounds.filter((r) => typeof r.context_peak === 'number').map((r) => r.context_peak);
59
60
  return {
60
61
  start,
61
62
  end,
@@ -66,6 +67,7 @@ export function summarize(cycle) {
66
67
  how: ENDS[cycle.end](rounds.at(-1)),
67
68
  stages: [...stages.values()].map((s) => ({ ...s, runs: s.runs.size })),
68
69
  files: files.length ? Math.max(...files) : null,
70
+ peak: peaks.length ? Math.max(...peaks) : null,
69
71
  };
70
72
  }
71
73
 
@@ -111,7 +113,10 @@ export async function runs(argv) {
111
113
  const count = s.rounds > s.runs ? `${s.rounds} rounds` : `${s.rounds}`;
112
114
  return `${pink(s.role)} ${count}${s.back ? amber(`, ${s.back} sent back`) : ''}`;
113
115
  });
114
- console.log(` ${chain.join(dim(' · '))}${c.files === null ? '' : dim(` · largest write ${c.files} file(s)`)}`);
116
+ console.log(
117
+ ` ${chain.join(dim(' · '))}${c.files === null ? '' : dim(` · largest write ${c.files} file(s)`)}` +
118
+ (c.peak === null ? '' : dim(` · context up to ${Math.round(c.peak / 1000)}k`)),
119
+ );
115
120
  }
116
121
  const total = shown.reduce((a, c) => a + c.cost, 0);
117
122
  const costliest = shown.reduce((a, c) => (c.cost > a.cost ? c : a), shown[0]);
@@ -223,6 +223,42 @@ function costReport(records) {
223
223
  console.log(bold(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`));
224
224
  }
225
225
 
226
+ /**
227
+ * How each stage's rounds spent their context: turns, tool calls sent per turn, the context a turn
228
+ * re-read when the round began and at its largest, and how long the prompt that opened the round was. Every turn is billed the whole context again, so the
229
+ * cost of a round is its turns times its context, and a round that starts where a long one ended pays for
230
+ * all of it on every turn. The first rounds of a run are set beside the later ones, which are resumes.
231
+ *
232
+ * @param {object[]} records - The rounds in the window.
233
+ */
234
+ function contextReport(records) {
235
+ const shaped = records.filter((r) => typeof r.turns === 'number' && r.turns > 0);
236
+ if (shaped.length === 0) return;
237
+ const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
238
+ const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
239
+ const chars = (xs) => (xs.length ? median(xs).toLocaleString('en-US') : '—');
240
+ console.log(heading('context', 'every turn re-reads the whole context: a round costs its turns times its size, and a resumed one starts where it ended; the prompt it was handed is in characters'));
241
+ console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padEnd(8)}${'n'.padStart(4)}${'turns'.padStart(7)}${'calls/turn'.padStart(12)}${'starts at'.padStart(11)}${'peak'.padStart(8)}${'prompt'.padStart(9)}`));
242
+ const byRole = new Map();
243
+ for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
244
+ for (const [role, rounds] of [...byRole].sort((a, b) => b[1].reduce((x, r) => x + r.turns, 0) - a[1].reduce((x, r) => x + r.turns, 0))) {
245
+ [
246
+ ['first', rounds.filter((r) => (r.round ?? 1) === 1)],
247
+ ['resumed', rounds.filter((r) => (r.round ?? 1) > 1)],
248
+ ].forEach(([label, group], i) => {
249
+ if (group.length === 0) return;
250
+ const turns = group.reduce((a, r) => a + r.turns, 0);
251
+ const calls = group.reduce((a, r) => a + (r.tool_calls ?? 0), 0);
252
+ const name = i === 0 || !rounds.some((r) => (r.round ?? 1) === 1) ? pink(role) + ' '.repeat(Math.max(20 - role.length, 1)) : ' '.repeat(20);
253
+ console.log(
254
+ ` ${name}${dim(label.padEnd(8))}${String(group.length).padStart(4)}${String(median(group.map((r) => r.turns))).padStart(7)}` +
255
+ `${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}` +
256
+ `${chars(group.map((r) => r.prompt_chars).filter((n) => typeof n === 'number')).padStart(9)}`,
257
+ );
258
+ });
259
+ }
260
+ }
261
+
226
262
  /**
227
263
  * Whether a model a run used is the one a spec declares: an alias (`opus`) names a family — a whole
228
264
  * segment of the id, wherever it sits, so `claude-3-sonnet` is a sonnet — anything else names a model
@@ -722,6 +758,7 @@ export async function stats(argv, ctx) {
722
758
  }
723
759
 
724
760
  costReport(records.filter((r) => r.status !== 'denied'));
761
+ contextReport(records.filter((r) => r.status !== 'denied'));
725
762
  modelReport(records.filter((r) => r.status !== 'denied'));
726
763
  proportionReport(records.filter((r) => r.status !== 'denied'), stepLimitOf(ctx));
727
764
 
@@ -37,7 +37,8 @@ const BANNER = /^[\s█╗╔╝║═╚▄▀]*$|harness orchestration ·/;
37
37
  * fault — everything else it finds still rolls the move back.
38
38
  *
39
39
  * @param {string} target - The project.
40
- * @param {string[]} newSlots - Project slots this move introduces, as `<file> <slot id>`.
40
+ * @param {string[]} newSlots - Project slots this move introduces, as `<file> <slot id>`, and the
41
+ * vocabulary names it introduces, as `{{NAME}}`.
41
42
  * @returns {{label: string, args: string[]}[]}
42
43
  */
43
44
  function validationsFor(target, newSlots = []) {
@@ -242,6 +243,10 @@ export async function upgrade(argv, ctx) {
242
243
  // A slot the new release fills itself is not asked of the project: it composes as the release wrote it.
243
244
  const defaultedAfter = await defaultedSlots(onto.dir);
244
245
  const newSlots = [...slotsAfter].filter((s) => !slotsBefore.has(s) && !filled.has(s) && !defaultedAfter.has(s)).sort();
246
+ // A name only the new release uses is owed the way a new slot is: it cannot be filled with meaning
247
+ // before the core that asks for it is pinned, and rolling back for it left no order the move completes in.
248
+ const newVocab = needed.filter((v) => !vocabBefore.has(v));
249
+ const awaited = [...newSlots, ...newVocab.map((v) => `{{${v}}}`)];
245
250
  const stranded = [...filled].filter((s) => !slotsAfter.has(s)).sort();
246
251
 
247
252
  console.log(` ${profile.core} → ${to}\n`);
@@ -357,7 +362,7 @@ export async function upgrade(argv, ctx) {
357
362
  // What was already failing is not this upgrade's doing, and rolling back for it would blame
358
363
  // the move for a problem the project brought with it. Measured before anything is written,
359
364
  // so the only rollback is for something that WAS passing and now is not.
360
- const VALIDATIONS = validationsFor(target, newSlots);
365
+ const VALIDATIONS = validationsFor(target, awaited);
361
366
  const DETECTORS = "running the project's own detectors";
362
367
  const hasDetectors = await declares(target, 'harness:check');
363
368
 
@@ -416,7 +421,7 @@ export async function upgrade(argv, ctx) {
416
421
  // `harness:check` — an npm script whose detector list this command does not own and cannot
417
422
  // pass an argument to. The environment is what crosses that boundary, and setting it once
418
423
  // means a reporter added to the chain later is told without anyone remembering to tell it.
419
- if (newSlots.length > 0) process.env[EXPECT_ENV] = newSlots.join(',');
424
+ if (awaited.length > 0) process.env[EXPECT_ENV] = awaited.join(',');
420
425
 
421
426
  // Compose writes and never deletes. So the files the new version stops composing are removed right
422
427
  // after it composes — before anything is verified, so what is verified is what stays — unless they
@@ -516,6 +521,10 @@ export async function upgrade(argv, ctx) {
516
521
  console.log(`\n ${newSlots.length} project slot(s) still to fill — only this project can write them:`);
517
522
  for (const s of newSlots) console.log(` ${s}`);
518
523
  }
524
+ if (newVocab.length > 0) {
525
+ console.log(`\n ${newVocab.length} vocabulary name(s) ${to} asks for and this profile does not answer — fill each, or defer it with why:`);
526
+ console.log(` ${newVocab.join(', ')}`);
527
+ }
519
528
  console.log('');
520
529
  return 0;
521
530
  }
package/src/expected.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The project slots a caller has declared it already knows are open.
2
+ * The project slots, and vocabulary names, a caller has declared it already knows are open.
3
3
  *
4
4
  * `nina upgrade --apply` creates slots: a new core can introduce one, and it cannot be filled
5
- * before the core that introduces it is pinned. Every reporter that fails on an unfilled slot
5
+ * before the core that introduces it is pinned. A name only the new core uses is the same case,
6
+ * named as `{{NAME}}`: `check` rolled a move back for one the preview had just listed as needed. Every reporter that fails on an unfilled slot
6
7
  * therefore has to be told, or the move deadlocks — there is no order in which it completes.
7
8
  *
8
9
  * Two reporters say this same thing, and the first fix taught only one: `check` counts a slot
@@ -21,7 +22,7 @@ export const EXPECT_ENV = 'NINA_EXPECT_UNFILLED';
21
22
  /**
22
23
  * @param {string[]} argv - The command's arguments; `--expect-unfilled <a,b>` names slots.
23
24
  * @param {NodeJS.ProcessEnv} [env] - Where to read `NINA_EXPECT_UNFILLED` from.
24
- * @returns {Set<string>} Slot ids as `<relative path> <slot id>`.
25
+ * @returns {Set<string>} Slot ids as `<relative path> <slot id>`, names as `{{NAME}}`.
25
26
  */
26
27
  export function expectedUnfilled(argv = [], env = process.env) {
27
28
  const fromArgv = argv.includes('--expect-unfilled') ? argv[argv.indexOf('--expect-unfilled') + 1] ?? '' : '';
package/src/langfuse.mjs CHANGED
@@ -100,31 +100,47 @@ function usageAttributes(tokens, model, priced) {
100
100
  return out;
101
101
  }
102
102
 
103
+ /**
104
+ * How much of a stage's context a project sends, from its switch in the Langfuse configuration: all of it
105
+ * (`--content`), only what passed between the agents (`--prompts`), or none.
106
+ *
107
+ * @param {{content?: boolean|string}|undefined} setting - The project's entry, if it is on.
108
+ * @returns {'full'|'prompts'|null}
109
+ */
110
+ export function contextMode(setting) {
111
+ return setting?.content === 'prompts' ? 'prompts' : setting?.content ? 'full' : null;
112
+ }
113
+
114
+ /** A round's shape, as metadata on its root: how many turns and calls it took, and how large its context and prompt were. */
115
+ const SHAPE = ['turns', 'tool_calls', 'context_start', 'context_peak', 'prompt_chars'];
116
+
103
117
  /**
104
118
  * The OTLP span at the root of a dispatch's trace. Without its context, a run that spent tokens is a
105
119
  * generation carrying the run's whole usage and estimated cost, and one that did not is a span. With it,
106
120
  * the root is the agent — its prompt in, its report out — and the usage is on the messages beneath it,
107
- * so no sum counts a token twice. Its verdict is the score beside it rather than an attribute on it: a
108
- * run whose verdict is read only later can still be given one, since that is a first score and not a
109
- * second span.
121
+ * so no sum counts a token twice. With the prompt and report alone, nothing is beneath it, so the root is
122
+ * a generation again, carrying the usage beside them. Its verdict is the score beside it rather than an
123
+ * attribute on it: a run whose verdict is read only later can still be given one, since that is a first
124
+ * score and not a second span.
110
125
  *
111
126
  * @param {object} record - A snapshot record.
112
127
  * @param {string} project - The name the project goes by in Langfuse.
113
128
  * @param {{prompt: string, report: string}} [context] - What the stage was given and handed back.
129
+ * @param {{usage?: boolean}} [options] - `usage`: the root carries the usage even with its context.
114
130
  * @returns {object|null} Null for a record with no time to place it at.
115
131
  */
116
- export function spanOf(record, project, context = null) {
132
+ export function spanOf(record, project, context = null, { usage = false } = {}) {
117
133
  const start = nanos(record.ts);
118
134
  if (!start) return null;
119
135
  const end = nanos(record.result_ts) ?? start;
120
- const tokens = !context && record.tokens && typeof record.tokens === 'object' ? record.tokens : null;
136
+ const tokens = (!context || usage) && record.tokens && typeof record.tokens === 'object' ? record.tokens : null;
121
137
  const attributes = [
122
138
  attribute('langfuse.session.id', record.session ?? 'no-session'),
123
139
  attribute('langfuse.trace.name', record.role),
124
140
  attribute('langfuse.trace.tags', ['nina', project]),
125
141
  attribute('langfuse.trace.metadata.project', project),
126
142
  attribute('langfuse.trace.metadata.role', record.role),
127
- attribute('langfuse.observation.type', context ? 'agent' : tokens ? 'generation' : 'span'),
143
+ attribute('langfuse.observation.type', tokens ? 'generation' : context ? 'agent' : 'span'),
128
144
  attribute('langfuse.observation.metadata.role', record.role),
129
145
  ];
130
146
  if (typeof record.issues === 'number') attributes.push(attribute('langfuse.observation.metadata.issues', record.issues));
@@ -133,6 +149,7 @@ export function spanOf(record, project, context = null) {
133
149
  if (record.effort) attributes.push(attribute('langfuse.observation.metadata.effort', record.effort));
134
150
  // A round after the first is the same agent resumed after it reported; the trace says which.
135
151
  if (typeof record.round === 'number') attributes.push(attribute('langfuse.observation.metadata.round', record.round));
152
+ for (const key of SHAPE) if (typeof record[key] === 'number') attributes.push(attribute(`langfuse.observation.metadata.${key}`, record[key]));
136
153
  if (tokens) attributes.push(...usageAttributes(tokens, record.usage_model, record.usage_model));
137
154
  if (context) {
138
155
  attributes.push(attribute('langfuse.observation.input', context.prompt ?? ''));
@@ -144,23 +161,26 @@ export function spanOf(record, project, context = null) {
144
161
  /**
145
162
  * Everything a dispatch sends: the root, and — when its context goes too — a generation per message it
146
163
  * wrote, with that message's own tokens and cost, and an observation per tool it called, with what went
147
- * in and what came back. Every text is masked (`redact`), cut to a length, and has the owner's home
148
- * directory written as `~`.
164
+ * in and what came back. With `prompts`, only what passed between the agents goes: the root, with the
165
+ * prompt the stage was given and the report it handed back, and none of what it read, wrote or called in
166
+ * between. Every text is masked (`redact`), cut to a length, and has the owner's home directory written
167
+ * as `~`.
149
168
  *
150
169
  * @param {object} record - A snapshot record.
151
170
  * @param {string} project - The name the project goes by in Langfuse.
152
171
  * @param {object|null} run - From `readRun`, or null to send the record alone.
153
172
  * @param {{text?: number, tool?: number}} [limits] - How much of each text goes.
173
+ * @param {{prompts?: boolean}} [options] - `prompts`: the prompt and the report, and nothing beneath them.
154
174
  * @returns {object[]} Spans; empty for a record with no time to place it at.
155
175
  */
156
- export function spansOf(record, project, run, limits = {}) {
176
+ export function spansOf(record, project, run, limits = {}, { prompts = false } = {}) {
157
177
  const textChars = limits.text ?? TEXT_CHARS;
158
178
  const toolChars = limits.tool ?? TOOL_CHARS;
159
179
  const home = homedir();
160
180
  const clean = (text, chars) => clip(redact(String(text ?? '')).replaceAll(home, '~'), chars);
161
- const root = spanOf(record, project, run ? { prompt: clean(run.prompt, textChars), report: clean(run.report, textChars) } : null);
181
+ const root = spanOf(record, project, run ? { prompt: clean(run.prompt, textChars), report: clean(run.report, textChars) } : null, { usage: prompts });
162
182
  if (!root) return [];
163
- if (!run) return [root];
183
+ if (!run || prompts) return [root];
164
184
  const child = (id, name, start, end, attributes) => ({
165
185
  traceId: root.traceId,
166
186
  spanId: hexId(`${record.dispatch_id}|${id}`, 16),
@@ -626,9 +626,13 @@ async function attachAgentDetail(projectDir, dispatches) {
626
626
  // 744 of one project's 826 in full on every turn, two seconds before the person saw an answer.
627
627
  const size = (await stat(join(dir, file)).catch(() => null))?.size ?? null;
628
628
  const settled = (r) => r.handed_back === false || String(r.verdict_source ?? '').startsWith('handback');
629
+ // `resumes` is written only by a reader from before rounds, which bills the whole run to its first
630
+ // record. A store both versions wrote to — one project on the old pin, a hook elsewhere on the new —
631
+ // had its runs marked read by the old one and skipped here, their first round billed twice.
629
632
  if (
630
633
  run.agent_read_bytes === size &&
631
- [...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r)
634
+ !('resumes' in run) &&
635
+ [...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r && 'turns' in r && 'prompt_chars' in r)
632
636
  ) {
633
637
  continue;
634
638
  }
@@ -656,6 +660,10 @@ async function attachAgentDetail(projectDir, dispatches) {
656
660
  usage: new Map(),
657
661
  /** The files the round wrote through the edit tools. Only their number is kept. */
658
662
  written: new Set(),
663
+ /** The tool calls the round made, by id: over its turns, how many calls each turn sent at once. */
664
+ calls: new Set(),
665
+ /** How long the prompt that opened the round was, in characters; never the prompt. */
666
+ promptChars: null,
659
667
  });
660
668
  const rounds = [fresh()];
661
669
  const roundOf = roundsOf();
@@ -675,8 +683,17 @@ async function attachAgentDetail(projectDir, dispatches) {
675
683
  }
676
684
  }
677
685
  const round = rounds[n - 1];
686
+ if (round.promptChars === null && line.includes('"type":"user"')) {
687
+ try {
688
+ const said = promptOf(JSON.parse(line), n);
689
+ if (said !== null) round.promptChars = said.length;
690
+ } catch {
691
+ // A torn line is not the prompt.
692
+ }
693
+ }
678
694
  const hasSkill = line.includes('"Skill"');
679
695
  const hasHandback = line.includes('SubagentHandback');
696
+ for (const m of line.matchAll(/"type":"tool_use","id":"([^"]+)"/g)) round.calls.add(m[1]);
680
697
  if (/"name":"(Edit|Write|MultiEdit|NotebookEdit)"/.test(line)) {
681
698
  try {
682
699
  for (const block of JSON.parse(line)?.message?.content ?? []) {
@@ -755,9 +772,15 @@ async function attachAgentDetail(projectDir, dispatches) {
755
772
  targets = [dispatches.get(id)];
756
773
  }
757
774
  const spent = tokensOf(round.usage);
775
+ const shape = contextOf(round.usage);
758
776
  targets.forEach((record, j) => {
759
777
  // One transcript, one bill: were two records ever to share an agent, the second would count it again.
760
778
  record.tokens = j === 0 ? spent.tokens : null;
779
+ record.turns = shape.turns;
780
+ record.tool_calls = round.calls.size;
781
+ record.context_start = shape.start;
782
+ record.context_peak = shape.peak;
783
+ record.prompt_chars = round.promptChars;
761
784
  record.usage_model = spent.model;
762
785
  record.effort = spent.effort;
763
786
  record.files_touched = round.written.size;
@@ -790,6 +813,35 @@ async function attachAgentDetail(projectDir, dispatches) {
790
813
  }
791
814
  }
792
815
 
816
+ /**
817
+ * What a round was told, from one transcript row, or null when the row is not it. The dispatch's prompt
818
+ * opens the first round; a later round opens with the message that resumed the run, which Claude Code
819
+ * marks as meta, while a meta row in the first round is a reminder. The snapshot measures it and the
820
+ * Langfuse export sends it, so both read it here.
821
+ *
822
+ * @param {object} row - A parsed transcript row.
823
+ * @param {number} round - The round the row is in, from 1.
824
+ * @returns {string|null}
825
+ */
826
+ export function promptOf(row, round) {
827
+ const content = row?.message?.content;
828
+ return row?.type === 'user' && (round > 1 || !row.isMeta) && typeof content === 'string' ? content : null;
829
+ }
830
+
831
+ /**
832
+ * How a round spent its context: how many turns it took, and how large the context each turn re-read was
833
+ * when the round began and at its largest. Every turn is billed the whole context again, so a round that
834
+ * begins where a long one ended — a run resumed for a fix — pays for everything before it on each turn.
835
+ *
836
+ * @param {Map<string, {usage: object}>} usage - Message id → its final usage, in the order the round sent them.
837
+ * @returns {{turns: number, start: number|null, peak: number|null}} Context sizes in tokens.
838
+ */
839
+ export function contextOf(usage) {
840
+ const n = (v) => (typeof v === 'number' ? v : 0);
841
+ const sizes = [...usage.values()].map(({ usage: u }) => n(u.input_tokens) + n(u.cache_read_input_tokens) + n(u.cache_creation_input_tokens));
842
+ return { turns: sizes.length, start: sizes[0] ?? null, peak: sizes.length > 0 ? Math.max(...sizes) : null };
843
+ }
844
+
793
845
  /**
794
846
  * What a run spent, from each of its messages' final usage: tokens by kind, and the model and effort
795
847
  * level that most of its messages ran at. Metadata — counts, a model id and a level, never a message.