@xhulz/nina 0.50.3 → 0.52.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 (273) hide show
  1. package/README.md +11 -5
  2. package/bin/nina.mjs +1 -0
  3. package/package.json +1 -1
  4. package/releases/0.51.0/core/GAPS.md +125 -0
  5. package/releases/0.51.0/core/answered.json +68 -0
  6. package/releases/0.51.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  7. package/releases/0.51.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  8. package/releases/0.51.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  9. package/releases/0.51.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  10. package/releases/0.51.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  11. package/releases/0.51.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  12. package/releases/0.51.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  13. package/releases/0.51.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  14. package/releases/0.51.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  15. package/releases/0.51.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  16. package/releases/0.51.0/core/tree/.claude/agents/architect.md +129 -0
  17. package/releases/0.51.0/core/tree/.claude/agents/dba.md +117 -0
  18. package/releases/0.51.0/core/tree/.claude/agents/devops.md +118 -0
  19. package/releases/0.51.0/core/tree/.claude/agents/implementer.md +163 -0
  20. package/releases/0.51.0/core/tree/.claude/agents/integration-tester.md +168 -0
  21. package/releases/0.51.0/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.51.0/core/tree/.claude/agents/qa.md +150 -0
  23. package/releases/0.51.0/core/tree/.claude/agents/reviewer.md +184 -0
  24. package/releases/0.51.0/core/tree/.claude/agents/secops.md +120 -0
  25. package/releases/0.51.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  26. package/releases/0.51.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  27. package/releases/0.51.0/core/tree/.claude/agents-overview.md +64 -0
  28. package/releases/0.51.0/core/tree/.claude/frontend.md +10 -0
  29. package/releases/0.51.0/core/tree/.claude/graph.md +75 -0
  30. package/releases/0.51.0/core/tree/.claude/patterns.md +251 -0
  31. package/releases/0.51.0/core/tree/.claude/pills/README.md +150 -0
  32. package/releases/0.51.0/core/tree/.claude/pipeline.md +101 -0
  33. package/releases/0.51.0/core/tree/.claude/retrieval.md +103 -0
  34. package/releases/0.51.0/core/tree/.claude/router.md +202 -0
  35. package/releases/0.51.0/core/tree/.claude/templates/integration.md +46 -0
  36. package/releases/0.51.0/core/tree/CLAUDE.md +164 -0
  37. package/releases/0.51.0/core/tree/scripts/cost-watch.mjs +25 -0
  38. package/releases/0.51.0/core/tree/scripts/edit-guard.mjs +24 -0
  39. package/releases/0.51.0/core/tree/scripts/harness-check.mjs +71 -0
  40. package/releases/0.51.0/core/tree/scripts/loop-gate.mjs +19 -0
  41. package/releases/0.51.0/core/vocabulary.json +13 -0
  42. package/releases/0.51.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  43. package/releases/0.51.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  44. package/releases/0.51.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  45. package/releases/0.51.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  46. package/releases/0.51.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  47. package/releases/0.51.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  48. package/releases/0.51.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  49. package/releases/0.51.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  50. package/releases/0.51.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  51. package/releases/0.51.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  52. package/releases/0.51.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  53. package/releases/0.51.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.51.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  55. package/releases/0.51.0/surfaces/d1/tree/CLAUDE.md +2 -0
  56. package/releases/0.51.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  57. package/releases/0.51.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  58. package/releases/0.51.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  59. package/releases/0.51.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  60. package/releases/0.51.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  61. package/releases/0.51.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  62. package/releases/0.51.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  63. package/releases/0.51.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  64. package/releases/0.51.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  65. package/releases/0.51.0/surfaces/db/tree/.claude/graph.md +8 -0
  66. package/releases/0.51.0/surfaces/db/tree/.claude/patterns.md +22 -0
  67. package/releases/0.51.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  68. package/releases/0.51.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  69. package/releases/0.51.0/surfaces/db/tree/.claude/router.md +4 -0
  70. package/releases/0.51.0/surfaces/db/tree/CLAUDE.md +17 -0
  71. package/releases/0.51.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  72. package/releases/0.51.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  73. package/releases/0.51.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  74. package/releases/0.51.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  75. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  76. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  77. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  78. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  79. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  80. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  81. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  82. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  83. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  84. package/releases/0.51.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  85. package/releases/0.51.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  86. package/releases/0.51.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  87. package/releases/0.51.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  88. package/releases/0.51.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  89. package/releases/0.51.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  90. package/releases/0.51.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  91. package/releases/0.51.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  92. package/releases/0.51.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  93. package/releases/0.51.0/surfaces/frontend/tree/.claude/router.md +14 -0
  94. package/releases/0.51.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  95. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  96. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  97. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  98. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  99. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  100. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  101. package/releases/0.51.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  102. package/releases/0.51.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  103. package/releases/0.51.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  104. package/releases/0.51.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  105. package/releases/0.51.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  106. package/releases/0.51.0/surfaces/integrations/tree/.claude/router.md +6 -0
  107. package/releases/0.51.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  108. package/releases/0.51.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  109. package/releases/0.51.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  110. package/releases/0.51.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  111. package/releases/0.51.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  112. package/releases/0.51.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  113. package/releases/0.51.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  114. package/releases/0.51.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  115. package/releases/0.51.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  116. package/releases/0.51.0/surfaces/money/tree/.claude/graph.md +2 -0
  117. package/releases/0.51.0/surfaces/money/tree/.claude/patterns.md +49 -0
  118. package/releases/0.51.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  119. package/releases/0.51.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  120. package/releases/0.51.0/surfaces/money/tree/CLAUDE.md +11 -0
  121. package/releases/0.51.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  122. package/releases/0.51.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  123. package/releases/0.51.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  124. package/releases/0.51.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  125. package/releases/0.51.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  126. package/releases/0.51.0/surfaces/pii/tree/CLAUDE.md +5 -0
  127. package/releases/0.51.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  128. package/releases/0.51.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  129. package/releases/0.51.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  130. package/releases/0.51.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  131. package/releases/0.51.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  132. package/releases/0.51.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  133. package/releases/0.51.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  134. package/releases/0.51.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  135. package/releases/0.52.0/core/GAPS.md +125 -0
  136. package/releases/0.52.0/core/answered.json +68 -0
  137. package/releases/0.52.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  138. package/releases/0.52.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  139. package/releases/0.52.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  140. package/releases/0.52.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  141. package/releases/0.52.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  142. package/releases/0.52.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  143. package/releases/0.52.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  144. package/releases/0.52.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  145. package/releases/0.52.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  146. package/releases/0.52.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  147. package/releases/0.52.0/core/tree/.claude/agents/architect.md +129 -0
  148. package/releases/0.52.0/core/tree/.claude/agents/dba.md +117 -0
  149. package/releases/0.52.0/core/tree/.claude/agents/devops.md +118 -0
  150. package/releases/0.52.0/core/tree/.claude/agents/implementer.md +163 -0
  151. package/releases/0.52.0/core/tree/.claude/agents/integration-tester.md +168 -0
  152. package/releases/0.52.0/core/tree/.claude/agents/planner.md +132 -0
  153. package/releases/0.52.0/core/tree/.claude/agents/qa.md +150 -0
  154. package/releases/0.52.0/core/tree/.claude/agents/reviewer.md +184 -0
  155. package/releases/0.52.0/core/tree/.claude/agents/secops.md +120 -0
  156. package/releases/0.52.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  157. package/releases/0.52.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  158. package/releases/0.52.0/core/tree/.claude/agents-overview.md +64 -0
  159. package/releases/0.52.0/core/tree/.claude/frontend.md +10 -0
  160. package/releases/0.52.0/core/tree/.claude/graph.md +75 -0
  161. package/releases/0.52.0/core/tree/.claude/patterns.md +251 -0
  162. package/releases/0.52.0/core/tree/.claude/pills/README.md +150 -0
  163. package/releases/0.52.0/core/tree/.claude/pipeline.md +101 -0
  164. package/releases/0.52.0/core/tree/.claude/retrieval.md +103 -0
  165. package/releases/0.52.0/core/tree/.claude/router.md +202 -0
  166. package/releases/0.52.0/core/tree/.claude/templates/integration.md +46 -0
  167. package/releases/0.52.0/core/tree/CLAUDE.md +164 -0
  168. package/releases/0.52.0/core/tree/scripts/cost-watch.mjs +25 -0
  169. package/releases/0.52.0/core/tree/scripts/edit-guard.mjs +24 -0
  170. package/releases/0.52.0/core/tree/scripts/harness-check.mjs +71 -0
  171. package/releases/0.52.0/core/tree/scripts/loop-gate.mjs +19 -0
  172. package/releases/0.52.0/core/vocabulary.json +13 -0
  173. package/releases/0.52.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  174. package/releases/0.52.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  175. package/releases/0.52.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  176. package/releases/0.52.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  177. package/releases/0.52.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  178. package/releases/0.52.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  179. package/releases/0.52.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  180. package/releases/0.52.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  181. package/releases/0.52.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  182. package/releases/0.52.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  183. package/releases/0.52.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  184. package/releases/0.52.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  185. package/releases/0.52.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  186. package/releases/0.52.0/surfaces/d1/tree/CLAUDE.md +2 -0
  187. package/releases/0.52.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  188. package/releases/0.52.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  189. package/releases/0.52.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  190. package/releases/0.52.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  191. package/releases/0.52.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  192. package/releases/0.52.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  193. package/releases/0.52.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  194. package/releases/0.52.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  195. package/releases/0.52.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  196. package/releases/0.52.0/surfaces/db/tree/.claude/graph.md +8 -0
  197. package/releases/0.52.0/surfaces/db/tree/.claude/patterns.md +22 -0
  198. package/releases/0.52.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  199. package/releases/0.52.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  200. package/releases/0.52.0/surfaces/db/tree/.claude/router.md +4 -0
  201. package/releases/0.52.0/surfaces/db/tree/CLAUDE.md +17 -0
  202. package/releases/0.52.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  203. package/releases/0.52.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  204. package/releases/0.52.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  205. package/releases/0.52.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  206. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  207. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  208. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  209. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  210. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  211. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  212. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  213. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  214. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  215. package/releases/0.52.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  216. package/releases/0.52.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  217. package/releases/0.52.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  218. package/releases/0.52.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  219. package/releases/0.52.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  220. package/releases/0.52.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  221. package/releases/0.52.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  222. package/releases/0.52.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  223. package/releases/0.52.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  224. package/releases/0.52.0/surfaces/frontend/tree/.claude/router.md +14 -0
  225. package/releases/0.52.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  226. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  227. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  228. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  229. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  230. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  231. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  232. package/releases/0.52.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  233. package/releases/0.52.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  234. package/releases/0.52.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  235. package/releases/0.52.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  236. package/releases/0.52.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  237. package/releases/0.52.0/surfaces/integrations/tree/.claude/router.md +6 -0
  238. package/releases/0.52.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  239. package/releases/0.52.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  240. package/releases/0.52.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  241. package/releases/0.52.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  242. package/releases/0.52.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  243. package/releases/0.52.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  244. package/releases/0.52.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  245. package/releases/0.52.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  246. package/releases/0.52.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  247. package/releases/0.52.0/surfaces/money/tree/.claude/graph.md +2 -0
  248. package/releases/0.52.0/surfaces/money/tree/.claude/patterns.md +49 -0
  249. package/releases/0.52.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  250. package/releases/0.52.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  251. package/releases/0.52.0/surfaces/money/tree/CLAUDE.md +11 -0
  252. package/releases/0.52.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  253. package/releases/0.52.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  254. package/releases/0.52.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  255. package/releases/0.52.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  256. package/releases/0.52.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  257. package/releases/0.52.0/surfaces/pii/tree/CLAUDE.md +5 -0
  258. package/releases/0.52.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  259. package/releases/0.52.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  260. package/releases/0.52.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  261. package/releases/0.52.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  262. package/releases/0.52.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  263. package/releases/0.52.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  264. package/releases/0.52.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  265. package/releases/0.52.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  266. package/src/commands/learn.mjs +302 -48
  267. package/src/commands/pills.mjs +11 -8
  268. package/src/commands/stats.mjs +28 -8
  269. package/src/commands/upgrade.mjs +1 -1
  270. package/src/gate.mjs +14 -2
  271. package/src/office.html +142 -4
  272. package/src/office.mjs +144 -21
  273. package/src/store.mjs +13 -1
package/src/gate.mjs CHANGED
@@ -628,7 +628,7 @@ function onPost(input, project, path, at) {
628
628
  const resumed = tool === 'SendMessage' ? Boolean(response.resumedAgentId) : undefined;
629
629
  // The name an agent was launched with, if any: a later SendMessage may reach it by that name alone.
630
630
  const name = tool !== 'SendMessage' && typeof input.tool_input?.name === 'string' ? input.tool_input.name.slice(0, 80) : null;
631
- append(path, { k: 'dispatch', at, role, agent, via: tool, id: input.tool_use_id ?? null, ...(name ? { name } : {}), ...(resumed === undefined ? {} : { resumed }) });
631
+ append(path, { k: 'dispatch', at, role, agent, via: tool, id: input.tool_use_id ?? null, ...describedAs(input), ...(name ? { name } : {}), ...(resumed === undefined ? {} : { resumed }) });
632
632
  return null;
633
633
  }
634
634
 
@@ -663,6 +663,18 @@ function confirmation(r) {
663
663
  };
664
664
  }
665
665
 
666
+ /**
667
+ * The few words the orchestrator gave a dispatch — the `description` the measurement store keeps as `desc` — which
668
+ * the office names a piece of work by. A message to an agent carries none: it adds to work already named.
669
+ *
670
+ * @param {object} input - The hook's input.
671
+ * @returns {{desc?: string}}
672
+ */
673
+ const describedAs = (input) => {
674
+ const desc = input.tool_name === 'SendMessage' ? null : input.tool_input?.description;
675
+ return typeof desc === 'string' && desc.trim() ? { desc: desc.trim().slice(0, 200) } : {};
676
+ };
677
+
666
678
  /** A dispatch is about to go out: note when, and send the round past its edge's cap to the owner to confirm. */
667
679
  function onPre(input, project, path, at) {
668
680
  // The orchestrator asking the owner, as it asks: when it was, and nothing it said. Nothing counts it; the office
@@ -681,7 +693,7 @@ function onPre(input, project, path, at) {
681
693
  // names its agent when the ledger already knows it — never a name the model wrote that nothing launched — so the
682
694
  // office can tell a message to a running agent from a resume before the `dispatch` entry is written.
683
695
  const known = to?.agent && entries.some((e) => e.k === 'dispatch' && e.agent === to.agent);
684
- append(path, { k: 'pre', at, role, id: input.tool_use_id ?? null, ...(to ? { via: 'SendMessage', ...(known ? { agent: to.agent } : {}) } : {}) });
696
+ append(path, { k: 'pre', at, role, id: input.tool_use_id ?? null, ...describedAs(input), ...(to ? { via: 'SendMessage', ...(known ? { agent: to.agent } : {}) } : {}) });
685
697
  const over = roundsFor(entries, role, project.loops, project.forward)
686
698
  .filter((r) => excess(r) > 0)
687
699
  .sort((a, b) => excess(b) - excess(a));
package/src/office.html CHANGED
@@ -14,15 +14,41 @@
14
14
  #zoom button { font: 600 14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; min-width: 34px; height: 30px; padding: 0 9px; color: #fff8e8; background: rgba(34, 32, 43, 0.88); border: 2px solid #262331; border-radius: 4px; cursor: pointer; }
15
15
  #zoom button:hover { background: #3a3f58; }
16
16
  #zoom button[aria-pressed="true"] { color: #ffd34d; }
17
+ #tasks { position: fixed; top: 0; right: 0; bottom: 0; width: 300px; box-sizing: border-box; padding: 16px 16px 24px 18px; overflow-y: auto; color: #e6e8ee; background: #22202b; border-left: 2px solid #262331; font: 13px/1.35 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
18
+ #tasks.overlay { top: 50px; width: min(300px, 88vw); border-top: 2px solid #262331; box-shadow: -6px 6px 18px rgba(0, 0, 0, 0.4); }
19
+ #tasks[hidden] { display: none; }
20
+ #tasks h2 { margin: 0; font: 600 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; letter-spacing: 0.08em; text-transform: uppercase; color: #ffd34d; }
21
+ #tasks .sub { margin: 2px 0 16px; color: #8a8fa8; font-size: 12px; }
22
+ #tasks .empty { color: #8a8fa8; margin: 0; }
23
+ #tasks ol { list-style: none; margin: 0; padding: 0 0 0 14px; border-left: 2px solid #3a3f58; }
24
+ #tasks li { position: relative; margin: 0 0 16px; padding-left: 8px; }
25
+ #tasks li::before { content: ''; position: absolute; left: -21px; top: 3px; width: 10px; height: 10px; border-radius: 50%; background: #6b7089; border: 2px solid #22202b; }
26
+ #tasks li.running::before { background: #ffd34d; animation: beat 1.2s ease-in-out infinite; }
27
+ #tasks li.idle::before { background: #c9a227; }
28
+ #tasks li.done::before { background: #3ec74d; }
29
+ #tasks .name { font-weight: 600; color: #fff8e8; overflow-wrap: anywhere; }
30
+ #tasks .when { margin-top: 3px; color: #a9adbf; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
31
+ #tasks .who { margin-top: 3px; color: #c9cde0; font-size: 12px; overflow-wrap: anywhere; }
32
+ #tasks .now { margin-top: 2px; color: #ffd34d; font-size: 12px; }
33
+ #tasks .now.quiet { color: #8a8fa8; }
34
+ @keyframes beat { 50% { opacity: 0.35; } }
35
+ @media (prefers-reduced-motion: reduce) { #tasks li.running::before { animation: none; } }
17
36
  </style>
18
37
  </head>
19
38
  <body>
20
39
  <canvas id="screen"></canvas>
21
40
  <div id="zoom" role="toolbar" aria-label="zoom">
41
+ <button id="tasks-toggle" type="button" title="the session's tasks (t)" aria-controls="tasks">tasks</button>
22
42
  <button id="zoom-out" type="button" title="zoom out (−)" aria-label="zoom out">−</button>
23
43
  <button id="zoom-fit" type="button" title="the whole office (0)">fit</button>
24
44
  <button id="zoom-in" type="button" title="zoom in (+)" aria-label="zoom in">+</button>
25
45
  </div>
46
+ <aside id="tasks" aria-label="the session's tasks" hidden>
47
+ <h2>Tasks</h2>
48
+ <p class="sub">this session, newest first</p>
49
+ <p id="task-empty" class="empty">Nothing yet: the first dispatch starts a task.</p>
50
+ <ol id="task-list"></ol>
51
+ </aside>
26
52
  <script>
27
53
  'use strict';
28
54
  /*
@@ -525,6 +551,7 @@ function settleCap() {
525
551
  }
526
552
 
527
553
  function onBeat(b) {
554
+ if (b.type === 'work' || b.type === 'verdict') lastActivity = Math.max(lastActivity, b.at);
528
555
  const stage = b.role ? stages.get(b.role) : null;
529
556
  if (b.type === 'work') {
530
557
  if (!stage) return;
@@ -687,6 +714,9 @@ function reset() {
687
714
  place(p, p.home);
688
715
  }
689
716
  lines.length = 0;
717
+ tasks = [];
718
+ lastActivity = 0;
719
+ drawTasks();
690
720
  confetti = [];
691
721
  banner = null;
692
722
  capRole = null;
@@ -704,6 +734,10 @@ events.addEventListener('hello', (m) => {
704
734
  status = hello.session ? (hello.replay ? 'replay' : 'live') : 'waiting for the first dispatch';
705
735
  });
706
736
  events.addEventListener('beat', (m) => onBeat(JSON.parse(m.data)));
737
+ events.addEventListener('timeline', (m) => {
738
+ tasks = JSON.parse(m.data).tasks ?? [];
739
+ drawTasks();
740
+ });
707
741
  events.addEventListener('end', () => (status = 'replay finished — reload to watch it again'));
708
742
  events.onerror = () => {
709
743
  if (!status.startsWith('replay finished')) status = 'reconnecting…';
@@ -808,11 +842,114 @@ function syncButtons() {
808
842
  document.getElementById('zoom-fit').setAttribute('aria-pressed', String(level() <= (fit / dpr) * 1.001));
809
843
  }
810
844
 
845
+ // ─── the timeline beside the room ─────────────────────────────────────────────────────────────
846
+ /*
847
+ * The session's work, task by task, as the server cuts it — the way `nina runs` does: each named by the words the
848
+ * orchestrator gave its first dispatch, with the stages that worked on it, from when that went out to the verdict
849
+ * that closed it. The server sends the whole list whenever it changes, and it replaces what is here. A task still
850
+ * open counts up while anyone is at work on it, and stops at the last thing that happened while nobody is.
851
+ */
852
+ const SIDE = 300;
853
+ /** Narrower than this, the timeline is drawn over the room, and starts hidden. */
854
+ const NARROW = 760;
855
+ const TASKS_KEY = 'nina-office-tasks';
856
+ let tasks = [];
857
+ /** The newest dispatch or verdict: a replay's clock, and where an idle task's count stops. */
858
+ let lastActivity = 0;
859
+ let tasksOpen = (() => {
860
+ try {
861
+ const kept = localStorage.getItem(TASKS_KEY);
862
+ if (kept !== null) return kept === 'open';
863
+ } catch {
864
+ // Kept for this page only.
865
+ }
866
+ return window.innerWidth >= NARROW;
867
+ })();
868
+
869
+ /** The stages someone is at work for now. */
870
+ const atWork = () => [...new Set([...people.values()].filter((p) => p.work && !p.reported && !p.leaving).map((p) => p.role))];
871
+
872
+ const two = (n) => String(n).padStart(2, '0');
873
+ /** A moment as the hour, with the day when it is not the day of `beside` — now, or the moment it follows. */
874
+ function stamp(at, beside) {
875
+ const t = new Date(at);
876
+ const hm = `${two(t.getHours())}:${two(t.getMinutes())}`;
877
+ return t.toDateString() === new Date(beside).toDateString() ? hm : `${t.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${hm}`;
878
+ }
879
+ /** A finished length of time: 2h05m, 41m, 50s. */
880
+ function span(ms) {
881
+ const s = Math.max(0, Math.round(ms / 1000));
882
+ if (s < 60) return `${s}s`;
883
+ const m = Math.round(s / 60);
884
+ return m >= 60 ? `${Math.floor(m / 60)}h${two(m % 60)}m` : `${m}m`;
885
+ }
886
+ /** A running count: 1:27:04, 27:04. */
887
+ function counter(ms) {
888
+ const s = Math.max(0, Math.floor(ms / 1000));
889
+ const h = Math.floor(s / 3600);
890
+ return h > 0 ? `${h}:${two(Math.floor((s % 3600) / 60))}:${two(s % 60)}` : `${Math.floor(s / 60)}:${two(s % 60)}`;
891
+ }
892
+
893
+ function drawTasks() {
894
+ if (!tasksOpen) return;
895
+ const clock = status.startsWith('replay') ? lastActivity : Date.now();
896
+ const working = atWork();
897
+ const line = (cls, text) => {
898
+ const el = document.createElement('div');
899
+ el.className = cls;
900
+ el.textContent = text;
901
+ return el;
902
+ };
903
+ const shown = [...tasks].sort((a, b) => b.start - a.start);
904
+ document.getElementById('task-list').replaceChildren(
905
+ ...shown.map((t) => {
906
+ const li = document.createElement('li');
907
+ const running = t.end === null;
908
+ const idle = running && working.length === 0;
909
+ li.className = running ? (idle ? 'idle' : 'running') : t.how === 'closed' ? 'done' : 'ended';
910
+ li.append(line('name', t.name || '(no description)'));
911
+ if (t.who?.length > 0) li.append(line('who', t.who.map(([role, n]) => (n > 1 ? `${role} ×${n}` : role)).join(' · ')));
912
+ if (running) {
913
+ const upTo = idle ? Math.max(t.start, lastActivity) : Math.max(t.start, clock);
914
+ li.append(line('when', `${stamp(t.start, clock)} → now · ${counter(upTo - t.start)}`));
915
+ li.append(line('now', idle ? `idle since ${stamp(lastActivity, clock)}` : `at work: ${working.join(', ')}`));
916
+ } else {
917
+ li.append(line('when', `${stamp(t.start, clock)} → ${stamp(t.end, t.start)} · ${span(t.end - t.start)}`));
918
+ // A task that waited — overnight, on the owner — says how much of it anyone was at work.
919
+ if (t.busy !== null && t.busy < (t.end - t.start) * 0.8) li.append(line('now quiet', `${span(t.busy)} at work`));
920
+ }
921
+ return li;
922
+ }),
923
+ );
924
+ document.getElementById('task-empty').hidden = tasks.length > 0;
925
+ }
926
+ setInterval(drawTasks, 1000);
927
+
928
+ function showTasks(open) {
929
+ tasksOpen = open;
930
+ try {
931
+ localStorage.setItem(TASKS_KEY, open ? 'open' : 'closed');
932
+ } catch {
933
+ // Kept for this page only.
934
+ }
935
+ resize();
936
+ drawTasks();
937
+ }
938
+ document.getElementById('tasks-toggle').addEventListener('click', () => showTasks(!tasksOpen));
939
+
811
940
  function resize() {
812
941
  dpr = window.devicePixelRatio || 1;
813
- screen.width = Math.floor(window.innerWidth * dpr);
942
+ // The timeline takes its width from the room on a wide window, and is drawn over it on a narrow one.
943
+ const overlay = window.innerWidth < NARROW;
944
+ const side = tasksOpen && !overlay ? SIDE : 0;
945
+ const aside = document.getElementById('tasks');
946
+ aside.hidden = !tasksOpen;
947
+ aside.classList.toggle('overlay', overlay);
948
+ document.getElementById('zoom').style.right = `${side + 10}px`;
949
+ document.getElementById('tasks-toggle').setAttribute('aria-pressed', String(tasksOpen));
950
+ screen.width = Math.floor((window.innerWidth - side) * dpr);
814
951
  screen.height = Math.floor(window.innerHeight * dpr);
815
- screen.style.width = `${window.innerWidth}px`;
952
+ screen.style.width = `${window.innerWidth - side}px`;
816
953
  screen.style.height = `${window.innerHeight}px`;
817
954
  band = Math.round(BAND * dpr);
818
955
  fit = Math.min(screen.width / LW, (screen.height - band) / LH);
@@ -849,6 +986,7 @@ window.addEventListener('keydown', (e) => {
849
986
  if (e.key === '+' || e.key === '=') zoomBy(1.25);
850
987
  else if (e.key === '-' || e.key === '_') zoomBy(0.8);
851
988
  else if (e.key === '0') zoomBy(0);
989
+ else if (e.key === 't') showTasks(!tasksOpen);
852
990
  });
853
991
  let drag = null;
854
992
  screen.addEventListener('pointerdown', (e) => {
@@ -1059,13 +1197,13 @@ function drawScreen(now) {
1059
1197
  const size = ui(12);
1060
1198
  {
1061
1199
  // Whether anything is going on: between dispatches the room is still, and says so.
1062
- const working = [...new Set([...people.values()].filter((p) => p.work && !p.reported && !p.leaving).map((p) => p.role))];
1200
+ const working = atWork();
1063
1201
  const going = status === 'live' || status === 'replay' ? (working.length > 0 ? ` · at work: ${working.join(', ')}` : ' · waiting for the next dispatch') : '';
1064
1202
  const title = `NINA office · ${ROOM.project}${session ? ` · ${session.slice(0, 8)}` : ''} · ${status}${going}`;
1065
1203
  s.font = `${size}px ${MONO}`;
1066
1204
  s.fillStyle = '#1b1d26';
1067
1205
  s.fillRect(0, 0, screen.width, band);
1068
- const width = Math.min(s.measureText(title).width + ui(16), screen.width - ui(150));
1206
+ const width = Math.min(s.measureText(title).width + ui(16), screen.width - ui(230));
1069
1207
  panel(ui(10), ui(9), width, ui(30));
1070
1208
  text(title, ui(18), ui(24), size, '#e6e8ee', 'left', width - ui(16));
1071
1209
  }
package/src/office.mjs CHANGED
@@ -2,8 +2,10 @@
2
2
  * `nina pipeline --view` — the pipeline as an office seen from above, drawn in a browser page.
3
3
  *
4
4
  * Each stage of the project's composed graph has a desk, and each agent the loop gate saw launched is someone
5
- * sitting at one. The gate's ledger is the only thing read, and it holds metadata only, so the office shows who
6
- * worked, what they answered and the ids of the issues they named — never what anyone wrote:
5
+ * sitting at one. The gate's ledger is what is read, and it holds metadata only, so the office shows who worked,
6
+ * what they answered, the ids of the issues they named and the few words the orchestrator gave each dispatch —
7
+ * never what an agent wrote or reported. Beside the room, a timeline of the session's tasks (`officeTimeline`), cut
8
+ * and named as `nina runs` cuts and names them:
7
9
  *
8
10
  * a dispatch someone sits down to work, the folder carried over by whoever handed it on
9
11
  * a verdict a check over their head, or a cross when it sends work back
@@ -28,6 +30,8 @@ import { fileURLToPath } from 'node:url';
28
30
  import { TERMINALS, parseGraph } from './graph.mjs';
29
31
  import { readLedger, sessionLedgers } from './gate.mjs';
30
32
  import { readProfile } from './paths.mjs';
33
+ import { projectStoreFile, readStoreFile } from './store.mjs';
34
+ import { IN_FLIGHT, cyclesOf } from './commands/stats.mjs';
31
35
  import { isLoopBack } from './transcripts.mjs';
32
36
  import { shapeOf } from './commands/pipeline.mjs';
33
37
 
@@ -64,26 +68,38 @@ const REFUSED_AFTER_MS = 5000;
64
68
  /** How often a live office looks at the ledger. The gate appends to it as each fact happens. */
65
69
  const POLL_MS = 700;
66
70
 
71
+ /**
72
+ * How long anyone was at work on a piece of work: its runs' spans joined, so runs side by side count once and a
73
+ * night between two of them not at all — a piece begun one evening and closed the next morning was at work two hours.
74
+ *
75
+ * @param {{at: number, end: number|null}[]} starts - Its runs.
76
+ * @returns {number} Milliseconds.
77
+ */
78
+ export function busyFor(starts) {
79
+ const spans = starts.map((s) => [s.at, Math.max(s.at, s.end ?? s.at)]).sort((a, b) => a[0] - b[0]);
80
+ let total = 0;
81
+ let open = null;
82
+ for (const [from, to] of spans) {
83
+ if (open && from <= open[1]) open[1] = Math.max(open[1], to);
84
+ else {
85
+ if (open) total += open[1] - open[0];
86
+ open = [from, to];
87
+ }
88
+ }
89
+ return open ? total + open[1] - open[0] : total;
90
+ }
91
+
67
92
  /** The longest a replay waits between two beats, and the shortest: hours of a session play in minutes. */
68
93
  const REPLAY_MAX_GAP_MS = 3000;
69
94
  const REPLAY_MIN_GAP_MS = 500;
70
95
 
71
96
  /**
72
- * A session's ledger as the office's beats, in the order they happened.
97
+ * Each run the ledger saw start, in order: when, at which stage, by which agent once known, whether it was a message
98
+ * to a running agent, when it was refused, and when its verdict ended it. The beats and the timeline read the same.
73
99
  *
74
- * A foreground dispatch's `dispatch` entry is written when the agent has finished, after its own verdict, so work
75
- * starts where its `pre` entry stands, joined to the agent by the tool_use id; until that entry is written, the
76
- * agent is not known yet, and the page binds the next verdict from that desk to it. The work ends — a party — on a
77
- * pass when nothing since the last dispatch was sent back, something since then could route to `done`, nobody is
78
- * still at work, and no dispatch follows within `PARTY_QUIET_MS`; at once when the pass routes only to a terminal.
79
- * A party that waits on the quiet is only beaten once `now` has passed it.
80
- *
81
- * @param {object[]} entries - The ledger, oldest first.
82
- * @param {{edges: {from: string, to: string, token: string}[]}} graph - The project's parsed graph.
83
- * @param {number} [now] - The time a waiting party is judged against; `Infinity` for a session that has ended.
84
- * @returns {object[]} Beats, each `{id, at, type, ...}`; the id is stable as the ledger grows.
100
+ * @param {object[]} ledger - The ledger, oldest first.
85
101
  */
86
- export function officeBeats(ledger, graph, now = Date.now()) {
102
+ function readStarts(ledger) {
87
103
  const entries = ledger.filter((e) => Number.isFinite(Date.parse(e?.at)));
88
104
  const time = (e) => Date.parse(e.at);
89
105
  const dispatches = new Map();
@@ -113,11 +129,28 @@ export function officeBeats(ledger, graph, now = Date.now()) {
113
129
  const after = e.k === 'pre' && e.id && asked.has(e.id) && !dispatches.has(e.id) ? entries.slice(i + 1).find((x) => x.k === 'pre' && time(x) > at + REFUSED_AFTER_MS) : null;
114
130
  starts.push({ i, at, role: e.role, agent: launched?.agent ?? null, message, refusedAt: after ? time(after) : null });
115
131
  });
116
- const endOf = (s) => {
117
- const v = verdicts.find((x) => time(x) >= s.at && (s.agent ? x.agent === s.agent : x.role === s.role));
118
- return v ? time(v) : null;
119
- };
120
- for (const s of starts) s.end = s.message ? s.at : endOf(s);
132
+ const verdictOf = (s) => verdicts.find((x) => time(x) >= s.at && (s.agent ? x.agent === s.agent : x.role === s.role)) ?? null;
133
+ for (const s of starts) s.end = s.message ? s.at : verdictOf(s) ? time(verdictOf(s)) : null;
134
+ return { entries, time, startsWork, launchOf, starts, verdictOf };
135
+ }
136
+
137
+ /**
138
+ * A session's ledger as the office's beats, in the order they happened.
139
+ *
140
+ * A foreground dispatch's `dispatch` entry is written when the agent has finished, after its own verdict, so work
141
+ * starts where its `pre` entry stands, joined to the agent by the tool_use id; until that entry is written, the
142
+ * agent is not known yet, and the page binds the next verdict from that desk to it. The work ends — a party — on a
143
+ * pass when nothing since the last dispatch was sent back, something since then could route to `done`, nobody is
144
+ * still at work, and no dispatch follows within `PARTY_QUIET_MS`; at once when the pass routes only to a terminal.
145
+ * A party that waits on the quiet is only beaten once `now` has passed it.
146
+ *
147
+ * @param {object[]} entries - The ledger, oldest first.
148
+ * @param {{edges: {from: string, to: string, token: string}[]}} graph - The project's parsed graph.
149
+ * @param {number} [now] - The time a waiting party is judged against; `Infinity` for a session that has ended.
150
+ * @returns {object[]} Beats, each `{id, at, type, ...}`; the id is stable as the ledger grows.
151
+ */
152
+ export function officeBeats(ledger, graph, now = Date.now()) {
153
+ const { entries, time, startsWork, launchOf, starts } = readStarts(ledger);
121
154
  const busyAt = (moment) =>
122
155
  starts.some((s) => !s.message && s.at <= moment && moment - s.at < STALE_MS && !(s.refusedAt !== null && s.refusedAt <= moment) && !(s.end !== null && s.end <= moment));
123
156
 
@@ -187,6 +220,74 @@ export function officeBeats(ledger, graph, now = Date.now()) {
187
220
  return beats.sort((a, b) => a.at - b.at);
188
221
  }
189
222
 
223
+ /**
224
+ * The session's tasks for the timeline beside the room, oldest first: its runs cut the way `nina runs` cuts a session
225
+ * (`cyclesOf`), each named by the description its first dispatch was given — the ledger's, or, for a ledger from
226
+ * before the gate kept one, the measurement store's (`names`) — with who worked on it, when it started and ended, and
227
+ * how long anyone was at work on it.
228
+ *
229
+ * It is the whole list, cut again each time, and the page replaces what it has with it: the cut moves as the ledger
230
+ * grows — a run still at work lets nothing through until its verdict, a dispatch is known refused only seconds later
231
+ * — and sent piece by piece, a task that a later fact merged away stayed on the page for good.
232
+ *
233
+ * @param {object[]} ledger - The ledger, oldest first.
234
+ * @param {number} [now] - The moment it is read at: a run younger than `STALE_MS` with no verdict is still at work.
235
+ * @param {Map<string, string>} [names] - Descriptions by dispatch id, for entries that carry none.
236
+ * @returns {{key: string, name: string|null, start: number, end: number|null, how: string|null, busy: number|null, who: [string, number][]}[]}
237
+ */
238
+ export function officeTimeline(ledger, now = Date.now(), names = new Map()) {
239
+ const { entries, starts, verdictOf } = readStarts(ledger);
240
+ const runs = starts
241
+ .filter((s) => !s.message && s.refusedAt === null)
242
+ .map((s) => {
243
+ const e = entries[s.i];
244
+ const v = verdictOf(s);
245
+ return { ts: e.at, role: s.role, verdict: v ? v.verdict : now < s.at + STALE_MS ? IN_FLIGHT : null, desc: e.desc ?? names.get(e.id) ?? null, start: s, key: e.id ?? `at${s.i}` };
246
+ });
247
+ return cyclesOf(runs).map((cycle) => {
248
+ const first = cycle.runs[0];
249
+ const who = new Map();
250
+ for (const r of cycle.runs) who.set(r.role, (who.get(r.role) ?? 0) + 1);
251
+ const open = cycle.end === 'open';
252
+ return {
253
+ key: first.key,
254
+ name: first.desc ? String(first.desc) : null,
255
+ start: first.start.at,
256
+ end: open ? null : Math.max(...cycle.runs.map((r) => r.start.end ?? r.start.at)),
257
+ how: open ? null : cycle.end,
258
+ busy: open ? null : busyFor(cycle.runs.map((r) => r.start)),
259
+ who: [...who],
260
+ };
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Descriptions by dispatch id from a project's measurement store, for a ledger written before the gate kept them:
266
+ * read again only when the store has changed since — a dispatch the store never names would otherwise have it read
267
+ * whole every few seconds for as long as the office runs.
268
+ *
269
+ * @param {string} target - The project directory.
270
+ * @returns {(entries: object[]) => Map<string, string>}
271
+ */
272
+ export function storeNames(target) {
273
+ let seen = { file: null, mtime: -1, names: new Map() };
274
+ return (entries) => {
275
+ const missing = entries.some((e) => (e.k === 'pre' || e.k === 'dispatch') && e.via !== 'SendMessage' && e.id && !e.desc && !seen.names.has(e.id));
276
+ if (!missing) return seen.names;
277
+ try {
278
+ const file = projectStoreFile(target);
279
+ const mtime = file ? statSync(file).mtimeMs : -1;
280
+ if (file !== seen.file || mtime !== seen.mtime) {
281
+ const records = file ? readStoreFile(file) : [];
282
+ seen = { file, mtime, names: new Map(records.filter((r) => r.dispatch_id && r.desc).map((r) => [r.dispatch_id, String(r.desc)])) };
283
+ }
284
+ } catch {
285
+ // A store that cannot be read names nothing; the ledger's own words still do.
286
+ }
287
+ return seen.names;
288
+ };
289
+ }
290
+
190
291
  /**
191
292
  * What the page is told about the room: the project, its stages in the order the line takes them, the stages that
192
293
  * run as several agents at once, and what to call the owner.
@@ -222,6 +323,8 @@ export async function serveOffice({ target, wanted, replay = false, port = OFFIC
222
323
  const clients = new Set();
223
324
  const timers = new Set();
224
325
 
326
+ const names = storeNames(target);
327
+
225
328
  /** The session to show, and its entries; read again only when its ledger changed. */
226
329
  let cached = { path: null, size: -1, mtime: 0, entries: [] };
227
330
  const current = () => {
@@ -248,19 +351,34 @@ export async function serveOffice({ target, wanted, replay = false, port = OFFIC
248
351
  client.since = client.restoring ? Number.NEGATIVE_INFINITY : Date.now() - SWITCH_GRACE_MS;
249
352
  client.session = state.session;
250
353
  client.sent = new Set();
354
+ client.timeline = null;
251
355
  send(client.res, 'hello', { session: state.session, replay });
252
356
  };
253
357
 
358
+ // The timeline, whole, to a client whose copy differs from it.
359
+ const tell = (client, tasks) => {
360
+ const text = JSON.stringify(tasks);
361
+ if (client.timeline === text) return;
362
+ client.timeline = text;
363
+ send(client.res, 'timeline', { tasks });
364
+ };
365
+
254
366
  const tick = () => {
367
+ // Nobody watching live: nothing to cut or send.
368
+ if (![...clients].some((client) => !client.replay)) return;
255
369
  const state = current();
370
+ const now = Date.now();
371
+ const beats = officeBeats(state.entries, graph, now);
372
+ const tasks = officeTimeline(state.entries, now, names(state.entries));
256
373
  for (const client of clients) {
257
374
  if (client.replay) continue;
258
375
  if (client.session !== state.session) greet(client, state);
259
- for (const beat of officeBeats(state.entries, graph, Date.now())) {
376
+ for (const beat of beats) {
260
377
  if (client.sent.has(beat.id)) continue;
261
378
  client.sent.add(beat.id);
262
379
  send(client.res, 'beat', { ...beat, instant: client.restoring || beat.at < client.since });
263
380
  }
381
+ tell(client, tasks);
264
382
  client.restoring = false;
265
383
  }
266
384
  };
@@ -269,10 +387,14 @@ export async function serveOffice({ target, wanted, replay = false, port = OFFIC
269
387
  const state = current();
270
388
  greet(client, state);
271
389
  const beats = officeBeats(state.entries, graph, Number.POSITIVE_INFINITY);
390
+ // The timeline as it stood at each beat: cut from the ledger up to that moment, so a replay tells nothing early.
391
+ const known = names(state.entries);
392
+ const tasksAt = (moment) => officeTimeline(state.entries.filter((e) => Date.parse(e?.at) <= moment), moment, known);
272
393
  // What came before the first dispatch sets the room, at once: a session often opens on many of the owner's
273
394
  // messages, and played out they kept the office empty for a minute.
274
395
  let i = Math.max(0, beats.findIndex((b) => b.type === 'work'));
275
396
  for (const beat of beats.slice(0, i)) send(client.res, 'beat', { ...beat, instant: true });
397
+ if (i > 0) tell(client, tasksAt(beats[i - 1].at));
276
398
  const next = () => {
277
399
  if (!clients.has(client)) return;
278
400
  if (i >= beats.length) {
@@ -281,6 +403,7 @@ export async function serveOffice({ target, wanted, replay = false, port = OFFIC
281
403
  }
282
404
  const beat = beats[i];
283
405
  send(client.res, 'beat', beat);
406
+ tell(client, tasksAt(beat.at));
284
407
  i += 1;
285
408
  // The owner speaking moves nobody across the room: it passes quickly. An answer keeps its wait, or the question
286
409
  // it answers would not be seen.
package/src/store.mjs CHANGED
@@ -92,9 +92,21 @@ function storeNamesOf(dir) {
92
92
  * @returns {object[]|null} Null when the store has nothing for it.
93
93
  */
94
94
  export function projectRecords(dir, store = snapshotsDir()) {
95
+ const file = projectStoreFile(dir, store);
96
+ return file === null ? null : readStoreFile(file);
97
+ }
98
+
99
+ /**
100
+ * The file a project's records are kept in, or null when the store has none for it.
101
+ *
102
+ * @param {string} dir - The project.
103
+ * @param {string} [store] - The store directory.
104
+ * @returns {string|null}
105
+ */
106
+ export function projectStoreFile(dir, store = snapshotsDir()) {
95
107
  for (const name of storeNamesOf(dir)) {
96
108
  const file = join(store, `${name}.jsonl`);
97
- if (existsSync(file)) return readStoreFile(file);
109
+ if (existsSync(file)) return file;
98
110
  }
99
111
  return null;
100
112
  }