@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
package/src/agentrun.mjs CHANGED
@@ -30,18 +30,74 @@ export function clip(text, chars) {
30
30
  }
31
31
 
32
32
  /** Names whose value is a credential when they are assigned one. */
33
- const SECRET_NAME = /(?:secret|token|passw(?:or)?d|api_?key|private_?key|access_?key|credential)/i;
33
+ const SECRET_NAME = /(?:secret|token(?![a-z])|passw(?:or)?d|(?:^|[_-])pass$|api[_-]?key|private[_-]?key|access[_-]?key|signing[_-]?key|credential|dsn$)/i;
34
+
35
+ /** Names that say where a secret is, or what kind, rather than holding one: `--password-file`, `TOKEN_TYPE`. */
36
+ const ABOUT_A_SECRET = /[_-](?:file|path|dir|type|count|length|size|limit|url|endpoint)$/i;
34
37
 
35
38
  /** Token formats that are credentials on sight. */
36
- const SECRET_TOKEN = /\b(?:sk-(?:ant-|lf-|proj-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35})/g;
39
+ const SECRET_TOKEN = new RegExp(
40
+ [
41
+ 'sk-(?:ant-|lf-|proj-)?[A-Za-z0-9_-]{16,}',
42
+ 'gh[pousr]_[A-Za-z0-9]{30,}',
43
+ 'github_pat_[A-Za-z0-9_]{30,}',
44
+ 'glpat-[A-Za-z0-9_-]{20,}',
45
+ 'xox[abprs]-[A-Za-z0-9-]{10,}',
46
+ '(?:AKIA|ASIA)[0-9A-Z]{16}',
47
+ 'AIza[0-9A-Za-z_-]{35}',
48
+ '(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}',
49
+ 'whsec_[A-Za-z0-9+/=]{16,}',
50
+ 'npm_[A-Za-z0-9]{30,}',
51
+ 'hf_[A-Za-z0-9]{30,}',
52
+ 'SG\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}',
53
+ 'eyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}',
54
+ ]
55
+ .map((p) => `\\b${p}`)
56
+ .join('|'),
57
+ 'g',
58
+ );
59
+
60
+ /** `scheme://user:password@host` — the password, however the URL is named. */
61
+ const URL_CREDENTIAL = /\b([a-z][a-z0-9+.-]{1,20}:\/\/[^\s:/@]{0,64}):[^\s@/]{1,256}@/gi;
62
+
63
+ /**
64
+ * `NAME=value`, `NAME: value`, `"name": "value"` — the name and what joins it to a value of six characters or
65
+ * more, and not the value: {@link redact} takes that only for a secret's name. Every part is bounded, so no
66
+ * position costs more than a few dozen steps, whatever follows it.
67
+ */
68
+ const ASSIGNMENT = /\b([A-Za-z0-9_-]{0,40})(["']?\s{0,3}[:=]\s{0,3}["']?)(?=[^\s"'`,;]{6})/g;
69
+
70
+ /** The value that follows an assignment, read from where {@link ASSIGNMENT} stopped. */
71
+ const VALUE = /[^\s"'`,;]+/y;
37
72
 
38
- /** `NAME=value`, `NAME: value`, `"name": "value"` — the name bounded, so the match cannot run away. */
39
- const ASSIGNMENT = /\b([A-Za-z0-9_]{0,40})(["']?\s{0,3}[:=]\s{0,3}["']?)([^\s"'`,;]{6,})/g;
73
+ /** The same with the value in quotes, which may hold spaces: `password: "correct horse battery"`. */
74
+ const QUOTED = /\b([A-Za-z0-9_-]{1,40})(["']?\s{0,3}[:=]\s{0,3})(["'])([^"'\n]{1,256})\3/g;
75
+
76
+ /** A command-line option with its value after a space — `--password hunter2`, `--api-key "a b"`. */
77
+ const FLAG = /(--?[A-Za-z0-9][A-Za-z0-9_-]{0,40})\s{1,3}(?:(["'])([^"'\n]{1,256})\2|([^\s"'`]{3,}))/g;
78
+
79
+ /**
80
+ * curl's and friends' `-u user:password`, quoted or not. The user has to look like one — `date -u +%H:%M`
81
+ * is a format — and a password of digits alone is a group: `docker run -u 1000:1000`.
82
+ */
83
+ const USER_FLAG = /((?:^|\s)(?:-u|--user)\s{1,3}[A-Za-z0-9_][A-Za-z0-9._@-]{0,63}):([^\s"']{1,256})/g;
84
+
85
+ /** The same in quotes, where the password may hold spaces. */
86
+ const USER_QUOTED = /((?:^|\s)(?:-u|--user)\s{1,3}(["'])[A-Za-z0-9_][A-Za-z0-9._@-]{0,63}):[^"'\n]{1,256}\2/g;
87
+
88
+ /** A cookie header's whole value: every cookie in it may be a session. */
89
+ const COOKIE = /\b((?:Set-)?Cookie["']?\s{0,3}:\s{0,3})[^\n"]{1,2000}/gi;
90
+
91
+ /** An `Authorization` value under any scheme, or none — `ApiKey …` and a bare token as much as `Bearer …`. */
92
+ const AUTHORIZATION = /\b((?:Proxy-)?Authorization["']?\s{0,3}[:=]\s{0,3}["']?)(?:([A-Za-z][A-Za-z0-9_-]{0,20})\s+)?[^\s"',;]{8,}/gi;
93
+
94
+ /** Whether a name and the value assigned to it make a credential. A count is not one: `max_tokens: 100000`. */
95
+ const secretPair = (name, value) => SECRET_NAME.test(name) && !ABOUT_A_SECRET.test(name) && !/^\d+$/.test(value) && value !== '[redacted]';
40
96
 
41
97
  /**
42
98
  * Text with the secrets it obviously carries masked: private keys, the token formats of the common
43
- * providers, an `Authorization` value, and anything assigned to a name that says it is a secret. A value
44
- * of digits only is left: `max_tokens: 100000` is not a credential.
99
+ * providers, the password in a URL, an `Authorization` value, and anything assigned to a name that says it
100
+ * is a secret. A value of digits only is left: `max_tokens: 100000` is not a credential.
45
101
  *
46
102
  * @param {string} text
47
103
  * @returns {string}
@@ -57,10 +113,34 @@ export function redact(text) {
57
113
  out = `${out.slice(0, start)}[redacted private key]${close === -1 ? '' : out.slice(close + 5)}`;
58
114
  }
59
115
  out = out.replace(SECRET_TOKEN, '[redacted]');
60
- out = out.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/g, '$1 [redacted]');
61
- return out.replace(ASSIGNMENT, (whole, name, sep, value) =>
62
- SECRET_NAME.test(name) && !/^\d+$/.test(value) && value !== '[redacted]' ? `${name}${sep}[redacted]` : whole,
116
+ out = out.replace(URL_CREDENTIAL, '$1:[redacted]@');
117
+ out = out.replace(/\b(Bearer|Basic|Token|Bot)\s+[A-Za-z0-9._~+/=-]{16,}/g, '$1 [redacted]');
118
+ out = out.replace(AUTHORIZATION, (whole, head, scheme) => `${head}${scheme ? `${scheme} ` : ''}[redacted]`);
119
+ out = out.replace(COOKIE, '$1[redacted]');
120
+ out = out.replace(USER_QUOTED, (whole, head, quote) => `${head}:[redacted]${quote}`);
121
+ out = out.replace(USER_FLAG, (whole, head, password) => (/^\d+$/.test(password) || password === '[redacted]' ? whole : `${head}:[redacted]`));
122
+ // The shell is where most of a stage's input is written, and it passes a secret after a space.
123
+ out = out.replace(FLAG, (whole, flag, quote, quoted, bare) =>
124
+ secretPair(flag.replace(/^-+/, ''), quoted ?? bare) ? `${flag} ${quote ?? ''}[redacted]${quote ?? ''}` : whole,
63
125
  );
126
+ out = out.replace(QUOTED, (whole, name, sep, quote, value) => (secretPair(name, value) ? `${name}${sep}${quote}[redacted]${quote}` : whole));
127
+ // By hand rather than by `replace`: an assignment to a name that is not a secret's leaves its value to be
128
+ // read, so one inside it still is. Replaced whole, `{"content":"API_KEY=…"}` took the key's value with it.
129
+ // The value is taken once, and only for a secret's name, which keeps the whole pass linear.
130
+ let masked = '';
131
+ let last = 0;
132
+ ASSIGNMENT.lastIndex = 0;
133
+ for (let m = ASSIGNMENT.exec(out); m !== null; m = ASSIGNMENT.exec(out)) {
134
+ const [head, name, sep] = m;
135
+ if (!SECRET_NAME.test(name)) continue;
136
+ VALUE.lastIndex = m.index + head.length;
137
+ const value = VALUE.exec(out)?.[0] ?? '';
138
+ if (!secretPair(name, value)) continue;
139
+ masked += `${out.slice(last, m.index)}${name}${sep}[redacted]`;
140
+ last = VALUE.lastIndex;
141
+ ASSIGNMENT.lastIndex = last;
142
+ }
143
+ return masked + out.slice(last);
64
144
  }
65
145
 
66
146
  /** A tool result's text: a string, or the text blocks of a list, with images named rather than sent. */
@@ -9,15 +9,15 @@
9
9
  */
10
10
 
11
11
  import { readFile, readdir } from 'node:fs/promises';
12
- import { HARNESS, legacyHint } from '../paths.mjs';
12
+ import { HARNESS, describeMove, interruptedMove, legacyHint } from '../paths.mjs';
13
13
  import { expectedUnfilled } from '../expected.mjs';
14
14
  import { parseGraph, validateGraph } from '../graph.mjs';
15
15
  import { frontmatterFindings, installedSkills, modelFindings, toolFindings } from '../tools.mjs';
16
16
  import { HOOK_SCRIPTS, missingWiring, shippedScripts } from '../wiring.mjs';
17
17
  import { existsSync } from 'node:fs';
18
18
  import { join, resolve, sep } from 'node:path';
19
- import { REQUIRES, SLOT, defaultedSlots, layerRootFor, stripWhy, walk } from './compose.mjs';
20
- import { defaultVocabulary } from '../vocabulary.mjs';
19
+ import { REQUIRES, SLOT, defaultedSlots, layerRootFor, noticeOf, readLayerText, stripWhy, walk } from './compose.mjs';
20
+ import { PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
21
21
  import { NEEDS } from '../surfaces.mjs';
22
22
 
23
23
  /** Fields every declared integration must carry, and why each one matters. */
@@ -56,13 +56,13 @@ export async function referencedVocabulary(layerRoot, surfaces, target) {
56
56
  const t = await readFile(join(layerRoot, 'surfaces', s, 'tree', rel), 'utf8').catch(() => null);
57
57
  if (t !== null) texts.push(t);
58
58
  }
59
- for (const [, name] of stripWhy(texts.join('\n')).matchAll(/\{\{([A-Z_]+)\}\}/g)) found.add(name);
59
+ for (const [, name] of stripWhy(texts.join('\n')).matchAll(PLACEHOLDER)) found.add(name);
60
60
  }
61
61
  if (target) {
62
62
  const projectTree = join(target, HARNESS, 'project', 'tree');
63
63
  for (const rel of await walk(projectTree)) {
64
- const text = stripWhy(await readFile(join(projectTree, rel), 'utf8'));
65
- for (const [, name] of text.matchAll(/\{\{([A-Z_]+)\}\}/g)) found.add(name);
64
+ const text = stripWhy((await readLayerText(join(projectTree, rel))) ?? '');
65
+ for (const [, name] of text.matchAll(PLACEHOLDER)) found.add(name);
66
66
  }
67
67
  }
68
68
  return found;
@@ -100,7 +100,7 @@ export async function filledSlots(target) {
100
100
  const tree = join(target, HARNESS, 'project', 'tree');
101
101
  const filled = new Set();
102
102
  for (const rel of await walk(tree)) {
103
- for (const line of (await readFile(join(tree, rel), 'utf8')).split('\n')) {
103
+ for (const line of ((await readLayerText(join(tree, rel))) ?? '').split('\n')) {
104
104
  const marker = SLOT.exec(line);
105
105
  if (marker) filled.add(`${rel} ${marker[1]}`);
106
106
  }
@@ -230,6 +230,10 @@ export async function check(argv, ctx) {
230
230
  return 1;
231
231
  }
232
232
 
233
+ // The move measures this check itself while its journal is written, and that journal is not a problem then.
234
+ const stopped = process.env.NINA_UPGRADE ? null : interruptedMove(target);
235
+ if (stopped) problems.push(`${describeMove(stopped)} \`nina upgrade --abort\` undoes it`);
236
+
233
237
  const surfaces = profile.surfaces ?? [];
234
238
  const available = (await readdir(join(resolved.dir, 'surfaces'), { withFileTypes: true }))
235
239
  .filter((e) => e.isDirectory())
@@ -350,7 +354,7 @@ export async function check(argv, ctx) {
350
354
  // them; they are outside the pipeline graph, not a stage the graph forgot.
351
355
  for (const file of (await readdir(agentsDir).catch(() => [])).filter((f) => f.endsWith('.md'))) {
352
356
  const text = await readFile(join(agentsDir, file), 'utf8');
353
- if (text.includes('nina:generated')) specs.set(file.replace(/\.md$/, ''), text);
357
+ if (noticeOf(text) !== null) specs.set(file.replace(/\.md$/, ''), text);
354
358
  }
355
359
  // A core with a graph and no agent specs has no roles to name — an empty set, not a crash that
356
360
  // `upgrade` would read as a new failure and roll a move back for.
@@ -16,12 +16,12 @@
16
16
  * sit unenforceable for three months here.
17
17
  */
18
18
 
19
- import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
19
+ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
20
20
  import { HARNESS, legacyHint } from '../paths.mjs';
21
21
  import { expectedUnfilled } from '../expected.mjs';
22
22
  import { existsSync, lstatSync, readFileSync } from 'node:fs';
23
23
  import { dirname, join, relative, resolve } from 'node:path';
24
- import { defaultVocabulary } from '../vocabulary.mjs';
24
+ import { NAME, defaultVocabulary } from '../vocabulary.mjs';
25
25
 
26
26
  /**
27
27
  * Matches a slot marker on its own line, with an optional label.
@@ -139,6 +139,48 @@ export function generatedNotice(rel, slots, script = false) {
139
139
  return script ? lines.map((line) => `// ${line}`).join('\n') : `<!-- ${lines.join('\n ')} -->`;
140
140
  }
141
141
 
142
+ /** Where compose keeps a copy of each file it stops composing when a surface leaves the profile. */
143
+ export const REMOVED = join(HARNESS, 'removed');
144
+
145
+ /**
146
+ * How far into a file the notice is looked for: below a frontmatter block, or a shebang. The compose
147
+ * suite holds every composed file's notice inside it.
148
+ */
149
+ export const NOTICE_HEAD = 60;
150
+
151
+ /**
152
+ * A composed file's notice, as one sentence without its comment syntax, or null when it has none.
153
+ *
154
+ * This is the one test of whether a file was composed. A file that only mentions the notice — a CLAUDE.md
155
+ * written by hand that explains the harness, say — is not composed: read as "contains the words", compose
156
+ * took such a file for its own and wrote over it with no copy kept.
157
+ *
158
+ * @param {string} text - The file.
159
+ * @returns {string|null}
160
+ */
161
+ export function noticeOf(text) {
162
+ const lines = String(text).split('\n').slice(0, NOTICE_HEAD);
163
+ const at = lines.findIndex((l) => /^(<!--|\/\/) nina:generated/.test(l));
164
+ if (at < 0) return null;
165
+ const block = [];
166
+ if (lines[at].startsWith('//')) {
167
+ for (const line of lines.slice(at)) {
168
+ if (!line.startsWith('//')) break;
169
+ block.push(line.replace(/^\/\/ ?/, ''));
170
+ }
171
+ } else {
172
+ // The notice's own delimiters, taken off by position: this reads text the compiler wrote, it filters
173
+ // nothing, and a pattern for it read to a scanner as an HTML sanitizer that misses `--!>`.
174
+ for (const line of lines.slice(at)) {
175
+ const open = line.startsWith('<!-- ') ? line.slice('<!-- '.length) : line;
176
+ const end = open.lastIndexOf('-->');
177
+ block.push(end >= 0 ? open.slice(0, end) : open);
178
+ if (end >= 0) break;
179
+ }
180
+ }
181
+ return block.map((l) => l.trim()).join(' ');
182
+ }
183
+
142
184
  /**
143
185
  * Puts the notice at the top of a composed file, below its frontmatter where it has any.
144
186
  *
@@ -241,6 +283,20 @@ export async function walk(dir) {
241
283
  return out;
242
284
  }
243
285
 
286
+ /**
287
+ * A layer file's text, read the way every reader of the layers must read it: with a byte-order mark taken
288
+ * off and Windows line ends made `\n`. A fragment saved with CRLF matched no slot marker — the marker's
289
+ * pattern ends at `-->` and the line ended in a `\r` — so the whole file composed to nothing, and the only
290
+ * sign was the count of unfilled slots. A BOM did the same to the first slot.
291
+ *
292
+ * @param {string} path - A file in a layer's tree.
293
+ * @returns {Promise<string|null>} Null when there is no such file.
294
+ */
295
+ export async function readLayerText(path) {
296
+ const text = await readFile(path, 'utf8').catch(() => null);
297
+ return text === null ? null : text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
298
+ }
299
+
244
300
  /**
245
301
  * Reads the fragments one layer declares for one file.
246
302
  *
@@ -254,7 +310,7 @@ export async function walk(dir) {
254
310
  */
255
311
  async function readFragments(path) {
256
312
  const bySlot = new Map();
257
- const text = await readFile(path, 'utf8').catch(() => null);
313
+ const text = await readLayerText(path);
258
314
  if (text === null) return bySlot;
259
315
  let current = null;
260
316
  const buffer = [];
@@ -350,7 +406,7 @@ export function projectOwned(target, paths) {
350
406
  }
351
407
  const stamped = p.endsWith('.md') || SCRIPT.test(p);
352
408
  // Read without following links, a link is not a file: it is the project's, whatever it points at.
353
- if (!entry.isFile() || (stamped && !readFileSync(join(target, p), 'utf8').includes('nina:generated'))) out.push(p);
409
+ if (!entry.isFile() || (stamped && noticeOf(readFileSync(join(target, p), 'utf8')) === null)) out.push(p);
354
410
  }
355
411
  return out.sort();
356
412
  }
@@ -366,13 +422,15 @@ export function projectOwned(target, paths) {
366
422
  * @param {string} target - The project directory.
367
423
  * @param {{root: string}} ctx - CLI context; `root` is the NINA install directory.
368
424
  * @param {{check?: boolean}} [options] - `check` compares without writing.
369
- * @returns {Promise<{error?: string, written: string[], differ: string[], unfilled: string[], skipped: string[]}>}
425
+ * @returns {Promise<{error?: string, written: string[], differ: string[], unfilled: string[], skipped: string[], removed?: string[]}>}
370
426
  */
371
427
  export async function composeProject(target, ctx, options = {}) {
372
428
  const check = options.check === true;
373
429
  const written = [];
374
430
  const differ = [];
375
431
  const skipped = [];
432
+ /** Files composed for a surface this profile no longer declares, moved to {@link REMOVED}. */
433
+ const removed = [];
376
434
 
377
435
  const profilePath = join(target, HARNESS, 'profile.json');
378
436
  if (!existsSync(profilePath)) {
@@ -425,9 +483,23 @@ export async function composeProject(target, ctx, options = {}) {
425
483
  if (requires) {
426
484
  core = core.slice(requires[0].length);
427
485
  if (!surfaces.includes(requires[1])) {
428
- // The project has no such surface, so the file does not exist for it.
486
+ // The project has no such surface, so the file does not exist for it. One composed for it before the
487
+ // surface left the profile is still dispatched as a stage the graph no longer has, and was reported as
488
+ // drift that no compose cleared: it goes, with a copy kept. A file there with no notice is the project's.
429
489
  skipped.push(rel);
430
- if (check && existsSync(dest)) differ.push(`${rel} (present, but no '${requires[1]}' surface)`);
490
+ let stale = false;
491
+ try {
492
+ stale = lstatSync(dest).isFile() && noticeOf(await readFile(dest, 'utf8')) !== null;
493
+ } catch {
494
+ // Not there.
495
+ }
496
+ if (stale && check) differ.push(`${rel} (present, but no '${requires[1]}' surface)`);
497
+ if (stale && !check) {
498
+ await mkdir(dirname(join(target, REMOVED, rel)), { recursive: true });
499
+ await writeFile(join(target, REMOVED, rel), await readFile(dest));
500
+ await rm(dest);
501
+ removed.push(rel);
502
+ }
431
503
  continue;
432
504
  }
433
505
  }
@@ -500,7 +572,7 @@ export async function composeProject(target, ctx, options = {}) {
500
572
  // A name the project deferred, with its reason, reads as not decided: a stage that meets it asks
501
573
  // instead of guessing, and the raw placeholder it would otherwise read looks like an editing slip.
502
574
  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]`);
575
+ if (typeof why === 'string' && why.trim() && NAME.test(name)) text = text.split(`{{${name}}}`).join(`[${name}: not decided yet]`);
504
576
  }
505
577
 
506
578
  // Only what has a comment syntax the notice knows. A layer that one day composes JSON
@@ -518,7 +590,7 @@ export async function composeProject(target, ctx, options = {}) {
518
590
  if (previous !== null && previous !== text) differ.push(rel);
519
591
  }
520
592
 
521
- return { written, differ, unfilled, skipped, malformed };
593
+ return { written, differ, unfilled, skipped, malformed, removed };
522
594
  }
523
595
 
524
596
  /**
@@ -577,6 +649,10 @@ export async function compose(argv, ctx) {
577
649
  return bad === 0 ? 0 : 1;
578
650
  }
579
651
  console.log(`composed ${result.written.length} file(s) → ${relative(process.cwd(), target) || '.'}`);
652
+ if (result.removed.length > 0) {
653
+ console.log(` removed ${result.removed.length} file(s) composed for a surface this profile no longer declares — a copy of each is in ${REMOVED}/:`);
654
+ for (const rel of result.removed) console.log(` ${rel}`);
655
+ }
580
656
  // The one place a pointer to `nina where` costs nothing. Put it in the notice instead and
581
657
  // every composed file in every project differs until it recomposes — for a pointer, not a
582
658
  // fix. This line is written to no file, so it can say what it likes, and it reaches whoever
@@ -28,6 +28,7 @@ import { readRun, runFile } from '../agentrun.mjs';
28
28
  import { BATCH, SETTLE_MINUTES, contextMode, postScore, postScores, postSpans, scoreOf, settled, spanOf, spansOf } from '../langfuse.mjs';
29
29
  import { runOf, transcriptsOf } from '../transcripts.mjs';
30
30
  import { exportsDir, snapshotsDir } from '../paths.mjs';
31
+ import { readStoreFile } from '../store.mjs';
31
32
  import { readConfig, statusPath, targetOf } from './langfuse.mjs';
32
33
  import { projectName } from './stats.mjs';
33
34
  import { handsToModel } from '../detectors.mjs';
@@ -48,25 +49,16 @@ const SMALLER = [
48
49
  /** Scores per ingestion request. */
49
50
  const SCORES_PER_REQUEST = 100;
50
51
 
51
- /** What a record held when it was sent, as a digest: a record that changes afterwards is noticed. */
52
- export const digest = (record) => createHash('sha256').update(JSON.stringify(record)).digest('hex').slice(0, 16);
52
+ /**
53
+ * What was sent for a record, as a digest: the span it becomes, so a change to what went is noticed and a change
54
+ * to what never goes is not. Taken over the whole record, a run resumed after it was sent — its first round
55
+ * learning how much it read, when it was told, its status — read as changed, about half of one project's runs.
56
+ */
57
+ export const digest = (record) => createHash('sha256').update(JSON.stringify(spanOf(record, ''))).digest('hex').slice(0, 16);
53
58
 
54
59
  /** Where each project's record of what was sent lives. */
55
60
  const sentPath = (slug) => join(exportsDir(), 'langfuse', `${slug}.json`);
56
61
 
57
- /** Reads one project's snapshot records. */
58
- async function recordsOf(file) {
59
- return (await readFile(file, 'utf8'))
60
- .split('\n')
61
- .filter((l) => l.trim())
62
- .flatMap((l) => {
63
- try {
64
- return [JSON.parse(l)];
65
- } catch {
66
- return [];
67
- }
68
- });
69
- }
70
62
 
71
63
  /**
72
64
  * What was sent for a project: a digest per span, and the scores. Only a file that is not there means
@@ -267,7 +259,7 @@ export async function exportCommand(argv, ctx = {}) {
267
259
  */
268
260
  async function exportProject({ slug, name, file, target, dry, now, since = '', quiet = false, content = null }) {
269
261
  const say = quiet ? () => {} : (line) => console.log(line);
270
- const records = await recordsOf(file);
262
+ const records = readStoreFile(file);
271
263
  const sent = await sentOf(slug);
272
264
  if (typeof sent === 'string') {
273
265
  say(` ${name}: ✗ ${sent}`);
@@ -21,7 +21,7 @@ import { join, resolve } from 'node:path';
21
21
  import { HARNESS, legacyHint, slugFor } from '../paths.mjs';
22
22
  import { GATE, hookCommand, missingWiring, shippedScripts } from '../wiring.mjs';
23
23
  import { LEDGER_TAIL, ledgerPath, loadProject, projectGateDir, readLedger, runGate } from '../gate.mjs';
24
- import { storedRecords } from './snapshot.mjs';
24
+ import { projectRecords } from '../store.mjs';
25
25
  import { layerRootFor } from './compose.mjs';
26
26
  import { handsToModel } from '../detectors.mjs';
27
27
 
@@ -102,7 +102,8 @@ const LIVE_SAMPLE = 3;
102
102
  *
103
103
  * @param {string} target - The project directory.
104
104
  * @param {{graph: {stages: Set<string>}, tokens: Map<string, Set<string>>}} project - Its composed graph.
105
- * @returns {string|null}
105
+ * @returns {{problem: string}|{compared: number}|{unasked: string}} What the comparison found, how many reports it
106
+ * compared, or why there was nothing to compare yet — which the selftest used to report as a comparison that passed.
106
107
  */
107
108
  function liveness(target, project) {
108
109
  const dir = projectGateDir(target);
@@ -110,10 +111,10 @@ function liveness(target, project) {
110
111
  try {
111
112
  since = readFileSync(join(dir, SINCE), 'utf8').trim();
112
113
  } catch {
113
- return null;
114
+ return { unasked: 'no session has been measured since the gate was first asked' };
114
115
  }
115
116
  const now = Date.now();
116
- const reports = (storedRecords(target) ?? []).filter(
117
+ const reports = (projectRecords(target) ?? []).filter(
117
118
  (r) =>
118
119
  r.verdict_source === 'handback' &&
119
120
  r.session &&
@@ -128,18 +129,20 @@ function liveness(target, project) {
128
129
  .filter((rs) => rs.length >= LIVE_SAMPLE)
129
130
  .sort((a, b) => String(a.map((r) => r.result_ts).sort().at(-1)).localeCompare(String(b.map((r) => r.result_ts).sort().at(-1))))
130
131
  .at(-1);
131
- if (!last) return null;
132
+ if (!last) return { unasked: `no session since then holds ${LIVE_SAMPLE} reports with a verdict line to compare` };
132
133
  const path = ledgerPath(target, last[0].session);
133
134
  const entries = readLedger(path);
134
135
  // A ledger too large to read whole is read from its tail; what came before the tail is not asked about.
135
136
  const from = entries.length > 0 && statSync(path).size > LEDGER_TAIL ? String(entries[0].at) : '';
136
137
  const shown = last.filter((r) => String(r.result_ts) >= from).length;
137
138
  const recorded = entries.filter((e) => e.k === 'verdict').length;
138
- if (shown < LIVE_SAMPLE || recorded * 2 >= shown) return null;
139
- return (
140
- `in session ${last[0].session.slice(0, 8)}, the transcripts show ${shown} report(s) with a verdict line and the gate recorded ${recorded}: ` +
141
- `Claude Code may have changed what it sends to the hooks, and a loop the gate does not see is not capped (${path})`
142
- );
139
+ if (shown < LIVE_SAMPLE) return { unasked: `the newest measured session holds ${shown} report(s) with a verdict line, too few to compare` };
140
+ if (recorded * 2 >= shown) return { compared: shown };
141
+ return {
142
+ problem:
143
+ `in session ${last[0].session.slice(0, 8)}, the transcripts show ${shown} report(s) with a verdict line and the gate recorded ${recorded}: ` +
144
+ `Claude Code may have changed what it sends to the hooks, and a loop the gate does not see is not capped (${path})`,
145
+ };
143
146
  }
144
147
 
145
148
  /**
@@ -299,11 +302,14 @@ export async function gate(argv, ctx) {
299
302
  // A gate that is not wired sees nothing, and says so above; asked whether it saw the pipeline, it would
300
303
  // say the same thing twice.
301
304
  const project = composed && unwired.length === 0 ? loadProject(target) : null;
302
- const blind = project ? liveness(target, project) : null;
303
- if (blind) problems.push(blind);
305
+ const seen = project ? liveness(target, project) : null;
306
+ if (seen?.problem) problems.push(seen.problem);
304
307
 
305
308
  if (problems.length === 0) {
306
- console.log(` wired, writable, no new failure, a dry run through the hook command sent the round past a cap to the owner, and it recorded the reports the transcripts show — ledgers in ${projectGateDir(target)}`);
309
+ const compared = seen?.compared
310
+ ? `and it recorded the ${seen.compared} reports the transcripts show`
311
+ : `and whether it records what the transcripts show is not asked yet — ${seen?.unasked ?? 'nothing to compare'}`;
312
+ console.log(` wired, writable, no new failure, a dry run through the hook command sent the round past a cap to the owner, ${compared} — ledgers in ${projectGateDir(target)}`);
307
313
  console.log('gate: current');
308
314
  return 0;
309
315
  }
@@ -20,7 +20,7 @@ import { existsSync, readFileSync } from 'node:fs';
20
20
  import { join, resolve } from 'node:path';
21
21
  import { REQUIRES, SLOT, byVersion, composeProject, composedPaths, defaultedSlots, layerRootFor, projectOwned, walk } from './compose.mjs';
22
22
  import { owedDocuments } from './check.mjs';
23
- import { defaultVocabulary } from '../vocabulary.mjs';
23
+ import { PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
24
24
  import { PINK, useColor } from '../banner.mjs';
25
25
  import { DETECTABLE, NEEDS } from '../surfaces.mjs';
26
26
 
@@ -273,8 +273,17 @@ export async function init(argv, ctx) {
273
273
  let previous = null;
274
274
  try {
275
275
  previous = JSON.parse(readFileSync(join(harness, 'profile.json'), 'utf8'));
276
- } catch {
277
- // None, or unreadable: there is nothing to keep.
276
+ } catch (error) {
277
+ // None: there is nothing to keep. One that is there but cannot be read still holds the pin and every
278
+ // value the project declared — read as "none", a trailing comma moved the pin to the newest release and
279
+ // wrote every vocabulary value away.
280
+ if (error.code !== 'ENOENT') {
281
+ console.error(
282
+ ` ${HARNESS}/profile.json cannot be read (${error.message}). Starting over would write a new one over it and keep ` +
283
+ 'nothing it declares — fix it and run this again, or move it aside to start from nothing.\n',
284
+ );
285
+ return 1;
286
+ }
278
287
  }
279
288
  if (previous?.core && arg('--core') && arg('--core') !== previous.core) {
280
289
  console.error(` ${target} pins ${previous.core}. A pin moves with \`nina upgrade --to ${arg('--core')}\`, which says what the move costs and rolls it back when it fails.\n`);
@@ -341,7 +350,7 @@ export async function init(argv, ctx) {
341
350
  const released = await defaultedSlots(resolved.dir);
342
351
  for (const { rel, core, fragments } of files) {
343
352
  const lines = core.split('\n');
344
- for (const [, name] of [core, ...fragments].join('\n').matchAll(/\{\{([A-Z_]+)\}\}/g)) {
353
+ for (const [, name] of [core, ...fragments].join('\n').matchAll(PLACEHOLDER)) {
345
354
  vocabulary.add(name);
346
355
  }
347
356
  /** The nearest non-empty neighbour, naming it when it is another slot. */
@@ -26,6 +26,7 @@ import { existsSync, readdirSync } from 'node:fs';
26
26
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
27
27
  import { join, relative, resolve } from 'node:path';
28
28
  import { HARNESS, legacyHint, slugFor, snapshotsDir } from '../paths.mjs';
29
+ import { localDay, projectRecords } from '../store.mjs';
29
30
  import { isLoopBack, runOf } from '../transcripts.mjs';
30
31
  import { REQUIRES, byVersion, layerRootFor } from './compose.mjs';
31
32
  import { GRADUATION_AT, frontmatter, graduationTarget, list, pillFiles, roleGates, tidyRetired } from './pills.mjs';
@@ -52,7 +53,8 @@ const VERIFY_MIN = 5;
52
53
  export { slugFor };
53
54
 
54
55
  /** The day part of a timestamp or a date. */
55
- const day = (ts) => String(ts ?? '').slice(0, 10);
56
+ /** The owner's day a moment fell on: a pill's date is written in it. */
57
+ const day = localDay;
56
58
 
57
59
  /** A verdict the parser actually read, as opposed to one it could not find. */
58
60
  const readable = (v) => Boolean(v) && v !== 'UNCLEAR' && v !== 'NONE';
@@ -64,19 +66,7 @@ const readable = (v) => Boolean(v) && v !== 'UNCLEAR' && v !== 'NONE';
64
66
  * @returns {Promise<object[]>}
65
67
  */
66
68
  async function recorded(target) {
67
- const file = join(snapshotsDir(), `${slugFor(target)}.jsonl`);
68
- const text = await readFile(file, 'utf8').catch(() => '');
69
- return text
70
- .split('\n')
71
- .filter((l) => l.trim())
72
- .map((l) => {
73
- try {
74
- return JSON.parse(l);
75
- } catch {
76
- return null;
77
- }
78
- })
79
- .filter(Boolean);
69
+ return projectRecords(target) ?? [];
80
70
  }
81
71
 
82
72
  /**
@@ -305,11 +295,20 @@ async function answerRequest(target, file, answer) {
305
295
  */
306
296
  async function close(target, which) {
307
297
  const dir = join(target, HARNESS, 'requests');
308
- const file = (await readdir(dir).catch(() => [])).find((f) => f === which || join(dir, f) === resolve(target, which) || f.startsWith(which));
309
- if (!file) {
310
- console.error(` no request matching ${which} under ${HARNESS}/requests/\n`);
298
+ const files = (await readdir(dir).catch(() => [])).filter((f) => f.endsWith('.md'));
299
+ // The name exactly, else a prefix of one — and exactly one, as `requests --answer` asks. A prefix alone let
300
+ // an empty name match every file: a bare `--close` closed whichever request sorted first and retired its pill.
301
+ const exact = files.filter((f) => f === which || join(dir, f) === resolve(target, which));
302
+ const matches = exact.length > 0 ? exact : files.filter((f) => which && f.startsWith(which));
303
+ if (matches.length !== 1) {
304
+ console.error(
305
+ matches.length === 0
306
+ ? ` no request matching "${which}" under ${HARNESS}/requests/ — \`nina learn\` lists them\n`
307
+ : ` "${which}" matches ${matches.length} requests — name one:\n${matches.map((f) => ` ${f}`).join('\n')}\n`,
308
+ );
311
309
  return 1;
312
310
  }
311
+ const [file] = matches;
313
312
  const fields = frontmatter(await readFile(join(dir, file), 'utf8')) ?? {};
314
313
  const { pill } = await answerRequest(target, file, { status: 'closed', retire: true });
315
314
  console.log(` closed ${HARNESS}/requests/${file}`);
@@ -477,7 +476,7 @@ async function deepReport(target, records, known, argv) {
477
476
  */
478
477
  async function fileRequest(target, profile, layerRoot, lesson) {
479
478
  const target_ = graduationTarget(lesson.roles, await roleGates(layerRoot));
480
- const date = new Date().toISOString().slice(0, 10);
479
+ const date = localDay(new Date().toISOString());
481
480
  // The directory separator becomes `__`, so `reviewer/a.b.md` and `reviewer-a/b.md` stay two
482
481
  // names — flattening both `/` and `.` to `-` gave them the same one.
483
482
  const name = lesson.rel.replace(/^\.claude\/pills\//, '').replace(/\.md$/, '').replace(/\//g, '__');
@@ -18,7 +18,7 @@ import { TERMINALS, parseGraph } from '../graph.mjs';
18
18
  import { required } from '../tools.mjs';
19
19
  import { isLoopBack, runOf } from '../transcripts.mjs';
20
20
  import { frontmatter } from './pills.mjs';
21
- import { storedRecords } from './snapshot.mjs';
21
+ import { localDay, projectRecords } from '../store.mjs';
22
22
 
23
23
  /** A stage line under "## Stages": "- `name` — what it does". */
24
24
  const STAGE_LINE = /^- `([a-z][a-z-]*)` — (.*)$/;
@@ -149,7 +149,7 @@ export function historyOf(records, since) {
149
149
  const byRole = new Map();
150
150
  const seen = new Map();
151
151
  for (const r of records) {
152
- if (!r.role || r.status === 'denied' || String(r.ts ?? '') < since) continue;
152
+ if (!r.role || r.status === 'denied' || localDay(r.ts) < since) continue;
153
153
  const s = byRole.get(r.role) ?? { runs: 0, rounds: 0, read: 0, back: 0 };
154
154
  const runs = seen.get(r.role) ?? new Set();
155
155
  runs.add(runOf(r));
@@ -288,8 +288,8 @@ export async function pipeline(argv) {
288
288
  for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
289
289
  specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
290
290
  }
291
- const since = arg('--since') || new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString().slice(0, 10);
292
- const runs = storedRecords(dir);
291
+ const since = arg('--since') || localDay(new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString());
292
+ const runs = projectRecords(dir);
293
293
  const history = runs ? historyOf(runs, since) : null;
294
294
  const stages = [...graph.stages].filter((s) => covered.has(s));
295
295
  const col = Math.max(...stages.map((s) => s.length), 10) + 2;