jfl 0.9.1 → 0.9.2

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 (66) hide show
  1. package/dist/commands/context-hub.d.ts.map +1 -1
  2. package/dist/commands/context-hub.js +118 -2
  3. package/dist/commands/context-hub.js.map +1 -1
  4. package/dist/commands/ide.d.ts.map +1 -1
  5. package/dist/commands/ide.js +22 -0
  6. package/dist/commands/ide.js.map +1 -1
  7. package/dist/commands/linear.d.ts.map +1 -1
  8. package/dist/commands/linear.js +24 -0
  9. package/dist/commands/linear.js.map +1 -1
  10. package/dist/commands/pi.d.ts +3 -0
  11. package/dist/commands/pi.d.ts.map +1 -1
  12. package/dist/commands/pi.js +19 -0
  13. package/dist/commands/pi.js.map +1 -1
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/advanced-setup.js +7 -7
  17. package/dist/lib/advanced-setup.js.map +1 -1
  18. package/dist/lib/discovery-agent.js +1 -1
  19. package/dist/lib/discovery-agent.js.map +1 -1
  20. package/dist/lib/linear-webhook.d.ts +50 -0
  21. package/dist/lib/linear-webhook.d.ts.map +1 -0
  22. package/dist/lib/linear-webhook.js +92 -0
  23. package/dist/lib/linear-webhook.js.map +1 -0
  24. package/dist/lib/onboarding.js +1 -1
  25. package/dist/lib/onboarding.js.map +1 -1
  26. package/dist/lib/rl-manager.d.ts +1 -1
  27. package/dist/lib/rl-manager.d.ts.map +1 -1
  28. package/dist/lib/rl-manager.js +3 -3
  29. package/dist/lib/rl-manager.js.map +1 -1
  30. package/dist/lib/tool-schemas.d.ts +35 -0
  31. package/dist/lib/tool-schemas.d.ts.map +1 -0
  32. package/dist/lib/tool-schemas.js +246 -0
  33. package/dist/lib/tool-schemas.js.map +1 -0
  34. package/dist/lib/workspace/data-pipeline.d.ts.map +1 -1
  35. package/dist/lib/workspace/data-pipeline.js +29 -20
  36. package/dist/lib/workspace/data-pipeline.js.map +1 -1
  37. package/dist/lib/workspace/engine.d.ts +1 -0
  38. package/dist/lib/workspace/engine.d.ts.map +1 -1
  39. package/dist/lib/workspace/engine.js +10 -0
  40. package/dist/lib/workspace/engine.js.map +1 -1
  41. package/dist/mcp/context-hub-mcp.js +7 -1
  42. package/dist/mcp/context-hub-mcp.js.map +1 -1
  43. package/dist/types/telemetry.d.ts +1 -0
  44. package/dist/types/telemetry.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/packages/pi/assets/boot.mp3 +0 -0
  47. package/packages/pi/extensions/autoresearch.ts +3 -2
  48. package/packages/pi/extensions/context.ts +29 -116
  49. package/packages/pi/extensions/eval.ts +2 -1
  50. package/packages/pi/extensions/hub-tools.ts +31 -11
  51. package/packages/pi/extensions/hud-tool.ts +230 -69
  52. package/packages/pi/extensions/index.ts +39 -63
  53. package/packages/pi/extensions/jfl-resolve.ts +98 -0
  54. package/packages/pi/extensions/journal.ts +91 -6
  55. package/packages/pi/extensions/map-bridge.ts +31 -0
  56. package/packages/pi/extensions/onboarding-v2.ts +367 -399
  57. package/packages/pi/extensions/peter-parker.ts +2 -1
  58. package/packages/pi/extensions/policy-head-tool.ts +3 -2
  59. package/packages/pi/extensions/portfolio-bridge.ts +3 -4
  60. package/packages/pi/extensions/session.ts +91 -15
  61. package/packages/pi/extensions/stratus-bridge.ts +2 -1
  62. package/packages/pi/extensions/synopsis-tool.ts +6 -1
  63. package/packages/pi/extensions/training-buffer-tool.ts +3 -2
  64. package/packages/pi/extensions/types.ts +2 -0
  65. package/packages/pi/package.json +3 -1
  66. package/packages/pi/skills/viz/SKILL.md +204 -0
@@ -12,6 +12,7 @@ import { spawn } from "child_process"
12
12
  import { existsSync, readFileSync } from "fs"
13
13
  import { join } from "path"
14
14
  import type { PiContext, JflConfig } from "./types.js"
15
+ import { jflImport } from "./jfl-resolve.js"
15
16
  import { emitCustomEvent, hubUrl, authToken } from "./map-bridge.js"
16
17
 
17
18
  interface ReviewTask {
@@ -38,7 +39,7 @@ let activeIterations = new Map<string, PeterIteration>()
38
39
  async function getPredictor(root: string) {
39
40
  try {
40
41
  // @ts-ignore — resolved from jfl package at runtime
41
- const { Predictor } = await import("../../src/lib/predictor.js")
42
+ const { Predictor } = await jflImport("predictor")
42
43
  return new Predictor(root)
43
44
  } catch {
44
45
  return null
@@ -14,6 +14,7 @@
14
14
  import { existsSync, readFileSync } from "fs"
15
15
  import { join } from "path"
16
16
  import type { PiContext, JflConfig } from "./types.js"
17
+ import { jflImport } from "./jfl-resolve.js"
17
18
  import { emitCustomEvent } from "./map-bridge.js"
18
19
 
19
20
  let projectRoot = ""
@@ -40,7 +41,7 @@ function getWeightsInfo(): PolicyWeights | null {
40
41
  async function getPolicyHead(): Promise<any> {
41
42
  try {
42
43
  // @ts-ignore — resolved from jfl package at runtime
43
- const { PolicyHeadInference } = await import("../../src/lib/policy-head.js")
44
+ const { PolicyHeadInference } = await jflImport("policy-head")
44
45
  return new PolicyHeadInference(projectRoot)
45
46
  } catch {
46
47
  return null
@@ -50,7 +51,7 @@ async function getPolicyHead(): Promise<any> {
50
51
  async function getTrainingBuffer(): Promise<any> {
51
52
  try {
52
53
  // @ts-ignore — resolved from jfl package at runtime
53
- const { TrainingBuffer } = await import("../../src/lib/training-buffer.js")
54
+ const { TrainingBuffer } = await jflImport("training-buffer")
54
55
  return new TrainingBuffer(projectRoot)
55
56
  } catch {
56
57
  return null
@@ -11,6 +11,7 @@ import { existsSync, readFileSync } from "fs"
11
11
  import { join } from "path"
12
12
  import type { PiContext, JflConfig } from "./types.js"
13
13
  import { emitCustomEvent } from "./map-bridge.js"
14
+ import { jflImport } from "./jfl-resolve.js"
14
15
 
15
16
  let projectRoot = ""
16
17
  let portfolioParent: string | null = null
@@ -19,8 +20,7 @@ async function phoneHome(ctx: PiContext): Promise<void> {
19
20
  if (!portfolioParent || !existsSync(portfolioParent)) return
20
21
 
21
22
  try {
22
- // @ts-ignore resolved from jfl package at runtime
23
- const { phoneHomeToPortfolio } = await import("../../src/lib/service-gtm.js")
23
+ const { phoneHomeToPortfolio } = await jflImport("service-gtm")
24
24
  await phoneHomeToPortfolio(projectRoot)
25
25
  ctx.log("Portfolio phoneHome complete", "debug")
26
26
  } catch (err) {
@@ -32,8 +32,7 @@ async function syncJournalEntry(ctx: PiContext, entry: unknown): Promise<void> {
32
32
  if (!portfolioParent || !existsSync(portfolioParent)) return
33
33
 
34
34
  try {
35
- // @ts-ignore resolved from jfl package at runtime
36
- const { writeSyncToParent } = await import("../../src/lib/service-gtm.js")
35
+ const { writeSyncToParent } = await jflImport("service-gtm")
37
36
  await (writeSyncToParent as (root: string, entry: unknown) => Promise<void>)(projectRoot, entry)
38
37
  } catch (err) {
39
38
  ctx.log(`Journal sync to parent failed: ${err}`, "debug")
@@ -41,7 +41,7 @@ export async function setupSession(ctx: PiContext, _config: JflConfig): Promise<
41
41
  ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
42
42
  },
43
43
  body: JSON.stringify({ runtime: "pi" }),
44
- signal: AbortSignal.timeout(90000),
44
+ signal: AbortSignal.timeout(30000),
45
45
  })
46
46
 
47
47
  if (resp.ok) {
@@ -55,12 +55,16 @@ export async function setupSession(ctx: PiContext, _config: JflConfig): Promise<
55
55
  }
56
56
  ctx.log(`Session init via Hub: branch=${sessionBranch}, sync=${data.syncOk}`, "debug")
57
57
  } else {
58
- ctx.log("Hub session init failed, falling back to local", "warn")
58
+ const errText = await resp.text().catch(() => "unknown")
59
+ ctx.log(`Hub session init failed (${resp.status}): ${errText}`, "warn")
60
+ ctx.ui.notify("Session init failed — working on current branch", { level: "warn" })
59
61
  sessionBranch = ctx.session.branch
60
62
  }
61
- } catch {
63
+ } catch (err) {
62
64
  // Hub not available — fall back to local branch detection
63
- ctx.log("Hub unavailable for session init, using local branch", "debug")
65
+ const msg = err instanceof Error ? err.message : String(err)
66
+ ctx.log(`Hub unavailable for session init: ${msg}`, "warn")
67
+ ctx.ui.notify(`Hub unreachable (${hubUrl}) — no session branch created`, { level: "warn" })
64
68
  sessionBranch = ctx.session.branch
65
69
  }
66
70
 
@@ -148,14 +152,38 @@ export async function pivot(ctx: PiContext, summary?: string): Promise<{
148
152
 
149
153
  export async function onShutdown(ctx: PiContext): Promise<void> {
150
154
  const root = ctx.session.projectRoot
155
+ const branch = getSessionBranch() || getCurrentBranch(root)
156
+
157
+ ctx.ui.notify("Shutting down session…", { level: "info" })
158
+
159
+ // ── 1. Journal check (parity with CC Stop hook) ──────────────────────
160
+ const journalPath = join(root, ".jfl", "journal", `${branch}.jsonl`)
161
+ const hasJournal = existsSync(journalPath) && readFileSync(journalPath, "utf-8").trim().length > 0
162
+ if (!hasJournal) {
163
+ ctx.ui.notify(
164
+ "⚠️ No journal entry for this session. Context will be lost.\n" +
165
+ ` File: .jfl/journal/${branch}.jsonl`,
166
+ { level: "warn" }
167
+ )
168
+ }
151
169
 
152
- // Kill auto-commit daemon
170
+ // ── 2. Kill auto-commit daemon ───────────────────────────────────────
153
171
  if (autoCommitProcess) {
154
172
  try { autoCommitProcess.kill() } catch {}
155
173
  autoCommitProcess = null
174
+ ctx.ui.notify(" ✓ Auto-commit stopped", { level: "info" })
156
175
  }
157
176
 
158
- // Call Hub session end handles journal check + cleanup
177
+ // ── 3. Auto-commit any uncommitted changes ───────────────────────────
178
+ try {
179
+ const status = execSync("git status --porcelain", { cwd: root, timeout: 3000, encoding: "utf-8" }).trim()
180
+ if (status) {
181
+ execSync("git add -A && git commit -m 'auto: session-end save'", { cwd: root, timeout: 5000, stdio: "pipe" })
182
+ ctx.ui.notify(" ✓ Uncommitted changes saved", { level: "info" })
183
+ }
184
+ } catch {}
185
+
186
+ // ── 4. Call Hub session end ──────────────────────────────────────────
159
187
  try {
160
188
  await fetch(`${hubUrl}/api/session/end`, {
161
189
  method: "POST",
@@ -164,24 +192,72 @@ export async function onShutdown(ctx: PiContext): Promise<void> {
164
192
  ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
165
193
  },
166
194
  body: JSON.stringify({ runtime: "pi" }),
167
- signal: AbortSignal.timeout(90000),
195
+ signal: AbortSignal.timeout(5000),
168
196
  })
169
- ctx.log("Session ended via Hub", "debug")
197
+ ctx.ui.notify(" Hub session closed", { level: "info" })
170
198
  } catch {
171
- // Fallback: run cleanup script directly
172
- const cleanupScript = findScript(root, "session-cleanup.sh")
173
- if (cleanupScript) {
199
+ ctx.log("Hub session end failed — running local cleanup", "debug")
200
+ }
201
+
202
+ // ── 5. Merge session branch back (parity with CC session-cleanup.sh) ─
203
+ if (branch.startsWith("session-")) {
204
+ try {
205
+ // Get working branch from config or default to main
206
+ let workingBranch = "main"
207
+ const configPath = join(root, ".jfl", "config.json")
208
+ if (existsSync(configPath)) {
209
+ try {
210
+ const config = JSON.parse(readFileSync(configPath, "utf-8"))
211
+ if (config.working_branch) workingBranch = config.working_branch
212
+ } catch {}
213
+ }
214
+
215
+ // Push session branch first
216
+ try {
217
+ execSync(`git push origin ${branch}`, { cwd: root, timeout: 15000, stdio: "pipe" })
218
+ ctx.ui.notify(" ✓ Session branch pushed", { level: "info" })
219
+ } catch {}
220
+
221
+ // Merge session into working branch
222
+ execSync(`git checkout ${workingBranch}`, { cwd: root, timeout: 5000, stdio: "pipe" })
223
+ execSync(`git merge ${branch} --no-edit`, { cwd: root, timeout: 10000, stdio: "pipe" })
224
+ ctx.ui.notify(` ✓ Merged ${branch} → ${workingBranch}`, { level: "info" })
225
+
226
+ // Push merged working branch
174
227
  try {
175
- execSync(`bash "${cleanupScript}"`, { cwd: root, stdio: "inherit" })
176
- } catch (err) {
177
- ctx.log(`session-cleanup.sh failed: ${err}`, "warn")
228
+ execSync(`git push origin ${workingBranch}`, { cwd: root, timeout: 15000, stdio: "pipe" })
229
+ } catch {}
230
+
231
+ // Delete session branch (local + remote)
232
+ try {
233
+ execSync(`git branch -d ${branch}`, { cwd: root, timeout: 5000, stdio: "pipe" })
234
+ execSync(`git push origin --delete ${branch}`, { cwd: root, timeout: 10000, stdio: "pipe" })
235
+ ctx.ui.notify(" ✓ Session branch cleaned up", { level: "info" })
236
+ } catch {}
237
+ } catch (err) {
238
+ const msg = err instanceof Error ? err.message : String(err)
239
+ if (msg.includes("CONFLICT") || msg.includes("conflict")) {
240
+ ctx.ui.notify(` ⚠ Merge conflict — session branch ${branch} preserved`, { level: "warn" })
241
+ // Switch back to session branch so user can resolve
242
+ try { execSync(`git checkout ${branch}`, { cwd: root, timeout: 5000, stdio: "pipe" }) } catch {}
243
+ } else {
244
+ ctx.ui.notify(` ⚠ Branch merge failed: ${msg.slice(0, 80)}`, { level: "warn" })
178
245
  }
179
246
  }
180
247
  }
181
248
 
249
+ // ── 6. Fallback cleanup script ───────────────────────────────────────
250
+ const cleanupScript = findScript(root, "session-cleanup.sh")
251
+ if (cleanupScript && !branch.startsWith("session-")) {
252
+ // Only run cleanup script if we didn't already handle merge above
253
+ try {
254
+ execSync(`bash "${cleanupScript}"`, { cwd: root, timeout: 10000, stdio: "pipe" })
255
+ } catch {}
256
+ }
257
+
182
258
  ctx.emit("hook:session-end", {
183
259
  session: ctx.session.id,
184
- branch: ctx.session.branch,
260
+ branch,
185
261
  ts: new Date().toISOString(),
186
262
  })
187
263
  }
@@ -11,6 +11,7 @@ import { existsSync, appendFileSync, mkdirSync } from "fs"
11
11
  import { join } from "path"
12
12
  import { homedir } from "os"
13
13
  import type { PiContext, AgentStartEvent, AgentEndEvent } from "./types.js"
14
+ import { jflImport } from "./jfl-resolve.js"
14
15
  import { emitCustomEvent } from "./map-bridge.js"
15
16
 
16
17
  const TRAINING_BUFFER = join(homedir(), ".jfl", "training-buffer.jsonl")
@@ -30,7 +31,7 @@ function getTrainingBufferPath(): string {
30
31
  async function getPredictor(root: string) {
31
32
  try {
32
33
  // @ts-ignore — resolved from jfl package at runtime
33
- const { Predictor } = await import("../../src/lib/predictor.js")
34
+ const { Predictor } = await jflImport("predictor")
34
35
  return new Predictor(root)
35
36
  } catch {
36
37
  return null
@@ -48,7 +48,12 @@ export function setupSynopsisTool(ctx: PiContext): void {
48
48
  },
49
49
  },
50
50
  async handler(input) {
51
- const { hours, author } = input as { hours?: number; author?: string }
51
+ const raw = input as { hours?: number | string; author?: string }
52
+ // Model sometimes passes "24h" or "48h" — strip the 'h' and parse
53
+ const hours = typeof raw.hours === "string"
54
+ ? parseInt(String(raw.hours).replace(/[^0-9]/g, ""), 10) || 24
55
+ : raw.hours
56
+ const author = raw.author
52
57
 
53
58
  const synopsisScript = findSynopsisScript(projectRoot)
54
59
  if (synopsisScript) {
@@ -14,6 +14,7 @@
14
14
  import { existsSync, readFileSync } from "fs"
15
15
  import { join } from "path"
16
16
  import type { PiContext, JflConfig, AgentEndEvent } from "./types.js"
17
+ import { jflImport } from "./jfl-resolve.js"
17
18
  import { emitCustomEvent } from "./map-bridge.js"
18
19
 
19
20
  let projectRoot = ""
@@ -21,7 +22,7 @@ let projectRoot = ""
21
22
  async function getTrainingBuffer(): Promise<any> {
22
23
  try {
23
24
  // @ts-ignore — resolved from jfl package at runtime
24
- const { TrainingBuffer } = await import("../../src/lib/training-buffer.js")
25
+ const { TrainingBuffer } = await jflImport("training-buffer")
25
26
  return new TrainingBuffer(projectRoot)
26
27
  } catch {
27
28
  return null
@@ -31,7 +32,7 @@ async function getTrainingBuffer(): Promise<any> {
31
32
  async function getTupleMiner(): Promise<any> {
32
33
  try {
33
34
  // @ts-ignore — resolved from jfl package at runtime
34
- return await import("../../src/lib/tuple-miner.js")
35
+ return await jflImport("tuple-miner")
35
36
  } catch {
36
37
  return null
37
38
  }
@@ -115,6 +115,8 @@ export interface AgentEndEvent {
115
115
  export interface ToolExecutionEvent {
116
116
  toolName?: string
117
117
  tool?: string
118
+ input?: Record<string, any>
119
+ args?: Record<string, any>
118
120
  result?: unknown
119
121
  isError?: boolean
120
122
  duration?: number
@@ -36,7 +36,9 @@
36
36
  "typescript": "^5.3.3"
37
37
  },
38
38
  "pi": {
39
- "entry": "dist/extensions/index.js",
39
+
40
+ "entry": "dist/index.js",
41
+ "extensions": ["dist/index.js"],
40
42
  "skills": "skills",
41
43
  "themes": "themes/jfl.theme.json",
42
44
  "teams": "teams"
@@ -0,0 +1,204 @@
1
+ ---
2
+ name: viz
3
+ description: Terminal data visualization via kuva — inline plots for agents and humans
4
+ ---
5
+
6
+ # /viz — Terminal Visualization
7
+
8
+ Pipe JFL data to kuva for inline terminal plots. Agents see results without leaving flow.
9
+
10
+ ## Prerequisites
11
+
12
+ ```bash
13
+ cargo install kuva --features cli
14
+ ```
15
+
16
+ If kuva is not installed, falls back to ASCII bar/sparkline rendering.
17
+
18
+ ## Commands
19
+
20
+ ```
21
+ /viz events # Event bus activity (bar chart by type, sankey of service flows)
22
+ /viz sessions # Session activity (line chart over time, duration box plot)
23
+ /viz costs # API costs (bar by model, pie by provider)
24
+ /viz tools # Tool usage frequency (bar chart)
25
+ /viz flows # Flow trigger rates (bar chart, sankey of trigger→action)
26
+ /viz arena # Arena leaderboard (composite bar chart, score trajectory)
27
+ /viz learning # Pattern confidence trends (line chart)
28
+ /viz health # Service health dashboard (cost sparklines, error bars, latency box)
29
+ /viz <custom> # Describe what you want to visualize
30
+ ```
31
+
32
+ ## How It Works
33
+
34
+ Each `/viz` command:
35
+ 1. Reads the relevant data source (events, journals, telemetry, arena results)
36
+ 2. Transforms to TSV format
37
+ 3. Pipes to `kuva [plot-type] --terminal`
38
+ 4. Renders inline in the terminal
39
+
40
+ ## Implementation
41
+
42
+ ### /viz events
43
+
44
+ Read `.jfl/service-events.jsonl` and `.jfl/map-events.jsonl`:
45
+
46
+ ```bash
47
+ # Event type distribution
48
+ cat .jfl/service-events.jsonl | \
49
+ jq -r '.type' | sort | uniq -c | sort -rn | \
50
+ awk '{print $2"\t"$1}' | \
51
+ kuva bar --label-col 0 --value-col 1 --title "Events by Type" --terminal
52
+
53
+ # Service-to-service flow (sankey)
54
+ cat .jfl/service-events.jsonl | \
55
+ jq -r '[.source // "unknown", .type, 1] | @tsv' | \
56
+ kuva sankey --source-col 0 --target-col 1 --value-col 2 --title "Event Flows" --terminal
57
+ ```
58
+
59
+ Or use the programmatic API from `jfl-cli/src/lib/kuva.ts`:
60
+
61
+ ```typescript
62
+ import { barChart, linePlot, sparkline } from 'jfl-cli/src/lib/kuva.js'
63
+
64
+ const events = loadEvents()
65
+ const byType = countBy(events, 'type')
66
+ const chart = barChart(
67
+ Object.entries(byType).map(([label, value]) => ({ label, value })),
68
+ 'Events by Type'
69
+ )
70
+ console.log(chart)
71
+ ```
72
+
73
+ ### /viz sessions
74
+
75
+ ```bash
76
+ # Sessions over time (line)
77
+ cat .jfl/journal/*.jsonl | \
78
+ jq -r 'select(.type == "session-end") | [.ts[:10], 1] | @tsv' | \
79
+ sort | uniq -c | awk '{print $2"\t"$1}' | \
80
+ kuva line --x 0 --y 1 --title "Sessions per Day" --terminal
81
+ ```
82
+
83
+ ### /viz costs
84
+
85
+ ```bash
86
+ # Cost by model (bar)
87
+ cat .jfl/telemetry-queue.jsonl | \
88
+ jq -r 'select(.event == "stratus:api_call") | [(.model_name // "unknown"), .estimated_cost_usd] | @tsv' | \
89
+ kuva bar --label-col 0 --value-col 1 --title "Cost by Model" --terminal
90
+ ```
91
+
92
+ ### /viz arena
93
+
94
+ ```bash
95
+ # Run from arena directory
96
+ cd /path/to/productrank-arena
97
+ npm run arena -- leaderboard --all --plots
98
+ ```
99
+
100
+ Or programmatically — arena's `formatLeaderboardPlots()` renders composite scores as kuva bar chart.
101
+
102
+ ### /viz flows
103
+
104
+ ```bash
105
+ # Flow triggers (bar)
106
+ cat .jfl/service-events.jsonl | \
107
+ jq -r 'select(.type == "flow:triggered") | .data.flow_name' | \
108
+ sort | uniq -c | sort -rn | awk '{print $2"\t"$1}' | \
109
+ kuva bar --label-col 0 --value-col 1 --title "Flow Triggers" --terminal
110
+
111
+ # Flow trigger→action sankey
112
+ cat .jfl/service-events.jsonl | \
113
+ jq -r 'select(.type == "flow:completed") | [.data.flow_name, .data.action_type, 1] | @tsv' | \
114
+ kuva sankey --source-col 0 --target-col 1 --value-col 2 --title "Flow Actions" --terminal
115
+ ```
116
+
117
+ ### /viz health
118
+
119
+ Composite dashboard — renders multiple charts:
120
+
121
+ ```
122
+ Health Dashboard
123
+ ━━━━━━━━━━━━━━━━
124
+
125
+ API Costs (last 24h)
126
+ [kuva bar: cost by model]
127
+
128
+ Error Rate ▁▂▁▁▃▅▂▁▁▁ (sparkline)
129
+
130
+ Latency Distribution
131
+ [kuva box: latency by endpoint]
132
+
133
+ Session Duration
134
+ [kuva box: duration by session]
135
+ ```
136
+
137
+ ### /viz learning (RL / Training)
138
+
139
+ For agents with reinforcement learning loops:
140
+
141
+ ```typescript
142
+ // After each training epoch
143
+ const rewards = epochs.map((e, i) => ({ ts: `epoch-${i}`, value: e.reward }))
144
+ const chart = linePlot(rewards, 'Reward per Epoch')
145
+ console.log(chart)
146
+
147
+ // Action distribution
148
+ const actions = countBy(episodes, 'action')
149
+ const actionChart = barChart(
150
+ Object.entries(actions).map(([label, value]) => ({ label, value })),
151
+ 'Action Frequency'
152
+ )
153
+ console.log(actionChart)
154
+ ```
155
+
156
+ ### /viz custom
157
+
158
+ When user describes what they want to visualize, the agent:
159
+ 1. Identifies the data source
160
+ 2. Extracts/transforms to TSV
161
+ 3. Picks the right kuva plot type
162
+ 4. Renders with `--terminal`
163
+
164
+ ## Available Plot Types
165
+
166
+ | kuva command | Best for | Key flags |
167
+ |-------------|----------|-----------|
168
+ | `bar` | Categorical comparisons | `--label-col`, `--value-col` |
169
+ | `line` | Time series, trends | `--x`, `--y`, `--color-by` |
170
+ | `scatter` | Correlations, clustering | `--x`, `--y`, `--color-by` |
171
+ | `box` | Distributions, outliers | `--group-col`, `--value-col` |
172
+ | `histogram` | Value distributions | `--value-col`, `--bins` |
173
+ | `pie` | Proportions | `--label-col`, `--value-col`, `--donut` |
174
+ | `sankey` | Flow routing, transitions | `--source-col`, `--target-col`, `--value-col` |
175
+ | `heatmap` | Matrix data, correlations | First col = row labels |
176
+ | `violin` | Distribution shape | `--group-col`, `--value-col` |
177
+
178
+ ## Design Principles
179
+
180
+ 1. **TSV is the universal interface** — any data source that can produce rows can generate kuva plots
181
+ 2. **Graceful degradation** — ASCII fallback (bars, sparklines) when kuva isn't installed
182
+ 3. **Agent-native** — inline terminal output, no browser needed, no context-switching
183
+ 4. **Composable** — pipe any JSONL through `jq -r @tsv` into kuva. Unix philosophy.
184
+ 5. **Zero coupling** — kuva is a standalone Rust binary. kuva.ts is a standalone module. Neither needs the other's internals.
185
+
186
+ ## Examples
187
+
188
+ ```
189
+ User: "show me event activity"
190
+ Agent: *runs /viz events*
191
+ → Bar chart of event types + sankey of service flows
192
+
193
+ User: "what's our cost breakdown?"
194
+ Agent: *runs /viz costs*
195
+ → Bar chart by model + pie chart by provider
196
+
197
+ User: "how is the arena looking?"
198
+ Agent: *runs /viz arena*
199
+ → Leaderboard table + composite score bar chart
200
+
201
+ User: "plot my training rewards"
202
+ Agent: *runs /viz learning with user's data*
203
+ → Line chart of reward per epoch + action frequency bars
204
+ ```