@tangle-network/traces 0.23.1 → 1.0.1

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 (75) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-2KF4KSPB.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-2XXABIRL.js +25 -0
  6. package/dist/chunk-4VKPN432.js +138 -0
  7. package/dist/chunk-6GOG5KIK.js +53 -0
  8. package/dist/chunk-7USMM4MA.js +5695 -0
  9. package/dist/chunk-CG5QHQO4.js +492 -0
  10. package/dist/chunk-CQ6UYZG6.js +1217 -0
  11. package/dist/chunk-DU534HAX.js +263 -0
  12. package/dist/chunk-FC4V25CG.js +503 -0
  13. package/dist/chunk-GLUDGUGJ.js +232 -0
  14. package/dist/chunk-GNRXKQEQ.js +120 -0
  15. package/dist/chunk-K2KH2HHD.js +228 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-O52XK7OB.js +19 -0
  18. package/dist/chunk-PYH3PL3N.js +62 -0
  19. package/dist/chunk-QLVP37YB.js +782 -0
  20. package/dist/chunk-QUHUMZ4W.js +759 -0
  21. package/dist/chunk-REO2JQJY.js +535 -0
  22. package/dist/chunk-RZL5GAWH.js +60 -0
  23. package/dist/chunk-TOCNGML5.js +273 -0
  24. package/dist/chunk-UQSAGXPH.js +208 -0
  25. package/dist/chunk-VFOLC4OX.js +407 -0
  26. package/dist/chunk-WF7PTDCZ.js +3921 -0
  27. package/dist/chunk-YAX72TST.js +259 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-J4RAIOXO.js +16 -0
  30. package/dist/improvement-LCCVGGUS.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-XXL7VZWB.js +14 -0
  34. package/dist/mcp-EYIDKBEK.js +85 -0
  35. package/dist/report-E3G2T5TJ.js +338 -0
  36. package/dist/run-watch-RAOOAXOF.js +649 -0
  37. package/dist/supervisor-report-RMPLLBGZ.js +29 -0
  38. package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
  39. package/dist/upload-3R7GRSKS.js +20 -0
  40. package/dist/verify-safe-C5ZXZOZ2.js +8 -0
  41. package/docs/analyze.md +76 -0
  42. package/docs/ask.md +88 -0
  43. package/docs/bundle.md +40 -0
  44. package/docs/check.md +77 -0
  45. package/docs/convert.md +44 -0
  46. package/docs/develop.md +32 -0
  47. package/docs/diff.md +38 -0
  48. package/docs/facts.md +114 -0
  49. package/docs/mcp.md +36 -0
  50. package/docs/migrate-1.0.md +33 -0
  51. package/docs/parsers.md +66 -0
  52. package/docs/release-1.0.0.md +147 -0
  53. package/docs/release-1.0.1.md +4 -0
  54. package/docs/sdk.md +77 -0
  55. package/docs/select.md +70 -0
  56. package/docs/skills.md +46 -0
  57. package/docs/upload.md +47 -0
  58. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  59. package/docs/watch.md +66 -0
  60. package/examples/README.md +9 -0
  61. package/examples/custom-analyst.ts +65 -74
  62. package/install.sh +22 -2
  63. package/package.json +10 -9
  64. package/skills/inspect-agent-traces/SKILL.md +47 -110
  65. package/dist/chunk-7DVZCCZI.js +0 -20983
  66. package/docs/replay-verify.md +0 -123
  67. package/docs/stream-findings.svg +0 -39
  68. package/docs/trace-analysts.md +0 -643
  69. package/examples/custom-backend.ts +0 -21
  70. package/examples/external-engines.ts +0 -62
  71. package/examples/improvement-config.mjs +0 -63
  72. package/examples/observe-and-alert.ts +0 -19
  73. package/examples/register-harness.ts +0 -26
  74. package/skills/build-trace-analyst/SKILL.md +0 -76
  75. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
@@ -0,0 +1,14 @@
1
+ import {
2
+ streamSessions,
3
+ watchSource
4
+ } from "./chunk-GNRXKQEQ.js";
5
+ import "./chunk-REO2JQJY.js";
6
+ import "./chunk-WF7PTDCZ.js";
7
+ import "./chunk-7USMM4MA.js";
8
+ import "./chunk-UQSAGXPH.js";
9
+ import "./chunk-CQ6UYZG6.js";
10
+ import "./chunk-DU534HAX.js";
11
+ export {
12
+ streamSessions,
13
+ watchSource
14
+ };
@@ -0,0 +1,85 @@
1
+ import {
2
+ exportBytes,
3
+ openAgenticTraceStore
4
+ } from "./chunk-2XXABIRL.js";
5
+ import {
6
+ tracesVersion
7
+ } from "./chunk-CQ6UYZG6.js";
8
+ import {
9
+ assessSpans,
10
+ redactSpans
11
+ } from "./chunk-RZL5GAWH.js";
12
+ import "./chunk-DU534HAX.js";
13
+
14
+ // src/mcp.ts
15
+ import {
16
+ buildTraceAnalysisToolDescriptors,
17
+ combineVerdicts,
18
+ redact,
19
+ shareAllowed,
20
+ UNTRUSTED_TRACE_TEXT
21
+ } from "@tangle-network/agent-eval/traces";
22
+ import { createStdioToolServer } from "@tangle-network/agent-runtime/mcp";
23
+ var MCP_RESULT_BYTE_CAP = 512 * 1024;
24
+ function createServedSpans() {
25
+ const chunks = [];
26
+ const verdicts = [];
27
+ return {
28
+ add(spans) {
29
+ if (spans.length === 0) return;
30
+ const { spans: redacted } = redactSpans(spans);
31
+ verdicts.push(assessSpans(redacted));
32
+ chunks.push(exportBytes(redacted));
33
+ },
34
+ finish() {
35
+ return { bytes: Buffer.concat(chunks), verdict: combineVerdicts("default", verdicts) };
36
+ }
37
+ };
38
+ }
39
+ function traceMcpTools(options) {
40
+ const served = options.served ?? (() => {
41
+ const builder = createServedSpans();
42
+ builder.add(options.spans ?? []);
43
+ return builder.finish();
44
+ })();
45
+ const { verdict } = served;
46
+ if (!shareAllowed(verdict)) {
47
+ const findings = verdict.findings.map((f) => `${f.category}/${f.detector} (${f.count})`).join(", ");
48
+ throw new Error(
49
+ `mcp: refusing to serve \u2014 redacted spans are still ${verdict.status}` + (findings ? `: ${findings}` : "") + (verdict.unreadable.length > 0 ? `; unreadable: ${verdict.unreadable.join("; ")}` : "")
50
+ );
51
+ }
52
+ const store = openAgenticTraceStore(served.bytes);
53
+ const descriptors = buildTraceAnalysisToolDescriptors({ store }).filter((d) => d.name !== "readSpanSource");
54
+ return descriptors.map((descriptor) => ({
55
+ name: descriptor.name,
56
+ description: descriptor.description,
57
+ inputSchema: descriptor.parameters,
58
+ annotations: {
59
+ readOnlyHint: descriptor.readOnly,
60
+ idempotentHint: descriptor.idempotent,
61
+ destructiveHint: false,
62
+ openWorldHint: false
63
+ },
64
+ handler: async (args) => {
65
+ const { value } = redact(await descriptor.handler(args));
66
+ const result = { untrusted: UNTRUSTED_TRACE_TEXT, result: value };
67
+ const bytes = Buffer.byteLength(JSON.stringify(result));
68
+ if (bytes > MCP_RESULT_BYTE_CAP) {
69
+ throw new RangeError(
70
+ `${descriptor.name}: result is ${bytes} bytes, above the ${MCP_RESULT_BYTE_CAP}-byte cap; narrow the request`
71
+ );
72
+ }
73
+ return result;
74
+ }
75
+ }));
76
+ }
77
+ async function serveTraceMcp(options) {
78
+ const tools = traceMcpTools(options);
79
+ const server = createStdioToolServer({ serverName: "traces", serverVersion: tracesVersion(), tools });
80
+ await server.serve();
81
+ }
82
+ export {
83
+ createServedSpans,
84
+ serveTraceMcp
85
+ };
@@ -0,0 +1,338 @@
1
+ import {
2
+ describeSessionRelationship,
3
+ minutesText,
4
+ renderFirstFailure,
5
+ sheetText,
6
+ topCounts,
7
+ utcMinute
8
+ } from "./chunk-WF7PTDCZ.js";
9
+ import "./chunk-7USMM4MA.js";
10
+ import {
11
+ ACTOR_ATTR,
12
+ ATTR,
13
+ isInheritedSpan,
14
+ sessionIdFromAttributes
15
+ } from "./chunk-UQSAGXPH.js";
16
+ import "./chunk-CQ6UYZG6.js";
17
+ import "./chunk-DU534HAX.js";
18
+
19
+ // src/report.ts
20
+ var CORRUPTION_RECEIPT_DISPLAY_LIMIT = 100;
21
+ function sessionReportSource(ref, spans, sessionIdOverride) {
22
+ const root = spans.find((item) => item.parent_span_id === null) ?? spans[0];
23
+ const inScope = spans.filter((item) => !isInheritedSpan(item.attributes));
24
+ const prompt = inScope.find(
25
+ (item) => item.name === "user.prompt" && item.attributes[ACTOR_ATTR] === "human"
26
+ ) ?? inScope.find((item) => item.name === "user.prompt") ?? inScope.find(
27
+ (item) => item.attributes["span.type"] === "interaction" && typeof item.attributes.content === "string"
28
+ );
29
+ const content = typeof prompt?.attributes.content === "string" ? prompt.attributes.content : "";
30
+ const firstLine = content.split(/\r?\n/, 1)[0].trim();
31
+ const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
32
+ const role = root?.attributes["traces.session.role"];
33
+ const parentSessionId = root?.attributes["traces.parent_session_id"];
34
+ const relationship = describeSessionRelationship(ref, spans);
35
+ const corruptionDigest = root?.attributes[ATTR.CORRUPTION_DIGEST];
36
+ const sessionId = [
37
+ sessionIdOverride,
38
+ root ? sessionIdFromAttributes(root.attributes) : void 0,
39
+ root?.trace_id,
40
+ ref.sessionId
41
+ ].find((value) => typeof value === "string" && value.length > 0);
42
+ return {
43
+ sessionId,
44
+ path: ref.path,
45
+ subject,
46
+ role: role === "operator" || role === "child" ? role : "unknown",
47
+ ...typeof parentSessionId === "string" ? { parentSessionId } : {},
48
+ childSessionIds: relationship.childSessionIds,
49
+ ...relationship.depth !== void 0 ? { depth: relationship.depth } : {},
50
+ ...relationship.agentNickname ? { agentNickname: relationship.agentNickname } : {},
51
+ ...relationship.agentRole ? { agentRole: relationship.agentRole } : {},
52
+ ...relationship.agentPath ? { agentPath: relationship.agentPath } : {},
53
+ ...relationship.taskScope ? { taskScope: relationship.taskScope } : {},
54
+ ...relationship.turnId ? { turnId: relationship.turnId } : {},
55
+ integrity: ref.integrity?.status ?? "complete",
56
+ ...ref.integrity ? {
57
+ corruptionCount: ref.integrity.corruptions.length,
58
+ ...typeof corruptionDigest === "string" ? { corruptionDigest } : {},
59
+ corruptions: ref.integrity.corruptions.slice(0, CORRUPTION_RECEIPT_DISPLAY_LIMIT)
60
+ } : {}
61
+ };
62
+ }
63
+ var ANALYZE_TEXT_LINES = 40;
64
+ var TABLE_SESSIONS = 15;
65
+ var SEVERITY_LABEL = {
66
+ critical: "CRIT",
67
+ high: "HIGH",
68
+ medium: "MED",
69
+ low: "LOW",
70
+ info: "INFO"
71
+ };
72
+ function plural(count, noun, many = `${noun}s`) {
73
+ return `${count} ${count === 1 ? noun : many}`;
74
+ }
75
+ var short = (id) => id.slice(0, 8);
76
+ function elapsedText(ms) {
77
+ const minutes = ms / 6e4;
78
+ if (minutes < 60) return minutesText(minutes);
79
+ const whole = Math.round(minutes);
80
+ return `${Math.floor(whole / 60)}h ${whole % 60}m`;
81
+ }
82
+ function costText(facts) {
83
+ return facts.cost.usd === null ? `cost unknown: ${sheetText(facts.cost.unavailable ?? "no reason recorded", 60)}` : `$${facts.cost.usd.toFixed(facts.cost.usd < 1 ? 4 : 2)} ${facts.cost.label}`;
84
+ }
85
+ function windowText(first, last) {
86
+ if (first === null || last === null) return null;
87
+ const [from, to] = [utcMinute(first), utcMinute(last)];
88
+ const elapsed = Date.parse(last) - Date.parse(first);
89
+ return `${from}\u2192${from.slice(0, 5) === to.slice(0, 5) ? to.slice(6) : to} UTC${Number.isFinite(elapsed) && elapsed > 0 ? `, ${elapsedText(elapsed)}` : ""}`;
90
+ }
91
+ function placeOf(facts) {
92
+ const repo = facts.repo.value;
93
+ return repo ? `${repo.repository.split("/").at(-1)}${repo.branch ? ` (${repo.branch})` : ""}` : facts.cwd.value?.split("/").filter(Boolean).at(-1);
94
+ }
95
+ function header(facts) {
96
+ const parts = [`${short(facts.sessionId ?? facts.traceId)} ${facts.harness ?? "unknown harness"}`];
97
+ const place = placeOf(facts);
98
+ if (place) parts.push(place);
99
+ const window = windowText(facts.firstRecordAt.value, facts.lastRecordAt.value);
100
+ if (window) parts.push(window);
101
+ if (facts.measurements.activeMinutes.value !== null) parts.push(`active ${minutesText(facts.measurements.activeMinutes.value)}`);
102
+ if (facts.parentSessionId) parts.push(`child of ${short(facts.parentSessionId)}`);
103
+ parts.push(costText(facts));
104
+ return parts.join(" \xB7 ");
105
+ }
106
+ function askedLine(facts) {
107
+ const turns = facts.humanTurns.value;
108
+ if (turns === null) return null;
109
+ const [first, ...rest] = turns;
110
+ if (!first) return null;
111
+ return `asked "${sheetText(first.text, 110)}"${rest.length > 0 ? ` (+${plural(rest.length, "more human turn")})` : ""}`;
112
+ }
113
+ function firstFailureLine(facts) {
114
+ const failure = facts.firstFailure;
115
+ return failure.status === "found" || failure.status === "ambiguous" ? `first failure ${renderFirstFailure(failure)}` : null;
116
+ }
117
+ function commandLines(facts) {
118
+ const waste = facts.measurements.waste;
119
+ const lines = [];
120
+ const streaks = [...waste.failingStreaks.value ?? []].sort((a, b) => b.minutes - a.minutes || b.runs - a.runs);
121
+ if (streaks.length > 0) {
122
+ const shown = streaks.slice(0, 2).map((streak) => {
123
+ const command = streak.command === null ? `a ${streak.tool} command` : `\`${sheetText(streak.command, 60)}\``;
124
+ return `${command} ${streak.runs}\xD7 in a row (${minutesText(streak.minutes)}, ${utcMinute(streak.at).slice(6)}, span ${streak.spanIds[0]})`;
125
+ });
126
+ lines.push(`failing commands ${streaks.length}: ${shown.join(" \xB7 ")}${streaks.length > 2 ? ` \xB7 +${streaks.length - 2} more` : ""}`);
127
+ }
128
+ const repeated = waste.repeatedCommands.value;
129
+ const rollbacks = waste.rollbacks.value ?? 0;
130
+ const rework = [];
131
+ if (repeated && repeated.commands > 0) rework.push(`${plural(repeated.commands, "command")} rerun, ${plural(repeated.extraRuns, "extra run")}`);
132
+ if (rollbacks > 0) rework.push(`${plural(rollbacks, "revert or reset --hard", "reverts or resets --hard")}`);
133
+ if (rework.length > 0) lines.push(`repeated ${rework.join(" \xB7 ")}`);
134
+ return lines;
135
+ }
136
+ function toolErrorLine(errors) {
137
+ if (!errors || errors.errors === 0) return null;
138
+ const clusters = errors.clusters.slice(0, 3).map((cluster) => `${cluster.tool} \xD7${cluster.count} "${sheetText(cluster.error, 50)}" (span ${cluster.spanIds[0]})`);
139
+ return `tool errors ${errors.errors}/${errors.calls}: ${clusters.join(" \xB7 ")}`;
140
+ }
141
+ function stallLine(facts) {
142
+ const stalls = facts.measurements.waste.stalls.value ?? [];
143
+ if (stalls.length === 0) return null;
144
+ const total = stalls.reduce((sum2, stall) => sum2 + stall.minutes, 0);
145
+ const shown = [...stalls].sort((a, b) => b.minutes - a.minutes).slice(0, 2).map((stall) => `${utcMinute(stall.at).slice(6)} ${stall.kind} ${minutesText(stall.minutes)} (span ${stall.spanId})`);
146
+ return `stalls ${stalls.length} \u2265 10 min, ${minutesText(total)}: ${shown.join(" \xB7 ")}${stalls.length > 2 ? ` \xB7 +${stalls.length - 2} more` : ""}`;
147
+ }
148
+ function signalQuote(turn) {
149
+ const when = utcMinute(turn.at).slice(6);
150
+ if (turn.signals.includes("interrupt")) return ` ${when} after an interrupt: "${sheetText(turn.text, 90)}"`;
151
+ if (turn.signals.includes("rejected-tool")) return ` ${when} after a rejected tool call: "${sheetText(turn.text, 90)}"`;
152
+ return ` ${when} "${sheetText(turn.text, 60)}" after the claim "${sheetText(turn.answered ?? "", 60)}"`;
153
+ }
154
+ function interruptLines(facts) {
155
+ const turns = facts.humanTurns.value ?? [];
156
+ const with_ = (signal) => turns.filter((turn) => turn.signals.includes(signal));
157
+ const interrupts = with_("interrupt");
158
+ const rejected = with_("rejected-tool");
159
+ const afterClaim = with_("after-claim");
160
+ if (interrupts.length + rejected.length + afterClaim.length === 0) return [];
161
+ const quoted = [.../* @__PURE__ */ new Set([...interrupts, ...rejected, ...afterClaim])].slice(0, 3);
162
+ return [
163
+ `Interrupts and rejections: ${plural(interrupts.length, "turn")} after an interrupt \xB7 ${plural(rejected.length, "turn")} after a rejected tool call \xB7 ${plural(afterClaim.length, "turn")} right after a completion claim`,
164
+ ...quoted.map(signalQuote)
165
+ ];
166
+ }
167
+ function prList(entries, keep = 6) {
168
+ const ids = entries.map((entry) => entry.identifier === null ? "unnumbered" : /^\d+$/u.test(entry.identifier) ? `#${entry.identifier}` : sheetText(entry.identifier, 30));
169
+ return `${entries.length}${ids.length > 0 ? ` (${ids.slice(0, keep).join(" ")}${ids.length > keep ? ` +${ids.length - keep}` : ""})` : ""}`;
170
+ }
171
+ function prLine(facts) {
172
+ const prs = facts.pullRequests.value;
173
+ if (prs === null || prs.created.length + prs.merged.length === 0) return null;
174
+ const files = facts.changedFiles.value === null ? "" : ` \xB7 changed ${plural(facts.changedFiles.value.length, "file")}`;
175
+ return `PRs opened ${prList(prs.created)} \xB7 merged ${prList(prs.merged)}${files}`;
176
+ }
177
+ function citedSpans(finding) {
178
+ const fromUris = finding.evidence_refs.flatMap((ref) => {
179
+ const match = /\/span\/([^/]+)$/u.exec(ref.uri);
180
+ return match ? [decodeURIComponent(match[1])] : [];
181
+ });
182
+ if (fromUris.length > 0) return [...new Set(fromUris)];
183
+ return finding.evidence_refs.flatMap((ref) => {
184
+ if (!ref.excerpt?.startsWith("{")) return [];
185
+ try {
186
+ const parsed = JSON.parse(ref.excerpt);
187
+ return [parsed.first_span_id, parsed.last_span_id].filter((id) => typeof id === "string");
188
+ } catch {
189
+ return [];
190
+ }
191
+ });
192
+ }
193
+ function findingLine(finding, sessions) {
194
+ const spans = citedSpans(finding);
195
+ const session = sessions > 1 && typeof finding.metadata?.sessionId === "string" ? `${short(finding.metadata.sessionId)} ` : "";
196
+ const cite = spans.length > 0 ? ` (span${spans.length > 1 ? "s" : ""} ${spans.slice(0, 2).join(", ")}${spans.length > 2 ? ", \u2026" : ""})` : "";
197
+ return ` ${SEVERITY_LABEL[finding.severity].padEnd(4)} ${session}${finding.subject ?? finding.area}: ${sheetText(finding.claim, 110)}${cite}`;
198
+ }
199
+ function closingLines(result, used) {
200
+ const findings = result.packet.findings;
201
+ const lines = [];
202
+ if (findings.length > 0) {
203
+ const counts = ["critical", "high", "medium", "low", "info"].map((severity) => [SEVERITY_LABEL[severity], findings.filter((finding) => finding.severity === severity).length]).filter(([, count]) => count > 0).map(([label, count]) => `${label} ${count}`);
204
+ lines.push(`findings ${findings.length} (${counts.join(", ")})`);
205
+ const room = ANALYZE_TEXT_LINES - used - lines.length - 1 - (result.cannotAnswer.length > 0 ? 1 : 0);
206
+ const shown = findings.length > room ? Math.max(0, room - 1) : findings.length;
207
+ for (const finding of findings.slice(0, shown)) lines.push(findingLine(finding, result.facts.sessions.length));
208
+ if (shown < findings.length) lines.push(` \u2026 ${findings.length - shown} more: --format json | jq .packet.findings`);
209
+ } else {
210
+ lines.push("findings none");
211
+ }
212
+ if (result.cannotAnswer.length > 0) {
213
+ lines.push(`cannot answer ${plural(result.cannotAnswer.length, "question")}: --format json | jq .cannotAnswer, or report.md under --out DIR`);
214
+ }
215
+ return lines;
216
+ }
217
+ function singleSession(result, facts) {
218
+ const sessionId = facts.sessionId ?? facts.traceId;
219
+ return [
220
+ header(facts),
221
+ askedLine(facts),
222
+ firstFailureLine(facts),
223
+ ...commandLines(facts),
224
+ toolErrorLine(result.measures.toolErrors.find((entry) => entry.sessionId === sessionId)),
225
+ stallLine(facts),
226
+ ...interruptLines(facts),
227
+ prLine(facts)
228
+ ].filter((line) => line !== null);
229
+ }
230
+ function sum(values) {
231
+ return values.reduce((total, value) => total + (value ?? 0), 0);
232
+ }
233
+ function fleetHeader(result) {
234
+ const sessions = result.facts.sessions;
235
+ const byHarness = {};
236
+ for (const facts of sessions) byHarness[facts.harness ?? "unknown"] = (byHarness[facts.harness ?? "unknown"] ?? 0) + 1;
237
+ const firsts = sessions.map((facts) => facts.firstRecordAt.value).filter((value) => value !== null).sort();
238
+ const lasts = sessions.map((facts) => facts.lastRecordAt.value).filter((value) => value !== null).sort();
239
+ const unpriced = sessions.filter((facts) => facts.cost.usd === null).length;
240
+ const priced = sum(sessions.map((facts) => facts.cost.usd));
241
+ const cost = unpriced === sessions.length ? "cost unknown" : `$${priced.toFixed(2)} list price (Router rates)${unpriced > 0 ? `, ${unpriced} unpriced` : ""}`;
242
+ const wasteKnown = sessions.filter((facts) => facts.measurements.waste.minutes !== null);
243
+ const waste = wasteKnown.length === 0 ? "waste unknown" : `waste ${(sum(wasteKnown.map((facts) => facts.measurements.waste.minutes)) / 60).toFixed(1)} h${wasteKnown.length < sessions.length ? ` (${sessions.length - wasteKnown.length} unknown)` : ""}`;
244
+ const interrupts = sum(sessions.map((facts) => (facts.humanTurns.value ?? []).filter((turn) => turn.signals.includes("interrupt")).length));
245
+ const window = windowText(firsts[0] ?? null, lasts.at(-1) ?? null);
246
+ const lines = [[
247
+ `${plural(sessions.length, "session")} (${topCounts(byHarness, 4)})`,
248
+ ...window ? [window] : [],
249
+ cost,
250
+ waste,
251
+ `${plural(interrupts, "interrupt")}`
252
+ ].join(" \xB7 ")];
253
+ const unread = result.facts.selection?.unreadSessions ?? [];
254
+ if (unread.length > 0) lines.push(`unreadable ${unread.length}: ${unread.slice(0, 2).map((entry) => `${short(entry.sessionId ?? entry.traceId ?? entry.source ?? "?")} ${sheetText(entry.reason, 50)}`).join(" \xB7 ")}`);
255
+ return lines;
256
+ }
257
+ var kTokens = (tokens) => tokens < 1e3 ? String(tokens) : `${Math.round(tokens / 1e3)}k`;
258
+ var seconds = (ms) => `${(ms / 1e3).toFixed(1)} s`;
259
+ function operatingLine(result) {
260
+ const { modelCalls, guardRefusals, limitStops } = result.measures.operating;
261
+ if (modelCalls.calls === 0 && guardRefusals.toolCalls === 0) return void 0;
262
+ const parts = [];
263
+ const { mean, p50 } = modelCalls.contextTokens;
264
+ if (mean !== null) parts.push(`context/call mean ${kTokens(mean)}${p50 === void 0 ? "" : ` p50 ${kTokens(p50)}`}`);
265
+ const bands = modelCalls.latencyByContext;
266
+ if (bands.length > 0) {
267
+ const ends = bands.length === 1 ? [bands[0]] : [bands[0], bands.at(-1)];
268
+ parts.push(`reply p50 ${ends.map((band) => `${seconds(band.p50Ms)} at ${band.band}`).join(", ")}`);
269
+ }
270
+ if (guardRefusals.perThousandCalls !== null) {
271
+ const top = guardRefusals.byGuard[0];
272
+ parts.push(`guard refusals ${guardRefusals.perThousandCalls}/1k calls${top ? ` (${top.guard} ${top.refusals})` : ""}`);
273
+ }
274
+ parts.push(`limit stops ${limitStops.sessions} (${limitStops.terminalFailuresAtLimit}/${limitStops.terminalFailures} terminal failures)`);
275
+ return parts.join(" \xB7 ");
276
+ }
277
+ function cell(text, width) {
278
+ return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text.padEnd(width);
279
+ }
280
+ function sessionTable(result) {
281
+ const sessions = [...result.facts.sessions].sort((a, b) => (b.measurements.waste.minutes ?? -1) - (a.measurements.waste.minutes ?? -1));
282
+ const rows = sessions.slice(0, TABLE_SESSIONS).map((facts) => {
283
+ const waste = facts.measurements.waste;
284
+ const turns = facts.humanTurns.value ?? [];
285
+ const prs = facts.pullRequests.value;
286
+ const signals = turns.filter((turn) => turn.signals.length > 0).length;
287
+ return [
288
+ cell(short(facts.sessionId ?? facts.traceId), 8),
289
+ cell(facts.harness ?? "?", 11),
290
+ cell(placeOf(facts) ?? "?", 22),
291
+ cell(facts.measurements.activeMinutes.value === null ? "?" : minutesText(facts.measurements.activeMinutes.value), 7),
292
+ cell(facts.cost.usd === null ? "?" : `$${facts.cost.usd.toFixed(2)}`, 8),
293
+ cell(waste.minutes === null ? "?" : minutesText(waste.minutes), 7),
294
+ cell(String((waste.failingStreaks.value ?? []).length), 7),
295
+ cell(String((waste.stalls.value ?? []).length), 6),
296
+ cell(String(signals), 7),
297
+ cell(prs === null ? "?" : `${prs.created.length}/${prs.merged.length}`, 5),
298
+ sheetText(turns[0]?.text ?? "", 40)
299
+ ].join(" ").trimEnd();
300
+ });
301
+ const head = ["session ", "harness ", "repo ", "active ", "cost ", "waste ", "streaks", "stalls", "signals", "PRs ", "asked"].join(" ");
302
+ return [head, ...rows, ...sessions.length > TABLE_SESSIONS ? [`\u2026 ${sessions.length - TABLE_SESSIONS} more sessions: --format json | jq .facts.sessions`] : []];
303
+ }
304
+ function renderAnalysis(result) {
305
+ const sessions = result.facts.sessions;
306
+ const operating = sessions.length === 1 ? void 0 : operatingLine(result);
307
+ const body = sessions.length === 1 ? singleSession(result, sessions[0]) : [...fleetHeader(result), ...sessionTable(result), ...operating ? [operating] : []];
308
+ const lines = [...body, ...closingLines(result, body.length)];
309
+ return `${lines.slice(0, ANALYZE_TEXT_LINES).join("\n")}
310
+ `;
311
+ }
312
+ function cannotAnswerLine(entry) {
313
+ return `- **${entry.analysis}**${entry.sessionId ? ` (session \`${entry.sessionId}\`)` : ""}: ${entry.reason}`;
314
+ }
315
+ function renderAnalysisReport(result) {
316
+ return [
317
+ "# traces analyze",
318
+ "",
319
+ "```text",
320
+ renderAnalysis(result).trimEnd(),
321
+ "```",
322
+ "",
323
+ "## Findings",
324
+ ...result.packet.report.split("\n").filter((line) => !/^#{1,2} /u.test(line)),
325
+ "",
326
+ `## What these traces cannot answer (${result.cannotAnswer.length})`,
327
+ "",
328
+ ...result.cannotAnswer.length > 0 ? result.cannotAnswer.map(cannotAnswerLine) : ["Nothing: every analysis had what it reads."],
329
+ ""
330
+ ].join("\n");
331
+ }
332
+ export {
333
+ ANALYZE_TEXT_LINES,
334
+ elapsedText,
335
+ renderAnalysis,
336
+ renderAnalysisReport,
337
+ sessionReportSource
338
+ };