@mmerterden/multi-agent-pipeline 20.24.0 → 20.25.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 (86) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +6 -5
  3. package/README.tr.md +6 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -6
  7. package/docs/features.md +9 -4
  8. package/install/catalog-history.json +1 -1
  9. package/install/templates/claude-hooks.json +1 -1
  10. package/install/templates/codex-hooks.json +18 -0
  11. package/manifest.json +89 -82
  12. package/package.json +1 -1
  13. package/pipeline/agents/task-clarifier.md +1 -1
  14. package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
  15. package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
  16. package/pipeline/commands/multi-agent/postmortem/SKILL.md +18 -6
  17. package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
  18. package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
  19. package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
  20. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  21. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
  22. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  23. package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
  24. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  25. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  26. package/pipeline/contract/fixtures/runs-running.json +1 -1
  27. package/pipeline/lib/credential-inventory.sh +5 -5
  28. package/pipeline/lib/fetch-crashlytics.sh +6 -8
  29. package/pipeline/lib/model-rung.sh +1 -1
  30. package/pipeline/lib/plan-todos.sh +21 -4
  31. package/pipeline/lib/post-pr-review.sh +4 -6
  32. package/pipeline/lib/route-state.sh +1 -1
  33. package/pipeline/lib/shadow-git.sh +4 -5
  34. package/pipeline/multi-agent-refs/cross-cli-contract.md +8 -9
  35. package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
  36. package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
  37. package/pipeline/multi-agent-refs/features/model-fallback.md +17 -12
  38. package/pipeline/multi-agent-refs/help/en.md +1 -0
  39. package/pipeline/multi-agent-refs/help/tr.md +1 -0
  40. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  41. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +2 -0
  42. package/pipeline/multi-agent-refs/phases/phase-3-review.md +5 -5
  43. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  44. package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
  45. package/pipeline/schemas/agent-state.schema.json +5 -0
  46. package/pipeline/schemas/phases.json +4 -0
  47. package/pipeline/schemas/plan-todos.schema.json +4 -4
  48. package/pipeline/schemas/prefs.schema.json +27 -4
  49. package/pipeline/scripts/_code-graph.mjs +2 -2
  50. package/pipeline/scripts/_transcript-hosts.mjs +406 -0
  51. package/pipeline/scripts/build-stack-plugins.mjs +3 -3
  52. package/pipeline/scripts/capture-flush.sh +7 -7
  53. package/pipeline/scripts/code-graph-rules/android.json +1 -1
  54. package/pipeline/scripts/cost-analyze.mjs +26 -19
  55. package/pipeline/scripts/cost-table.json +35 -19
  56. package/pipeline/scripts/dep-bump.mjs +1556 -0
  57. package/pipeline/scripts/edit-syntax-gate.mjs +151 -29
  58. package/pipeline/scripts/edit-syntax-gate.sh +9 -6
  59. package/pipeline/scripts/gc-abandoned.sh +2 -2
  60. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
  61. package/pipeline/scripts/git-path.mjs +1 -2
  62. package/pipeline/scripts/host-hooks.mjs +265 -0
  63. package/pipeline/scripts/jira-attach.sh +1 -1
  64. package/pipeline/scripts/keychain.py +3 -3
  65. package/pipeline/scripts/output-quality-check.sh +1 -1
  66. package/pipeline/scripts/phase-tracker.sh +5 -6
  67. package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
  68. package/pipeline/scripts/pre-push-check.sh +3 -6
  69. package/pipeline/scripts/transcript-index.mjs +423 -242
  70. package/pipeline/scripts/uninstall.mjs +90 -4
  71. package/pipeline/scripts/update-issue-progress.sh +4 -6
  72. package/pipeline/scripts/usage-report.mjs +2 -2
  73. package/pipeline/skills/.skill-manifest.json +14 -10
  74. package/pipeline/skills/.skills-index.json +12 -1
  75. package/pipeline/skills/shared/README.md +4 -3
  76. package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
  77. package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
  78. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
  79. package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +10 -7
  80. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
  81. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
  82. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
  83. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  84. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
  85. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  86. package/pipeline/skills/skills-index.md +2 -1
@@ -7,7 +7,10 @@
7
7
  * session actually did on the way: which tool calls it repeated, where it sat
8
8
  * waiting, where the context was compacted, which subagent failed, what was
9
9
  * denied. This reads a Claude Code session file (`<session>.jsonl`, plus the
10
- * subagent files beside it) and extracts those facts deterministically, so the
10
+ * subagent files beside it), a Copilot CLI session log
11
+ * (`session-state/<session>/events.jsonl`) or a Codex rollout
12
+ * (`sessions/YYYY/MM/DD/rollout-*.jsonl`), detecting the format per file
13
+ * (_transcript-hosts.mjs), and extracts those facts deterministically, so the
11
14
  * same transcript always gives the same index and every finding carries the
12
15
  * `file:line` it came from. Interpreting them is left to the model that writes
13
16
  * the post-mortem (/multi-agent:postmortem); nothing here guesses.
@@ -46,7 +49,10 @@
46
49
  * options: --json --no-subagents --max-mb N --max-events N
47
50
  * --loop-threshold N --stall-minutes N --excerpt N
48
51
  *
49
- * TRANSCRIPTS_ROOT overrides ~/.claude/projects, LOGS_ROOT the run log root.
52
+ * TRANSCRIPTS_ROOT overrides ~/.claude/projects, COPILOT_TRANSCRIPTS_ROOT
53
+ * ~/.copilot/session-state (else $COPILOT_HOME/session-state),
54
+ * CODEX_TRANSCRIPTS_ROOT ~/.codex/sessions (else $CODEX_HOME/sessions), and
55
+ * LOGS_ROOT the run log root.
50
56
  *
51
57
  * Exit codes: 0 indexed | 2 usage | 3 nothing to read | 4 redaction guard
52
58
  *
@@ -58,6 +64,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
58
64
  import { homedir } from "node:os";
59
65
  import { basename, dirname, join } from "node:path";
60
66
  import { readJsonl } from "./_jsonl.mjs";
67
+ import { createAdapter, detectFormat, sessionCwd } from "./_transcript-hosts.mjs";
61
68
  import { redactCredentials, scrubCounted } from "./_redact.mjs";
62
69
  import { listRuns, logsRoot, taskIdVariants } from "./_run-paths.mjs";
63
70
  import { redact } from "../lib/redact.mjs";
@@ -75,7 +82,8 @@ export const DEFAULTS = {
75
82
  subagents: true,
76
83
  };
77
84
 
78
- const SUBAGENT_TOOLS = new Set(["Agent", "Task"]);
85
+ const SUBAGENT_TOOLS = new Set(["Agent", "Task", "task", "spawn_agent"]);
86
+ const SKILL_TOOL = /^skill$/i;
79
87
  const DENIAL =
80
88
  /doesn't want to proceed|User rejected tool use|^Permission to use \S+ (?:has been|was) denied|permission denied by the user/i;
81
89
  const HOOK_BLOCK = /\bhook (?:error|blocked)|PreToolUse:|PostToolUse:/;
@@ -84,6 +92,33 @@ const INTERRUPT = /^\[Request interrupted by user/;
84
92
  export const defaultProjectsRoot = () =>
85
93
  process.env.TRANSCRIPTS_ROOT || join(homedir(), ".claude", "projects");
86
94
 
95
+ export const defaultCopilotRoot = () =>
96
+ process.env.COPILOT_TRANSCRIPTS_ROOT ||
97
+ join(process.env.COPILOT_HOME || join(homedir(), ".copilot"), "session-state");
98
+
99
+ export const defaultCodexRoot = () =>
100
+ process.env.CODEX_TRANSCRIPTS_ROOT ||
101
+ join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
102
+
103
+ export { detectFormat };
104
+
105
+ const COPILOT_LOG = "events.jsonl";
106
+
107
+ const CODEX_NAME = /^rollout-[\dT:-]+-([0-9a-f]{8})[0-9a-f-]*\.jsonl$/i;
108
+
109
+ /**
110
+ * The name a file is cited by. Every Copilot CLI log is `events.jsonl`, so it
111
+ * is prefixed with the first eight characters of its session directory; a
112
+ * Codex rollout name carries its start time and full session id, so it is cut
113
+ * to `rollout-<first eight of the id>.jsonl`. Claude Code names stay as they are.
114
+ */
115
+ export function displayName(path) {
116
+ const name = basename(path);
117
+ if (name === COPILOT_LOG) return `${basename(dirname(path)).slice(0, 8)}/${name}`;
118
+ const codex = name.match(CODEX_NAME);
119
+ return codex ? `rollout-${codex[1]}.jsonl` : name;
120
+ }
121
+
87
122
  /** A session id as Claude Code writes it (a UUID), or any token safe as a file name. */
88
123
  const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
89
124
 
@@ -202,6 +237,7 @@ function inputGist(name, input) {
202
237
  if (!input || typeof input !== "object") return "";
203
238
  for (const k of [
204
239
  "command",
240
+ "cmd",
205
241
  "file_path",
206
242
  "path",
207
243
  "pattern",
@@ -209,6 +245,7 @@ function inputGist(name, input) {
209
245
  "query",
210
246
  "skill",
211
247
  "description",
248
+ "input",
212
249
  ]) {
213
250
  if (typeof input[k] === "string" && input[k]) return input[k];
214
251
  }
@@ -274,10 +311,18 @@ export async function indexTranscripts(paths, options = {}) {
274
311
  let idleGaps = 0;
275
312
 
276
313
  for (const path of expandFiles(paths, o.subagents)) {
277
- const name = basename(path);
314
+ const name = displayName(path);
278
315
  const at = (line) => `${name}:${line}`;
279
316
  const stats = {};
280
- const fileRow = { name, path: R.clean(path), sidechain: false, malformedAt: [] };
317
+ const fileRow = {
318
+ name,
319
+ path: R.clean(path),
320
+ format: null,
321
+ sidechain: false,
322
+ usagePartial: false,
323
+ malformedAt: [],
324
+ };
325
+ let adapt = null;
281
326
  const pending = new Map();
282
327
  const groups = new Map();
283
328
  let lastKey = null;
@@ -306,246 +351,287 @@ export async function indexTranscripts(paths, options = {}) {
306
351
  if (row.reason !== "truncated") fileRow.malformedAt.push(at(row.line));
307
352
  continue;
308
353
  }
309
- const rec = row.record;
354
+ if (!adapt) {
355
+ fileRow.format = detectFormat(row.record);
356
+ adapt = createAdapter(fileRow.format);
357
+ }
310
358
  const line = row.line;
311
- const ts = tsOf(rec);
312
- if (rec.isSidechain) fileRow.sidechain = true;
313
-
314
- if (ts !== null && prev && prev.ts !== null) {
315
- const gap = ts - prev.ts;
316
- if (gap >= stallMs) {
317
- const startsTurn = rec.type === "user" && isPrompt(rec);
318
- if (startsTurn && pending.size === 0) idleGaps++;
319
- else {
320
- const running = [...pending.values()].at(-1);
321
- stalls.push({
322
- from: at(prev.line),
323
- to: at(line),
324
- gapMs: gap,
325
- pendingTool: running ? running.name : null,
326
- });
359
+ for (const rec of adapt(row.record)) {
360
+ const ts = tsOf(rec);
361
+ if (rec.isSidechain && fileRow.format === "claude") fileRow.sidechain = true;
362
+
363
+ if (ts !== null && prev && prev.ts !== null) {
364
+ const gap = ts - prev.ts;
365
+ if (gap >= stallMs) {
366
+ const startsTurn = rec.type === "user" && isPrompt(rec);
367
+ if (startsTurn && pending.size === 0) idleGaps++;
368
+ else {
369
+ const running = [...pending.values()].at(-1);
370
+ stalls.push({
371
+ from: at(prev.line),
372
+ to: at(line),
373
+ gapMs: gap,
374
+ pendingTool: running ? running.name : null,
375
+ });
376
+ }
327
377
  }
328
378
  }
329
- }
330
- if (ts !== null) prev = { ts, line };
331
- if (turn && ts !== null) turn.lastTs = ts;
332
-
333
- if (rec.type === "system") {
334
- if (rec.subtype === "compact_boundary") {
335
- const m = rec.compactMetadata || {};
336
- compactions.push({
337
- at: at(line),
338
- kind: "boundary",
339
- trigger: R.clean(m.trigger || "unknown"),
340
- preTokens: numberOr(m.preTokens, null),
341
- postTokens: numberOr(m.postTokens, null),
342
- });
343
- push({ at: at(line), role: "system", kind: "compaction" });
344
- } else if (rec.subtype === "turn_duration") {
345
- if (turn) turn.durationMs = numberOr(rec.durationMs, null);
346
- } else if (Array.isArray(rec.hookErrors) && rec.hookErrors.length) {
347
- errors.push({ at: at(line), kind: "hook", tool: null, text: ex(textOf(rec.hookErrors)) });
348
- if (turn) turn.errors++;
349
- } else if (rec.level === "error") {
350
- errors.push({ at: at(line), kind: "system", tool: null, text: ex(rec.content) });
351
- if (turn) turn.errors++;
379
+ if (ts !== null) prev = { ts, line };
380
+ if (turn && ts !== null) turn.lastTs = ts;
381
+
382
+ if (rec.type === "x-usage") {
383
+ const add = rec.usage || {};
384
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
385
+ const n = numberOr(add[key]);
386
+ usage[key] += n;
387
+ if (turn && rec.inTurn) turn.usage[key] += n;
388
+ }
389
+ if (rec.countsMessage) usage.messages++;
390
+ if (turn && rec.inTurn) {
391
+ const ctx = numberOr(add.input) + numberOr(add.cacheRead) + numberOr(add.cacheWrite);
392
+ if (ctx > turn.usage.peakContext) turn.usage.peakContext = ctx;
393
+ }
394
+ continue;
352
395
  }
353
- continue;
354
- }
355
396
 
356
- if (rec.type === "user") {
357
- const content = rec.message?.content;
358
- if (rec.isCompactSummary) {
359
- compactions.push({ at: at(line), kind: "summary" });
397
+ if (rec.type === "system") {
398
+ if (rec.subtype === "compact_boundary") {
399
+ const m = rec.compactMetadata || {};
400
+ compactions.push({
401
+ at: at(line),
402
+ kind: "boundary",
403
+ trigger: R.clean(m.trigger || "unknown"),
404
+ preTokens: numberOr(m.preTokens, null),
405
+ postTokens: numberOr(m.postTokens, null),
406
+ });
407
+ push({ at: at(line), role: "system", kind: "compaction" });
408
+ } else if (rec.subtype === "turn_duration") {
409
+ if (turn) turn.durationMs = numberOr(rec.durationMs, null);
410
+ } else if (Array.isArray(rec.hookErrors) && rec.hookErrors.length) {
411
+ errors.push({
412
+ at: at(line),
413
+ kind: "hook",
414
+ tool: null,
415
+ text: ex(textOf(rec.hookErrors)),
416
+ });
417
+ if (turn) turn.errors++;
418
+ } else if (rec.level === "error") {
419
+ errors.push({ at: at(line), kind: "system", tool: null, text: ex(rec.content) });
420
+ if (turn) turn.errors++;
421
+ }
360
422
  continue;
361
423
  }
362
- if (Array.isArray(content) && content.some((b) => b?.type === "tool_result")) {
363
- let first = true;
364
- for (const b of content) {
365
- if (b?.type !== "tool_result") continue;
366
- const call = pending.get(b.tool_use_id);
367
- pending.delete(b.tool_use_id);
368
- const result = first && rec.toolUseResult !== undefined ? rec.toolUseResult : undefined;
369
- first = false;
370
- const body = textOf(b.content) || (typeof result === "string" ? result : "");
371
- const denied = DENIAL.test(body) || (typeof result === "string" && DENIAL.test(result));
372
- const isError = b.is_error === true || denied;
373
- if (call) {
374
- call.resultAt = at(line);
375
- call.isError = isError;
376
- const reported =
377
- result && typeof result === "object"
378
- ? (result.durationMs ?? result.totalDurationMs)
379
- : undefined;
380
- if (Number.isFinite(reported)) {
381
- call.durationMs = reported;
382
- call.durationSource = "reported";
383
- } else if (call.ts !== null && ts !== null) {
384
- call.durationMs = ts - call.ts;
385
- call.durationSource = "timestamps";
386
- }
387
- if (call.group) call.group.errors += isError ? 1 : 0;
388
- const sub = subagentByToolId.get(b.tool_use_id);
389
- if (sub) {
390
- sub.resultAt = at(line);
391
- if (result && typeof result === "object") {
392
- sub.status = R.clean(result.status || (isError ? "error" : "completed"));
393
- if (result.agentId) subagentByAgentId.set(String(result.agentId), sub);
394
- if (Number.isFinite(result.totalDurationMs))
395
- sub.durationMs = result.totalDurationMs;
396
- if (Number.isFinite(result.totalTokens)) sub.tokens = result.totalTokens;
397
- if (Number.isFinite(result.totalToolUseCount))
398
- sub.toolUses = result.totalToolUseCount;
399
- } else {
400
- sub.status = isError ? "error" : "completed";
424
+
425
+ if (rec.type === "user") {
426
+ const content = rec.message?.content;
427
+ if (rec.isCompactSummary) {
428
+ compactions.push({ at: at(line), kind: "summary" });
429
+ continue;
430
+ }
431
+ if (Array.isArray(content) && content.some((b) => b?.type === "tool_result")) {
432
+ let first = true;
433
+ for (const b of content) {
434
+ if (b?.type !== "tool_result") continue;
435
+ const call = pending.get(b.tool_use_id);
436
+ pending.delete(b.tool_use_id);
437
+ const result =
438
+ first && rec.toolUseResult !== undefined ? rec.toolUseResult : undefined;
439
+ first = false;
440
+ const body = textOf(b.content) || (typeof result === "string" ? result : "");
441
+ const denied =
442
+ b.denied === true ||
443
+ DENIAL.test(body) ||
444
+ (typeof result === "string" && DENIAL.test(result));
445
+ const isError = b.is_error === true || denied;
446
+ if (call) {
447
+ call.resultAt = at(line);
448
+ call.isError = isError;
449
+ const reported =
450
+ result && typeof result === "object"
451
+ ? (result.durationMs ?? result.totalDurationMs)
452
+ : undefined;
453
+ if (Number.isFinite(reported)) {
454
+ call.durationMs = reported;
455
+ call.durationSource = "reported";
456
+ } else if (call.ts !== null && ts !== null) {
457
+ call.durationMs = ts - call.ts;
458
+ call.durationSource = "timestamps";
459
+ }
460
+ if (call.group) call.group.errors += isError ? 1 : 0;
461
+ const sub = subagentByToolId.get(b.tool_use_id);
462
+ if (sub) {
463
+ sub.resultAt = at(line);
464
+ if (result && typeof result === "object") {
465
+ sub.status = R.clean(result.status || (isError ? "error" : "completed"));
466
+ if (result.agentId) subagentByAgentId.set(String(result.agentId), sub);
467
+ if (Number.isFinite(result.totalDurationMs))
468
+ sub.durationMs = result.totalDurationMs;
469
+ if (Number.isFinite(result.totalTokens)) sub.tokens = result.totalTokens;
470
+ if (Number.isFinite(result.totalToolUseCount))
471
+ sub.toolUses = result.totalToolUseCount;
472
+ } else {
473
+ sub.status = isError ? "error" : "completed";
474
+ }
475
+ if (
476
+ !sub.durationMs &&
477
+ call.durationMs !== undefined &&
478
+ sub.status !== "async_launched"
479
+ )
480
+ sub.durationMs = call.durationMs;
481
+ sub.result = ex(body);
401
482
  }
402
- if (
403
- !sub.durationMs &&
404
- call.durationMs !== undefined &&
405
- sub.status !== "async_launched"
406
- )
407
- sub.durationMs = call.durationMs;
408
- sub.result = ex(body);
483
+ }
484
+ if (isError) {
485
+ const kind = denied ? "denial" : HOOK_BLOCK.test(body) ? "hook" : "error";
486
+ errors.push({ at: at(line), kind, tool: call ? call.name : null, text: ex(body) });
487
+ if (turn) turn.errors++;
488
+ push({ at: at(line), role: "user", kind, name: call ? call.name : null });
409
489
  }
410
490
  }
411
- if (isError) {
412
- const kind = denied ? "denial" : HOOK_BLOCK.test(body) ? "hook" : "error";
413
- errors.push({ at: at(line), kind, tool: call ? call.name : null, text: ex(body) });
414
- if (turn) turn.errors++;
415
- push({ at: at(line), role: "user", kind, name: call ? call.name : null });
416
- }
491
+ continue;
417
492
  }
418
- continue;
419
- }
420
493
 
421
- const text = textOf(content);
422
- if (rec.isSidechain) continue;
423
- const notice = text.match(/<task-notification>([\s\S]*?)<\/task-notification>/);
424
- if (notice) {
425
- const id = (notice[1].match(/<task-id>([^<]+)<\/task-id>/) || [])[1];
426
- const status = (notice[1].match(/<status>([^<]+)<\/status>/) || [])[1];
427
- const sub = id ? subagentByAgentId.get(id.trim()) : null;
428
- if (sub) {
429
- sub.notifiedAt = at(line);
430
- if (status) sub.status = R.clean(status.trim());
494
+ const text = textOf(content);
495
+ if (rec.isSidechain) continue;
496
+ const notice = text.match(/<task-notification>([\s\S]*?)<\/task-notification>/);
497
+ if (notice) {
498
+ const id = (notice[1].match(/<task-id>([^<]+)<\/task-id>/) || [])[1];
499
+ const status = (notice[1].match(/<status>([^<]+)<\/status>/) || [])[1];
500
+ const sub = id ? subagentByAgentId.get(id.trim()) : null;
501
+ if (sub) {
502
+ sub.notifiedAt = at(line);
503
+ if (status) sub.status = R.clean(status.trim());
504
+ const st = rec.subagentStats;
505
+ if (st) {
506
+ if (Number.isFinite(st.tokens)) sub.tokens = st.tokens;
507
+ if (Number.isFinite(st.durationMs)) sub.durationMs = st.durationMs;
508
+ if (Number.isFinite(st.toolUses)) sub.toolUses = st.toolUses;
509
+ }
510
+ }
511
+ push({ at: at(line), role: "user", kind: "notification", text: ex(status || "") });
512
+ continue;
431
513
  }
432
- push({ at: at(line), role: "user", kind: "notification", text: ex(status || "") });
433
- continue;
434
- }
435
- if (INTERRUPT.test(text)) {
436
- errors.push({ at: at(line), kind: "interrupt", tool: null, text: ex(text) });
437
- if (turn) turn.errors++;
438
- push({ at: at(line), role: "user", kind: "interrupt" });
439
- continue;
440
- }
441
- if (rec.isMeta) continue;
442
- const command = text.match(/<command-name>\s*([^<\s]+)\s*<\/command-name>/);
443
- if (command) {
444
- const cmd = R.clean(command[1]);
445
- invocations.push({ at: at(line), kind: "command", name: cmd });
446
- openTurn(line, ts, cmd);
447
- push({ at: at(line), role: "user", kind: "command", name: cmd });
514
+ if (INTERRUPT.test(text)) {
515
+ errors.push({ at: at(line), kind: "interrupt", tool: null, text: ex(text) });
516
+ if (turn) turn.errors++;
517
+ push({ at: at(line), role: "user", kind: "interrupt" });
518
+ continue;
519
+ }
520
+ if (rec.isMeta) continue;
521
+ const command = text.match(/<command-name>\s*([^<\s]+)\s*<\/command-name>/);
522
+ if (command) {
523
+ const cmd = R.clean(command[1]);
524
+ invocations.push({ at: at(line), kind: "command", name: cmd });
525
+ openTurn(line, ts, cmd);
526
+ push({ at: at(line), role: "user", kind: "command", name: cmd });
527
+ continue;
528
+ }
529
+ if (!text.trim()) continue;
530
+ const prompt = ex(text);
531
+ openTurn(line, ts, prompt);
532
+ push({ at: at(line), role: "user", kind: "prompt", text: prompt });
448
533
  continue;
449
534
  }
450
- if (!text.trim()) continue;
451
- const prompt = ex(text);
452
- openTurn(line, ts, prompt);
453
- push({ at: at(line), role: "user", kind: "prompt", text: prompt });
454
- continue;
455
- }
456
535
 
457
- if (rec.type === "assistant") {
458
- const msg = rec.message || {};
459
- if (!turn && !rec.isSidechain) openTurn(line, ts, null);
460
- if (turn) turn.assistantMessages++;
461
- const u = msg.usage;
462
- if (u && !(msg.id && seenMessages.has(msg.id))) {
463
- if (msg.id) seenMessages.add(msg.id);
464
- const add = {
465
- input: numberOr(u.input_tokens),
466
- output: numberOr(u.output_tokens),
467
- cacheRead: numberOr(u.cache_read_input_tokens),
468
- cacheWrite: numberOr(u.cache_creation_input_tokens),
469
- };
470
- for (const k of Object.keys(add)) usage[k] += add[k];
471
- usage.messages++;
472
- if (turn) {
473
- for (const k of Object.keys(add)) turn.usage[k] += add[k];
474
- const ctx = add.input + add.cacheRead + add.cacheWrite;
475
- if (ctx > turn.usage.peakContext) turn.usage.peakContext = ctx;
476
- }
477
- }
478
- if (rec.isApiErrorMessage) {
479
- errors.push({
480
- at: at(line),
481
- kind: "api-error",
482
- tool: null,
483
- text: ex(textOf(msg.content)),
484
- });
485
- if (turn) turn.errors++;
486
- }
487
- for (const b of Array.isArray(msg.content) ? msg.content : []) {
488
- if (b?.type === "text" && b.text?.trim()) {
489
- push({ at: at(line), role: "assistant", kind: "text", text: ex(b.text) });
490
- continue;
536
+ if (rec.type === "assistant") {
537
+ const msg = rec.message || {};
538
+ if (!turn && !rec.isSidechain) openTurn(line, ts, null);
539
+ if (turn) turn.assistantMessages++;
540
+ const u = msg.usage;
541
+ if (u && !(msg.id && seenMessages.has(msg.id))) {
542
+ if (msg.id) seenMessages.add(msg.id);
543
+ const add = {
544
+ input: numberOr(u.input_tokens),
545
+ output: numberOr(u.output_tokens),
546
+ cacheRead: numberOr(u.cache_read_input_tokens),
547
+ cacheWrite: numberOr(u.cache_creation_input_tokens),
548
+ };
549
+ for (const k of Object.keys(add)) usage[k] += add[k];
550
+ usage.messages++;
551
+ if (turn) {
552
+ for (const k of Object.keys(add)) turn.usage[k] += add[k];
553
+ const ctx = add.input + add.cacheRead + add.cacheWrite;
554
+ if (ctx > turn.usage.peakContext) turn.usage.peakContext = ctx;
555
+ }
491
556
  }
492
- if (b?.type !== "tool_use") continue;
493
- const toolName = R.clean(b.name || "unknown");
494
- const input = b.input || {};
495
- const key = `${b.name}\u0000${stable(input)}`;
496
- let group = groups.get(key);
497
- if (!group) {
498
- group = { name: toolName, hash: digest(key), at: [], errors: 0, consecutive: 0 };
499
- group.input = ex(inputGist(b.name, input));
500
- groups.set(key, group);
557
+ if (rec.isApiErrorMessage) {
558
+ errors.push({
559
+ at: at(line),
560
+ kind: "api-error",
561
+ tool: null,
562
+ text: ex(textOf(msg.content)),
563
+ });
564
+ if (turn) turn.errors++;
501
565
  }
502
- group.at.push(at(line));
503
- streak = key === lastKey ? streak + 1 : 1;
504
- lastKey = key;
505
- if (streak > group.consecutive) group.consecutive = streak;
506
- const call = {
507
- at: at(line),
508
- name: toolName,
509
- input: group.input,
510
- ts,
511
- durationMs: undefined,
512
- durationSource: null,
513
- resultAt: null,
514
- isError: false,
515
- group,
516
- };
517
- toolCalls.push(call);
518
- if (b.id) pending.set(b.id, call);
519
- if (turn) turn.toolCalls++;
520
- push({ at: at(line), role: "assistant", kind: "tool", name: toolName, text: call.input });
521
-
522
- if (SUBAGENT_TOOLS.has(b.name)) {
523
- const sub = {
566
+ for (const b of Array.isArray(msg.content) ? msg.content : []) {
567
+ if (b?.type === "text" && b.text?.trim()) {
568
+ push({ at: at(line), role: "assistant", kind: "text", text: ex(b.text) });
569
+ continue;
570
+ }
571
+ if (b?.type !== "tool_use") continue;
572
+ const toolName = R.clean(b.name || "unknown");
573
+ const input = b.input || {};
574
+ const key = `${b.name}\u0000${stable(input)}`;
575
+ let group = groups.get(key);
576
+ if (!group) {
577
+ group = { name: toolName, hash: digest(key), at: [], errors: 0, consecutive: 0 };
578
+ group.input = ex(inputGist(b.name, input));
579
+ groups.set(key, group);
580
+ }
581
+ group.at.push(at(line));
582
+ streak = key === lastKey ? streak + 1 : 1;
583
+ lastKey = key;
584
+ if (streak > group.consecutive) group.consecutive = streak;
585
+ const call = {
524
586
  at: at(line),
525
- agentType: R.clean(input.subagent_type || "general-purpose"),
526
- description: ex(input.description || ""),
527
- background: input.run_in_background === true,
528
- model: input.model ? R.clean(input.model) : null,
529
- status: null,
587
+ name: toolName,
588
+ input: group.input,
589
+ ts,
590
+ durationMs: undefined,
591
+ durationSource: null,
530
592
  resultAt: null,
531
- notifiedAt: null,
532
- durationMs: null,
533
- tokens: null,
534
- toolUses: null,
535
- result: null,
593
+ isError: false,
594
+ group,
536
595
  };
537
- subagents.push(sub);
538
- if (b.id) subagentByToolId.set(b.id, sub);
539
- }
540
- if (b.name === "Skill" && (input.skill || input.command)) {
541
- invocations.push({
596
+ toolCalls.push(call);
597
+ if (b.id) pending.set(b.id, call);
598
+ if (turn) turn.toolCalls++;
599
+ push({
542
600
  at: at(line),
543
- kind: "skill",
544
- name: R.clean(input.skill || input.command),
601
+ role: "assistant",
602
+ kind: "tool",
603
+ name: toolName,
604
+ text: call.input,
545
605
  });
546
- }
547
- if (b.name === "SlashCommand" && input.command) {
548
- invocations.push({ at: at(line), kind: "command", name: R.clean(input.command) });
606
+
607
+ if (SUBAGENT_TOOLS.has(b.name)) {
608
+ const sub = {
609
+ at: at(line),
610
+ agentType: R.clean(input.subagent_type || input.agent_type || "general-purpose"),
611
+ description: ex(input.description || input.message || ""),
612
+ background: input.run_in_background === true || input.mode === "background",
613
+ model: input.model ? R.clean(input.model) : null,
614
+ status: null,
615
+ resultAt: null,
616
+ notifiedAt: null,
617
+ durationMs: null,
618
+ tokens: null,
619
+ toolUses: null,
620
+ result: null,
621
+ };
622
+ subagents.push(sub);
623
+ if (b.id) subagentByToolId.set(b.id, sub);
624
+ }
625
+ if (SKILL_TOOL.test(b.name || "") && (input.skill || input.command)) {
626
+ invocations.push({
627
+ at: at(line),
628
+ kind: "skill",
629
+ name: R.clean(input.skill || input.command),
630
+ });
631
+ }
632
+ if (b.name === "SlashCommand" && input.command) {
633
+ invocations.push({ at: at(line), kind: "command", name: R.clean(input.command) });
634
+ }
549
635
  }
550
636
  }
551
637
  }
@@ -564,6 +650,7 @@ export async function indexTranscripts(paths, options = {}) {
564
650
  });
565
651
  }
566
652
  }
653
+ fileRow.usagePartial = Boolean(adapt?.usagePartial?.());
567
654
  fileRow.lines = stats.lines;
568
655
  fileRow.malformed = stats.malformed;
569
656
  fileRow.oversized = stats.oversized;
@@ -603,7 +690,7 @@ export async function indexTranscripts(paths, options = {}) {
603
690
  errors: errors.length,
604
691
  malformedLines: files.reduce((a, f) => a + f.malformed, 0),
605
692
  },
606
- usage,
693
+ usage: { ...usage, partial: files.some((f) => f.usagePartial) },
607
694
  turns,
608
695
  loops,
609
696
  stalls,
@@ -685,22 +772,99 @@ async function windowMatches(dirs, start, end) {
685
772
  return files;
686
773
  }
687
774
 
775
+ /** Every Copilot CLI session log under `root` (`<session>/events.jsonl`). */
776
+ function copilotLogs(root) {
777
+ let dirs;
778
+ try {
779
+ dirs = readdirSync(root, { withFileTypes: true });
780
+ } catch {
781
+ return [];
782
+ }
783
+ return dirs
784
+ .filter((e) => e.isDirectory())
785
+ .map((e) => join(root, e.name, COPILOT_LOG))
786
+ .filter((p) => existsSync(p))
787
+ .sort();
788
+ }
789
+
790
+ const CODEX_ROLLOUT = /^rollout-.*\.jsonl$/;
791
+
792
+ /** Every Codex rollout under `root` (`YYYY/MM/DD/rollout-*.jsonl`), at most four levels down. */
793
+ function codexRollouts(root, depth = 0) {
794
+ let entries;
795
+ try {
796
+ entries = readdirSync(root, { withFileTypes: true });
797
+ } catch {
798
+ return [];
799
+ }
800
+ const out = [];
801
+ for (const e of entries) {
802
+ const p = join(root, e.name);
803
+ if (e.isDirectory() && depth < 3) out.push(...codexRollouts(p, depth + 1));
804
+ else if (e.isFile() && CODEX_ROLLOUT.test(e.name)) out.push(p);
805
+ }
806
+ return out.sort();
807
+ }
808
+
809
+ /** The first timestamp and the starting cwd of a Copilot CLI or Codex session. */
810
+ async function sessionHead(path) {
811
+ let first = null;
812
+ let cwd = null;
813
+ let seen = 0;
814
+ for await (const row of readJsonl(path, { maxBytes: 1024 * 1024 })) {
815
+ if (!row.record) continue;
816
+ if (first === null) first = tsOf(row.record);
817
+ cwd = cwd ?? sessionCwd(row.record);
818
+ if ((first !== null && cwd !== null) || ++seen >= 8) break;
819
+ }
820
+ return { first, cwd };
821
+ }
822
+
823
+ const samePath = (p) => String(p).replace(/\/+$/, "");
824
+
825
+ async function hostWindowMatches(paths, cwds, start, end) {
826
+ const files = [];
827
+ for (const p of paths) {
828
+ let mtime;
829
+ try {
830
+ mtime = statSync(p).mtimeMs;
831
+ } catch {
832
+ continue;
833
+ }
834
+ if (mtime < start) continue;
835
+ const head = await sessionHead(p);
836
+ if (head.first === null || head.first > end) continue;
837
+ if (head.cwd === null || !cwds.has(samePath(head.cwd))) continue;
838
+ files.push(p);
839
+ }
840
+ return files;
841
+ }
842
+
688
843
  /**
689
- * The session files a run ran in.
844
+ * The session files a run ran in, across Claude Code, Copilot CLI and Codex.
690
845
  *
691
846
  * A run launched with a session id (continuous mode, a client that set
692
847
  * MULTI_AGENT_SESSION_ID) has it recorded in agent-state.json, and the file is
693
- * `<sessionId>.jsonl` in whichever project directory holds it. An attended run
694
- * records none, so the fallback is the sessions whose working directory was
695
- * the worktree or the main checkout and whose time span overlaps the run's:
696
- * started before the run finished, last written after it started. That match
697
- * is a heuristic and is reported as `time-window`.
848
+ * `<sessionId>.jsonl` in whichever Claude Code project directory holds it,
849
+ * `<sessionId>/events.jsonl` under Copilot CLI's session-state, or the Codex
850
+ * rollout whose name ends in `-<sessionId>.jsonl`. An attended run records
851
+ * none, so the fallback is the sessions whose working directory was the
852
+ * worktree or the main checkout and whose time span overlaps the run's:
853
+ * started before the run finished, last written after it started. Claude Code
854
+ * keys its directories by the working directory; Copilot CLI and Codex record
855
+ * it in the session's opening record. That match is a heuristic and is
856
+ * reported as `time-window`.
698
857
  *
699
858
  * @returns {Promise<{taskId:string|null, runDir:string|null, matchedBy:"session"|"time-window"|null, files:string[], reason?:string}>}
700
859
  */
701
860
  export async function findRunTranscripts(
702
861
  id,
703
- { logsRoot: root = logsRoot(), projectsRoot = defaultProjectsRoot() } = {},
862
+ {
863
+ logsRoot: root = logsRoot(),
864
+ projectsRoot = defaultProjectsRoot(),
865
+ copilotRoot = defaultCopilotRoot(),
866
+ codexRoot = defaultCodexRoot(),
867
+ } = {},
704
868
  ) {
705
869
  const hit = locateRun(id, root);
706
870
  if (!hit)
@@ -715,17 +879,24 @@ export async function findRunTranscripts(
715
879
  const base = { taskId: doc.taskId || hit.run.taskId, runDir: hit.run.dir };
716
880
  const none = (reason) => ({ ...base, matchedBy: null, files: [], reason });
717
881
 
718
- let projectDirs;
882
+ let projectDirs = [];
719
883
  try {
720
884
  projectDirs = readdirSync(projectsRoot).map((d) => join(projectsRoot, d));
721
885
  } catch {
722
- return none("no transcript directory");
886
+ // No Claude Code transcripts on this machine; the other hosts may have some.
723
887
  }
888
+ const copilot = copilotLogs(copilotRoot);
889
+ const codex = codexRollouts(codexRoot);
890
+ if (!projectDirs.length && !copilot.length && !codex.length)
891
+ return none("no transcript directory");
724
892
 
725
893
  if (typeof doc.sessionId === "string" && SAFE_SESSION_ID.test(doc.sessionId)) {
726
- const files = projectDirs
727
- .map((d) => join(d, `${doc.sessionId}.jsonl`))
728
- .filter((p) => existsSync(p));
894
+ const sid = doc.sessionId;
895
+ const files = [
896
+ ...projectDirs.map((d) => join(d, `${sid}.jsonl`)).filter((p) => existsSync(p)),
897
+ ...copilot.filter((p) => basename(dirname(p)) === sid),
898
+ ...codex.filter((p) => basename(p).endsWith(`-${sid}.jsonl`)),
899
+ ];
729
900
  if (files.length) return { ...base, matchedBy: "session", files };
730
901
  }
731
902
 
@@ -741,12 +912,16 @@ export async function findRunTranscripts(
741
912
  const cwds = [doc.worktreePath, doc.projectRoot];
742
913
  for (const p of Array.isArray(doc.projects) ? doc.projects : [])
743
914
  cwds.push(p?.worktreePath, p?.projectRoot);
744
- const slugs = new Set(cwds.filter((c) => typeof c === "string" && c).map(projectSlug));
915
+ const known = cwds.filter((c) => typeof c === "string" && c);
916
+ const slugs = new Set(known.map(projectSlug));
745
917
  const dirs = projectDirs.filter((d) => slugs.has(basename(d)));
746
- if (!Number.isFinite(start) || !dirs.length)
918
+ if (!Number.isFinite(start) || (!dirs.length && !copilot.length && !codex.length))
747
919
  return none("no session id recorded and no transcript directory for the run's checkout");
748
920
 
749
- const files = await windowMatches(dirs, start, end);
921
+ const files = [
922
+ ...(await windowMatches(dirs, start, end)),
923
+ ...(await hostWindowMatches([...copilot, ...codex], new Set(known.map(samePath)), start, end)),
924
+ ];
750
925
  return files.length
751
926
  ? { ...base, matchedBy: "time-window", files }
752
927
  : none("no session overlaps the run's time window");
@@ -781,7 +956,10 @@ export function renderIndex(idx) {
781
956
  `${c.malformedLines} malformed line(s) skipped, redacted ${idx.redaction.total} item(s)${by ? ` (${by})` : ""}`,
782
957
  );
783
958
  L.push(
784
- `usage: in ${k(idx.usage.input)} / out ${k(idx.usage.output)} / cache read ${k(idx.usage.cacheRead)} / cache write ${k(idx.usage.cacheWrite)} over ${idx.usage.messages} message(s)`,
959
+ `usage: in ${k(idx.usage.input)} / out ${k(idx.usage.output)} / cache read ${k(idx.usage.cacheRead)} / cache write ${k(idx.usage.cacheWrite)} over ${idx.usage.messages} message(s)` +
960
+ (idx.usage.partial
961
+ ? " - partial: a Copilot CLI session wrote no session.shutdown record, so only its output tokens are counted"
962
+ : ""),
785
963
  );
786
964
  L.push("", "files");
787
965
  for (const f of idx.files) {
@@ -791,8 +969,11 @@ export function renderIndex(idx) {
791
969
  f.oversized ? `oversized ${f.oversized}` : null,
792
970
  f.truncated ? "truncated at the byte cap" : null,
793
971
  f.unreadable ? "unreadable" : null,
972
+ f.usagePartial ? "usage partial (no session.shutdown)" : null,
794
973
  ].filter(Boolean);
795
- L.push(` ${f.name} ${f.lines} lines${flags.length ? ` ${flags.join(" ")}` : ""}`);
974
+ L.push(
975
+ ` ${f.name} ${f.format ?? "empty"} ${f.lines} lines${flags.length ? ` ${flags.join(" ")}` : ""}`,
976
+ );
796
977
  }
797
978
  L.push("", `turns (${idx.turns.length})`);
798
979
  idx.turns.forEach((t, i) => {
@@ -821,7 +1002,7 @@ export function renderIndex(idx) {
821
1002
  for (const m of idx.compactions)
822
1003
  L.push(
823
1004
  m.kind === "boundary"
824
- ? ` ${m.at} ${m.trigger} ${k(m.preTokens ?? 0)} -> ${k(m.postTokens ?? 0)} tokens`
1005
+ ? ` ${m.at} ${m.trigger} ${m.preTokens === null ? "?" : k(m.preTokens)} -> ${m.postTokens === null ? "?" : k(m.postTokens)} tokens`
825
1006
  : ` ${m.at} summary turn`,
826
1007
  );
827
1008
  L.push("", `subagents (${idx.subagents.length})`);
@@ -913,7 +1094,7 @@ async function main() {
913
1094
  taskId: run.taskId ? R.clean(run.taskId) : null,
914
1095
  runDir: run.runDir ? tilde(run.runDir) : null,
915
1096
  matchedBy: run.matchedBy,
916
- files: run.files.map((f) => basename(f)),
1097
+ files: run.files.map((f) => displayName(f)),
917
1098
  reason: run.reason ?? null,
918
1099
  };
919
1100
  process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);