@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
@@ -497,6 +497,11 @@ export async function composeProject(target, ctx, options = {}) {
497
497
  if (name in (profile.vocabulary ?? {})) continue;
498
498
  text = text.split(`{{${name}}}`).join(value);
499
499
  }
500
+ // A name the project deferred, with its reason, reads as not decided: a stage that meets it asks
501
+ // instead of guessing, and the raw placeholder it would otherwise read looks like an editing slip.
502
+ for (const [name, why] of Object.entries(profile.deferred ?? {})) {
503
+ if (typeof why === 'string' && why.trim() && /^[A-Z][A-Z0-9_]*$/.test(name)) text = text.split(`{{${name}}}`).join(`[${name}: not decided yet]`);
504
+ }
500
505
 
501
506
  // Only what has a comment syntax the notice knows. A layer that one day composes JSON
502
507
  // would be corrupted by it rather than marked, so it gets nothing and says nothing.
@@ -28,7 +28,7 @@ import { join, relative, resolve } from 'node:path';
28
28
  import { HARNESS, legacyHint, slugFor, snapshotsDir } from '../paths.mjs';
29
29
  import { isLoopBack, runOf } from '../transcripts.mjs';
30
30
  import { REQUIRES, byVersion, layerRootFor } from './compose.mjs';
31
- import { GRADUATION_AT, frontmatter, graduationTarget, list, pillFiles, roleGates } from './pills.mjs';
31
+ import { GRADUATION_AT, frontmatter, graduationTarget, list, pillFiles, roleGates, tidyRetired } from './pills.mjs';
32
32
  import { decodeProjectDir } from './stats.mjs';
33
33
  import { snapshot } from './snapshot.mjs';
34
34
  import { loadProject, projectGateDir, readLedger, replay } from '../gate.mjs';
@@ -200,7 +200,7 @@ export function applied(records, known) {
200
200
  * rate moves with the work. The control does not remove either, but it shows whether the lesson's
201
201
  * roles moved differently from everything else, which the lesson's own numbers cannot.
202
202
  *
203
- * @returns {{rel: string, date: string, before: {n: number, loops: number}, after: {n: number, loops: number}, control: {before: {n: number, loops: number}, after: {n: number, loops: number}}}[]}
203
+ * @returns {{rel: string, date: string, on: string|null, comparable: boolean, was?: string, before?: {n: number, loops: number}, after?: {n: number, loops: number}, control?: {before: {n: number, loops: number}, after: {n: number, loops: number}}}[]}
204
204
  */
205
205
  export function verified(records, known) {
206
206
  const out = [];
@@ -209,15 +209,28 @@ export function verified(records, known) {
209
209
  const s = rs.filter(pick);
210
210
  return { n: s.length, loops: s.filter((r) => isLoopBack(r.verdict)).length };
211
211
  };
212
- const mine = records.filter((r) => lesson.roles.includes(r.role) && readable(r.verdict));
212
+ const all = records.filter((r) => lesson.roles.includes(r.role) && readable(r.verdict));
213
213
  const others = records.filter((r) => r.role && !lesson.roles.includes(r.role) && readable(r.verdict));
214
214
  const pre = (r) => day(r.ts) < lesson.date;
215
215
  const post = (r) => day(r.ts) > lesson.date;
216
+ if (rate(all, pre).n < VERIFY_MIN || rate(all, post).n < VERIFY_MIN) continue;
217
+ // Read on one model and effort level: the one the roles ran on most after the lesson. A lesson written
218
+ // the week a role moved to a stricter model read as making it worse — one project's reviewer went from
219
+ // 2% to 29% "after" a lesson, and from Sonnet to Opus in the same days. Where the roles ran on it too
220
+ // little before the lesson, the two sides are not comparable, and that is said rather than a number.
221
+ const ranOn = (r) => (r.usage_model ? `${r.usage_model}${r.effort ? ` · ${r.effort}` : ''}` : null);
222
+ const tally = new Map();
223
+ for (const r of all.filter(post)) if (ranOn(r)) tally.set(ranOn(r), (tally.get(ranOn(r)) ?? 0) + 1);
224
+ const on = [...tally].sort((x, y) => y[1] - x[1])[0]?.[0] ?? null;
225
+ const mine = on ? all.filter((r) => ranOn(r) === on) : all;
216
226
  const before = rate(mine, pre);
217
227
  const after = rate(mine, post);
218
- if (before.n >= VERIFY_MIN && after.n >= VERIFY_MIN) {
219
- out.push({ rel: lesson.rel, date: lesson.date, before, after, control: { before: rate(others, pre), after: rate(others, post) } });
228
+ if (before.n < VERIFY_MIN || after.n < VERIFY_MIN) {
229
+ const was = [...new Set(all.filter(pre).map(ranOn).filter(Boolean))].join(', ') || 'no recorded model';
230
+ out.push({ rel: lesson.rel, date: lesson.date, on, was, comparable: false });
231
+ continue;
220
232
  }
233
+ out.push({ rel: lesson.rel, date: lesson.date, on, comparable: true, before, after, control: { before: rate(others, pre), after: rate(others, post) } });
221
234
  }
222
235
  return out;
223
236
  }
@@ -242,7 +255,7 @@ export function unfiled(known, filed) {
242
255
  * @param {string} target - The project directory.
243
256
  * @returns {Promise<{file: string, status: string, pill: string, target: string, date: string}[]>}
244
257
  */
245
- async function filedRequests(target) {
258
+ export async function filedRequests(target) {
246
259
  const dir = join(target, HARNESS, 'requests');
247
260
  const out = [];
248
261
  for (const f of (await readdir(dir).catch(() => [])).filter((f) => f.endsWith('.md'))) {
@@ -279,6 +292,8 @@ async function answerRequest(target, file, answer) {
279
292
  const pill = await readFile(join(target, fields.pill), 'utf8');
280
293
  const retired = /^status: .*$/m.test(pill) ? pill.replace(/^status: .*$/m, 'status: retired') : pill.replace(/^---\n/, '---\nstatus: retired\n');
281
294
  await writeFile(join(target, fields.pill), retired);
295
+ // And out of the way of the agents that open every pill to see which apply.
296
+ await tidyRetired(target);
282
297
  return { pill: fields.pill };
283
298
  }
284
299
 
@@ -691,8 +706,13 @@ export async function learn(argv, ctx) {
691
706
  for (const r of open) console.log(` open since ${r.date}: ${r.pill} → ${r.target}. \`nina upgrade\` to the release that answers it closes it`);
692
707
  if (checks.length === 0) console.log(` verify no lesson has ${VERIFY_MIN}+ readable verdicts on both sides of its date yet`);
693
708
  checks.forEach((c, i) => {
709
+ const head = ` ${i === 0 ? 'verify ' : ' '} ${c.rel.replace(/^\.claude\/pills\//, '')} (${c.date})`;
710
+ if (!c.comparable) {
711
+ console.log(`${head}: not comparable — the role ran on ${c.on} after it, and before it on ${c.was}`);
712
+ return;
713
+ }
694
714
  console.log(
695
- ` ${i === 0 ? 'verify ' : ' '} ${c.rel.replace(/^\.claude\/pills\//, '')} (${c.date}): ` +
715
+ `${head}${c.on ? `, on ${c.on}` : ''}: ` +
696
716
  `${pct(c.before.loops, c.before.n)} → ${pct(c.after.loops, c.after.n)} loop-back (n ${c.before.n} → ${c.after.n}); ` +
697
717
  `every other role ${pct(c.control.before.loops, c.control.before.n)} → ${pct(c.control.after.loops, c.control.after.n)}`,
698
718
  );
@@ -10,11 +10,11 @@
10
10
  * This reads the project, never the layers — pills are written at runtime, not composed.
11
11
  */
12
12
 
13
- import { readFile, readdir } from 'node:fs/promises';
13
+ import { mkdir, readFile, readdir, rename } from 'node:fs/promises';
14
14
  import { HARNESS } from '../paths.mjs';
15
15
  import { existsSync, readFileSync, statSync } from 'node:fs';
16
16
  import { spawnSync } from 'node:child_process';
17
- import { join, resolve } from 'node:path';
17
+ import { dirname, join, resolve } from 'node:path';
18
18
  import { REQUIRES, layerRootFor } from './compose.mjs';
19
19
 
20
20
  /** Fields every pill must carry, and what breaks without each one. */
@@ -33,6 +33,34 @@ const SEVERITIES = ['low', 'medium', 'high'];
33
33
  /** The statuses a pill may declare. */
34
34
  const STATUSES = ['active', 'retired'];
35
35
 
36
+ /**
37
+ * Where a retired pill goes, beside the directories agents read: every agent opens every pill in its own
38
+ * directory and in `shared/` to learn which apply, and in the first new project 16 KB of the 33 KB it opened
39
+ * on every dispatch was retired.
40
+ */
41
+ export const RETIRED = 'retired';
42
+
43
+ /**
44
+ * Moves every retired pill still where agents read it into `retired/<the directory it was read from>/`.
45
+ *
46
+ * @param {string} target - The project.
47
+ * @returns {Promise<string[]>} The pills moved, as paths relative to `.claude/pills/`.
48
+ */
49
+ export async function tidyRetired(target) {
50
+ const pillsDir = join(target, '.claude', 'pills');
51
+ if (!existsSync(pillsDir)) return [];
52
+ const moved = [];
53
+ for (const pill of await pillFiles(pillsDir)) {
54
+ if (!pill.dir || pill.dir === RETIRED || pill.dir.startsWith(`${RETIRED}/`)) continue;
55
+ if (frontmatter(await readFile(pill.path, 'utf8'))?.status !== 'retired') continue;
56
+ const to = join(pillsDir, RETIRED, pill.dir, pill.file);
57
+ await mkdir(dirname(to), { recursive: true });
58
+ await rename(pill.path, to);
59
+ moved.push(`${pill.dir}/${pill.file}`);
60
+ }
61
+ return moved;
62
+ }
63
+
36
64
  /**
37
65
  * How often the same lesson must be learned before it stops being an anecdote.
38
66
  *
@@ -184,6 +212,15 @@ export function graduationTarget(applies, gates) {
184
212
  export async function pillFiles(pillsDir) {
185
213
  const found = [];
186
214
  for (const entry of await readdir(pillsDir, { withFileTypes: true })) {
215
+ // Retired pills keep the directory they were read from, one level down, where no agent globs.
216
+ if (entry.isDirectory() && entry.name === RETIRED) {
217
+ for (const inner of await readdir(join(pillsDir, RETIRED), { withFileTypes: true })) {
218
+ const dir = inner.isDirectory() ? `${RETIRED}/${inner.name}` : RETIRED;
219
+ const files = inner.isDirectory() ? await readdir(join(pillsDir, dir)) : [inner.name];
220
+ for (const file of files) if (file.endsWith('.md')) found.push({ dir, file, path: join(pillsDir, dir, file) });
221
+ }
222
+ continue;
223
+ }
187
224
  if (entry.isDirectory()) {
188
225
  for (const file of await readdir(join(pillsDir, entry.name))) {
189
226
  if (file.endsWith('.md')) found.push({ dir: entry.name, file, path: join(pillsDir, entry.name, file) });
@@ -208,6 +245,7 @@ export function validate(pill, text, roles, today) {
208
245
  const label = `${pill.dir ? `${pill.dir}/` : ''}${pill.file}`;
209
246
  const problems = [];
210
247
  const notes = [];
248
+ const shelved = pill.dir === RETIRED || pill.dir.startsWith(`${RETIRED}/`);
211
249
 
212
250
  if (pill.dir === '') {
213
251
  problems.push(`${label} sits at the top level of pills/ — no agent globs there, so nobody reads it`);
@@ -224,6 +262,14 @@ export function validate(pill, text, roles, today) {
224
262
  }
225
263
 
226
264
  const applies = list(fields.applies_to);
265
+ // Where a retired pill sits says only where it was read from, and nobody reads it there now.
266
+ if (shelved) {
267
+ if (fields.status !== 'retired') problems.push(`${label} is ${fields.status || 'active'} but sits in ${RETIRED}/, where no agent reads it — move it back, or retire it`);
268
+ return { problems, notes, fields, label, citations: [], uncited: false };
269
+ }
270
+ if (fields.status === 'retired') {
271
+ notes.push(`${label} is retired but still where agents open it — \`nina pills --tidy\` moves it to ${RETIRED}/${pill.dir}/`);
272
+ }
227
273
  for (const role of applies) {
228
274
  if (!roles.includes(role)) {
229
275
  problems.push(`${label} applies_to "${role}", which this project does not compose — available: ${roles.join(', ')}`);
@@ -306,6 +352,12 @@ export async function pills(argv, ctx) {
306
352
  .sort();
307
353
 
308
354
  const today = new Date().toISOString().slice(0, 10);
355
+ if (argv.includes('--tidy')) {
356
+ const moved = await tidyRetired(target);
357
+ for (const m of moved) console.log(` moved ${m} → ${RETIRED}/${m}`);
358
+ console.log(moved.length ? `\npills: ${moved.length} retired pill(s) moved where no agent opens them\n` : 'pills: no retired pill where agents read\n');
359
+ return 0;
360
+ }
309
361
  const files = existsSync(pillsDir) ? await pillFiles(pillsDir) : [];
310
362
 
311
363
  // The directory itself is composed, so its presence says nothing. An empty corpus is the
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `nina runs` — what each piece of work cost: the pipeline's cycles in one project, newest first, each with
3
+ * the stages it went through, their rounds, what was sent back, how long it took and what it cost.
4
+ *
5
+ * `stats` answers per stage and over a window, which is the right question for a gate and the wrong one for
6
+ * the owner, whose question is what a feature, a spike or a fix came to. A cycle is the unit `stats` already
7
+ * sizes (`cyclesOf`): a session's rounds up to the verdict that closes one. It is named by the description
8
+ * the orchestrator gave its first dispatch, which is the one name the record keeps.
9
+ *
10
+ * nina runs [--project <dir>] [--last N] [--since YYYY-MM-DD]
11
+ */
12
+
13
+ import { existsSync } from 'node:fs';
14
+ import { join, resolve } from 'node:path';
15
+ import { HARNESS } from '../paths.mjs';
16
+ import { costOf } from '../prices.mjs';
17
+ import { isLoopBack, runOf } from '../transcripts.mjs';
18
+ import { amber, bold, dim, pink } from '../look.mjs';
19
+ import { storedRecords } from './snapshot.mjs';
20
+ import { cyclesOf, projectName } from './stats.mjs';
21
+
22
+ /** What ended a cycle, in words. */
23
+ const ENDS = {
24
+ closed: (last) => `ended at ${last.role} ${last.verdict}`,
25
+ unreadable: (last) => `ended at a ${last.role} whose verdict could not be read`,
26
+ next: () => 'ended where the next design began',
27
+ open: () => 'still open where the record ends',
28
+ };
29
+
30
+ /**
31
+ * One cycle, summed: when, how long, what it cost, and each stage's rounds and loop-backs in the order the
32
+ * stages first ran.
33
+ *
34
+ * @param {{runs: object[], end: string}} cycle
35
+ * @returns {{start: string, end: string, minutes: number|null, cost: number, unpriced: number, name: string, how: string, stages: {role: string, runs: number, rounds: number, back: number}[], files: number|null, peak: number|null}}
36
+ */
37
+ export function summarize(cycle) {
38
+ const rounds = cycle.runs;
39
+ const start = rounds.map((r) => String(r.ts)).sort()[0];
40
+ const end = rounds.map((r) => String(r.result_ts ?? r.ts)).sort().at(-1);
41
+ const minutes = Number.isFinite(Date.parse(end) - Date.parse(start)) ? Math.round((Date.parse(end) - Date.parse(start)) / 60_000) : null;
42
+ let cost = 0;
43
+ let unpriced = 0;
44
+ for (const r of rounds) {
45
+ if (!r.tokens || typeof r.tokens !== 'object') continue;
46
+ const c = costOf(r.tokens, r.usage_model);
47
+ if (c === null) unpriced += 1;
48
+ else cost += c;
49
+ }
50
+ const stages = new Map();
51
+ for (const r of rounds) {
52
+ const s = stages.get(r.role) ?? { role: r.role, runs: new Set(), rounds: 0, back: 0 };
53
+ s.runs.add(runOf(r));
54
+ s.rounds += 1;
55
+ if (isLoopBack(r.verdict)) s.back += 1;
56
+ stages.set(r.role, s);
57
+ }
58
+ const files = rounds.filter((r) => typeof r.files_touched === 'number').map((r) => r.files_touched);
59
+ const peaks = rounds.filter((r) => typeof r.context_peak === 'number').map((r) => r.context_peak);
60
+ return {
61
+ start,
62
+ end,
63
+ minutes,
64
+ cost,
65
+ unpriced,
66
+ name: String(rounds[0]?.desc ?? '').trim() || '(no description)',
67
+ how: ENDS[cycle.end](rounds.at(-1)),
68
+ stages: [...stages.values()].map((s) => ({ ...s, runs: s.runs.size })),
69
+ files: files.length ? Math.max(...files) : null,
70
+ peak: peaks.length ? Math.max(...peaks) : null,
71
+ };
72
+ }
73
+
74
+ /** A duration in minutes, as hours and minutes. */
75
+ const span = (m) => (m === null ? '—' : m >= 60 ? `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}` : `${m}m`);
76
+
77
+ /** A cost in dollars. */
78
+ const money = (n) => `$${n >= 100 ? n.toFixed(0) : n.toFixed(2)}`;
79
+
80
+ /**
81
+ * Runs the command.
82
+ *
83
+ * @param {string[]} argv
84
+ * @returns {Promise<number>}
85
+ */
86
+ export async function runs(argv) {
87
+ const arg = (name) => (argv.includes(name) ? argv[argv.indexOf(name) + 1] : null);
88
+ const dir = resolve(arg('--project') ?? '.');
89
+ if (!existsSync(join(dir, HARNESS, 'profile.json'))) {
90
+ console.error(` no ${HARNESS}/profile.json under ${dir} — run it inside a project, or name one with --project\n`);
91
+ return 1;
92
+ }
93
+ const since = arg('--since');
94
+ const last = Math.max(1, Number(arg('--last') ?? 10) || 10);
95
+ const records = (storedRecords(dir) ?? []).filter((r) => r.status !== 'denied' && (!since || String(r.ts).slice(0, 10) >= since));
96
+ if (records.length === 0) {
97
+ console.error(' no measured history for this project yet — `nina snapshot` captures it\n');
98
+ return 1;
99
+ }
100
+ const cycles = cyclesOf(records)
101
+ .map(summarize)
102
+ .sort((a, b) => b.start.localeCompare(a.start));
103
+ const shown = cycles.slice(0, last);
104
+ console.log(
105
+ ` ${bold(pink(projectName(records[0].project)))} · ${cycles.length} cycle(s)` +
106
+ dim(` newest first${cycles.length > shown.length ? `, the last ${shown.length} — --last N for more` : ''}; a cycle runs up to the verdict that closes it`),
107
+ );
108
+ for (const c of shown) {
109
+ const when = `${c.start.slice(0, 10)} ${c.start.slice(11, 16)}`;
110
+ console.log(`\n ${pink('▌')} ${bold(when)} · ${span(c.minutes)} · ${bold(money(c.cost))}${c.unpriced ? dim(` + ${c.unpriced} unpriced`) : ''} · ${dim(c.how)}`);
111
+ console.log(` ${c.name.length > 96 ? `${c.name.slice(0, 95)}…` : c.name}`);
112
+ const chain = c.stages.map((s) => {
113
+ const count = s.rounds > s.runs ? `${s.rounds} rounds` : `${s.rounds}`;
114
+ return `${pink(s.role)} ${count}${s.back ? amber(`, ${s.back} sent back`) : ''}`;
115
+ });
116
+ console.log(
117
+ ` ${chain.join(dim(' · '))}${c.files === null ? '' : dim(` · largest write ${c.files} file(s)`)}` +
118
+ (c.peak === null ? '' : dim(` · context up to ${Math.round(c.peak / 1000)}k`)),
119
+ );
120
+ }
121
+ const total = shown.reduce((a, c) => a + c.cost, 0);
122
+ const costliest = shown.reduce((a, c) => (c.cost > a.cost ? c : a), shown[0]);
123
+ console.log(
124
+ `\n ${shown.length} cycle(s): ${bold(money(total))} in all` +
125
+ (shown.length > 1 ? `; the costliest, ${money(costliest.cost)}, ${costliest.start.slice(0, 10)} — ${costliest.name.slice(0, 60)}` : '') +
126
+ '\n',
127
+ );
128
+ return 0;
129
+ }
@@ -8,7 +8,7 @@
8
8
  * the transcripts are hundreds of megabytes and growing.
9
9
  */
10
10
 
11
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
11
+ import { copyFile, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
12
12
  import { slugFor, snapshotsDir } from '../paths.mjs';
13
13
  import { createReadStream, existsSync, readFileSync, realpathSync } from 'node:fs';
14
14
  import { createInterface } from 'node:readline';
@@ -117,6 +117,40 @@ export function storedRecords(dir) {
117
117
  return null;
118
118
  }
119
119
 
120
+ /**
121
+ * Takes a project's lock, or says it is taken. The records and the cursors are written one after the other,
122
+ * and two snapshots of one project at once — a project's own detector and a global hook, both on every
123
+ * Stop — could leave the cursors of the scan that read further beside the records of the one that read less,
124
+ * and whatever lay between was never captured. One that finds the lock taken skips the project: the next
125
+ * snapshot reads from where this one leaves it. A lock names its process, so one left by a snapshot that
126
+ * died is taken over.
127
+ *
128
+ * @param {string} path - The lock file.
129
+ * @returns {Promise<(() => Promise<void>)|null>} Its release, or null when another snapshot holds it.
130
+ */
131
+ async function lock(path) {
132
+ for (let attempt = 0; attempt < 2; attempt += 1) {
133
+ try {
134
+ const handle = await open(path, 'wx');
135
+ await handle.writeFile(String(process.pid));
136
+ await handle.close();
137
+ return () => rm(path, { force: true });
138
+ } catch (error) {
139
+ if (error.code !== 'EEXIST') throw error;
140
+ const pid = Number(await readFile(path, 'utf8').catch(() => ''));
141
+ let alive = false;
142
+ try {
143
+ alive = pid > 0 && process.kill(pid, 0);
144
+ } catch (e) {
145
+ alive = e.code === 'EPERM';
146
+ }
147
+ if (alive) return null;
148
+ await rm(path, { force: true });
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+
120
154
  /**
121
155
  * Runs the snapshot.
122
156
  *
@@ -155,40 +189,56 @@ export async function snapshot(argv, ctx) {
155
189
 
156
190
  let totalNew = 0;
157
191
  for (const project of targets) {
158
- const file = join(outDir, `${project.slug}.jsonl`);
159
- const prior = rebuild ? [] : await readRecords(file);
160
- const own = rebuild ? {} : await readJson(join(outDir, `${project.slug}.state.json`));
161
- const cursors = rebuild ? {} : (own.cursors ?? legacy[project.slug]?.cursors ?? {});
162
-
163
- const scanned = await scanProject(project.dir, { cursors, records: prior });
164
- const records = scanned.records.map((r) => ({ project: project.slug, ...r }));
165
- if (records.length === 0) continue;
166
-
167
- const fresh = records.length - prior.length;
168
- // A prior record can change without the count changing — a field learned after it was
169
- // captured, filled in from its transcript — and a write gated on the count alone would
170
- // compute the backfill and throw it away.
171
- const changed =
172
- fresh !== 0 ||
173
- prior.length === 0 ||
174
- rebuild ||
175
- records.some((r, i) => JSON.stringify(r) !== JSON.stringify({ project: project.slug, ...prior[i] }));
176
- if (changed) await writeAtomic(file, records);
177
- await writeAtomicText(join(outDir, `${project.slug}.state.json`), `${JSON.stringify({ cursors: scanned.cursors, captured: records.length }, null, 2)}\n`);
178
- totalNew += Math.max(0, fresh);
179
-
180
- if (!quiet) {
181
- const span = `${String(records[0].ts).slice(0, 10)} → ${String(records.at(-1).ts).slice(0, 10)}`;
182
- console.log(
183
- ` ${project.slug.padEnd(48)} ${String(records.length).padStart(5)} rounds ` +
184
- `${String(fresh).padStart(5)} new ${span}`,
185
- );
192
+ const release = await lock(join(outDir, `${project.slug}.lock`));
193
+ if (!release) continue;
194
+ try {
195
+ totalNew += await snapshotProject(project, { outDir, rebuild, quiet, legacy });
196
+ } finally {
197
+ await release();
186
198
  }
187
199
  }
188
200
 
189
-
190
201
  if (!quiet) {
191
202
  console.log(`\n ${totalNew} new round${totalNew === 1 ? '' : 's'} captured → ${outDir}`);
192
203
  }
193
204
  return 0;
194
205
  }
206
+
207
+ /**
208
+ * Snapshots one project, under its lock.
209
+ *
210
+ * @returns {Promise<number>} How many rounds it captured that were not on record.
211
+ */
212
+ async function snapshotProject(project, { outDir, rebuild, quiet, legacy }) {
213
+ const file = join(outDir, `${project.slug}.jsonl`);
214
+ // The store is the only copy of history older than the transcripts, and a rebuild re-reads only what is
215
+ // still on disk: the record it replaces is kept beside it first.
216
+ if (rebuild && existsSync(file)) await copyFile(file, `${file}.before-rebuild-${new Date().toISOString().replace(/[:.]/g, '-')}`);
217
+ const prior = rebuild ? [] : await readRecords(file);
218
+ const own = rebuild ? {} : await readJson(join(outDir, `${project.slug}.state.json`));
219
+ const cursors = rebuild ? {} : (own.cursors ?? legacy[project.slug]?.cursors ?? {});
220
+
221
+ const scanned = await scanProject(project.dir, { cursors, records: prior });
222
+ const records = scanned.records.map((r) => ({ project: project.slug, ...r }));
223
+ if (records.length === 0) return 0;
224
+
225
+ const fresh = records.length - prior.length;
226
+ // A prior record can change without the count changing — a field learned after it was
227
+ // captured, filled in from its transcript — and a write gated on the count alone would
228
+ // compute the backfill and throw it away.
229
+ const changed =
230
+ fresh !== 0 ||
231
+ prior.length === 0 ||
232
+ rebuild ||
233
+ records.some((r, i) => JSON.stringify(r) !== JSON.stringify({ project: project.slug, ...prior[i] }));
234
+ if (changed) await writeAtomic(file, records);
235
+ await writeAtomicText(join(outDir, `${project.slug}.state.json`), `${JSON.stringify({ cursors: scanned.cursors, captured: records.length }, null, 2)}\n`);
236
+ if (!quiet) {
237
+ const span = `${String(records[0].ts).slice(0, 10)} → ${String(records.at(-1).ts).slice(0, 10)}`;
238
+ console.log(
239
+ ` ${project.slug.padEnd(48)} ${String(records.length).padStart(5)} rounds ` +
240
+ `${String(fresh).padStart(5)} new ${span}`,
241
+ );
242
+ }
243
+ return Math.max(0, fresh);
244
+ }
@@ -223,6 +223,40 @@ function costReport(records) {
223
223
  console.log(bold(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`));
224
224
  }
225
225
 
226
+ /**
227
+ * How each stage's rounds spent their context: turns, tool calls sent per turn, and the context a turn
228
+ * re-read when the round began and at its largest. Every turn is billed the whole context again, so the
229
+ * cost of a round is its turns times its context, and a round that starts where a long one ended pays for
230
+ * all of it on every turn. The first rounds of a run are set beside the later ones, which are resumes.
231
+ *
232
+ * @param {object[]} records - The rounds in the window.
233
+ */
234
+ function contextReport(records) {
235
+ const shaped = records.filter((r) => typeof r.turns === 'number' && r.turns > 0);
236
+ if (shaped.length === 0) return;
237
+ const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
238
+ const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
239
+ console.log(heading('context', 'every turn re-reads the whole context: a round costs its turns times its size, and a resumed one starts where it ended'));
240
+ console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padEnd(8)}${'n'.padStart(4)}${'turns'.padStart(7)}${'calls/turn'.padStart(12)}${'starts at'.padStart(11)}${'peak'.padStart(8)}`));
241
+ const byRole = new Map();
242
+ for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
243
+ for (const [role, rounds] of [...byRole].sort((a, b) => b[1].reduce((x, r) => x + r.turns, 0) - a[1].reduce((x, r) => x + r.turns, 0))) {
244
+ [
245
+ ['first', rounds.filter((r) => (r.round ?? 1) === 1)],
246
+ ['resumed', rounds.filter((r) => (r.round ?? 1) > 1)],
247
+ ].forEach(([label, group], i) => {
248
+ if (group.length === 0) return;
249
+ const turns = group.reduce((a, r) => a + r.turns, 0);
250
+ const calls = group.reduce((a, r) => a + (r.tool_calls ?? 0), 0);
251
+ const name = i === 0 || !rounds.some((r) => (r.round ?? 1) === 1) ? pink(role) + ' '.repeat(Math.max(20 - role.length, 1)) : ' '.repeat(20);
252
+ console.log(
253
+ ` ${name}${dim(label.padEnd(8))}${String(group.length).padStart(4)}${String(median(group.map((r) => r.turns))).padStart(7)}` +
254
+ `${(calls / turns).toFixed(2).padStart(12)}${k(median(group.map((r) => r.context_start ?? 0))).padStart(11)}${k(median(group.map((r) => r.context_peak ?? 0))).padStart(8)}`,
255
+ );
256
+ });
257
+ }
258
+ }
259
+
226
260
  /**
227
261
  * Whether a model a run used is the one a spec declares: an alias (`opus`) names a family — a whole
228
262
  * segment of the id, wherever it sits, so `claude-3-sonnet` is a sonnet — anything else names a model
@@ -348,29 +382,15 @@ const WRITERS = new Set(['implementer', 'solidity-dev']);
348
382
  const DESIGNERS = new Set(['planner', 'architect']);
349
383
 
350
384
  /**
351
- * How the size of a change sat against the weight of the chain it went through: the core's first hard
352
- * rule, which nothing had measured. A cycle is a session's dispatches up to the verdict that closes one
353
- * (`CYCLE_ENDS`), so a loop-back's fix rounds stay with the design that preceded them — the owner's own
354
- * prompts were tried as the boundary first, and every "pode seguir" after a spec cut a pipeline in two.
355
- * Two more things end a cycle, both found in real sessions: a design stage dispatched after code was
356
- * written starts the next change — the light chain has no qa to close it, and it was absorbed into the
357
- * design that followed — unless the verdict before it sent work back, which the graph routes into both
358
- * designers; and a closing stage whose verdict cannot be read still closes it, counted as unreadable,
359
- * because a qa run with no verdict line otherwise let one cycle swallow a dozen pipelines. A cycle is
360
- * sized by the most files any one of its writers wrote, not their sum, so three fix rounds on two files
361
- * stay two files; and only edits through the edit tools count, so a file written from a shell is missed
362
- * and the size is a floor.
385
+ * A session's records cut into cycles: each up to the verdict that closes one, or to the next design stage
386
+ * after code was written unless a loop-back sent the work there, or to a closing stage whose verdict cannot
387
+ * be read (see `proportionReport`). What `stats` sizes and `nina runs` prices.
363
388
  *
364
- * It reports a distribution, not verdicts. A critical path is gated in full at any size and cannot be
365
- * seen from a file count, and one spec legitimately covers sibling steps (`ONE-SPEC`), so a cycle with
366
- * no architect of its own is not by itself a skipped gate. What the table can show is the shape: how
367
- * often a design stage ran for a change of one or two files, and how the large changes were carried.
368
- *
369
- * @param {object[]} records - The runs in the window.
389
+ * @param {object[]} records - Records of any number of sessions and projects.
390
+ * @returns {{runs: object[], end: 'closed'|'unreadable'|'next'|'open'}[]} In the order they happened within
391
+ * each session.
370
392
  */
371
- function proportionReport(records, stepLimit = () => null) {
372
- const measured = records.filter((r) => WRITERS.has(r.role) && typeof r.files_touched === 'number');
373
- if (measured.length === 0) return;
393
+ export function cyclesOf(records) {
374
394
  /** @type {Map<string, object[]>} */
375
395
  const bySession = new Map();
376
396
  for (const r of [...records].sort((a, b) => String(a.ts).localeCompare(String(b.ts)))) {
@@ -398,6 +418,34 @@ function proportionReport(records, stepLimit = () => null) {
398
418
  }
399
419
  end('open');
400
420
  }
421
+ return cycles;
422
+ }
423
+
424
+ /**
425
+ * How the size of a change sat against the weight of the chain it went through: the core's first hard
426
+ * rule, which nothing had measured. A cycle is a session's dispatches up to the verdict that closes one
427
+ * (`CYCLE_ENDS`), so a loop-back's fix rounds stay with the design that preceded them — the owner's own
428
+ * prompts were tried as the boundary first, and every "pode seguir" after a spec cut a pipeline in two.
429
+ * Two more things end a cycle, both found in real sessions: a design stage dispatched after code was
430
+ * written starts the next change — the light chain has no qa to close it, and it was absorbed into the
431
+ * design that followed — unless the verdict before it sent work back, which the graph routes into both
432
+ * designers; and a closing stage whose verdict cannot be read still closes it, counted as unreadable,
433
+ * because a qa run with no verdict line otherwise let one cycle swallow a dozen pipelines. A cycle is
434
+ * sized by the most files any one of its writers wrote, not their sum, so three fix rounds on two files
435
+ * stay two files; and only edits through the edit tools count, so a file written from a shell is missed
436
+ * and the size is a floor.
437
+ *
438
+ * It reports a distribution, not verdicts. A critical path is gated in full at any size and cannot be
439
+ * seen from a file count, and one spec legitimately covers sibling steps (`ONE-SPEC`), so a cycle with
440
+ * no architect of its own is not by itself a skipped gate. What the table can show is the shape: how
441
+ * often a design stage ran for a change of one or two files, and how the large changes were carried.
442
+ *
443
+ * @param {object[]} records - The runs in the window.
444
+ */
445
+ function proportionReport(records, stepLimit = () => null) {
446
+ const measured = records.filter((r) => WRITERS.has(r.role) && typeof r.files_touched === 'number');
447
+ if (measured.length === 0) return;
448
+ const cycles = cyclesOf(records);
401
449
  const rows = [
402
450
  { label: '1–2 files', from: 1, to: 2 },
403
451
  { label: '3–9 files', from: 3, to: 9 },
@@ -708,6 +756,7 @@ export async function stats(argv, ctx) {
708
756
  }
709
757
 
710
758
  costReport(records.filter((r) => r.status !== 'denied'));
759
+ contextReport(records.filter((r) => r.status !== 'denied'));
711
760
  modelReport(records.filter((r) => r.status !== 'denied'));
712
761
  proportionReport(records.filter((r) => r.status !== 'denied'), stepLimitOf(ctx));
713
762