@xhulz/nina 0.69.0 → 0.71.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 (276) hide show
  1. package/bin/nina.mjs +2 -1
  2. package/package.json +1 -1
  3. package/releases/0.70.0/core/GAPS.md +125 -0
  4. package/releases/0.70.0/core/answered.json +89 -0
  5. package/releases/0.70.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  6. package/releases/0.70.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  7. package/releases/0.70.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  8. package/releases/0.70.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  9. package/releases/0.70.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  10. package/releases/0.70.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  11. package/releases/0.70.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  12. package/releases/0.70.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  13. package/releases/0.70.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  14. package/releases/0.70.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  15. package/releases/0.70.0/core/tree/.claude/agents/architect.md +135 -0
  16. package/releases/0.70.0/core/tree/.claude/agents/dba.md +119 -0
  17. package/releases/0.70.0/core/tree/.claude/agents/design-reviewer.md +78 -0
  18. package/releases/0.70.0/core/tree/.claude/agents/devops.md +118 -0
  19. package/releases/0.70.0/core/tree/.claude/agents/implementer.md +178 -0
  20. package/releases/0.70.0/core/tree/.claude/agents/integration-tester.md +170 -0
  21. package/releases/0.70.0/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.70.0/core/tree/.claude/agents/qa.md +150 -0
  23. package/releases/0.70.0/core/tree/.claude/agents/reviewer.md +187 -0
  24. package/releases/0.70.0/core/tree/.claude/agents/secops.md +120 -0
  25. package/releases/0.70.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  26. package/releases/0.70.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  27. package/releases/0.70.0/core/tree/.claude/agents-overview.md +65 -0
  28. package/releases/0.70.0/core/tree/.claude/frontend.md +10 -0
  29. package/releases/0.70.0/core/tree/.claude/graph.md +79 -0
  30. package/releases/0.70.0/core/tree/.claude/patterns.md +252 -0
  31. package/releases/0.70.0/core/tree/.claude/pills/README.md +150 -0
  32. package/releases/0.70.0/core/tree/.claude/pipeline.md +101 -0
  33. package/releases/0.70.0/core/tree/.claude/retrieval.md +103 -0
  34. package/releases/0.70.0/core/tree/.claude/router.md +221 -0
  35. package/releases/0.70.0/core/tree/.claude/templates/integration.md +48 -0
  36. package/releases/0.70.0/core/tree/CLAUDE.md +167 -0
  37. package/releases/0.70.0/core/tree/scripts/cost-watch.mjs +25 -0
  38. package/releases/0.70.0/core/tree/scripts/edit-guard.mjs +24 -0
  39. package/releases/0.70.0/core/tree/scripts/harness-check.mjs +82 -0
  40. package/releases/0.70.0/core/tree/scripts/loop-gate.mjs +19 -0
  41. package/releases/0.70.0/core/vocabulary.json +14 -0
  42. package/releases/0.70.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  43. package/releases/0.70.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  44. package/releases/0.70.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  45. package/releases/0.70.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  46. package/releases/0.70.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  47. package/releases/0.70.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  48. package/releases/0.70.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  49. package/releases/0.70.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  50. package/releases/0.70.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  51. package/releases/0.70.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  52. package/releases/0.70.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  53. package/releases/0.70.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.70.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  55. package/releases/0.70.0/surfaces/d1/tree/CLAUDE.md +2 -0
  56. package/releases/0.70.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  57. package/releases/0.70.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  58. package/releases/0.70.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  59. package/releases/0.70.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  60. package/releases/0.70.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  61. package/releases/0.70.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  62. package/releases/0.70.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  63. package/releases/0.70.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  64. package/releases/0.70.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  65. package/releases/0.70.0/surfaces/db/tree/.claude/graph.md +8 -0
  66. package/releases/0.70.0/surfaces/db/tree/.claude/patterns.md +22 -0
  67. package/releases/0.70.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  68. package/releases/0.70.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  69. package/releases/0.70.0/surfaces/db/tree/.claude/router.md +4 -0
  70. package/releases/0.70.0/surfaces/db/tree/CLAUDE.md +17 -0
  71. package/releases/0.70.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  72. package/releases/0.70.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  73. package/releases/0.70.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  74. package/releases/0.70.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  75. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  76. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  77. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  78. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  79. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  80. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  81. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  82. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  83. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  84. package/releases/0.70.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  85. package/releases/0.70.0/surfaces/edge-cf/tree/CLAUDE.md +29 -0
  86. package/releases/0.70.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  87. package/releases/0.70.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  88. package/releases/0.70.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  89. package/releases/0.70.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  90. package/releases/0.70.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  91. package/releases/0.70.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  92. package/releases/0.70.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  93. package/releases/0.70.0/surfaces/frontend/tree/.claude/router.md +14 -0
  94. package/releases/0.70.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  95. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  96. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  97. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  98. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  99. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  100. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  101. package/releases/0.70.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  102. package/releases/0.70.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  103. package/releases/0.70.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  104. package/releases/0.70.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  105. package/releases/0.70.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  106. package/releases/0.70.0/surfaces/integrations/tree/.claude/router.md +6 -0
  107. package/releases/0.70.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  108. package/releases/0.70.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  109. package/releases/0.70.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  110. package/releases/0.70.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  111. package/releases/0.70.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  112. package/releases/0.70.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  113. package/releases/0.70.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  114. package/releases/0.70.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  115. package/releases/0.70.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  116. package/releases/0.70.0/surfaces/money/tree/.claude/graph.md +2 -0
  117. package/releases/0.70.0/surfaces/money/tree/.claude/patterns.md +49 -0
  118. package/releases/0.70.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  119. package/releases/0.70.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  120. package/releases/0.70.0/surfaces/money/tree/CLAUDE.md +11 -0
  121. package/releases/0.70.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  122. package/releases/0.70.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  123. package/releases/0.70.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  124. package/releases/0.70.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  125. package/releases/0.70.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  126. package/releases/0.70.0/surfaces/pii/tree/CLAUDE.md +5 -0
  127. package/releases/0.70.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  128. package/releases/0.70.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  129. package/releases/0.70.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  130. package/releases/0.70.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  131. package/releases/0.70.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  132. package/releases/0.70.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  133. package/releases/0.70.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  134. package/releases/0.70.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  135. package/releases/0.71.0/core/GAPS.md +125 -0
  136. package/releases/0.71.0/core/answered.json +89 -0
  137. package/releases/0.71.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  138. package/releases/0.71.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  139. package/releases/0.71.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  140. package/releases/0.71.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  141. package/releases/0.71.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  142. package/releases/0.71.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  143. package/releases/0.71.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  144. package/releases/0.71.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  145. package/releases/0.71.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  146. package/releases/0.71.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  147. package/releases/0.71.0/core/tree/.claude/agents/architect.md +135 -0
  148. package/releases/0.71.0/core/tree/.claude/agents/dba.md +119 -0
  149. package/releases/0.71.0/core/tree/.claude/agents/design-reviewer.md +78 -0
  150. package/releases/0.71.0/core/tree/.claude/agents/devops.md +118 -0
  151. package/releases/0.71.0/core/tree/.claude/agents/implementer.md +178 -0
  152. package/releases/0.71.0/core/tree/.claude/agents/integration-tester.md +170 -0
  153. package/releases/0.71.0/core/tree/.claude/agents/planner.md +132 -0
  154. package/releases/0.71.0/core/tree/.claude/agents/qa.md +150 -0
  155. package/releases/0.71.0/core/tree/.claude/agents/reviewer.md +187 -0
  156. package/releases/0.71.0/core/tree/.claude/agents/secops.md +120 -0
  157. package/releases/0.71.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  158. package/releases/0.71.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  159. package/releases/0.71.0/core/tree/.claude/agents-overview.md +65 -0
  160. package/releases/0.71.0/core/tree/.claude/frontend.md +10 -0
  161. package/releases/0.71.0/core/tree/.claude/graph.md +79 -0
  162. package/releases/0.71.0/core/tree/.claude/patterns.md +252 -0
  163. package/releases/0.71.0/core/tree/.claude/pills/README.md +150 -0
  164. package/releases/0.71.0/core/tree/.claude/pipeline.md +101 -0
  165. package/releases/0.71.0/core/tree/.claude/retrieval.md +103 -0
  166. package/releases/0.71.0/core/tree/.claude/router.md +221 -0
  167. package/releases/0.71.0/core/tree/.claude/templates/integration.md +48 -0
  168. package/releases/0.71.0/core/tree/CLAUDE.md +167 -0
  169. package/releases/0.71.0/core/tree/scripts/cost-watch.mjs +27 -0
  170. package/releases/0.71.0/core/tree/scripts/edit-guard.mjs +24 -0
  171. package/releases/0.71.0/core/tree/scripts/harness-check.mjs +82 -0
  172. package/releases/0.71.0/core/tree/scripts/loop-gate.mjs +19 -0
  173. package/releases/0.71.0/core/vocabulary.json +15 -0
  174. package/releases/0.71.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  175. package/releases/0.71.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  176. package/releases/0.71.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  177. package/releases/0.71.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  178. package/releases/0.71.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  179. package/releases/0.71.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  180. package/releases/0.71.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  181. package/releases/0.71.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  182. package/releases/0.71.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  183. package/releases/0.71.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  184. package/releases/0.71.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  185. package/releases/0.71.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  186. package/releases/0.71.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  187. package/releases/0.71.0/surfaces/d1/tree/CLAUDE.md +2 -0
  188. package/releases/0.71.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  189. package/releases/0.71.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  190. package/releases/0.71.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  191. package/releases/0.71.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  192. package/releases/0.71.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  193. package/releases/0.71.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  194. package/releases/0.71.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  195. package/releases/0.71.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  196. package/releases/0.71.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  197. package/releases/0.71.0/surfaces/db/tree/.claude/graph.md +8 -0
  198. package/releases/0.71.0/surfaces/db/tree/.claude/patterns.md +22 -0
  199. package/releases/0.71.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  200. package/releases/0.71.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  201. package/releases/0.71.0/surfaces/db/tree/.claude/router.md +4 -0
  202. package/releases/0.71.0/surfaces/db/tree/CLAUDE.md +17 -0
  203. package/releases/0.71.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  204. package/releases/0.71.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  205. package/releases/0.71.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  206. package/releases/0.71.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  207. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  208. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  209. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  210. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  211. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  212. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  213. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  214. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  215. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  216. package/releases/0.71.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  217. package/releases/0.71.0/surfaces/edge-cf/tree/CLAUDE.md +29 -0
  218. package/releases/0.71.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  219. package/releases/0.71.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  220. package/releases/0.71.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  221. package/releases/0.71.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  222. package/releases/0.71.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  223. package/releases/0.71.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  224. package/releases/0.71.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  225. package/releases/0.71.0/surfaces/frontend/tree/.claude/router.md +14 -0
  226. package/releases/0.71.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  227. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  228. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  229. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  230. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  231. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  232. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  233. package/releases/0.71.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  234. package/releases/0.71.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  235. package/releases/0.71.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  236. package/releases/0.71.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  237. package/releases/0.71.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  238. package/releases/0.71.0/surfaces/integrations/tree/.claude/router.md +6 -0
  239. package/releases/0.71.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  240. package/releases/0.71.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  241. package/releases/0.71.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  242. package/releases/0.71.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  243. package/releases/0.71.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  244. package/releases/0.71.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  245. package/releases/0.71.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  246. package/releases/0.71.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  247. package/releases/0.71.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  248. package/releases/0.71.0/surfaces/money/tree/.claude/graph.md +2 -0
  249. package/releases/0.71.0/surfaces/money/tree/.claude/patterns.md +49 -0
  250. package/releases/0.71.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  251. package/releases/0.71.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  252. package/releases/0.71.0/surfaces/money/tree/CLAUDE.md +11 -0
  253. package/releases/0.71.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  254. package/releases/0.71.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  255. package/releases/0.71.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  256. package/releases/0.71.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  257. package/releases/0.71.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  258. package/releases/0.71.0/surfaces/pii/tree/CLAUDE.md +5 -0
  259. package/releases/0.71.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  260. package/releases/0.71.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  261. package/releases/0.71.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  262. package/releases/0.71.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  263. package/releases/0.71.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  264. package/releases/0.71.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  265. package/releases/0.71.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  266. package/releases/0.71.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  267. package/src/commands/learn.mjs +44 -13
  268. package/src/commands/sizes.mjs +94 -24
  269. package/src/commands/stats.mjs +57 -1
  270. package/src/commands/upgrade.mjs +3 -3
  271. package/src/cost.mjs +174 -19
  272. package/src/dashboard.html +3 -1
  273. package/src/gate.mjs +10 -4
  274. package/src/langfuse.mjs +5 -2
  275. package/src/transcripts.mjs +45 -7
  276. package/src/wiring.mjs +33 -5
@@ -75,6 +75,23 @@ export function forecastIn(text) {
75
75
  return { lines: Number(m[1].replace(/,/g, '')), files: files ? Number(files[1]) : null };
76
76
  }
77
77
 
78
+ /**
79
+ * What a step file's `Size:` line says when {@link forecastIn} reads no forecast from it, or null when it reads one or
80
+ * the file has no such line. A step whose line was rewritten into prose — "round 1 landed +498/−95 against ~272" — was
81
+ * left out of every rate without a word. The label opens its line, as `Kind:` does: `font-size:` in a step's CSS and a
82
+ * sentence that names the `Size:` line are no line of their own.
83
+ *
84
+ * @param {string} text - A step file.
85
+ * @returns {string|null} The line after `Size:`, shortened.
86
+ */
87
+ export function unreadSize(text) {
88
+ if (forecastIn(text)) return null;
89
+ const m = /^[\s>*_-]*Size:[*_ \t]*([^\n]*)/im.exec(text);
90
+ if (!m) return null;
91
+ const said = m[1].trim();
92
+ return said.length > 72 ? `${said.slice(0, 71)}…` : said;
93
+ }
94
+
78
95
  /** Runs git in the project, or says it could not. */
79
96
  function git(dir, args) {
80
97
  const r = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
@@ -178,7 +195,8 @@ const median = (xs) => {
178
195
  * Every step with a forecast, landed or not: what it was forecast before it was built, and what its commits added.
179
196
  *
180
197
  * @param {string} dir - The project.
181
- * @returns {{steps: object[], limit: number}|null} Null outside a git repository.
198
+ * @returns {{steps: object[], limit: number, unread: object[]}|null} Null outside a git repository. `unread` is each step
199
+ * file whose `Size:` line reads as no forecast, with what it says (`said`).
182
200
  */
183
201
  export function measure(dir) {
184
202
  const profile = (() => {
@@ -191,7 +209,7 @@ export function measure(dir) {
191
209
  const limit = Number(profile?.vocabulary?.STEP_LINES) || Number(defaultVocabulary(PACKAGE).STEP_LINES) || 400;
192
210
  // It runs every turn: a project with no step file does not pay for its history.
193
211
  const files = stepFiles(dir);
194
- if (files.length === 0) return git(dir, ['rev-parse', '--verify', '--quiet', 'HEAD']) === null ? null : { steps: [], limit };
212
+ if (files.length === 0) return git(dir, ['rev-parse', '--verify', '--quiet', 'HEAD']) === null ? null : { steps: [], limit, unread: [] };
195
213
  const log = git(dir, ['log', '--format=%H%x09%ct%x09%cI%x09%s', 'HEAD']);
196
214
  if (log === null) return null;
197
215
  // Times as instants: an ISO time carries its zone, and two zones compared as text order wrongly.
@@ -227,23 +245,36 @@ export function measure(dir) {
227
245
  return when;
228
246
  };
229
247
  const steps = [];
248
+ // Step files whose `Size:` line reads as no forecast: left out of every rate, so said rather than skipped.
249
+ const unread = [];
230
250
  for (const file of files) {
231
251
  const current = readFileSync(join(dir, file.path), 'utf8');
232
252
  const now = forecastIn(current);
233
253
  const kind = kindIn(current);
234
254
  const landings = named.get(file.path).filter((c) => code.get(c.hash)?.size > 0);
235
255
  if (landings.length === 0) {
236
- if (!now) continue;
256
+ if (!now) {
257
+ const said = unreadSize(current);
258
+ if (said !== null) unread.push({ ...file, landed: false, said });
259
+ continue;
260
+ }
237
261
  // Forecast again in the working tree, it is newer than every commit.
238
262
  const since = at('HEAD', file.path)?.lines === now.lines ? writtenAt(file.path, Infinity, now.lines) : Infinity;
239
- steps.push({ ...file, kind, landed: false, forecast: now, since });
263
+ // What it was first forecast at: a forecast restated at a rate carries that rate over it, and rates multiplied
264
+ // onto one another — another kind's among them — put one project's steps at ×2.54 of their count.
265
+ const original = changes.get(file.path).map((c) => at(c.hash, file.path)).filter(Boolean).at(-1) ?? null;
266
+ steps.push({ ...file, kind, landed: false, forecast: now, since, original: original && original.lines !== now.lines ? original : null });
240
267
  continue;
241
268
  }
242
269
  // The forecast the step was built to: its file before its first commit, or as that commit wrote it.
243
270
  const first = landings[0];
244
271
  const before = at(`${first.hash}^`, file.path);
245
272
  const forecast = before ?? at(first.hash, file.path);
246
- if (!forecast) continue;
273
+ if (!forecast) {
274
+ const said = unreadSize(text.get(`${first.hash}^:${file.path}`) ?? '') ?? unreadSize(text.get(`${first.hash}:${file.path}`) ?? '');
275
+ if (said !== null) unread.push({ ...file, landed: true, said, hash: first.hash });
276
+ continue;
277
+ }
247
278
  const since = (before ? writtenAt(file.path, order.get(first.hash), forecast.lines) : null) ?? first.t;
248
279
  const lines = new Map();
249
280
  for (const c of landings) for (const [path, added] of code.get(c.hash)) lines.set(path, (lines.get(path) ?? 0) + added);
@@ -272,7 +303,7 @@ export function measure(dir) {
272
303
  hash: last.hash,
273
304
  });
274
305
  }
275
- return { steps, limit };
306
+ return { steps, limit, unread };
276
307
  }
277
308
 
278
309
  /**
@@ -294,6 +325,20 @@ export function rateOf(steps, { spec, since, kind = null }) {
294
325
  return over.length ? { rate: median(over.map((s) => s.measured ?? s.ratio)), over: over.length, kind: mine.length ? kind : null } : null;
295
326
  }
296
327
 
328
+ /**
329
+ * What a spec's newest landed steps of one kind missed by, whenever they were forecast: set beside a step to land that
330
+ * carries more over its first forecast than its kind ever missed by, which reads as rates carried from elsewhere.
331
+ *
332
+ * @param {object[]} steps
333
+ * @param {{spec: string, kind?: string|null}} step - Its spec and kind.
334
+ * @returns {number|null}
335
+ */
336
+ export function kindRate(steps, { spec, kind = null }) {
337
+ if (!kind) return null;
338
+ const over = steps.filter((s) => s.landed && s.spec === spec && s.kind === kind).sort((a, b) => a.t - b.t).slice(-RATE_OVER);
339
+ return over.length ? median(over.map((s) => s.measured ?? s.ratio)) : null;
340
+ }
341
+
297
342
  /**
298
343
  * What the detector says: the newest landed step, when it overran, and the steps of its spec still forecast as they
299
344
  * were when it landed — until one of them is forecast again. The next step to go out is forecast again at its turn, and
@@ -336,7 +381,7 @@ export async function sizes(argv) {
336
381
  else console.error(' not a git repository, or one with no commit yet: a step is measured by its commits\n');
337
382
  return check ? 0 : 1;
338
383
  }
339
- const { steps, limit } = measured;
384
+ const { steps, limit, unread } = measured;
340
385
  if (check) {
341
386
  // During `nina upgrade --apply`: a step that overran is the project's history, not the move — and the release that
342
387
  // first shipped this detector rolled itself back in the project that asked for it. The move says it after.
@@ -344,30 +389,50 @@ export async function sizes(argv) {
344
389
  console.log('sizes: current');
345
390
  return 0;
346
391
  }
392
+ const said = [];
347
393
  const found = overrun(measured);
348
- if (!found) {
394
+ if (found) {
395
+ const { step, stale } = found;
396
+ const again = stale.slice(0, 4).map((s) => {
397
+ const rate = rateOf(steps, s);
398
+ const next = Math.round(s.forecast.lines * (rate?.rate ?? 1));
399
+ return `${s.step} ~${s.forecast.lines} → ~${next}${rate?.kind ? ` (${rate.kind})` : ''}${next > limit ? ` past ${limit}` : ''}`;
400
+ });
401
+ const first = step.first === null ? '' : `, ${step.first} in its first round (${times(step.measured)})`;
402
+ said.push(
403
+ `step ${step.step} of ${step.spec} landed ${step.lines} lines against ~${step.forecast.lines} (${times(step.ratio)}${first}); ` +
404
+ `before the next of its ${stale.length} step(s) forecast when it was goes out, forecast that one again — a \`Size:\` line at the rate \`nina sizes\` gives it — ` +
405
+ `and split it if that puts it past ${limit}; the rest at their turn: ${again.join(', ')}${stale.length > again.length ? ', …' : ''}`,
406
+ );
407
+ }
408
+ // Only a step still to land: its line can be written again now, and once it lands, the line it carried is history.
409
+ for (const u of unread.filter((x) => !x.landed)) {
410
+ // "~215 raw lines" opens with a number too: what reads is the forecast it is measured by, followed by `lines`.
411
+ said.push(`step ${u.step} of ${u.spec}: no forecast it can read in its \`Size:\` line ("${u.said}") — write it as \`Size: ~N lines in M files\`, the forecast the step is measured by followed by \`lines\`, before the step lands, or no rate counts it`);
412
+ }
413
+ if (said.length === 0) {
349
414
  console.log('sizes: current');
350
415
  return 0;
351
416
  }
352
- const { step, stale } = found;
353
- const again = stale.slice(0, 4).map((s) => {
354
- const rate = rateOf(steps, s);
355
- const next = Math.round(s.forecast.lines * (rate?.rate ?? 1));
356
- return `${s.step} ~${s.forecast.lines} → ~${next}${rate?.kind ? ` (${rate.kind})` : ''}${next > limit ? ` past ${limit}` : ''}`;
357
- });
358
- const first = step.first === null ? '' : `, ${step.first} in its first round (${times(step.measured)})`;
359
- console.log(
360
- `step ${step.step} of ${step.spec} landed ${step.lines} lines against ~${step.forecast.lines} (${times(step.ratio)}${first}); ` +
361
- `before the next of its ${stale.length} step(s) forecast when it was goes out, forecast that one again — a \`Size:\` line at the rate \`nina sizes\` gives it — ` +
362
- `and split it if that puts it past ${limit}; the rest at their turn: ${again.join(', ')}${stale.length > again.length ? ', …' : ''}`,
363
- );
417
+ for (const line of said) console.log(line);
364
418
  return 1;
365
419
  }
366
- if (steps.length === 0) {
420
+ if (steps.length === 0 && unread.length === 0) {
367
421
  console.log(` no step file under ${SPECS} states a \`Size:\` line yet\n`);
368
422
  return 0;
369
423
  }
370
- for (const spec of [...new Set(steps.map((s) => s.spec))]) {
424
+ // What a step to land carries over its first forecast, beside what its kind's landed steps missed by: rates multiplied
425
+ // onto one another, another kind's among them, read as one step's own.
426
+ const carried = (s) => {
427
+ if (!s.original) return '';
428
+ const factor = s.forecast.lines / s.original.lines;
429
+ const own = kindRate(steps, s);
430
+ const note = ` · first forecast ~${s.original.lines}, ${times(factor)} since`;
431
+ if (own === null) return dim(note);
432
+ const beside = `${note}; its ${s.kind} steps missed by ${times(own)}`;
433
+ return factor > own * OVERRUN ? amber(beside) : dim(beside);
434
+ };
435
+ for (const spec of [...new Set([...steps, ...unread].map((s) => s.spec))]) {
371
436
  const mine = steps.filter((s) => s.spec === spec);
372
437
  console.log(`\n ${bold(pink(spec))}${dim(` landed against the forecast it was built to; limit ${limit} lines`)}`);
373
438
  for (const s of mine.filter((x) => x.landed).sort((a, b) => a.t - b.t)) {
@@ -382,16 +447,21 @@ export async function sizes(argv) {
382
447
  for (const s of mine.filter((x) => !x.landed)) {
383
448
  const rate = rateOf(steps, s);
384
449
  if (!rate) {
385
- console.log(` ${s.step.padEnd(10)} ~${String(s.forecast.lines).padEnd(5)} ${dim('to land — forecast after every step that landed, so none measures it yet')}`);
450
+ console.log(` ${s.step.padEnd(10)} ~${String(s.forecast.lines).padEnd(5)} ${dim('to land — forecast after every step that landed, so none measures it yet')}${carried(s)}`);
386
451
  continue;
387
452
  }
388
453
  const next = Math.round(s.forecast.lines * rate.rate);
389
454
  console.log(
390
455
  ` ${s.step.padEnd(10)} ~${String(s.forecast.lines).padEnd(5)} → ~${String(next).padEnd(5)} ` +
391
456
  dim(`to land, at ${times(rate.rate)} over ${rate.over} ${rate.kind ? `${rate.kind} ` : ''}step(s) forecast as it was`) +
392
- (next > limit ? amber(` past ${limit}: split`) : ''),
457
+ (next > limit ? amber(` past ${limit}: split`) : '') +
458
+ carried(s),
393
459
  );
394
460
  }
461
+ for (const u of unread.filter((x) => x.spec === spec)) {
462
+ const where = u.landed ? `before ${u.hash.slice(0, 7)}, the commit that landed it,` : 'to land,';
463
+ console.log(` ${u.step.padEnd(10)} ${amber('no forecast it can read')} ${dim(`— its \`Size:\` line ${where} says "${u.said}", so no rate counts it`)}`);
464
+ }
395
465
  }
396
466
  console.log('');
397
467
  return 0;
@@ -299,6 +299,17 @@ function contextReport(records) {
299
299
  `${(row.prompt === null ? '—' : row.prompt.toLocaleString('en-US')).padStart(9)}`,
300
300
  );
301
301
  }
302
+ if (d.reread.length === 0) return;
303
+ const pct = (x) => (x === null ? '—' : `${Math.round(x * 100)}%`);
304
+ console.log('');
305
+ console.log(dim(' what the turns re-read: the context each round opened with, the expired cache written again, the outputs clipped to a file'));
306
+ console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padStart(6)}${'opened with'.padStart(13)}${'rewrites'.padStart(10)}${'of spend'.padStart(10)}${'clipped'.padStart(9)}`));
307
+ for (const row of d.reread) {
308
+ console.log(
309
+ ` ${pink(row.role)}${' '.repeat(Math.max(20 - row.role.length, 1))}${String(row.n).padStart(6)}${pct(row.fixed).padStart(13)}` +
310
+ `${String(row.rewrites).padStart(10)}${pct(row.rewriteShare).padStart(10)}${String(row.clipped).padStart(9)}`,
311
+ );
312
+ }
302
313
  }
303
314
 
304
315
  /**
@@ -334,7 +345,52 @@ export function contextData(records) {
334
345
  });
335
346
  }
336
347
  }
337
- return { rows };
348
+ return { rows, reread: rereadData(shaped) };
349
+ }
350
+
351
+ /**
352
+ * Where each stage's re-read went, as data: the share of the context its turns re-read that was what each
353
+ * round opened with — the system prompt, CLAUDE.md, the spec, the dispatch — the share of what it spent that
354
+ * went on writing an expired cache again, and the outputs the cost watch clipped to a file. Rounds measured
355
+ * before the fields existed are left out of the shares rather than counted as none; a stage with no
356
+ * measured round has no row.
357
+ *
358
+ * @param {object[]} shaped - The rounds that recorded their turns.
359
+ * @returns {{role: string, n: number, fixed: number|null, rewrites: number, rewriteShare: number|null, clipped: number}[]}
360
+ */
361
+ export function rereadData(shaped) {
362
+ const measured = shaped.filter((r) => typeof r.cache_rewrites === 'number');
363
+ const byRole = new Map();
364
+ for (const r of measured) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
365
+ const t = (n) => (typeof n === 'number' ? n : 0);
366
+ const rows = [];
367
+ for (const [role, rounds] of byRole) {
368
+ let opened = 0;
369
+ let reread = 0;
370
+ let rewriteCost = 0;
371
+ let spent = 0;
372
+ for (const r of rounds) {
373
+ const total = t(r.tokens?.input) + t(r.tokens?.read) + t(r.tokens?.write_5m) + t(r.tokens?.write_1h);
374
+ if (total > 0 && typeof r.context_start === 'number') {
375
+ opened += Math.min(r.context_start * r.turns, total);
376
+ reread += total;
377
+ }
378
+ const cost = costOf(r.tokens, r.usage_model);
379
+ if (cost !== null && cost > 0) {
380
+ spent += cost;
381
+ rewriteCost += costOf({ write_5m: t(r.cache_rewritten) }, r.usage_model);
382
+ }
383
+ }
384
+ rows.push({
385
+ role,
386
+ n: rounds.length,
387
+ fixed: reread > 0 ? opened / reread : null,
388
+ rewrites: rounds.reduce((a, r) => a + t(r.cache_rewrites), 0),
389
+ rewriteShare: spent > 0 ? rewriteCost / spent : null,
390
+ clipped: rounds.reduce((a, r) => a + t(r.clipped), 0),
391
+ });
392
+ }
393
+ return rows.sort((a, b) => b.n - a.n);
338
394
  }
339
395
 
340
396
  /**
@@ -366,9 +366,9 @@ export async function upgrade(argv, ctx) {
366
366
  const newScripts = new Set([...(await shippedScripts(onto.dir, kept))].filter((s) => !scriptsBefore.has(s)));
367
367
  const unwired = newScripts.size > 0 ? await missingWiring(target, newScripts) : [];
368
368
  // A script already run whose hooks this NINA needs more of — a matcher that grew — holds what it held, so it does
369
- // not stop the move; it is said, with what fixes it.
369
+ // not stop the move; it is said, with what fixes it. So is a setting it now ships beside them.
370
370
  const stillRun = new Set([...(await shippedScripts(onto.dir, kept))].filter((s) => scriptsBefore.has(s)));
371
- const widened = stillRun.size > 0 ? (await missingWiring(target, stillRun)).filter((w) => / hook running `[^`]+` does not match /.test(w)) : [];
371
+ const widened = stillRun.size > 0 ? (await missingWiring(target, stillRun)).filter((w) => / hook running `[^`]+` does not match /.test(w) || /^\.claude\/settings\.json has no "/.test(w)) : [];
372
372
  if (widened.length > 0) {
373
373
  for (const w of widened) console.log(` · ${w}`);
374
374
  console.log('');
@@ -633,7 +633,7 @@ export async function upgrade(argv, ctx) {
633
633
  }
634
634
  const forecasts = nina(ctx, target, ['sizes', '--project', target, '--check']);
635
635
  if (!forecasts.ok && forecasts.out.trim()) {
636
- console.log('\n a step landed past its forecast — not caused by this move, now visible:');
636
+ console.log('\n the step forecasts have something to act on — not caused by this move, now visible:');
637
637
  for (const line of forecasts.out.trim().split('\n')) console.log(` ${line}`);
638
638
  }
639
639
  // The move is done and the tree is sound, but it is not finished: until these are written
package/src/cost.mjs CHANGED
@@ -11,11 +11,19 @@
11
11
  *
12
12
  * It reads only what the run's transcript gained since its last call, keeps counts per message and
13
13
  * nothing else, under the project's gate directory, and lets every call through if anything goes wrong.
14
+ *
15
+ * Two things a run pays for on every turn after are settled at the call itself. A shell command's long
16
+ * output stays in the context and is re-read on every turn — outputs longer than a screen were a fifth of
17
+ * what two projects' stages re-read — so past `clipAt` characters it goes to a file beside the ones Claude
18
+ * Code keeps for the session, and the run sees its first and last lines and where the rest is. And a call
19
+ * that ran past the five minutes a subagent's cache lives makes the next turn write the whole context again,
20
+ * at twelve times a read's price — a quarter of what one project's stages spent — so the run is told then.
14
21
  */
15
22
 
16
23
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
17
24
  import { dirname, join } from 'node:path';
18
25
  import { projectGateDir } from './gate.mjs';
26
+ import { CLIP_MARK } from './transcripts.mjs';
19
27
 
20
28
  /** Where a run's counts are kept between calls, one file per agent. */
21
29
  const stateDir = (root) => join(projectGateDir(root), 'cost');
@@ -45,18 +53,168 @@ function gained(path, from) {
45
53
  return { text: buffer.subarray(0, end + 1).toString('utf8'), to: from + end + 1 };
46
54
  }
47
55
 
56
+ /** How long a subagent's prompt cache lives without a turn: a call that runs past it is paid for again in full. */
57
+ const CACHE_MS = 5 * 60_000;
58
+
59
+ /**
60
+ * Whether a shell command reads a file or a diff rather than producing output: what it prints is what the
61
+ * run asked to read, so clipping it would only send the run to read the file it was cut to. What a command
62
+ * line prints last is its last command's, and of a pipe its last stage's, so that is the one judged:
63
+ * `sed -i … && node run` produces output, and `cd p && cat f` or `node run && tail -n 50 log` reads.
64
+ *
65
+ * @param {unknown} command - The Bash call's command.
66
+ * @returns {boolean}
67
+ */
68
+ export function readsByShell(command) {
69
+ if (typeof command !== 'string') return false;
70
+ // Read as words, never by a pattern that backtracks: the command is the model's, of any length.
71
+ const assignment = (word) => /^\w+=/.test(word);
72
+ // A subshell's parentheses are not part of the words they hold.
73
+ const unwrap = (word) => {
74
+ let from = 0;
75
+ let to = word.length;
76
+ while (from < to && word[from] === '(') from += 1;
77
+ while (to > from && word[to - 1] === ')') to -= 1;
78
+ return word.slice(from, to);
79
+ };
80
+ const segments = command
81
+ .split(/&&|\|\||;|\n/)
82
+ .map((segment) => segment.split(/\s+/).map(unwrap).filter(Boolean))
83
+ // A segment of assignments alone prints nothing.
84
+ .filter((words) => words.length && !words.every(assignment));
85
+ const piped = (segments.at(-1) ?? []).join(' ').split('|').at(-1);
86
+ const words = piped.split(/\s+/).filter(Boolean);
87
+ // Less what runs a command rather than being it: assignments, `export NAME=…`, `timeout N`, `nice`, `command`.
88
+ let i = 0;
89
+ while (i < words.length) {
90
+ if (assignment(words[i]) || words[i] === 'nice' || words[i] === 'command') i += 1;
91
+ else if ((words[i] === 'export' && assignment(words[i + 1] ?? '')) || (words[i] === 'timeout' && words[i + 1])) i += 2;
92
+ else break;
93
+ }
94
+ if (['cat', 'sed', 'head', 'tail', 'nl', 'awk', 'less', 'more'].includes(words[i])) return true;
95
+ if (words[i] !== 'git') return false;
96
+ // `git diff` and `git show`, past the options that come before a subcommand.
97
+ i += 1;
98
+ while (words[i] === '--no-pager' || words[i] === '-C' || words[i] === '-c') i += words[i] === '--no-pager' ? 1 : 2;
99
+ return words[i] === 'diff' || words[i] === 'show';
100
+ }
101
+
102
+ /**
103
+ * The first lines of a text up to a size, or its last ones; a single line longer than the size is cut.
104
+ *
105
+ * @param {string} text - The output.
106
+ * @param {number} size - Characters at most.
107
+ * @param {boolean} fromEnd - The last lines rather than the first.
108
+ * @returns {string}
109
+ */
110
+ function lines(text, size, fromEnd) {
111
+ const all = text.split('\n');
112
+ const kept = [];
113
+ let length = 0;
114
+ for (const line of fromEnd ? all.reverse() : all) {
115
+ if (length + line.length + 1 > size) break;
116
+ kept.push(line);
117
+ length += line.length + 1;
118
+ }
119
+ if (kept.length === 0) return fromEnd ? text.slice(-size) : text.slice(0, size);
120
+ return (fromEnd ? kept.reverse() : kept).join('\n');
121
+ }
122
+
123
+ /**
124
+ * A long Bash output clipped: the whole of it written to a file in the session's `tool-results` directory,
125
+ * where Claude Code keeps the outputs it moves out itself and a stage may read, and in its place the first
126
+ * lines, the last ones — where a test runner or a battery prints its summary — and where the rest is.
127
+ * Null when the output is short, the command reads a file, or the file cannot be written: the run then sees
128
+ * the output as it was.
129
+ *
130
+ * @param {object} input - The PostToolUse hook's input.
131
+ * @param {number} clipAt - The characters past which an output is clipped; 0 clips nothing.
132
+ * @returns {{updatedToolOutput: object, chars: number}|null}
133
+ */
134
+ export function clipOutput(input, clipAt) {
135
+ const response = input?.tool_response;
136
+ if (input?.tool_name !== 'Bash' || !(clipAt > 0) || response === null || typeof response !== 'object' || typeof response.stdout !== 'string') return null;
137
+ const stderr = typeof response.stderr === 'string' ? response.stderr : '';
138
+ const chars = response.stdout.length + stderr.length;
139
+ if (chars <= clipAt || response.isImage || readsByShell(input.tool_input?.command)) return null;
140
+ const id = /^[A-Za-z0-9_-]{1,80}$/.test(String(input.tool_use_id ?? '')) ? input.tool_use_id : `${Date.now()}`;
141
+ const file = join(dirname(input.transcript_path), input.session_id, 'tool-results', `nina-${id}.txt`);
142
+ const whole = stderr ? `${response.stdout}\n--- stderr ---\n${stderr}` : response.stdout;
143
+ try {
144
+ mkdirSync(dirname(file), { recursive: true });
145
+ writeFileSync(file, whole);
146
+ } catch {
147
+ return null;
148
+ }
149
+ const said =
150
+ `${CLIP_MARK}: ${whole.split('\n').length.toLocaleString('en-US')} lines and ${chars.toLocaleString('en-US')} characters, of which the first and last lines are shown. ` +
151
+ `The whole of it${stderr ? ', stderr after stdout,' : ''} is in ${file} — grep it, or read the range you need: read whole, it all comes back into your context and is re-read on every turn after.]`;
152
+ // A stdout short enough to show whole is shown once, the notice after it: only what is long is cut.
153
+ const head = Math.round(clipAt / 8);
154
+ const tail = Math.round((clipAt * 3) / 8);
155
+ const stdout = response.stdout.length > head + tail ? `${lines(response.stdout, head, false)}\n…\n${said}\n…\n${lines(response.stdout, tail, true)}` : `${response.stdout}\n${said}`;
156
+ return {
157
+ chars,
158
+ updatedToolOutput: { ...response, stdout, stderr: stderr ? lines(stderr, head, true) : stderr },
159
+ };
160
+ }
161
+
48
162
  /**
49
163
  * What one call of the watch says, or null: the counts are brought up to date from the run's transcript,
50
164
  * and a warning is due each time the total re-read crosses `warnAt`, then twice it, four times it, and on.
165
+ * A long Bash output is clipped to a file ({@link clipOutput}), and a call that outlived the cache is named.
51
166
  *
52
167
  * @param {object} input - The PostToolUse hook's input.
53
- * @param {{root: string, warnAt: number, now?: number}} options
168
+ * @param {{root: string, warnAt: number, clipAt?: number, now?: number}} options
54
169
  * @returns {object|null} The hook's answer.
55
170
  */
56
- export function watch(input, { root, warnAt, now = Date.now() }) {
171
+ export function watch(input, { root, warnAt, clipAt = 0, now = Date.now() }) {
57
172
  // 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
173
  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;
174
+ // The session's id becomes a directory name below, in the clip's path and the transcript's.
175
+ if (typeof input.transcript_path !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(String(input.session_id ?? ''))) return null;
176
+ const clipped = clipOutput(input, clipAt);
177
+ const counted = warnAt > 0 ? count(input, { root, warnAt, now }) : null;
178
+ const ran = typeof input.duration_ms === 'number' && input.duration_ms > CACHE_MS ? input.duration_ms : null;
179
+ if (!clipped && !counted?.due && ran === null) return null;
180
+ const size = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1000)}k`);
181
+ const context = [];
182
+ const person = [];
183
+ if (counted?.due) {
184
+ context.push(
185
+ `NINA cost watch: this run has re-read ${counted.total} tokens of its own context${counted.turn}; every further turn costs more than the last. ` +
186
+ '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.',
187
+ );
188
+ person.push(`${input.agent_type ?? 'a subagent'} ${input.agent_id.slice(0, 8)} has re-read ${counted.total} tokens of its own context${counted.turn}.`);
189
+ }
190
+ if (ran !== null) {
191
+ const minutes = Math.round(ran / 60_000);
192
+ const whole = counted?.context ? `, about ${size(counted.context)} tokens,` : '';
193
+ context.push(
194
+ `NINA cost watch: that call ran ${minutes} minutes, past the five your context stays cached between turns, so your next turn writes all of it${whole} again — at twelve times what re-reading it costs. ` +
195
+ 'Wait in calls that each end within three minutes: run the rest in shares, or start it in the background and check on it no more often than every three minutes (Hard Rule #19).',
196
+ );
197
+ person.push(`${input.agent_type ?? 'a subagent'} ${input.agent_id.slice(0, 8)} waited ${minutes} min in one call; its next turn writes its whole context again.`);
198
+ }
199
+ return {
200
+ ...(person.length ? { systemMessage: `NINA: ${person.join(' ')}` } : {}),
201
+ hookSpecificOutput: {
202
+ hookEventName: 'PostToolUse',
203
+ ...(context.length ? { additionalContext: context.join('\n\n') } : {}),
204
+ ...(clipped ? { updatedToolOutput: clipped.updatedToolOutput } : {}),
205
+ },
206
+ };
207
+ }
208
+
209
+ /**
210
+ * The run's re-read brought up to date from its transcript, and whether a warning is due; null when the
211
+ * transcript is not there.
212
+ *
213
+ * @param {object} input - The PostToolUse hook's input.
214
+ * @param {{root: string, warnAt: number, now: number}} options
215
+ * @returns {{due: boolean, total: string, turn: string, context: number|null}|null}
216
+ */
217
+ function count(input, { root, warnAt, now }) {
60
218
  const transcript = join(dirname(input.transcript_path), input.session_id, 'subagents', `agent-${input.agent_id}.jsonl`);
61
219
  if (!existsSync(transcript)) return null;
62
220
  const dir = stateDir(root);
@@ -69,6 +227,7 @@ export function watch(input, { root, warnAt, now = Date.now() }) {
69
227
  }
70
228
  const { text, to } = gained(transcript, state.offset);
71
229
  let last = null;
230
+ const n = (v) => (typeof v === 'number' ? v : 0);
72
231
  for (const line of text.split('\n')) {
73
232
  if (!line.includes('"cache_read_input_tokens"')) continue;
74
233
  try {
@@ -78,6 +237,8 @@ export function watch(input, { root, warnAt, now = Date.now() }) {
78
237
  if (message?.id && typeof read === 'number') {
79
238
  state.read[message.id] = read;
80
239
  last = read;
240
+ // The whole context that turn held, which a turn after an expired cache writes again.
241
+ state.context = read + n(message.usage.cache_creation_input_tokens) + n(message.usage.input_tokens);
81
242
  }
82
243
  } catch {
83
244
  // A torn line loses one message's count.
@@ -92,18 +253,12 @@ export function watch(input, { root, warnAt, now = Date.now() }) {
92
253
  state.warned = crossed;
93
254
  mkdirSync(dir, { recursive: true });
94
255
  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`;
256
+ const size = (v) => (v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : `${Math.round(v / 1000)}k`);
99
257
  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
- },
258
+ due,
259
+ total: `${Math.round(total / 1e6)}M`,
260
+ turn: last === null ? '' : `, and each turn now re-reads about ${size(last)} tokens more`,
261
+ context: typeof state.context === 'number' ? state.context : null,
107
262
  };
108
263
  }
109
264
 
@@ -120,18 +275,18 @@ function prune(root, now) {
120
275
  }
121
276
 
122
277
  /**
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.
278
+ * The hook: reads the event from stdin, answers on stdout when a warning is due or an output is clipped,
279
+ * and exits 0 whatever happens — a hook that fails must not stop the run it watches.
125
280
  *
126
- * @param {{root: string, warnAt: number, stdin?: NodeJS.ReadableStream, stdout?: NodeJS.WritableStream}} options
281
+ * @param {{root: string, warnAt: number, clipAt?: number, stdin?: NodeJS.ReadableStream, stdout?: NodeJS.WritableStream}} options
127
282
  * @returns {Promise<number>}
128
283
  */
129
- export async function runCostWatch({ root, warnAt, stdin = process.stdin, stdout = process.stdout }) {
284
+ export async function runCostWatch({ root, warnAt, clipAt = 0, stdin = process.stdin, stdout = process.stdout }) {
130
285
  try {
131
286
  const chunks = [];
132
287
  for await (const chunk of stdin) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
133
288
  const now = Date.now();
134
- const answer = watch(JSON.parse(Buffer.concat(chunks).toString('utf8')), { root, warnAt, now });
289
+ const answer = watch(JSON.parse(Buffer.concat(chunks).toString('utf8')), { root, warnAt, clipAt, now });
135
290
  if (answer) stdout.write(`${JSON.stringify(answer)}\n`);
136
291
  if (Math.random() < 0.01) prune(root, now);
137
292
  } catch {
@@ -362,7 +362,9 @@ const RENDER = {
362
362
  table(['stage', 'runs', 'median', 'total', 'share', ''], d.cost.rows.map((r) => [el('span', { class: 'role' }, r.role), String(r.runs), money(r.median), money(r.total), pct(r.total, d.cost.total), bar([[r.total / d.cost.total, 'var(--pink)']])]), [1, 2, 3, 4]),
363
363
  d.cost.unpriced ? el('p', { class: 'note faint' }, `${d.cost.unpriced} run(s) on a model the price table does not know.`) : null] : null,
364
364
  d.context ? [el('h3', {}, 'context · every turn re-reads it all'),
365
- table(['stage', 'rounds', 'n', 'turns', 'calls/turn', 'starts at', 'peak', 'prompt'], d.context.rows.map((r, i, all) => [i > 0 && all[i - 1].role === r.role ? '' : el('span', { class: 'role' }, r.role), el('span', { class: 'faint' }, r.label), String(r.n), String(r.turns), r.callsPerTurn.toFixed(2), kTokens(r.start), kTokens(r.peak), r.prompt === null ? '—' : r.prompt.toLocaleString('en-US')]), [2, 3, 4, 5, 6, 7])] : null,
365
+ table(['stage', 'rounds', 'n', 'turns', 'calls/turn', 'starts at', 'peak', 'prompt'], d.context.rows.map((r, i, all) => [i > 0 && all[i - 1].role === r.role ? '' : el('span', { class: 'role' }, r.role), el('span', { class: 'faint' }, r.label), String(r.n), String(r.turns), r.callsPerTurn.toFixed(2), kTokens(r.start), kTokens(r.peak), r.prompt === null ? '—' : r.prompt.toLocaleString('en-US')]), [2, 3, 4, 5, 6, 7]),
366
+ d.context.reread?.length ? el('h3', {}, 'what the turns re-read') : null,
367
+ d.context.reread?.length ? table(['stage', 'rounds', 'opened with', 'rewrites', 'of spend', 'clipped'], d.context.reread.map((r) => [el('span', { class: 'role' }, r.role), String(r.n), r.fixed === null ? '—' : `${Math.round(r.fixed * 100)}%`, String(r.rewrites), r.rewriteShare === null ? '—' : `${Math.round(r.rewriteShare * 100)}%`, String(r.clipped)]), [1, 2, 3, 4, 5]) : null] : null,
366
368
  d.proportion ? [el('h3', {}, 'proportion · the size of a change against its chain'),
367
369
  table(['largest write', 'cycles', 'with a planner or architect', ''], d.proportion.rows.map((r) => [r.label, String(r.cycles), r.cycles ? `${r.designed} (${pct(r.designed, r.cycles)})` : '—', bar([[r.cycles ? r.designed / r.cycles : 0, 'var(--pink)']])]), [1, 2]),
368
370
  d.proportion.over ? el('p', { class: 'note warn' }, `${d.proportion.over.count} implementer round(s) wrote more files than one step may (${d.proportion.over.limit}); the largest wrote ${d.proportion.over.largest}.`) : null,
package/src/gate.mjs CHANGED
@@ -268,6 +268,8 @@ function append(path, entry) {
268
268
  *
269
269
  * - a rejection is cancelled by a pass launched after a fix went out for it: that review saw the fix. A
270
270
  * sibling launched with it, or a reviewer launched after it that saw no fix, cancels nothing;
271
+ * - a rejection is withdrawn by a later completion of the agent that made it, passing with no fix sent between:
272
+ * the same review judged again, which nothing else would settle;
271
273
  * - a rejection from a review that was already running when the edge's last round went out belongs to
272
274
  * that round, and makes no new one;
273
275
  * - the rest make this dispatch a round on the edge: one more than the rounds since the loop last
@@ -335,12 +337,15 @@ export function replay(entries, loops, forward = new Map()) {
335
337
  };
336
338
 
337
339
  /**
338
- * A source's waiting verdicts, settled: the rejections no later review of a fix cancelled, and whether a pass
339
- * closes its loops — one that began after the latest fix, with no rejection running beside it.
340
+ * A source's waiting verdicts, settled: the rejections no later review of a fix cancelled and no later completion of
341
+ * their own agent withdrew, and whether a pass closes its loops — one that began after the latest fix, with no
342
+ * rejection running beside it.
340
343
  */
341
344
  const settle = (source, batch) => {
342
345
  const passes = batch.filter((v) => !isLoopBack(v.verdict));
343
- const live = batch.filter((v) => isLoopBack(v.verdict) && !passes.some((p) => p.launch > v.at && fixBetween(source, v.at, p.launch)));
346
+ const live = batch.filter(
347
+ (v) => isLoopBack(v.verdict) && !passes.some((p) => p.launch > v.at && (p.agent === v.agent || fixBetween(source, v.at, p.launch))),
348
+ );
344
349
  return { live, closes: passes.some((p) => p.launch > (lastFix.get(source) ?? -1) && !live.some((v) => v.launch < p.at)) };
345
350
  };
346
351
 
@@ -461,7 +466,8 @@ export function replay(entries, loops, forward = new Map()) {
461
466
  const onward = [...forward].filter(([, sources]) => sources.has(source)).map(([stage]) => stage);
462
467
  return { source, token, rounds: s.n, cap, given: grace.get(key) ?? 0, issues, closing, targets: [...targetsOf(source)], onward };
463
468
  });
464
- const waiting = [...pending.entries()].flatMap(([source, batch]) => batch.filter((v) => isLoopBack(v.verdict)).map((v) => ({ source, verdict: v.verdict, issues: v.issues ?? [], index: v.at })));
469
+ // A rejection its own agent withdrew waits on no fix: counted as waiting, the next session inherited it.
470
+ const waiting = [...pending.entries()].flatMap(([source, batch]) => settle(source, batch).live.map((v) => ({ source, verdict: v.verdict, issues: v.issues ?? [], index: v.at })));
465
471
  return { effect: (to) => effect(to, entries.length), rounds: made, open, waiting };
466
472
  }
467
473
 
package/src/langfuse.mjs CHANGED
@@ -112,8 +112,11 @@ export function contextMode(setting) {
112
112
  return setting?.content === 'prompts' ? 'prompts' : setting?.content ? 'full' : null;
113
113
  }
114
114
 
115
- /** A round's shape, as metadata on its root: how many turns and calls it took, and how large its context and prompt were. */
116
- const SHAPE = ['turns', 'tool_calls', 'context_start', 'context_peak', 'prompt_chars'];
115
+ /**
116
+ * A round's shape, as metadata on its root: how many turns and calls it took, how large its context and prompt
117
+ * were, how often it wrote an expired cache again and how much, and how many outputs the cost watch clipped.
118
+ */
119
+ const SHAPE = ['turns', 'tool_calls', 'context_start', 'context_peak', 'prompt_chars', 'cache_rewrites', 'cache_rewritten', 'clipped'];
117
120
 
118
121
  /**
119
122
  * The OTLP span at the root of a dispatch's trace. Without its context, a run that spent tokens is a