@xhulz/nina 0.28.17 → 0.28.19

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 (231) hide show
  1. package/README.md +1 -1
  2. package/bin/nina.mjs +2 -0
  3. package/package.json +1 -1
  4. package/releases/0.28.18/core/GAPS.md +125 -0
  5. package/releases/0.28.18/core/answered.json +29 -0
  6. package/releases/0.28.18/core/defaults/tree/.claude/agents/architect.md +1 -0
  7. package/releases/0.28.18/core/defaults/tree/.claude/agents/dba.md +3 -0
  8. package/releases/0.28.18/core/defaults/tree/.claude/agents/devops.md +1 -0
  9. package/releases/0.28.18/core/defaults/tree/.claude/agents/implementer.md +1 -0
  10. package/releases/0.28.18/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  11. package/releases/0.28.18/core/defaults/tree/.claude/agents/qa.md +2 -0
  12. package/releases/0.28.18/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  13. package/releases/0.28.18/core/defaults/tree/.claude/agents/secops.md +3 -0
  14. package/releases/0.28.18/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  15. package/releases/0.28.18/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  16. package/releases/0.28.18/core/tree/.claude/agents/architect.md +125 -0
  17. package/releases/0.28.18/core/tree/.claude/agents/dba.md +107 -0
  18. package/releases/0.28.18/core/tree/.claude/agents/devops.md +113 -0
  19. package/releases/0.28.18/core/tree/.claude/agents/implementer.md +155 -0
  20. package/releases/0.28.18/core/tree/.claude/agents/integration-tester.md +160 -0
  21. package/releases/0.28.18/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.28.18/core/tree/.claude/agents/qa.md +146 -0
  23. package/releases/0.28.18/core/tree/.claude/agents/reviewer.md +180 -0
  24. package/releases/0.28.18/core/tree/.claude/agents/secops.md +121 -0
  25. package/releases/0.28.18/core/tree/.claude/agents/solidity-auditor.md +126 -0
  26. package/releases/0.28.18/core/tree/.claude/agents/solidity-dev.md +123 -0
  27. package/releases/0.28.18/core/tree/.claude/agents-overview.md +64 -0
  28. package/releases/0.28.18/core/tree/.claude/graph.md +72 -0
  29. package/releases/0.28.18/core/tree/.claude/patterns.md +212 -0
  30. package/releases/0.28.18/core/tree/.claude/pills/README.md +145 -0
  31. package/releases/0.28.18/core/tree/.claude/pipeline.md +102 -0
  32. package/releases/0.28.18/core/tree/.claude/retrieval.md +104 -0
  33. package/releases/0.28.18/core/tree/.claude/router.md +201 -0
  34. package/releases/0.28.18/core/tree/.claude/templates/integration.md +45 -0
  35. package/releases/0.28.18/core/tree/CLAUDE.md +155 -0
  36. package/releases/0.28.18/core/tree/scripts/edit-guard.mjs +16 -0
  37. package/releases/0.28.18/core/tree/scripts/harness-check.mjs +71 -0
  38. package/releases/0.28.18/core/tree/scripts/loop-gate.mjs +19 -0
  39. package/releases/0.28.18/core/vocabulary.json +12 -0
  40. package/releases/0.28.18/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  41. package/releases/0.28.18/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  42. package/releases/0.28.18/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  43. package/releases/0.28.18/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  44. package/releases/0.28.18/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  45. package/releases/0.28.18/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  46. package/releases/0.28.18/surfaces/blockchain/tree/.claude/graph.md +11 -0
  47. package/releases/0.28.18/surfaces/blockchain/tree/.claude/router.md +9 -0
  48. package/releases/0.28.18/surfaces/blockchain/tree/CLAUDE.md +12 -0
  49. package/releases/0.28.18/surfaces/db/tree/.claude/agents/architect.md +8 -0
  50. package/releases/0.28.18/surfaces/db/tree/.claude/agents/dba.md +84 -0
  51. package/releases/0.28.18/surfaces/db/tree/.claude/agents/devops.md +14 -0
  52. package/releases/0.28.18/surfaces/db/tree/.claude/agents/implementer.md +5 -0
  53. package/releases/0.28.18/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
  54. package/releases/0.28.18/surfaces/db/tree/.claude/agents/planner.md +2 -0
  55. package/releases/0.28.18/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
  56. package/releases/0.28.18/surfaces/db/tree/.claude/agents/secops.md +2 -0
  57. package/releases/0.28.18/surfaces/db/tree/.claude/agents-overview.md +5 -0
  58. package/releases/0.28.18/surfaces/db/tree/.claude/graph.md +8 -0
  59. package/releases/0.28.18/surfaces/db/tree/.claude/patterns.md +29 -0
  60. package/releases/0.28.18/surfaces/db/tree/.claude/pipeline.md +13 -0
  61. package/releases/0.28.18/surfaces/db/tree/.claude/retrieval.md +5 -0
  62. package/releases/0.28.18/surfaces/db/tree/.claude/router.md +10 -0
  63. package/releases/0.28.18/surfaces/db/tree/CLAUDE.md +23 -0
  64. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  65. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  66. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
  67. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  68. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  69. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  70. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  71. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  72. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  73. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  74. package/releases/0.28.18/surfaces/edge-cf/tree/.claude/router.md +21 -0
  75. package/releases/0.28.18/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  76. package/releases/0.28.18/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  77. package/releases/0.28.18/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  78. package/releases/0.28.18/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  79. package/releases/0.28.18/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  80. package/releases/0.28.18/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
  81. package/releases/0.28.18/surfaces/frontend/tree/.claude/patterns.md +15 -0
  82. package/releases/0.28.18/surfaces/frontend/tree/.claude/retrieval.md +5 -0
  83. package/releases/0.28.18/surfaces/frontend/tree/.claude/router.md +17 -0
  84. package/releases/0.28.18/surfaces/frontend/tree/CLAUDE.md +5 -0
  85. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  86. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  87. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  88. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  89. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  90. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  91. package/releases/0.28.18/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  92. package/releases/0.28.18/surfaces/integrations/tree/.claude/graph.md +9 -0
  93. package/releases/0.28.18/surfaces/integrations/tree/.claude/patterns.md +38 -0
  94. package/releases/0.28.18/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  95. package/releases/0.28.18/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  96. package/releases/0.28.18/surfaces/integrations/tree/.claude/router.md +6 -0
  97. package/releases/0.28.18/surfaces/integrations/tree/CLAUDE.md +17 -0
  98. package/releases/0.28.18/surfaces/money/tree/.claude/agents/architect.md +12 -0
  99. package/releases/0.28.18/surfaces/money/tree/.claude/agents/dba.md +14 -0
  100. package/releases/0.28.18/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  101. package/releases/0.28.18/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  102. package/releases/0.28.18/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  103. package/releases/0.28.18/surfaces/money/tree/.claude/agents/secops.md +5 -0
  104. package/releases/0.28.18/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  105. package/releases/0.28.18/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  106. package/releases/0.28.18/surfaces/money/tree/.claude/graph.md +2 -0
  107. package/releases/0.28.18/surfaces/money/tree/.claude/patterns.md +49 -0
  108. package/releases/0.28.18/surfaces/money/tree/.claude/pipeline.md +2 -0
  109. package/releases/0.28.18/surfaces/money/tree/.claude/retrieval.md +2 -0
  110. package/releases/0.28.18/surfaces/money/tree/CLAUDE.md +11 -0
  111. package/releases/0.28.18/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  112. package/releases/0.28.18/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  113. package/releases/0.28.18/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  114. package/releases/0.28.18/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  115. package/releases/0.28.18/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  116. package/releases/0.28.18/surfaces/pii/tree/CLAUDE.md +5 -0
  117. package/releases/0.28.19/core/GAPS.md +125 -0
  118. package/releases/0.28.19/core/answered.json +29 -0
  119. package/releases/0.28.19/core/defaults/tree/.claude/agents/architect.md +1 -0
  120. package/releases/0.28.19/core/defaults/tree/.claude/agents/dba.md +3 -0
  121. package/releases/0.28.19/core/defaults/tree/.claude/agents/devops.md +1 -0
  122. package/releases/0.28.19/core/defaults/tree/.claude/agents/implementer.md +1 -0
  123. package/releases/0.28.19/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  124. package/releases/0.28.19/core/defaults/tree/.claude/agents/qa.md +2 -0
  125. package/releases/0.28.19/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  126. package/releases/0.28.19/core/defaults/tree/.claude/agents/secops.md +3 -0
  127. package/releases/0.28.19/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  128. package/releases/0.28.19/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  129. package/releases/0.28.19/core/tree/.claude/agents/architect.md +125 -0
  130. package/releases/0.28.19/core/tree/.claude/agents/dba.md +107 -0
  131. package/releases/0.28.19/core/tree/.claude/agents/devops.md +113 -0
  132. package/releases/0.28.19/core/tree/.claude/agents/implementer.md +155 -0
  133. package/releases/0.28.19/core/tree/.claude/agents/integration-tester.md +160 -0
  134. package/releases/0.28.19/core/tree/.claude/agents/planner.md +132 -0
  135. package/releases/0.28.19/core/tree/.claude/agents/qa.md +146 -0
  136. package/releases/0.28.19/core/tree/.claude/agents/reviewer.md +180 -0
  137. package/releases/0.28.19/core/tree/.claude/agents/secops.md +121 -0
  138. package/releases/0.28.19/core/tree/.claude/agents/solidity-auditor.md +126 -0
  139. package/releases/0.28.19/core/tree/.claude/agents/solidity-dev.md +123 -0
  140. package/releases/0.28.19/core/tree/.claude/agents-overview.md +64 -0
  141. package/releases/0.28.19/core/tree/.claude/graph.md +73 -0
  142. package/releases/0.28.19/core/tree/.claude/patterns.md +212 -0
  143. package/releases/0.28.19/core/tree/.claude/pills/README.md +145 -0
  144. package/releases/0.28.19/core/tree/.claude/pipeline.md +102 -0
  145. package/releases/0.28.19/core/tree/.claude/retrieval.md +104 -0
  146. package/releases/0.28.19/core/tree/.claude/router.md +201 -0
  147. package/releases/0.28.19/core/tree/.claude/templates/integration.md +45 -0
  148. package/releases/0.28.19/core/tree/CLAUDE.md +155 -0
  149. package/releases/0.28.19/core/tree/scripts/edit-guard.mjs +16 -0
  150. package/releases/0.28.19/core/tree/scripts/harness-check.mjs +71 -0
  151. package/releases/0.28.19/core/tree/scripts/loop-gate.mjs +19 -0
  152. package/releases/0.28.19/core/vocabulary.json +12 -0
  153. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  154. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  155. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  156. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  157. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  158. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  159. package/releases/0.28.19/surfaces/blockchain/tree/.claude/graph.md +11 -0
  160. package/releases/0.28.19/surfaces/blockchain/tree/.claude/router.md +9 -0
  161. package/releases/0.28.19/surfaces/blockchain/tree/CLAUDE.md +12 -0
  162. package/releases/0.28.19/surfaces/db/tree/.claude/agents/architect.md +8 -0
  163. package/releases/0.28.19/surfaces/db/tree/.claude/agents/dba.md +84 -0
  164. package/releases/0.28.19/surfaces/db/tree/.claude/agents/devops.md +14 -0
  165. package/releases/0.28.19/surfaces/db/tree/.claude/agents/implementer.md +5 -0
  166. package/releases/0.28.19/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
  167. package/releases/0.28.19/surfaces/db/tree/.claude/agents/planner.md +2 -0
  168. package/releases/0.28.19/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
  169. package/releases/0.28.19/surfaces/db/tree/.claude/agents/secops.md +2 -0
  170. package/releases/0.28.19/surfaces/db/tree/.claude/agents-overview.md +5 -0
  171. package/releases/0.28.19/surfaces/db/tree/.claude/graph.md +8 -0
  172. package/releases/0.28.19/surfaces/db/tree/.claude/patterns.md +29 -0
  173. package/releases/0.28.19/surfaces/db/tree/.claude/pipeline.md +13 -0
  174. package/releases/0.28.19/surfaces/db/tree/.claude/retrieval.md +5 -0
  175. package/releases/0.28.19/surfaces/db/tree/.claude/router.md +10 -0
  176. package/releases/0.28.19/surfaces/db/tree/CLAUDE.md +23 -0
  177. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  178. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  179. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
  180. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  181. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  182. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  183. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  184. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  185. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  186. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  187. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/router.md +21 -0
  188. package/releases/0.28.19/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  189. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  190. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  191. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  192. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  193. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
  194. package/releases/0.28.19/surfaces/frontend/tree/.claude/patterns.md +15 -0
  195. package/releases/0.28.19/surfaces/frontend/tree/.claude/retrieval.md +5 -0
  196. package/releases/0.28.19/surfaces/frontend/tree/.claude/router.md +17 -0
  197. package/releases/0.28.19/surfaces/frontend/tree/CLAUDE.md +5 -0
  198. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  199. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  200. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  201. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  202. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  203. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  204. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  205. package/releases/0.28.19/surfaces/integrations/tree/.claude/graph.md +9 -0
  206. package/releases/0.28.19/surfaces/integrations/tree/.claude/patterns.md +38 -0
  207. package/releases/0.28.19/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  208. package/releases/0.28.19/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  209. package/releases/0.28.19/surfaces/integrations/tree/.claude/router.md +6 -0
  210. package/releases/0.28.19/surfaces/integrations/tree/CLAUDE.md +17 -0
  211. package/releases/0.28.19/surfaces/money/tree/.claude/agents/architect.md +12 -0
  212. package/releases/0.28.19/surfaces/money/tree/.claude/agents/dba.md +14 -0
  213. package/releases/0.28.19/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  214. package/releases/0.28.19/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  215. package/releases/0.28.19/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  216. package/releases/0.28.19/surfaces/money/tree/.claude/agents/secops.md +5 -0
  217. package/releases/0.28.19/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  218. package/releases/0.28.19/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  219. package/releases/0.28.19/surfaces/money/tree/.claude/graph.md +2 -0
  220. package/releases/0.28.19/surfaces/money/tree/.claude/patterns.md +49 -0
  221. package/releases/0.28.19/surfaces/money/tree/.claude/pipeline.md +2 -0
  222. package/releases/0.28.19/surfaces/money/tree/.claude/retrieval.md +2 -0
  223. package/releases/0.28.19/surfaces/money/tree/CLAUDE.md +11 -0
  224. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  225. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  226. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  227. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  228. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  229. package/releases/0.28.19/surfaces/pii/tree/CLAUDE.md +5 -0
  230. package/src/commands/pipeline.mjs +367 -0
  231. package/src/graph.mjs +16 -1
@@ -0,0 +1,367 @@
1
+ /**
2
+ * `nina pipeline` — draws a project's agent chain: the line work moves along, the gates it passes on the
3
+ * way, the stages that run as several agents at once, where each stage sends work back and how many rounds
4
+ * it may, each stage's model, effort and skills with what it did over the last thirty days, and which chain
5
+ * a task of each shape takes. `--for` draws one shape's chain alone.
6
+ *
7
+ * Opening a project, the pipeline was a graph, a router, ten specs and a table in CLAUDE.md to read before
8
+ * anyone could say which stage came after which, on which model, with which skills. Every fact here is
9
+ * already composed, and `nina check` already holds it to the graph, or already measured; this only reads it
10
+ * back and draws it.
11
+ */
12
+
13
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
14
+ import { basename, join, resolve } from 'node:path';
15
+ import { PINK, useColor } from '../banner.mjs';
16
+ import { HARNESS, slugFor, snapshotsDir } from '../paths.mjs';
17
+ import { TERMINALS, parseGraph } from '../graph.mjs';
18
+ import { required } from '../tools.mjs';
19
+ import { isLoopBack } from '../transcripts.mjs';
20
+ import { frontmatter } from './pills.mjs';
21
+
22
+ /** A stage line under "## Stages": "- `name` — what it does". */
23
+ const STAGE_LINE = /^- `([a-z][a-z-]*)` — (.*)$/;
24
+
25
+ /** How far back the history beside each stage looks, unless `--since` says. */
26
+ const HISTORY_DAYS = 30;
27
+
28
+ /**
29
+ * What each stage does, in the graph's own words: shorter than a spec's `description:`, which is written
30
+ * for Claude Code to choose an agent by.
31
+ *
32
+ * @param {string} text - The composed `.claude/graph.md`.
33
+ * @returns {Map<string, string>}
34
+ */
35
+ export function stageWords(text) {
36
+ const out = new Map();
37
+ let inStages = false;
38
+ for (const line of text.split('\n')) {
39
+ if (line.startsWith('## ')) inStages = line.slice(3).trim().toLowerCase() === 'stages';
40
+ else if (inStages) {
41
+ const m = STAGE_LINE.exec(line);
42
+ if (m) out.set(m[1], m[2].trim());
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * The shape of the pipeline, read from its edges.
50
+ *
51
+ * - The line: from the first stage no forward edge reaches, the one forward edge each stage takes with no
52
+ * condition, until a stage has none.
53
+ * - The gates: a stage off the line that a line stage sends work to on a condition, and that hands it on
54
+ * to the next stage of the line.
55
+ * - The ends: where the line's last stage sends work on a condition, followed forward to a terminal.
56
+ * - The loop-backs: every edge on a verdict that sends work back, to a stage or to the owner, grouped by
57
+ * the stage that sends it.
58
+ *
59
+ * @param {{stages: Set<string>, edges: object[]}} graph - A parsed graph.
60
+ * @returns {{line: string[], gates: object[], ends: object[], back: object[]}}
61
+ */
62
+ export function shapeOf(graph) {
63
+ const forward = graph.edges.filter((e) => !isLoopBack(e.token));
64
+ const order = [...graph.stages];
65
+ const start = order.find((s) => !forward.some((e) => e.to === s)) ?? order[0];
66
+ const line = start ? [start] : [];
67
+ for (;;) {
68
+ const next = forward.filter((e) => e.from === line.at(-1) && !e.when && !line.includes(e.to) && !TERMINALS.has(e.to));
69
+ if (next.length !== 1) break;
70
+ line.push(next[0].to);
71
+ }
72
+ const gates = [];
73
+ line.forEach((stage, i) => {
74
+ for (const e of forward.filter((e) => e.from === stage && e.when && !line.includes(e.to) && !TERMINALS.has(e.to))) {
75
+ if (i + 1 < line.length && forward.some((f) => f.from === e.to && f.to === line[i + 1])) gates.push({ from: stage, gate: e.to, to: line[i + 1], when: e.when });
76
+ }
77
+ });
78
+ const last = line.at(-1);
79
+ const ends = forward
80
+ .filter((e) => e.from === last && e.when)
81
+ .map((e) => {
82
+ const path = [e.to];
83
+ while (!TERMINALS.has(path.at(-1))) {
84
+ const on = forward.filter((f) => f.from === path.at(-1) && !path.includes(f.to));
85
+ if (on.length !== 1) break;
86
+ path.push(on[0].to);
87
+ }
88
+ return { path, when: e.when };
89
+ });
90
+ const back = graph.edges.filter((e) => isLoopBack(e.token)).sort((a, b) => order.indexOf(a.from) - order.indexOf(b.from));
91
+ return { line, gates, ends, back };
92
+ }
93
+
94
+ /**
95
+ * The rows of the "Required chain by task shape" table in a composed CLAUDE.md: the shape, and the chain it
96
+ * takes — the part the table sets in bold, where it does. A row whose chain runs stage → stage, or is
97
+ * "none", is a chain of its own; any other (`+ dba before reviewer`) is an add-on to whichever chain the
98
+ * task takes.
99
+ *
100
+ * @param {string} text - The composed CLAUDE.md.
101
+ * @returns {{shape: string, chain: string, addOn: boolean}[]}
102
+ */
103
+ export function chainsByShape(text) {
104
+ const at = text.indexOf('| Task shape |');
105
+ if (at === -1) return [];
106
+ const rows = [];
107
+ for (const row of text.slice(at).split('\n').slice(2)) {
108
+ if (!row.startsWith('|')) break;
109
+ const [shape = '', chain = ''] = row.split('|').slice(1).map((c) => c.trim());
110
+ const bold = /\*\*(.+?)\*\*/.exec(chain)?.[1];
111
+ const plain = (s) => s.replace(/\*\*/g, '').replace(/`/g, '').trim();
112
+ const taken = plain(bold ?? chain);
113
+ rows.push({ shape: plain(shape), chain: taken, addOn: !taken.includes('→') && !/^none\b/i.test(taken) });
114
+ }
115
+ return rows;
116
+ }
117
+
118
+ /**
119
+ * The task shapes a `--for` names: by its number in the list, or by every word it gives.
120
+ *
121
+ * @param {{shape: string}[]} shapes - The chain table's rows.
122
+ * @param {string} query - A number, or words.
123
+ * @returns {object[]}
124
+ */
125
+ export function pickShape(shapes, query) {
126
+ if (/^\d+$/.test(query.trim())) {
127
+ const row = shapes[Number(query) - 1];
128
+ return row ? [row] : [];
129
+ }
130
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
131
+ if (words.length === 0) return [];
132
+ return shapes.filter((r) => words.every((w) => r.shape.toLowerCase().includes(w)));
133
+ }
134
+
135
+ /**
136
+ * What each stage did since a date, from the measurement store: its runs, the verdicts that could be read,
137
+ * and how many of those sent work back. A run a hook denied never ran, and is not counted.
138
+ *
139
+ * @param {object[]} records - The project's records.
140
+ * @param {string} since - An ISO date.
141
+ * @returns {Map<string, {runs: number, read: number, back: number}>}
142
+ */
143
+ export function historyOf(records, since) {
144
+ const byRole = new Map();
145
+ for (const r of records) {
146
+ if (!r.role || r.status === 'denied' || String(r.ts ?? '') < since) continue;
147
+ const s = byRole.get(r.role) ?? { runs: 0, read: 0, back: 0 };
148
+ s.runs += 1;
149
+ if (r.verdict && r.verdict !== 'UNCLEAR' && r.verdict !== 'NONE') {
150
+ s.read += 1;
151
+ if (isLoopBack(r.verdict)) s.back += 1;
152
+ }
153
+ byRole.set(r.role, s);
154
+ }
155
+ return byRole;
156
+ }
157
+
158
+ /**
159
+ * The project's records in the measurement store, under the name Claude Code gives its directory — as
160
+ * given, or with its links resolved, since either may be the one the sessions ran in.
161
+ *
162
+ * @param {string} dir - The project.
163
+ * @returns {object[]|null} Null when the store has nothing for it.
164
+ */
165
+ function storedRuns(dir) {
166
+ const names = new Set([slugFor(dir)]);
167
+ try {
168
+ names.add(slugFor(realpathSync(dir)));
169
+ } catch {
170
+ // A directory that cannot be resolved is looked up as given.
171
+ }
172
+ for (const name of names) {
173
+ const file = join(snapshotsDir(), `${name}.jsonl`);
174
+ if (!existsSync(file)) continue;
175
+ return readFileSync(file, 'utf8')
176
+ .split('\n')
177
+ .filter(Boolean)
178
+ .flatMap((l) => {
179
+ try {
180
+ return [JSON.parse(l)];
181
+ } catch {
182
+ return [];
183
+ }
184
+ });
185
+ }
186
+ return null;
187
+ }
188
+
189
+ /**
190
+ * Runs the command.
191
+ *
192
+ * @param {string[]} argv - `[--project <dir>] [--for <number or words>] [--since <YYYY-MM-DD>]`.
193
+ * @returns {Promise<number>} Process exit code.
194
+ */
195
+ export async function pipeline(argv) {
196
+ const arg = (flag) => (argv.includes(flag) ? argv[argv.indexOf(flag) + 1] ?? '' : null);
197
+ const dir = resolve(arg('--project') ?? process.cwd());
198
+ const graphPath = join(dir, '.claude', 'graph.md');
199
+ if (!existsSync(graphPath)) {
200
+ console.error(` no .claude/graph.md in ${dir} — this draws a composed pipeline: \`nina compose\` writes one`);
201
+ return 1;
202
+ }
203
+ const graphText = readFileSync(graphPath, 'utf8');
204
+ const graph = parseGraph(graphText);
205
+ const words = stageWords(graphText);
206
+ const shape = shapeOf(graph);
207
+ const claude = join(dir, 'CLAUDE.md');
208
+ const shapes = existsSync(claude) ? chainsByShape(readFileSync(claude, 'utf8')) : [];
209
+
210
+ // `--for`: one shape's chain, and only the stages on it.
211
+ const query = arg('--for');
212
+ let chosen = null;
213
+ if (query !== null) {
214
+ const picked = pickShape(shapes, query);
215
+ if (picked.length !== 1) {
216
+ console.error(picked.length === 0 ? ` no task shape matches "${query}" — by number or by words:` : ` "${query}" matches ${picked.length} task shapes — narrow it, or give the number:`);
217
+ for (const [i, r] of shapes.entries()) if (picked.length === 0 || picked.includes(r)) console.error(` ${String(i + 1).padStart(2)} ${r.shape}`);
218
+ return 1;
219
+ }
220
+ chosen = picked[0];
221
+ }
222
+
223
+ const color = useColor();
224
+ const name = (s) => (color && !TERMINALS.has(s) ? `\x1b[38;2;${PINK.join(';')}m${s}\x1b[0m` : s);
225
+ const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
226
+ const width = Math.max(80, Math.min(process.stdout.columns ?? 110, 140));
227
+ const fit = (s, room) => (s.length > room ? `${s.slice(0, Math.max(room - 1, 1))}…` : s);
228
+ const arrow = ' ─▶ ';
229
+ const many = graph.many ?? new Map();
230
+ // A stage that may run as several agents at once carries ×n wherever the line draws it. Alignment is
231
+ // counted on the plain text, and the names are painted after.
232
+ const plainStage = (s) => `${s}${many.has(s) ? ' ×n' : ''}`;
233
+ const shownStage = (s) => `${name(s)}${many.has(s) ? ' ×n' : ''}`;
234
+ const padName = (s, n) => `${name(s)}${' '.repeat(Math.max(n - s.length, 1))}`;
235
+
236
+ let profile = {};
237
+ try {
238
+ profile = JSON.parse(readFileSync(join(dir, HARNESS, 'profile.json'), 'utf8'));
239
+ } catch {
240
+ // A pipeline composed by hand draws all the same; only the header has less to say.
241
+ }
242
+ const surfaces = (profile.surfaces ?? []).slice().sort();
243
+ console.log(` pipeline · ${basename(dir)}${profile.core ? ` · core ${profile.core}` : ''}${surfaces.length ? ` · surfaces ${surfaces.join(', ')}` : ''}`);
244
+
245
+ // The stages this drawing covers: all of them, or those the chosen shape's chain names.
246
+ const onChain = chosen ? (chosen.chain.match(/[a-z][a-z-]*/g) ?? []).filter((w) => graph.stages.has(w)) : null;
247
+ const covered = new Set(onChain ?? graph.stages);
248
+
249
+ if (chosen) {
250
+ console.log(` for: ${chosen.shape}\n`);
251
+ if (onChain.length === 0) console.log(` ${chosen.chain}`);
252
+ else console.log(` ${onChain.map(shownStage).join(arrow)}`);
253
+ const addOns = shapes.filter((r) => r.addOn && r !== chosen);
254
+ if (addOns.length > 0 && onChain.length > 0) {
255
+ console.log(`\n ${dim('and, when the change calls for them')}`);
256
+ const col = Math.min(Math.max(...addOns.map((r) => r.chain.length)), 44) + 4;
257
+ for (const r of addOns) console.log(` ${fit(r.chain, col - 4).padEnd(col)}${dim(fit(r.shape, width - col - 6))}`);
258
+ }
259
+ } else {
260
+ console.log('');
261
+ // The line, and where its last stage sends work.
262
+ const plainHead = ` ${shape.line.map(plainStage).join(arrow)}`;
263
+ const head = ` ${shape.line.map(shownStage).join(arrow)}`;
264
+ if (shape.ends.length === 0) console.log(head);
265
+ const lead = ' '.repeat(plainHead.length + 1);
266
+ const joint = (i) => (shape.ends.length === 1 ? '──▶ ' : i === 0 ? '─┬─▶ ' : i === shape.ends.length - 1 ? ' └─▶ ' : ' ├─▶ ');
267
+ const endCol = Math.max(0, ...shape.ends.map((end, i) => `${lead}${joint(i)}${end.path.join(arrow)}`.length)) + 3;
268
+ shape.ends.forEach((end, i) => {
269
+ const plain = `${lead}${joint(i)}${end.path.join(arrow)}`;
270
+ console.log(`${i === 0 ? `${head} ` : lead}${joint(i)}${end.path.map(name).join(arrow)}${' '.repeat(endCol - plain.length)}${dim(fit(end.when, width - endCol))}`);
271
+ });
272
+
273
+ if (shape.gates.length > 0) {
274
+ console.log(`\n ${dim('gates on the way, when the diff calls for them')}`);
275
+ const chain = (g) => `${g.from}${arrow}${g.gate}${arrow}${g.to}`;
276
+ const col = Math.max(...shape.gates.map((g) => chain(g).length)) + 4;
277
+ for (const g of shape.gates) {
278
+ console.log(` ${name(g.from)}${arrow}${name(g.gate)}${arrow}${name(g.to)}${' '.repeat(col - chain(g).length)}${dim(fit(g.when, width - col - 6))}`);
279
+ }
280
+ }
281
+ }
282
+
283
+ const drawnMany = [...many].filter(([s]) => covered.has(s));
284
+ if (drawnMany.length > 0) {
285
+ console.log(`\n ${dim('×n runs as several agents at once, each on its own share of the work')}`);
286
+ const col = Math.max(...drawnMany.map(([s]) => s.length)) + 3;
287
+ for (const [s, { when }] of drawnMany) console.log(` ${padName(s, col)}${dim(fit(when, width - col - 8))}`);
288
+ }
289
+
290
+ const back = shape.back.filter((e) => covered.has(e.from) && (covered.has(e.to) || TERMINALS.has(e.to)));
291
+ if (back.length > 0) {
292
+ console.log(`\n ${dim('sent back: to whom, on which verdict, and how many rounds per issue')}`);
293
+ const fromCol = Math.max(...back.map((e) => e.from.length)) + 2;
294
+ const toCol = Math.max(...back.map((e) => e.to.length)) + 2;
295
+ const tokenCol = Math.max(...back.map((e) => e.token.length)) + 2;
296
+ let previous = '';
297
+ for (const e of back) {
298
+ const from = e.from === previous ? '' : e.from;
299
+ previous = e.from;
300
+ const rounds = e.to === 'human' ? '—' : e.max === null ? '?' : String(e.max);
301
+ const tail = `${e.token.padEnd(tokenCol)}${rounds.padStart(2)} `;
302
+ const used = 4 + fromCol + 2 + toCol + tail.length;
303
+ console.log(` ${from ? padName(from, fromCol) : ' '.repeat(fromCol)}↩ ${padName(e.to, toCol)}${tail}${dim(fit(e.when ?? '', width - used))}`);
304
+ }
305
+ }
306
+
307
+ // Each stage: what it does, on which model and effort, with which skills, and what it did lately.
308
+ const agentsDir = join(dir, '.claude', 'agents');
309
+ const specs = new Map();
310
+ for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
311
+ specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
312
+ }
313
+ const since = arg('--since') || new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString().slice(0, 10);
314
+ const runs = storedRuns(dir);
315
+ const history = runs ? historyOf(runs, since) : null;
316
+ const stages = [...graph.stages].filter((s) => covered.has(s));
317
+ const col = Math.max(...stages.map((s) => s.length), 10) + 2;
318
+ console.log(`\n ${dim(history ? `stages · what each ran since ${since}, from the measured history` : 'stages · no measured history for this project yet')}`);
319
+ for (const stage of stages) {
320
+ const spec = specs.get(stage) ?? '';
321
+ const front = frontmatter(spec) ?? {};
322
+ const on = [front.model, front.effort].filter(Boolean).join(' · ') || 'no spec';
323
+ console.log(` ${padName(stage, col)}${on.padEnd(26)}${dim(fit(words.get(stage) ?? '', width - col - 32))}`);
324
+ if (history) {
325
+ const h = history.get(stage);
326
+ const said = !h
327
+ ? 'no run'
328
+ : `${h.runs} run(s)${h.read === 0 ? ', no verdict read' : `, sent back ${h.back} of ${h.read} (${Math.round((100 * h.back) / h.read)}%)`}`;
329
+ console.log(` ${' '.repeat(col)}${dim('ran ')}${said}`);
330
+ }
331
+ const skills = [...required(spec).skills].sort();
332
+ if (skills.length > 0) {
333
+ const lines = [];
334
+ let current = '';
335
+ for (const skill of skills) {
336
+ const next = current ? `${current} · ${skill}` : skill;
337
+ if (next.length > width - col - 16 && current) {
338
+ lines.push(current);
339
+ current = skill;
340
+ } else current = next;
341
+ }
342
+ lines.push(current);
343
+ lines.forEach((l, i) => console.log(` ${' '.repeat(col)}${dim(i === 0 ? 'skills ' : ' ')}${l}`));
344
+ }
345
+ }
346
+
347
+ if (!chosen) {
348
+ // An agent the project wrote for itself is not a stage, and the graph does not route to it.
349
+ const own = [...specs.keys()].filter((role) => !graph.stages.has(role));
350
+ if (own.length > 0) {
351
+ console.log(`\n ${dim("the project's own agents, outside the pipeline")}`);
352
+ for (const role of own) {
353
+ const front = frontmatter(specs.get(role)) ?? {};
354
+ console.log(` ${role}${' '.repeat(Math.max(col - role.length, 2))}${dim(fit(front.description ?? '', width - col - 6))}`);
355
+ }
356
+ }
357
+ if (shapes.length > 0) {
358
+ console.log(`\n ${dim('which chain a task takes, by its shape (CLAUDE.md) — `nina pipeline --for <number or words>` draws one')}`);
359
+ const shapeCol = Math.min(Math.max(...shapes.map((r) => r.shape.length)), 56) + 2;
360
+ for (const [i, r] of shapes.entries()) {
361
+ console.log(` ${String(i + 1).padStart(2)} ${fit(r.shape, shapeCol - 2).padEnd(shapeCol)}${fit(r.chain, width - shapeCol - 10)}`);
362
+ }
363
+ }
364
+ }
365
+ console.log('');
366
+ return 0;
367
+ }
package/src/graph.mjs CHANGED
@@ -24,6 +24,13 @@ const STAGE = /^- `([a-z][a-z-]*)` — /;
24
24
  */
25
25
  const EDGE = /^- `([a-z][a-z-]*)` → `([a-z][a-z-]*)` on `([A-Z][A-Z-]*)`(?: — (.*?))?(?: · max (\d+))?\s*$/;
26
26
 
27
+ /**
28
+ * A line under "## Concurrency" naming a stage that may run as several agents at once, each on a share of
29
+ * the work: "- `name` × many — when". Declared rather than left to the prose, so a drawing of the pipeline
30
+ * can show it and `check` can hold it to the stages.
31
+ */
32
+ const MANY = /^- `([a-z][a-z-]*)` × many — (.*)$/;
33
+
27
34
  /** Where work may end: finished, or handed to the owner. Neither needs a spec. */
28
35
  export const TERMINALS = new Set(['done', 'human']);
29
36
 
@@ -40,6 +47,8 @@ export function parseGraph(text) {
40
47
  const stages = new Set();
41
48
  const edges = [];
42
49
  const stray = [];
50
+ /** @type {Map<string, {when: string, line: number}>} */
51
+ const many = new Map();
43
52
  const sections = new Set();
44
53
  const twice = [];
45
54
  let section = '';
@@ -61,9 +70,12 @@ export function parseGraph(text) {
61
70
  return;
62
71
  }
63
72
  edges.push({ from: m[1], to: m[2], token: m[3], when: m[4] ?? '', max: m[5] ? Number(m[5]) : null, line: i + 1 });
73
+ } else if (section === 'concurrency') {
74
+ const m = MANY.exec(line);
75
+ if (m) many.set(m[1], { when: m[2].trim(), line: i + 1 });
64
76
  }
65
77
  });
66
- return { stages, edges, stray, sections, twice };
78
+ return { stages, edges, stray, sections, twice, many };
67
79
  }
68
80
 
69
81
  /**
@@ -122,6 +134,9 @@ export function validateGraph(graph, specs, roles = new Set()) {
122
134
  for (const s of graph.twice ?? []) problems.push(`stage \`${s}\` is listed more than once`);
123
135
 
124
136
  for (const s of stray) problems.push(`graph.md:${s.line} is under "## Edges" but is not an edge: ${s.text}`);
137
+ for (const [s, { line }] of graph.many ?? []) {
138
+ if (!stages.has(s)) problems.push(`graph.md:${line} says \`${s}\` runs as several agents, and it is not a stage here`);
139
+ }
125
140
 
126
141
  // Every stage is a spec on disk and every spec on disk is a stage — the profile decides both, so
127
142
  // a mismatch means a layer named a stage the profile does not compose, or composed one no edge