@xhulz/nina 0.33.0 → 0.35.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 (264) hide show
  1. package/README.md +4 -2
  2. package/bin/nina.mjs +219 -48
  3. package/package.json +1 -1
  4. package/releases/0.34.0/core/GAPS.md +125 -0
  5. package/releases/0.34.0/core/answered.json +29 -0
  6. package/releases/0.34.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  7. package/releases/0.34.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  8. package/releases/0.34.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  9. package/releases/0.34.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  10. package/releases/0.34.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  11. package/releases/0.34.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  12. package/releases/0.34.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  13. package/releases/0.34.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  14. package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  15. package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  16. package/releases/0.34.0/core/tree/.claude/agents/architect.md +127 -0
  17. package/releases/0.34.0/core/tree/.claude/agents/dba.md +110 -0
  18. package/releases/0.34.0/core/tree/.claude/agents/devops.md +114 -0
  19. package/releases/0.34.0/core/tree/.claude/agents/implementer.md +156 -0
  20. package/releases/0.34.0/core/tree/.claude/agents/integration-tester.md +168 -0
  21. package/releases/0.34.0/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.34.0/core/tree/.claude/agents/qa.md +148 -0
  23. package/releases/0.34.0/core/tree/.claude/agents/reviewer.md +183 -0
  24. package/releases/0.34.0/core/tree/.claude/agents/secops.md +121 -0
  25. package/releases/0.34.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  26. package/releases/0.34.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  27. package/releases/0.34.0/core/tree/.claude/agents-overview.md +64 -0
  28. package/releases/0.34.0/core/tree/.claude/frontend.md +10 -0
  29. package/releases/0.34.0/core/tree/.claude/graph.md +75 -0
  30. package/releases/0.34.0/core/tree/.claude/patterns.md +213 -0
  31. package/releases/0.34.0/core/tree/.claude/pills/README.md +146 -0
  32. package/releases/0.34.0/core/tree/.claude/pipeline.md +101 -0
  33. package/releases/0.34.0/core/tree/.claude/retrieval.md +103 -0
  34. package/releases/0.34.0/core/tree/.claude/router.md +198 -0
  35. package/releases/0.34.0/core/tree/.claude/templates/integration.md +45 -0
  36. package/releases/0.34.0/core/tree/CLAUDE.md +161 -0
  37. package/releases/0.34.0/core/tree/scripts/cost-watch.mjs +18 -0
  38. package/releases/0.34.0/core/tree/scripts/edit-guard.mjs +16 -0
  39. package/releases/0.34.0/core/tree/scripts/harness-check.mjs +71 -0
  40. package/releases/0.34.0/core/tree/scripts/loop-gate.mjs +19 -0
  41. package/releases/0.34.0/core/vocabulary.json +13 -0
  42. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  43. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  44. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  45. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  46. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  47. package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  48. package/releases/0.34.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  49. package/releases/0.34.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  50. package/releases/0.34.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  51. package/releases/0.34.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  52. package/releases/0.34.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  53. package/releases/0.34.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  54. package/releases/0.34.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  55. package/releases/0.34.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  56. package/releases/0.34.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  57. package/releases/0.34.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  58. package/releases/0.34.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  59. package/releases/0.34.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  60. package/releases/0.34.0/surfaces/db/tree/.claude/graph.md +8 -0
  61. package/releases/0.34.0/surfaces/db/tree/.claude/patterns.md +19 -0
  62. package/releases/0.34.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  63. package/releases/0.34.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  64. package/releases/0.34.0/surfaces/db/tree/.claude/router.md +4 -0
  65. package/releases/0.34.0/surfaces/db/tree/CLAUDE.md +17 -0
  66. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  67. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  68. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  69. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  70. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  71. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  72. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  73. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  74. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  75. package/releases/0.34.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  76. package/releases/0.34.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  77. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  78. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  79. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  80. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  81. package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  82. package/releases/0.34.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  83. package/releases/0.34.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  84. package/releases/0.34.0/surfaces/frontend/tree/.claude/router.md +14 -0
  85. package/releases/0.34.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  86. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  87. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  88. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  89. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  90. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  91. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  92. package/releases/0.34.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  93. package/releases/0.34.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  94. package/releases/0.34.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  95. package/releases/0.34.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  96. package/releases/0.34.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  97. package/releases/0.34.0/surfaces/integrations/tree/.claude/router.md +6 -0
  98. package/releases/0.34.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  99. package/releases/0.34.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  100. package/releases/0.34.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  101. package/releases/0.34.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  102. package/releases/0.34.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  103. package/releases/0.34.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  104. package/releases/0.34.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  105. package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  106. package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  107. package/releases/0.34.0/surfaces/money/tree/.claude/graph.md +2 -0
  108. package/releases/0.34.0/surfaces/money/tree/.claude/patterns.md +49 -0
  109. package/releases/0.34.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  110. package/releases/0.34.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  111. package/releases/0.34.0/surfaces/money/tree/CLAUDE.md +11 -0
  112. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  113. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  114. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  115. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  116. package/releases/0.34.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  117. package/releases/0.34.0/surfaces/pii/tree/CLAUDE.md +5 -0
  118. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  119. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  120. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  121. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  122. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  123. package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  124. package/releases/0.34.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  125. package/releases/0.34.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  126. package/releases/0.35.0/core/GAPS.md +125 -0
  127. package/releases/0.35.0/core/answered.json +29 -0
  128. package/releases/0.35.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  129. package/releases/0.35.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  130. package/releases/0.35.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  131. package/releases/0.35.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  132. package/releases/0.35.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  133. package/releases/0.35.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  134. package/releases/0.35.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  135. package/releases/0.35.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  136. package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  137. package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  138. package/releases/0.35.0/core/tree/.claude/agents/architect.md +127 -0
  139. package/releases/0.35.0/core/tree/.claude/agents/dba.md +110 -0
  140. package/releases/0.35.0/core/tree/.claude/agents/devops.md +114 -0
  141. package/releases/0.35.0/core/tree/.claude/agents/implementer.md +156 -0
  142. package/releases/0.35.0/core/tree/.claude/agents/integration-tester.md +168 -0
  143. package/releases/0.35.0/core/tree/.claude/agents/planner.md +132 -0
  144. package/releases/0.35.0/core/tree/.claude/agents/qa.md +153 -0
  145. package/releases/0.35.0/core/tree/.claude/agents/reviewer.md +183 -0
  146. package/releases/0.35.0/core/tree/.claude/agents/secops.md +121 -0
  147. package/releases/0.35.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  148. package/releases/0.35.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  149. package/releases/0.35.0/core/tree/.claude/agents-overview.md +64 -0
  150. package/releases/0.35.0/core/tree/.claude/frontend.md +10 -0
  151. package/releases/0.35.0/core/tree/.claude/graph.md +75 -0
  152. package/releases/0.35.0/core/tree/.claude/patterns.md +213 -0
  153. package/releases/0.35.0/core/tree/.claude/pills/README.md +146 -0
  154. package/releases/0.35.0/core/tree/.claude/pipeline.md +101 -0
  155. package/releases/0.35.0/core/tree/.claude/retrieval.md +103 -0
  156. package/releases/0.35.0/core/tree/.claude/router.md +198 -0
  157. package/releases/0.35.0/core/tree/.claude/templates/integration.md +45 -0
  158. package/releases/0.35.0/core/tree/CLAUDE.md +161 -0
  159. package/releases/0.35.0/core/tree/scripts/cost-watch.mjs +18 -0
  160. package/releases/0.35.0/core/tree/scripts/edit-guard.mjs +16 -0
  161. package/releases/0.35.0/core/tree/scripts/harness-check.mjs +71 -0
  162. package/releases/0.35.0/core/tree/scripts/loop-gate.mjs +19 -0
  163. package/releases/0.35.0/core/vocabulary.json +13 -0
  164. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  165. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  166. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  167. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  168. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  169. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  170. package/releases/0.35.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  171. package/releases/0.35.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  172. package/releases/0.35.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  173. package/releases/0.35.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  174. package/releases/0.35.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  175. package/releases/0.35.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  176. package/releases/0.35.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  177. package/releases/0.35.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  178. package/releases/0.35.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  179. package/releases/0.35.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  180. package/releases/0.35.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  181. package/releases/0.35.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  182. package/releases/0.35.0/surfaces/db/tree/.claude/graph.md +8 -0
  183. package/releases/0.35.0/surfaces/db/tree/.claude/patterns.md +19 -0
  184. package/releases/0.35.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  185. package/releases/0.35.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  186. package/releases/0.35.0/surfaces/db/tree/.claude/router.md +4 -0
  187. package/releases/0.35.0/surfaces/db/tree/CLAUDE.md +17 -0
  188. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  189. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  190. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  191. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  192. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  193. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  194. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  195. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  196. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  197. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  198. package/releases/0.35.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  199. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  200. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  201. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  202. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  203. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  204. package/releases/0.35.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  205. package/releases/0.35.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  206. package/releases/0.35.0/surfaces/frontend/tree/.claude/router.md +14 -0
  207. package/releases/0.35.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  208. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  209. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  210. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  211. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  212. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  213. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  214. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  215. package/releases/0.35.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  216. package/releases/0.35.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  217. package/releases/0.35.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  218. package/releases/0.35.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  219. package/releases/0.35.0/surfaces/integrations/tree/.claude/router.md +6 -0
  220. package/releases/0.35.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  221. package/releases/0.35.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  222. package/releases/0.35.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  223. package/releases/0.35.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  224. package/releases/0.35.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  225. package/releases/0.35.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  226. package/releases/0.35.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  227. package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  228. package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  229. package/releases/0.35.0/surfaces/money/tree/.claude/graph.md +2 -0
  230. package/releases/0.35.0/surfaces/money/tree/.claude/patterns.md +49 -0
  231. package/releases/0.35.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  232. package/releases/0.35.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  233. package/releases/0.35.0/surfaces/money/tree/CLAUDE.md +11 -0
  234. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  235. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  236. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  237. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  238. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  239. package/releases/0.35.0/surfaces/pii/tree/CLAUDE.md +5 -0
  240. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  241. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  242. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  243. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  244. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  245. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  246. package/releases/0.35.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  247. package/releases/0.35.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  248. package/src/agentrun.mjs +91 -12
  249. package/src/commands/check.mjs +12 -8
  250. package/src/commands/compose.mjs +85 -9
  251. package/src/commands/export.mjs +9 -7
  252. package/src/commands/init.mjs +13 -4
  253. package/src/commands/langfuse.mjs +25 -10
  254. package/src/commands/learn.mjs +12 -3
  255. package/src/commands/release.mjs +21 -3
  256. package/src/commands/stats.mjs +64 -12
  257. package/src/commands/upgrade.mjs +131 -25
  258. package/src/guard.mjs +7 -37
  259. package/src/langfuse.mjs +31 -11
  260. package/src/paths.mjs +50 -1
  261. package/src/prices.mjs +7 -7
  262. package/src/transcripts.mjs +27 -1
  263. package/src/vocabulary.mjs +12 -1
  264. package/src/wiring.mjs +106 -41
@@ -14,13 +14,13 @@
14
14
  import { amber, bar, bold, dim, heading, note, pink, stacked } from '../look.mjs';
15
15
  import { readFile, readdir } from 'node:fs/promises';
16
16
  import { spawnSync } from 'node:child_process';
17
- import { createReadStream, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
17
+ import { closeSync, createReadStream, existsSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from 'node:fs';
18
18
  import { createInterface } from 'node:readline';
19
19
  import { homedir } from 'node:os';
20
20
  import { basename, join } from 'node:path';
21
- import { ROLE_TOKENS, isLoopBack, runOf } from '../transcripts.mjs';
21
+ import { ROLE_TOKENS, isLoopBack, runOf, transcriptsOf } from '../transcripts.mjs';
22
22
  import { frontmatter, pillFiles } from './pills.mjs';
23
- import { HARNESS, snapshotsDir } from '../paths.mjs';
23
+ import { HARNESS, slugFor, snapshotsDir } from '../paths.mjs';
24
24
  import { defaultVocabulary } from '../vocabulary.mjs';
25
25
  import { layerRootFor } from './compose.mjs';
26
26
  import { PRICES_AS_OF, costOf } from '../prices.mjs';
@@ -80,17 +80,64 @@ async function load(dir, opts) {
80
80
  }
81
81
 
82
82
 
83
+ /** Directories already found for an encoded name: `stats` asks for the same project many times over. */
84
+ const decoded = new Map();
85
+
86
+ /**
87
+ * The directory a project's sessions ran in, as its own transcripts record it: every line carries the
88
+ * session's `cwd`. Taken only when it encodes back to the same name and is still there.
89
+ *
90
+ * @param {string} encoded - A snapshot's project name, which is also its transcript directory's.
91
+ * @returns {string|null}
92
+ */
93
+ function recordedDir(encoded) {
94
+ const dir = transcriptsOf(encoded);
95
+ let files = [];
96
+ try {
97
+ files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
98
+ } catch {
99
+ return null;
100
+ }
101
+ for (const file of files.slice(0, 5)) {
102
+ let head = '';
103
+ try {
104
+ const fd = openSync(join(dir, file), 'r');
105
+ try {
106
+ const buffer = Buffer.alloc(64 * 1024);
107
+ head = buffer.toString('utf8', 0, readSync(fd, buffer, 0, buffer.length, 0));
108
+ } finally {
109
+ closeSync(fd);
110
+ }
111
+ } catch {
112
+ continue;
113
+ }
114
+ for (const line of head.split('\n')) {
115
+ let cwd;
116
+ try {
117
+ cwd = JSON.parse(line)?.cwd;
118
+ } catch {
119
+ continue;
120
+ }
121
+ if (typeof cwd === 'string' && slugFor(cwd) === encoded && existsSync(cwd)) return cwd;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
83
127
  /**
84
128
  * Turns a snapshot's encoded project name back into a directory.
85
129
  *
86
- * The encoding replaces every separator with a dash, so `Code-IA-harness` could be
87
- * `Code/IA/harness` or `Code/IA-harness` and the string alone cannot say which. The
88
- * filesystem can, so the split is resolved against it rather than guessed.
130
+ * The encoding makes every character that is not a letter or a digit a dash, so the name alone cannot say
131
+ * what it was. The project's transcripts can: each line records the directory its session ran in. Failing
132
+ * that, the split is resolved against the file system — which can only rebuild `/` from a dash, so a
133
+ * project whose path held a `.`, a space or an `_` was never found, and its pills, models and requests went
134
+ * missing from every report without a word.
89
135
  *
90
136
  * @param {string} encoded - The `project` field of a snapshot record.
91
137
  * @returns {string | null} The directory, or `null` when no split of the name exists.
92
138
  */
93
139
  export function decodeProjectDir(encoded) {
140
+ if (decoded.has(encoded)) return decoded.get(encoded);
94
141
  const parts = encoded.replace(/^-/, '').split('-');
95
142
  const walk = (base, i) => {
96
143
  if (i === parts.length) return base;
@@ -102,7 +149,10 @@ export function decodeProjectDir(encoded) {
102
149
  }
103
150
  return null;
104
151
  };
105
- return walk('/', 0);
152
+ const found = recordedDir(encoded) ?? walk('/', 0);
153
+ // Only what was found: a directory made after a miss is found the next time it is asked for.
154
+ if (found) decoded.set(encoded, found);
155
+ return found;
106
156
  }
107
157
 
108
158
  /**
@@ -224,8 +274,8 @@ function costReport(records) {
224
274
  }
225
275
 
226
276
  /**
227
- * How each stage's rounds spent their context: turns, tool calls sent per turn, and the context a turn
228
- * re-read when the round began and at its largest. Every turn is billed the whole context again, so the
277
+ * How each stage's rounds spent their context: turns, tool calls sent per turn, the context a turn
278
+ * 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
279
  * cost of a round is its turns times its context, and a round that starts where a long one ended pays for
230
280
  * all of it on every turn. The first rounds of a run are set beside the later ones, which are resumes.
231
281
  *
@@ -236,8 +286,9 @@ function contextReport(records) {
236
286
  if (shaped.length === 0) return;
237
287
  const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
238
288
  const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
239
- 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'));
240
- 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)}`));
289
+ const chars = (xs) => (xs.length ? median(xs).toLocaleString('en-US') : '—');
290
+ 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'));
291
+ 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)}`));
241
292
  const byRole = new Map();
242
293
  for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
243
294
  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))) {
@@ -251,7 +302,8 @@ function contextReport(records) {
251
302
  const name = i === 0 || !rounds.some((r) => (r.round ?? 1) === 1) ? pink(role) + ' '.repeat(Math.max(20 - role.length, 1)) : ' '.repeat(20);
252
303
  console.log(
253
304
  ` ${name}${dim(label.padEnd(8))}${String(group.length).padStart(4)}${String(median(group.map((r) => r.turns))).padStart(7)}` +
254
- `${(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)}`,
305
+ `${(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)}` +
306
+ `${chars(group.map((r) => r.prompt_chars).filter((n) => typeof n === 'number')).padStart(9)}`,
255
307
  );
256
308
  });
257
309
  }
@@ -6,16 +6,15 @@
6
6
  * already wrote that the new harness has nowhere to put. A slot that was renamed or dropped
7
7
  * takes the project's own text out of the composition with it, silently, because a fragment
8
8
  * nothing references composes to nothing and no check notices. So this reports first and
9
- * writes only when told to, and what it writes is one field: the version. Reconciling the
10
- * fragments is work, and work does not get done by a flag.
9
+ * writes only when told to. Reconciling the fragments is work, and work does not get done by a flag.
11
10
  */
12
11
 
13
- import { readFile, readdir, writeFile } from 'node:fs/promises';
12
+ import { readFile, readdir } from 'node:fs/promises';
14
13
  import { spawnSync } from 'node:child_process';
15
- import { HARNESS, legacyHint } from '../paths.mjs';
16
- import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { HARNESS, describeMove, interruptedMove, legacyHint, moveJournal, movedFrom } from '../paths.mjs';
15
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
17
16
  import { dirname, join, resolve } from 'node:path';
18
- import { composeProject, composedPaths, defaultedSlots, layerRootFor, walk } from './compose.mjs';
17
+ import { REMOVED, composeProject, composedPaths, defaultedSlots, layerRootFor, walk } from './compose.mjs';
19
18
  import { NEEDS, detected } from '../surfaces.mjs';
20
19
  import { filledSlots, projectSlots, referencedVocabulary } from './check.mjs';
21
20
  import { EXPECT_ENV } from '../expected.mjs';
@@ -25,6 +24,9 @@ import { missingFragment, missingWiring, shippedScripts } from '../wiring.mjs';
25
24
  import { runSteps } from '../steps.mjs';
26
25
  import { defaultVocabulary } from '../vocabulary.mjs';
27
26
 
27
+ /** What stops a move from outside it: Ctrl-C, a `kill`, the terminal closing. */
28
+ const SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
29
+
28
30
  /** Banner lines, which are for a person watching and only noise inside a captured log. */
29
31
  const BANNER = /^[\s█╗╔╝║═╚▄▀]*$|harness orchestration ·/;
30
32
 
@@ -75,7 +77,7 @@ function nina(ctx, target, args) {
75
77
  cwd: target,
76
78
  encoding: 'utf8',
77
79
  });
78
- const out = [result.stdout ?? '', result.stderr ?? '']
80
+ const out = [result.stdout ?? '', result.stderr ?? '', result.signal ? `stopped by ${result.signal}` : '']
79
81
  .join('\n')
80
82
  .split('\n')
81
83
  .filter((line) => line.trim() && !BANNER.test(line))
@@ -121,7 +123,7 @@ function shell(target, script) {
121
123
  encoding: 'utf8',
122
124
  env: { ...process.env, PATH: `${join(target, 'node_modules', '.bin')}:${process.env.PATH ?? ''}` },
123
125
  });
124
- const out = [result.stdout ?? '', result.stderr ?? ''].join('\n').split('\n').filter((l) => l.trim()).join('\n');
126
+ const out = [result.stdout ?? '', result.stderr ?? '', result.signal ? `stopped by ${result.signal}` : ''].join('\n').split('\n').filter((l) => l.trim()).join('\n');
125
127
  return { ok: result.status === 0, out, summary: summarise(out) };
126
128
  }
127
129
 
@@ -157,6 +159,74 @@ function summarise(out) {
157
159
  return [...lines].reverse().find((l) => /^\w[\w ]*:/.test(l))?.trim();
158
160
  }
159
161
 
162
+ /**
163
+ * Puts a project back on the version a move started from: the pin, then the files the move composed for the
164
+ * first time removed, a recompose, and every file it could touch written back as it was — the recompose alone
165
+ * would write the old composition over the owner's own edits and call it restored.
166
+ *
167
+ * @param {{root: string}} ctx - CLI context.
168
+ * @param {string} target - The project.
169
+ * @param {string} profilePath - Its profile.
170
+ * @param {object} profile - The profile as read, whose pin is set back.
171
+ * @param {string} version - The version the move started from.
172
+ * @param {string[]} created - Paths the move composed where nothing was.
173
+ * @param {Map<string, Buffer>} saved - Every file the move could touch, as it was.
174
+ * @returns {boolean} Whether the recompose passed.
175
+ */
176
+ function restore(ctx, target, profilePath, profile, version, created, saved) {
177
+ profile.core = version;
178
+ writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
179
+ for (const p of created) rmSync(join(target, p), { force: true });
180
+ const recomposed = nina(ctx, target, ['compose', '--project', target]);
181
+ for (const [p, bytes] of saved) {
182
+ mkdirSync(dirname(join(target, p)), { recursive: true });
183
+ writeFileSync(join(target, p), bytes);
184
+ }
185
+ return recomposed.ok;
186
+ }
187
+
188
+ /**
189
+ * `nina upgrade --abort`: undoes a move that was stopped before it finished, from what it wrote down first.
190
+ *
191
+ * @param {{root: string}} ctx - CLI context.
192
+ * @param {string} target - The project.
193
+ * @param {string} profilePath - Its profile.
194
+ * @param {object} profile - The profile as read.
195
+ * @param {ReturnType<typeof interruptedMove>} stopped - The move it was left in.
196
+ * @returns {number} Process exit code.
197
+ */
198
+ function abort(ctx, target, profilePath, profile, stopped) {
199
+ if (!stopped) {
200
+ console.log(' no upgrade was stopped half-way here — there is nothing to undo.\n');
201
+ return 0;
202
+ }
203
+ if (stopped.unreadable) {
204
+ console.error(
205
+ ` ${describeMove(stopped)} Nothing can be undone from it: set "core" in ${HARNESS}/profile.json back by hand, run \`nina compose\`, ` +
206
+ `and delete ${HARNESS}/upgrade.json.\n`,
207
+ );
208
+ return 1;
209
+ }
210
+ const saved = new Map();
211
+ for (const p of stopped.saved ?? []) {
212
+ const copy = join(movedFrom(target), p);
213
+ if (existsSync(copy)) saved.set(p, readFileSync(copy));
214
+ }
215
+ const lost = (stopped.saved ?? []).filter((p) => !saved.has(p));
216
+ if (lost.length > 0) {
217
+ console.error(` ${lost.length} of the files the move kept are missing from ${HARNESS}/upgrade-saved/, so it cannot be undone exactly: ${lost.slice(0, 5).join(', ')}${lost.length > 5 ? ', …' : ''}\n`);
218
+ return 1;
219
+ }
220
+ const recomposed = restore(ctx, target, profilePath, profile, stopped.from, stopped.created ?? [], saved);
221
+ rmSync(moveJournal(target), { force: true });
222
+ rmSync(movedFrom(target), { recursive: true, force: true });
223
+ console.log(
224
+ ` undid the move to ${stopped.to}: pinned ${stopped.from} again and put ${saved.size} file(s) back as they were.` +
225
+ `${recomposed ? '' : ' Recomposing failed along the way — run `nina compose` yourself.'} Run the move again when ready.\n`,
226
+ );
227
+ return recomposed ? 0 : 1;
228
+ }
229
+
160
230
  /**
161
231
  * `nina upgrade`.
162
232
  *
@@ -176,6 +246,15 @@ export async function upgrade(argv, ctx) {
176
246
  }
177
247
  const profile = JSON.parse(await readFile(profilePath, 'utf8'));
178
248
 
249
+ // A move stopped half-way comes before anything else asked of this command: its pin may already name the
250
+ // version it was moving to, and "already pins" left the tree half-moved for good.
251
+ const stopped = interruptedMove(target);
252
+ if (argv.includes('--abort')) return abort(ctx, target, profilePath, profile, stopped);
253
+ if (stopped) {
254
+ console.error(` ${describeMove(stopped)} \`nina upgrade --abort --project ${target}\` undoes it; then run the move again.\n`);
255
+ return 1;
256
+ }
257
+
179
258
  const to = arg('--to');
180
259
  if (!to) {
181
260
  console.error(' usage: nina upgrade --to <version> [--project <dir>] [--apply]\n');
@@ -354,10 +433,6 @@ export async function upgrade(argv, ctx) {
354
433
  }
355
434
 
356
435
  const previous = profile.core;
357
- const pin = async (version) => {
358
- profile.core = version;
359
- await writeFile(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
360
- };
361
436
 
362
437
  // What was already failing is not this upgrade's doing, and rolling back for it would blame
363
438
  // the move for a problem the project brought with it. Measured before anything is written,
@@ -437,8 +512,12 @@ export async function upgrade(argv, ctx) {
437
512
  const keptEdited = dropped.filter((p) => edited.has(p));
438
513
  // Every file the move can touch, as it is now, so a rollback puts the tree back exactly — the
439
514
  // recompose alone would write the old composition over the owner's own edits and call it restored.
515
+ // That is every path either version composes for any surface, not only the declared ones: compose moves
516
+ // a file gated on an undeclared surface to .nina/removed/, and one left out of here stayed there after a
517
+ // rollback that said the tree was as it was.
518
+ const reachable = [...(await composedPaths(from.dir, offeredBefore)), ...(await composedPaths(onto.dir, available))];
440
519
  const saved = new Map();
441
- for (const p of new Set([...pathsBefore, ...pathsAfter])) {
520
+ for (const p of new Set([...pathsBefore, ...pathsAfter, ...reachable])) {
442
521
  const entry = entryAt(p);
443
522
  if (entry?.isFile()) saved.set(p, readFileSync(join(target, p)));
444
523
  }
@@ -449,7 +528,31 @@ export async function upgrade(argv, ctx) {
449
528
  writeFileSync(copy, saved.get(p) ?? readFileSync(join(target, p)));
450
529
  }
451
530
 
452
- await pin(to);
531
+ // All of it written down before the pin moves, copies first and the journal last, so a journal always has
532
+ // its copies. A move stopped from here on — Ctrl-C, a closed terminal, a crash — left the pin moved, the tree
533
+ // half-composed, and the owner's own edits, held only in this process's memory, gone; now the next run says
534
+ // so, and `--abort` undoes it.
535
+ rmSync(movedFrom(target), { recursive: true, force: true });
536
+ for (const [p, bytes] of saved) {
537
+ mkdirSync(dirname(join(movedFrom(target), p)), { recursive: true });
538
+ writeFileSync(join(movedFrom(target), p), bytes);
539
+ }
540
+ const journal = { from: previous, to, started: new Date().toISOString(), created, saved: [...saved.keys()] };
541
+ writeFileSync(`${moveJournal(target)}.tmp`, `${JSON.stringify(journal, null, 2)}\n`);
542
+ renameSync(`${moveJournal(target)}.tmp`, moveJournal(target));
543
+ // Ctrl-C reaches the step running in a child too, which dies of it, and its step fails: with this process
544
+ // still alive, that is an ordinary failed step, and the move rolls back. Without a listener this process died
545
+ // first, half-way through, with nothing left to roll anything back.
546
+ const hold = () => {};
547
+ for (const s of SIGNALS) process.on(s, hold);
548
+ const settle = () => {
549
+ for (const s of SIGNALS) process.off(s, hold);
550
+ rmSync(moveJournal(target), { force: true });
551
+ rmSync(movedFrom(target), { recursive: true, force: true });
552
+ };
553
+
554
+ profile.core = to;
555
+ writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
453
556
  console.log(` pinned ${to}\n`);
454
557
 
455
558
  const steps = [
@@ -473,12 +576,20 @@ export async function upgrade(argv, ctx) {
473
576
  const { failed } = runSteps(steps);
474
577
 
475
578
  if (!failed) {
579
+ settle();
476
580
  delete process.env.NINA_UPGRADE;
477
581
  console.log(`\n upgrade: ${previous} → ${to} applied and verified.`);
478
582
  if (removable.length > 0) console.log(`\n removed ${removable.length} file(s) ${to} no longer composes: ${removable.join(', ')}`);
479
- if (keptEdited.length > 0) {
480
- console.log(`\n kept ${keptEdited.length} file(s) ${to} no longer composes, because they carry edits of yours — delete them once nothing in them is needed:`);
481
- for (const p of keptEdited) console.log(` ${p}`);
583
+ // A file gated on a surface the project does not declare is moved aside by compose itself, edited or not.
584
+ const stillKept = keptEdited.filter((p) => entryAt(p));
585
+ const setAside = keptEdited.filter((p) => !entryAt(p));
586
+ if (stillKept.length > 0) {
587
+ console.log(`\n kept ${stillKept.length} file(s) ${to} no longer composes, because they carry edits of yours — delete them once nothing in them is needed:`);
588
+ for (const p of stillKept) console.log(` ${p}`);
589
+ }
590
+ if (setAside.length > 0) {
591
+ console.log(`\n moved ${setAside.length} file(s) with edits of yours to ${REMOVED}/, since ${to} composes them only for a surface this project does not declare:`);
592
+ for (const p of setAside) console.log(` ${p}`);
482
593
  }
483
594
  if (occupied.length > 0) {
484
595
  console.log(`\n replaced ${occupied.length} file(s) of yours with what ${to} composes there — the originals are in ${HARNESS}/replaced/:`);
@@ -530,15 +641,10 @@ export async function upgrade(argv, ctx) {
530
641
  }
531
642
 
532
643
  console.log(`\n ✗ ${failed.label} failed — rolling back to ${previous}.\n`);
533
- await pin(previous);
534
- for (const p of created) rmSync(join(target, p), { force: true });
535
- const recomposed = nina(ctx, target, ['compose', '--project', target]);
536
- for (const [p, bytes] of saved) {
537
- mkdirSync(dirname(join(target, p)), { recursive: true });
538
- writeFileSync(join(target, p), bytes);
539
- }
644
+ const recomposed = restore(ctx, target, profilePath, profile, previous, created, saved);
645
+ settle();
540
646
  console.log(
541
- recomposed.ok
647
+ recomposed
542
648
  ? ` rolled back: pinned ${previous} and recomposed. The tree is as it was.\n`
543
649
  : ` rolled back the pin to ${previous}, but recomposing failed — run \`nina compose\` yourself.\n`,
544
650
  );
package/src/guard.mjs CHANGED
@@ -20,21 +20,18 @@
20
20
  import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync } from 'node:fs';
21
21
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { composedPaths } from './commands/compose.mjs';
23
+ import { NOTICE_HEAD, composedPaths, noticeOf } from './commands/compose.mjs';
24
24
  import { HARNESS } from './paths.mjs';
25
25
 
26
+ // Read here and by the suites from here; the test itself lives beside the compiler that writes the notice.
27
+ export { NOTICE_HEAD, noticeOf };
28
+
26
29
  /** The package this runs from: its releases say whether a pinned version ships the guard. */
27
30
  const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
28
31
 
29
32
  /** The tools that change a file, and where each names it. */
30
33
  const EDITS = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' };
31
34
 
32
- /**
33
- * How far into a file the notice is looked for: below a frontmatter block, or a shebang. The compose
34
- * suite holds every composed file's notice inside it.
35
- */
36
- export const NOTICE_HEAD = 60;
37
-
38
35
  /** How much of a file is read to find it: the head, not the whole of a large file on every edit. */
39
36
  const HEAD_BYTES = 16 * 1024;
40
37
 
@@ -69,35 +66,6 @@ function head(path) {
69
66
  }
70
67
  }
71
68
 
72
- /**
73
- * A composed file's notice, as one sentence without its comment syntax, or null when it has none.
74
- *
75
- * @param {string} text - The file.
76
- * @returns {string|null}
77
- */
78
- export function noticeOf(text) {
79
- const lines = String(text).split('\n').slice(0, NOTICE_HEAD);
80
- const at = lines.findIndex((l) => /^(<!--|\/\/) nina:generated/.test(l));
81
- if (at < 0) return null;
82
- const block = [];
83
- if (lines[at].startsWith('//')) {
84
- for (const line of lines.slice(at)) {
85
- if (!line.startsWith('//')) break;
86
- block.push(line.replace(/^\/\/ ?/, ''));
87
- }
88
- } else {
89
- // The notice's own delimiters, taken off by position: this reads text the compiler wrote, it filters
90
- // nothing, and a pattern for it read to a scanner as an HTML sanitizer that misses `--!>`.
91
- for (const line of lines.slice(at)) {
92
- const open = line.startsWith('<!-- ') ? line.slice('<!-- '.length) : line;
93
- const end = open.lastIndexOf('-->');
94
- block.push(end >= 0 ? open.slice(0, end) : open);
95
- if (end >= 0) break;
96
- }
97
- }
98
- return block.map((l) => l.trim()).join(' ');
99
- }
100
-
101
69
  /**
102
70
  * Handles one hook event: the JSON to print, or null to let the call through. Never throws.
103
71
  *
@@ -121,7 +89,9 @@ export async function handleEdit(input, { root, pkg = PACKAGE }) {
121
89
  if (!pinned) return null;
122
90
  const target = isAbsolute(path) ? path : resolve(input.cwd ?? root, path);
123
91
  if (!existsSync(target)) return null;
124
- const rel = relative(realpathSync(root), realpathSync(target)).split(sep).join('/');
92
+ // The native call returns the name as it is on disk: where case is ignored, `claude.md` is CLAUDE.md, and
93
+ // read as spelled it reached no composed path and let the edit through.
94
+ const rel = relative(realpathSync.native(root), realpathSync.native(target)).split(sep).join('/');
125
95
  // Outside the project, or in its own layer — where a change to a composed file belongs.
126
96
  if (rel.startsWith('..') || isAbsolute(rel) || rel.split('/')[0] === HARNESS) return null;
127
97
  const notice = noticeOf(head(target));
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),
package/src/paths.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * project, and losing it is irreversible — so it lives in the user's own directory.
9
9
  */
10
10
 
11
- import { existsSync } from 'node:fs';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
13
13
  import { join, resolve } from 'node:path';
14
14
 
@@ -100,3 +100,52 @@ export function legacyHint(target) {
100
100
  ? ` — this project still has .harness/, which is what ${HARNESS}/ used to be called. Rename it; do not start over.`
101
101
  : '';
102
102
  }
103
+
104
+ /**
105
+ * Where `nina upgrade --apply` writes down a move while it is in flight: the versions it moves between, the
106
+ * files it composes for the first time, and — under {@link movedFrom} — every file it can touch, as it was.
107
+ * It is written before the pin moves and removed once the move is verified or rolled back, so one that is
108
+ * still there is a move that was stopped half-way.
109
+ *
110
+ * @param {string} target - The project.
111
+ * @returns {string}
112
+ */
113
+ export const moveJournal = (target) => join(target, HARNESS, 'upgrade.json');
114
+
115
+ /**
116
+ * Where an in-flight move keeps the files it can touch, as they were before it — at their own paths below.
117
+ *
118
+ * @param {string} target - The project.
119
+ * @returns {string}
120
+ */
121
+ export const movedFrom = (target) => join(target, HARNESS, 'upgrade-saved');
122
+
123
+ /**
124
+ * The move a project was left in when `upgrade --apply` was stopped before it finished, or null when none is.
125
+ *
126
+ * A pin that moved with the composed files half-written looks like hand edits to every check, and the
127
+ * owner's own edits it was about to put back lived only in the stopped process's memory. This is what says
128
+ * so instead, and what `nina upgrade --abort` undoes it from.
129
+ *
130
+ * @param {string} target - The project.
131
+ * @returns {{from: string, to: string, started: string, created: string[], saved: string[]}|{unreadable: string}|null}
132
+ */
133
+ export function interruptedMove(target) {
134
+ try {
135
+ return JSON.parse(readFileSync(moveJournal(target), 'utf8'));
136
+ } catch (error) {
137
+ return error.code === 'ENOENT' ? null : { unreadable: error.message };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * A stopped move, said once for every command that finds one.
143
+ *
144
+ * @param {NonNullable<ReturnType<typeof interruptedMove>>} stopped - The move.
145
+ * @returns {string}
146
+ */
147
+ export function describeMove(stopped) {
148
+ return stopped.unreadable
149
+ ? `An upgrade was stopped before it finished, and its journal ${HARNESS}/upgrade.json cannot be read (${stopped.unreadable}).`
150
+ : `An upgrade from ${stopped.from} to ${stopped.to}, started ${stopped.started}, was stopped before it finished — the pin and the composed files may be half-moved.`;
151
+ }
package/src/prices.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  /** The date the table was read, printed beside every estimate made from it. */
16
16
  export const PRICES_AS_OF = '2026-06-24';
17
17
 
18
- /** model id prefix → $/MTok. Longest prefix wins, so `claude-opus-5-5` is not read as `claude-opus-5`. */
18
+ /** model id → $/MTok. */
19
19
  const PRICES = {
20
20
  'claude-fable-5-1': { input: 10, output: 50, read: 0.25 },
21
21
  'claude-mythos-5-1': { input: 10, output: 50 },
@@ -39,12 +39,12 @@ const PRICES = {
39
39
  * @returns {{input: number, output: number, read: number, write5m: number, write1h: number}|null}
40
40
  */
41
41
  export function priceOf(model) {
42
- const id = String(model ?? '');
43
- const key = Object.keys(PRICES)
44
- .filter((k) => id === k || id.startsWith(`${k}-`) || id.startsWith(`${k}[`))
45
- .sort((a, b) => b.length - a.length)[0];
46
- if (!key) return null;
47
- const p = PRICES[key];
42
+ // The id itself, less the two suffixes a transcript adds to one: a context window (`[1m]`) and a snapshot
43
+ // date (`-20251001`). Matched by prefix, a model newer than the table was priced as the one before it —
44
+ // `claude-opus-5-6` as Opus 5, at five times its cache reads.
45
+ const id = String(model ?? '').replace(/\[[^\]]*\]$/, '').replace(/-\d{8}$/, '');
46
+ const p = Object.hasOwn(PRICES, id) ? PRICES[id] : null;
47
+ if (!p) return null;
48
48
  return { input: p.input, output: p.output, read: p.read ?? p.input * 0.1, write5m: p.input * 1.25, write1h: p.input * 2 };
49
49
  }
50
50