@xhulz/nina 0.28.18 → 0.28.20

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/package.json +1 -1
  2. package/releases/0.28.19/core/GAPS.md +125 -0
  3. package/releases/0.28.19/core/answered.json +29 -0
  4. package/releases/0.28.19/core/defaults/tree/.claude/agents/architect.md +1 -0
  5. package/releases/0.28.19/core/defaults/tree/.claude/agents/dba.md +3 -0
  6. package/releases/0.28.19/core/defaults/tree/.claude/agents/devops.md +1 -0
  7. package/releases/0.28.19/core/defaults/tree/.claude/agents/implementer.md +1 -0
  8. package/releases/0.28.19/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  9. package/releases/0.28.19/core/defaults/tree/.claude/agents/qa.md +2 -0
  10. package/releases/0.28.19/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  11. package/releases/0.28.19/core/defaults/tree/.claude/agents/secops.md +3 -0
  12. package/releases/0.28.19/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  13. package/releases/0.28.19/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  14. package/releases/0.28.19/core/tree/.claude/agents/architect.md +125 -0
  15. package/releases/0.28.19/core/tree/.claude/agents/dba.md +107 -0
  16. package/releases/0.28.19/core/tree/.claude/agents/devops.md +113 -0
  17. package/releases/0.28.19/core/tree/.claude/agents/implementer.md +155 -0
  18. package/releases/0.28.19/core/tree/.claude/agents/integration-tester.md +160 -0
  19. package/releases/0.28.19/core/tree/.claude/agents/planner.md +132 -0
  20. package/releases/0.28.19/core/tree/.claude/agents/qa.md +146 -0
  21. package/releases/0.28.19/core/tree/.claude/agents/reviewer.md +180 -0
  22. package/releases/0.28.19/core/tree/.claude/agents/secops.md +121 -0
  23. package/releases/0.28.19/core/tree/.claude/agents/solidity-auditor.md +126 -0
  24. package/releases/0.28.19/core/tree/.claude/agents/solidity-dev.md +123 -0
  25. package/releases/0.28.19/core/tree/.claude/agents-overview.md +64 -0
  26. package/releases/0.28.19/core/tree/.claude/graph.md +73 -0
  27. package/releases/0.28.19/core/tree/.claude/patterns.md +212 -0
  28. package/releases/0.28.19/core/tree/.claude/pills/README.md +145 -0
  29. package/releases/0.28.19/core/tree/.claude/pipeline.md +102 -0
  30. package/releases/0.28.19/core/tree/.claude/retrieval.md +104 -0
  31. package/releases/0.28.19/core/tree/.claude/router.md +201 -0
  32. package/releases/0.28.19/core/tree/.claude/templates/integration.md +45 -0
  33. package/releases/0.28.19/core/tree/CLAUDE.md +155 -0
  34. package/releases/0.28.19/core/tree/scripts/edit-guard.mjs +16 -0
  35. package/releases/0.28.19/core/tree/scripts/harness-check.mjs +71 -0
  36. package/releases/0.28.19/core/tree/scripts/loop-gate.mjs +19 -0
  37. package/releases/0.28.19/core/vocabulary.json +12 -0
  38. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  39. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  40. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  41. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  42. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  43. package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  44. package/releases/0.28.19/surfaces/blockchain/tree/.claude/graph.md +11 -0
  45. package/releases/0.28.19/surfaces/blockchain/tree/.claude/router.md +9 -0
  46. package/releases/0.28.19/surfaces/blockchain/tree/CLAUDE.md +12 -0
  47. package/releases/0.28.19/surfaces/db/tree/.claude/agents/architect.md +8 -0
  48. package/releases/0.28.19/surfaces/db/tree/.claude/agents/dba.md +84 -0
  49. package/releases/0.28.19/surfaces/db/tree/.claude/agents/devops.md +14 -0
  50. package/releases/0.28.19/surfaces/db/tree/.claude/agents/implementer.md +5 -0
  51. package/releases/0.28.19/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
  52. package/releases/0.28.19/surfaces/db/tree/.claude/agents/planner.md +2 -0
  53. package/releases/0.28.19/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
  54. package/releases/0.28.19/surfaces/db/tree/.claude/agents/secops.md +2 -0
  55. package/releases/0.28.19/surfaces/db/tree/.claude/agents-overview.md +5 -0
  56. package/releases/0.28.19/surfaces/db/tree/.claude/graph.md +8 -0
  57. package/releases/0.28.19/surfaces/db/tree/.claude/patterns.md +29 -0
  58. package/releases/0.28.19/surfaces/db/tree/.claude/pipeline.md +13 -0
  59. package/releases/0.28.19/surfaces/db/tree/.claude/retrieval.md +5 -0
  60. package/releases/0.28.19/surfaces/db/tree/.claude/router.md +10 -0
  61. package/releases/0.28.19/surfaces/db/tree/CLAUDE.md +23 -0
  62. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  63. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  64. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
  65. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  66. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  67. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  68. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  69. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  70. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  71. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  72. package/releases/0.28.19/surfaces/edge-cf/tree/.claude/router.md +21 -0
  73. package/releases/0.28.19/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  74. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  75. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  76. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  77. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  78. package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
  79. package/releases/0.28.19/surfaces/frontend/tree/.claude/patterns.md +15 -0
  80. package/releases/0.28.19/surfaces/frontend/tree/.claude/retrieval.md +5 -0
  81. package/releases/0.28.19/surfaces/frontend/tree/.claude/router.md +17 -0
  82. package/releases/0.28.19/surfaces/frontend/tree/CLAUDE.md +5 -0
  83. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  84. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  85. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  86. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  87. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  88. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  89. package/releases/0.28.19/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  90. package/releases/0.28.19/surfaces/integrations/tree/.claude/graph.md +9 -0
  91. package/releases/0.28.19/surfaces/integrations/tree/.claude/patterns.md +38 -0
  92. package/releases/0.28.19/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  93. package/releases/0.28.19/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  94. package/releases/0.28.19/surfaces/integrations/tree/.claude/router.md +6 -0
  95. package/releases/0.28.19/surfaces/integrations/tree/CLAUDE.md +17 -0
  96. package/releases/0.28.19/surfaces/money/tree/.claude/agents/architect.md +12 -0
  97. package/releases/0.28.19/surfaces/money/tree/.claude/agents/dba.md +14 -0
  98. package/releases/0.28.19/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  99. package/releases/0.28.19/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  100. package/releases/0.28.19/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  101. package/releases/0.28.19/surfaces/money/tree/.claude/agents/secops.md +5 -0
  102. package/releases/0.28.19/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  103. package/releases/0.28.19/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  104. package/releases/0.28.19/surfaces/money/tree/.claude/graph.md +2 -0
  105. package/releases/0.28.19/surfaces/money/tree/.claude/patterns.md +49 -0
  106. package/releases/0.28.19/surfaces/money/tree/.claude/pipeline.md +2 -0
  107. package/releases/0.28.19/surfaces/money/tree/.claude/retrieval.md +2 -0
  108. package/releases/0.28.19/surfaces/money/tree/CLAUDE.md +11 -0
  109. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  110. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  111. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  112. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  113. package/releases/0.28.19/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  114. package/releases/0.28.19/surfaces/pii/tree/CLAUDE.md +5 -0
  115. package/releases/0.28.20/core/GAPS.md +125 -0
  116. package/releases/0.28.20/core/answered.json +29 -0
  117. package/releases/0.28.20/core/defaults/tree/.claude/agents/architect.md +1 -0
  118. package/releases/0.28.20/core/defaults/tree/.claude/agents/dba.md +3 -0
  119. package/releases/0.28.20/core/defaults/tree/.claude/agents/devops.md +1 -0
  120. package/releases/0.28.20/core/defaults/tree/.claude/agents/implementer.md +1 -0
  121. package/releases/0.28.20/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  122. package/releases/0.28.20/core/defaults/tree/.claude/agents/qa.md +2 -0
  123. package/releases/0.28.20/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  124. package/releases/0.28.20/core/defaults/tree/.claude/agents/secops.md +3 -0
  125. package/releases/0.28.20/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  126. package/releases/0.28.20/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  127. package/releases/0.28.20/core/tree/.claude/agents/architect.md +125 -0
  128. package/releases/0.28.20/core/tree/.claude/agents/dba.md +107 -0
  129. package/releases/0.28.20/core/tree/.claude/agents/devops.md +113 -0
  130. package/releases/0.28.20/core/tree/.claude/agents/implementer.md +155 -0
  131. package/releases/0.28.20/core/tree/.claude/agents/integration-tester.md +160 -0
  132. package/releases/0.28.20/core/tree/.claude/agents/planner.md +132 -0
  133. package/releases/0.28.20/core/tree/.claude/agents/qa.md +146 -0
  134. package/releases/0.28.20/core/tree/.claude/agents/reviewer.md +180 -0
  135. package/releases/0.28.20/core/tree/.claude/agents/secops.md +121 -0
  136. package/releases/0.28.20/core/tree/.claude/agents/solidity-auditor.md +126 -0
  137. package/releases/0.28.20/core/tree/.claude/agents/solidity-dev.md +123 -0
  138. package/releases/0.28.20/core/tree/.claude/agents-overview.md +64 -0
  139. package/releases/0.28.20/core/tree/.claude/graph.md +73 -0
  140. package/releases/0.28.20/core/tree/.claude/patterns.md +212 -0
  141. package/releases/0.28.20/core/tree/.claude/pills/README.md +145 -0
  142. package/releases/0.28.20/core/tree/.claude/pipeline.md +102 -0
  143. package/releases/0.28.20/core/tree/.claude/retrieval.md +104 -0
  144. package/releases/0.28.20/core/tree/.claude/router.md +201 -0
  145. package/releases/0.28.20/core/tree/.claude/templates/integration.md +45 -0
  146. package/releases/0.28.20/core/tree/CLAUDE.md +155 -0
  147. package/releases/0.28.20/core/tree/scripts/edit-guard.mjs +16 -0
  148. package/releases/0.28.20/core/tree/scripts/harness-check.mjs +71 -0
  149. package/releases/0.28.20/core/tree/scripts/loop-gate.mjs +19 -0
  150. package/releases/0.28.20/core/vocabulary.json +12 -0
  151. package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  152. package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  153. package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  154. package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  155. package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  156. package/releases/0.28.20/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  157. package/releases/0.28.20/surfaces/blockchain/tree/.claude/graph.md +11 -0
  158. package/releases/0.28.20/surfaces/blockchain/tree/.claude/router.md +9 -0
  159. package/releases/0.28.20/surfaces/blockchain/tree/CLAUDE.md +12 -0
  160. package/releases/0.28.20/surfaces/db/tree/.claude/agents/architect.md +8 -0
  161. package/releases/0.28.20/surfaces/db/tree/.claude/agents/dba.md +84 -0
  162. package/releases/0.28.20/surfaces/db/tree/.claude/agents/devops.md +14 -0
  163. package/releases/0.28.20/surfaces/db/tree/.claude/agents/implementer.md +5 -0
  164. package/releases/0.28.20/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
  165. package/releases/0.28.20/surfaces/db/tree/.claude/agents/planner.md +2 -0
  166. package/releases/0.28.20/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
  167. package/releases/0.28.20/surfaces/db/tree/.claude/agents/secops.md +2 -0
  168. package/releases/0.28.20/surfaces/db/tree/.claude/agents-overview.md +5 -0
  169. package/releases/0.28.20/surfaces/db/tree/.claude/graph.md +8 -0
  170. package/releases/0.28.20/surfaces/db/tree/.claude/patterns.md +29 -0
  171. package/releases/0.28.20/surfaces/db/tree/.claude/pipeline.md +13 -0
  172. package/releases/0.28.20/surfaces/db/tree/.claude/retrieval.md +5 -0
  173. package/releases/0.28.20/surfaces/db/tree/.claude/router.md +10 -0
  174. package/releases/0.28.20/surfaces/db/tree/CLAUDE.md +23 -0
  175. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  176. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  177. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
  178. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  179. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  180. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  181. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  182. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  183. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  184. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  185. package/releases/0.28.20/surfaces/edge-cf/tree/.claude/router.md +21 -0
  186. package/releases/0.28.20/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  187. package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  188. package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  189. package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  190. package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  191. package/releases/0.28.20/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
  192. package/releases/0.28.20/surfaces/frontend/tree/.claude/patterns.md +15 -0
  193. package/releases/0.28.20/surfaces/frontend/tree/.claude/retrieval.md +5 -0
  194. package/releases/0.28.20/surfaces/frontend/tree/.claude/router.md +17 -0
  195. package/releases/0.28.20/surfaces/frontend/tree/CLAUDE.md +5 -0
  196. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  197. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  198. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  199. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  200. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  201. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  202. package/releases/0.28.20/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  203. package/releases/0.28.20/surfaces/integrations/tree/.claude/graph.md +9 -0
  204. package/releases/0.28.20/surfaces/integrations/tree/.claude/patterns.md +38 -0
  205. package/releases/0.28.20/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  206. package/releases/0.28.20/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  207. package/releases/0.28.20/surfaces/integrations/tree/.claude/router.md +6 -0
  208. package/releases/0.28.20/surfaces/integrations/tree/CLAUDE.md +17 -0
  209. package/releases/0.28.20/surfaces/money/tree/.claude/agents/architect.md +12 -0
  210. package/releases/0.28.20/surfaces/money/tree/.claude/agents/dba.md +14 -0
  211. package/releases/0.28.20/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  212. package/releases/0.28.20/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  213. package/releases/0.28.20/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  214. package/releases/0.28.20/surfaces/money/tree/.claude/agents/secops.md +5 -0
  215. package/releases/0.28.20/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  216. package/releases/0.28.20/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  217. package/releases/0.28.20/surfaces/money/tree/.claude/graph.md +2 -0
  218. package/releases/0.28.20/surfaces/money/tree/.claude/patterns.md +49 -0
  219. package/releases/0.28.20/surfaces/money/tree/.claude/pipeline.md +2 -0
  220. package/releases/0.28.20/surfaces/money/tree/.claude/retrieval.md +2 -0
  221. package/releases/0.28.20/surfaces/money/tree/CLAUDE.md +11 -0
  222. package/releases/0.28.20/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  223. package/releases/0.28.20/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  224. package/releases/0.28.20/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  225. package/releases/0.28.20/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  226. package/releases/0.28.20/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  227. package/releases/0.28.20/surfaces/pii/tree/CLAUDE.md +5 -0
  228. package/src/commands/pipeline.mjs +192 -59
  229. package/src/commands/stats.mjs +83 -48
  230. package/src/graph.mjs +16 -1
  231. package/src/look.mjs +118 -0
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * `nina pipeline` — draws a project's agent chain: the line work moves along, the gates it passes on the
3
- * way, where each stage sends work back and how many rounds it may, each stage's model, effort and skills,
4
- * and which chain a task of each shape takes.
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.
5
6
  *
6
7
  * Opening a project, the pipeline was a graph, a router, ten specs and a table in CLAUDE.md to read before
7
8
  * anyone could say which stage came after which, on which model, with which skills. Every fact here is
8
- * already composed, and `nina check` already holds it to the graph; this only reads it back and draws it.
9
+ * already composed, and `nina check` already holds it to the graph, or already measured; this only reads it
10
+ * back and draws it.
9
11
  */
10
12
 
11
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
13
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
12
14
  import { basename, join, resolve } from 'node:path';
13
- import { PINK, useColor } from '../banner.mjs';
14
- import { HARNESS } from '../paths.mjs';
15
+ import { dim, pink } from '../look.mjs';
16
+ import { HARNESS, slugFor, snapshotsDir } from '../paths.mjs';
15
17
  import { TERMINALS, parseGraph } from '../graph.mjs';
16
18
  import { required } from '../tools.mjs';
17
19
  import { isLoopBack } from '../transcripts.mjs';
@@ -20,6 +22,9 @@ import { frontmatter } from './pills.mjs';
20
22
  /** A stage line under "## Stages": "- `name` — what it does". */
21
23
  const STAGE_LINE = /^- `([a-z][a-z-]*)` — (.*)$/;
22
24
 
25
+ /** How far back the history beside each stage looks, unless `--since` says. */
26
+ const HISTORY_DAYS = 30;
27
+
23
28
  /**
24
29
  * What each stage does, in the graph's own words: shorter than a spec's `description:`, which is written
25
30
  * for Claude Code to choose an agent by.
@@ -48,7 +53,8 @@ export function stageWords(text) {
48
53
  * - The gates: a stage off the line that a line stage sends work to on a condition, and that hands it on
49
54
  * to the next stage of the line.
50
55
  * - The ends: where the line's last stage sends work on a condition, followed forward to a terminal.
51
- * - The loop-backs: every edge on a verdict that sends work back, to a stage or to the owner.
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.
52
58
  *
53
59
  * @param {{stages: Set<string>, edges: object[]}} graph - A parsed graph.
54
60
  * @returns {{line: string[], gates: object[], ends: object[], back: object[]}}
@@ -81,17 +87,18 @@ export function shapeOf(graph) {
81
87
  }
82
88
  return { path, when: e.when };
83
89
  });
84
- // Grouped by the stage that sends the work back, in the order the stages are listed.
85
90
  const back = graph.edges.filter((e) => isLoopBack(e.token)).sort((a, b) => order.indexOf(a.from) - order.indexOf(b.from));
86
91
  return { line, gates, ends, back };
87
92
  }
88
93
 
89
94
  /**
90
95
  * The rows of the "Required chain by task shape" table in a composed CLAUDE.md: the shape, and the chain it
91
- * takes — the part the table sets in bold, where it does.
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.
92
99
  *
93
100
  * @param {string} text - The composed CLAUDE.md.
94
- * @returns {{shape: string, chain: string}[]}
101
+ * @returns {{shape: string, chain: string, addOn: boolean}[]}
95
102
  */
96
103
  export function chainsByShape(text) {
97
104
  const at = text.indexOf('| Task shape |');
@@ -102,34 +109,127 @@ export function chainsByShape(text) {
102
109
  const [shape = '', chain = ''] = row.split('|').slice(1).map((c) => c.trim());
103
110
  const bold = /\*\*(.+?)\*\*/.exec(chain)?.[1];
104
111
  const plain = (s) => s.replace(/\*\*/g, '').replace(/`/g, '').trim();
105
- rows.push({ shape: plain(shape), chain: plain(bold ?? chain) });
112
+ const taken = plain(bold ?? chain);
113
+ rows.push({ shape: plain(shape), chain: taken, addOn: !taken.includes('→') && !/^none\b/i.test(taken) });
106
114
  }
107
115
  return rows;
108
116
  }
109
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
+
110
189
  /**
111
190
  * Runs the command.
112
191
  *
113
- * @param {string[]} argv - `[--project <dir>]`.
192
+ * @param {string[]} argv - `[--project <dir>] [--for <number or words>] [--since <YYYY-MM-DD>]`.
114
193
  * @returns {Promise<number>} Process exit code.
115
194
  */
116
195
  export async function pipeline(argv) {
117
- const dir = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : process.cwd());
196
+ const arg = (flag) => (argv.includes(flag) ? argv[argv.indexOf(flag) + 1] ?? '' : null);
197
+ const dir = resolve(arg('--project') ?? process.cwd());
118
198
  const graphPath = join(dir, '.claude', 'graph.md');
119
199
  if (!existsSync(graphPath)) {
120
200
  console.error(` no .claude/graph.md in ${dir} — this draws a composed pipeline: \`nina compose\` writes one`);
121
201
  return 1;
122
202
  }
123
- const graph = parseGraph(readFileSync(graphPath, 'utf8'));
124
- const words = stageWords(readFileSync(graphPath, 'utf8'));
125
- const { line, gates, ends, back } = shapeOf(graph);
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
+ }
126
222
 
127
- const color = useColor();
128
- const name = (s) => (color && !TERMINALS.has(s) ? `\x1b[38;2;${PINK.join(';')}m${s}\x1b[0m` : s);
129
- const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
223
+ const name = (s) => (TERMINALS.has(s) ? s : pink(s));
130
224
  const width = Math.max(80, Math.min(process.stdout.columns ?? 110, 140));
131
225
  const fit = (s, room) => (s.length > room ? `${s.slice(0, Math.max(room - 1, 1))}…` : s);
132
226
  const arrow = ' ─▶ ';
227
+ const many = graph.many ?? new Map();
228
+ // A stage that may run as several agents at once carries ×n wherever the line draws it. Alignment is
229
+ // counted on the plain text, and the names are painted after.
230
+ const plainStage = (s) => `${s}${many.has(s) ? ' ×n' : ''}`;
231
+ const shownStage = (s) => `${name(s)}${many.has(s) ? ' ×n' : ''}`;
232
+ const padName = (s, n) => `${name(s)}${' '.repeat(Math.max(n - s.length, 1))}`;
133
233
 
134
234
  let profile = {};
135
235
  try {
@@ -138,32 +238,54 @@ export async function pipeline(argv) {
138
238
  // A pipeline composed by hand draws all the same; only the header has less to say.
139
239
  }
140
240
  const surfaces = (profile.surfaces ?? []).slice().sort();
141
- console.log(` pipeline · ${basename(dir)}${profile.core ? ` · core ${profile.core}` : ''}${surfaces.length ? ` · surfaces ${surfaces.join(', ')}` : ''}\n`);
241
+ console.log(` pipeline · ${basename(dir)}${profile.core ? ` · core ${profile.core}` : ''}${surfaces.length ? ` · surfaces ${surfaces.join(', ')}` : ''}`);
142
242
 
143
- // Alignment is counted on the plain text, and the names are painted after.
144
- const padName = (s, n) => `${name(s)}${' '.repeat(Math.max(n - s.length, 1))}`;
243
+ // The stages this drawing covers: all of them, or those the chosen shape's chain names.
244
+ const onChain = chosen ? (chosen.chain.match(/[a-z][a-z-]*/g) ?? []).filter((w) => graph.stages.has(w)) : null;
245
+ const covered = new Set(onChain ?? graph.stages);
145
246
 
146
- // The line, and where its last stage sends work.
147
- const plainHead = ` ${line.join(arrow)}`;
148
- const head = ` ${line.map(name).join(arrow)}`;
149
- if (ends.length === 0) console.log(head);
150
- const lead = ' '.repeat(plainHead.length + 1);
151
- const joint = (i) => (ends.length === 1 ? '──▶ ' : i === 0 ? '─┬─▶ ' : i === ends.length - 1 ? ' └─▶ ' : ' ├─▶ ');
152
- const endCol = Math.max(0, ...ends.map((end, i) => `${lead}${joint(i)}${end.path.join(arrow)}`.length)) + 3;
153
- ends.forEach((end, i) => {
154
- const plain = `${lead}${joint(i)}${end.path.join(arrow)}`;
155
- console.log(`${i === 0 ? `${head} ` : lead}${joint(i)}${end.path.map(name).join(arrow)}${' '.repeat(endCol - plain.length)}${dim(fit(end.when, width - endCol))}`);
156
- });
247
+ if (chosen) {
248
+ console.log(` for: ${chosen.shape}\n`);
249
+ if (onChain.length === 0) console.log(` ${chosen.chain}`);
250
+ else console.log(` ${onChain.map(shownStage).join(arrow)}`);
251
+ const addOns = shapes.filter((r) => r.addOn && r !== chosen);
252
+ if (addOns.length > 0 && onChain.length > 0) {
253
+ console.log(`\n ${dim('and, when the change calls for them')}`);
254
+ const col = Math.min(Math.max(...addOns.map((r) => r.chain.length)), 44) + 4;
255
+ for (const r of addOns) console.log(` ${fit(r.chain, col - 4).padEnd(col)}${dim(fit(r.shape, width - col - 6))}`);
256
+ }
257
+ } else {
258
+ console.log('');
259
+ // The line, and where its last stage sends work.
260
+ const plainHead = ` ${shape.line.map(plainStage).join(arrow)}`;
261
+ const head = ` ${shape.line.map(shownStage).join(arrow)}`;
262
+ if (shape.ends.length === 0) console.log(head);
263
+ const lead = ' '.repeat(plainHead.length + 1);
264
+ const joint = (i) => (shape.ends.length === 1 ? '──▶ ' : i === 0 ? '─┬─▶ ' : i === shape.ends.length - 1 ? ' └─▶ ' : ' ├─▶ ');
265
+ const endCol = Math.max(0, ...shape.ends.map((end, i) => `${lead}${joint(i)}${end.path.join(arrow)}`.length)) + 3;
266
+ shape.ends.forEach((end, i) => {
267
+ const plain = `${lead}${joint(i)}${end.path.join(arrow)}`;
268
+ console.log(`${i === 0 ? `${head} ` : lead}${joint(i)}${end.path.map(name).join(arrow)}${' '.repeat(endCol - plain.length)}${dim(fit(end.when, width - endCol))}`);
269
+ });
157
270
 
158
- if (gates.length > 0) {
159
- console.log(`\n ${dim('gates on the way, when the diff calls for them')}`);
160
- const chain = (g) => `${g.from}${arrow}${g.gate}${arrow}${g.to}`;
161
- const col = Math.max(...gates.map((g) => chain(g).length)) + 4;
162
- for (const g of gates) {
163
- 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))}`);
271
+ if (shape.gates.length > 0) {
272
+ console.log(`\n ${dim('gates on the way, when the diff calls for them')}`);
273
+ const chain = (g) => `${g.from}${arrow}${g.gate}${arrow}${g.to}`;
274
+ const col = Math.max(...shape.gates.map((g) => chain(g).length)) + 4;
275
+ for (const g of shape.gates) {
276
+ 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))}`);
277
+ }
164
278
  }
165
279
  }
166
280
 
281
+ const drawnMany = [...many].filter(([s]) => covered.has(s));
282
+ if (drawnMany.length > 0) {
283
+ console.log(`\n ${dim('×n runs as several agents at once, each on its own share of the work')}`);
284
+ const col = Math.max(...drawnMany.map(([s]) => s.length)) + 3;
285
+ for (const [s, { when }] of drawnMany) console.log(` ${padName(s, col)}${dim(fit(when, width - col - 8))}`);
286
+ }
287
+
288
+ const back = shape.back.filter((e) => covered.has(e.from) && (covered.has(e.to) || TERMINALS.has(e.to)));
167
289
  if (back.length > 0) {
168
290
  console.log(`\n ${dim('sent back: to whom, on which verdict, and how many rounds per issue')}`);
169
291
  const fromCol = Math.max(...back.map((e) => e.from.length)) + 2;
@@ -180,20 +302,30 @@ export async function pipeline(argv) {
180
302
  }
181
303
  }
182
304
 
183
- // Each stage: what it does, on which model and effort, with which skills.
305
+ // Each stage: what it does, on which model and effort, with which skills, and what it did lately.
184
306
  const agentsDir = join(dir, '.claude', 'agents');
185
307
  const specs = new Map();
186
308
  for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
187
309
  specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
188
310
  }
189
- const stages = [...graph.stages];
311
+ const since = arg('--since') || new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString().slice(0, 10);
312
+ const runs = storedRuns(dir);
313
+ const history = runs ? historyOf(runs, since) : null;
314
+ const stages = [...graph.stages].filter((s) => covered.has(s));
190
315
  const col = Math.max(...stages.map((s) => s.length), 10) + 2;
191
- console.log(`\n ${dim('stages')}`);
316
+ console.log(`\n ${dim(history ? `stages · what each ran since ${since}, from the measured history` : 'stages · no measured history for this project yet')}`);
192
317
  for (const stage of stages) {
193
318
  const spec = specs.get(stage) ?? '';
194
319
  const front = frontmatter(spec) ?? {};
195
- const runs = [front.model, front.effort].filter(Boolean).join(' · ') || 'no spec';
196
- console.log(` ${padName(stage, col)}${runs.padEnd(26)}${dim(fit(words.get(stage) ?? '', width - col - 32))}`);
320
+ const on = [front.model, front.effort].filter(Boolean).join(' · ') || 'no spec';
321
+ console.log(` ${padName(stage, col)}${on.padEnd(26)}${dim(fit(words.get(stage) ?? '', width - col - 32))}`);
322
+ if (history) {
323
+ const h = history.get(stage);
324
+ const said = !h
325
+ ? 'no run'
326
+ : `${h.runs} run(s)${h.read === 0 ? ', no verdict read' : `, sent back ${h.back} of ${h.read} (${Math.round((100 * h.back) / h.read)}%)`}`;
327
+ console.log(` ${' '.repeat(col)}${dim('ran ')}${said}`);
328
+ }
197
329
  const skills = [...required(spec).skills].sort();
198
330
  if (skills.length > 0) {
199
331
  const lines = [];
@@ -210,22 +342,23 @@ export async function pipeline(argv) {
210
342
  }
211
343
  }
212
344
 
213
- // An agent the project wrote for itself is not a stage, and the graph does not route to it.
214
- const own = [...specs.keys()].filter((role) => !graph.stages.has(role));
215
- if (own.length > 0) {
216
- console.log(`\n ${dim("the project's own agents, outside the pipeline")}`);
217
- for (const role of own) {
218
- const front = frontmatter(specs.get(role)) ?? {};
219
- console.log(` ${role}${' '.repeat(Math.max(col - role.length, 2))}${dim(fit(front.description ?? '', width - col - 6))}`);
345
+ if (!chosen) {
346
+ // An agent the project wrote for itself is not a stage, and the graph does not route to it.
347
+ const own = [...specs.keys()].filter((role) => !graph.stages.has(role));
348
+ if (own.length > 0) {
349
+ console.log(`\n ${dim("the project's own agents, outside the pipeline")}`);
350
+ for (const role of own) {
351
+ const front = frontmatter(specs.get(role)) ?? {};
352
+ console.log(` ${role}${' '.repeat(Math.max(col - role.length, 2))}${dim(fit(front.description ?? '', width - col - 6))}`);
353
+ }
354
+ }
355
+ if (shapes.length > 0) {
356
+ console.log(`\n ${dim('which chain a task takes, by its shape (CLAUDE.md) — `nina pipeline --for <number or words>` draws one')}`);
357
+ const shapeCol = Math.min(Math.max(...shapes.map((r) => r.shape.length)), 56) + 2;
358
+ for (const [i, r] of shapes.entries()) {
359
+ console.log(` ${String(i + 1).padStart(2)} ${fit(r.shape, shapeCol - 2).padEnd(shapeCol)}${fit(r.chain, width - shapeCol - 10)}`);
360
+ }
220
361
  }
221
- }
222
-
223
- const claude = join(dir, 'CLAUDE.md');
224
- const shapes = existsSync(claude) ? chainsByShape(readFileSync(claude, 'utf8')) : [];
225
- if (shapes.length > 0) {
226
- console.log(`\n ${dim('which chain a task takes, by its shape (CLAUDE.md)')}`);
227
- const shapeCol = Math.min(Math.max(...shapes.map((r) => r.shape.length)), 58) + 2;
228
- for (const r of shapes) console.log(` ${fit(r.shape, shapeCol - 2).padEnd(shapeCol)}${fit(r.chain, width - shapeCol - 6)}`);
229
362
  }
230
363
  console.log('');
231
364
  return 0;
@@ -11,6 +11,7 @@
11
11
  * loop-backs it was supposed to harvest were being counted right here.
12
12
  */
13
13
 
14
+ import { amber, bar, bold, dim, heading, note, pink, stacked } from '../look.mjs';
14
15
  import { readFile, readdir } from 'node:fs/promises';
15
16
  import { spawnSync } from 'node:child_process';
16
17
  import { createReadStream, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
@@ -183,18 +184,21 @@ function costReport(records) {
183
184
  // The same convention as the duration column beside it: the upper middle of an even count.
184
185
  const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
185
186
  console.log(
186
- `\n cost — at API list prices of ${PRICES_AS_OF}; ${measured.length} of ${records.length} runs have a token record` +
187
- (unpriced > 0 ? `, ${unpriced} on a model the price table does not know` : ''),
187
+ heading(
188
+ 'cost',
189
+ `at API list prices of ${PRICES_AS_OF}; ${measured.length} of ${records.length} runs have a token record` +
190
+ (unpriced > 0 ? `, ${unpriced} on a model the price table does not know` : ''),
191
+ ),
188
192
  );
189
- console.log(` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'median'.padStart(10)}${'total'.padStart(10)}${'share'.padStart(8)}`);
193
+ console.log(dim(` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'median'.padStart(10)}${'total'.padStart(10)}${'share'.padStart(8)}`));
190
194
  for (const [role, costs] of [...byRole].sort((a, b) => b[1].reduce((x, y) => x + y, 0) - a[1].reduce((x, y) => x + y, 0))) {
191
195
  const sum = costs.reduce((a, b) => a + b, 0);
192
196
  console.log(
193
- ` ${role.padEnd(20)}${String(costs.length).padStart(6)}${money(median(costs)).padStart(10)}` +
194
- `${money(sum).padStart(10)}${pct(sum, total).padStart(8)}`,
197
+ ` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(costs.length).padStart(6)}${money(median(costs)).padStart(10)}` +
198
+ `${bold(money(sum).padStart(10))}${pct(sum, total).padStart(8)} ${pink(bar(sum / total, 20))}`,
195
199
  );
196
200
  }
197
- console.log(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`);
201
+ console.log(bold(` ${'all stages'.padEnd(20)}${String([...byRole.values()].flat().length).padStart(6)}${''.padStart(10)}${money(total).padStart(10)}`));
198
202
  }
199
203
 
200
204
  /**
@@ -271,7 +275,7 @@ function modelReport(records) {
271
275
  const costs = runs.map((r) => (r.tokens ? costOf(r.tokens, r.usage_model) : null)).filter((c) => c !== null);
272
276
  const dates = runs.map((r) => String(r.ts).slice(0, 10)).sort();
273
277
  lines.push(
274
- ` ${(i === 0 ? role : '').padEnd(20)}${model.padEnd(26)}${String(runs.length).padStart(5)} run(s) ${dates[0]} → ${dates.at(-1)}` +
278
+ ` ${i === 0 ? `${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}` : ''.padEnd(20)}${model.padEnd(26)}${String(runs.length).padStart(5)} run(s) ${dim(`${dates[0]} → ${dates.at(-1)}`)}` +
275
279
  (clear.length ? ` loop-back ${pct(loops, clear.length)} of ${clear.length}` : ' no readable verdict') +
276
280
  (costs.length ? ` median ${money(median(costs))}` : ''),
277
281
  );
@@ -303,9 +307,9 @@ function modelReport(records) {
303
307
  }
304
308
 
305
309
  if (lines.length === 0 && drift.length === 0) return;
306
- console.log('\n models — a stage that ran on more than one model or effort level, one line each; different weeks, not an experiment');
310
+ console.log(heading('models', 'a stage that ran on more than one model or effort level, one line each; different weeks, not an experiment'));
307
311
  for (const line of lines) console.log(line);
308
- for (const line of drift) console.log(line);
312
+ for (const line of drift) console.log(amber(line));
309
313
  }
310
314
 
311
315
  /** The verdicts that close a pipeline cycle: the change passed its tests, reached preview, or cleared the audit. */
@@ -390,15 +394,19 @@ function proportionReport(records, stepLimit = () => null) {
390
394
  }
391
395
  if (built === 0) return;
392
396
  console.log(
393
- `\n proportion — ${built} cycle(s) that wrote code; a cycle closes at qa PASS, devops DEPLOYED or secops SECURE, or at the next design after code` +
394
- (unreadable > 0 ? `; ${unreadable} closed by a run whose verdict could not be read` : '') +
395
- (open > 0 ? `; ${open} still open when their session's record ends` : ''),
397
+ heading(
398
+ 'proportion',
399
+ `${built} cycle(s) that wrote code; a cycle closes at qa PASS, devops DEPLOYED or secops SECURE, or at the next design after code` +
400
+ (unreadable > 0 ? `; ${unreadable} closed by a run whose verdict could not be read` : '') +
401
+ (open > 0 ? `; ${open} still open when their session's record ends` : ''),
402
+ ),
396
403
  );
397
- console.log(` ${'largest write'.padEnd(16)}${'cycles'.padStart(8)}${'with a planner or architect'.padStart(30)}`);
404
+ console.log(dim(` ${'largest write'.padEnd(16)}${'cycles'.padStart(8)}${'with a planner or architect'.padStart(30)}`));
398
405
  for (const row of rows) {
399
- console.log(` ${row.label.padEnd(16)}${String(row.cycles).padStart(8)}${`${row.designed} (${pct(row.designed, row.cycles)})`.padStart(30)}`);
406
+ const designed = `${row.designed} (${pct(row.designed, row.cycles)})`.padStart(30);
407
+ console.log(` ${row.label.padEnd(16)}${String(row.cycles).padStart(8)}${row.cycles ? designed : dim(designed)} ${pink(bar(row.cycles ? row.designed / row.cycles : 0, 10))}`);
400
408
  }
401
- console.log(' A shape, not a verdict: a critical path is gated in full at any size, and one spec may cover sibling steps.');
409
+ console.log(dim(' A shape, not a verdict: a critical path is gated in full at any size, and one spec may cover sibling steps.'));
402
410
 
403
411
  // The one size the harness does limit: what one implementer run writes. A run re-reads the context it
404
412
  // has built on every turn, so its cost grows faster than its size — measured over two projects, the
@@ -413,7 +421,10 @@ function proportionReport(records, stepLimit = () => null) {
413
421
  const largest = over.reduce((a, b) => (b.files_touched > a.files_touched ? b : a));
414
422
  const read = typeof largest.tokens?.read === 'number' ? `, ${Math.round(largest.tokens.read / 1e6)}M tokens read from cache` : '';
415
423
  console.log(
416
- ` ${over.length} implementer run(s) wrote more files than one step may (${stepLimit(largest.project)}); the largest wrote ${largest.files_touched}${read}. The architect splits such a spec into steps.`,
424
+ note(
425
+ `${over.length} implementer run(s) wrote more files than one step may (${stepLimit(largest.project)}); the largest wrote ${largest.files_touched}${read}. The architect splits such a spec into steps.`,
426
+ 'warn',
427
+ ),
417
428
  );
418
429
  }
419
430
  }
@@ -551,34 +562,53 @@ export async function stats(argv, ctx) {
551
562
  const span = [records[0]?.ts, records.at(-1)?.ts].map((t) => String(t).slice(0, 10));
552
563
  const projectCount = new Set(records.map((r) => r.project)).size;
553
564
  console.log(
554
- ` ${records.length} dispatches · ${projectCount} project${projectCount === 1 ? '' : 's'} · ` +
565
+ ` ${projectCount === 1 ? `${bold(pink(projectName(records[0].project)))} · ` : ''}${bold(records.length)} dispatches · ${projectCount} project${projectCount === 1 ? '' : 's'} · ` +
555
566
  `${span[0]} → ${span[1]}` +
556
- (here ? ' (this project; --all for every project)' : '') +
557
- (skipped > 0 ? ` (${skipped} non-harness project${skipped === 1 ? '' : 's'} hidden, --all to include)` : '') +
558
- (held > 0 ? ` · ${held} dispatch(es) denied by a hook, not counted as runs` : '') +
559
- '\n',
567
+ (here ? dim(' (this project; --all for every project)') : '') +
568
+ (skipped > 0 ? dim(` (${skipped} non-harness project${skipped === 1 ? '' : 's'} hidden, --all to include)`) : '') +
569
+ (held > 0 ? ` · ${held} dispatch(es) denied by a hook, not counted as runs` : ''),
560
570
  );
571
+ console.log(heading('stages', 'what each ran, and what came of it'));
561
572
  console.log(
562
- ` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'verdict'.padStart(9)}${'loop-back'.padStart(11)}` +
563
- `${'rate'.padStart(7)}${'declared'.padStart(10)}${'unreadable'.padStart(12)}` +
564
- `${'median'.padStart(9)}`,
573
+ dim(
574
+ ` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'verdict'.padStart(9)}${'loop-back'.padStart(11)}` +
575
+ `${'rate'.padStart(7)}${'declared'.padStart(10)}${'unreadable'.padStart(12)}` +
576
+ `${'median'.padStart(9)}`,
577
+ ),
565
578
  );
566
579
 
567
580
  const rows = [...byRole.entries()].sort((a, b) => b[1].n - a[1].n);
581
+ // Each stage's runs as one bar, as long as its share of the busiest stage's: what went forward, what
582
+ // was sent back, and what said nothing readable (or is still running).
583
+ const busiest = Math.max(...rows.map(([, s]) => s.n), 1);
584
+ const zero = (text, n) => (n === 0 ? dim(text) : text);
568
585
  for (const [role, s] of rows) {
586
+ const runsBar = stacked(
587
+ [
588
+ { n: s.clear - s.loop, char: '█', paint: pink },
589
+ { n: s.loop, char: '▓', paint: amber },
590
+ { n: s.n - s.clear, char: '░', paint: dim },
591
+ ],
592
+ Math.max(1, Math.round((16 * s.n) / busiest)),
593
+ );
594
+ const declared = pct(s.declared, s.done).padStart(10);
595
+ const unread = pct(s.unclear, s.done).padStart(12);
569
596
  console.log(
570
- ` ${role.padEnd(20)}${String(s.n).padStart(6)}${String(s.clear).padStart(9)}` +
571
- `${String(s.loop).padStart(11)}${pct(s.loop, s.clear).padStart(7)}` +
572
- `${pct(s.declared, s.done).padStart(10)}${pct(s.unclear, s.done).padStart(12)}` +
573
- `${medianMin(s.durations).padStart(9)}`,
597
+ ` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(s.n).padStart(6)}${String(s.clear).padStart(9)}` +
598
+ `${zero(String(s.loop).padStart(11), s.loop)}${zero(pct(s.loop, s.clear).padStart(7), s.loop)}` +
599
+ `${s.declared < s.done ? amber(declared) : dim(declared)}${s.unclear > 0 ? amber(unread) : dim(unread)}` +
600
+ `${medianMin(s.durations).padStart(9)} ${runsBar}`,
574
601
  );
575
602
  }
603
+ console.log(dim(` ${' '.repeat(86)}█ forward ▓ sent back ░ no verdict read`));
576
604
 
577
605
  const unreadable = rows.reduce((a, [, s]) => a + s.unclear, 0);
578
606
  const done = rows.reduce((a, [, s]) => a + s.done, 0);
607
+ console.log('');
579
608
  console.log(
580
- `\n ${pct(unreadable, done)} of finished runs report no machine-readable verdict.` +
581
- ' Those stages need a verdict token on the report\'s first line.',
609
+ unreadable === 0
610
+ ? note("every finished run's verdict could be read.", 'ok')
611
+ : note(`${pct(unreadable, done)} of finished runs report no machine-readable verdict. Those stages need a verdict token on the report's first line.`, 'warn'),
582
612
  );
583
613
 
584
614
  // Whether the stages that send work back say what they send back. Counted only over reports read
@@ -587,8 +617,7 @@ export async function stats(argv, ctx) {
587
617
  if (named.length > 0) {
588
618
  const withIds = named.filter((r) => r.issues > 0).length;
589
619
  console.log(
590
- ` ${withIds} of ${named.length} declared loop-back(s) (${pct(withIds, named.length)}) named their issues` +
591
- ' on the `ISSUES` line under the verdict.',
620
+ note(`${withIds} of ${named.length} declared loop-back(s) (${pct(withIds, named.length)}) named their issues on the \`ISSUES\` line under the verdict.`, withIds < named.length ? 'warn' : 'ok'),
592
621
  );
593
622
  }
594
623
 
@@ -600,9 +629,11 @@ export async function stats(argv, ctx) {
600
629
  for (const r of withSkills) for (const k of r.skills) tally.set(k, (tally.get(k) ?? 0) + 1);
601
630
  const list = [...tally].sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ×${n}`);
602
631
  console.log(
603
- ` ${withSkills.length} of ${observable} runs with a readable transcript` +
604
- ` (${pct(withSkills.length, observable)}) invoked a Skill` +
605
- (list.length > 0 ? ` — ${list.join(', ')}` : ' — none at all'),
632
+ note(
633
+ `${withSkills.length} of ${observable} runs with a readable transcript (${pct(withSkills.length, observable)}) invoked a Skill` +
634
+ (list.length > 0 ? ` — ${list.join(', ')}` : ' — none at all'),
635
+ withSkills.length === 0 ? 'warn' : 'info',
636
+ ),
606
637
  );
607
638
  }
608
639
 
@@ -617,7 +648,8 @@ export async function stats(argv, ctx) {
617
648
  others.reduce((a, [, o]) => a + o.clear, 0),
618
649
  );
619
650
  console.log(
620
- ` ${role}: ${s.loop} loop-back(s) in ${s.clear} readable verdict(s) (${pct(s.loop, s.clear)}),` +
651
+ note(
652
+ `${role}: ${s.loop} loop-back(s) in ${s.clear} readable verdict(s) (${pct(s.loop, s.clear)}),` +
621
653
  ` against ${elsewhere} across the other gates — check whether it still gates anything.` +
622
654
  (s.n - s.clear > 0
623
655
  ? ` ${s.n - s.clear} further run(s) produced no readable verdict at all, so the rate may understate it.`
@@ -627,6 +659,8 @@ export async function stats(argv, ctx) {
627
659
  (s.declared < s.clear
628
660
  ? ` Only ${s.declared} of the ${s.clear} were declared by the stage; the rest were inferred from its report.`
629
661
  : ''),
662
+ 'warn',
663
+ ),
630
664
  );
631
665
  }
632
666
 
@@ -642,30 +676,31 @@ export async function stats(argv, ctx) {
642
676
  const newest = pills.dates.at(-1);
643
677
  const inWindow = pills.dates.filter((d) => d >= span[0] && d <= span[1]).length;
644
678
 
645
- console.log('\n learning');
679
+ console.log(heading('learning', 'what the loop-backs taught, and what became a rule'));
646
680
  if (pills.total === 0) {
647
- console.log(
648
- ` ${loopBacks} loop-back(s) and not one pill — either nothing was learned, or nothing was written down.`,
649
- );
681
+ console.log(note(`${loopBacks} loop-back(s) and not one pill — either nothing was learned, or nothing was written down.`, loopBacks > 0 ? 'warn' : 'info'));
650
682
  } else {
651
683
  console.log(
652
- // A share over 100% says nothing: the pills were written for something other than these loop-backs.
653
- ` ${loopBacks} loop-back(s) in the window → ${inWindow} pill(s) written${inWindow <= loopBacks ? ` (${pct(inWindow, loopBacks)})` : ''}` +
654
- ' — every loop-back is looked at for a lesson; `nina learn` shows which roles are owed one.' +
655
- (pills.undated > 0 ? ` ${pills.undated} pill(s) carry no date and cannot be placed.` : ''),
684
+ note(
685
+ // A share over 100% says nothing: the pills were written for something other than these loop-backs.
686
+ `${loopBacks} loop-back(s) in the window → ${inWindow} pill(s) written${inWindow <= loopBacks ? ` (${pct(inWindow, loopBacks)})` : ''}` +
687
+ ' — every loop-back is looked at for a lesson; `nina learn` shows which roles are owed one.' +
688
+ (pills.undated > 0 ? ` ${pills.undated} pill(s) carry no date and cannot be placed.` : ''),
689
+ ),
656
690
  );
657
691
  console.log(
658
692
  pills.retired === 0
659
- ? ` 0 of ${pills.total} pill(s) retired — no correction has ever graduated into a rule.`
660
- : ` ${pills.retired} of ${pills.total} pill(s) retired into a rule.`,
693
+ ? note(`0 of ${pills.total} pill(s) retired — no correction has ever graduated into a rule.`, 'warn')
694
+ : note(`${pills.retired} of ${pills.total} pill(s) retired into a rule.`, 'ok'),
661
695
  );
662
696
  if (newest) {
663
697
  const since = records.filter((r) => String(r.ts).slice(0, 10) > newest).length;
664
- if (since > 0) console.log(` ${since} dispatch(es) since the newest pill (${newest}).`);
698
+ if (since > 0) console.log(note(`${since} dispatch(es) since the newest pill (${newest}).`));
665
699
  }
666
700
  }
667
701
  for (const name of pills.unresolved) {
668
- console.log(` ${name} could not be located on disk, so its pills are not counted.`);
702
+ console.log(note(`${name} could not be located on disk, so its pills are not counted.`, 'warn'));
669
703
  }
704
+ console.log('');
670
705
  return 0;
671
706
  }