@xhulz/nina 0.34.0 → 0.36.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 (267) hide show
  1. package/bin/nina.mjs +222 -48
  2. package/package.json +1 -1
  3. package/releases/0.35.0/core/GAPS.md +125 -0
  4. package/releases/0.35.0/core/answered.json +29 -0
  5. package/releases/0.35.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  6. package/releases/0.35.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  7. package/releases/0.35.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  8. package/releases/0.35.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  9. package/releases/0.35.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  10. package/releases/0.35.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  11. package/releases/0.35.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  12. package/releases/0.35.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  13. package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  14. package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  15. package/releases/0.35.0/core/tree/.claude/agents/architect.md +127 -0
  16. package/releases/0.35.0/core/tree/.claude/agents/dba.md +110 -0
  17. package/releases/0.35.0/core/tree/.claude/agents/devops.md +114 -0
  18. package/releases/0.35.0/core/tree/.claude/agents/implementer.md +156 -0
  19. package/releases/0.35.0/core/tree/.claude/agents/integration-tester.md +168 -0
  20. package/releases/0.35.0/core/tree/.claude/agents/planner.md +132 -0
  21. package/releases/0.35.0/core/tree/.claude/agents/qa.md +153 -0
  22. package/releases/0.35.0/core/tree/.claude/agents/reviewer.md +183 -0
  23. package/releases/0.35.0/core/tree/.claude/agents/secops.md +121 -0
  24. package/releases/0.35.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  25. package/releases/0.35.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  26. package/releases/0.35.0/core/tree/.claude/agents-overview.md +64 -0
  27. package/releases/0.35.0/core/tree/.claude/frontend.md +10 -0
  28. package/releases/0.35.0/core/tree/.claude/graph.md +75 -0
  29. package/releases/0.35.0/core/tree/.claude/patterns.md +213 -0
  30. package/releases/0.35.0/core/tree/.claude/pills/README.md +146 -0
  31. package/releases/0.35.0/core/tree/.claude/pipeline.md +101 -0
  32. package/releases/0.35.0/core/tree/.claude/retrieval.md +103 -0
  33. package/releases/0.35.0/core/tree/.claude/router.md +198 -0
  34. package/releases/0.35.0/core/tree/.claude/templates/integration.md +45 -0
  35. package/releases/0.35.0/core/tree/CLAUDE.md +161 -0
  36. package/releases/0.35.0/core/tree/scripts/cost-watch.mjs +18 -0
  37. package/releases/0.35.0/core/tree/scripts/edit-guard.mjs +16 -0
  38. package/releases/0.35.0/core/tree/scripts/harness-check.mjs +71 -0
  39. package/releases/0.35.0/core/tree/scripts/loop-gate.mjs +19 -0
  40. package/releases/0.35.0/core/vocabulary.json +13 -0
  41. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  42. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  43. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  44. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  45. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  46. package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  47. package/releases/0.35.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  48. package/releases/0.35.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  49. package/releases/0.35.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  50. package/releases/0.35.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  51. package/releases/0.35.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  52. package/releases/0.35.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  53. package/releases/0.35.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.35.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  55. package/releases/0.35.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  56. package/releases/0.35.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  57. package/releases/0.35.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  58. package/releases/0.35.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  59. package/releases/0.35.0/surfaces/db/tree/.claude/graph.md +8 -0
  60. package/releases/0.35.0/surfaces/db/tree/.claude/patterns.md +19 -0
  61. package/releases/0.35.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  62. package/releases/0.35.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  63. package/releases/0.35.0/surfaces/db/tree/.claude/router.md +4 -0
  64. package/releases/0.35.0/surfaces/db/tree/CLAUDE.md +17 -0
  65. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  66. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  67. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  68. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  69. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  70. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  71. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  72. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  73. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  74. package/releases/0.35.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  75. package/releases/0.35.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  76. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  77. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  78. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  79. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  80. package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  81. package/releases/0.35.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  82. package/releases/0.35.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  83. package/releases/0.35.0/surfaces/frontend/tree/.claude/router.md +14 -0
  84. package/releases/0.35.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  85. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  86. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  87. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  88. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  89. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  90. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  91. package/releases/0.35.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  92. package/releases/0.35.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  93. package/releases/0.35.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  94. package/releases/0.35.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  95. package/releases/0.35.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  96. package/releases/0.35.0/surfaces/integrations/tree/.claude/router.md +6 -0
  97. package/releases/0.35.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  98. package/releases/0.35.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  99. package/releases/0.35.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  100. package/releases/0.35.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  101. package/releases/0.35.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  102. package/releases/0.35.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  103. package/releases/0.35.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  104. package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  105. package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  106. package/releases/0.35.0/surfaces/money/tree/.claude/graph.md +2 -0
  107. package/releases/0.35.0/surfaces/money/tree/.claude/patterns.md +49 -0
  108. package/releases/0.35.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  109. package/releases/0.35.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  110. package/releases/0.35.0/surfaces/money/tree/CLAUDE.md +11 -0
  111. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  112. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  113. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  114. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  115. package/releases/0.35.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  116. package/releases/0.35.0/surfaces/pii/tree/CLAUDE.md +5 -0
  117. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  118. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  119. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  120. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  121. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  122. package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  123. package/releases/0.35.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  124. package/releases/0.35.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  125. package/releases/0.36.0/core/GAPS.md +125 -0
  126. package/releases/0.36.0/core/answered.json +29 -0
  127. package/releases/0.36.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  128. package/releases/0.36.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  129. package/releases/0.36.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  130. package/releases/0.36.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  131. package/releases/0.36.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  132. package/releases/0.36.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  133. package/releases/0.36.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  134. package/releases/0.36.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  135. package/releases/0.36.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  136. package/releases/0.36.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  137. package/releases/0.36.0/core/tree/.claude/agents/architect.md +127 -0
  138. package/releases/0.36.0/core/tree/.claude/agents/dba.md +110 -0
  139. package/releases/0.36.0/core/tree/.claude/agents/devops.md +114 -0
  140. package/releases/0.36.0/core/tree/.claude/agents/implementer.md +156 -0
  141. package/releases/0.36.0/core/tree/.claude/agents/integration-tester.md +168 -0
  142. package/releases/0.36.0/core/tree/.claude/agents/planner.md +132 -0
  143. package/releases/0.36.0/core/tree/.claude/agents/qa.md +153 -0
  144. package/releases/0.36.0/core/tree/.claude/agents/reviewer.md +183 -0
  145. package/releases/0.36.0/core/tree/.claude/agents/secops.md +121 -0
  146. package/releases/0.36.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  147. package/releases/0.36.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  148. package/releases/0.36.0/core/tree/.claude/agents-overview.md +64 -0
  149. package/releases/0.36.0/core/tree/.claude/frontend.md +10 -0
  150. package/releases/0.36.0/core/tree/.claude/graph.md +75 -0
  151. package/releases/0.36.0/core/tree/.claude/patterns.md +213 -0
  152. package/releases/0.36.0/core/tree/.claude/pills/README.md +146 -0
  153. package/releases/0.36.0/core/tree/.claude/pipeline.md +101 -0
  154. package/releases/0.36.0/core/tree/.claude/retrieval.md +103 -0
  155. package/releases/0.36.0/core/tree/.claude/router.md +198 -0
  156. package/releases/0.36.0/core/tree/.claude/templates/integration.md +45 -0
  157. package/releases/0.36.0/core/tree/CLAUDE.md +161 -0
  158. package/releases/0.36.0/core/tree/scripts/cost-watch.mjs +18 -0
  159. package/releases/0.36.0/core/tree/scripts/edit-guard.mjs +16 -0
  160. package/releases/0.36.0/core/tree/scripts/harness-check.mjs +71 -0
  161. package/releases/0.36.0/core/tree/scripts/loop-gate.mjs +19 -0
  162. package/releases/0.36.0/core/vocabulary.json +13 -0
  163. package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  164. package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  165. package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  166. package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  167. package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  168. package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  169. package/releases/0.36.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  170. package/releases/0.36.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  171. package/releases/0.36.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  172. package/releases/0.36.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  173. package/releases/0.36.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  174. package/releases/0.36.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  175. package/releases/0.36.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  176. package/releases/0.36.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  177. package/releases/0.36.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  178. package/releases/0.36.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  179. package/releases/0.36.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  180. package/releases/0.36.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  181. package/releases/0.36.0/surfaces/db/tree/.claude/graph.md +8 -0
  182. package/releases/0.36.0/surfaces/db/tree/.claude/patterns.md +19 -0
  183. package/releases/0.36.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  184. package/releases/0.36.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  185. package/releases/0.36.0/surfaces/db/tree/.claude/router.md +4 -0
  186. package/releases/0.36.0/surfaces/db/tree/CLAUDE.md +17 -0
  187. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  188. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  189. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  190. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  191. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  192. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  193. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  194. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  195. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  196. package/releases/0.36.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  197. package/releases/0.36.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  198. package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  199. package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  200. package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  201. package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  202. package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  203. package/releases/0.36.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  204. package/releases/0.36.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  205. package/releases/0.36.0/surfaces/frontend/tree/.claude/router.md +14 -0
  206. package/releases/0.36.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  207. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  208. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  209. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  210. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  211. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  212. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  213. package/releases/0.36.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  214. package/releases/0.36.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  215. package/releases/0.36.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  216. package/releases/0.36.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  217. package/releases/0.36.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  218. package/releases/0.36.0/surfaces/integrations/tree/.claude/router.md +6 -0
  219. package/releases/0.36.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  220. package/releases/0.36.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  221. package/releases/0.36.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  222. package/releases/0.36.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  223. package/releases/0.36.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  224. package/releases/0.36.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  225. package/releases/0.36.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  226. package/releases/0.36.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  227. package/releases/0.36.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  228. package/releases/0.36.0/surfaces/money/tree/.claude/graph.md +2 -0
  229. package/releases/0.36.0/surfaces/money/tree/.claude/patterns.md +49 -0
  230. package/releases/0.36.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  231. package/releases/0.36.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  232. package/releases/0.36.0/surfaces/money/tree/CLAUDE.md +11 -0
  233. package/releases/0.36.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  234. package/releases/0.36.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  235. package/releases/0.36.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  236. package/releases/0.36.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  237. package/releases/0.36.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  238. package/releases/0.36.0/surfaces/pii/tree/CLAUDE.md +5 -0
  239. package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  240. package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  241. package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  242. package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  243. package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  244. package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  245. package/releases/0.36.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  246. package/releases/0.36.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  247. package/src/agentrun.mjs +89 -9
  248. package/src/commands/check.mjs +12 -8
  249. package/src/commands/compose.mjs +85 -9
  250. package/src/commands/export.mjs +8 -16
  251. package/src/commands/gate.mjs +19 -13
  252. package/src/commands/init.mjs +13 -4
  253. package/src/commands/learn.mjs +17 -18
  254. package/src/commands/pipeline.mjs +4 -4
  255. package/src/commands/release.mjs +21 -3
  256. package/src/commands/runs.mjs +14 -8
  257. package/src/commands/snapshot.mjs +63 -64
  258. package/src/commands/stats.mjs +98 -30
  259. package/src/commands/status.mjs +2 -2
  260. package/src/commands/upgrade.mjs +131 -25
  261. package/src/guard.mjs +7 -37
  262. package/src/paths.mjs +50 -1
  263. package/src/prices.mjs +7 -7
  264. package/src/store.mjs +158 -0
  265. package/src/transcripts.mjs +15 -4
  266. package/src/vocabulary.mjs +12 -1
  267. package/src/wiring.mjs +106 -41
@@ -6,16 +6,15 @@
6
6
  * already wrote that the new harness has nowhere to put. A slot that was renamed or dropped
7
7
  * takes the project's own text out of the composition with it, silently, because a fragment
8
8
  * nothing references composes to nothing and no check notices. So this reports first and
9
- * writes only when told to, and what it writes is one field: the version. Reconciling the
10
- * fragments is work, and work does not get done by a flag.
9
+ * writes only when told to. Reconciling the fragments is work, and work does not get done by a flag.
11
10
  */
12
11
 
13
- import { readFile, readdir, writeFile } from 'node:fs/promises';
12
+ import { readFile, readdir } from 'node:fs/promises';
14
13
  import { spawnSync } from 'node:child_process';
15
- import { HARNESS, legacyHint } from '../paths.mjs';
16
- import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { HARNESS, describeMove, interruptedMove, legacyHint, moveJournal, movedFrom } from '../paths.mjs';
15
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
17
16
  import { dirname, join, resolve } from 'node:path';
18
- import { composeProject, composedPaths, defaultedSlots, layerRootFor, walk } from './compose.mjs';
17
+ import { REMOVED, composeProject, composedPaths, defaultedSlots, layerRootFor, walk } from './compose.mjs';
19
18
  import { NEEDS, detected } from '../surfaces.mjs';
20
19
  import { filledSlots, projectSlots, referencedVocabulary } from './check.mjs';
21
20
  import { EXPECT_ENV } from '../expected.mjs';
@@ -25,6 +24,9 @@ import { missingFragment, missingWiring, shippedScripts } from '../wiring.mjs';
25
24
  import { runSteps } from '../steps.mjs';
26
25
  import { defaultVocabulary } from '../vocabulary.mjs';
27
26
 
27
+ /** What stops a move from outside it: Ctrl-C, a `kill`, the terminal closing. */
28
+ const SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
29
+
28
30
  /** Banner lines, which are for a person watching and only noise inside a captured log. */
29
31
  const BANNER = /^[\s█╗╔╝║═╚▄▀]*$|harness orchestration ·/;
30
32
 
@@ -75,7 +77,7 @@ function nina(ctx, target, args) {
75
77
  cwd: target,
76
78
  encoding: 'utf8',
77
79
  });
78
- const out = [result.stdout ?? '', result.stderr ?? '']
80
+ const out = [result.stdout ?? '', result.stderr ?? '', result.signal ? `stopped by ${result.signal}` : '']
79
81
  .join('\n')
80
82
  .split('\n')
81
83
  .filter((line) => line.trim() && !BANNER.test(line))
@@ -121,7 +123,7 @@ function shell(target, script) {
121
123
  encoding: 'utf8',
122
124
  env: { ...process.env, PATH: `${join(target, 'node_modules', '.bin')}:${process.env.PATH ?? ''}` },
123
125
  });
124
- const out = [result.stdout ?? '', result.stderr ?? ''].join('\n').split('\n').filter((l) => l.trim()).join('\n');
126
+ const out = [result.stdout ?? '', result.stderr ?? '', result.signal ? `stopped by ${result.signal}` : ''].join('\n').split('\n').filter((l) => l.trim()).join('\n');
125
127
  return { ok: result.status === 0, out, summary: summarise(out) };
126
128
  }
127
129
 
@@ -157,6 +159,74 @@ function summarise(out) {
157
159
  return [...lines].reverse().find((l) => /^\w[\w ]*:/.test(l))?.trim();
158
160
  }
159
161
 
162
+ /**
163
+ * Puts a project back on the version a move started from: the pin, then the files the move composed for the
164
+ * first time removed, a recompose, and every file it could touch written back as it was — the recompose alone
165
+ * would write the old composition over the owner's own edits and call it restored.
166
+ *
167
+ * @param {{root: string}} ctx - CLI context.
168
+ * @param {string} target - The project.
169
+ * @param {string} profilePath - Its profile.
170
+ * @param {object} profile - The profile as read, whose pin is set back.
171
+ * @param {string} version - The version the move started from.
172
+ * @param {string[]} created - Paths the move composed where nothing was.
173
+ * @param {Map<string, Buffer>} saved - Every file the move could touch, as it was.
174
+ * @returns {boolean} Whether the recompose passed.
175
+ */
176
+ function restore(ctx, target, profilePath, profile, version, created, saved) {
177
+ profile.core = version;
178
+ writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
179
+ for (const p of created) rmSync(join(target, p), { force: true });
180
+ const recomposed = nina(ctx, target, ['compose', '--project', target]);
181
+ for (const [p, bytes] of saved) {
182
+ mkdirSync(dirname(join(target, p)), { recursive: true });
183
+ writeFileSync(join(target, p), bytes);
184
+ }
185
+ return recomposed.ok;
186
+ }
187
+
188
+ /**
189
+ * `nina upgrade --abort`: undoes a move that was stopped before it finished, from what it wrote down first.
190
+ *
191
+ * @param {{root: string}} ctx - CLI context.
192
+ * @param {string} target - The project.
193
+ * @param {string} profilePath - Its profile.
194
+ * @param {object} profile - The profile as read.
195
+ * @param {ReturnType<typeof interruptedMove>} stopped - The move it was left in.
196
+ * @returns {number} Process exit code.
197
+ */
198
+ function abort(ctx, target, profilePath, profile, stopped) {
199
+ if (!stopped) {
200
+ console.log(' no upgrade was stopped half-way here — there is nothing to undo.\n');
201
+ return 0;
202
+ }
203
+ if (stopped.unreadable) {
204
+ console.error(
205
+ ` ${describeMove(stopped)} Nothing can be undone from it: set "core" in ${HARNESS}/profile.json back by hand, run \`nina compose\`, ` +
206
+ `and delete ${HARNESS}/upgrade.json.\n`,
207
+ );
208
+ return 1;
209
+ }
210
+ const saved = new Map();
211
+ for (const p of stopped.saved ?? []) {
212
+ const copy = join(movedFrom(target), p);
213
+ if (existsSync(copy)) saved.set(p, readFileSync(copy));
214
+ }
215
+ const lost = (stopped.saved ?? []).filter((p) => !saved.has(p));
216
+ if (lost.length > 0) {
217
+ console.error(` ${lost.length} of the files the move kept are missing from ${HARNESS}/upgrade-saved/, so it cannot be undone exactly: ${lost.slice(0, 5).join(', ')}${lost.length > 5 ? ', …' : ''}\n`);
218
+ return 1;
219
+ }
220
+ const recomposed = restore(ctx, target, profilePath, profile, stopped.from, stopped.created ?? [], saved);
221
+ rmSync(moveJournal(target), { force: true });
222
+ rmSync(movedFrom(target), { recursive: true, force: true });
223
+ console.log(
224
+ ` undid the move to ${stopped.to}: pinned ${stopped.from} again and put ${saved.size} file(s) back as they were.` +
225
+ `${recomposed ? '' : ' Recomposing failed along the way — run `nina compose` yourself.'} Run the move again when ready.\n`,
226
+ );
227
+ return recomposed ? 0 : 1;
228
+ }
229
+
160
230
  /**
161
231
  * `nina upgrade`.
162
232
  *
@@ -176,6 +246,15 @@ export async function upgrade(argv, ctx) {
176
246
  }
177
247
  const profile = JSON.parse(await readFile(profilePath, 'utf8'));
178
248
 
249
+ // A move stopped half-way comes before anything else asked of this command: its pin may already name the
250
+ // version it was moving to, and "already pins" left the tree half-moved for good.
251
+ const stopped = interruptedMove(target);
252
+ if (argv.includes('--abort')) return abort(ctx, target, profilePath, profile, stopped);
253
+ if (stopped) {
254
+ console.error(` ${describeMove(stopped)} \`nina upgrade --abort --project ${target}\` undoes it; then run the move again.\n`);
255
+ return 1;
256
+ }
257
+
179
258
  const to = arg('--to');
180
259
  if (!to) {
181
260
  console.error(' usage: nina upgrade --to <version> [--project <dir>] [--apply]\n');
@@ -354,10 +433,6 @@ export async function upgrade(argv, ctx) {
354
433
  }
355
434
 
356
435
  const previous = profile.core;
357
- const pin = async (version) => {
358
- profile.core = version;
359
- await writeFile(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
360
- };
361
436
 
362
437
  // What was already failing is not this upgrade's doing, and rolling back for it would blame
363
438
  // the move for a problem the project brought with it. Measured before anything is written,
@@ -437,8 +512,12 @@ export async function upgrade(argv, ctx) {
437
512
  const keptEdited = dropped.filter((p) => edited.has(p));
438
513
  // Every file the move can touch, as it is now, so a rollback puts the tree back exactly — the
439
514
  // recompose alone would write the old composition over the owner's own edits and call it restored.
515
+ // That is every path either version composes for any surface, not only the declared ones: compose moves
516
+ // a file gated on an undeclared surface to .nina/removed/, and one left out of here stayed there after a
517
+ // rollback that said the tree was as it was.
518
+ const reachable = [...(await composedPaths(from.dir, offeredBefore)), ...(await composedPaths(onto.dir, available))];
440
519
  const saved = new Map();
441
- for (const p of new Set([...pathsBefore, ...pathsAfter])) {
520
+ for (const p of new Set([...pathsBefore, ...pathsAfter, ...reachable])) {
442
521
  const entry = entryAt(p);
443
522
  if (entry?.isFile()) saved.set(p, readFileSync(join(target, p)));
444
523
  }
@@ -449,7 +528,31 @@ export async function upgrade(argv, ctx) {
449
528
  writeFileSync(copy, saved.get(p) ?? readFileSync(join(target, p)));
450
529
  }
451
530
 
452
- await pin(to);
531
+ // All of it written down before the pin moves, copies first and the journal last, so a journal always has
532
+ // its copies. A move stopped from here on — Ctrl-C, a closed terminal, a crash — left the pin moved, the tree
533
+ // half-composed, and the owner's own edits, held only in this process's memory, gone; now the next run says
534
+ // so, and `--abort` undoes it.
535
+ rmSync(movedFrom(target), { recursive: true, force: true });
536
+ for (const [p, bytes] of saved) {
537
+ mkdirSync(dirname(join(movedFrom(target), p)), { recursive: true });
538
+ writeFileSync(join(movedFrom(target), p), bytes);
539
+ }
540
+ const journal = { from: previous, to, started: new Date().toISOString(), created, saved: [...saved.keys()] };
541
+ writeFileSync(`${moveJournal(target)}.tmp`, `${JSON.stringify(journal, null, 2)}\n`);
542
+ renameSync(`${moveJournal(target)}.tmp`, moveJournal(target));
543
+ // Ctrl-C reaches the step running in a child too, which dies of it, and its step fails: with this process
544
+ // still alive, that is an ordinary failed step, and the move rolls back. Without a listener this process died
545
+ // first, half-way through, with nothing left to roll anything back.
546
+ const hold = () => {};
547
+ for (const s of SIGNALS) process.on(s, hold);
548
+ const settle = () => {
549
+ for (const s of SIGNALS) process.off(s, hold);
550
+ rmSync(moveJournal(target), { force: true });
551
+ rmSync(movedFrom(target), { recursive: true, force: true });
552
+ };
553
+
554
+ profile.core = to;
555
+ writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
453
556
  console.log(` pinned ${to}\n`);
454
557
 
455
558
  const steps = [
@@ -473,12 +576,20 @@ export async function upgrade(argv, ctx) {
473
576
  const { failed } = runSteps(steps);
474
577
 
475
578
  if (!failed) {
579
+ settle();
476
580
  delete process.env.NINA_UPGRADE;
477
581
  console.log(`\n upgrade: ${previous} → ${to} applied and verified.`);
478
582
  if (removable.length > 0) console.log(`\n removed ${removable.length} file(s) ${to} no longer composes: ${removable.join(', ')}`);
479
- if (keptEdited.length > 0) {
480
- console.log(`\n kept ${keptEdited.length} file(s) ${to} no longer composes, because they carry edits of yours — delete them once nothing in them is needed:`);
481
- for (const p of keptEdited) console.log(` ${p}`);
583
+ // A file gated on a surface the project does not declare is moved aside by compose itself, edited or not.
584
+ const stillKept = keptEdited.filter((p) => entryAt(p));
585
+ const setAside = keptEdited.filter((p) => !entryAt(p));
586
+ if (stillKept.length > 0) {
587
+ console.log(`\n kept ${stillKept.length} file(s) ${to} no longer composes, because they carry edits of yours — delete them once nothing in them is needed:`);
588
+ for (const p of stillKept) console.log(` ${p}`);
589
+ }
590
+ if (setAside.length > 0) {
591
+ console.log(`\n moved ${setAside.length} file(s) with edits of yours to ${REMOVED}/, since ${to} composes them only for a surface this project does not declare:`);
592
+ for (const p of setAside) console.log(` ${p}`);
482
593
  }
483
594
  if (occupied.length > 0) {
484
595
  console.log(`\n replaced ${occupied.length} file(s) of yours with what ${to} composes there — the originals are in ${HARNESS}/replaced/:`);
@@ -530,15 +641,10 @@ export async function upgrade(argv, ctx) {
530
641
  }
531
642
 
532
643
  console.log(`\n ✗ ${failed.label} failed — rolling back to ${previous}.\n`);
533
- await pin(previous);
534
- for (const p of created) rmSync(join(target, p), { force: true });
535
- const recomposed = nina(ctx, target, ['compose', '--project', target]);
536
- for (const [p, bytes] of saved) {
537
- mkdirSync(dirname(join(target, p)), { recursive: true });
538
- writeFileSync(join(target, p), bytes);
539
- }
644
+ const recomposed = restore(ctx, target, profilePath, profile, previous, created, saved);
645
+ settle();
540
646
  console.log(
541
- recomposed.ok
647
+ recomposed
542
648
  ? ` rolled back: pinned ${previous} and recomposed. The tree is as it was.\n`
543
649
  : ` rolled back the pin to ${previous}, but recomposing failed — run \`nina compose\` yourself.\n`,
544
650
  );
package/src/guard.mjs CHANGED
@@ -20,21 +20,18 @@
20
20
  import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync } from 'node:fs';
21
21
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { composedPaths } from './commands/compose.mjs';
23
+ import { NOTICE_HEAD, composedPaths, noticeOf } from './commands/compose.mjs';
24
24
  import { HARNESS } from './paths.mjs';
25
25
 
26
+ // Read here and by the suites from here; the test itself lives beside the compiler that writes the notice.
27
+ export { NOTICE_HEAD, noticeOf };
28
+
26
29
  /** The package this runs from: its releases say whether a pinned version ships the guard. */
27
30
  const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
28
31
 
29
32
  /** The tools that change a file, and where each names it. */
30
33
  const EDITS = { Edit: 'file_path', Write: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' };
31
34
 
32
- /**
33
- * How far into a file the notice is looked for: below a frontmatter block, or a shebang. The compose
34
- * suite holds every composed file's notice inside it.
35
- */
36
- export const NOTICE_HEAD = 60;
37
-
38
35
  /** How much of a file is read to find it: the head, not the whole of a large file on every edit. */
39
36
  const HEAD_BYTES = 16 * 1024;
40
37
 
@@ -69,35 +66,6 @@ function head(path) {
69
66
  }
70
67
  }
71
68
 
72
- /**
73
- * A composed file's notice, as one sentence without its comment syntax, or null when it has none.
74
- *
75
- * @param {string} text - The file.
76
- * @returns {string|null}
77
- */
78
- export function noticeOf(text) {
79
- const lines = String(text).split('\n').slice(0, NOTICE_HEAD);
80
- const at = lines.findIndex((l) => /^(<!--|\/\/) nina:generated/.test(l));
81
- if (at < 0) return null;
82
- const block = [];
83
- if (lines[at].startsWith('//')) {
84
- for (const line of lines.slice(at)) {
85
- if (!line.startsWith('//')) break;
86
- block.push(line.replace(/^\/\/ ?/, ''));
87
- }
88
- } else {
89
- // The notice's own delimiters, taken off by position: this reads text the compiler wrote, it filters
90
- // nothing, and a pattern for it read to a scanner as an HTML sanitizer that misses `--!>`.
91
- for (const line of lines.slice(at)) {
92
- const open = line.startsWith('<!-- ') ? line.slice('<!-- '.length) : line;
93
- const end = open.lastIndexOf('-->');
94
- block.push(end >= 0 ? open.slice(0, end) : open);
95
- if (end >= 0) break;
96
- }
97
- }
98
- return block.map((l) => l.trim()).join(' ');
99
- }
100
-
101
69
  /**
102
70
  * Handles one hook event: the JSON to print, or null to let the call through. Never throws.
103
71
  *
@@ -121,7 +89,9 @@ export async function handleEdit(input, { root, pkg = PACKAGE }) {
121
89
  if (!pinned) return null;
122
90
  const target = isAbsolute(path) ? path : resolve(input.cwd ?? root, path);
123
91
  if (!existsSync(target)) return null;
124
- const rel = relative(realpathSync(root), realpathSync(target)).split(sep).join('/');
92
+ // The native call returns the name as it is on disk: where case is ignored, `claude.md` is CLAUDE.md, and
93
+ // read as spelled it reached no composed path and let the edit through.
94
+ const rel = relative(realpathSync.native(root), realpathSync.native(target)).split(sep).join('/');
125
95
  // Outside the project, or in its own layer — where a change to a composed file belongs.
126
96
  if (rel.startsWith('..') || isAbsolute(rel) || rel.split('/')[0] === HARNESS) return null;
127
97
  const notice = noticeOf(head(target));
package/src/paths.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * project, and losing it is irreversible — so it lives in the user's own directory.
9
9
  */
10
10
 
11
- import { existsSync } from 'node:fs';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
13
13
  import { join, resolve } from 'node:path';
14
14
 
@@ -100,3 +100,52 @@ export function legacyHint(target) {
100
100
  ? ` — this project still has .harness/, which is what ${HARNESS}/ used to be called. Rename it; do not start over.`
101
101
  : '';
102
102
  }
103
+
104
+ /**
105
+ * Where `nina upgrade --apply` writes down a move while it is in flight: the versions it moves between, the
106
+ * files it composes for the first time, and — under {@link movedFrom} — every file it can touch, as it was.
107
+ * It is written before the pin moves and removed once the move is verified or rolled back, so one that is
108
+ * still there is a move that was stopped half-way.
109
+ *
110
+ * @param {string} target - The project.
111
+ * @returns {string}
112
+ */
113
+ export const moveJournal = (target) => join(target, HARNESS, 'upgrade.json');
114
+
115
+ /**
116
+ * Where an in-flight move keeps the files it can touch, as they were before it — at their own paths below.
117
+ *
118
+ * @param {string} target - The project.
119
+ * @returns {string}
120
+ */
121
+ export const movedFrom = (target) => join(target, HARNESS, 'upgrade-saved');
122
+
123
+ /**
124
+ * The move a project was left in when `upgrade --apply` was stopped before it finished, or null when none is.
125
+ *
126
+ * A pin that moved with the composed files half-written looks like hand edits to every check, and the
127
+ * owner's own edits it was about to put back lived only in the stopped process's memory. This is what says
128
+ * so instead, and what `nina upgrade --abort` undoes it from.
129
+ *
130
+ * @param {string} target - The project.
131
+ * @returns {{from: string, to: string, started: string, created: string[], saved: string[]}|{unreadable: string}|null}
132
+ */
133
+ export function interruptedMove(target) {
134
+ try {
135
+ return JSON.parse(readFileSync(moveJournal(target), 'utf8'));
136
+ } catch (error) {
137
+ return error.code === 'ENOENT' ? null : { unreadable: error.message };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * A stopped move, said once for every command that finds one.
143
+ *
144
+ * @param {NonNullable<ReturnType<typeof interruptedMove>>} stopped - The move.
145
+ * @returns {string}
146
+ */
147
+ export function describeMove(stopped) {
148
+ return stopped.unreadable
149
+ ? `An upgrade was stopped before it finished, and its journal ${HARNESS}/upgrade.json cannot be read (${stopped.unreadable}).`
150
+ : `An upgrade from ${stopped.from} to ${stopped.to}, started ${stopped.started}, was stopped before it finished — the pin and the composed files may be half-moved.`;
151
+ }
package/src/prices.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  /** The date the table was read, printed beside every estimate made from it. */
16
16
  export const PRICES_AS_OF = '2026-06-24';
17
17
 
18
- /** model id prefix → $/MTok. Longest prefix wins, so `claude-opus-5-5` is not read as `claude-opus-5`. */
18
+ /** model id → $/MTok. */
19
19
  const PRICES = {
20
20
  'claude-fable-5-1': { input: 10, output: 50, read: 0.25 },
21
21
  'claude-mythos-5-1': { input: 10, output: 50 },
@@ -39,12 +39,12 @@ const PRICES = {
39
39
  * @returns {{input: number, output: number, read: number, write5m: number, write1h: number}|null}
40
40
  */
41
41
  export function priceOf(model) {
42
- const id = String(model ?? '');
43
- const key = Object.keys(PRICES)
44
- .filter((k) => id === k || id.startsWith(`${k}-`) || id.startsWith(`${k}[`))
45
- .sort((a, b) => b.length - a.length)[0];
46
- if (!key) return null;
47
- const p = PRICES[key];
42
+ // The id itself, less the two suffixes a transcript adds to one: a context window (`[1m]`) and a snapshot
43
+ // date (`-20251001`). Matched by prefix, a model newer than the table was priced as the one before it —
44
+ // `claude-opus-5-6` as Opus 5, at five times its cache reads.
45
+ const id = String(model ?? '').replace(/\[[^\]]*\]$/, '').replace(/-\d{8}$/, '');
46
+ const p = Object.hasOwn(PRICES, id) ? PRICES[id] : null;
47
+ if (!p) return null;
48
48
  return { input: p.input, output: p.output, read: p.read ?? p.input * 0.1, write5m: p.input * 1.25, write1h: p.input * 2 };
49
49
  }
50
50
 
package/src/store.mjs ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The measurement store, read one way by every report.
3
+ *
4
+ * Five readers each parsed `~/.nina/snapshots/<project>.jsonl` their own way — `stats`, `runs`, `learn`,
5
+ * `export` and `status` — and chose the project four different ways, so two reports on the same history
6
+ * disagreed and neither said why. Every report reads it here now. Only `snapshot`, which rewrites the file,
7
+ * reads it raw.
8
+ */
9
+
10
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
11
+ import { join, resolve } from 'node:path';
12
+ import { slugFor, snapshotsDir } from './paths.mjs';
13
+ import { ROLE_TOKENS } from './transcripts.mjs';
14
+
15
+ /** What a record says when no verdict was read from its report. */
16
+ const UNREAD = new Set(['UNCLEAR', 'NONE', null, undefined]);
17
+
18
+ /**
19
+ * Records from a store file's text. A torn line loses one record, never the file.
20
+ *
21
+ * @param {string} text - The file.
22
+ * @returns {object[]}
23
+ */
24
+ export function parseStore(text) {
25
+ return String(text)
26
+ .split('\n')
27
+ .filter((l) => l.trim())
28
+ .flatMap((l) => {
29
+ try {
30
+ return [JSON.parse(l)];
31
+ } catch {
32
+ return [];
33
+ }
34
+ });
35
+ }
36
+
37
+ /**
38
+ * A record as the reports read it. A verdict outside the vocabulary of the role that gave it is not a
39
+ * verdict that role can give: records captured before the classifier knew the role read a dba's report
40
+ * as `PASS`, qa's token, and 21 of those made a gate that had stopped one change in fourteen look like a
41
+ * rubber stamp. Kept in the store as captured, and read as unreadable.
42
+ *
43
+ * @param {object} record - As captured.
44
+ * @returns {object}
45
+ */
46
+ export function asRead(record) {
47
+ const tokens = ROLE_TOKENS[record?.role];
48
+ if (!tokens || UNREAD.has(record.verdict) || tokens.includes(record.verdict)) return record;
49
+ return { ...record, verdict: 'UNCLEAR', verdict_source: 'outside-role' };
50
+ }
51
+
52
+ /**
53
+ * Every record in one store file.
54
+ *
55
+ * @param {string} file - The file.
56
+ * @param {{raw?: boolean}} [options] - `raw`: as captured, for the one writer of the store.
57
+ * @returns {object[]} Empty when there is no such file.
58
+ */
59
+ export function readStoreFile(file, { raw = false } = {}) {
60
+ let text;
61
+ try {
62
+ text = readFileSync(file, 'utf8');
63
+ } catch {
64
+ return [];
65
+ }
66
+ const records = parseStore(text);
67
+ return raw ? records : records.map(asRead);
68
+ }
69
+
70
+ /**
71
+ * The store names a project's records may be under: its path as given, and with its links resolved, since
72
+ * either may be the one its sessions ran in.
73
+ *
74
+ * @param {string} dir - The project.
75
+ * @returns {string[]}
76
+ */
77
+ export function storeNamesOf(dir) {
78
+ const names = new Set([slugFor(dir)]);
79
+ try {
80
+ names.add(slugFor(realpathSync(dir)));
81
+ } catch {
82
+ // A directory that cannot be resolved is looked up as given.
83
+ }
84
+ return [...names];
85
+ }
86
+
87
+ /**
88
+ * A project's records.
89
+ *
90
+ * @param {string} dir - The project.
91
+ * @param {string} [store] - The store directory.
92
+ * @returns {object[]|null} Null when the store has nothing for it.
93
+ */
94
+ export function projectRecords(dir, store = snapshotsDir()) {
95
+ for (const name of storeNamesOf(dir)) {
96
+ const file = join(store, `${name}.jsonl`);
97
+ if (existsSync(file)) return readStoreFile(file);
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * The store names a `--project` value picks, one rule for every report. A directory is that project, by the
104
+ * name its sessions ran under; anything else is a store name, exactly, or else every name that contains it.
105
+ * `stats --project ../thing` read the directory as a piece of a name, found none, and said to run a snapshot.
106
+ *
107
+ * @param {string} value - What was given.
108
+ * @param {string} [store] - The store directory.
109
+ * @returns {string[]}
110
+ */
111
+ export function namesFor(value, store = snapshotsDir()) {
112
+ const names = (() => {
113
+ try {
114
+ return readdirSync(store).filter((f) => f.endsWith('.jsonl')).map((f) => f.slice(0, -'.jsonl'.length));
115
+ } catch {
116
+ return [];
117
+ }
118
+ })();
119
+ let isDir = false;
120
+ try {
121
+ isDir = statSync(resolve(value)).isDirectory();
122
+ } catch {
123
+ // Not a directory here: a name.
124
+ }
125
+ if (isDir) return storeNamesOf(resolve(value)).filter((n) => names.includes(n));
126
+ if (names.includes(value)) return [value];
127
+ return names.filter((n) => n.includes(value));
128
+ }
129
+
130
+ /** Two digits. */
131
+ const two = (n) => String(n).padStart(2, '0');
132
+
133
+ /**
134
+ * The day a moment fell on where this machine is, as `YYYY-MM-DD`. The store keeps UTC; the reports read
135
+ * the owner's day. Cut from the UTC string, `--since` and the days a report printed moved a round from the
136
+ * evening it ran in to the next morning for anyone west of Greenwich — 20 of one project's 130.
137
+ *
138
+ * @param {unknown} ts - An ISO timestamp.
139
+ * @returns {string} The day, or the first ten characters of whatever could not be read as a moment.
140
+ */
141
+ export function localDay(ts) {
142
+ const d = new Date(String(ts ?? ''));
143
+ return Number.isNaN(d.getTime()) ? String(ts ?? '').slice(0, 10) : `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}`;
144
+ }
145
+
146
+ /**
147
+ * A moment where this machine is, as `YYYY-MM-DD HH:MM`.
148
+ *
149
+ * @param {unknown} ts - An ISO timestamp.
150
+ * @returns {string}
151
+ */
152
+ export function localMinute(ts) {
153
+ const d = new Date(String(ts ?? ''));
154
+ return Number.isNaN(d.getTime()) ? String(ts ?? '') : `${localDay(ts)} ${two(d.getHours())}:${two(d.getMinutes())}`;
155
+ }
156
+
157
+ /** The zone the reports read days in, named where a report prints a time. */
158
+ export const zone = () => Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'local time';
@@ -20,8 +20,15 @@ import { homedir } from 'node:os';
20
20
  /** Root under which Claude Code stores one directory of transcripts per project. */
21
21
  export const PROJECTS_ROOT = join(homedir(), '.claude', 'projects');
22
22
 
23
- /** The transcript directory of a project, by its snapshot name; `NINA_TRANSCRIPTS` moves the root, for tests. */
24
- export const transcriptsOf = (slug) => join(process.env.NINA_TRANSCRIPTS ?? PROJECTS_ROOT, slug);
23
+ /**
24
+ * Where the transcripts are read from: Claude Code's own directory, or `NINA_TRANSCRIPTS`, which moves it for
25
+ * a test. Every reader asks this: listing the projects read Claude Code's own whatever the variable said, so a
26
+ * snapshot meant to stay in a sandbox captured the machine's real projects into it.
27
+ */
28
+ const transcriptsRoot = () => process.env.NINA_TRANSCRIPTS ?? PROJECTS_ROOT;
29
+
30
+ /** The transcript directory of a project, by its snapshot name. */
31
+ export const transcriptsOf = (slug) => join(transcriptsRoot(), slug);
25
32
 
26
33
  /**
27
34
  * The verdict tokens the agent specs mandate on a report's first line, per role.
@@ -486,6 +493,9 @@ export async function scanProject(projectDir, prior = {}) {
486
493
  model: block?.input?.model ?? row?.message?.model ?? null,
487
494
  branch: row.gitBranch ?? null,
488
495
  session: row.sessionId ?? null,
496
+ // The Claude Code that ran the dispatch: a change in how its transcripts are written first shows
497
+ // as a parser that stops finding something, and this says which version it stopped at.
498
+ claude_code: row.version ?? null,
489
499
  verdict: null,
490
500
  verdict_source: null,
491
501
  issues: null,
@@ -757,6 +767,7 @@ async function attachAgentDetail(projectDir, dispatches) {
757
767
  model: run.model,
758
768
  branch: round.branch ?? run.branch,
759
769
  session: run.session,
770
+ claude_code: run.claude_code ?? null,
760
771
  verdict: null,
761
772
  verdict_source: null,
762
773
  issues: null,
@@ -884,10 +895,10 @@ export function tokensOf(usage) {
884
895
  * @returns {Promise<{slug: string, dir: string, mtime: number}[]>} The projects found.
885
896
  */
886
897
  export async function listProjects() {
887
- const slugs = await readdir(PROJECTS_ROOT).catch(() => []);
898
+ const slugs = await readdir(transcriptsRoot()).catch(() => []);
888
899
  const out = [];
889
900
  for (const slug of slugs) {
890
- const dir = join(PROJECTS_ROOT, slug);
901
+ const dir = transcriptsOf(slug);
891
902
  const info = await stat(dir).catch(() => null);
892
903
  if (info?.isDirectory()) out.push({ slug, dir, mtime: info.mtimeMs });
893
904
  }
@@ -16,6 +16,17 @@
16
16
  import { readFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
 
19
+ /**
20
+ * A vocabulary name: capitals, digits and `_`, from a letter — `{{S3_BUCKET}}` as much as `{{OWNER}}`. Every
21
+ * reader of a placeholder reads it with this one grammar. Compose took any name the profile declared while
22
+ * `check`, `init` and `upgrade` looked only for capitals and `_`, so `{{S3_BUCKET}}` was reported as declared
23
+ * and referenced by nothing, and deleting it on that advice composed the placeholder raw, with nobody saying so.
24
+ */
25
+ export const NAME = /^[A-Z][A-Z0-9_]*$/;
26
+
27
+ /** A placeholder in a text, its name captured. */
28
+ export const PLACEHOLDER = /\{\{([A-Z][A-Z0-9_]*)\}\}/g;
29
+
19
30
  /**
20
31
  * The defaults a layer root supplies, name → value. Anything that is not a string under an
21
32
  * upper-case name is ignored rather than trusted: a default reaches every project that pins it.
@@ -26,7 +37,7 @@ import { join } from 'node:path';
26
37
  export function defaultVocabulary(layerRoot) {
27
38
  try {
28
39
  const parsed = JSON.parse(readFileSync(join(layerRoot, 'core', 'vocabulary.json'), 'utf8'));
29
- return Object.fromEntries(Object.entries(parsed).filter(([name, value]) => /^[A-Z_]+$/.test(name) && typeof value === 'string'));
40
+ return Object.fromEntries(Object.entries(parsed).filter(([name, value]) => NAME.test(name) && typeof value === 'string'));
30
41
  } catch {
31
42
  return {};
32
43
  }