@xhulz/nina 0.38.0 → 0.40.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 (274) hide show
  1. package/package.json +1 -1
  2. package/releases/0.39.0/core/GAPS.md +125 -0
  3. package/releases/0.39.0/core/answered.json +32 -0
  4. package/releases/0.39.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  5. package/releases/0.39.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  6. package/releases/0.39.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  7. package/releases/0.39.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  8. package/releases/0.39.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  9. package/releases/0.39.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  10. package/releases/0.39.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  11. package/releases/0.39.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  12. package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  13. package/releases/0.39.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  14. package/releases/0.39.0/core/tree/.claude/agents/architect.md +127 -0
  15. package/releases/0.39.0/core/tree/.claude/agents/dba.md +110 -0
  16. package/releases/0.39.0/core/tree/.claude/agents/devops.md +114 -0
  17. package/releases/0.39.0/core/tree/.claude/agents/implementer.md +156 -0
  18. package/releases/0.39.0/core/tree/.claude/agents/integration-tester.md +168 -0
  19. package/releases/0.39.0/core/tree/.claude/agents/planner.md +132 -0
  20. package/releases/0.39.0/core/tree/.claude/agents/qa.md +152 -0
  21. package/releases/0.39.0/core/tree/.claude/agents/reviewer.md +183 -0
  22. package/releases/0.39.0/core/tree/.claude/agents/secops.md +121 -0
  23. package/releases/0.39.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  24. package/releases/0.39.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  25. package/releases/0.39.0/core/tree/.claude/agents-overview.md +64 -0
  26. package/releases/0.39.0/core/tree/.claude/frontend.md +10 -0
  27. package/releases/0.39.0/core/tree/.claude/graph.md +75 -0
  28. package/releases/0.39.0/core/tree/.claude/patterns.md +213 -0
  29. package/releases/0.39.0/core/tree/.claude/pills/README.md +147 -0
  30. package/releases/0.39.0/core/tree/.claude/pipeline.md +101 -0
  31. package/releases/0.39.0/core/tree/.claude/retrieval.md +103 -0
  32. package/releases/0.39.0/core/tree/.claude/router.md +198 -0
  33. package/releases/0.39.0/core/tree/.claude/templates/integration.md +45 -0
  34. package/releases/0.39.0/core/tree/CLAUDE.md +163 -0
  35. package/releases/0.39.0/core/tree/scripts/cost-watch.mjs +18 -0
  36. package/releases/0.39.0/core/tree/scripts/edit-guard.mjs +24 -0
  37. package/releases/0.39.0/core/tree/scripts/harness-check.mjs +71 -0
  38. package/releases/0.39.0/core/tree/scripts/loop-gate.mjs +19 -0
  39. package/releases/0.39.0/core/vocabulary.json +13 -0
  40. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  41. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  42. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  43. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  44. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  45. package/releases/0.39.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  46. package/releases/0.39.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  47. package/releases/0.39.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  48. package/releases/0.39.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  49. package/releases/0.39.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  50. package/releases/0.39.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  51. package/releases/0.39.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  52. package/releases/0.39.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  53. package/releases/0.39.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  54. package/releases/0.39.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  55. package/releases/0.39.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  56. package/releases/0.39.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  57. package/releases/0.39.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  58. package/releases/0.39.0/surfaces/db/tree/.claude/graph.md +8 -0
  59. package/releases/0.39.0/surfaces/db/tree/.claude/patterns.md +19 -0
  60. package/releases/0.39.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  61. package/releases/0.39.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  62. package/releases/0.39.0/surfaces/db/tree/.claude/router.md +4 -0
  63. package/releases/0.39.0/surfaces/db/tree/CLAUDE.md +17 -0
  64. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  65. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  66. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  67. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  68. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  69. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  70. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  71. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  72. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  73. package/releases/0.39.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  74. package/releases/0.39.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  75. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  76. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  77. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  78. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  79. package/releases/0.39.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  80. package/releases/0.39.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  81. package/releases/0.39.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  82. package/releases/0.39.0/surfaces/frontend/tree/.claude/router.md +14 -0
  83. package/releases/0.39.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  84. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  85. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  86. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  87. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  88. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  89. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  90. package/releases/0.39.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  91. package/releases/0.39.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  92. package/releases/0.39.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  93. package/releases/0.39.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  94. package/releases/0.39.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  95. package/releases/0.39.0/surfaces/integrations/tree/.claude/router.md +6 -0
  96. package/releases/0.39.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  97. package/releases/0.39.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  98. package/releases/0.39.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  99. package/releases/0.39.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  100. package/releases/0.39.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  101. package/releases/0.39.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  102. package/releases/0.39.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  103. package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  104. package/releases/0.39.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  105. package/releases/0.39.0/surfaces/money/tree/.claude/graph.md +2 -0
  106. package/releases/0.39.0/surfaces/money/tree/.claude/patterns.md +49 -0
  107. package/releases/0.39.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  108. package/releases/0.39.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  109. package/releases/0.39.0/surfaces/money/tree/CLAUDE.md +11 -0
  110. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  111. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  112. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  113. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  114. package/releases/0.39.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  115. package/releases/0.39.0/surfaces/pii/tree/CLAUDE.md +5 -0
  116. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  117. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  118. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  119. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  120. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  121. package/releases/0.39.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  122. package/releases/0.39.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  123. package/releases/0.39.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  124. package/releases/0.40.0/core/GAPS.md +125 -0
  125. package/releases/0.40.0/core/answered.json +32 -0
  126. package/releases/0.40.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  127. package/releases/0.40.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  128. package/releases/0.40.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  129. package/releases/0.40.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  130. package/releases/0.40.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  131. package/releases/0.40.0/core/defaults/tree/.claude/agents/qa.md +2 -0
  132. package/releases/0.40.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  133. package/releases/0.40.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  134. package/releases/0.40.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  135. package/releases/0.40.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  136. package/releases/0.40.0/core/tree/.claude/agents/architect.md +127 -0
  137. package/releases/0.40.0/core/tree/.claude/agents/dba.md +110 -0
  138. package/releases/0.40.0/core/tree/.claude/agents/devops.md +114 -0
  139. package/releases/0.40.0/core/tree/.claude/agents/implementer.md +156 -0
  140. package/releases/0.40.0/core/tree/.claude/agents/integration-tester.md +168 -0
  141. package/releases/0.40.0/core/tree/.claude/agents/planner.md +132 -0
  142. package/releases/0.40.0/core/tree/.claude/agents/qa.md +152 -0
  143. package/releases/0.40.0/core/tree/.claude/agents/reviewer.md +183 -0
  144. package/releases/0.40.0/core/tree/.claude/agents/secops.md +121 -0
  145. package/releases/0.40.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  146. package/releases/0.40.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  147. package/releases/0.40.0/core/tree/.claude/agents-overview.md +64 -0
  148. package/releases/0.40.0/core/tree/.claude/frontend.md +10 -0
  149. package/releases/0.40.0/core/tree/.claude/graph.md +75 -0
  150. package/releases/0.40.0/core/tree/.claude/patterns.md +213 -0
  151. package/releases/0.40.0/core/tree/.claude/pills/README.md +147 -0
  152. package/releases/0.40.0/core/tree/.claude/pipeline.md +101 -0
  153. package/releases/0.40.0/core/tree/.claude/retrieval.md +103 -0
  154. package/releases/0.40.0/core/tree/.claude/router.md +198 -0
  155. package/releases/0.40.0/core/tree/.claude/templates/integration.md +45 -0
  156. package/releases/0.40.0/core/tree/CLAUDE.md +163 -0
  157. package/releases/0.40.0/core/tree/scripts/cost-watch.mjs +25 -0
  158. package/releases/0.40.0/core/tree/scripts/edit-guard.mjs +24 -0
  159. package/releases/0.40.0/core/tree/scripts/harness-check.mjs +71 -0
  160. package/releases/0.40.0/core/tree/scripts/loop-gate.mjs +19 -0
  161. package/releases/0.40.0/core/vocabulary.json +13 -0
  162. package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  163. package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  164. package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  165. package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  166. package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  167. package/releases/0.40.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  168. package/releases/0.40.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  169. package/releases/0.40.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  170. package/releases/0.40.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  171. package/releases/0.40.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  172. package/releases/0.40.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
  173. package/releases/0.40.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  174. package/releases/0.40.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  175. package/releases/0.40.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  176. package/releases/0.40.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  177. package/releases/0.40.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  178. package/releases/0.40.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  179. package/releases/0.40.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  180. package/releases/0.40.0/surfaces/db/tree/.claude/graph.md +8 -0
  181. package/releases/0.40.0/surfaces/db/tree/.claude/patterns.md +19 -0
  182. package/releases/0.40.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  183. package/releases/0.40.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  184. package/releases/0.40.0/surfaces/db/tree/.claude/router.md +4 -0
  185. package/releases/0.40.0/surfaces/db/tree/CLAUDE.md +17 -0
  186. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  187. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  188. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
  189. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  190. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  191. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  192. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  193. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  194. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  195. package/releases/0.40.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  196. package/releases/0.40.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  197. package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  198. package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  199. package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  200. package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  201. package/releases/0.40.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  202. package/releases/0.40.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  203. package/releases/0.40.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  204. package/releases/0.40.0/surfaces/frontend/tree/.claude/router.md +14 -0
  205. package/releases/0.40.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  206. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  207. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  208. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  209. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  210. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  211. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  212. package/releases/0.40.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  213. package/releases/0.40.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  214. package/releases/0.40.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  215. package/releases/0.40.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  216. package/releases/0.40.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  217. package/releases/0.40.0/surfaces/integrations/tree/.claude/router.md +6 -0
  218. package/releases/0.40.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  219. package/releases/0.40.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  220. package/releases/0.40.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  221. package/releases/0.40.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  222. package/releases/0.40.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  223. package/releases/0.40.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  224. package/releases/0.40.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  225. package/releases/0.40.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  226. package/releases/0.40.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  227. package/releases/0.40.0/surfaces/money/tree/.claude/graph.md +2 -0
  228. package/releases/0.40.0/surfaces/money/tree/.claude/patterns.md +49 -0
  229. package/releases/0.40.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  230. package/releases/0.40.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  231. package/releases/0.40.0/surfaces/money/tree/CLAUDE.md +11 -0
  232. package/releases/0.40.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  233. package/releases/0.40.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  234. package/releases/0.40.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  235. package/releases/0.40.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  236. package/releases/0.40.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  237. package/releases/0.40.0/surfaces/pii/tree/CLAUDE.md +5 -0
  238. package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  239. package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
  240. package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  241. package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  242. package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  243. package/releases/0.40.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  244. package/releases/0.40.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  245. package/releases/0.40.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  246. package/src/commands/check.mjs +15 -28
  247. package/src/commands/compose.mjs +50 -64
  248. package/src/commands/eval.mjs +1 -1
  249. package/src/commands/export.mjs +4 -34
  250. package/src/commands/gate.mjs +4 -11
  251. package/src/commands/init.mjs +22 -30
  252. package/src/commands/langfuse.mjs +1 -1
  253. package/src/commands/learn.mjs +10 -12
  254. package/src/commands/pills.mjs +5 -5
  255. package/src/commands/pipeline.mjs +4 -8
  256. package/src/commands/release.mjs +2 -3
  257. package/src/commands/runs.mjs +1 -1
  258. package/src/commands/snapshot.mjs +12 -50
  259. package/src/commands/stats.mjs +7 -8
  260. package/src/commands/status.mjs +4 -5
  261. package/src/commands/upgrade.mjs +9 -12
  262. package/src/commands/where.mjs +9 -18
  263. package/src/commands/wire.mjs +8 -19
  264. package/src/deep.mjs +3 -3
  265. package/src/gate.mjs +54 -7
  266. package/src/graph.mjs +1 -1
  267. package/src/guard.mjs +194 -7
  268. package/src/langfuse.mjs +9 -8
  269. package/src/lock.mjs +82 -0
  270. package/src/paths.mjs +65 -0
  271. package/src/shell.mjs +3 -3
  272. package/src/steps.mjs +5 -19
  273. package/src/store.mjs +2 -2
  274. package/src/wiring.mjs +20 -7
@@ -9,14 +9,14 @@
9
9
  */
10
10
 
11
11
  import { readFile, readdir } from 'node:fs/promises';
12
- import { HARNESS, describeMove, interruptedMove, legacyHint } from '../paths.mjs';
12
+ import { describeMove, HARNESS, interruptedMove, readProfile, surfacesIn } 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, noticeOf, readLayerText, stripWhy, walk } from './compose.mjs';
19
+ import { SLOT, closedGate, defaultedSlots, layerRootFor, noticeOf, readLayerText, stripWhy, walk } from './compose.mjs';
20
20
  import { PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
21
21
  import { NEEDS } from '../surfaces.mjs';
22
22
 
@@ -49,8 +49,7 @@ export async function referencedVocabulary(layerRoot, surfaces, target) {
49
49
  const found = new Set();
50
50
  for (const rel of await walk(coreTree)) {
51
51
  const core = await readFile(join(coreTree, rel), 'utf8');
52
- const requires = REQUIRES.exec(core);
53
- if (requires && !surfaces.includes(requires[1])) continue;
52
+ if (closedGate(core, surfaces)) continue;
54
53
  const texts = [core];
55
54
  for (const s of surfaces) {
56
55
  const t = await readFile(join(layerRoot, 'surfaces', s, 'tree', rel), 'utf8').catch(() => null);
@@ -80,8 +79,7 @@ export async function projectSlots(layerRoot, surfaces) {
80
79
  const slots = new Set();
81
80
  for (const rel of await walk(coreTree)) {
82
81
  const core = await readFile(join(coreTree, rel), 'utf8');
83
- const requires = REQUIRES.exec(core);
84
- if (requires && !surfaces.includes(requires[1])) continue;
82
+ if (closedGate(core, surfaces)) continue;
85
83
  for (const line of core.split('\n')) {
86
84
  const marker = SLOT.exec(line);
87
85
  if (marker?.[1].startsWith('project.')) slots.add(`${rel} ${marker[1]}`);
@@ -142,13 +140,6 @@ export async function owedDocuments(layerRoot, surfaces) {
142
140
  return owed;
143
141
  }
144
142
 
145
- /**
146
- * `nina check`.
147
- *
148
- * @param {string[]} argv - Command arguments; `--project <dir>` selects the target.
149
- * @param {{root: string}} ctx - CLI context; `root` is the NINA install directory.
150
- * @returns {Promise<number>} Process exit code.
151
- */
152
143
  /**
153
144
  * Composed scripts that nothing in the project runs.
154
145
  *
@@ -191,6 +182,13 @@ export async function unwiredScripts(layerRoot, surfaces, target) {
191
182
  .sort();
192
183
  }
193
184
 
185
+ /**
186
+ * `nina check`.
187
+ *
188
+ * @param {string[]} argv - Command arguments; `--project <dir>` selects the target.
189
+ * @param {{root: string}} ctx - CLI context; `root` is the NINA install directory.
190
+ * @returns {Promise<number>} Process exit code.
191
+ */
194
192
  export async function check(argv, ctx) {
195
193
  const target = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : '.');
196
194
  // `--detector`: run by every project's harness:check. Inside `nina upgrade --apply` the move measures
@@ -204,10 +202,9 @@ export async function check(argv, ctx) {
204
202
  }
205
203
 
206
204
  const expected = expectedUnfilled(argv);
207
- const profilePath = join(target, HARNESS, 'profile.json');
208
- if (!existsSync(profilePath)) {
209
- const hint = legacyHint(target);
210
- console.error(` no .nina/profile.json under ${target}${hint || ' — run `nina init` first.'}\n`);
205
+ const { profile, error } = readProfile(target);
206
+ if (error) {
207
+ console.error(` ${error}\n`);
211
208
  return 1;
212
209
  }
213
210
 
@@ -216,14 +213,6 @@ export async function check(argv, ctx) {
216
213
  /** Things that are merely stale — worth saying, not worth failing. */
217
214
  const notes = [];
218
215
 
219
- let profile;
220
- try {
221
- profile = JSON.parse(await readFile(profilePath, 'utf8'));
222
- } catch (error) {
223
- console.error(` .nina/profile.json is not valid JSON: ${error.message}\n`);
224
- return 1;
225
- }
226
-
227
216
  const resolved = layerRootFor(ctx.root, profile.core);
228
217
  if (resolved.error) {
229
218
  console.error(` ${resolved.error}\n`);
@@ -235,9 +224,7 @@ export async function check(argv, ctx) {
235
224
  if (stopped) problems.push(`${describeMove(stopped)} \`nina upgrade --abort\` undoes it`);
236
225
 
237
226
  const surfaces = profile.surfaces ?? [];
238
- const available = (await readdir(join(resolved.dir, 'surfaces'), { withFileTypes: true }))
239
- .filter((e) => e.isDirectory())
240
- .map((e) => e.name);
227
+ const available = (await surfacesIn(resolved.dir));
241
228
  for (const s of surfaces) {
242
229
  if (!available.includes(s)) {
243
230
  problems.push(`surface "${s}" does not exist in core ${profile.core} — available: ${available.join(', ')}`);
@@ -10,18 +10,20 @@
10
10
  * placeholder, filled from the profile. A surface the project does not declare has its
11
11
  * markers dropped, so a project with no database never reads a word about Prisma.
12
12
  *
13
- * The test that keeps this honest is a recomposition of the project it was extracted
14
- * from: the output must be byte-identical to what is running. Anything weaker depends on
15
- * someone's judgement that no rule was lost, and judgement is what let a mandatory rule
16
- * sit unenforceable for three months here.
13
+ * What keeps this honest is `scripts/compose-test.mjs`: every fixture composed and held to the
14
+ * properties it lists, so no rule is lost on someone's judgement that none was. It began as a
15
+ * recomposition of the project the harness was extracted from, byte for byte; that oracle went
16
+ * when projects began pinning frozen releases.
17
17
  */
18
18
 
19
19
  import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
20
- import { HARNESS, legacyHint } from '../paths.mjs';
20
+ import { HARNESS, layerRootFor, readProfile } 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 { NAME, defaultVocabulary } from '../vocabulary.mjs';
24
+ import { NAME, PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
25
+
26
+ export { layerRootFor };
25
27
 
26
28
  /**
27
29
  * Matches a slot marker on its own line, with an optional label.
@@ -60,6 +62,19 @@ export function stripWhy(text, { keepLines = false } = {}) {
60
62
  */
61
63
  export const REQUIRES = /^<!-- nina:requires ([a-z-]+) -->\n/;
62
64
 
65
+ /**
66
+ * The surface a core file is gated on when the project does not declare it — the file is not composed there —
67
+ * or null. Every reader that asks whether a gated file applies asks this.
68
+ *
69
+ * @param {string} text - The core file.
70
+ * @param {string[]} surfaces - What the project declares.
71
+ * @returns {string|null}
72
+ */
73
+ export function closedGate(text, surfaces) {
74
+ const gate = REQUIRES.exec(text)?.[1] ?? null;
75
+ return gate && !surfaces.includes(gate) ? gate : null;
76
+ }
77
+
63
78
  /**
64
79
  * A slot inside a line — text before it, and the marker last. Filled with its fragment collapsed
65
80
  * to one line; dropped, like any slot, when its surface is not declared.
@@ -199,33 +214,6 @@ export function stamp(text, rel, slots) {
199
214
  return `${preamble}${generatedNotice(rel, slots, script)}\n\n${body}`;
200
215
  }
201
216
 
202
- /**
203
- * Where a project's layers are read from.
204
- *
205
- * A project pins a frozen release, or tracks the working tree with "dev". A pinned release
206
- * that is missing is an error, never a silent fall back to whatever the harness happens to
207
- * look like right now: that would compose an unreviewed core into a project that asked for
208
- * a reviewed one, and nothing downstream would say so.
209
- *
210
- * @param {string} root - The NINA install directory.
211
- * @param {string|undefined} core - The profile's `core` field.
212
- * @returns {{dir: string}|{error: string}}
213
- */
214
- export function layerRootFor(root, core) {
215
- if (!core || core === 'dev') {
216
- // `dev` means "track the layers as they are being worked on", which only exists in a
217
- // checkout of this repo. An installed package ships frozen releases and nothing else,
218
- // so say that rather than composing an empty tree and reporting nothing wrong.
219
- if (!existsSync(join(root, 'core'))) {
220
- return { error: `profile pins core "dev", which tracks the NINA working tree — ${root} is an installed package and has only releases` };
221
- }
222
- return { dir: root };
223
- }
224
- const dir = join(root, 'releases', core);
225
- if (!existsSync(dir)) return { error: `profile pins core ${core}, which is not in ${join(root, 'releases')}` };
226
- return { dir };
227
- }
228
-
229
217
  /**
230
218
  * Orders release names by their numbers rather than as text.
231
219
  *
@@ -376,8 +364,7 @@ export async function composedPaths(layerRoot, surfaces = []) {
376
364
  const coreTree = join(layerRoot, 'core', 'tree');
377
365
  const out = new Set();
378
366
  for (const rel of await walk(coreTree)) {
379
- const gate = REQUIRES.exec(await readFile(join(coreTree, rel), 'utf8'));
380
- if (gate && !surfaces.includes(gate[1])) continue;
367
+ if (closedGate(await readFile(join(coreTree, rel), 'utf8'), surfaces)) continue;
381
368
  out.add(rel);
382
369
  }
383
370
  return out;
@@ -432,11 +419,8 @@ export async function composeProject(target, ctx, options = {}) {
432
419
  /** Files composed for a surface this profile no longer declares, moved to {@link REMOVED}. */
433
420
  const removed = [];
434
421
 
435
- const profilePath = join(target, HARNESS, 'profile.json');
436
- if (!existsSync(profilePath)) {
437
- return { error: `no .nina/profile.json under ${target}${legacyHint(target)}`, written, differ, unfilled: [], skipped };
438
- }
439
- const profile = JSON.parse(await readFile(profilePath, 'utf8'));
422
+ const { profile, error } = readProfile(target);
423
+ if (error) return { error, written, differ, unfilled: [], skipped };
440
424
  const surfaces = profile.surfaces ?? [];
441
425
 
442
426
  const resolved = layerRootFor(ctx.root, profile.core);
@@ -479,29 +463,27 @@ export async function composeProject(target, ctx, options = {}) {
479
463
  let core = await readFile(join(coreTree, rel), 'utf8');
480
464
  const dest = join(target, rel);
481
465
 
482
- const requires = REQUIRES.exec(core);
483
- if (requires) {
484
- core = core.slice(requires[0].length);
485
- if (!surfaces.includes(requires[1])) {
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.
489
- skipped.push(rel);
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
- }
503
- continue;
466
+ const closed = closedGate(core, surfaces);
467
+ core = core.replace(REQUIRES, '');
468
+ if (closed) {
469
+ // The project has no such surface, so the file does not exist for it. One composed for it before the
470
+ // surface left the profile is still dispatched as a stage the graph no longer has, and was reported as
471
+ // drift that no compose cleared: it goes, with a copy kept. A file there with no notice is the project's.
472
+ skipped.push(rel);
473
+ let stale = false;
474
+ try {
475
+ stale = lstatSync(dest).isFile() && noticeOf(await readFile(dest, 'utf8')) !== null;
476
+ } catch {
477
+ // Not there.
504
478
  }
479
+ if (stale && check) differ.push(`${rel} (present, but no '${closed}' surface)`);
480
+ if (stale && !check) {
481
+ await mkdir(dirname(join(target, REMOVED, rel)), { recursive: true });
482
+ await writeFile(join(target, REMOVED, rel), await readFile(dest));
483
+ await rm(dest);
484
+ removed.push(rel);
485
+ }
486
+ continue;
505
487
  }
506
488
 
507
489
  // History is stripped from each piece before the pieces are joined. Joined first, an opener left
@@ -558,8 +540,8 @@ export async function composeProject(target, ctx, options = {}) {
558
540
  let text = out.join('\n');
559
541
  if (text.includes('nina:why')) malformed.push(rel);
560
542
  for (const [name, value] of Object.entries(profile.vocabulary ?? {})) {
561
- // `null` means "declared but not filled in yet" — leave the placeholder standing so
562
- // it is reported, rather than composing the rule with a hole where a noun should be.
543
+ // `null` means "declared but not filled in yet": it composes as owed, below, and `check` reports it,
544
+ // rather than the rule composing with a hole where a noun should be.
563
545
  if (value === null || value === undefined) continue;
564
546
  text = text.split(`{{${name}}}`).join(value);
565
547
  }
@@ -574,6 +556,10 @@ export async function composeProject(target, ctx, options = {}) {
574
556
  for (const [name, why] of Object.entries(profile.deferred ?? {})) {
575
557
  if (typeof why === 'string' && why.trim() && NAME.test(name)) text = text.split(`{{${name}}}`).join(`[${name}: not decided yet]`);
576
558
  }
559
+ // What is still a placeholder is owed: no default answers it, and the project has not declared it, or has
560
+ // declared it null. Left raw, it reached a stage as `{{TENANT_KEY}}` — once inside a shell block it was told to
561
+ // run — and reads as an editing slip to guess past. Said, it is a noun the stage knows it does not have.
562
+ text = text.replace(PLACEHOLDER, (_, name) => `[${name}: not declared yet]`);
577
563
 
578
564
  // Only what has a comment syntax the notice knows. A layer that one day composes JSON
579
565
  // would be corrupted by it rather than marked, so it gets nothing and says nothing.
@@ -225,7 +225,7 @@ export function fixtureDiff(fixture) {
225
225
  }
226
226
 
227
227
  /** What the judge must answer: a call on every planted defect, with the words it rests on, and on everything else. */
228
- export const JUDGE_SCHEMA = {
228
+ const JUDGE_SCHEMA = {
229
229
  type: 'object',
230
230
  properties: {
231
231
  defects: {
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { createHash } from 'node:crypto';
22
22
  import { existsSync } from 'node:fs';
23
- import { mkdir, open, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
23
+ import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
24
24
  import { spawn } from 'node:child_process';
25
25
  import { join } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
@@ -32,6 +32,7 @@ import { readStoreFile } from '../store.mjs';
32
32
  import { readConfig, statusPath, targetOf } from './langfuse.mjs';
33
33
  import { projectName } from './stats.mjs';
34
34
  import { handsToModel } from '../detectors.mjs';
35
+ import { takeLock } from '../lock.mjs';
35
36
 
36
37
  /** The CLI, for the export the detector starts in the background. */
37
38
  const BIN = fileURLToPath(new URL('../../bin/nina.mjs', import.meta.url));
@@ -84,37 +85,6 @@ async function sentOf(slug) {
84
85
  }
85
86
  }
86
87
 
87
- /**
88
- * Holds a project's export for one run. Two at once would each read what was sent before either wrote,
89
- * and send the same runs twice. A lock left by a run that died is taken over: it names its process.
90
- *
91
- * @returns {Promise<(() => Promise<void>)|null>} The release, or null when another export holds it.
92
- */
93
- async function lock(slug) {
94
- await mkdir(join(exportsDir(), 'langfuse'), { recursive: true });
95
- const path = join(exportsDir(), 'langfuse', `${slug}.lock`);
96
- for (let attempt = 0; attempt < 2; attempt += 1) {
97
- try {
98
- const handle = await open(path, 'wx');
99
- await handle.writeFile(String(process.pid));
100
- await handle.close();
101
- return () => rm(path, { force: true });
102
- } catch (error) {
103
- if (error.code !== 'EEXIST') throw error;
104
- const pid = Number(await readFile(path, 'utf8').catch(() => ''));
105
- let alive = false;
106
- try {
107
- alive = pid > 0 && process.kill(pid, 0);
108
- } catch (e) {
109
- alive = e.code === 'EPERM';
110
- }
111
- if (alive) return null;
112
- await rm(path, { force: true });
113
- }
114
- }
115
- return null;
116
- }
117
-
118
88
  /** Writes it by rename, so an interruption leaves the last whole version. */
119
89
  async function keep(slug, sent) {
120
90
  await mkdir(join(exportsDir(), 'langfuse'), { recursive: true });
@@ -203,7 +173,7 @@ export async function exportCommand(argv, ctx = {}) {
203
173
  if (auto) {
204
174
  const setting = config.projects[only ?? ''];
205
175
  if (!setting || !target.publicKey || !target.secretKey) return 0;
206
- const release = await lock(only);
176
+ const release = await takeLock(join(exportsDir(), 'langfuse', `${only}.lock`));
207
177
  if (!release) return 0;
208
178
  try {
209
179
  const result = await exportProject({ slug: only, name: projectName(only), file: join(snapshotsDir(), `${only}.jsonl`), target, now: ctx.now, since: setting.since, quiet: true, content: contextMode(setting) });
@@ -236,7 +206,7 @@ export async function exportCommand(argv, ctx = {}) {
236
206
  let failed = false;
237
207
  for (const slug of slugs) {
238
208
  const name = projectName(slug);
239
- const release = dry ? async () => {} : await lock(slug);
209
+ const release = dry ? async () => {} : await takeLock(join(exportsDir(), 'langfuse', `${slug}.lock`));
240
210
  if (!release) {
241
211
  console.log(` ${name}: ✗ another export of this project is running — nothing sent`);
242
212
  failed = true;
@@ -18,7 +18,7 @@ import { spawnSync } from 'node:child_process';
18
18
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
19
19
  import { tmpdir } from 'node:os';
20
20
  import { join, resolve } from 'node:path';
21
- import { HARNESS, legacyHint, slugFor } from '../paths.mjs';
21
+ import { readProfile, 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
24
  import { projectRecords } from '../store.mjs';
@@ -279,16 +279,9 @@ export async function gate(argv, ctx) {
279
279
  const target = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : '.');
280
280
  if (argv.includes('--hook')) return runGate({ root: target });
281
281
 
282
- const profilePath = join(target, HARNESS, 'profile.json');
283
- if (!existsSync(profilePath)) {
284
- console.error(` no ${HARNESS}/profile.json under ${target}${legacyHint(target)}\n`);
285
- return 2;
286
- }
287
- let profile;
288
- try {
289
- profile = JSON.parse(readFileSync(profilePath, 'utf8'));
290
- } catch (error) {
291
- console.error(` ${profilePath} is not valid JSON — ${error.message}\n`);
282
+ const { profile, error } = readProfile(target);
283
+ if (error) {
284
+ console.error(` ${error}\n`);
292
285
  return 2;
293
286
  }
294
287
  const resolved = layerRootFor(ctx.root, profile.core);
@@ -13,16 +13,16 @@
13
13
  */
14
14
 
15
15
  import { createInterface } from 'node:readline/promises';
16
- import { HARNESS } from '../paths.mjs';
16
+ import { HARNESS, readProfile, surfacesIn } from '../paths.mjs';
17
17
  import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
18
- import { applyWiring, missingWiring, shippedScripts } from '../wiring.mjs';
19
- import { existsSync, readFileSync } from 'node:fs';
18
+ import { applyWiring, lacksPackage, missingWiring, shippedScripts } from '../wiring.mjs';
19
+ import { existsSync } from 'node:fs';
20
20
  import { join, resolve } from 'node:path';
21
- import { REQUIRES, SLOT, byVersion, composeProject, composedPaths, defaultedSlots, layerRootFor, projectOwned, walk } from './compose.mjs';
21
+ import { REQUIRES, SLOT, byVersion, closedGate, composedPaths, composeProject, defaultedSlots, layerRootFor, projectOwned, walk } from './compose.mjs';
22
22
  import { owedDocuments } from './check.mjs';
23
23
  import { PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
24
- import { PINK, useColor } from '../banner.mjs';
25
24
  import { DETECTABLE, NEEDS } from '../surfaces.mjs';
25
+ import { bold, dim, green, pink } from '../look.mjs';
26
26
 
27
27
  /**
28
28
  * The question each surface answers. Detection can confirm three of them from files; all
@@ -72,11 +72,10 @@ export function askOrder(available) {
72
72
  return [...available].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
73
73
  }
74
74
 
75
- /** The interview's colors — the banner's pink, on a terminal that takes color, and plain everywhere else. */
76
- const tint = (code, text) => (useColor() ? `\x1b[${code}m${text}\x1b[0m` : text);
77
- const strong = (text) => tint(`1;38;2;${PINK.join(';')}`, text);
78
- const faint = (text) => tint('2', text);
79
- const found = (text) => tint('32', text);
75
+ /** The interview's colors — the reports' own, on a terminal that takes color, and plain everywhere else. */
76
+ const strong = (text) => bold(pink(text));
77
+ const faint = dim;
78
+ const found = green;
80
79
 
81
80
  /**
82
81
  * What declaring a surface actually adds, counted from the layer rather than described.
@@ -149,14 +148,13 @@ async function applicableFiles(root, surfaces) {
149
148
  const out = [];
150
149
  for (const rel of (await walk(coreTree)).sort()) {
151
150
  const core = await readFile(join(coreTree, rel), 'utf8');
152
- const requires = REQUIRES.exec(core);
153
- if (requires && !surfaces.includes(requires[1])) continue;
151
+ if (closedGate(core, surfaces)) continue;
154
152
  const fragments = [];
155
153
  for (const s of surfaces) {
156
154
  const text = await readFile(join(root, 'surfaces', s, 'tree', rel), 'utf8').catch(() => null);
157
155
  if (text !== null) fragments.push(text);
158
156
  }
159
- out.push({ rel, core: requires ? core.slice(requires[0].length) : core, fragments });
157
+ out.push({ rel, core: core.replace(REQUIRES, ''), fragments });
160
158
  }
161
159
  return out;
162
160
  }
@@ -270,21 +268,15 @@ export async function init(argv, ctx) {
270
268
  // itself says to re-run with --force to add one, and doing so used to write a profile from nothing —
271
269
  // every vocabulary value and integration gone, and the pin moved to the newest release past every
272
270
  // check `upgrade` makes.
273
- let previous = null;
274
- try {
275
- previous = JSON.parse(readFileSync(join(harness, 'profile.json'), 'utf8'));
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
- }
271
+ // None: there is nothing to keep. One that is there but cannot be read still holds the pin and every value
272
+ // the project declared — read as "none", a trailing comma moved the pin to the newest release and wrote every
273
+ // vocabulary value away.
274
+ const read = readProfile(target);
275
+ if (read.error && !read.missing) {
276
+ console.error(` ${read.error}. Starting over would write a new one over it and keep nothing it declares — fix it and run this again, or move it aside to start from nothing.\n`);
277
+ return 1;
287
278
  }
279
+ const previous = read.profile ?? null;
288
280
  if (previous?.core && arg('--core') && arg('--core') !== previous.core) {
289
281
  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`);
290
282
  return 1;
@@ -314,7 +306,7 @@ export async function init(argv, ctx) {
314
306
  // harness the project is actually going to compose, not whatever the working tree says.
315
307
  // In the order the interview asks them, so the TODO, the brief and the hints list them as it did.
316
308
  const available = askOrder(
317
- (await readdir(join(resolved.dir, 'surfaces'), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name),
309
+ (await surfacesIn(resolved.dir)),
318
310
  );
319
311
  // Only what the pinned core has: a file can reveal a surface a release added after it.
320
312
  matched = matched.filter((d) => available.includes(d.surface));
@@ -441,7 +433,7 @@ export async function init(argv, ctx) {
441
433
  `## 1. Vocabulary — ${vocabulary.size} entries`,
442
434
  '',
443
435
  `Every name below appears in a rule the chosen core and surfaces state generically. Fill each one`,
444
- `in \`.nina/profile.json\`; a \`null\` leaves the placeholder standing in the composed output.`,
436
+ `in \`.nina/profile.json\`; a \`null\` composes as \`[NAME: not declared yet]\` until it is filled.`,
445
437
  '',
446
438
  ...[...vocabulary].sort().map((name) => `- [ ] \`${name}\``),
447
439
  ...(defaulted.length > 0
@@ -608,7 +600,7 @@ export async function init(argv, ctx) {
608
600
  : ` composed: ${composed.written.length} file(s), with holes where .nina/TODO.md §1–§3 go`,
609
601
  );
610
602
  }
611
- const uninstalled = unwired.some((w) => w.startsWith('@xhulz/nina is not installed'));
603
+ const uninstalled = lacksPackage(unwired);
612
604
  if (uninstalled) {
613
605
  console.log(' first: install @xhulz/nina here — until then no hook can load, and nothing tells the model anything');
614
606
  }
@@ -27,7 +27,7 @@ import { exportsDir, slugFor } from '../paths.mjs';
27
27
  import { projectName } from './stats.mjs';
28
28
 
29
29
  /** Where the keys and the switches live. */
30
- export const configPath = () => join(exportsDir(), 'langfuse.json');
30
+ const configPath = () => join(exportsDir(), 'langfuse.json');
31
31
 
32
32
  /** What a project sends, in words, by `contextMode`. */
33
33
  const LABELS = { full: "with each stage's own context", prompts: "with each stage's prompt and report", none: 'as metadata only' };
@@ -25,10 +25,10 @@
25
25
  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
- import { HARNESS, legacyHint, slugFor, snapshotsDir } from '../paths.mjs';
28
+ import { HARNESS, readProfile, slugFor, snapshotsDir } from '../paths.mjs';
29
29
  import { localDay, projectRecords } from '../store.mjs';
30
30
  import { isLoopBack, runOf } from '../transcripts.mjs';
31
- import { REQUIRES, byVersion, layerRootFor } from './compose.mjs';
31
+ import { byVersion, closedGate, layerRootFor } from './compose.mjs';
32
32
  import { GRADUATION_AT, RETIRED, frontmatter, graduationTarget, list, pillFiles, roleGates, tidyRetired } from './pills.mjs';
33
33
  import { decodeProjectDir } from './stats.mjs';
34
34
  import { snapshot } from './snapshot.mjs';
@@ -39,7 +39,7 @@ import { autoExport } from './export.mjs';
39
39
  import { beforeModel } from '../detectors.mjs';
40
40
 
41
41
  /** Loop-backs from one role, since its newest lesson, that make a lesson overdue. */
42
- export const CAPTURE_AT = 3;
42
+ const CAPTURE_AT = 3;
43
43
 
44
44
  /**
45
45
  * How far back a loop-back still counts. Older ones are history nobody remembers well enough to
@@ -226,7 +226,7 @@ const shift = (date, days) => new Date(Date.parse(`${date}T12:00:00Z`) + days *
226
226
  * @param {number} n - Out of how many.
227
227
  * @returns {[number, number]}
228
228
  */
229
- export function wilson(k, n) {
229
+ function wilson(k, n) {
230
230
  if (n === 0) return [0, 1];
231
231
  const z = 1.96;
232
232
  const p = k / n;
@@ -349,7 +349,7 @@ export function relapsed(known, filed) {
349
349
  * @param {{pill: string}[]} filed - Requests already written, open or closed.
350
350
  * @returns {Awaited<ReturnType<typeof lessons>>}
351
351
  */
352
- export function unfiled(known, filed) {
352
+ function unfiled(known, filed) {
353
353
  const asked = new Set(filed.map((r) => r.pill));
354
354
  return known.filter((l) => !l.retired && l.occurrences >= GRADUATION_AT && !asked.has(l.rel));
355
355
  }
@@ -473,8 +473,7 @@ async function missingSurface(layerRoot, layerPath, surfaces) {
473
473
  const own = /^surfaces\/([^/]+)\/tree\//.exec(layerPath)?.[1];
474
474
  if (own && !surfaces.includes(own)) return own;
475
475
  const core = await readFile(join(layerRoot, 'core', 'tree', composedPath(layerPath)), 'utf8').catch(() => '');
476
- const gate = REQUIRES.exec(core)?.[1] ?? null;
477
- return gate && !surfaces.includes(gate) ? gate : null;
476
+ return closedGate(core, surfaces);
478
477
  }
479
478
 
480
479
  /**
@@ -494,7 +493,7 @@ async function missingSurface(layerRoot, layerPath, surfaces) {
494
493
  */
495
494
  export async function closeAnswered(target, layerRoot, version) {
496
495
  const given = await answers(layerRoot);
497
- const profile = JSON.parse(await readFile(join(target, HARNESS, 'profile.json'), 'utf8').catch(() => '{}'));
496
+ const profile = readProfile(target).profile ?? {};
498
497
  const out = [];
499
498
  for (const r of (await filedRequests(target)).filter((r) => r.status === 'open' && given[r.id])) {
500
499
  const a = given[r.id];
@@ -685,12 +684,11 @@ export async function learn(argv, ctx) {
685
684
  const check = argv.includes('--check');
686
685
  const days = argv.includes('--days') ? Number(argv[argv.indexOf('--days') + 1]) || CAPTURE_DAYS : CAPTURE_DAYS;
687
686
 
688
- const profilePath = join(target, HARNESS, 'profile.json');
689
- if (!existsSync(profilePath)) {
690
- console.error(` no ${HARNESS}/profile.json under ${target}${legacyHint(target)}\n`);
687
+ const { profile, error } = readProfile(target);
688
+ if (error) {
689
+ console.error(` ${error}\n`);
691
690
  return check ? 2 : 1;
692
691
  }
693
- const profile = JSON.parse(await readFile(profilePath, 'utf8'));
694
692
  const resolved = layerRootFor(ctx.root, profile.core);
695
693
  if (resolved.error) {
696
694
  console.error(` ${resolved.error}\n`);
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { mkdir, readFile, readdir, rename } from 'node:fs/promises';
14
- import { HARNESS } from '../paths.mjs';
14
+ import { HARNESS, readProfile } from '../paths.mjs';
15
15
  import { existsSync, readFileSync, statSync } from 'node:fs';
16
16
  import { spawnSync } from 'node:child_process';
17
17
  import { dirname, join, resolve } from 'node:path';
@@ -124,7 +124,7 @@ export function list(value) {
124
124
  * @param {string} citation - `path` or `path:line`.
125
125
  * @returns {{state: 'ok'|'gone'|'truncated', path: string, line: number|null, lines?: number}}
126
126
  */
127
- export function resolveCitation(target, citation) {
127
+ function resolveCitation(target, citation) {
128
128
  const at = /^(.*):(\d+)$/.exec(citation);
129
129
  const path = at ? at[1] : citation;
130
130
  const line = at ? Number(at[2]) : null;
@@ -153,7 +153,7 @@ export function resolveCitation(target, citation) {
153
153
  * @param {string} path - A path inside it.
154
154
  * @returns {string | null} `YYYY-MM-DD`, or `null` when git cannot say.
155
155
  */
156
- export function lastChanged(target, path) {
156
+ function lastChanged(target, path) {
157
157
  const git = spawnSync('git', ['-C', target, 'log', '-1', '--format=%cs', '--', path], { encoding: 'utf8' });
158
158
  if (git.status !== 0) return null;
159
159
  const when = git.stdout.trim();
@@ -383,8 +383,8 @@ export async function pills(argv, ctx) {
383
383
  // of a surface, and composition strips that gate — so it has to come from the layers.
384
384
  let gates = null;
385
385
  try {
386
- const profile = JSON.parse(await readFile(join(target, HARNESS, 'profile.json'), 'utf8'));
387
- const layers = layerRootFor(ctx?.root ?? '.', profile.core);
386
+ const { profile } = readProfile(target);
387
+ const layers = layerRootFor(ctx?.root ?? '.', profile?.core);
388
388
  if (!layers.error) gates = await roleGates(layers.dir);
389
389
  } catch {
390
390
  /* without a profile the candidate is still worth naming; only its target is unknown */