@xhulz/nina 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +4 -0
  2. package/bin/nina.mjs +29 -5
  3. package/package.json +1 -1
  4. package/releases/0.53.0/core/GAPS.md +125 -0
  5. package/releases/0.53.0/core/answered.json +68 -0
  6. package/releases/0.53.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  7. package/releases/0.53.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  8. package/releases/0.53.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  9. package/releases/0.53.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  10. package/releases/0.53.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  11. package/releases/0.53.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  12. package/releases/0.53.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  13. package/releases/0.53.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  14. package/releases/0.53.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  15. package/releases/0.53.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  16. package/releases/0.53.0/core/tree/.claude/agents/architect.md +129 -0
  17. package/releases/0.53.0/core/tree/.claude/agents/dba.md +117 -0
  18. package/releases/0.53.0/core/tree/.claude/agents/devops.md +118 -0
  19. package/releases/0.53.0/core/tree/.claude/agents/implementer.md +163 -0
  20. package/releases/0.53.0/core/tree/.claude/agents/integration-tester.md +168 -0
  21. package/releases/0.53.0/core/tree/.claude/agents/planner.md +132 -0
  22. package/releases/0.53.0/core/tree/.claude/agents/qa.md +150 -0
  23. package/releases/0.53.0/core/tree/.claude/agents/reviewer.md +184 -0
  24. package/releases/0.53.0/core/tree/.claude/agents/secops.md +120 -0
  25. package/releases/0.53.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  26. package/releases/0.53.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  27. package/releases/0.53.0/core/tree/.claude/agents-overview.md +64 -0
  28. package/releases/0.53.0/core/tree/.claude/frontend.md +10 -0
  29. package/releases/0.53.0/core/tree/.claude/graph.md +75 -0
  30. package/releases/0.53.0/core/tree/.claude/patterns.md +251 -0
  31. package/releases/0.53.0/core/tree/.claude/pills/README.md +150 -0
  32. package/releases/0.53.0/core/tree/.claude/pipeline.md +101 -0
  33. package/releases/0.53.0/core/tree/.claude/retrieval.md +103 -0
  34. package/releases/0.53.0/core/tree/.claude/router.md +202 -0
  35. package/releases/0.53.0/core/tree/.claude/templates/integration.md +46 -0
  36. package/releases/0.53.0/core/tree/CLAUDE.md +164 -0
  37. package/releases/0.53.0/core/tree/scripts/cost-watch.mjs +25 -0
  38. package/releases/0.53.0/core/tree/scripts/edit-guard.mjs +24 -0
  39. package/releases/0.53.0/core/tree/scripts/harness-check.mjs +71 -0
  40. package/releases/0.53.0/core/tree/scripts/loop-gate.mjs +19 -0
  41. package/releases/0.53.0/core/vocabulary.json +13 -0
  42. package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  43. package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  44. package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  45. package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  46. package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  47. package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  48. package/releases/0.53.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  49. package/releases/0.53.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  50. package/releases/0.53.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  51. package/releases/0.53.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  52. package/releases/0.53.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  53. package/releases/0.53.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  54. package/releases/0.53.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  55. package/releases/0.53.0/surfaces/d1/tree/CLAUDE.md +2 -0
  56. package/releases/0.53.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  57. package/releases/0.53.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  58. package/releases/0.53.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  59. package/releases/0.53.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  60. package/releases/0.53.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  61. package/releases/0.53.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  62. package/releases/0.53.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  63. package/releases/0.53.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  64. package/releases/0.53.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  65. package/releases/0.53.0/surfaces/db/tree/.claude/graph.md +8 -0
  66. package/releases/0.53.0/surfaces/db/tree/.claude/patterns.md +22 -0
  67. package/releases/0.53.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  68. package/releases/0.53.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  69. package/releases/0.53.0/surfaces/db/tree/.claude/router.md +4 -0
  70. package/releases/0.53.0/surfaces/db/tree/CLAUDE.md +17 -0
  71. package/releases/0.53.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  72. package/releases/0.53.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  73. package/releases/0.53.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  74. package/releases/0.53.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  75. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  76. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  77. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  78. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  79. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  80. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  81. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  82. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  83. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  84. package/releases/0.53.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  85. package/releases/0.53.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
  86. package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  87. package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  88. package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  89. package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  90. package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  91. package/releases/0.53.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  92. package/releases/0.53.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  93. package/releases/0.53.0/surfaces/frontend/tree/.claude/router.md +14 -0
  94. package/releases/0.53.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  95. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  96. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  97. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  98. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  99. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  100. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  101. package/releases/0.53.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  102. package/releases/0.53.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  103. package/releases/0.53.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  104. package/releases/0.53.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  105. package/releases/0.53.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  106. package/releases/0.53.0/surfaces/integrations/tree/.claude/router.md +6 -0
  107. package/releases/0.53.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  108. package/releases/0.53.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  109. package/releases/0.53.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  110. package/releases/0.53.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  111. package/releases/0.53.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  112. package/releases/0.53.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  113. package/releases/0.53.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  114. package/releases/0.53.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  115. package/releases/0.53.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  116. package/releases/0.53.0/surfaces/money/tree/.claude/graph.md +2 -0
  117. package/releases/0.53.0/surfaces/money/tree/.claude/patterns.md +49 -0
  118. package/releases/0.53.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  119. package/releases/0.53.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  120. package/releases/0.53.0/surfaces/money/tree/CLAUDE.md +11 -0
  121. package/releases/0.53.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  122. package/releases/0.53.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  123. package/releases/0.53.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  124. package/releases/0.53.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  125. package/releases/0.53.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  126. package/releases/0.53.0/surfaces/pii/tree/CLAUDE.md +5 -0
  127. package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  128. package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  129. package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  130. package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  131. package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  132. package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  133. package/releases/0.53.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  134. package/releases/0.53.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  135. package/src/commands/dashboard.mjs +33 -0
  136. package/src/commands/learn.mjs +51 -19
  137. package/src/commands/pills.mjs +55 -28
  138. package/src/commands/pipeline.mjs +62 -33
  139. package/src/commands/runs.mjs +14 -3
  140. package/src/commands/stats.mjs +199 -107
  141. package/src/dashboard.html +328 -0
  142. package/src/dashboard.mjs +185 -0
  143. package/src/office.mjs +48 -83
  144. package/src/serve.mjs +108 -0
@@ -336,43 +336,24 @@ export function validate(pill, text, roles, today) {
336
336
  }
337
337
 
338
338
  /**
339
- * `nina pills`.
339
+ * A project's pills, checked: what `nina pills` prints and the dashboard's pills tab shows — each pill's stage, status,
340
+ * count and trigger, and the problems and notes over the whole corpus. Null when the project has composed no agents.
340
341
  *
341
- * @param {string[]} argv - Command arguments; `--project <dir>` selects the target.
342
- * @param {{root: string}} [ctx] - CLI context; `root` is the NINA install directory.
343
- * @returns {Promise<number>} Process exit code.
342
+ * @param {string} target - The project.
343
+ * @param {{root?: string}} [ctx] - CLI context: where the layers are, to say where a recurring lesson belongs.
344
344
  */
345
- export async function pills(argv, ctx) {
346
- const target = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : '.');
347
- const quiet = argv.includes('--quiet');
345
+ export async function pillsData(target, ctx) {
348
346
  const pillsDir = join(target, '.claude', 'pills');
349
347
  const agentsDir = join(target, '.claude', 'agents');
350
-
351
- if (!existsSync(agentsDir)) {
352
- console.error(` no .claude/agents under ${target} — run \`nina compose\` first.\n`);
353
- return 1;
354
- }
348
+ if (!existsSync(agentsDir)) return null;
355
349
  const roles = (await readdir(agentsDir))
356
350
  .filter((f) => f.endsWith('.md'))
357
351
  .map((f) => f.replace(/\.md$/, ''))
358
352
  .sort();
359
-
360
353
  const today = new Date().toISOString().slice(0, 10);
361
- if (argv.includes('--tidy')) {
362
- const moved = await tidyRetired(target);
363
- for (const m of moved) console.log(` moved ${m} → ${RETIRED}/${m}`);
364
- console.log(moved.length ? `\npills: ${moved.length} retired pill(s) moved where no agent opens them\n` : 'pills: no retired pill where agents read\n');
365
- return 0;
366
- }
367
354
  const files = existsSync(pillsDir) ? await pillFiles(pillsDir) : [];
368
355
 
369
- // The directory itself is composed, so its presence says nothing. An empty corpus is the
370
- // normal state of a new project and the suspicious state of an old one — say which, and
371
- // leave the judgement to whoever knows how long the pipeline has been running.
372
- if (files.length === 0) {
373
- if (!quiet) console.log(` no pill written yet under ${target} — either nothing has been learned the expensive way, or nothing was written down.\n`);
374
- return 0;
375
- }
356
+ if (files.length === 0) return { files: 0, active: 0, retired: 0, problems: [], notes: [], pills: [] };
376
357
  const problems = [];
377
358
  const notes = [];
378
359
  const active = [];
@@ -404,8 +385,21 @@ export async function pills(argv, ctx) {
404
385
  let resolved = 0;
405
386
  let cited = 0;
406
387
 
388
+ const each = [];
407
389
  for (const pill of files) {
408
390
  const result = validate(pill, await readFile(pill.path, 'utf8'), roles, today);
391
+ const f = result.fields ?? {};
392
+ each.push({
393
+ label: result.label,
394
+ status: f.status ?? null,
395
+ roles: list(f.applies_to),
396
+ occurrences: Number(f.occurrences ?? 1) || 1,
397
+ date: f.date ?? null,
398
+ last_seen: f.last_seen ?? null,
399
+ severity: f.severity ?? null,
400
+ trigger: f.trigger ?? null,
401
+ problems: result.problems.length,
402
+ });
409
403
  problems.push(...result.problems);
410
404
  notes.push(...result.notes);
411
405
  if (pill.dir && pill.dir !== 'shared' && pill.dir !== RETIRED) taught.add(pill.dir);
@@ -478,14 +472,47 @@ export async function pills(argv, ctx) {
478
472
  const dated = active.map((p) => p.date).filter(Boolean).sort();
479
473
  if (dated.length > 0) notes.push(`oldest active pill is ${dated[0]}, newest ${dated[dated.length - 1]}`);
480
474
 
475
+ return { files: files.length, active: active.length, retired, problems, notes, pills: each };
476
+ }
477
+
478
+ /**
479
+ * `nina pills`.
480
+ *
481
+ * @param {string[]} argv - Command arguments; `--project <dir>` selects the target.
482
+ * @param {{root: string}} [ctx] - CLI context; `root` is the NINA install directory.
483
+ * @returns {Promise<number>} Process exit code.
484
+ */
485
+ export async function pills(argv, ctx) {
486
+ const target = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : '.');
487
+ const quiet = argv.includes('--quiet');
488
+ const agentsDir = join(target, '.claude', 'agents');
489
+
490
+ if (!existsSync(agentsDir)) {
491
+ console.error(` no .claude/agents under ${target} — run \`nina compose\` first.\n`);
492
+ return 1;
493
+ }
494
+ if (argv.includes('--tidy')) {
495
+ const moved = await tidyRetired(target);
496
+ for (const m of moved) console.log(` moved ${m} → ${RETIRED}/${m}`);
497
+ console.log(moved.length ? `\npills: ${moved.length} retired pill(s) moved where no agent opens them\n` : 'pills: no retired pill where agents read\n');
498
+ return 0;
499
+ }
500
+ const { files, active, retired, problems, notes } = await pillsData(target, ctx);
501
+ // The directory itself is composed, so its presence says nothing. An empty corpus is the
502
+ // normal state of a new project and the suspicious state of an old one — say which, and
503
+ // leave the judgement to whoever knows how long the pipeline has been running.
504
+ if (files === 0) {
505
+ if (!quiet) console.log(` no pill written yet under ${target} — either nothing has been learned the expensive way, or nothing was written down.\n`);
506
+ return 0;
507
+ }
481
508
  if (!(quiet && problems.length === 0)) {
482
- console.log(` ${files.length} pill(s): ${active.length} active, ${retired} retired\n`);
509
+ console.log(` ${files} pill(s): ${active} active, ${retired} retired\n`);
483
510
  for (const p of problems) console.log(` ✗ ${p}`);
484
511
  for (const n of notes) console.log(` · ${n}`);
485
512
  if (problems.length === 0 && notes.length === 0) console.log(' ✓ every pill is well formed, placed where it is read, and carries evidence');
486
513
  console.log(
487
514
  problems.length === 0
488
- ? `\npills: all ${files.length} well formed${notes.length > 0 ? ` (${notes.length} note(s))` : ''}\n`
515
+ ? `\npills: all ${files} well formed${notes.length > 0 ? ` (${notes.length} note(s))` : ''}\n`
489
516
  : `\npills: ${problems.length} problem(s)\n`,
490
517
  );
491
518
  }
@@ -165,6 +165,54 @@ export function historyOf(records, since) {
165
165
  return byRole;
166
166
  }
167
167
 
168
+ /**
169
+ * A composed pipeline, as data: what `nina pipeline` draws and the dashboard's pipeline tab shows — the line and where
170
+ * it ends, the gates, the stages that run as several agents, who sends work back to whom and how many rounds per
171
+ * issue, and each stage's model, effort, skills and what it ran since `since`; the project's own agents; and which
172
+ * chain each task shape takes. Null when the project has no composed graph.
173
+ *
174
+ * @param {string} dir - The project.
175
+ * @param {{since?: string}} [options] - The first day of the history each stage is measured over.
176
+ */
177
+ export function pipelineData(dir, { since } = {}) {
178
+ const graphPath = join(dir, '.claude', 'graph.md');
179
+ if (!existsSync(graphPath)) return null;
180
+ const graphText = readFileSync(graphPath, 'utf8');
181
+ const graph = parseGraph(graphText);
182
+ const words = stageWords(graphText);
183
+ const shape = shapeOf(graph);
184
+ const claude = join(dir, 'CLAUDE.md');
185
+ const profile = readProfile(dir).profile ?? {};
186
+ const agentsDir = join(dir, '.claude', 'agents');
187
+ const specs = new Map();
188
+ for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
189
+ specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
190
+ }
191
+ const from = since || localDay(new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString());
192
+ const records = projectRecords(dir);
193
+ const history = records ? historyOf(records, from) : null;
194
+ return {
195
+ project: basename(dir),
196
+ core: profile.core ?? null,
197
+ surfaces: (profile.surfaces ?? []).slice().sort(),
198
+ graph,
199
+ line: shape.line,
200
+ ends: shape.ends,
201
+ gates: shape.gates,
202
+ many: [...(graph.many ?? new Map())].map(([stage, { when }]) => ({ stage, when })),
203
+ back: shape.back,
204
+ since: from,
205
+ measured: history !== null,
206
+ stages: [...graph.stages].map((stage) => {
207
+ const spec = specs.get(stage) ?? '';
208
+ const front = frontmatter(spec) ?? {};
209
+ return { stage, model: front.model ?? null, effort: front.effort ?? null, words: words.get(stage) ?? '', history: history?.get(stage) ?? null, skills: [...required(spec).skills].sort() };
210
+ }),
211
+ own: [...specs.keys()].filter((role) => !graph.stages.has(role)).map((role) => ({ role, description: (frontmatter(specs.get(role)) ?? {}).description ?? '' })),
212
+ shapes: existsSync(claude) ? chainsByShape(readFileSync(claude, 'utf8')) : [],
213
+ };
214
+ }
215
+
168
216
  /**
169
217
  * Runs the command.
170
218
  *
@@ -200,12 +248,9 @@ export async function pipeline(argv) {
200
248
  }
201
249
  return viewOffice({ target: dir, wanted: arg('--session') || undefined, replay: argv.includes('--replay'), port: port === null ? undefined : Number(port), open: argv.includes('--open') });
202
250
  }
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')) : [];
251
+ const data = pipelineData(dir, { since: arg('--since') || undefined });
252
+ const { graph, shapes } = data;
253
+ const shape = { line: data.line, ends: data.ends, gates: data.gates, back: data.back };
209
254
 
210
255
  // `--for`: one shape's chain, and only the stages on it.
211
256
  const query = arg('--for');
@@ -232,9 +277,7 @@ export async function pipeline(argv) {
232
277
  const padName = (s, n) => `${name(s)}${' '.repeat(Math.max(n - s.length, 1))}`;
233
278
 
234
279
  // A pipeline composed by hand draws all the same; only the header has less to say.
235
- const profile = readProfile(dir).profile ?? {};
236
- const surfaces = (profile.surfaces ?? []).slice().sort();
237
- console.log(` pipeline · ${basename(dir)}${profile.core ? ` · core ${profile.core}` : ''}${surfaces.length ? ` · surfaces ${surfaces.join(', ')}` : ''}`);
280
+ console.log(` pipeline · ${data.project}${data.core ? ` · core ${data.core}` : ''}${data.surfaces.length ? ` · surfaces ${data.surfaces.join(', ')}` : ''}`);
238
281
 
239
282
  // The stages this drawing covers: all of them, or those the chosen shape's chain names.
240
283
  const onChain = chosen ? (chosen.chain.match(/[a-z][a-z-]*/g) ?? []).filter((w) => graph.stages.has(w)) : null;
@@ -300,30 +343,18 @@ export async function pipeline(argv) {
300
343
  }
301
344
 
302
345
  // Each stage: what it does, on which model and effort, with which skills, and what it did lately.
303
- const agentsDir = join(dir, '.claude', 'agents');
304
- const specs = new Map();
305
- for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
306
- specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
307
- }
308
- const since = arg('--since') || localDay(new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString());
309
- const runs = projectRecords(dir);
310
- const history = runs ? historyOf(runs, since) : null;
311
- const stages = [...graph.stages].filter((s) => covered.has(s));
312
- const col = Math.max(...stages.map((s) => s.length), 10) + 2;
313
- console.log(`\n ${dim(history ? `stages · what each ran since ${since}, from the measured history` : 'stages · no measured history for this project yet')}`);
314
- for (const stage of stages) {
315
- const spec = specs.get(stage) ?? '';
316
- const front = frontmatter(spec) ?? {};
317
- const on = [front.model, front.effort].filter(Boolean).join(' · ') || 'no spec';
318
- console.log(` ${padName(stage, col)}${on.padEnd(26)}${dim(fit(words.get(stage) ?? '', width - col - 32))}`);
319
- if (history) {
320
- const h = history.get(stage);
346
+ const stages = data.stages.filter((s) => covered.has(s.stage));
347
+ const col = Math.max(...stages.map((s) => s.stage.length), 10) + 2;
348
+ console.log(`\n ${dim(data.measured ? `stages · what each ran since ${data.since}, from the measured history` : 'stages · no measured history for this project yet')}`);
349
+ for (const { stage, model, effort, words, history: h, skills } of stages) {
350
+ const on = [model, effort].filter(Boolean).join(' · ') || 'no spec';
351
+ console.log(` ${padName(stage, col)}${on.padEnd(26)}${dim(fit(words, width - col - 32))}`);
352
+ if (data.measured) {
321
353
  const said = !h
322
354
  ? 'no run'
323
355
  : `${h.runs} run(s)${h.rounds > h.runs ? ` in ${h.rounds} rounds` : ''}${h.read === 0 ? ', no verdict read' : `, sent back ${h.back} of ${h.read} (${Math.round((100 * h.back) / h.read)}%)`}`;
324
356
  console.log(` ${' '.repeat(col)}${dim('ran ')}${said}`);
325
357
  }
326
- const skills = [...required(spec).skills].sort();
327
358
  if (skills.length > 0) {
328
359
  const lines = [];
329
360
  let current = '';
@@ -341,12 +372,10 @@ export async function pipeline(argv) {
341
372
 
342
373
  if (!chosen) {
343
374
  // An agent the project wrote for itself is not a stage, and the graph does not route to it.
344
- const own = [...specs.keys()].filter((role) => !graph.stages.has(role));
345
- if (own.length > 0) {
375
+ if (data.own.length > 0) {
346
376
  console.log(`\n ${dim("the project's own agents, outside the pipeline")}`);
347
- for (const role of own) {
348
- const front = frontmatter(specs.get(role)) ?? {};
349
- console.log(` ${role}${' '.repeat(Math.max(col - role.length, 2))}${dim(fit(front.description ?? '', width - col - 6))}`);
377
+ for (const { role, description } of data.own) {
378
+ console.log(` ${role}${' '.repeat(Math.max(col - role.length, 2))}${dim(fit(description, width - col - 6))}`);
350
379
  }
351
380
  }
352
381
  if (shapes.length > 0) {
@@ -66,12 +66,25 @@ function summarize(cycle) {
66
66
  unpriced,
67
67
  name: String(rounds[0]?.desc ?? '').trim() || '(no description)',
68
68
  how: ENDS[cycle.end](rounds.at(-1)),
69
+ end_kind: cycle.end,
69
70
  stages: [...stages.values()].map((s) => ({ ...s, runs: s.runs.size })),
70
71
  files: files.length ? Math.max(...files) : null,
71
72
  peak: peaks.length ? Math.max(...peaks) : null,
72
73
  };
73
74
  }
74
75
 
76
+ /**
77
+ * A project's pieces of work, newest first: what `nina runs` prints and the dashboard's tasks tab shows.
78
+ *
79
+ * @param {object[]} records - The project's measured rounds.
80
+ * @returns {ReturnType<typeof summarize>[]}
81
+ */
82
+ export function taskList(records) {
83
+ return cyclesOf(records.filter((r) => r.status !== 'denied'))
84
+ .map(summarize)
85
+ .sort((a, b) => b.start.localeCompare(a.start));
86
+ }
87
+
75
88
  /** A duration in minutes, as hours and minutes. */
76
89
  const span = (m) => (m === null ? '—' : m >= 60 ? `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}` : `${m}m`);
77
90
 
@@ -103,9 +116,7 @@ export async function runs(argv) {
103
116
  );
104
117
  return 1;
105
118
  }
106
- const cycles = cyclesOf(records)
107
- .map(summarize)
108
- .sort((a, b) => b.start.localeCompare(a.start));
119
+ const cycles = taskList(records);
109
120
  const shown = cycles.slice(0, last);
110
121
  console.log(
111
122
  ` ${bold(pink(projectName(records[0].project)))} · ${cycles.length} cycle(s)` +