@shield-agent/kya 0.22.0 → 0.23.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 (243) hide show
  1. package/.mcp.json +1 -1
  2. package/README.md +72 -30
  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/certify/attest.js +5 -5
  8. package/dist/certify/catalog.d.ts +1 -1
  9. package/dist/certify/catalog.js +3 -3
  10. package/dist/certify/context.js +1 -1
  11. package/dist/certify/evaluate.d.ts +2 -2
  12. package/dist/certify/evaluate.js +12 -12
  13. package/dist/certify/live.js +2 -2
  14. package/dist/certify/render.d.ts +1 -1
  15. package/dist/certify/render.js +10 -10
  16. package/dist/cli.d.ts +2 -2
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +49 -16
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/certify.d.ts +1 -1
  21. package/dist/commands/certify.js +3 -3
  22. package/dist/commands/connect.d.ts +2 -2
  23. package/dist/commands/connect.js +22 -22
  24. package/dist/commands/dash.js +4 -4
  25. package/dist/commands/gate-serve.js +3 -3
  26. package/dist/commands/gate.d.ts +1 -1
  27. package/dist/commands/gate.js +16 -16
  28. package/dist/commands/hook.d.ts +1 -1
  29. package/dist/commands/hook.d.ts.map +1 -1
  30. package/dist/commands/hook.js +55 -18
  31. package/dist/commands/hook.js.map +1 -1
  32. package/dist/commands/import-traces.d.ts +16 -0
  33. package/dist/commands/import-traces.d.ts.map +1 -0
  34. package/dist/commands/import-traces.js +18 -0
  35. package/dist/commands/import-traces.js.map +1 -0
  36. package/dist/commands/init.js +1 -1
  37. package/dist/commands/investigate.d.ts +13 -0
  38. package/dist/commands/investigate.d.ts.map +1 -0
  39. package/dist/commands/investigate.js +22 -0
  40. package/dist/commands/investigate.js.map +1 -0
  41. package/dist/commands/notify-flush.d.ts +2 -0
  42. package/dist/commands/notify-flush.d.ts.map +1 -0
  43. package/dist/commands/notify-flush.js +47 -0
  44. package/dist/commands/notify-flush.js.map +1 -0
  45. package/dist/commands/orr.d.ts +3 -3
  46. package/dist/commands/orr.js +5 -5
  47. package/dist/commands/receipt-serve.d.ts +1 -1
  48. package/dist/commands/receipt-serve.js +1 -1
  49. package/dist/commands/serve-mcp.js +1 -1
  50. package/dist/commands/start.d.ts +2 -2
  51. package/dist/commands/start.js +20 -20
  52. package/dist/commands/stop.d.ts +1 -1
  53. package/dist/commands/stop.js +6 -6
  54. package/dist/commands/update.d.ts +19 -0
  55. package/dist/commands/update.d.ts.map +1 -0
  56. package/dist/commands/update.js +96 -0
  57. package/dist/commands/update.js.map +1 -0
  58. package/dist/commands/wire-hooks.d.ts +1 -1
  59. package/dist/commands/wire-hooks.js +14 -14
  60. package/dist/commands/wrap.d.ts.map +1 -1
  61. package/dist/commands/wrap.js +12 -7
  62. package/dist/commands/wrap.js.map +1 -1
  63. package/dist/config.d.ts +4 -4
  64. package/dist/config.js +5 -5
  65. package/dist/dash/dash.d.ts +1 -1
  66. package/dist/dash/dash.js +3 -3
  67. package/dist/dash/entitlement.d.ts +1 -1
  68. package/dist/dash/entitlement.js +1 -1
  69. package/dist/dash/render.js +2 -2
  70. package/dist/dash/screens/enterprise.js +9 -9
  71. package/dist/dash/screens/free.js +5 -5
  72. package/dist/diff-preview.d.ts +1 -1
  73. package/dist/diff-preview.js +2 -2
  74. package/dist/errors.d.ts +1 -1
  75. package/dist/errors.js +1 -1
  76. package/dist/fs-atomic.d.ts +1 -1
  77. package/dist/fs-atomic.js +2 -2
  78. package/dist/gate/binary.js +4 -4
  79. package/dist/gate/config-gen.d.ts +1 -1
  80. package/dist/gate/config-gen.js +7 -7
  81. package/dist/gate/config.d.ts +2 -2
  82. package/dist/gate/config.js +6 -6
  83. package/dist/gate/daemon.js +1 -1
  84. package/dist/gate/discover.d.ts +2 -2
  85. package/dist/gate/discover.js +7 -7
  86. package/dist/gate/hosted-sync.js +1 -1
  87. package/dist/gate/otlp-decode.d.ts +2 -2
  88. package/dist/gate/otlp-decode.js +2 -2
  89. package/dist/gate/otlp-receiver.d.ts +1 -1
  90. package/dist/gate/otlp-receiver.js +3 -3
  91. package/dist/host-reload.js +11 -11
  92. package/dist/host-tools.d.ts +1 -1
  93. package/dist/host-tools.js +1 -1
  94. package/dist/importers/formats.d.ts +26 -0
  95. package/dist/importers/formats.d.ts.map +1 -0
  96. package/dist/importers/formats.js +281 -0
  97. package/dist/importers/formats.js.map +1 -0
  98. package/dist/importers/index.d.ts +26 -0
  99. package/dist/importers/index.d.ts.map +1 -0
  100. package/dist/importers/index.js +122 -0
  101. package/dist/importers/index.js.map +1 -0
  102. package/dist/index.d.ts +1 -1
  103. package/dist/index.js +1 -1
  104. package/dist/investigate/detectors.d.ts +35 -0
  105. package/dist/investigate/detectors.d.ts.map +1 -0
  106. package/dist/investigate/detectors.js +277 -0
  107. package/dist/investigate/detectors.js.map +1 -0
  108. package/dist/investigate/fix-brief.d.ts +3 -0
  109. package/dist/investigate/fix-brief.d.ts.map +1 -0
  110. package/dist/investigate/fix-brief.js +88 -0
  111. package/dist/investigate/fix-brief.js.map +1 -0
  112. package/dist/investigate/incidents.d.ts +19 -0
  113. package/dist/investigate/incidents.d.ts.map +1 -0
  114. package/dist/investigate/incidents.js +84 -0
  115. package/dist/investigate/incidents.js.map +1 -0
  116. package/dist/investigate/index.d.ts +25 -0
  117. package/dist/investigate/index.d.ts.map +1 -0
  118. package/dist/investigate/index.js +46 -0
  119. package/dist/investigate/index.js.map +1 -0
  120. package/dist/mcp/protocol.d.ts +2 -2
  121. package/dist/mcp/protocol.d.ts.map +1 -1
  122. package/dist/mcp/protocol.js +39 -8
  123. package/dist/mcp/protocol.js.map +1 -1
  124. package/dist/mcp/stdio.d.ts +1 -1
  125. package/dist/mcp/stdio.js +2 -2
  126. package/dist/mcp-servers.d.ts +2 -2
  127. package/dist/mcp-servers.js +2 -2
  128. package/dist/node-upgrade.d.ts +2 -2
  129. package/dist/node-upgrade.js +9 -9
  130. package/dist/notify/dispatch.d.ts +20 -0
  131. package/dist/notify/dispatch.d.ts.map +1 -0
  132. package/dist/notify/dispatch.js +52 -0
  133. package/dist/notify/dispatch.js.map +1 -0
  134. package/dist/notify/index.d.ts +39 -0
  135. package/dist/notify/index.d.ts.map +1 -0
  136. package/dist/notify/index.js +119 -0
  137. package/dist/notify/index.js.map +1 -0
  138. package/dist/notify/sender.d.ts +56 -0
  139. package/dist/notify/sender.d.ts.map +1 -0
  140. package/dist/notify/sender.js +139 -0
  141. package/dist/notify/sender.js.map +1 -0
  142. package/dist/notify/templates.d.ts +33 -0
  143. package/dist/notify/templates.d.ts.map +1 -0
  144. package/dist/notify/templates.js +119 -0
  145. package/dist/notify/templates.js.map +1 -0
  146. package/dist/offline-evaluate.d.ts +2 -2
  147. package/dist/offline-evaluate.js +3 -3
  148. package/dist/orr/agentshield.d.ts +2 -2
  149. package/dist/orr/agentshield.js +4 -4
  150. package/dist/orr/scorecard.d.ts +1 -1
  151. package/dist/orr/scorecard.js +3 -3
  152. package/dist/otel/exporter.d.ts +64 -0
  153. package/dist/otel/exporter.d.ts.map +1 -0
  154. package/dist/otel/exporter.js +247 -0
  155. package/dist/otel/exporter.js.map +1 -0
  156. package/dist/parse-args.d.ts +1 -1
  157. package/dist/parse-args.js +1 -1
  158. package/dist/receipt/auto-open.d.ts +1 -1
  159. package/dist/receipt/auto-open.js +1 -1
  160. package/dist/receipt/daemon.d.ts +6 -6
  161. package/dist/receipt/daemon.js +15 -15
  162. package/dist/receipt/dashboard.d.ts +1 -1
  163. package/dist/receipt/dashboard.js +2 -2
  164. package/dist/receipt/embeddings.d.ts +15 -4
  165. package/dist/receipt/embeddings.d.ts.map +1 -1
  166. package/dist/receipt/embeddings.js +116 -35
  167. package/dist/receipt/embeddings.js.map +1 -1
  168. package/dist/receipt/enrich.d.ts +2 -2
  169. package/dist/receipt/enrich.js +5 -5
  170. package/dist/receipt/gate-card.d.ts +2 -2
  171. package/dist/receipt/gate-card.d.ts.map +1 -1
  172. package/dist/receipt/gate-card.js +3 -3
  173. package/dist/receipt/live-server.d.ts.map +1 -1
  174. package/dist/receipt/live-server.js +22 -8
  175. package/dist/receipt/live-server.js.map +1 -1
  176. package/dist/receipt/receipt-css.d.ts +1 -1
  177. package/dist/receipt/receipt-css.d.ts.map +1 -1
  178. package/dist/receipt/receipt-css.js +72 -11
  179. package/dist/receipt/receipt-css.js.map +1 -1
  180. package/dist/receipt/render-receipt.d.ts +1 -1
  181. package/dist/receipt/render-receipt.d.ts.map +1 -1
  182. package/dist/receipt/render-receipt.js +138 -87
  183. package/dist/receipt/render-receipt.js.map +1 -1
  184. package/dist/receipt/search.d.ts +11 -0
  185. package/dist/receipt/search.d.ts.map +1 -1
  186. package/dist/receipt/search.js +70 -3
  187. package/dist/receipt/search.js.map +1 -1
  188. package/dist/receipt/share.d.ts +2 -2
  189. package/dist/receipt/share.js +5 -5
  190. package/dist/sample-tools.d.ts +1 -1
  191. package/dist/sample-tools.js +1 -1
  192. package/dist/sandbox/mock-driver.d.ts +1 -1
  193. package/dist/sandbox/mock-driver.js +1 -1
  194. package/dist/sdk/a2a.d.ts +2 -2
  195. package/dist/sdk/a2a.js +1 -1
  196. package/dist/sdk/adapters/claude-agent.d.ts +1 -1
  197. package/dist/sdk/adapters/claude-agent.js +1 -1
  198. package/dist/sdk/adapters/langgraph.d.ts +1 -1
  199. package/dist/sdk/adapters/langgraph.js +1 -1
  200. package/dist/sdk/adapters/mastra.d.ts +1 -1
  201. package/dist/sdk/adapters/mastra.js +1 -1
  202. package/dist/sdk/adapters/openai-agents.d.ts +1 -1
  203. package/dist/sdk/adapters/openai-agents.js +1 -1
  204. package/dist/sdk/adapters/vercel-ai.d.ts +2 -2
  205. package/dist/sdk/adapters/vercel-ai.js +1 -1
  206. package/dist/sdk/govern.d.ts +2 -2
  207. package/dist/sdk/govern.js +3 -3
  208. package/dist/sdk/index.d.ts +1 -1
  209. package/dist/sdk/index.js +1 -1
  210. package/dist/showback/trail-usage.d.ts +30 -0
  211. package/dist/showback/trail-usage.d.ts.map +1 -0
  212. package/dist/showback/trail-usage.js +50 -0
  213. package/dist/showback/trail-usage.js.map +1 -0
  214. package/dist/sign/evidence-bundle.d.ts +6 -6
  215. package/dist/sign/evidence-bundle.js +5 -5
  216. package/dist/star-cta.d.ts +1 -1
  217. package/dist/star-cta.d.ts.map +1 -1
  218. package/dist/star-cta.js +3 -3
  219. package/dist/trail-summary.d.ts +1 -1
  220. package/dist/trail-summary.js +3 -3
  221. package/dist/trail.d.ts +8 -2
  222. package/dist/trail.d.ts.map +1 -1
  223. package/dist/trail.js +18 -5
  224. package/dist/trail.js.map +1 -1
  225. package/dist/update-check.d.ts +17 -0
  226. package/dist/update-check.d.ts.map +1 -0
  227. package/dist/update-check.js +134 -0
  228. package/dist/update-check.js.map +1 -0
  229. package/droid/README.md +1 -1
  230. package/droid/mcp.example.json +1 -1
  231. package/gemini/settings.example.json +1 -1
  232. package/grok/README.md +1 -1
  233. package/kilo/kilo.example.json +1 -1
  234. package/kimi/mcp.example.json +1 -1
  235. package/kiro/mcp.example.json +1 -1
  236. package/manifest.json +1 -1
  237. package/mastracode/mcp.example.json +1 -1
  238. package/mcp.json +1 -1
  239. package/openai/codex.config.example.toml +1 -1
  240. package/opencode/opencode.example.json +1 -1
  241. package/package.json +5 -3
  242. package/qwen/settings.example.json +1 -1
  243. package/server.json +2 -2
@@ -8,6 +8,7 @@ import { productLabel, readTrail, readTrailSince } from "../trail.js";
8
8
  import { mcpServerLabel, parseMcpToolId } from "../mcp-servers.js";
9
9
  import { loadCertifyCard, loadIdentity, loadOrrCard, loadSandboxes, loadShowbackCard, loadWiredHosts, } from "./enrich.js";
10
10
  import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
11
+ import { summarizeTrailUsage } from "../showback/trail-usage.js";
11
12
  import { computeDashboard } from "./dashboard.js";
12
13
  import { buildChangesModel } from "./changes.js";
13
14
  import { GATE_NOT_SETUP_QUICKSTART, loadGatePage, toolNameFromToolId, toolServerPrefix, } from "./gate-page.js";
@@ -304,7 +305,7 @@ function modeBadge(mode) {
304
305
  return "";
305
306
  return `<span class="modeb" title="${letter === "O" ? "observe" : letter === "H" ? "hold" : "offline"} mode">${letter}</span>`;
306
307
  }
307
- // project is attacker-controllable (trail files are user-editable) — always escaped.
308
+ // project is attacker-controllable (trail files are user-editable) - always escaped.
308
309
  function projectMeta(project) {
309
310
  const p = project?.trim();
310
311
  if (!p)
@@ -316,8 +317,8 @@ function projectMeta(project) {
316
317
  function renderFeed(events, nowMs, live, preserveOrder) {
317
318
  if (events.length === 0) {
318
319
  const hint = live
319
- ? "No events yet — evaluate a tool call with <code>kya wrap</code> and this page will refresh."
320
- : "No events yet — evaluate a tool call with <code>kya wrap</code>, then regenerate this receipt.";
320
+ ? "No events yet - evaluate a tool call with <code>kya wrap</code> and this page will refresh."
321
+ : "No events yet - evaluate a tool call with <code>kya wrap</code>, then regenerate this receipt.";
321
322
  return `<p class="empty">${hint}</p>`;
322
323
  }
323
324
  const ordered = preserveOrder ? [...events] : [...events].sort((a, b) => b.ts.localeCompare(a.ts));
@@ -377,7 +378,7 @@ function changeWorstPill(worst) {
377
378
  */
378
379
  function changesPanel(changes, nowMs) {
379
380
  if (changes.sessions.length === 0) {
380
- return `<p class="empty">No recorded changes yet — write/edit tool calls will show up here.</p>`;
381
+ return `<p class="empty">No recorded changes yet - write/edit tool calls will show up here.</p>`;
381
382
  }
382
383
  return changes.sessions
383
384
  .map((s) => {
@@ -427,7 +428,7 @@ function identityLine(identity) {
427
428
  const bits = [nameId, identity.host, identity.baseUrl].filter((b) => typeof b === "string" && b.trim().length > 0);
428
429
  if (bits.length === 0)
429
430
  return "";
430
- // baseUrl is inert text here — never an anchor.
431
+ // baseUrl is inert text here - never an anchor.
431
432
  return `<div class="identity">${esc(clip(bits.join(" · "), 160))}</div>`;
432
433
  }
433
434
  function navItem(href, label, count, runningDot) {
@@ -526,7 +527,7 @@ function worstTone(worst) {
526
527
  /**
527
528
  * Analytics dashboard: four cards in a 2×2 grid. Every data row except the
528
529
  * timeline is a filter toggle on the same data-fgroup/data-fvalue contract as
529
- * the header chips — the engine picks them up via the [data-fgroup] query.
530
+ * the header chips - the engine picks them up via the [data-fgroup] query.
530
531
  */
531
532
  function dashboardPanel(db) {
532
533
  if (db.verdictMix.total === 0)
@@ -554,7 +555,7 @@ function dashboardPanel(db) {
554
555
  const tlCols = buckets
555
556
  .map((b) => {
556
557
  const h = b.count === 0 ? 0 : Math.max(6, Math.round((b.count / maxCount) * 100));
557
- return `<div class="col" title="${esc(b.label)} — ${b.count} event${b.count === 1 ? "" : "s"}"><span class="db-vbar" style="height:${h}%"></span><span class="lab">${esc(b.label)}</span></div>`;
558
+ return `<div class="col" title="${esc(b.label)} - ${b.count} event${b.count === 1 ? "" : "s"}"><span class="db-vbar" style="height:${h}%"></span><span class="lab">${esc(b.label)}</span></div>`;
558
559
  })
559
560
  .join("\n ");
560
561
  const timelineCard = `<div class="db-card">
@@ -608,14 +609,14 @@ function wiredHostsPanel(hosts) {
608
609
  .sort((a, b) => rank(a) - rank(b))
609
610
  .map((h) => {
610
611
  if (h.recipeOnly) {
611
- return `<li class="mute">${esc(h.label)} — manual setup <span class="hint" title="${esc(h.recipeOnly)}">docs recipe</span></li>`;
612
+ return `<li class="mute">${esc(h.label)} - manual setup <span class="hint" title="${esc(h.recipeOnly)}">docs recipe</span></li>`;
612
613
  }
613
614
  if (h.wired === "none") {
614
- return `<li class="mute">${esc(h.label)} — not wired</li>`;
615
+ return `<li class="mute">${esc(h.label)} - not wired</li>`;
615
616
  }
616
617
  const reload = h.reload ? ` · ${esc(h.reload.reload)}` : "";
617
618
  const running = h.running ? "running" : "not running";
618
- return `<li>${esc(h.label)} — wired (${h.wired})${reload} · ${running}</li>`;
619
+ return `<li>${esc(h.label)} - wired (${h.wired})${reload} · ${running}</li>`;
619
620
  })
620
621
  .join("\n ");
621
622
  return `<section class="panel" aria-label="Wired hosts">
@@ -644,7 +645,7 @@ function gatePanel(card, nowMs) {
644
645
  }
645
646
  const binary = card.binaryPresent
646
647
  ? `binary ${card.binaryVersion ? esc(clip(card.binaryVersion, 40)) : "installed"}`
647
- : "binary missing — kya gate setup";
648
+ : "binary missing - kya gate setup";
648
649
  const uptime = card.startedAt ? ` · up since ${esc(relativeTime(card.startedAt, nowMs))}` : "";
649
650
  const pill = card.state === "running"
650
651
  ? `<span class="pill ok">running</span>`
@@ -752,7 +753,7 @@ function gateHomePage(page, nowMs) {
752
753
  if (page.state === "not-set-up") {
753
754
  return `<section class="zone gate-zone" id="gateway-home" aria-labelledby="tab-gateway-home">
754
755
  ${gateStatusHeader(page, nowMs)}
755
- <p class="page-sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
756
+ <p class="page-sub">Local MCP gate - every proxied tool call is evaluated and audited.</p>
756
757
  <div class="card">
757
758
  <span class="pill">not set up</span>
758
759
  <p class="line">${esc(GATE_NOT_SETUP_QUICKSTART)}</p>
@@ -762,7 +763,7 @@ function gateHomePage(page, nowMs) {
762
763
  }
763
764
  return `<section class="zone gate-zone" id="gateway-home" aria-labelledby="tab-gateway-home">
764
765
  ${gateStatusHeader(page, nowMs)}
765
- <p class="page-sub">Local MCP gate — every proxied tool call is evaluated and audited.</p>
766
+ <p class="page-sub">Local MCP gate - every proxied tool call is evaluated and audited.</p>
766
767
  ${gateStatCards(page)}
767
768
  <div class="card-grid">
768
769
  <div class="card">
@@ -770,7 +771,7 @@ function gateHomePage(page, nowMs) {
770
771
  <p class="line">${page.url ? `<code>${esc(clip(page.url, 80))}</code>` : `Listener port ${page.port ?? 3930}`} · ${page.state}</p>
771
772
  <p class="line">OTLP receiver port ${page.otlpPort}</p>
772
773
  <p class="line">Failure mode <span class="pill">${esc(page.failureMode)}</span></p>
773
- ${page.binaryPresent ? `<p class="line">Binary <code>${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}</code></p>` : `<p class="line">Binary missing — <code>kya gate setup</code></p>`}
774
+ ${page.binaryPresent ? `<p class="line">Binary <code>${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}</code></p>` : `<p class="line">Binary missing - <code>kya gate setup</code></p>`}
774
775
  </div>
775
776
  <div class="card">
776
777
  <h3>Quick actions</h3>
@@ -787,7 +788,7 @@ function gateListenersPage(page) {
787
788
  <td>${esc(l.name)}</td>
788
789
  <td>${esc(l.protocol)}</td>
789
790
  <td><code>${esc(l.address)}</code></td>
790
- <td>${l.uptime ? `<time datetime="${esc(l.uptime)}">${esc(relativeTime(l.uptime, Date.now()))}</time>` : '<span class="mute">—</span>'}</td>
791
+ <td>${l.uptime ? `<time datetime="${esc(l.uptime)}">${esc(relativeTime(l.uptime, Date.now()))}</time>` : '<span class="mute">-</span>'}</td>
791
792
  <td>${esc(l.detail ?? "")}</td>
792
793
  </tr>`)
793
794
  .join("\n");
@@ -810,7 +811,7 @@ function gateRoutesPage(page) {
810
811
  <h2 class="page-title">Routes</h2>
811
812
  <p class="page-sub">One route per configured backend server.</p>
812
813
  <div class="card">
813
- <p class="empty small">No servers configured — add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
814
+ <p class="empty small">No servers configured - add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
814
815
  </div>
815
816
  </section>`;
816
817
  }
@@ -818,7 +819,7 @@ function gateRoutesPage(page) {
818
819
  .map((r) => {
819
820
  const dot = r.worst === "none" ? "" : `<span class="wdot ${r.worst}"></span>`;
820
821
  const denied = r.deniedTools.length === 0
821
- ? '<span class="mute">—</span>'
822
+ ? '<span class="mute">-</span>'
822
823
  : r.deniedTools.map((t) => `<span class="chip bad">${esc(t)}</span>`).join("");
823
824
  return ` <tr data-server="${esc(r.backendLabel)}">
824
825
  <td><code>${esc(r.pattern)}</code></td>
@@ -849,7 +850,7 @@ function gateBackendsPage(page) {
849
850
  <h2 class="page-title">Backends</h2>
850
851
  <p class="page-sub">Configured upstream MCP servers.</p>
851
852
  <div class="card">
852
- <p class="empty small">No servers configured — add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
853
+ <p class="empty small">No servers configured - add one to .kya/gateways.json or run <code>kya gate init</code>.</p>
853
854
  </div>
854
855
  </section>`;
855
856
  }
@@ -861,7 +862,7 @@ function gateBackendsPage(page) {
861
862
  const provenance = s.importedFrom.map((h) => `<span class="chip">${esc(h)}</span>`).join("");
862
863
  const dot = s.worst === "none" ? "" : `<span class="wdot ${s.worst}"></span>`;
863
864
  const denied = s.deniedTools.length === 0
864
- ? '<span class="mute">—</span>'
865
+ ? '<span class="mute">-</span>'
865
866
  : s.deniedTools.map((t) => `<span class="chip bad">${esc(t)}</span>`).join("");
866
867
  return ` <tr data-server="${esc(s.serverFacet)}">
867
868
  <td><span class="wdot ${s.worst === "none" ? "mute" : s.worst}"></span> <code>${esc(s.id)}</code></td>
@@ -910,7 +911,7 @@ function gatePoliciesPage(page) {
910
911
  : page.servers
911
912
  .map((s) => `<div class="card">
912
913
  <h3><code>${esc(s.id)}</code> · ${s.policy.denyPatterns.length} pattern${s.policy.denyPatterns.length === 1 ? "" : "s"}</h3>
913
- ${s.policy.denyPatterns.length === 0 ? '<p class="mute small">No deny patterns — all tools observed.</p>' : `<pre><code>${esc(s.policy.denyPatterns.join("\n"))}</code></pre>`}
914
+ ${s.policy.denyPatterns.length === 0 ? '<p class="mute small">No deny patterns - all tools observed.</p>' : `<pre><code>${esc(s.policy.denyPatterns.join("\n"))}</code></pre>`}
914
915
  </div>`)
915
916
  .join("\n");
916
917
  return `<section class="zone gate-zone" id="gateway-policies" aria-labelledby="tab-gateway-policies">
@@ -938,7 +939,7 @@ function gatePlaygroundPage(page) {
938
939
  : `<p class="mute small">Recent tools: ${page.playgroundSamples.map((t) => `<code>${esc(t)}</code>`).join(" ")}</p>`;
939
940
  return `<section class="zone gate-zone" id="gateway-playground" aria-labelledby="tab-gateway-playground">
940
941
  <h2 class="page-title">Playground</h2>
941
- <p class="page-sub">Dry-run a tool call against the generated policy — no tool is executed.</p>
942
+ <p class="page-sub">Dry-run a tool call against the generated policy - no tool is executed.</p>
942
943
  <div class="card">
943
944
  <form class="playground-form" id="gate-play-form">
944
945
  <label class="sr-only" for="gate-play-server">Server</label>
@@ -1028,7 +1029,7 @@ function usdText(usd) {
1028
1029
  return usd === null ? "USD n/a" : `~$${usd.toFixed(2)}`;
1029
1030
  }
1030
1031
  /** KPI tile: big tabular number over a small caps label. The tone class is
1031
- * only applied above zero — a zero count never reads as a signal. */
1032
+ * only applied above zero - a zero count never reads as a signal. */
1032
1033
  function kpiTile(label, value, cls = "") {
1033
1034
  const v = typeof value === "number" ? value : esc(value);
1034
1035
  const toned = typeof value === "number" && value > 0 && cls ? ` ${cls}` : "";
@@ -1043,17 +1044,21 @@ function kpiTile(label, value, cls = "") {
1043
1044
  function heroCertifyPanel(certify) {
1044
1045
  if (!certify) {
1045
1046
  return `<section class="panel hero-certify none" aria-label="Certify">
1046
- <h2>Certify — Agent Trust Baseline</h2>
1047
+ <h2>Certify - Agent Trust Baseline</h2>
1047
1048
  <div class="orrline">
1048
1049
  <span class="pill">not evaluated</span>
1049
1050
  </div>
1050
- <p class="mute small">No live evaluation available — wrap tool calls and run kya certify to build the baseline.</p>
1051
+ <p class="mute small">No live evaluation available - wrap tool calls and run kya certify to build the baseline.</p>
1051
1052
  </section>`;
1052
1053
  }
1053
1054
  const state = certify.result === "pass" ? "pass" : "gap";
1054
- // Status legend: one muted line under the count tiles so the four numbers
1055
- // read as definitions, not bare counters.
1056
- const legend = `<p class="legend">gap = requirement failing — your work plan · insufficient = not enough local evidence to evaluate (never counts as pass) · attested = your signed statement, unverified</p>`;
1055
+ // Status legend as a definition list so each counter is explained on first
1056
+ // glance, not hidden in a dense sentence.
1057
+ const legend = `<dl class="cert-legend">
1058
+ <div><dt>Gap</dt><dd>requirement is failing - this is your work plan</dd></div>
1059
+ <div><dt>Insufficient</dt><dd>not enough local evidence to evaluate (never counts as a pass)</dd></div>
1060
+ <div><dt>Attested</dt><dd>your signed statement, unverified until audited</dd></div>
1061
+ </dl>`;
1057
1062
  // Attested summary: the hero's gap rows are gap-status only, so attestations
1058
1063
  // surface as one line naming the first attested requirement (+N more) with
1059
1064
  // its attestation text.
@@ -1061,22 +1066,38 @@ function heroCertifyPanel(certify) {
1061
1066
  const attestedLine = certify.attested === 0
1062
1067
  ? ""
1063
1068
  : firstAttested
1064
- ? `<p class="att-line">attested: <code>${esc(firstAttested.id)}</code>${certify.attested > 1 ? ` (+${certify.attested - 1} more)` : ""} — "${esc(clip(firstAttested.attestation?.text ?? firstAttested.evidence, 100))}"</p>`
1065
- : `<p class="att-line">attested: ${certify.attested} — your signed statement, unverified</p>`;
1069
+ ? `<div class="att-line">attested: <code>${esc(firstAttested.id)}</code>${certify.attested > 1 ? ` (+${certify.attested - 1} more)` : ""} - "${expandableText("att-quote", firstAttested.attestation?.text ?? firstAttested.evidence, 100)}"</div>`
1070
+ : `<p class="att-line">attested: ${certify.attested} - your signed statement, unverified</p>`;
1066
1071
  // topGaps arrives severity-ordered from the loader; cap defensively at 5.
1067
- // Each row is id — title + severity chip, with the redacted evidence
1068
- // one-liner beneath (clipped so the hero stays compact).
1072
+ // Each gap is an expandable <details> row: summary stays compact, the body
1073
+ // reveals the full requirement title, what the status means, the unclipped
1074
+ // evidence, and the exact CLI command to close the gap.
1069
1075
  const gaps = certify.topGaps.length === 0
1070
1076
  ? ""
1071
- : `<ul class="rows">
1077
+ : `<div class="cert-rows">
1072
1078
  ${certify.topGaps
1073
1079
  .slice(0, 5)
1074
- .map((g) => ` <li><code>${esc(g.id)}</code> — ${esc(g.title)} <span class="sev ${esc(g.severity)}">${esc(g.severity)}</span>${g.evidence ? `<span class="gap-ev">${esc(clip(g.evidence, 140))}</span>` : ""}</li>`)
1080
+ .map((g) => {
1081
+ const cmd = `kya certify --attest ${g.id} --text "…"`;
1082
+ return ` <details class="req-row">
1083
+ <summary><code>${esc(g.id)}</code> - ${esc(g.title)} <span class="sev ${esc(g.severity)}">${esc(g.severity)}</span></summary>
1084
+ <div class="req-body">
1085
+ <p class="req-mean">Gap means this requirement is currently failing against the local evidence in the ${certify.windowDays}-day window.</p>
1086
+ ${g.evidence ? `<p class="req-evi"><strong>Evidence:</strong> ${esc(g.evidence)}</p>` : ""}
1087
+ <p class="req-cta"><strong>Close it:</strong> <code>${esc(cmd)}</code> <span class="req-hint">(replace "…" with your accountable statement or fix the underlying control)</span></p>
1088
+ </div>
1089
+ </details>`;
1090
+ })
1075
1091
  .join("\n")}
1076
- </ul>`;
1092
+ </div>`;
1093
+ // Call-to-action: one primary command and one secondary link to the detail tab.
1094
+ const cta = `<div class="cert-cta">
1095
+ <a class="btn" href="#certify">Open full requirement table</a>
1096
+ <span class="mute small">or run <code>kya certify --open</code> for the signed bundle</span>
1097
+ </div>`;
1077
1098
  // Pill tones reuse the ORR palette: pass is green, gap is amber.
1078
1099
  return `<section class="panel hero-certify ${state}" aria-label="Certify">
1079
- <h2>Certify — Agent Trust Baseline</h2>
1100
+ <h2>Certify - Agent Trust Baseline</h2>
1080
1101
  <div class="orrline">
1081
1102
  <span class="pill orr-${state === "pass" ? "green" : "amber"}">${esc(certify.result)}</span>
1082
1103
  </div>
@@ -1089,7 +1110,8 @@ ${certify.topGaps
1089
1110
  ${legend}
1090
1111
  ${attestedLine}
1091
1112
  ${gaps}
1092
- <p class="mute small">live evaluation — window ${certify.windowDays}d, ${certify.trailEvents} trail events · the Certify tab has the full live requirement table · kya certify for the gap report + signed bundle</p>
1113
+ ${cta}
1114
+ <p class="mute small">live evaluation - window ${certify.windowDays}d, ${certify.trailEvents} trail events · the Certify tab has the complete gap report · <code>kya certify</code> writes the signed evidence bundle</p>
1093
1115
  </section>`;
1094
1116
  }
1095
1117
  /** Detail-table pill tones, reusing the existing pill/tone system: pass is
@@ -1101,14 +1123,24 @@ const DETAIL_PILL_TONE = {
1101
1123
  insufficient_evidence: "",
1102
1124
  attested: "att",
1103
1125
  };
1104
- const CERTIFY_DETAIL_TITLE_CLIP = 120;
1105
1126
  const CERTIFY_DETAIL_CLIP = 160;
1106
1127
  /**
1107
- * Certify tab body: the full live requirement table — every evaluated
1128
+ * Long evidence/attestation text renders in full behind a Read more toggle;
1129
+ * at or under the preview length it stays a plain inline span. The preview
1130
+ * keeps the old clip length, so collapsed rows look exactly as before.
1131
+ */
1132
+ function expandableText(cls, text, previewLen = CERTIFY_DETAIL_CLIP) {
1133
+ const clean = stripEscapes(text).replace(/\r?\n/g, " ").replace(/\s{2,}/g, " ").trim();
1134
+ if (clean.length <= previewLen)
1135
+ return `<span class="${cls}">${esc(clean)}</span>`;
1136
+ return `<details class="req-more ${cls}"><summary>${esc(clip(clean, previewLen))}</summary><div class="full">${esc(clean)}</div></details>`;
1137
+ }
1138
+ /**
1139
+ * Certify tab body: the full live requirement table - every evaluated
1108
1140
  * requirement grouped by domain, domains in catalog order (the card's
1109
1141
  * requirements arrive in catalog order, so first-seen order IS the catalog
1110
1142
  * order). Each domain gets a heading row with mini-counts, then one row per
1111
- * requirement: status pill, id, title, and a muted evidence line beneath —
1143
+ * requirement: status pill, id, title, and a muted evidence line beneath -
1112
1144
  * or the attestation text for attested rows. Empty input yields "" so the
1113
1145
  * caller falls back to the zone-empty hint (fail-closed: no table without
1114
1146
  * data).
@@ -1135,11 +1167,11 @@ function certifyDetailPanel(requirements) {
1135
1167
  const tone = DETAIL_PILL_TONE[r.status];
1136
1168
  const pill = `<span class="pill${tone ? ` ${tone}` : ""}">${esc(r.status.replace(/_/g, " "))}</span>`;
1137
1169
  const detail = r.attestation
1138
- ? `<span class="att">attested ${esc(r.attestation.at)} — "${esc(clip(r.attestation.text, CERTIFY_DETAIL_CLIP))}"</span>`
1170
+ ? expandableText("att", `attested ${r.attestation.at} - "${r.attestation.text}"`)
1139
1171
  : r.evidence
1140
- ? `<span class="evi">${esc(clip(r.evidence, CERTIFY_DETAIL_CLIP))}</span>`
1172
+ ? expandableText("evi", r.evidence)
1141
1173
  : "";
1142
- return ` <li>${pill} <code>${esc(r.id)}</code> — ${esc(clip(r.title, CERTIFY_DETAIL_TITLE_CLIP))}${detail}</li>`;
1174
+ return ` <li>${pill} <code>${esc(r.id)}</code> - ${esc(r.title)}${detail}</li>`;
1143
1175
  })
1144
1176
  .join("\n");
1145
1177
  const label = DOMAIN_LABELS[domain] ?? domain;
@@ -1152,7 +1184,7 @@ ${items}
1152
1184
  })
1153
1185
  .join("\n");
1154
1186
  return `<section class="panel certify-detail" aria-label="Certify detail">
1155
- <h2>Every requirement — live evaluation</h2>
1187
+ <h2>Every requirement - live evaluation</h2>
1156
1188
  ${sections}
1157
1189
  </section>`;
1158
1190
  }
@@ -1161,7 +1193,7 @@ ${sections}
1161
1193
  function verdictsHeroCard(c, total) {
1162
1194
  // Base 100% = total events, matching dashboard.ts's verdictMix contract:
1163
1195
  // verdict is a free-form string from untrusted JSONL, so an event may land
1164
- // in no bucket (bogus verdict) while still counting in `never` — only the
1196
+ // in no bucket (bogus verdict) while still counting in `never` - only the
1165
1197
  // raw event count keeps every bar inside the base the Overview card uses.
1166
1198
  const pct = (n) => (total > 0 ? Math.round((n / total) * 100) : 0);
1167
1199
  const mixRow = (label, n, fill) => `<div class="mix-row"><span class="mix-lab">${label}</span><span class="mix-bar"><span class="mix-fill ${fill}" style="width:${pct(n)}%"></span></span><span class="mix-num">${n}</span></div>`;
@@ -1180,7 +1212,7 @@ const SPARK_H = 24;
1180
1212
  /**
1181
1213
  * Inline 7-bar sparkline (no JS): the last 7 activity buckets, left-padded
1182
1214
  * with zeros, heights normalized so the max bucket is full height. Empty
1183
- * input renders a flat baseline — heights are guarded, never NaN/Infinity.
1215
+ * input renders a flat baseline - heights are guarded, never NaN/Infinity.
1184
1216
  */
1185
1217
  function activitySparkline(activity) {
1186
1218
  const counts = activity.buckets.slice(-SPARK_BARS).map((b) => b.count);
@@ -1207,10 +1239,16 @@ function activityHeroCard(agg, db, total) {
1207
1239
  </section>`;
1208
1240
  }
1209
1241
  /** Hero-styled showback: same fields as the old panel (tokens in/out, est.
1210
- * USD, top runs, disclaimer), omitted entirely when no report exists. */
1211
- function showbackHeroCard(report) {
1242
+ * USD, top runs, disclaimer), omitted entirely when no report exists. When
1243
+ * window events carry host-reported token usage, a line states how much of
1244
+ * the total is real vs the static per-event estimate. */
1245
+ function showbackHeroCard(report, events = []) {
1212
1246
  if (!report)
1213
1247
  return "";
1248
+ const usage = summarizeTrailUsage(events);
1249
+ const realLine = usage.realUsageEvents === 0
1250
+ ? ""
1251
+ : `<p class="mute small">real host-reported usage on ${usage.realUsageEvents} of ${usage.events} window events (${usage.realTokensIn} in / ${usage.realTokensOut} out tokens); the rest is priced at the static estimate</p>`;
1214
1252
  const topRuns = [...report.perRun]
1215
1253
  .sort((a, b) => (b.estimatedUsd ?? -1) - (a.estimatedUsd ?? -1))
1216
1254
  .slice(0, 5);
@@ -1218,7 +1256,7 @@ function showbackHeroCard(report) {
1218
1256
  ? ""
1219
1257
  : `<ul class="rows">
1220
1258
  ${topRuns
1221
- .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>`)
1259
+ .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>`)
1222
1260
  .join("\n")}
1223
1261
  </ul>`;
1224
1262
  return `<section class="panel" aria-label="Showback">
@@ -1229,6 +1267,7 @@ ${topRuns
1229
1267
  ${kpiTile("Est. USD", usdText(report.estimatedUsd))}
1230
1268
  </div>
1231
1269
  ${runs}
1270
+ ${realLine}
1232
1271
  <p class="mute small">${esc(SHOWBACK_DISCLAIMER)}</p>
1233
1272
  </section>`;
1234
1273
  }
@@ -1242,7 +1281,7 @@ export function gatewayHeroCard(page, _nowMs) {
1242
1281
  : `<span class="pill">stopped</span>`;
1243
1282
  const binary = page.binaryPresent
1244
1283
  ? `binary ${page.binaryVersion ? esc(clip(page.binaryVersion, 40)) : "installed"}`
1245
- : "binary missing — kya gate setup";
1284
+ : "binary missing - kya gate setup";
1246
1285
  const detail = page.state === "running"
1247
1286
  ? `${page.url ? `<code>${esc(clip(page.url, 60))}</code>` : `port ${page.port ?? 3930}`} · ${page.listeners.length} listener${page.listeners.length === 1 ? "" : "s"} · ${page.servers.length} backend${page.servers.length === 1 ? "" : "s"}`
1248
1287
  : `${page.servers.length} backend${page.servers.length === 1 ? "" : "s"} configured · ${binary}`;
@@ -1371,7 +1410,7 @@ export function renderReceiptHtml(model) {
1371
1410
  <span class="label">Tools</span>
1372
1411
  ${tools.map((t) => `<code>${esc(t)}</code>`).join("")}
1373
1412
  </footer>`;
1374
- // liveToken is minted base64url by the live server — safe in a JS string
1413
+ // liveToken is minted base64url by the live server - safe in a JS string
1375
1414
  // literal; static renders omit it. No post-hoc html.replace (spoofable).
1376
1415
  const eventsUrl = model.liveToken ? `/events?t=${encodeURIComponent(model.liveToken)}` : "/events";
1377
1416
  const liveScript = model.live
@@ -1495,21 +1534,26 @@ export function renderReceiptHtml(model) {
1495
1534
  }
1496
1535
  function apply(){
1497
1536
  var on = Object.keys(state).some(function(g){ return Object.keys(state[g]).length > 0; });
1498
- // Guard: the script is only emitted for live reports, so the feed is
1499
- // always server-rendered; changes entries still filter client-side.
1500
- if (!LIVE) {
1501
- feed.querySelectorAll('.ev').forEach(function(row){
1502
- row.classList.toggle('filtered-out', !rowVisible(row, on));
1503
- });
1504
- var day = null, dayHasRows = false;
1505
- function flush(){ if (day) day.classList.toggle('filtered-out', on && !dayHasRows); }
1506
- for (var i = 0; i < feed.children.length; i++) {
1507
- var el = feed.children[i];
1508
- if (el.classList.contains('day')) { flush(); day = el; dayHasRows = false; }
1509
- else if (el.classList.contains('ev') && !el.classList.contains('filtered-out')) dayHasRows = true;
1510
- }
1511
- flush();
1537
+ // Feed rows: filter client-side immediately so the UI reacts without
1538
+ // waiting for the server round-trip. For live/paginated reports the server
1539
+ // still returns the authoritative page via feedUpdate(), but this keeps
1540
+ // the Activity tab responsive and survives SSE reloads that would otherwise
1541
+ // overwrite the server response before the user sees it.
1542
+ var toggled = 0;
1543
+ feed.querySelectorAll('.ev').forEach(function(row){
1544
+ var visible = rowVisible(row, on);
1545
+ if (!visible) toggled++;
1546
+ row.classList.toggle('filtered-out', !visible);
1547
+ });
1548
+ try { window.__kyaFilterDebug = { toggled: toggled, feedRows: feed.querySelectorAll('.ev').length }; } catch(e){}
1549
+ var day = null, dayHasRows = false;
1550
+ function flush(){ if (day) day.classList.toggle('filtered-out', on && !dayHasRows); }
1551
+ for (var i = 0; i < feed.children.length; i++) {
1552
+ var el = feed.children[i];
1553
+ if (el.classList.contains('day')) { flush(); day = el; dayHasRows = false; }
1554
+ else if (el.classList.contains('ev') && !el.classList.contains('filtered-out')) dayHasRows = true;
1512
1555
  }
1556
+ flush();
1513
1557
  // Changes tab: entries filter individually; file/session groups collapse
1514
1558
  // when no entry inside survives.
1515
1559
  if (chg) {
@@ -1536,10 +1580,17 @@ export function renderReceiptHtml(model) {
1536
1580
  function feedUpdate(){
1537
1581
  var T = readToken();
1538
1582
  if (!T) return;
1583
+ // Server expects { group: [value, ...] }, but client state stores
1584
+ // { group: { value: true } } for fast lookup. Convert before sending.
1585
+ var serverFilters = {};
1586
+ for (var g in state) {
1587
+ var vals = Object.keys(state[g]);
1588
+ if (vals.length) serverFilters[g] = vals;
1589
+ }
1539
1590
  fetch('/feed?t=' + encodeURIComponent(T), {
1540
1591
  method: 'POST',
1541
1592
  headers: { 'Content-Type': 'application/json' },
1542
- body: JSON.stringify({ filters: state, page: page, q: q })
1593
+ body: JSON.stringify({ filters: serverFilters, page: page, q: q })
1543
1594
  })
1544
1595
  .then(function(r){ return r.json(); })
1545
1596
  .then(function(data){
@@ -1857,15 +1908,15 @@ export function renderReceiptHtml(model) {
1857
1908
  const zoneBody = (parts, empty) => parts.some((p) => p.length > 0)
1858
1909
  ? parts.filter(Boolean).join("\n ")
1859
1910
  : `<p class="mute small zone-empty">${empty}</p>`;
1860
- const overviewBody = zoneBody([dashboardPanel(dashboard), sessionsPanel(agg, nowMs, changes), reasonsPanel(agg)], "No activity in this window yet — analytics appear once events land.");
1861
- const certifyBody = zoneBody([model.certify ? certifyDetailPanel(model.certify.requirements) : ""], "No live evaluation yet — wrap tool calls and run kya certify to build the baseline.");
1911
+ const overviewBody = zoneBody([dashboardPanel(dashboard), sessionsPanel(agg, nowMs, changes), reasonsPanel(agg)], "No activity in this window yet - analytics appear once events land.");
1912
+ const certifyBody = zoneBody([model.certify ? certifyDetailPanel(model.certify.requirements) : ""], "No live evaluation yet - wrap tool calls and run kya certify to build the baseline.");
1862
1913
  const systemBody = zoneBody([
1863
1914
  gatePanel(model.gate, nowMs),
1864
1915
  wiredHostsPanel(model.wiredHosts),
1865
1916
  sandboxesPanel(model.sandboxes, nowMs),
1866
1917
  orrPanel(model.orr, nowMs),
1867
1918
  toolsHtml,
1868
- ], "No system state yet — wire a host, configure a sandbox, or run an ORR.");
1919
+ ], "No system state yet - wire a host, configure a sandbox, or run an ORR.");
1869
1920
  const html = `<!DOCTYPE html>
1870
1921
  <html lang="en">
1871
1922
  <head>
@@ -1919,7 +1970,7 @@ ${model.liveToken ? '<main data-live-token="1">' : '<main>'}
1919
1970
  ${verdictsHeroCard(c, events.length)}
1920
1971
  ${activityHeroCard(agg, dashboard, events.length)}
1921
1972
  ${gatewayHeroCard(model.gate, nowMs)}
1922
- ${showbackHeroCard(model.showback)}
1973
+ ${showbackHeroCard(model.showback, model.events)}
1923
1974
  </div>
1924
1975
  </div>
1925
1976
  ${overviewBody}
@@ -2020,10 +2071,10 @@ export function renderReceiptMarkdown(model) {
2020
2071
  if (c.topGaps.length > 0) {
2021
2072
  lines.push("Top gaps:");
2022
2073
  for (const g of c.topGaps) {
2023
- lines.push(`- \`${mdInline(g.id)}\` — ${mdText(g.title)} (${mdText(g.severity)})`);
2074
+ lines.push(`- \`${mdInline(g.id)}\` - ${mdText(g.title)} (${mdText(g.severity)})`);
2024
2075
  }
2025
2076
  }
2026
- lines.push(`live evaluation — window ${c.windowDays}d, ${c.trailEvents} trail events · the Certify tab has the full live requirement table · kya certify for the gap report + signed bundle`, "");
2077
+ lines.push(`live evaluation - window ${c.windowDays}d, ${c.trailEvents} trail events · the Certify tab has the full live requirement table · kya certify for the gap report + signed bundle`, "");
2027
2078
  }
2028
2079
  const db = computeDashboard(model.events);
2029
2080
  if (db.verdictMix.total > 0) {
@@ -2065,7 +2116,7 @@ export function renderReceiptMarkdown(model) {
2065
2116
  const head = `- **${mdText(verdictWord(e.verdict))}** \`${mdInline(e.toolId)}\`${sum} - ${productLabel(e.product)} - ${mdText(e.reasonCode)}`;
2066
2117
  if (!e.diffPreview?.trim())
2067
2118
  return [head];
2068
- // Fenced content is verbatim (stripEscapes only — backslash escapes
2119
+ // Fenced content is verbatim (stripEscapes only - backslash escapes
2069
2120
  // are literal in code fences); the dynamically sized tilde fence
2070
2121
  // prevents breakout and backticks cannot close a tilde fence.
2071
2122
  const diff = stripEscapes(e.diffPreview.trim());
@@ -2078,21 +2129,21 @@ export function renderReceiptMarkdown(model) {
2078
2129
  if (changes.sessions.length > 0) {
2079
2130
  lines.push("## Changes", "");
2080
2131
  for (const s of changes.sessions) {
2081
- lines.push(`### \`${mdInline(s.sessionId)}\` — ${s.fileCount} file${s.fileCount === 1 ? "" : "s"}, ${s.changeCount} change${s.changeCount === 1 ? "" : "s"}, last ${relativeTime(s.lastTs, nowMs)}`, "");
2132
+ lines.push(`### \`${mdInline(s.sessionId)}\` - ${s.fileCount} file${s.fileCount === 1 ? "" : "s"}, ${s.changeCount} change${s.changeCount === 1 ? "" : "s"}, last ${relativeTime(s.lastTs, nowMs)}`, "");
2082
2133
  for (const f of s.files) {
2083
- lines.push(`#### \`${mdInline(f.path)}\` — ${f.writes} write${f.writes === 1 ? "" : "s"}, worst: ${f.worst}`, "");
2134
+ lines.push(`#### \`${mdInline(f.path)}\` - ${f.writes} write${f.writes === 1 ? "" : "s"}, worst: ${f.worst}`, "");
2084
2135
  for (const e of f.entries) {
2085
2136
  // Same fencing contract as the md feed: verbatim content, tilde
2086
2137
  // fence one longer than the longest run inside.
2087
2138
  const diff = stripEscapes(e.preview);
2088
2139
  const fence = mdFence(diff);
2089
- lines.push(`- **${mdText(verdictWord(e.verdict))}** \`${mdInline(e.toolId)}\` — ${mdText(e.ts)}`, "", fence, diff, fence, "");
2140
+ lines.push(`- **${mdText(verdictWord(e.verdict))}** \`${mdInline(e.toolId)}\` - ${mdText(e.ts)}`, "", fence, diff, fence, "");
2090
2141
  }
2091
2142
  }
2092
2143
  }
2093
2144
  }
2094
2145
  if (agg.sessions.length > 0) {
2095
- lines.push("## Sessions", ...agg.sessions.map((s) => `- \`${mdInline(s.sessionId)}\` — ${s.events} event${s.events === 1 ? "" : "s"}, worst: ${s.worst}, last ${relativeTime(s.lastTs, nowMs)}`), "");
2146
+ lines.push("## Sessions", ...agg.sessions.map((s) => `- \`${mdInline(s.sessionId)}\` - ${s.events} event${s.events === 1 ? "" : "s"}, worst: ${s.worst}, last ${relativeTime(s.lastTs, nowMs)}`), "");
2096
2147
  }
2097
2148
  if (agg.reasons.length > 0) {
2098
2149
  lines.push("## Reasons", ...agg.reasons.map((r) => `- ${mdText(r.label)} × ${r.count}`), "");
@@ -2101,20 +2152,20 @@ export function renderReceiptMarkdown(model) {
2101
2152
  const g = model.gate;
2102
2153
  lines.push("## Gateway");
2103
2154
  if (g.state === "not-set-up") {
2104
- lines.push(`not set up — ${GATE_NOT_SETUP_QUICKSTART}`);
2155
+ lines.push(`not set up - ${GATE_NOT_SETUP_QUICKSTART}`);
2105
2156
  }
2106
2157
  else {
2107
2158
  const binary = g.binaryPresent
2108
2159
  ? `binary ${g.binaryVersion ? mdInline(clip(g.binaryVersion, 40)) : "installed"}`
2109
- : "binary missing — kya gate setup";
2160
+ : "binary missing - kya gate setup";
2110
2161
  lines.push(g.state === "running"
2111
- ? `running — \`${mdInline(clip(g.url ?? "", 60))}\` · ${binary} · ${g.events} event${g.events === 1 ? "" : "s"} in window`
2112
- : `stopped — \`kya gate run\` · ${binary}`);
2162
+ ? `running - \`${mdInline(clip(g.url ?? "", 60))}\` · ${binary} · ${g.events} event${g.events === 1 ? "" : "s"} in window`
2163
+ : `stopped - \`kya gate run\` · ${binary}`);
2113
2164
  if (g.state === "running" && g.servers.length === 0) {
2114
- lines.push("running, no servers configured — add servers to .kya/gateways.json and restart");
2165
+ lines.push("running, no servers configured - add servers to .kya/gateways.json and restart");
2115
2166
  }
2116
2167
  for (const s of g.servers) {
2117
- lines.push(`- \`${mdInline(s.id)}\` — ${mdText(s.transport)} · ${s.events} event${s.events === 1 ? "" : "s"}${s.worst === "none" ? "" : `, worst: ${s.worst}`}`);
2168
+ lines.push(`- \`${mdInline(s.id)}\` - ${mdText(s.transport)} · ${s.events} event${s.events === 1 ? "" : "s"}${s.worst === "none" ? "" : `, worst: ${s.worst}`}`);
2118
2169
  }
2119
2170
  }
2120
2171
  lines.push("");
@@ -2124,14 +2175,14 @@ export function renderReceiptMarkdown(model) {
2124
2175
  lines.push("## Wired hosts");
2125
2176
  for (const h of hosts) {
2126
2177
  if (h.recipeOnly) {
2127
- lines.push(`- ${mdText(h.label)} — manual setup (docs recipe)`);
2178
+ lines.push(`- ${mdText(h.label)} - manual setup (docs recipe)`);
2128
2179
  }
2129
2180
  else if (h.wired === "none") {
2130
- lines.push(`- ${mdText(h.label)} — not wired`);
2181
+ lines.push(`- ${mdText(h.label)} - not wired`);
2131
2182
  }
2132
2183
  else {
2133
2184
  const reload = h.reload ? ` · ${mdText(h.reload.reload)}` : "";
2134
- lines.push(`- ${mdText(h.label)} — wired (${h.wired})${reload} · ${h.running ? "running" : "not running"}`);
2185
+ lines.push(`- ${mdText(h.label)} - wired (${h.wired})${reload} · ${h.running ? "running" : "not running"}`);
2135
2186
  }
2136
2187
  }
2137
2188
  lines.push("");
@@ -2142,7 +2193,7 @@ export function renderReceiptMarkdown(model) {
2142
2193
  if (sb.backend)
2143
2194
  lines.push(`Configured backend: ${mdText(sb.backend)}`);
2144
2195
  for (const s of sb.sandboxes) {
2145
- lines.push(`- \`${mdInline(s.sandboxId)}\` — ${mdText(s.backend)} · ${mdText(s.status)} · created ${relativeTime(s.createdAt, nowMs)}`);
2196
+ lines.push(`- \`${mdInline(s.sandboxId)}\` - ${mdText(s.backend)} · ${mdText(s.status)} · created ${relativeTime(s.createdAt, nowMs)}`);
2146
2197
  }
2147
2198
  lines.push("");
2148
2199
  }
@@ -2162,7 +2213,7 @@ export function renderReceiptMarkdown(model) {
2162
2213
  .sort((a, b) => (b.estimatedUsd ?? -1) - (a.estimatedUsd ?? -1))
2163
2214
  .slice(0, 5);
2164
2215
  for (const r of topRuns) {
2165
- lines.push(`- \`${mdInline(r.runId)}\` — ${r.steps} step${r.steps === 1 ? "" : "s"} · ${usdText(r.estimatedUsd)}`);
2216
+ lines.push(`- \`${mdInline(r.runId)}\` - ${r.steps} step${r.steps === 1 ? "" : "s"} · ${usdText(r.estimatedUsd)}`);
2166
2217
  }
2167
2218
  lines.push(SHOWBACK_DISCLAIMER, "");
2168
2219
  }