@xhulz/nina 0.31.0 → 0.33.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 (261) hide show
  1. package/README.md +7 -5
  2. package/bin/nina.mjs +4 -0
  3. package/package.json +2 -1
  4. package/releases/0.32.0/core/GAPS.md +125 -0
  5. package/releases/0.32.0/core/answered.json +29 -0
  6. package/releases/0.32.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  7. package/releases/0.32.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  8. package/releases/0.32.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  9. package/releases/0.32.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  10. package/releases/0.32.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  11. package/releases/0.32.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  12. package/releases/0.32.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  13. package/releases/0.32.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  14. package/releases/0.32.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  15. package/releases/0.32.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  16. package/releases/0.32.0/core/tree/.claude/agents/architect.md +126 -0
  17. package/releases/0.32.0/core/tree/.claude/agents/dba.md +110 -0
  18. package/releases/0.32.0/core/tree/.claude/agents/devops.md +114 -0
  19. package/releases/0.32.0/core/tree/.claude/agents/implementer.md +155 -0
  20. package/releases/0.32.0/core/tree/.claude/agents/integration-tester.md +168 -0
  21. package/releases/0.32.0/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.32.0/core/tree/.claude/agents/qa.md +148 -0
  23. package/releases/0.32.0/core/tree/.claude/agents/reviewer.md +183 -0
  24. package/releases/0.32.0/core/tree/.claude/agents/secops.md +121 -0
  25. package/releases/0.32.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  26. package/releases/0.32.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  27. package/releases/0.32.0/core/tree/.claude/agents-overview.md +64 -0
  28. package/releases/0.32.0/core/tree/.claude/frontend.md +10 -0
  29. package/releases/0.32.0/core/tree/.claude/graph.md +75 -0
  30. package/releases/0.32.0/core/tree/.claude/patterns.md +213 -0
  31. package/releases/0.32.0/core/tree/.claude/pills/README.md +146 -0
  32. package/releases/0.32.0/core/tree/.claude/pipeline.md +101 -0
  33. package/releases/0.32.0/core/tree/.claude/retrieval.md +103 -0
  34. package/releases/0.32.0/core/tree/.claude/router.md +195 -0
  35. package/releases/0.32.0/core/tree/.claude/templates/integration.md +45 -0
  36. package/releases/0.32.0/core/tree/CLAUDE.md +160 -0
  37. package/releases/0.32.0/core/tree/scripts/cost-watch.mjs +18 -0
  38. package/releases/0.32.0/core/tree/scripts/edit-guard.mjs +16 -0
  39. package/releases/0.32.0/core/tree/scripts/harness-check.mjs +71 -0
  40. package/releases/0.32.0/core/tree/scripts/loop-gate.mjs +19 -0
  41. package/releases/0.32.0/core/vocabulary.json +13 -0
  42. package/releases/0.32.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  43. package/releases/0.32.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  44. package/releases/0.32.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  45. package/releases/0.32.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  46. package/releases/0.32.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  47. package/releases/0.32.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  48. package/releases/0.32.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  49. package/releases/0.32.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  50. package/releases/0.32.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  51. package/releases/0.32.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  52. package/releases/0.32.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  53. package/releases/0.32.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  54. package/releases/0.32.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  55. package/releases/0.32.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  56. package/releases/0.32.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  57. package/releases/0.32.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  58. package/releases/0.32.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  59. package/releases/0.32.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  60. package/releases/0.32.0/surfaces/db/tree/.claude/graph.md +8 -0
  61. package/releases/0.32.0/surfaces/db/tree/.claude/patterns.md +19 -0
  62. package/releases/0.32.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  63. package/releases/0.32.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  64. package/releases/0.32.0/surfaces/db/tree/.claude/router.md +4 -0
  65. package/releases/0.32.0/surfaces/db/tree/CLAUDE.md +17 -0
  66. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  67. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  68. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  69. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  70. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  71. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  72. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  73. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  74. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  75. package/releases/0.32.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  76. package/releases/0.32.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  77. package/releases/0.32.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  78. package/releases/0.32.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  79. package/releases/0.32.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  80. package/releases/0.32.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  81. package/releases/0.32.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  82. package/releases/0.32.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  83. package/releases/0.32.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  84. package/releases/0.32.0/surfaces/frontend/tree/.claude/router.md +14 -0
  85. package/releases/0.32.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  86. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  87. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  88. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  89. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  90. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  91. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  92. package/releases/0.32.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  93. package/releases/0.32.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  94. package/releases/0.32.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  95. package/releases/0.32.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  96. package/releases/0.32.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  97. package/releases/0.32.0/surfaces/integrations/tree/.claude/router.md +6 -0
  98. package/releases/0.32.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  99. package/releases/0.32.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  100. package/releases/0.32.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  101. package/releases/0.32.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  102. package/releases/0.32.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  103. package/releases/0.32.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  104. package/releases/0.32.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  105. package/releases/0.32.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  106. package/releases/0.32.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  107. package/releases/0.32.0/surfaces/money/tree/.claude/graph.md +2 -0
  108. package/releases/0.32.0/surfaces/money/tree/.claude/patterns.md +49 -0
  109. package/releases/0.32.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  110. package/releases/0.32.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  111. package/releases/0.32.0/surfaces/money/tree/CLAUDE.md +11 -0
  112. package/releases/0.32.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  113. package/releases/0.32.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  114. package/releases/0.32.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  115. package/releases/0.32.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  116. package/releases/0.32.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  117. package/releases/0.32.0/surfaces/pii/tree/CLAUDE.md +5 -0
  118. package/releases/0.32.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  119. package/releases/0.32.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  120. package/releases/0.32.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  121. package/releases/0.32.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  122. package/releases/0.32.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  123. package/releases/0.32.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  124. package/releases/0.32.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  125. package/releases/0.32.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  126. package/releases/0.33.0/core/GAPS.md +125 -0
  127. package/releases/0.33.0/core/answered.json +29 -0
  128. package/releases/0.33.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  129. package/releases/0.33.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  130. package/releases/0.33.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  131. package/releases/0.33.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  132. package/releases/0.33.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  133. package/releases/0.33.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  134. package/releases/0.33.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  135. package/releases/0.33.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  136. package/releases/0.33.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  137. package/releases/0.33.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  138. package/releases/0.33.0/core/tree/.claude/agents/architect.md +127 -0
  139. package/releases/0.33.0/core/tree/.claude/agents/dba.md +110 -0
  140. package/releases/0.33.0/core/tree/.claude/agents/devops.md +114 -0
  141. package/releases/0.33.0/core/tree/.claude/agents/implementer.md +156 -0
  142. package/releases/0.33.0/core/tree/.claude/agents/integration-tester.md +168 -0
  143. package/releases/0.33.0/core/tree/.claude/agents/planner.md +132 -0
  144. package/releases/0.33.0/core/tree/.claude/agents/qa.md +148 -0
  145. package/releases/0.33.0/core/tree/.claude/agents/reviewer.md +183 -0
  146. package/releases/0.33.0/core/tree/.claude/agents/secops.md +121 -0
  147. package/releases/0.33.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  148. package/releases/0.33.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  149. package/releases/0.33.0/core/tree/.claude/agents-overview.md +64 -0
  150. package/releases/0.33.0/core/tree/.claude/frontend.md +10 -0
  151. package/releases/0.33.0/core/tree/.claude/graph.md +75 -0
  152. package/releases/0.33.0/core/tree/.claude/patterns.md +213 -0
  153. package/releases/0.33.0/core/tree/.claude/pills/README.md +146 -0
  154. package/releases/0.33.0/core/tree/.claude/pipeline.md +101 -0
  155. package/releases/0.33.0/core/tree/.claude/retrieval.md +103 -0
  156. package/releases/0.33.0/core/tree/.claude/router.md +198 -0
  157. package/releases/0.33.0/core/tree/.claude/templates/integration.md +45 -0
  158. package/releases/0.33.0/core/tree/CLAUDE.md +161 -0
  159. package/releases/0.33.0/core/tree/scripts/cost-watch.mjs +18 -0
  160. package/releases/0.33.0/core/tree/scripts/edit-guard.mjs +16 -0
  161. package/releases/0.33.0/core/tree/scripts/harness-check.mjs +71 -0
  162. package/releases/0.33.0/core/tree/scripts/loop-gate.mjs +19 -0
  163. package/releases/0.33.0/core/vocabulary.json +13 -0
  164. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  165. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  166. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  167. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  168. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  169. package/releases/0.33.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  170. package/releases/0.33.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  171. package/releases/0.33.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  172. package/releases/0.33.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  173. package/releases/0.33.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  174. package/releases/0.33.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  175. package/releases/0.33.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  176. package/releases/0.33.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  177. package/releases/0.33.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  178. package/releases/0.33.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  179. package/releases/0.33.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  180. package/releases/0.33.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  181. package/releases/0.33.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  182. package/releases/0.33.0/surfaces/db/tree/.claude/graph.md +8 -0
  183. package/releases/0.33.0/surfaces/db/tree/.claude/patterns.md +19 -0
  184. package/releases/0.33.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  185. package/releases/0.33.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  186. package/releases/0.33.0/surfaces/db/tree/.claude/router.md +4 -0
  187. package/releases/0.33.0/surfaces/db/tree/CLAUDE.md +17 -0
  188. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  189. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  190. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  191. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  192. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  193. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  194. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  195. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  196. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  197. package/releases/0.33.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  198. package/releases/0.33.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  199. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  200. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  201. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  202. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  203. package/releases/0.33.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  204. package/releases/0.33.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  205. package/releases/0.33.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  206. package/releases/0.33.0/surfaces/frontend/tree/.claude/router.md +14 -0
  207. package/releases/0.33.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  208. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  209. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  210. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  211. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  212. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  213. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  214. package/releases/0.33.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  215. package/releases/0.33.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  216. package/releases/0.33.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  217. package/releases/0.33.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  218. package/releases/0.33.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  219. package/releases/0.33.0/surfaces/integrations/tree/.claude/router.md +6 -0
  220. package/releases/0.33.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  221. package/releases/0.33.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  222. package/releases/0.33.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  223. package/releases/0.33.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  224. package/releases/0.33.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  225. package/releases/0.33.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  226. package/releases/0.33.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  227. package/releases/0.33.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  228. package/releases/0.33.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  229. package/releases/0.33.0/surfaces/money/tree/.claude/graph.md +2 -0
  230. package/releases/0.33.0/surfaces/money/tree/.claude/patterns.md +49 -0
  231. package/releases/0.33.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  232. package/releases/0.33.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  233. package/releases/0.33.0/surfaces/money/tree/CLAUDE.md +11 -0
  234. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  235. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  236. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  237. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  238. package/releases/0.33.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  239. package/releases/0.33.0/surfaces/pii/tree/CLAUDE.md +5 -0
  240. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  241. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  242. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  243. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  244. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  245. package/releases/0.33.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  246. package/releases/0.33.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  247. package/releases/0.33.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  248. package/src/commands/check.mjs +39 -7
  249. package/src/commands/compose.mjs +5 -0
  250. package/src/commands/learn.mjs +27 -7
  251. package/src/commands/pills.mjs +54 -2
  252. package/src/commands/runs.mjs +129 -0
  253. package/src/commands/snapshot.mjs +80 -30
  254. package/src/commands/stats.mjs +70 -21
  255. package/src/commands/status.mjs +150 -0
  256. package/src/commands/upgrade.mjs +18 -4
  257. package/src/cost.mjs +141 -0
  258. package/src/detectors.mjs +84 -14
  259. package/src/expected.mjs +4 -3
  260. package/src/transcripts.mjs +27 -1
  261. package/src/wiring.mjs +3 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `nina status` — one screen for one project: what it pins and what is installed, whether a newer release
3
+ * is out, whether its hooks are wired and its gate holds, what `check` says, how fresh its measured
4
+ * history is and what the last week cost, and where its learning stands.
5
+ *
6
+ * Each of these had its own command, and the question a person brings — is this project's harness in order,
7
+ * and is it current — needed six of them. Every line here reads what those commands read, or asks them, so
8
+ * the screen cannot disagree with them.
9
+ *
10
+ * nina status [--project <dir>] [--offline]
11
+ */
12
+
13
+ import { spawnSync } from 'node:child_process';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import { join, resolve } from 'node:path';
17
+ import { HARNESS } from '../paths.mjs';
18
+ import { costOf } from '../prices.mjs';
19
+ import { runOf } from '../transcripts.mjs';
20
+ import { missingWiring, shippedScripts } from '../wiring.mjs';
21
+ import { amber, bold, dim, green, pink } from '../look.mjs';
22
+ import { byVersion, layerRootFor } from './compose.mjs';
23
+ import { filedRequests } from './learn.mjs';
24
+ import { frontmatter, pillFiles } from './pills.mjs';
25
+ import { storedRecords } from './snapshot.mjs';
26
+
27
+ /** How long the registry gets to say which version is newest; a status is not worth waiting on. */
28
+ const REGISTRY_MS = 3000;
29
+
30
+ /**
31
+ * The version of NINA the project has installed, or null.
32
+ *
33
+ * @param {string} target
34
+ * @returns {string|null}
35
+ */
36
+ function installedVersion(target) {
37
+ try {
38
+ const path = createRequire(join(target, 'package.json')).resolve('@xhulz/nina/package.json');
39
+ return JSON.parse(readFileSync(path, 'utf8')).version ?? null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * The newest version on the registry, or null when it cannot be asked.
47
+ *
48
+ * @returns {Promise<string|null>}
49
+ */
50
+ async function latestVersion() {
51
+ try {
52
+ const answer = await fetch('https://registry.npmjs.org/@xhulz%2Fnina/latest', { signal: AbortSignal.timeout(REGISTRY_MS) });
53
+ return answer.ok ? ((await answer.json()).version ?? null) : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The last line a command printed, and whether it passed: what `check` and `gate --selftest` conclude.
61
+ *
62
+ * @param {string} bin - This install's entry point.
63
+ * @param {string[]} args
64
+ * @returns {{ok: boolean, line: string}}
65
+ */
66
+ function verdictOf(bin, args) {
67
+ const run = spawnSync(process.execPath, [bin, ...args, '--quiet'], { encoding: 'utf8', timeout: 120_000 });
68
+ const lines = `${run.stdout ?? ''}`.split('\n').map((l) => l.trim()).filter(Boolean);
69
+ return { ok: run.status === 0, line: lines.at(-1) ?? `exit ${run.status}` };
70
+ }
71
+
72
+ /** How long ago a moment was, in words. */
73
+ function ago(iso, now) {
74
+ const ms = now - Date.parse(iso);
75
+ if (!Number.isFinite(ms)) return 'at an unknown time';
76
+ const h = ms / 3_600_000;
77
+ return h < 1 ? `${Math.max(1, Math.round(ms / 60_000))} min ago` : h < 48 ? `${Math.round(h)}h ago` : `${Math.round(h / 24)} days ago`;
78
+ }
79
+
80
+ /**
81
+ * Runs the command.
82
+ *
83
+ * @param {string[]} argv
84
+ * @param {{root: string}} ctx
85
+ * @returns {Promise<number>}
86
+ */
87
+ export async function status(argv, ctx) {
88
+ const target = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : '.');
89
+ const profilePath = join(target, HARNESS, 'profile.json');
90
+ if (!existsSync(profilePath)) {
91
+ console.error(` no ${HARNESS}/profile.json under ${target} — run it inside a project, or name one with --project\n`);
92
+ return 1;
93
+ }
94
+ const profile = JSON.parse(readFileSync(profilePath, 'utf8'));
95
+ const bin = join(ctx.root, 'bin', 'nina.mjs');
96
+ const now = Date.now();
97
+ const row = (label, text) => console.log(` ${pink(label.padEnd(10))} ${text}`);
98
+ let attention = 0;
99
+ const warn = (text) => {
100
+ attention += 1;
101
+ return amber(text);
102
+ };
103
+
104
+ const installed = installedVersion(target);
105
+ const latest = argv.includes('--offline') ? null : await latestVersion();
106
+ const pinned = profile.core;
107
+ const behind = latest && pinned !== 'dev' && byVersion(pinned, latest) < 0;
108
+ row(
109
+ 'version',
110
+ `pins ${bold(pinned)} · installed ${installed ? bold(installed) : warn('none — pnpm add -D -E @xhulz/nina')}` +
111
+ (latest ? ` · newest ${bold(latest)}${behind ? warn(` — \`nina upgrade --to ${latest}\` says what the move costs`) : green(' — current')}` : dim(' · newest unknown (offline)')),
112
+ );
113
+
114
+ const layers = layerRootFor(ctx.root, pinned);
115
+ if (!layers.error) {
116
+ const unwired = await missingWiring(target, await shippedScripts(layers.dir, profile.surfaces ?? []));
117
+ row('hooks', unwired.length === 0 ? green('every hook its release needs is wired') : warn(`${unwired.length} missing — \`nina wire --apply\` merges them`));
118
+ } else {
119
+ row('hooks', warn(layers.error));
120
+ }
121
+ const gate = verdictOf(bin, ['gate', '--selftest', '--project', target]);
122
+ row('gate', gate.ok ? green(gate.line) : warn(`${gate.line} — \`nina gate --selftest\` says what`));
123
+ const check = verdictOf(bin, ['check', '--project', target]);
124
+ row('check', check.ok ? green(check.line) : warn(`${check.line} — \`nina check\` lists them`));
125
+
126
+ const records = storedRecords(target) ?? [];
127
+ if (records.length === 0) {
128
+ row('measured', dim('nothing yet — `nina snapshot` captures it'));
129
+ } else {
130
+ const latestAt = records.map((r) => String(r.result_ts ?? r.ts)).sort().at(-1);
131
+ const week = new Date(now - 7 * 86_400_000).toISOString();
132
+ const recent = records.filter((r) => String(r.ts) >= week);
133
+ const cost = recent.reduce((a, r) => a + ((r.tokens && costOf(r.tokens, r.usage_model)) || 0), 0);
134
+ row(
135
+ 'measured',
136
+ `${new Set(records.map(runOf)).size} runs in ${records.length} rounds, the latest ${ago(latestAt, now)} · last 7 days: ` +
137
+ `${new Set(recent.map(runOf)).size} runs, ${bold(`$${cost.toFixed(cost >= 100 ? 0 : 2)}`)} at API list prices — \`nina runs\` itemizes it`,
138
+ );
139
+ }
140
+
141
+ const pillsDir = join(target, '.claude', 'pills');
142
+ const pills = existsSync(pillsDir) ? await pillFiles(pillsDir) : [];
143
+ let active = 0;
144
+ for (const p of pills) if (frontmatter(readFileSync(p.path, 'utf8'))?.status !== 'retired') active += 1;
145
+ const open = (await filedRequests(target)).filter((r) => r.status === 'open').length;
146
+ row('learning', `${pills.length} pill(s), ${active} active · ${open} request(s) open with the harness${open ? dim(' — `nina upgrade` to the release that answers one closes it') : ''}`);
147
+
148
+ console.log(attention === 0 ? `\n status: ${green('in order')}\n` : `\n status: ${attention} thing(s) to look at\n`);
149
+ return attention === 0 ? 0 : 1;
150
+ }
@@ -20,6 +20,7 @@ import { NEEDS, detected } from '../surfaces.mjs';
20
20
  import { filledSlots, projectSlots, referencedVocabulary } from './check.mjs';
21
21
  import { EXPECT_ENV } from '../expected.mjs';
22
22
  import { closeAnswered } from './learn.mjs';
23
+ import { tidyRetired } from './pills.mjs';
23
24
  import { missingFragment, missingWiring, shippedScripts } from '../wiring.mjs';
24
25
  import { runSteps } from '../steps.mjs';
25
26
  import { defaultVocabulary } from '../vocabulary.mjs';
@@ -36,7 +37,8 @@ const BANNER = /^[\s█╗╔╝║═╚▄▀]*$|harness orchestration ·/;
36
37
  * fault — everything else it finds still rolls the move back.
37
38
  *
38
39
  * @param {string} target - The project.
39
- * @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}}`.
40
42
  * @returns {{label: string, args: string[]}[]}
41
43
  */
42
44
  function validationsFor(target, newSlots = []) {
@@ -235,11 +237,16 @@ export async function upgrade(argv, ctx) {
235
237
  const declared = profile.vocabulary ?? {};
236
238
  // A name the new release supplies a default for is not owed: it composes as the default until declared.
237
239
  const defaultsAfter = defaultVocabulary(onto.dir);
238
- const needed = [...vocabAfter].filter((v) => (!(v in declared) && !(v in defaultsAfter)) || declared[v] === null).sort();
240
+ const waits = (key) => typeof profile.deferred?.[key] === 'string' && profile.deferred[key].trim() !== '';
241
+ const needed = [...vocabAfter].filter((v) => ((!(v in declared) && !(v in defaultsAfter)) || declared[v] === null) && !waits(v)).sort();
239
242
  const unused = Object.keys(declared).filter((v) => !vocabAfter.has(v)).sort();
240
243
  // A slot the new release fills itself is not asked of the project: it composes as the release wrote it.
241
244
  const defaultedAfter = await defaultedSlots(onto.dir);
242
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}}}`)];
243
250
  const stranded = [...filled].filter((s) => !slotsAfter.has(s)).sort();
244
251
 
245
252
  console.log(` ${profile.core} → ${to}\n`);
@@ -355,7 +362,7 @@ export async function upgrade(argv, ctx) {
355
362
  // What was already failing is not this upgrade's doing, and rolling back for it would blame
356
363
  // the move for a problem the project brought with it. Measured before anything is written,
357
364
  // so the only rollback is for something that WAS passing and now is not.
358
- const VALIDATIONS = validationsFor(target, newSlots);
365
+ const VALIDATIONS = validationsFor(target, awaited);
359
366
  const DETECTORS = "running the project's own detectors";
360
367
  const hasDetectors = await declares(target, 'harness:check');
361
368
 
@@ -414,7 +421,7 @@ export async function upgrade(argv, ctx) {
414
421
  // `harness:check` — an npm script whose detector list this command does not own and cannot
415
422
  // pass an argument to. The environment is what crosses that boundary, and setting it once
416
423
  // means a reporter added to the chain later is told without anyone remembering to tell it.
417
- if (newSlots.length > 0) process.env[EXPECT_ENV] = newSlots.join(',');
424
+ if (awaited.length > 0) process.env[EXPECT_ENV] = awaited.join(',');
418
425
 
419
426
  // Compose writes and never deletes. So the files the new version stops composing are removed right
420
427
  // after it composes — before anything is verified, so what is verified is what stays — unless they
@@ -499,6 +506,9 @@ export async function upgrade(argv, ctx) {
499
506
  }
500
507
  }
501
508
  }
509
+ // Retired pills still where agents open them, from before they had a place of their own.
510
+ const shelved = await tidyRetired(target).catch(() => []);
511
+ if (shelved.length > 0) console.log(`\n moved ${shelved.length} retired pill(s) to .claude/pills/retired/, where no agent opens them: ${shelved.join(', ')}`);
502
512
  const lessons = nina(ctx, target, ['learn', '--project', target, '--check']);
503
513
  if (!lessons.ok && lessons.out.trim()) {
504
514
  console.log('\n the learning cycle has something to act on — not caused by this move, now visible:');
@@ -511,6 +521,10 @@ export async function upgrade(argv, ctx) {
511
521
  console.log(`\n ${newSlots.length} project slot(s) still to fill — only this project can write them:`);
512
522
  for (const s of newSlots) console.log(` ${s}`);
513
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
+ }
514
528
  console.log('');
515
529
  return 0;
516
530
  }
package/src/cost.mjs ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The cost watch: tells a subagent, while it runs, what its own context has come to cost.
3
+ *
4
+ * What a run spends is mostly its context read again: every turn re-reads everything the run has built,
5
+ * so its cost grows faster than its work. Over the first new project's first days, three quarters of the
6
+ * implementer's spend was cache read, a median 36M tokens a round and 159M at most, and nothing said so
7
+ * until `stats` read the transcripts afterwards. This runs on every tool call a subagent makes — Claude Code
8
+ * hands a PostToolUse hook's `additionalContext` to the subagent that made the call — and, each time the
9
+ * tokens its run has re-read cross a threshold and then each doubling of it, tells the run what each turn
10
+ * now costs and what to do about it, and tells the person in one line.
11
+ *
12
+ * It reads only what the run's transcript gained since its last call, keeps counts per message and
13
+ * nothing else, under the project's gate directory, and lets every call through if anything goes wrong.
14
+ */
15
+
16
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { projectGateDir } from './gate.mjs';
19
+
20
+ /** Where a run's counts are kept between calls, one file per agent. */
21
+ const stateDir = (root) => join(projectGateDir(root), 'cost');
22
+
23
+ /** Runs' counts are dropped this long after their last call: a run does not come back after a week. */
24
+ const KEEP_MS = 7 * 86_400_000;
25
+
26
+ /**
27
+ * Reads what a file gained after an offset, up to its last complete line.
28
+ *
29
+ * @param {string} path - The transcript.
30
+ * @param {number} from - The offset already read.
31
+ * @returns {{text: string, to: number}}
32
+ */
33
+ function gained(path, from) {
34
+ const size = statSync(path).size;
35
+ if (size <= from) return { text: '', to: size < from ? 0 : from };
36
+ const buffer = Buffer.alloc(size - from);
37
+ const fd = openSync(path, 'r');
38
+ try {
39
+ readSync(fd, buffer, 0, buffer.length, from);
40
+ } finally {
41
+ closeSync(fd);
42
+ }
43
+ const end = buffer.lastIndexOf(0x0a);
44
+ if (end === -1) return { text: '', to: from };
45
+ return { text: buffer.subarray(0, end + 1).toString('utf8'), to: from + end + 1 };
46
+ }
47
+
48
+ /**
49
+ * What one call of the watch says, or null: the counts are brought up to date from the run's transcript,
50
+ * and a warning is due each time the total re-read crosses `warnAt`, then twice it, four times it, and on.
51
+ *
52
+ * @param {object} input - The PostToolUse hook's input.
53
+ * @param {{root: string, warnAt: number, now?: number}} options
54
+ * @returns {object|null} The hook's answer.
55
+ */
56
+ export function watch(input, { root, warnAt, now = Date.now() }) {
57
+ // Only inside a subagent, which is where `agent_id` is set; and an id that could not be a file name is not one.
58
+ if (input?.hook_event_name !== 'PostToolUse' || !/^[A-Za-z0-9_-]{1,64}$/.test(String(input.agent_id ?? ''))) return null;
59
+ if (typeof input.transcript_path !== 'string' || typeof input.session_id !== 'string' || !(warnAt > 0)) return null;
60
+ const transcript = join(dirname(input.transcript_path), input.session_id, 'subagents', `agent-${input.agent_id}.jsonl`);
61
+ if (!existsSync(transcript)) return null;
62
+ const dir = stateDir(root);
63
+ const file = join(dir, `${input.agent_id}.json`);
64
+ let state = { offset: 0, read: {}, warned: 0 };
65
+ try {
66
+ state = { ...state, ...JSON.parse(readFileSync(file, 'utf8')) };
67
+ } catch {
68
+ // A run's first call, or its counts lost: read its transcript from the start.
69
+ }
70
+ const { text, to } = gained(transcript, state.offset);
71
+ let last = null;
72
+ for (const line of text.split('\n')) {
73
+ if (!line.includes('"cache_read_input_tokens"')) continue;
74
+ try {
75
+ const message = JSON.parse(line)?.message;
76
+ const read = message?.usage?.cache_read_input_tokens;
77
+ // A streamed message is written once per content block under one id: its last copy counts.
78
+ if (message?.id && typeof read === 'number') {
79
+ state.read[message.id] = read;
80
+ last = read;
81
+ }
82
+ } catch {
83
+ // A torn line loses one message's count.
84
+ }
85
+ }
86
+ state.offset = to;
87
+ state.at = now;
88
+ const total = Object.values(state.read).reduce((a, b) => a + b, 0);
89
+ let crossed = state.warned;
90
+ while (total >= warnAt * 2 ** crossed) crossed += 1;
91
+ const due = crossed > state.warned;
92
+ state.warned = crossed;
93
+ mkdirSync(dir, { recursive: true });
94
+ writeFileSync(file, JSON.stringify(state));
95
+ if (!due) return null;
96
+ const m = (n) => `${Math.round(n / 1e6)}M`;
97
+ const size = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1000)}k`);
98
+ const turn = last === null ? '' : `, and each turn now re-reads about ${size(last)} tokens more`;
99
+ return {
100
+ systemMessage: `NINA: ${input.agent_type ?? 'a subagent'} ${input.agent_id.slice(0, 8)} has re-read ${m(total)} tokens of its own context${turn}.`,
101
+ hookSpecificOutput: {
102
+ hookEventName: 'PostToolUse',
103
+ additionalContext:
104
+ `NINA cost watch: this run has re-read ${m(total)} tokens of its own context${turn}; every further turn costs more than the last. ` +
105
+ 'If what is left is more than one run should carry, stop where your work is whole, hand back what you have, and name what is left, so the orchestrator can send it as its own step. Otherwise finish without widening the work.',
106
+ },
107
+ };
108
+ }
109
+
110
+ /** Drops the counts of runs not heard from in a week. */
111
+ function prune(root, now) {
112
+ const dir = stateDir(root);
113
+ for (const name of existsSync(dir) ? readdirSync(dir) : []) {
114
+ try {
115
+ if (now - statSync(join(dir, name)).mtimeMs > KEEP_MS) rmSync(join(dir, name), { force: true });
116
+ } catch {
117
+ // Another call's, going as this one looks.
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The hook: reads the event from stdin, answers on stdout when a warning is due, and exits 0 whatever
124
+ * happens — a hook that fails must not stop the run it watches.
125
+ *
126
+ * @param {{root: string, warnAt: number, stdin?: NodeJS.ReadableStream, stdout?: NodeJS.WritableStream}} options
127
+ * @returns {Promise<number>}
128
+ */
129
+ export async function runCostWatch({ root, warnAt, stdin = process.stdin, stdout = process.stdout }) {
130
+ try {
131
+ const chunks = [];
132
+ for await (const chunk of stdin) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
133
+ const now = Date.now();
134
+ const answer = watch(JSON.parse(Buffer.concat(chunks).toString('utf8')), { root, warnAt, now });
135
+ if (answer) stdout.write(`${JSON.stringify(answer)}\n`);
136
+ if (Math.random() < 0.01) prune(root, now);
137
+ } catch {
138
+ // Fails open: the run goes on unwatched.
139
+ }
140
+ return 0;
141
+ }
package/src/detectors.mjs CHANGED
@@ -24,7 +24,7 @@ import { spawnSync } from 'node:child_process';
24
24
  import { createHash } from 'node:crypto';
25
25
  import { hookStateDir, slugFor } from './paths.mjs';
26
26
  import { CHECK } from './wiring.mjs';
27
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
27
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
29
 
30
30
  /**
@@ -157,20 +157,82 @@ const stopState = (root) => join(hookStateDir(), `${slugFor(root)}.stop.json`);
157
157
  /** Sessions remembered per project; the oldest is forgotten past this. */
158
158
  const SESSIONS_KEPT = 20;
159
159
 
160
+ /** What Claude Code wrote to the hook's stdin, read once: stdin can be read only once. */
161
+ let input;
162
+
163
+ /**
164
+ * The payload Claude Code writes to a hook's stdin, or an empty object run by hand.
165
+ *
166
+ * @returns {{session_id?: string, prompt?: string, transcript_path?: string}}
167
+ */
168
+ function hookInput() {
169
+ if (input === undefined) {
170
+ try {
171
+ input = process.stdin.isTTY ? {} : (JSON.parse(readFileSync(0, 'utf8')) ?? {});
172
+ } catch {
173
+ input = {};
174
+ }
175
+ }
176
+ return input;
177
+ }
178
+
160
179
  /**
161
- * The session a Stop hook runs in, from the payload Claude Code writes to its stdin. What was said is
162
- * kept per session: kept per project, a second session open on the same project found the finding
163
- * already told — to the first session's person — and told its own nothing.
180
+ * The session a hook runs in. What was said is kept per session: kept per project, a second session open
181
+ * on the same project found the finding already told — to the first session's person — and told its own
182
+ * nothing.
164
183
  *
165
184
  * @returns {string}
166
185
  */
167
- function hookSession() {
168
- if (process.stdin.isTTY) return '-';
186
+ const hookSession = () => String(hookInput().session_id ?? '-');
187
+
188
+ /**
189
+ * Whether the prompt the hook runs before is a subagent's completion notification rather than something a
190
+ * person said. Claude Code submits each as a prompt, and every one ran the detectors — a second or more
191
+ * before the model read it — and put their findings beside it: one session of the first new project did so
192
+ * 181 times over 23 of its owner's messages.
193
+ *
194
+ * @returns {boolean}
195
+ */
196
+ const notification = () => String(hookInput().prompt ?? '').trimStart().startsWith('<task-notification>');
197
+
198
+ /** How much of a transcript's end is read for a compaction since the last hand-over: it is the latest line. */
199
+ const TAIL = 1024 * 1024;
200
+
201
+ /**
202
+ * Whether the session was compacted after a moment: a compaction drops what the model was handed, so what it
203
+ * was handed is handed again.
204
+ *
205
+ * @param {unknown} path - The session's transcript.
206
+ * @param {unknown} since - An ISO time.
207
+ * @returns {boolean}
208
+ */
209
+ function compactedSince(path, since) {
210
+ if (typeof path !== 'string' || typeof since !== 'string' || !existsSync(path)) return false;
211
+ let text;
169
212
  try {
170
- return String(JSON.parse(readFileSync(0, 'utf8'))?.session_id ?? '-');
213
+ const size = statSync(path).size;
214
+ const length = Math.min(size, TAIL);
215
+ const buffer = Buffer.alloc(length);
216
+ const fd = openSync(path, 'r');
217
+ try {
218
+ readSync(fd, buffer, 0, length, size - length);
219
+ } finally {
220
+ closeSync(fd);
221
+ }
222
+ text = buffer.toString('utf8');
171
223
  } catch {
172
- return '-';
224
+ return false;
173
225
  }
226
+ for (const line of text.split('\n')) {
227
+ if (!line.includes('"compact_boundary"')) continue;
228
+ try {
229
+ const row = JSON.parse(line);
230
+ if (row?.subtype === 'compact_boundary' && String(row.timestamp) > since) return true;
231
+ } catch {
232
+ // A torn line at the head of the tail.
233
+ }
234
+ }
235
+ return false;
174
236
  }
175
237
 
176
238
  /** What was said, per session, for a project. */
@@ -237,8 +299,9 @@ function reportOf(drift, errored, missing, detectors) {
237
299
  /**
238
300
  * What the prompt hook hands the model, or null for nothing. A finding not handed over at the last
239
301
  * message goes in full, with the instruction to act on it or say it is pending. One that was, and has
240
- * not changed, goes as its summary, with the instruction not to say it again: told before every message
241
- * to say it was pending, the model closed every answer with the same line.
302
+ * not changed, goes as its summary beside a new one, with the instruction not to say it again — told
303
+ * before every message to say it was pending, the model closed every answer with the same line — and on
304
+ * its own not at all: the model has it from the first time, until a compaction drops it.
242
305
  *
243
306
  * @returns {string|null}
244
307
  */
@@ -317,6 +380,7 @@ function remember(root, session, change) {
317
380
  */
318
381
  export function runDetectors(detectors, options) {
319
382
  const { root, hook = false, context = false } = options;
383
+ if (context && notification()) return 0;
320
384
  const declared = declaredIn(root);
321
385
  const results = detectors.map((d) => runOne(d, root, declared, context ? 'context' : hook ? 'stop' : ''));
322
386
  const applicable = results.filter((r) => r.state !== 'n/a');
@@ -358,11 +422,17 @@ export function runDetectors(detectors, options) {
358
422
  // again: it has it already, and every copy stays in the conversation.
359
423
  if (context) {
360
424
  const session = hookSession();
361
- const before = readStops(root)[session]?.given;
362
- const handed = new Set(Array.isArray(before) ? before : []);
363
- remember(root, session, { given: drift.flatMap(findingKeys) });
425
+ const state = readStops(root)[session] ?? {};
426
+ // A compaction since the last hand-over dropped it from the model's context: everything goes again.
427
+ const compacted = compactedSince(hookInput().transcript_path, state.handed_at);
428
+ const handed = new Set(!compacted && Array.isArray(state.given) ? state.given : []);
364
429
  const known = drift.filter((r) => findingKeys(r).every((k) => handed.has(k)));
365
- const said = contextOf(drift.filter((r) => !known.includes(r)), known, errored, missing, detectors);
430
+ const fresh = drift.filter((r) => !known.includes(r));
431
+ remember(root, session, { given: drift.flatMap(findingKeys), ...(fresh.length > 0 ? { handed_at: new Date().toISOString() } : {}) });
432
+ // What the model was handed and has not changed is said only beside something new. Said on its own
433
+ // before every message, it went into the conversation on every prompt the session had.
434
+ const speaking = fresh.length > 0 || errored.length > 0 || missing.length > 0;
435
+ const said = contextOf(fresh, speaking ? known : [], errored, missing, detectors);
366
436
  if (said) {
367
437
  process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: said } })}\n`);
368
438
  }
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] ?? '' : '';
@@ -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)
632
636
  ) {
633
637
  continue;
634
638
  }
@@ -656,6 +660,8 @@ 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(),
659
665
  });
660
666
  const rounds = [fresh()];
661
667
  const roundOf = roundsOf();
@@ -677,6 +683,7 @@ async function attachAgentDetail(projectDir, dispatches) {
677
683
  const round = rounds[n - 1];
678
684
  const hasSkill = line.includes('"Skill"');
679
685
  const hasHandback = line.includes('SubagentHandback');
686
+ for (const m of line.matchAll(/"type":"tool_use","id":"([^"]+)"/g)) round.calls.add(m[1]);
680
687
  if (/"name":"(Edit|Write|MultiEdit|NotebookEdit)"/.test(line)) {
681
688
  try {
682
689
  for (const block of JSON.parse(line)?.message?.content ?? []) {
@@ -755,9 +762,14 @@ async function attachAgentDetail(projectDir, dispatches) {
755
762
  targets = [dispatches.get(id)];
756
763
  }
757
764
  const spent = tokensOf(round.usage);
765
+ const shape = contextOf(round.usage);
758
766
  targets.forEach((record, j) => {
759
767
  // One transcript, one bill: were two records ever to share an agent, the second would count it again.
760
768
  record.tokens = j === 0 ? spent.tokens : null;
769
+ record.turns = shape.turns;
770
+ record.tool_calls = round.calls.size;
771
+ record.context_start = shape.start;
772
+ record.context_peak = shape.peak;
761
773
  record.usage_model = spent.model;
762
774
  record.effort = spent.effort;
763
775
  record.files_touched = round.written.size;
@@ -790,6 +802,20 @@ async function attachAgentDetail(projectDir, dispatches) {
790
802
  }
791
803
  }
792
804
 
805
+ /**
806
+ * How a round spent its context: how many turns it took, and how large the context each turn re-read was
807
+ * when the round began and at its largest. Every turn is billed the whole context again, so a round that
808
+ * begins where a long one ended — a run resumed for a fix — pays for everything before it on each turn.
809
+ *
810
+ * @param {Map<string, {usage: object}>} usage - Message id → its final usage, in the order the round sent them.
811
+ * @returns {{turns: number, start: number|null, peak: number|null}} Context sizes in tokens.
812
+ */
813
+ export function contextOf(usage) {
814
+ const n = (v) => (typeof v === 'number' ? v : 0);
815
+ const sizes = [...usage.values()].map(({ usage: u }) => n(u.input_tokens) + n(u.cache_read_input_tokens) + n(u.cache_creation_input_tokens));
816
+ return { turns: sizes.length, start: sizes[0] ?? null, peak: sizes.length > 0 ? Math.max(...sizes) : null };
817
+ }
818
+
793
819
  /**
794
820
  * What a run spent, from each of its messages' final usage: tokens by kind, and the model and effort
795
821
  * level that most of its messages ran at. Metadata — counts, a model id and a level, never a message.
package/src/wiring.mjs CHANGED
@@ -23,6 +23,7 @@ import { join } from 'node:path';
23
23
  export const CHECK = 'scripts/harness-check.mjs';
24
24
  export const GATE = 'scripts/loop-gate.mjs';
25
25
  export const GUARD = 'scripts/edit-guard.mjs';
26
+ export const COST = 'scripts/cost-watch.mjs';
26
27
 
27
28
  /** The npm scripts the detectors hang off: the reviewer runs one, the other is a detector itself. */
28
29
  export const SCRIPTS = {
@@ -135,6 +136,8 @@ export const HOOKS = [
135
136
  event: 'PreToolUse', matcher: 'Edit|Write|MultiEdit|NotebookEdit', script: GUARD, timeout: 10, why: 'a hand edit to a composed file is found only after the turn, as drift, and the next compose overwrites it',
136
137
  crash: (cannot) => ({ systemMessage: `NINA: the edit guard could not start, so composed files can be edited in place — ${cannot}` }),
137
138
  },
139
+ // Every tool, since what it watches is a subagent's every turn; a call outside a subagent returns at once.
140
+ { event: 'PostToolUse', script: COST, timeout: 10, why: 'a run that keeps re-reading its own context is told what it cost only afterwards, by `stats`' },
138
141
  ];
139
142
 
140
143
  /** Where a project's hooks can live: Claude Code reads both, so a hook kept in the local file counts. */