@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.
- package/README.md +4 -0
- package/bin/nina.mjs +29 -5
- package/package.json +1 -1
- package/releases/0.53.0/core/GAPS.md +125 -0
- package/releases/0.53.0/core/answered.json +68 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/qa.md +7 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.53.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.53.0/core/tree/.claude/agents/architect.md +129 -0
- package/releases/0.53.0/core/tree/.claude/agents/dba.md +117 -0
- package/releases/0.53.0/core/tree/.claude/agents/devops.md +118 -0
- package/releases/0.53.0/core/tree/.claude/agents/implementer.md +163 -0
- package/releases/0.53.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.53.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.53.0/core/tree/.claude/agents/qa.md +150 -0
- package/releases/0.53.0/core/tree/.claude/agents/reviewer.md +184 -0
- package/releases/0.53.0/core/tree/.claude/agents/secops.md +120 -0
- package/releases/0.53.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.53.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.53.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.53.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.53.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.53.0/core/tree/.claude/patterns.md +251 -0
- package/releases/0.53.0/core/tree/.claude/pills/README.md +150 -0
- package/releases/0.53.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.53.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.53.0/core/tree/.claude/router.md +202 -0
- package/releases/0.53.0/core/tree/.claude/templates/integration.md +46 -0
- package/releases/0.53.0/core/tree/CLAUDE.md +164 -0
- package/releases/0.53.0/core/tree/scripts/cost-watch.mjs +25 -0
- package/releases/0.53.0/core/tree/scripts/edit-guard.mjs +24 -0
- package/releases/0.53.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.53.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.53.0/core/vocabulary.json +13 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.53.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.53.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.53.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
- package/releases/0.53.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
- package/releases/0.53.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.53.0/surfaces/d1/tree/.claude/patterns.md +8 -0
- package/releases/0.53.0/surfaces/d1/tree/CLAUDE.md +2 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/patterns.md +22 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.53.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.53.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.53.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
- package/releases/0.53.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
- package/releases/0.53.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.53.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.53.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.53.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.53.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.53.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.53.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.53.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.53.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.53.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.53.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.53.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.53.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.53.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.53.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.53.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.53.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/commands/dashboard.mjs +33 -0
- package/src/commands/learn.mjs +51 -19
- package/src/commands/pills.mjs +55 -28
- package/src/commands/pipeline.mjs +62 -33
- package/src/commands/runs.mjs +14 -3
- package/src/commands/stats.mjs +199 -107
- package/src/dashboard.html +328 -0
- package/src/dashboard.mjs +185 -0
- package/src/office.mjs +48 -83
- package/src/serve.mjs +108 -0
package/src/commands/stats.mjs
CHANGED
|
@@ -222,9 +222,15 @@ const running = (r, now) => !r.verdict && now - Date.parse(r.ts) <= RUNNING_HOUR
|
|
|
222
222
|
*
|
|
223
223
|
* @param {object[]} records - The runs in the window.
|
|
224
224
|
*/
|
|
225
|
-
|
|
225
|
+
/**
|
|
226
|
+
* What each stage's runs cost at API list prices, as data: what `stats` prints and the dashboard's stats tab shows.
|
|
227
|
+
* Null when no run has a token record, or none has a price.
|
|
228
|
+
*
|
|
229
|
+
* @param {object[]} records - The rounds in the window.
|
|
230
|
+
*/
|
|
231
|
+
export function costData(records) {
|
|
226
232
|
const measured = records.filter((r) => r.tokens && typeof r.tokens === 'object');
|
|
227
|
-
if (measured.length === 0) return;
|
|
233
|
+
if (measured.length === 0) return null;
|
|
228
234
|
// A run's cost is its rounds', so a reviewer resumed twice is one run at the price of three rounds.
|
|
229
235
|
/** @type {Map<string, number[]>} */
|
|
230
236
|
const byRole = new Map();
|
|
@@ -240,25 +246,33 @@ function costReport(records) {
|
|
|
240
246
|
byRole.set(role, [...(byRole.get(role) ?? []), costs.reduce((a, b) => a + b, 0)]);
|
|
241
247
|
}
|
|
242
248
|
const total = [...byRole.values()].flat().reduce((a, b) => a + b, 0);
|
|
243
|
-
if (total === 0) return;
|
|
249
|
+
if (total === 0) return null;
|
|
250
|
+
const rows = [...byRole]
|
|
251
|
+
.map(([role, costs]) => ({ role, runs: costs.length, median: median(costs), total: costs.reduce((a, b) => a + b, 0) }))
|
|
252
|
+
.sort((a, b) => b.total - a.total);
|
|
253
|
+
return { pricesAsOf: PRICES_AS_OF, measuredRuns: runs.size, allRuns: byRun(records).size, unpriced, total, rows };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function costReport(records) {
|
|
257
|
+
const d = costData(records);
|
|
258
|
+
if (!d) return;
|
|
244
259
|
const money = (n) => `$${n >= 100 ? n.toFixed(0) : n.toFixed(2)}`;
|
|
245
260
|
// The same convention as the duration column beside it: the upper middle of an even count.
|
|
246
261
|
console.log(
|
|
247
262
|
heading(
|
|
248
263
|
'cost',
|
|
249
|
-
`at API list prices of ${
|
|
250
|
-
(unpriced > 0 ? `, ${unpriced} on a model the price table does not know` : ''),
|
|
264
|
+
`at API list prices of ${d.pricesAsOf}; ${d.measuredRuns} of ${d.allRuns} runs have a token record` +
|
|
265
|
+
(d.unpriced > 0 ? `, ${d.unpriced} on a model the price table does not know` : ''),
|
|
251
266
|
),
|
|
252
267
|
);
|
|
253
268
|
console.log(dim(` ${'stage'.padEnd(20)}${'runs'.padStart(6)}${'median'.padStart(10)}${'total'.padStart(10)}${'share'.padStart(8)}`));
|
|
254
|
-
for (const
|
|
255
|
-
const sum = costs.reduce((a, b) => a + b, 0);
|
|
269
|
+
for (const { role, runs, median: middle, total: sum } of d.rows) {
|
|
256
270
|
console.log(
|
|
257
|
-
` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(
|
|
258
|
-
`${bold(money(sum).padStart(10))}${pct(sum, total).padStart(8)} ${pink(bar(sum / total, 20))}`,
|
|
271
|
+
` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(runs).padStart(6)}${money(middle).padStart(10)}` +
|
|
272
|
+
`${bold(money(sum).padStart(10))}${pct(sum, d.total).padStart(8)} ${pink(bar(sum / d.total, 20))}`,
|
|
259
273
|
);
|
|
260
274
|
}
|
|
261
|
-
console.log(bold(` ${'all stages'.padEnd(20)}${String(
|
|
275
|
+
console.log(bold(` ${'all stages'.padEnd(20)}${String(d.rows.reduce((a, r) => a + r.runs, 0)).padStart(6)}${''.padStart(10)}${money(d.total).padStart(10)}`));
|
|
262
276
|
}
|
|
263
277
|
|
|
264
278
|
/**
|
|
@@ -270,30 +284,57 @@ function costReport(records) {
|
|
|
270
284
|
* @param {object[]} records - The rounds in the window.
|
|
271
285
|
*/
|
|
272
286
|
function contextReport(records) {
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
287
|
+
const d = contextData(records);
|
|
288
|
+
if (!d) return;
|
|
275
289
|
const k = (n) => (typeof n === 'number' ? `${Math.round(n / 1000)}k` : '—');
|
|
276
|
-
const chars = (xs) => (xs.length ? median(xs).toLocaleString('en-US') : '—');
|
|
277
290
|
console.log(heading('context', 'every turn re-reads the whole context: a round costs its turns times its size, and a resumed one starts where it ended; the prompt it was handed is in characters'));
|
|
278
291
|
console.log(dim(` ${'stage'.padEnd(20)}${'rounds'.padEnd(8)}${'n'.padStart(4)}${'turns'.padStart(7)}${'calls/turn'.padStart(12)}${'starts at'.padStart(11)}${'peak'.padStart(8)}${'prompt'.padStart(9)}`));
|
|
292
|
+
let previous = null;
|
|
293
|
+
for (const row of d.rows) {
|
|
294
|
+
const name = row.role !== previous ? pink(row.role) + ' '.repeat(Math.max(20 - row.role.length, 1)) : ' '.repeat(20);
|
|
295
|
+
previous = row.role;
|
|
296
|
+
console.log(
|
|
297
|
+
` ${name}${dim(row.label.padEnd(8))}${String(row.n).padStart(4)}${String(row.turns).padStart(7)}` +
|
|
298
|
+
`${row.callsPerTurn.toFixed(2).padStart(12)}${k(row.start).padStart(11)}${k(row.peak).padStart(8)}` +
|
|
299
|
+
`${(row.prompt === null ? '—' : row.prompt.toLocaleString('en-US')).padStart(9)}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Each stage's rounds and how they spent their context, as data (see `contextReport`): the first rounds of a run
|
|
306
|
+
* and the resumed ones apart. Null when no round recorded its turns.
|
|
307
|
+
*
|
|
308
|
+
* @param {object[]} records - The rounds in the window.
|
|
309
|
+
*/
|
|
310
|
+
export function contextData(records) {
|
|
311
|
+
const shaped = records.filter((r) => typeof r.turns === 'number' && r.turns > 0);
|
|
312
|
+
if (shaped.length === 0) return null;
|
|
279
313
|
const byRole = new Map();
|
|
280
314
|
for (const r of shaped) byRole.set(r.role, [...(byRole.get(r.role) ?? []), r]);
|
|
315
|
+
const rows = [];
|
|
281
316
|
for (const [role, rounds] of [...byRole].sort((a, b) => b[1].reduce((x, r) => x + r.turns, 0) - a[1].reduce((x, r) => x + r.turns, 0))) {
|
|
282
|
-
[
|
|
317
|
+
for (const [label, group] of [
|
|
283
318
|
['first', rounds.filter((r) => (r.round ?? 1) === 1)],
|
|
284
319
|
['resumed', rounds.filter((r) => (r.round ?? 1) > 1)],
|
|
285
|
-
]
|
|
286
|
-
if (group.length === 0)
|
|
320
|
+
]) {
|
|
321
|
+
if (group.length === 0) continue;
|
|
287
322
|
const turns = group.reduce((a, r) => a + r.turns, 0);
|
|
288
323
|
const calls = group.reduce((a, r) => a + (r.tool_calls ?? 0), 0);
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
324
|
+
const prompts = group.map((r) => r.prompt_chars).filter((n) => typeof n === 'number');
|
|
325
|
+
rows.push({
|
|
326
|
+
role,
|
|
327
|
+
label,
|
|
328
|
+
n: group.length,
|
|
329
|
+
turns: median(group.map((r) => r.turns)),
|
|
330
|
+
callsPerTurn: calls / turns,
|
|
331
|
+
start: median(group.map((r) => r.context_start ?? 0)),
|
|
332
|
+
peak: median(group.map((r) => r.context_peak ?? 0)),
|
|
333
|
+
prompt: prompts.length ? median(prompts) : null,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
296
336
|
}
|
|
337
|
+
return { rows };
|
|
297
338
|
}
|
|
298
339
|
|
|
299
340
|
/**
|
|
@@ -518,8 +559,44 @@ export function cyclesOf(records) {
|
|
|
518
559
|
* @param {object[]} records - The runs in the window.
|
|
519
560
|
*/
|
|
520
561
|
function proportionReport(records, stepLimit = () => null) {
|
|
562
|
+
const d = proportionData(records, stepLimit);
|
|
563
|
+
if (!d) return;
|
|
564
|
+
console.log(
|
|
565
|
+
heading(
|
|
566
|
+
'proportion',
|
|
567
|
+
`${d.built} cycle(s) that wrote code; a cycle closes at qa PASS, devops DEPLOYED or secops SECURE, or at the next design after code` +
|
|
568
|
+
(d.unreadable > 0 ? `; ${d.unreadable} closed by a run whose verdict could not be read` : '') +
|
|
569
|
+
(d.open > 0 ? `; ${d.open} still open when their session's record ends` : ''),
|
|
570
|
+
),
|
|
571
|
+
);
|
|
572
|
+
console.log(dim(` ${'largest write'.padEnd(16)}${'cycles'.padStart(8)}${'with a planner or architect'.padStart(30)}`));
|
|
573
|
+
for (const row of d.rows) {
|
|
574
|
+
const designed = `${row.designed} (${pct(row.designed, row.cycles)})`.padStart(30);
|
|
575
|
+
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))}`);
|
|
576
|
+
}
|
|
577
|
+
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.'));
|
|
578
|
+
if (d.over) {
|
|
579
|
+
const read = d.over.readM === null ? '' : `, ${d.over.readM}M tokens read from cache`;
|
|
580
|
+
console.log(
|
|
581
|
+
note(
|
|
582
|
+
`${d.over.count} implementer round(s) wrote more files than one step may (${d.over.limit}); the largest wrote ${d.over.largest}${read}. The architect splits such a spec into steps.`,
|
|
583
|
+
'warn',
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* The size of each change against the chain that carried it, as data (see `proportionReport`): cycles that wrote code
|
|
591
|
+
* by their largest write, how many had a design stage, and the implementer rounds that wrote more files than one step
|
|
592
|
+
* may. Null when no cycle wrote code that was counted.
|
|
593
|
+
*
|
|
594
|
+
* @param {object[]} records - The rounds in the window.
|
|
595
|
+
* @param {(project: string) => number|null} [stepLimit] - The most files one implementer run may write, by project.
|
|
596
|
+
*/
|
|
597
|
+
export function proportionData(records, stepLimit = () => null) {
|
|
521
598
|
const measured = records.filter((r) => WRITERS.has(r.role) && typeof r.files_touched === 'number');
|
|
522
|
-
if (measured.length === 0) return;
|
|
599
|
+
if (measured.length === 0) return null;
|
|
523
600
|
const cycles = cyclesOf(records);
|
|
524
601
|
const rows = [
|
|
525
602
|
{ label: '1–2 files', from: 1, to: 2 },
|
|
@@ -541,22 +618,7 @@ function proportionReport(records, stepLimit = () => null) {
|
|
|
541
618
|
row.cycles += 1;
|
|
542
619
|
if (cycle.some((r) => DESIGNERS.has(r.role))) row.designed += 1;
|
|
543
620
|
}
|
|
544
|
-
if (built === 0) return;
|
|
545
|
-
console.log(
|
|
546
|
-
heading(
|
|
547
|
-
'proportion',
|
|
548
|
-
`${built} cycle(s) that wrote code; a cycle closes at qa PASS, devops DEPLOYED or secops SECURE, or at the next design after code` +
|
|
549
|
-
(unreadable > 0 ? `; ${unreadable} closed by a run whose verdict could not be read` : '') +
|
|
550
|
-
(open > 0 ? `; ${open} still open when their session's record ends` : ''),
|
|
551
|
-
),
|
|
552
|
-
);
|
|
553
|
-
console.log(dim(` ${'largest write'.padEnd(16)}${'cycles'.padStart(8)}${'with a planner or architect'.padStart(30)}`));
|
|
554
|
-
for (const row of rows) {
|
|
555
|
-
const designed = `${row.designed} (${pct(row.designed, row.cycles)})`.padStart(30);
|
|
556
|
-
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))}`);
|
|
557
|
-
}
|
|
558
|
-
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.'));
|
|
559
|
-
|
|
621
|
+
if (built === 0) return null;
|
|
560
622
|
// The one size the harness does limit: what one implementer run writes. A run re-reads the context it
|
|
561
623
|
// has built on every turn, so its cost grows faster than its size — measured over two projects, the
|
|
562
624
|
// cache read per file held near 1M up to 19 files and was 6.9M in a 52-file run. Asked only of a project
|
|
@@ -566,16 +628,16 @@ function proportionReport(records, stepLimit = () => null) {
|
|
|
566
628
|
const limit = stepLimit(r.project);
|
|
567
629
|
return limit !== null && r.files_touched > limit;
|
|
568
630
|
});
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
),
|
|
577
|
-
|
|
578
|
-
}
|
|
631
|
+
const largest = over.length ? over.reduce((a, b) => (b.files_touched > a.files_touched ? b : a)) : null;
|
|
632
|
+
return {
|
|
633
|
+
rows: rows.map(({ label, cycles: n, designed }) => ({ label, cycles: n, designed })),
|
|
634
|
+
built,
|
|
635
|
+
unreadable,
|
|
636
|
+
open,
|
|
637
|
+
over: largest
|
|
638
|
+
? { count: over.length, limit: stepLimit(largest.project), largest: largest.files_touched, readM: typeof largest.tokens?.read === 'number' ? Math.round(largest.tokens.read / 1e6) : null }
|
|
639
|
+
: null,
|
|
640
|
+
};
|
|
579
641
|
}
|
|
580
642
|
|
|
581
643
|
/**
|
|
@@ -607,13 +669,74 @@ function stepLimitOf(ctx) {
|
|
|
607
669
|
};
|
|
608
670
|
}
|
|
609
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Each stage's rounds and what came of them, as data: what `stats` prints in its stages table and the dashboard's
|
|
674
|
+
* stats tab shows. Counted in rounds — the verdicts are the rounds', and the loop gate counts rounds — with the runs
|
|
675
|
+
* they belong to; and the rounds of agents outside the pipeline apart, which have no verdict to give.
|
|
676
|
+
*
|
|
677
|
+
* @param {object[]} records - The rounds in the window.
|
|
678
|
+
* @param {number} [now] - Against which a round with no report is still running or lost.
|
|
679
|
+
*/
|
|
680
|
+
export function stageData(records, now = Date.now()) {
|
|
681
|
+
/** @type {Map<string, {n:number, runs:Set<string>, done:number, lost:number, clear:number, loop:number, unclear:number, declared:number, durations:number[]}>} */
|
|
682
|
+
const byRole = new Map();
|
|
683
|
+
// Agents outside the pipeline — Claude Code's own, a project's own — have no verdict to give. Read as
|
|
684
|
+
// stages they were 100% unreadable and were told to add a verdict token their spec never asked for.
|
|
685
|
+
const outside = new Map();
|
|
686
|
+
for (const r of records.filter((r) => r.status !== 'denied')) {
|
|
687
|
+
if (!PIPELINE_ROLES.has(r.role)) {
|
|
688
|
+
outside.set(r.role, (outside.get(r.role) ?? 0) + 1);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (!byRole.has(r.role)) {
|
|
692
|
+
byRole.set(r.role, { n: 0, runs: new Set(), done: 0, lost: 0, clear: 0, loop: 0, unclear: 0, declared: 0, durations: [] });
|
|
693
|
+
}
|
|
694
|
+
const s = byRole.get(r.role);
|
|
695
|
+
s.n += 1;
|
|
696
|
+
s.runs.add(runOf(r));
|
|
697
|
+
if (!r.verdict && !running(r, now)) s.lost += 1;
|
|
698
|
+
if (r.verdict) s.done += 1;
|
|
699
|
+
if (r.verdict_source === 'declared' || r.verdict_source === 'handback') s.declared += 1;
|
|
700
|
+
if (r.verdict === 'UNCLEAR' || r.verdict === 'NONE') s.unclear += 1;
|
|
701
|
+
else if (r.verdict) {
|
|
702
|
+
s.clear += 1;
|
|
703
|
+
if (isLoopBack(r.verdict)) s.loop += 1;
|
|
704
|
+
}
|
|
705
|
+
if (typeof r.duration_s === 'number') s.durations.push(r.duration_s);
|
|
706
|
+
}
|
|
707
|
+
const rows = [...byRole.entries()]
|
|
708
|
+
.sort((a, b) => b[1].n - a[1].n)
|
|
709
|
+
.map(([role, s]) => {
|
|
710
|
+
const durations = s.durations.filter((v) => typeof v === 'number' && v >= 0);
|
|
711
|
+
return { role, runs: s.runs.size, rounds: s.n, done: s.done, lost: s.lost, clear: s.clear, loop: s.loop, unclear: s.unclear, declared: s.declared, median_s: durations.length ? median(durations) : null };
|
|
712
|
+
});
|
|
713
|
+
return { rows, outside: [...outside.entries()].sort((a, b) => b[1] - a[1]) };
|
|
714
|
+
}
|
|
715
|
+
|
|
610
716
|
/** The middle of a list of numbers — the upper one of two — or undefined for none. */
|
|
611
717
|
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
|
|
612
718
|
|
|
613
|
-
/**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
719
|
+
/**
|
|
720
|
+
* One project's measured history, as the dashboard's stats tab shows it: the same sections `stats` prints — its
|
|
721
|
+
* stages, what they cost, how they spent their context, and the size of each change against its chain — each from
|
|
722
|
+
* the function the report prints from. Null when nothing is recorded.
|
|
723
|
+
*
|
|
724
|
+
* @param {object[]} records - The project's rounds, oldest first.
|
|
725
|
+
* @param {{root: string}} ctx - CLI context, for the step limit its pinned release states.
|
|
726
|
+
*/
|
|
727
|
+
export function statsData(records, ctx) {
|
|
728
|
+
if (records.length === 0) return null;
|
|
729
|
+
const ran = records.filter((r) => r.status !== 'denied');
|
|
730
|
+
return {
|
|
731
|
+
runs: byRun(ran).size,
|
|
732
|
+
rounds: ran.length,
|
|
733
|
+
denied: records.length - ran.length,
|
|
734
|
+
span: [records[0]?.ts, records.at(-1)?.ts].map(localDay),
|
|
735
|
+
stages: stageData(records),
|
|
736
|
+
cost: costData(ran),
|
|
737
|
+
context: contextData(ran),
|
|
738
|
+
proportion: proportionData(ran, stepLimitOf(ctx)),
|
|
739
|
+
};
|
|
617
740
|
}
|
|
618
741
|
|
|
619
742
|
/**
|
|
@@ -687,40 +810,9 @@ export async function stats(argv, ctx) {
|
|
|
687
810
|
|
|
688
811
|
const skipped = new Set(all.map((r) => r.project)).size - new Set(records.map((r) => r.project)).size;
|
|
689
812
|
|
|
690
|
-
/**
|
|
691
|
-
* Per stage, counted in rounds: the verdicts are the rounds', and the loop gate counts rounds.
|
|
692
|
-
*
|
|
693
|
-
* @type {Map<string, {n:number, runs:Set<string>, done:number, lost:number, clear:number, loop:number,
|
|
694
|
-
* unclear:number, declared:number, durations:number[]}>}
|
|
695
|
-
*/
|
|
696
|
-
const byRole = new Map();
|
|
697
813
|
// A dispatch a hook denied never ran, so it is neither a run nor a missing verdict.
|
|
698
814
|
const held = records.filter((r) => r.status === 'denied').length;
|
|
699
|
-
const
|
|
700
|
-
// Agents outside the pipeline — Claude Code's own, a project's own — have no verdict to give. Read as
|
|
701
|
-
// stages they were 100% unreadable and were told to add a verdict token their spec never asked for.
|
|
702
|
-
const outside = new Map();
|
|
703
|
-
for (const r of records.filter((r) => r.status !== 'denied')) {
|
|
704
|
-
if (!PIPELINE_ROLES.has(r.role)) {
|
|
705
|
-
outside.set(r.role, (outside.get(r.role) ?? 0) + 1);
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
if (!byRole.has(r.role)) {
|
|
709
|
-
byRole.set(r.role, { n: 0, runs: new Set(), done: 0, lost: 0, clear: 0, loop: 0, unclear: 0, declared: 0, durations: [] });
|
|
710
|
-
}
|
|
711
|
-
const s = byRole.get(r.role);
|
|
712
|
-
s.n += 1;
|
|
713
|
-
s.runs.add(runOf(r));
|
|
714
|
-
if (!r.verdict && !running(r, now)) s.lost += 1;
|
|
715
|
-
if (r.verdict) s.done += 1;
|
|
716
|
-
if (r.verdict_source === 'declared' || r.verdict_source === 'handback') s.declared += 1;
|
|
717
|
-
if (r.verdict === 'UNCLEAR' || r.verdict === 'NONE') s.unclear += 1;
|
|
718
|
-
else if (r.verdict) {
|
|
719
|
-
s.clear += 1;
|
|
720
|
-
if (isLoopBack(r.verdict)) s.loop += 1;
|
|
721
|
-
}
|
|
722
|
-
if (typeof r.duration_s === 'number') s.durations.push(r.duration_s);
|
|
723
|
-
}
|
|
815
|
+
const { rows, outside } = stageData(records);
|
|
724
816
|
|
|
725
817
|
const span = [records[0]?.ts, records.at(-1)?.ts].map(localDay);
|
|
726
818
|
const projectCount = new Set(records.map((r) => r.project)).size;
|
|
@@ -741,38 +833,37 @@ export async function stats(argv, ctx) {
|
|
|
741
833
|
),
|
|
742
834
|
);
|
|
743
835
|
|
|
744
|
-
const rows = [...byRole.entries()].sort((a, b) => b[1].n - a[1].n);
|
|
745
836
|
// Each stage's runs as one bar, as long as its share of the busiest stage's: what went forward, what
|
|
746
837
|
// was sent back, and what said nothing readable (or is still running).
|
|
747
|
-
const busiest = Math.max(...rows.map((
|
|
838
|
+
const busiest = Math.max(...rows.map((s) => s.rounds), 1);
|
|
748
839
|
const zero = (text, n) => (n === 0 ? dim(text) : text);
|
|
749
|
-
for (const
|
|
840
|
+
for (const s of rows) {
|
|
750
841
|
const runsBar = stacked(
|
|
751
842
|
[
|
|
752
843
|
{ n: s.clear - s.loop, char: '█', paint: pink },
|
|
753
844
|
{ n: s.loop, char: '▓', paint: amber },
|
|
754
|
-
{ n: s.
|
|
845
|
+
{ n: s.rounds - s.clear, char: '░', paint: dim },
|
|
755
846
|
],
|
|
756
|
-
Math.max(1, Math.round((16 * s.
|
|
847
|
+
Math.max(1, Math.round((16 * s.rounds) / busiest)),
|
|
757
848
|
);
|
|
758
849
|
const declared = pct(s.declared, s.done).padStart(10);
|
|
759
850
|
const unread = pct(s.unclear + s.lost, s.done + s.lost).padStart(12);
|
|
760
851
|
console.log(
|
|
761
|
-
` ${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}${String(s.runs
|
|
852
|
+
` ${pink(s.role)}${' '.repeat(Math.max(20 - s.role.length, 1))}${String(s.runs).padStart(6)}${String(s.rounds).padStart(9)}` +
|
|
762
853
|
`${zero(String(s.loop).padStart(11), s.loop)}${zero(pct(s.loop, s.clear).padStart(7), s.loop)}` +
|
|
763
854
|
`${s.declared < s.done ? amber(declared) : dim(declared)}${s.unclear + s.lost > 0 ? amber(unread) : dim(unread)}` +
|
|
764
|
-
`${
|
|
855
|
+
`${(s.median_s === null ? '—' : `${(s.median_s / 60).toFixed(1)}m`).padStart(9)} ${runsBar}`,
|
|
765
856
|
);
|
|
766
857
|
}
|
|
767
858
|
console.log(dim(` ${' '.repeat(86)}█ forward ▓ sent back ░ no verdict read`));
|
|
768
859
|
|
|
769
|
-
const unreadable = rows.reduce((a,
|
|
770
|
-
const lost = rows.reduce((a,
|
|
771
|
-
const done = rows.reduce((a,
|
|
860
|
+
const unreadable = rows.reduce((a, s) => a + s.unclear, 0);
|
|
861
|
+
const lost = rows.reduce((a, s) => a + s.lost, 0);
|
|
862
|
+
const done = rows.reduce((a, s) => a + s.done, 0);
|
|
772
863
|
console.log('');
|
|
773
|
-
if (outside.
|
|
774
|
-
const named =
|
|
775
|
-
console.log(note(`${
|
|
864
|
+
if (outside.length > 0) {
|
|
865
|
+
const named = outside.map(([role, n]) => `${role} ×${n}`).join(', ');
|
|
866
|
+
console.log(note(`${outside.reduce((a, [, n]) => a + n, 0)} round(s) by agents outside the pipeline (${named}) — not stages, so no verdict is asked of them.`, 'info'));
|
|
776
867
|
}
|
|
777
868
|
if (unreadable === 0 && lost === 0) console.log(note("every finished round's verdict could be read.", 'ok'));
|
|
778
869
|
// The same share as the table's column: rounds that finished, or should have, with no verdict read.
|
|
@@ -819,20 +910,21 @@ export async function stats(argv, ctx) {
|
|
|
819
910
|
|
|
820
911
|
// A gate that never stops anything is either upstream quality or a rubber stamp, and the
|
|
821
912
|
// rate alone cannot say which — but it can say which gate to go and look at.
|
|
822
|
-
const gates = rows.filter((
|
|
823
|
-
for (const
|
|
913
|
+
const gates = rows.filter((s) => GATES.has(s.role));
|
|
914
|
+
for (const s of gates) {
|
|
915
|
+
const { role } = s;
|
|
824
916
|
if (s.clear < GATE_SAMPLE || s.loop / s.clear >= GATE_FLOOR) continue;
|
|
825
|
-
const others = gates.filter((
|
|
917
|
+
const others = gates.filter((o) => o.role !== role);
|
|
826
918
|
const elsewhere = pct(
|
|
827
|
-
others.reduce((a,
|
|
828
|
-
others.reduce((a,
|
|
919
|
+
others.reduce((a, o) => a + o.loop, 0),
|
|
920
|
+
others.reduce((a, o) => a + o.clear, 0),
|
|
829
921
|
);
|
|
830
922
|
console.log(
|
|
831
923
|
note(
|
|
832
924
|
`${role}: ${s.loop} loop-back(s) in ${s.clear} readable verdict(s) (${pct(s.loop, s.clear)}),` +
|
|
833
925
|
` against ${elsewhere} across the other gates — check whether it still gates anything.` +
|
|
834
|
-
(s.
|
|
835
|
-
? ` ${s.
|
|
926
|
+
(s.rounds - s.clear > 0
|
|
927
|
+
? ` ${s.rounds - s.clear} further round(s) produced no readable verdict at all, so the rate may understate it.`
|
|
836
928
|
: '') +
|
|
837
929
|
// A rate built from verdicts a parser guessed at is not the same claim as one built
|
|
838
930
|
// from verdicts the stage declared, and the difference decides whether it can be acted on.
|
|
@@ -852,7 +944,7 @@ export async function stats(argv, ctx) {
|
|
|
852
944
|
// What the pipeline learned, against what it had to learn from. A loop-back is the raw
|
|
853
945
|
// material and a pill is the product, so the two numbers belong on the same screen: the
|
|
854
946
|
// rule that turns one into the other is the only rule here that nothing else can check.
|
|
855
|
-
const loopBacks = rows.reduce((a,
|
|
947
|
+
const loopBacks = rows.reduce((a, s) => a + s.loop, 0);
|
|
856
948
|
const pills = await harvest([...new Set(records.map((r) => r.project))]);
|
|
857
949
|
const newest = pills.dates.at(-1);
|
|
858
950
|
const inWindow = pills.dates.filter((d) => d >= span[0] && d <= span[1]).length;
|