@xhulz/nina 0.28.21 → 0.29.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 +2 -2
- package/package.json +1 -1
- package/releases/0.29.0/core/GAPS.md +125 -0
- package/releases/0.29.0/core/answered.json +29 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.29.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.29.0/core/tree/.claude/agents/architect.md +125 -0
- package/releases/0.29.0/core/tree/.claude/agents/dba.md +107 -0
- package/releases/0.29.0/core/tree/.claude/agents/devops.md +113 -0
- package/releases/0.29.0/core/tree/.claude/agents/implementer.md +155 -0
- package/releases/0.29.0/core/tree/.claude/agents/integration-tester.md +160 -0
- package/releases/0.29.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.29.0/core/tree/.claude/agents/qa.md +146 -0
- package/releases/0.29.0/core/tree/.claude/agents/reviewer.md +180 -0
- package/releases/0.29.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.29.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.29.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.29.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.29.0/core/tree/.claude/graph.md +73 -0
- package/releases/0.29.0/core/tree/.claude/patterns.md +212 -0
- package/releases/0.29.0/core/tree/.claude/pills/README.md +145 -0
- package/releases/0.29.0/core/tree/.claude/pipeline.md +102 -0
- package/releases/0.29.0/core/tree/.claude/retrieval.md +104 -0
- package/releases/0.29.0/core/tree/.claude/router.md +201 -0
- package/releases/0.29.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.29.0/core/tree/CLAUDE.md +155 -0
- package/releases/0.29.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.29.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.29.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.29.0/core/vocabulary.json +12 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.29.0/surfaces/blockchain/tree/.claude/router.md +9 -0
- package/releases/0.29.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/architect.md +8 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/dba.md +84 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/devops.md +14 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/implementer.md +5 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/patterns.md +29 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.29.0/surfaces/db/tree/.claude/router.md +10 -0
- package/releases/0.29.0/surfaces/db/tree/CLAUDE.md +23 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/.claude/router.md +21 -0
- package/releases/0.29.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/patterns.md +15 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/retrieval.md +5 -0
- package/releases/0.29.0/surfaces/frontend/tree/.claude/router.md +17 -0
- package/releases/0.29.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/graph.md +9 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.29.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.29.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.29.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.29.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.29.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.29.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.29.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.29.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.29.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.29.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/src/agentrun.mjs +12 -4
- package/src/commands/compose.mjs +45 -1
- package/src/commands/export.mjs +15 -4
- package/src/commands/gate.mjs +91 -8
- package/src/commands/init.mjs +34 -15
- package/src/commands/learn.mjs +22 -9
- package/src/commands/pipeline.mjs +17 -42
- package/src/commands/snapshot.mjs +35 -4
- package/src/commands/stats.mjs +96 -53
- package/src/commands/upgrade.mjs +3 -0
- package/src/deep.mjs +4 -31
- package/src/detectors.mjs +33 -4
- package/src/gate.mjs +1 -1
- package/src/langfuse.mjs +2 -0
- package/src/transcripts.mjs +232 -81
package/src/transcripts.mjs
CHANGED
|
@@ -270,6 +270,85 @@ const LOOP_BACK = new Set(['REJECTED', 'BLOCKED', 'FAIL', 'FAILED']);
|
|
|
270
270
|
*/
|
|
271
271
|
export const isLoopBack = (verdict) => LOOP_BACK.has(verdict);
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The run a record is a round of: the dispatch id of its first round. A run is one agent, from the Agent
|
|
275
|
+
* call that started it to the last report it handed back; a round is one pass of it that ends in a report —
|
|
276
|
+
* the dispatch itself, or a `SendMessage` that resumed the agent after it had reported. The first round is
|
|
277
|
+
* recorded under the dispatch's id, and each later one under that id with `#<round>` after it.
|
|
278
|
+
*
|
|
279
|
+
* A record with no id is a run of its own: read as one id they share, every such record would be one run.
|
|
280
|
+
*
|
|
281
|
+
* @param {{dispatch_id?: string}} record - A snapshot record.
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
export function runOf(record) {
|
|
285
|
+
if (record?.dispatch_id) return String(record.dispatch_id).split('#')[0];
|
|
286
|
+
if (!unnamed.has(record)) {
|
|
287
|
+
named += 1;
|
|
288
|
+
unnamed.set(record, `(unnamed ${named})`);
|
|
289
|
+
}
|
|
290
|
+
return unnamed.get(record);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** An id for each record that has none, for as long as the record is held. */
|
|
294
|
+
const unnamed = new WeakMap();
|
|
295
|
+
let named = 0;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Whether a parsed row of a subagent's transcript is the run handing its report back.
|
|
299
|
+
*
|
|
300
|
+
* @param {object|null} row - A parsed transcript row.
|
|
301
|
+
* @returns {boolean}
|
|
302
|
+
*/
|
|
303
|
+
const handsBack = (row) =>
|
|
304
|
+
row?.type === 'assistant' && Array.isArray(row.message?.content) && row.message.content.some((b) => b?.type === 'tool_use' && b.name === 'SubagentHandback');
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether a parsed row of a subagent's transcript is something said to the agent, as opposed to a tool's
|
|
308
|
+
* result or a reminder: the prompt that starts a run, or the message that resumes it.
|
|
309
|
+
*
|
|
310
|
+
* @param {object|null} row - A parsed transcript row.
|
|
311
|
+
* @returns {boolean}
|
|
312
|
+
*/
|
|
313
|
+
const saidToAgent = (row) => {
|
|
314
|
+
if (row?.type !== 'user') return false;
|
|
315
|
+
const content = row.message?.content;
|
|
316
|
+
if (Array.isArray(content) && content.some((b) => b?.type === 'tool_result')) return false;
|
|
317
|
+
return !textOf(content).trimStart().startsWith('<system-reminder>');
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Tells, line by line, which round of its run each line of a subagent's transcript belongs to. A round
|
|
322
|
+
* ends when the run hands its report back, and the next opens with the next thing said to the agent: a
|
|
323
|
+
* resume is written into the agent's own transcript as a message, and everything after it up to its next
|
|
324
|
+
* handback is that round's. What the agent writes after it reports and before anyone speaks to it again
|
|
325
|
+
* stays in the round it closed; a message that reaches it before it reports is part of the round it is in.
|
|
326
|
+
*
|
|
327
|
+
* The snapshot, the context sent to Langfuse and `learn --deep` all read rounds through this, so the three
|
|
328
|
+
* cannot disagree about where one ends. Only a line that could open a round or close one is parsed.
|
|
329
|
+
*
|
|
330
|
+
* @returns {(line: string) => number} Called on each line in order; returns the round it belongs to.
|
|
331
|
+
*/
|
|
332
|
+
export function roundsOf() {
|
|
333
|
+
let round = 1;
|
|
334
|
+
let reported = false;
|
|
335
|
+
const parse = (line) => {
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(line);
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
return (line) => {
|
|
343
|
+
if (reported && line.includes('"user"') && saidToAgent(parse(line))) {
|
|
344
|
+
round += 1;
|
|
345
|
+
reported = false;
|
|
346
|
+
}
|
|
347
|
+
if (line.includes('SubagentHandback') && handsBack(parse(line))) reported = true;
|
|
348
|
+
return round;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
273
352
|
/**
|
|
274
353
|
* Every place a transcript line can carry text.
|
|
275
354
|
*
|
|
@@ -331,8 +410,10 @@ async function completeLineOffset(file, size, lastLine) {
|
|
|
331
410
|
* Scans one project's transcripts and returns a record per subagent dispatch.
|
|
332
411
|
*
|
|
333
412
|
* A dispatch may notify more than once (an agent resumed via SendMessage re-notifies
|
|
334
|
-
* under the same id)
|
|
335
|
-
*
|
|
413
|
+
* under the same id), and every copy of one notification is one result. Where the run
|
|
414
|
+
* handed its reports back, each resume after a report is a round of its own, read from
|
|
415
|
+
* the agent's transcript (`attachAgentDetail`); a run from before the handback keeps
|
|
416
|
+
* its last notification's verdict.
|
|
336
417
|
*
|
|
337
418
|
* Reading is incremental: a per-file byte cursor means a session that has already been
|
|
338
419
|
* captured is not re-read, which matters because this runs from a Stop hook and the
|
|
@@ -411,7 +492,6 @@ export async function scanProject(projectDir, prior = {}) {
|
|
|
411
492
|
result_ts: null,
|
|
412
493
|
duration_s: null,
|
|
413
494
|
result_chars: null,
|
|
414
|
-
resumes: 0,
|
|
415
495
|
status: null,
|
|
416
496
|
agent_id: null,
|
|
417
497
|
skills: [],
|
|
@@ -449,29 +529,29 @@ export async function scanProject(projectDir, prior = {}) {
|
|
|
449
529
|
if (!record) continue;
|
|
450
530
|
|
|
451
531
|
const result = /<result>([\s\S]*?)(?:<\/result>|$)/.exec(text)?.[1] ?? '';
|
|
452
|
-
// The same notification read twice is not a second result.
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
// a notification without one is told by its time, as before.
|
|
532
|
+
// The same notification read twice is not a second result. It is read twice when bytes are
|
|
533
|
+
// re-walked after a cursor went back, and since Claude Code 2.1.282 one notification is also
|
|
534
|
+
// written up to three times: queued, taken off the queue or absorbed mid-turn, and delivered as a
|
|
535
|
+
// message, each at its own time. The agent's usage block (its time so far) tells a copy from a
|
|
536
|
+
// resume, which has run for longer; a notification without one is told by its time, as before.
|
|
458
537
|
const spentMs = Number(/<duration_ms>(\d+)<\/duration_ms>/.exec(text)?.[1] ?? Number.NaN);
|
|
459
538
|
const timed = Number.isFinite(spentMs);
|
|
460
539
|
if (timed ? record.notified_ms === spentMs : record.result_ts && record.result_ts === (row.timestamp ?? null)) continue;
|
|
461
|
-
if (timed ? record.notified_ms != null : record.result_ts) record.resumes += 1;
|
|
462
540
|
if (timed) record.notified_ms = spentMs;
|
|
463
541
|
record.status = /<status>(.*?)<\/status>/.exec(text)?.[1] ?? null;
|
|
464
|
-
record.result_ts = row.timestamp ?? null;
|
|
465
542
|
// A notification whose run handed its report back says only that: "delivered to you as a
|
|
466
543
|
// message … it is not repeated here". Read for a verdict, it replaced the one the handback
|
|
467
544
|
// declared with none, and since the subagent's transcript had not grown, nothing read it again.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
545
|
+
// Nor does it time the round the handback closed: a resumed run notifies again under the
|
|
546
|
+
// dispatch's id, and taken as the first round's end, that notification stretched the round
|
|
547
|
+
// across every hour the agent sat waiting to be resumed.
|
|
548
|
+
if (String(record.verdict_source ?? '').startsWith('handback')) continue;
|
|
549
|
+
record.result_ts = row.timestamp ?? null;
|
|
550
|
+
const { verdict, source } = classifyVerdict(result, record.role);
|
|
551
|
+
record.result_chars = result.length;
|
|
552
|
+
record.verdict = verdict;
|
|
553
|
+
record.verdict_source = source;
|
|
554
|
+
record.issues = issueCount(result, verdict, source);
|
|
475
555
|
if (record.ts && record.result_ts) {
|
|
476
556
|
record.duration_s = Math.round((Date.parse(record.result_ts) - Date.parse(record.ts)) / 1000);
|
|
477
557
|
}
|
|
@@ -487,8 +567,8 @@ export async function scanProject(projectDir, prior = {}) {
|
|
|
487
567
|
}
|
|
488
568
|
|
|
489
569
|
/**
|
|
490
|
-
* Reads each subagent's own transcript for
|
|
491
|
-
*
|
|
570
|
+
* Reads each subagent's own transcript for what only it knows: the reports it handed back, the skills
|
|
571
|
+
* it invoked, what it spent, and what it wrote — round by round.
|
|
492
572
|
*
|
|
493
573
|
* The report is the authoritative source for a verdict. Claude Code 2.1.276 moved the
|
|
494
574
|
* hand-off to a `SubagentHandback` tool call, whose `message` is the report itself —
|
|
@@ -496,17 +576,25 @@ export async function scanProject(projectDir, prior = {}) {
|
|
|
496
576
|
* live session that notification may not be written at all. Reading the subagent's own
|
|
497
577
|
* file works across both, and is where the mandatory-skill rule can be checked.
|
|
498
578
|
*
|
|
579
|
+
* A run the orchestrator resumed after it reported is recorded as a round per report (`roundsOf`). It
|
|
580
|
+
* used to be one record holding its last verdict, so a reviewer that rejected twice and then approved was
|
|
581
|
+
* one approval: over one project's first two days the gate's ledger held 34 loop-backs where the snapshot
|
|
582
|
+
* held 5, since the orchestrator resumed the same reviewer, architect and implementer through every fix.
|
|
583
|
+
* Each round keeps its own verdict, time, spend, files and skills, so every report reads rounds and none
|
|
584
|
+
* has to know that a run can have more than one.
|
|
585
|
+
*
|
|
499
586
|
* These per-agent transcripts are pruned sooner than the session's own, so the snapshot
|
|
500
587
|
* captures this while it exists.
|
|
501
588
|
*
|
|
502
589
|
* @param {string} projectDir - The project's transcript directory.
|
|
503
|
-
* @param {Map<string, object>} dispatches - Records to enrich, keyed by dispatch id
|
|
590
|
+
* @param {Map<string, object>} dispatches - Records to enrich, keyed by dispatch id; a run's later rounds
|
|
591
|
+
* are added to it.
|
|
504
592
|
*/
|
|
505
593
|
async function attachAgentDetail(projectDir, dispatches) {
|
|
506
|
-
/** @type {Map<string, object[]>} agent id → the dispatches that ran under it */
|
|
594
|
+
/** @type {Map<string, object[]>} agent id → the dispatches that ran under it, first rounds only */
|
|
507
595
|
const byAgent = new Map();
|
|
508
596
|
for (const record of dispatches.values()) {
|
|
509
|
-
if (!record.agent_id) continue;
|
|
597
|
+
if (!record.agent_id || (record.round ?? 1) !== 1) continue;
|
|
510
598
|
if (!byAgent.has(record.agent_id)) byAgent.set(record.agent_id, []);
|
|
511
599
|
byAgent.get(record.agent_id).push(record);
|
|
512
600
|
}
|
|
@@ -522,45 +610,78 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
522
610
|
const agentId = /^agent-([a-f0-9]+)\.jsonl$/.exec(file)?.[1];
|
|
523
611
|
const records = agentId && byAgent.get(agentId);
|
|
524
612
|
if (!records) continue;
|
|
613
|
+
const run = records[0];
|
|
614
|
+
const later = [];
|
|
615
|
+
for (let k = 2; dispatches.has(`${run.dispatch_id}#${k}`); k += 1) later.push(dispatches.get(`${run.dispatch_id}#${k}`));
|
|
525
616
|
// A run read in full whose transcript has not grown since has nothing new to say. It was assumed
|
|
526
617
|
// a finished run's transcript never changes, and 14 in 1,389 did: resumed after reporting, with a
|
|
527
618
|
// median 42% of their tokens — and their final verdict — written after the first handback, so the
|
|
528
|
-
// first read froze a partial bill. A run still in flight
|
|
619
|
+
// first read froze a partial bill. A run still in flight is read again when its transcript grows;
|
|
529
620
|
// a record from before a field existed is read once more; and one whose transcript is gone is
|
|
530
621
|
// never reached here, so it keeps what it had rather than being dropped, which is what a
|
|
531
|
-
// --rebuild would do to every run older than Claude Code's transcript retention. A
|
|
532
|
-
//
|
|
533
|
-
// read after it, and is read once more to get it back.
|
|
622
|
+
// --rebuild would do to every run older than Claude Code's transcript retention. A round that
|
|
623
|
+
// handed its report back has its verdict from the handback; one whose verdict says otherwise lost
|
|
624
|
+
// it to a notification read after it, and is read once more to get it back. A run from before the
|
|
625
|
+
// handback existed is read once, not on every snapshot: skipping only runs with a handback re-read
|
|
626
|
+
// 744 of one project's 826 in full on every turn, two seconds before the person saw an answer.
|
|
534
627
|
const size = (await stat(join(dir, file)).catch(() => null))?.size ?? null;
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
628
|
+
const settled = (r) => r.handed_back === false || String(r.verdict_source ?? '').startsWith('handback');
|
|
629
|
+
if (
|
|
630
|
+
run.agent_read_bytes === size &&
|
|
631
|
+
[...records, ...later].every((r) => settled(r) && 'round' in r && 'lessons_read' in r && 'tokens' in r && 'files_touched' in r && 'effort' in r)
|
|
632
|
+
) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
// A run captured before rounds were, holding every round's spend and its last verdict. Its later
|
|
636
|
+
// rounds are marked, so an export that already sent the whole run does not send them again.
|
|
637
|
+
const legacy = !('round' in run) && 'tokens' in run;
|
|
638
|
+
|
|
639
|
+
/** What one round did, read into as the transcript goes. */
|
|
640
|
+
const fresh = () => ({
|
|
641
|
+
start: null,
|
|
642
|
+
branch: null,
|
|
643
|
+
skills: new Set(),
|
|
644
|
+
// Lessons this round read, and whether it only listed the directory. Every spec tells its role
|
|
645
|
+
// to read its pills before acting, and until this was counted nothing could say whether one
|
|
646
|
+
// ever had. Only the count is kept, never the path or the content — this record is metadata.
|
|
647
|
+
lessonsRead: new Set(),
|
|
648
|
+
lessonsListed: false,
|
|
649
|
+
handback: null,
|
|
650
|
+
handbackTs: null,
|
|
651
|
+
/**
|
|
652
|
+
* Each API message's final usage, by message id. A streamed message is written once per content
|
|
653
|
+
* block under the same id, and its output count grows until the last copy — summing the rows
|
|
654
|
+
* counted the same input three or four times over.
|
|
655
|
+
*/
|
|
656
|
+
usage: new Map(),
|
|
657
|
+
/** The files the round wrote through the edit tools. Only their number is kept. */
|
|
658
|
+
written: new Set(),
|
|
659
|
+
});
|
|
660
|
+
const rounds = [fresh()];
|
|
661
|
+
const roundOf = roundsOf();
|
|
554
662
|
|
|
555
663
|
const rl = createInterface({ input: createReadStream(join(dir, file)), crlfDelay: Infinity });
|
|
556
664
|
for await (const line of rl) {
|
|
665
|
+
const n = roundOf(line);
|
|
666
|
+
if (n > rounds.length) {
|
|
667
|
+
// The line that opened the round: the message that resumed the run, and when.
|
|
668
|
+
rounds.push(fresh());
|
|
669
|
+
try {
|
|
670
|
+
const opened = JSON.parse(line);
|
|
671
|
+
rounds[n - 1].start = opened.timestamp ?? null;
|
|
672
|
+
rounds[n - 1].branch = opened.gitBranch ?? null;
|
|
673
|
+
} catch {
|
|
674
|
+
// Unreachable: a line that opens a round was parsed to tell that it does.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const round = rounds[n - 1];
|
|
557
678
|
const hasSkill = line.includes('"Skill"');
|
|
558
679
|
const hasHandback = line.includes('SubagentHandback');
|
|
559
680
|
if (/"name":"(Edit|Write|MultiEdit|NotebookEdit)"/.test(line)) {
|
|
560
681
|
try {
|
|
561
682
|
for (const block of JSON.parse(line)?.message?.content ?? []) {
|
|
562
683
|
const path = block?.type === 'tool_use' ? block.input?.[WRITES[block.name]] : null;
|
|
563
|
-
if (typeof path === 'string') written.add(path);
|
|
684
|
+
if (typeof path === 'string') round.written.add(path);
|
|
564
685
|
}
|
|
565
686
|
} catch {
|
|
566
687
|
// A torn line loses one edit, not the run's.
|
|
@@ -571,18 +692,18 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
571
692
|
const row = JSON.parse(line);
|
|
572
693
|
const message = row?.message;
|
|
573
694
|
const effort = typeof row?.effort === 'string' ? row.effort : null;
|
|
574
|
-
if (message?.id && message.usage) usage.set(message.id, { usage: message.usage, model: message.model ?? null, effort });
|
|
695
|
+
if (message?.id && message.usage) round.usage.set(message.id, { usage: message.usage, model: message.model ?? null, effort });
|
|
575
696
|
} catch {
|
|
576
697
|
// A torn line loses one message's count, not the run's.
|
|
577
698
|
}
|
|
578
699
|
}
|
|
579
700
|
const touched = pillReads(line);
|
|
580
|
-
for (const lesson of touched.read) lessonsRead.add(lesson);
|
|
581
|
-
if (touched.listed) lessonsListed = true;
|
|
701
|
+
for (const lesson of touched.read) round.lessonsRead.add(lesson);
|
|
702
|
+
if (touched.listed) round.lessonsListed = true;
|
|
582
703
|
if (!hasSkill && !hasHandback) continue;
|
|
583
704
|
|
|
584
705
|
if (hasSkill) {
|
|
585
|
-
for (const m of line.matchAll(/"skill"\s*:\s*"([^"]+)"/g)) skills.add(m[1]);
|
|
706
|
+
for (const m of line.matchAll(/"skill"\s*:\s*"([^"]+)"/g)) round.skills.add(m[1]);
|
|
586
707
|
}
|
|
587
708
|
if (!hasHandback) continue;
|
|
588
709
|
|
|
@@ -598,43 +719,73 @@ async function attachAgentDetail(projectDir, dispatches) {
|
|
|
598
719
|
if (block?.type !== 'tool_use' || block?.name !== 'SubagentHandback') continue;
|
|
599
720
|
const message = block?.input?.message;
|
|
600
721
|
if (typeof message !== 'string') continue;
|
|
601
|
-
handback = message;
|
|
602
|
-
handbackTs = row.timestamp ?? null;
|
|
722
|
+
round.handback = message;
|
|
723
|
+
round.handbackTs = row.timestamp ?? null;
|
|
603
724
|
}
|
|
604
725
|
}
|
|
605
726
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
727
|
+
rounds.forEach((round, i) => {
|
|
728
|
+
// A round nothing happened in — something said to the agent after it reported that it never
|
|
729
|
+
// answered — is not a round.
|
|
730
|
+
if (i > 0 && !round.handback && round.usage.size === 0) return;
|
|
731
|
+
const id = `${run.dispatch_id}#${i + 1}`;
|
|
732
|
+
let targets = records;
|
|
733
|
+
if (i > 0) {
|
|
734
|
+
if (!dispatches.has(id)) {
|
|
735
|
+
dispatches.set(id, {
|
|
736
|
+
dispatch_id: id,
|
|
737
|
+
ts: round.start,
|
|
738
|
+
role: run.role,
|
|
739
|
+
desc: run.desc,
|
|
740
|
+
model: run.model,
|
|
741
|
+
branch: round.branch ?? run.branch,
|
|
742
|
+
session: run.session,
|
|
743
|
+
verdict: null,
|
|
744
|
+
verdict_source: null,
|
|
745
|
+
issues: null,
|
|
746
|
+
result_ts: null,
|
|
747
|
+
duration_s: null,
|
|
748
|
+
result_chars: null,
|
|
749
|
+
status: null,
|
|
750
|
+
agent_id: run.agent_id,
|
|
751
|
+
skills: [],
|
|
752
|
+
...(legacy ? { backfilled: true } : {}),
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
targets = [dispatches.get(id)];
|
|
756
|
+
}
|
|
757
|
+
const spent = tokensOf(round.usage);
|
|
758
|
+
targets.forEach((record, j) => {
|
|
759
|
+
// One transcript, one bill: were two records ever to share an agent, the second would count it again.
|
|
760
|
+
record.tokens = j === 0 ? spent.tokens : null;
|
|
761
|
+
record.usage_model = spent.model;
|
|
762
|
+
record.effort = spent.effort;
|
|
763
|
+
record.files_touched = round.written.size;
|
|
764
|
+
record.skills = [...round.skills];
|
|
765
|
+
record.lessons_read = round.lessonsRead.size;
|
|
766
|
+
record.lessons_listed = round.lessonsListed;
|
|
767
|
+
record.round = i + 1;
|
|
768
|
+
record.handed_back = Boolean(round.handback);
|
|
769
|
+
record.agent_read = true;
|
|
770
|
+
// Fields a record no longer carries: the first definition of a lesson read, which counted a
|
|
771
|
+
// listing or a citation as one, and the count of resumes, which rounds replaced.
|
|
772
|
+
delete record.pills_read;
|
|
773
|
+
delete record.pills_looked;
|
|
774
|
+
delete record.resumes;
|
|
775
|
+
if (!round.handback) return;
|
|
776
|
+
const { verdict, source } = classifyVerdict(round.handback, record.role);
|
|
624
777
|
record.verdict = verdict;
|
|
625
778
|
record.verdict_source = source === 'declared' ? 'handback' : `handback:${source}`;
|
|
626
|
-
record.issues = issueCount(handback, verdict, source);
|
|
627
|
-
record.result_chars = handback.length;
|
|
628
|
-
record.
|
|
779
|
+
record.issues = issueCount(round.handback, verdict, source);
|
|
780
|
+
record.result_chars = round.handback.length;
|
|
781
|
+
record.status = record.status ?? 'completed';
|
|
782
|
+
record.result_ts = round.handbackTs ?? record.result_ts;
|
|
629
783
|
if (record.ts && record.result_ts) {
|
|
630
|
-
record.duration_s = Math.round(
|
|
631
|
-
(Date.parse(record.result_ts) - Date.parse(record.ts)) / 1000,
|
|
632
|
-
);
|
|
784
|
+
record.duration_s = Math.round((Date.parse(record.result_ts) - Date.parse(record.ts)) / 1000);
|
|
633
785
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
run.agent_read_bytes = size;
|
|
638
789
|
}
|
|
639
790
|
}
|
|
640
791
|
}
|