@shield-agent/kya 0.1.39 → 0.1.41

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 (40) hide show
  1. package/.mcp.json +1 -1
  2. package/README.md +3 -3
  3. package/amp/settings.example.json +1 -1
  4. package/claude/claude_desktop_config.example.json +1 -1
  5. package/cline/cline_mcp_settings.example.json +1 -1
  6. package/copilot/mcp-config.example.json +1 -1
  7. package/dist/commands/connect.d.ts +15 -0
  8. package/dist/commands/connect.d.ts.map +1 -1
  9. package/dist/commands/connect.js +4 -4
  10. package/dist/commands/connect.js.map +1 -1
  11. package/dist/commands/orr.d.ts.map +1 -1
  12. package/dist/commands/orr.js +3 -3
  13. package/dist/commands/orr.js.map +1 -1
  14. package/dist/receipt/enrich.d.ts +45 -0
  15. package/dist/receipt/enrich.d.ts.map +1 -0
  16. package/dist/receipt/enrich.js +205 -0
  17. package/dist/receipt/enrich.js.map +1 -0
  18. package/dist/receipt/render-receipt.d.ts +47 -0
  19. package/dist/receipt/render-receipt.d.ts.map +1 -1
  20. package/dist/receipt/render-receipt.js +433 -14
  21. package/dist/receipt/render-receipt.js.map +1 -1
  22. package/dist/showback/usage-file.d.ts +12 -0
  23. package/dist/showback/usage-file.d.ts.map +1 -0
  24. package/dist/showback/usage-file.js +14 -0
  25. package/dist/showback/usage-file.js.map +1 -0
  26. package/droid/README.md +1 -1
  27. package/droid/mcp.example.json +1 -1
  28. package/gemini/settings.example.json +1 -1
  29. package/grok/README.md +1 -1
  30. package/kilo/kilo.example.json +1 -1
  31. package/kimi/mcp.example.json +1 -1
  32. package/kiro/mcp.example.json +1 -1
  33. package/manifest.json +1 -1
  34. package/mastracode/mcp.example.json +1 -1
  35. package/mcp.json +1 -1
  36. package/openai/codex.config.example.toml +1 -1
  37. package/opencode/opencode.example.json +1 -1
  38. package/package.json +1 -1
  39. package/qwen/settings.example.json +1 -1
  40. package/server.json +2 -2
@@ -1,4 +1,7 @@
1
1
  import { type TrailEvent } from "../trail.js";
2
+ import type { KyaFileConfig } from "../config.js";
3
+ import { type OrrCard, type SandboxCard, type WiredHostRow } from "./enrich.js";
4
+ import { type ShowbackReport } from "../showback/cost-per-task.js";
2
5
  export interface ReceiptModel {
3
6
  readonly title: string;
4
7
  readonly rangeLabel: string;
@@ -14,7 +17,51 @@ export interface ReceiptModel {
14
17
  }[];
15
18
  /** When true, page connects to /events SSE for live refresh. */
16
19
  readonly live?: boolean;
20
+ /** Local identity from .kya/config.json (inert text; never a link). */
21
+ readonly identity?: KyaFileConfig;
22
+ /** Sandbox state + configured KYA_SANDBOX backend. */
23
+ readonly sandboxes?: SandboxCard;
24
+ /** Per-host wiring status across the connect registry. */
25
+ readonly wiredHosts?: readonly WiredHostRow[];
26
+ /** Slim ORR card from orr-report/report.json. */
27
+ readonly orr?: OrrCard;
28
+ /** Observe-only showback from .kya/usage.json. */
29
+ readonly showback?: ShowbackReport;
17
30
  }
31
+ export type WorstVerdict = "never" | "deny" | "hold" | "allow";
32
+ export interface SessionAggregate {
33
+ readonly sessionId: string;
34
+ readonly events: number;
35
+ readonly deny: number;
36
+ readonly hold: number;
37
+ readonly never: number;
38
+ readonly lastTs: string;
39
+ /** never > deny > hold > allow. */
40
+ readonly worst: WorstVerdict;
41
+ }
42
+ export interface CountRow {
43
+ readonly label: string;
44
+ readonly count: number;
45
+ }
46
+ export interface EventAggregates {
47
+ readonly modes: {
48
+ readonly observe: number;
49
+ readonly hold: number;
50
+ readonly offline: number;
51
+ };
52
+ readonly planes: {
53
+ readonly ide: number;
54
+ readonly runtime: number;
55
+ };
56
+ /** Top 8 sessions by recency. */
57
+ readonly sessions: readonly SessionAggregate[];
58
+ /** Top 8 reason codes by count. */
59
+ readonly reasons: readonly CountRow[];
60
+ /** Counts by productLabel, sorted desc. */
61
+ readonly products: readonly CountRow[];
62
+ }
63
+ /** Pure rollup of trail events for the report's stat chips and sections. */
64
+ export declare function aggregateEvents(events: readonly TrailEvent[]): EventAggregates;
18
65
  export declare function buildReceiptModel(sessionId: string, events: readonly TrailEvent[], extras?: Partial<Omit<ReceiptModel, "title" | "events" | "generatedAt" | "rangeLabel">>): ReceiptModel;
19
66
  export declare function buildWindowReceiptModel(events: readonly TrailEvent[], days: number, extras?: Partial<Omit<ReceiptModel, "title" | "events" | "generatedAt" | "rangeLabel">>): ReceiptModel;
20
67
  /** Shared loader for static artifacts and the live loopback server. */
@@ -1 +1 @@
1
- {"version":3,"file":"render-receipt.d.ts","sourceRoot":"","sources":["../../src/receipt/render-receipt.ts"],"names":[],"mappings":"AAKA,OAAO,EAA2C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAEvF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpF,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAyED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC,CAAC,GACtF,YAAY,CAWd;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC,CAAC,GACtF,YAAY,CAWd;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB,GAAG,YAAY,CAOf;AAiDD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CA2S7D;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAsBjE"}
1
+ {"version":3,"file":"render-receipt.d.ts","sourceRoot":"","sources":["../../src/receipt/render-receipt.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@
4
4
  import { assertNoSecrets, clip } from "../dash/render.js";
5
5
  import { clipMultiline, DIFF_MAX_TOTAL_CHARS } from "../diff-preview.js";
6
6
  import { productLabel, readTrail, readTrailSince } from "../trail.js";
7
+ import { loadIdentity, loadOrrCard, loadSandboxes, loadShowbackCard, loadWiredHosts, } from "./enrich.js";
8
+ import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
7
9
  function esc(s) {
8
10
  return s
9
11
  .replace(/&/g, "&amp;")
@@ -84,6 +86,67 @@ function verdictWord(verdict) {
84
86
  return "HOLD";
85
87
  return v;
86
88
  }
89
+ /** Pure rollup of trail events for the report's stat chips and sections. */
90
+ export function aggregateEvents(events) {
91
+ const modes = { observe: 0, hold: 0, offline: 0 };
92
+ const planes = { ide: 0, runtime: 0 };
93
+ const reasonCounts = new Map();
94
+ const productCounts = new Map();
95
+ const bySession = new Map();
96
+ for (const e of events) {
97
+ if (e.mode === "observe")
98
+ modes.observe++;
99
+ else if (e.mode === "hold")
100
+ modes.hold++;
101
+ else if (e.mode === "offline")
102
+ modes.offline++;
103
+ if (e.host === "ide")
104
+ planes.ide++;
105
+ else if (e.host === "runtime")
106
+ planes.runtime++;
107
+ reasonCounts.set(e.reasonCode, (reasonCounts.get(e.reasonCode) ?? 0) + 1);
108
+ const pl = productLabel(e.product);
109
+ productCounts.set(pl, (productCounts.get(pl) ?? 0) + 1);
110
+ const sid = e.sessionId || "unknown";
111
+ let s = bySession.get(sid);
112
+ if (!s) {
113
+ s = { events: 0, deny: 0, hold: 0, never: 0, lastTs: e.ts };
114
+ bySession.set(sid, s);
115
+ }
116
+ s.events++;
117
+ if (e.neverEvent || e.reasonCode === "NEVER_EVENT")
118
+ s.never++;
119
+ const v = e.verdict.toUpperCase();
120
+ if (v === "DENY")
121
+ s.deny++;
122
+ else if (v === "REQUIRE_APPROVE")
123
+ s.hold++;
124
+ if (e.ts > s.lastTs)
125
+ s.lastTs = e.ts;
126
+ }
127
+ const sessions = [...bySession.entries()]
128
+ .map(([sessionId, s]) => ({
129
+ sessionId,
130
+ ...s,
131
+ worst: (s.never > 0
132
+ ? "never"
133
+ : s.deny > 0
134
+ ? "deny"
135
+ : s.hold > 0
136
+ ? "hold"
137
+ : "allow"),
138
+ }))
139
+ .sort((a, b) => b.lastTs.localeCompare(a.lastTs))
140
+ .slice(0, 8);
141
+ const reasons = [...reasonCounts.entries()]
142
+ .map(([label, count]) => ({ label, count }))
143
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label))
144
+ .slice(0, 8);
145
+ const products = [...productCounts.entries()]
146
+ .map(([label, count]) => ({ label, count }))
147
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
148
+ return { modes, planes, sessions, reasons, products };
149
+ }
87
150
  export function buildReceiptModel(sessionId, events, extras) {
88
151
  const filtered = events.filter((e) => e.sessionId === sessionId);
89
152
  return {
@@ -94,6 +157,11 @@ export function buildReceiptModel(sessionId, events, extras) {
94
157
  spend: extras?.spend,
95
158
  mcpSeen: extras?.mcpSeen,
96
159
  live: extras?.live,
160
+ identity: extras?.identity,
161
+ sandboxes: extras?.sandboxes,
162
+ wiredHosts: extras?.wiredHosts,
163
+ orr: extras?.orr,
164
+ showback: extras?.showback,
97
165
  };
98
166
  }
99
167
  export function buildWindowReceiptModel(events, days, extras) {
@@ -106,16 +174,42 @@ export function buildWindowReceiptModel(events, days, extras) {
106
174
  spend: extras?.spend,
107
175
  mcpSeen: extras?.mcpSeen,
108
176
  live: extras?.live,
177
+ identity: extras?.identity,
178
+ sandboxes: extras?.sandboxes,
179
+ wiredHosts: extras?.wiredHosts,
180
+ orr: extras?.orr,
181
+ showback: extras?.showback,
109
182
  };
110
183
  }
111
184
  /** Shared loader for static artifacts and the live loopback server. */
112
185
  export function loadReceiptModel(input) {
113
186
  const days = input.days > 0 ? Math.floor(input.days) : 3;
187
+ const showback = loadShowbackCard(input.cwd);
188
+ const extras = {
189
+ live: input.live,
190
+ identity: loadIdentity(input.cwd),
191
+ sandboxes: loadSandboxes(input.cwd),
192
+ wiredHosts: loadWiredHosts(input.cwd),
193
+ orr: loadOrrCard(input.cwd),
194
+ showback,
195
+ spend: showback
196
+ ? {
197
+ tokens: showback.totalTokensIn + showback.totalTokensOut,
198
+ usdEstimate: showback.estimatedUsd ?? undefined,
199
+ }
200
+ : undefined,
201
+ };
114
202
  if (input.sessionId) {
115
- return buildReceiptModel(input.sessionId, readTrail(input.cwd), { live: input.live });
203
+ return buildReceiptModel(input.sessionId, readTrail(input.cwd), extras);
116
204
  }
117
205
  const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
118
- return buildWindowReceiptModel(readTrailSince(input.cwd, since), days, { live: input.live });
206
+ return buildWindowReceiptModel(readTrailSince(input.cwd, since), days, extras);
207
+ }
208
+ function modeBadge(mode) {
209
+ const letter = mode === "observe" ? "O" : mode === "hold" ? "H" : mode === "offline" ? "X" : "";
210
+ if (!letter)
211
+ return "";
212
+ return `<span class="modeb" title="${letter === "O" ? "observe" : letter === "H" ? "hold" : "offline"} mode">${letter}</span>`;
119
213
  }
120
214
  function renderFeed(events, nowMs, live) {
121
215
  if (events.length === 0) {
@@ -145,7 +239,7 @@ function renderFeed(events, nowMs, live) {
145
239
  <div class="rail" aria-hidden="true"><span class="tick"></span></div>
146
240
  <div class="main">
147
241
  <div class="top">
148
- <span class="verdict">${esc(verdictWord(e.verdict))}</span>
242
+ <span class="verdict">${esc(verdictWord(e.verdict))}</span>${modeBadge(e.mode)}
149
243
  <code class="tool">${esc(e.toolId)}</code>
150
244
  <time class="when" datetime="${esc(e.ts)}" title="${esc(e.ts)}">${esc(clock(e.ts))} <span class="rel">${esc(relativeTime(e.ts, nowMs))}</span></time>
151
245
  </div>
@@ -161,6 +255,167 @@ function renderFeed(events, nowMs, live) {
161
255
  }
162
256
  return parts.join("\n");
163
257
  }
258
+ function identityLine(identity) {
259
+ if (!identity)
260
+ return "";
261
+ const nameId = identity.agentName && identity.agentId
262
+ ? `${identity.agentName}/${identity.agentId}`
263
+ : (identity.agentName ?? identity.agentId);
264
+ const bits = [nameId, identity.host, identity.baseUrl].filter((b) => typeof b === "string" && b.trim().length > 0);
265
+ if (bits.length === 0)
266
+ return "";
267
+ // baseUrl is inert text here — never an anchor.
268
+ return `<div class="identity">${esc(clip(bits.join(" · "), 160))}</div>`;
269
+ }
270
+ function statChips(agg) {
271
+ const chip = (label, n, cls = "") => n > 0 ? `<span class="stat${cls ? ` ${cls}` : ""}">${label}<b>${n}</b></span>` : "";
272
+ return {
273
+ modes: chip("Mode: observe", agg.modes.observe) +
274
+ chip("Mode: hold", agg.modes.hold, "warn") +
275
+ chip("Mode: offline", agg.modes.offline),
276
+ planes: chip("IDE", agg.planes.ide) + chip("Runtime", agg.planes.runtime),
277
+ products: agg.products.length >= 2
278
+ ? `<div class="stats" aria-label="Products">${agg.products
279
+ .map((p) => `<span class="stat">${esc(clip(p.label, 40))}<b>${p.count}</b></span>`)
280
+ .join("")}</div>`
281
+ : "",
282
+ };
283
+ }
284
+ function sessionsPanel(agg, nowMs) {
285
+ if (agg.sessions.length === 0)
286
+ return "";
287
+ const rows = agg.sessions
288
+ .map((s) => `<li>
289
+ <span class="wdot ${s.worst}" title="worst verdict: ${s.worst}"></span>
290
+ <code class="sid" title="${esc(s.sessionId)}">${esc(clip(s.sessionId, 24))}</code>
291
+ <span class="cnt">${s.events} event${s.events === 1 ? "" : "s"}</span>
292
+ <span class="rel">${esc(relativeTime(s.lastTs, nowMs))}</span>
293
+ </li>`)
294
+ .join("\n");
295
+ return `<section class="panel" aria-label="Sessions">
296
+ <h2>Sessions</h2>
297
+ <ul class="rows">
298
+ ${rows}
299
+ </ul>
300
+ </section>`;
301
+ }
302
+ function reasonsPanel(agg) {
303
+ if (agg.reasons.length === 0)
304
+ return "";
305
+ const chips = agg.reasons
306
+ .map((r) => `<span class="chip"><code>${esc(clip(r.label, 60))}</code> × ${r.count}</span>`)
307
+ .join("\n ");
308
+ return `<section class="panel" aria-label="Reasons">
309
+ <h2>Reasons</h2>
310
+ <div class="chiprow">
311
+ ${chips}
312
+ </div>
313
+ </section>`;
314
+ }
315
+ function wiredHostsPanel(hosts) {
316
+ if (!hosts || hosts.length === 0)
317
+ return "";
318
+ if (!hosts.some((h) => h.wired !== "none"))
319
+ return "";
320
+ const rank = (h) => h.wired !== "none" ? 0 : h.recipeOnly ? 1 : 2;
321
+ const rows = [...hosts]
322
+ .sort((a, b) => rank(a) - rank(b))
323
+ .map((h) => {
324
+ if (h.recipeOnly) {
325
+ return `<li class="mute">${esc(h.label)} — manual setup <span class="hint" title="${esc(h.recipeOnly)}">docs recipe</span></li>`;
326
+ }
327
+ if (h.wired === "none") {
328
+ return `<li class="mute">${esc(h.label)} — not wired</li>`;
329
+ }
330
+ const reload = h.reload ? ` · ${esc(h.reload.reload)}` : "";
331
+ const running = h.running ? "running" : "not running";
332
+ return `<li>${esc(h.label)} — wired (${h.wired})${reload} · ${running}</li>`;
333
+ })
334
+ .join("\n ");
335
+ return `<section class="panel" aria-label="Wired hosts">
336
+ <h2>Wired hosts</h2>
337
+ <ul class="rows">
338
+ ${rows}
339
+ </ul>
340
+ </section>`;
341
+ }
342
+ function sandboxesPanel(card, nowMs) {
343
+ if (!card)
344
+ return "";
345
+ if (card.sandboxes.length === 0 && !card.backend)
346
+ return "";
347
+ const backend = card.backend
348
+ ? `<p class="sub">Configured backend: <code>${esc(clip(card.backend, 40))}</code></p>`
349
+ : "";
350
+ const tbl = card.sandboxes.length === 0
351
+ ? ""
352
+ : `<table class="tbl">
353
+ <thead><tr><th>id</th><th>backend</th><th>status</th><th>created</th></tr></thead>
354
+ <tbody>
355
+ ${card.sandboxes
356
+ .map((s) => ` <tr>
357
+ <td><code title="${esc(s.sandboxId)}">${esc(clip(s.sandboxId, 20))}</code></td>
358
+ <td>${esc(s.backend)}</td>
359
+ <td><span class="pill ${s.status === "running" ? "ok" : "mute"}">${esc(s.status)}</span></td>
360
+ <td>${esc(relativeTime(s.createdAt, nowMs))}</td>
361
+ </tr>`)
362
+ .join("\n")}
363
+ </tbody>
364
+ </table>`;
365
+ return `<section class="panel" aria-label="Sandboxes">
366
+ <h2>Sandboxes</h2>
367
+ ${backend}
368
+ ${tbl}
369
+ </section>`;
370
+ }
371
+ function orrPanel(orr, nowMs) {
372
+ if (!orr)
373
+ return "";
374
+ const target = orr.targetName
375
+ ? `<span class="mute">target ${esc(clip(orr.targetName, 60))}</span>`
376
+ : "";
377
+ const failure = orr.primaryFailureMode
378
+ ? `<p class="line">Primary failure mode: ${esc(clip(orr.primaryFailureMode, 200))}</p>`
379
+ : "";
380
+ const fix = orr.mostUrgentFix
381
+ ? `<p class="line">Most urgent fix: ${esc(clip(orr.mostUrgentFix, 200))}</p>`
382
+ : "";
383
+ const when = orr.generatedAt ? ` · ${esc(relativeTime(orr.generatedAt, nowMs))}` : "";
384
+ return `<section class="panel" aria-label="ORR">
385
+ <h2>Operational readiness</h2>
386
+ <div class="orrline">
387
+ <span class="pill orr-${esc(orr.overall)}">${esc(orr.overall)}</span>
388
+ <span class="disp">${esc(orr.disposition.replace(/_/g, " "))}</span>
389
+ ${target}
390
+ </div>
391
+ ${failure}
392
+ ${fix}
393
+ <p class="mute small">scorecards: ${orr.scorecards.pass} pass · ${orr.scorecards.fail} fail · ${orr.scorecards.partial} partial · ${orr.scorecards.notEvaluated} n/e${when}</p>
394
+ </section>`;
395
+ }
396
+ function usdText(usd) {
397
+ return usd === null ? "USD n/a" : `~$${usd.toFixed(2)}`;
398
+ }
399
+ function showbackPanel(report) {
400
+ if (!report)
401
+ return "";
402
+ const topRuns = [...report.perRun]
403
+ .sort((a, b) => (b.estimatedUsd ?? -1) - (a.estimatedUsd ?? -1))
404
+ .slice(0, 5);
405
+ const runs = topRuns.length === 0
406
+ ? ""
407
+ : `<ul class="rows">
408
+ ${topRuns
409
+ .map((r) => ` <li><code title="${esc(r.runId)}">${esc(clip(r.runId, 32))}</code> — ${r.steps} step${r.steps === 1 ? "" : "s"} · ${esc(usdText(r.estimatedUsd))}</li>`)
410
+ .join("\n")}
411
+ </ul>`;
412
+ return `<section class="panel" aria-label="Showback">
413
+ <h2>Showback (observe only)</h2>
414
+ <p class="line">${report.totalTokensIn} tokens in · ${report.totalTokensOut} tokens out · ${esc(usdText(report.estimatedUsd))}</p>
415
+ ${runs}
416
+ <p class="mute small">${esc(SHOWBACK_DISCLAIMER)}</p>
417
+ </section>`;
418
+ }
164
419
  export function renderReceiptHtml(model) {
165
420
  const nowMs = Date.now();
166
421
  const events = [...model.events].map((e) => ({
@@ -172,6 +427,8 @@ export function renderReceiptHtml(model) {
172
427
  diffPreview: e.diffPreview ? clipMultiline(e.diffPreview, DIFF_MAX_TOTAL_CHARS) : undefined,
173
428
  }));
174
429
  const c = countVerdicts(events);
430
+ const agg = aggregateEvents(events);
431
+ const chips = statChips(agg);
175
432
  const blocked = events.filter((e) => e.neverEvent || e.reasonCode === "NEVER_EVENT");
176
433
  const tools = model.mcpSeen && model.mcpSeen.length > 0
177
434
  ? model.mcpSeen.map((m) => m.toolId)
@@ -296,6 +553,81 @@ export function renderReceiptHtml(model) {
296
553
  .stat.bad b { color: var(--bad); }
297
554
  .stat.warn b { color: var(--warn); }
298
555
  .stat.ok b { color: var(--ok); }
556
+ .identity {
557
+ margin-top: 0.45rem; color: var(--mute); font-size: 0.78rem;
558
+ word-break: break-word;
559
+ }
560
+ .modeb {
561
+ display: inline-block; font-size: 0.6rem; font-weight: 700;
562
+ color: var(--mute); border: 1px solid var(--line); border-radius: 4px;
563
+ padding: 0 0.28rem; line-height: 1.1rem; vertical-align: baseline;
564
+ }
565
+ .panel {
566
+ background: var(--card); border: 1px solid var(--line);
567
+ border-radius: 12px; padding: 0.7rem 0.9rem 0.8rem; margin-bottom: 1rem;
568
+ }
569
+ .panel h2 {
570
+ margin: 0 0 0.5rem; font-size: 0.72rem; font-weight: 700;
571
+ letter-spacing: 0.07em; text-transform: uppercase; color: var(--day);
572
+ }
573
+ .panel .sub { margin: 0 0 0.45rem; color: var(--mute); font-size: 0.8rem; }
574
+ .panel .line { margin: 0.25rem 0; font-size: 0.84rem; word-break: break-word; }
575
+ .panel .mute { color: var(--mute); }
576
+ .panel .small { font-size: 0.74rem; }
577
+ .rows {
578
+ list-style: none; margin: 0; padding: 0;
579
+ display: flex; flex-direction: column; gap: 0.3rem;
580
+ font-size: 0.82rem;
581
+ }
582
+ .rows li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.45rem; min-width: 0; }
583
+ .rows li.mute { color: var(--mute); }
584
+ .rows .sid, .rows code {
585
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
586
+ font-size: 0.78rem; word-break: break-all;
587
+ }
588
+ .rows .cnt { color: var(--mute); font-size: 0.76rem; }
589
+ .rows .rel { margin-left: auto; color: var(--mute); font-size: 0.74rem; white-space: nowrap; }
590
+ .rows .hint {
591
+ font-size: 0.72rem; border-bottom: 1px dotted var(--mute); cursor: help;
592
+ }
593
+ .wdot {
594
+ width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: none;
595
+ align-self: center; background: var(--mute);
596
+ }
597
+ .wdot.allow { background: var(--ok); }
598
+ .wdot.hold { background: var(--warn); }
599
+ .wdot.deny, .wdot.never { background: var(--bad); }
600
+ .chiprow { display: flex; flex-wrap: wrap; gap: 0.35rem; }
601
+ .chip {
602
+ font-size: 0.76rem; color: var(--mute);
603
+ background: color-mix(in srgb, var(--bg) 55%, var(--card));
604
+ border: 1px solid var(--line); border-radius: 999px; padding: 0.15rem 0.55rem;
605
+ }
606
+ .chip code {
607
+ font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
608
+ color: var(--fg); font-size: 0.74rem;
609
+ }
610
+ .tbl { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
611
+ .tbl th {
612
+ text-align: left; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.06em;
613
+ text-transform: uppercase; color: var(--mute); padding: 0.15rem 0.5rem 0.3rem 0;
614
+ }
615
+ .tbl td { padding: 0.2rem 0.5rem 0.2rem 0; border-top: 1px solid var(--line); }
616
+ .tbl code {
617
+ font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace; font-size: 0.76rem;
618
+ }
619
+ .pill {
620
+ display: inline-block; font-size: 0.7rem; font-weight: 700;
621
+ letter-spacing: 0.04em; text-transform: uppercase;
622
+ border-radius: 999px; padding: 0.1rem 0.55rem;
623
+ border: 1px solid var(--line); color: var(--mute);
624
+ }
625
+ .pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
626
+ .pill.orr-green { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
627
+ .pill.orr-amber { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
628
+ .pill.orr-red { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
629
+ .orrline { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.55rem; }
630
+ .orrline .disp { font-size: 0.84rem; font-weight: 600; }
299
631
  .alert {
300
632
  display: flex; flex-wrap: wrap; gap: 0.45rem 0.75rem; align-items: baseline;
301
633
  background: color-mix(in srgb, var(--bad) 12%, var(--card));
@@ -320,7 +652,6 @@ export function renderReceiptHtml(model) {
320
652
  text-transform: uppercase; color: var(--day);
321
653
  background: color-mix(in srgb, var(--bg) 55%, var(--card));
322
654
  border-bottom: 1px solid var(--line);
323
- position: sticky; top: 4.2rem; z-index: 1;
324
655
  }
325
656
  .ev {
326
657
  display: grid; grid-template-columns: 1.1rem 1fr; gap: 0;
@@ -431,17 +762,26 @@ export function renderReceiptHtml(model) {
431
762
  <h1>${esc(model.title)}</h1>
432
763
  <div class="range">${esc(model.rangeLabel)}${livePill}</div>
433
764
  </div>
765
+ ${identityLine(model.identity)}
434
766
  <div class="stats" aria-label="Counts">
435
767
  <span class="stat ok">Allow<b>${c.allow}</b></span>
436
768
  <span class="stat bad">Deny<b>${c.deny}</b></span>
437
769
  <span class="stat warn">Hold<b>${c.require}</b></span>
438
770
  <span class="stat">Never<b>${c.never}</b></span>
771
+ ${chips.modes}${chips.planes}
439
772
  </div>
773
+ ${chips.products}
440
774
  </header>
441
775
  ${blockedBanner}
442
776
  <section class="feed" id="feed" aria-label="Activity feed">
443
777
  ${renderFeed(events, nowMs, model.live)}
444
778
  </section>
779
+ ${sessionsPanel(agg, nowMs)}
780
+ ${reasonsPanel(agg)}
781
+ ${wiredHostsPanel(model.wiredHosts)}
782
+ ${sandboxesPanel(model.sandboxes, nowMs)}
783
+ ${orrPanel(model.orr, nowMs)}
784
+ ${showbackPanel(model.showback)}
445
785
  ${toolsHtml}
446
786
  </main>
447
787
  ${liveScript}
@@ -453,6 +793,8 @@ ${liveScript}
453
793
  }
454
794
  export function renderReceiptMarkdown(model) {
455
795
  const c = countVerdicts(model.events);
796
+ const agg = aggregateEvents(model.events);
797
+ const nowMs = Date.now();
456
798
  const lines = [
457
799
  `# ${model.title}`,
458
800
  "",
@@ -460,17 +802,94 @@ export function renderReceiptMarkdown(model) {
460
802
  "",
461
803
  `Allow ${c.allow} | Deny ${c.deny} | Hold ${c.require} | Never ${c.never}`,
462
804
  "",
463
- "## Feed",
464
- ...[...model.events]
465
- .sort((a, b) => b.ts.localeCompare(a.ts))
466
- .flatMap((e) => {
467
- const sum = e.summary?.trim() ? ` - ${e.summary.trim()}` : "";
468
- const head = `- **${verdictWord(e.verdict)}** \`${e.toolId}\`${sum} - ${productLabel(e.product)} - ${e.reasonCode}`;
469
- if (!e.diffPreview?.trim())
470
- return [head];
471
- return [head, "", "```", e.diffPreview.trim(), "```", ""];
472
- }),
473
805
  ];
806
+ if (model.identity) {
807
+ const id = model.identity;
808
+ const nameId = id.agentName && id.agentId
809
+ ? `${id.agentName}/${id.agentId}`
810
+ : (id.agentName ?? id.agentId);
811
+ const bits = [nameId, id.host, id.baseUrl].filter((b) => typeof b === "string" && b.trim().length > 0);
812
+ if (bits.length > 0) {
813
+ lines.push(bits.join(" · "), "");
814
+ }
815
+ }
816
+ const modeBits = [
817
+ agg.modes.observe > 0 ? `observe ${agg.modes.observe}` : "",
818
+ agg.modes.hold > 0 ? `hold ${agg.modes.hold}` : "",
819
+ agg.modes.offline > 0 ? `offline ${agg.modes.offline}` : "",
820
+ ].filter(Boolean);
821
+ const planeBits = [
822
+ agg.planes.ide > 0 ? `IDE ${agg.planes.ide}` : "",
823
+ agg.planes.runtime > 0 ? `Runtime ${agg.planes.runtime}` : "",
824
+ ].filter(Boolean);
825
+ if (modeBits.length > 0 || planeBits.length > 0) {
826
+ lines.push(`Modes: ${modeBits.join(" | ") || "-"} · Planes: ${planeBits.join(" | ") || "-"}`, "");
827
+ }
828
+ if (agg.products.length >= 2) {
829
+ lines.push("## Products", ...agg.products.map((p) => `- ${p.label} × ${p.count}`), "");
830
+ }
831
+ lines.push("## Feed", ...[...model.events]
832
+ .sort((a, b) => b.ts.localeCompare(a.ts))
833
+ .flatMap((e) => {
834
+ const sum = e.summary?.trim() ? ` - ${e.summary.trim()}` : "";
835
+ const head = `- **${verdictWord(e.verdict)}** \`${e.toolId}\`${sum} - ${productLabel(e.product)} - ${e.reasonCode}`;
836
+ if (!e.diffPreview?.trim())
837
+ return [head];
838
+ return [head, "", "```", e.diffPreview.trim(), "```", ""];
839
+ }), "");
840
+ if (agg.sessions.length > 0) {
841
+ lines.push("## Sessions", ...agg.sessions.map((s) => `- \`${s.sessionId}\` — ${s.events} event${s.events === 1 ? "" : "s"}, worst: ${s.worst}, last ${relativeTime(s.lastTs, nowMs)}`), "");
842
+ }
843
+ if (agg.reasons.length > 0) {
844
+ lines.push("## Reasons", ...agg.reasons.map((r) => `- ${r.label} × ${r.count}`), "");
845
+ }
846
+ const hosts = model.wiredHosts ?? [];
847
+ if (hosts.some((h) => h.wired !== "none")) {
848
+ lines.push("## Wired hosts");
849
+ for (const h of hosts) {
850
+ if (h.recipeOnly) {
851
+ lines.push(`- ${h.label} — manual setup (docs recipe)`);
852
+ }
853
+ else if (h.wired === "none") {
854
+ lines.push(`- ${h.label} — not wired`);
855
+ }
856
+ else {
857
+ const reload = h.reload ? ` · ${h.reload.reload}` : "";
858
+ lines.push(`- ${h.label} — wired (${h.wired})${reload} · ${h.running ? "running" : "not running"}`);
859
+ }
860
+ }
861
+ lines.push("");
862
+ }
863
+ if (model.sandboxes && (model.sandboxes.sandboxes.length > 0 || model.sandboxes.backend)) {
864
+ const sb = model.sandboxes;
865
+ lines.push("## Sandboxes");
866
+ if (sb.backend)
867
+ lines.push(`Configured backend: ${sb.backend}`);
868
+ for (const s of sb.sandboxes) {
869
+ lines.push(`- \`${s.sandboxId}\` — ${s.backend} · ${s.status} · created ${relativeTime(s.createdAt, nowMs)}`);
870
+ }
871
+ lines.push("");
872
+ }
873
+ if (model.orr) {
874
+ const orr = model.orr;
875
+ lines.push("## Operational readiness", `overall: ${orr.overall} · disposition: ${orr.disposition.replace(/_/g, " ")}${orr.targetName ? ` · target ${orr.targetName}` : ""}`);
876
+ if (orr.primaryFailureMode)
877
+ lines.push(`Primary failure mode: ${orr.primaryFailureMode}`);
878
+ if (orr.mostUrgentFix)
879
+ lines.push(`Most urgent fix: ${orr.mostUrgentFix}`);
880
+ lines.push(`scorecards: ${orr.scorecards.pass} pass · ${orr.scorecards.fail} fail · ${orr.scorecards.partial} partial · ${orr.scorecards.notEvaluated} n/e${orr.generatedAt ? ` · ${relativeTime(orr.generatedAt, nowMs)}` : ""}`, "");
881
+ }
882
+ if (model.showback) {
883
+ const sb = model.showback;
884
+ lines.push("## Showback (observe only)", `${sb.totalTokensIn} tokens in · ${sb.totalTokensOut} tokens out · ${usdText(sb.estimatedUsd)}`);
885
+ const topRuns = [...sb.perRun]
886
+ .sort((a, b) => (b.estimatedUsd ?? -1) - (a.estimatedUsd ?? -1))
887
+ .slice(0, 5);
888
+ for (const r of topRuns) {
889
+ lines.push(`- \`${r.runId}\` — ${r.steps} step${r.steps === 1 ? "" : "s"} · ${usdText(r.estimatedUsd)}`);
890
+ }
891
+ lines.push(SHOWBACK_DISCLAIMER, "");
892
+ }
474
893
  const md = `${lines.join("\n")}\n`;
475
894
  assertNoSecrets(md);
476
895
  return md;