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.
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +118 -2
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/ide.d.ts.map +1 -1
- package/dist/commands/ide.js +22 -0
- package/dist/commands/ide.js.map +1 -1
- package/dist/commands/linear.d.ts.map +1 -1
- package/dist/commands/linear.js +24 -0
- package/dist/commands/linear.js.map +1 -1
- package/dist/commands/pi.d.ts +3 -0
- package/dist/commands/pi.d.ts.map +1 -1
- package/dist/commands/pi.js +19 -0
- package/dist/commands/pi.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/advanced-setup.js +7 -7
- package/dist/lib/advanced-setup.js.map +1 -1
- package/dist/lib/discovery-agent.js +1 -1
- package/dist/lib/discovery-agent.js.map +1 -1
- package/dist/lib/linear-webhook.d.ts +50 -0
- package/dist/lib/linear-webhook.d.ts.map +1 -0
- package/dist/lib/linear-webhook.js +92 -0
- package/dist/lib/linear-webhook.js.map +1 -0
- package/dist/lib/onboarding.js +1 -1
- package/dist/lib/onboarding.js.map +1 -1
- package/dist/lib/rl-manager.d.ts +1 -1
- package/dist/lib/rl-manager.d.ts.map +1 -1
- package/dist/lib/rl-manager.js +3 -3
- package/dist/lib/rl-manager.js.map +1 -1
- package/dist/lib/tool-schemas.d.ts +35 -0
- package/dist/lib/tool-schemas.d.ts.map +1 -0
- package/dist/lib/tool-schemas.js +246 -0
- package/dist/lib/tool-schemas.js.map +1 -0
- package/dist/lib/workspace/data-pipeline.d.ts.map +1 -1
- package/dist/lib/workspace/data-pipeline.js +29 -20
- package/dist/lib/workspace/data-pipeline.js.map +1 -1
- package/dist/lib/workspace/engine.d.ts +1 -0
- package/dist/lib/workspace/engine.d.ts.map +1 -1
- package/dist/lib/workspace/engine.js +10 -0
- package/dist/lib/workspace/engine.js.map +1 -1
- package/dist/mcp/context-hub-mcp.js +7 -1
- package/dist/mcp/context-hub-mcp.js.map +1 -1
- package/dist/types/telemetry.d.ts +1 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/package.json +1 -1
- package/packages/pi/assets/boot.mp3 +0 -0
- package/packages/pi/extensions/autoresearch.ts +3 -2
- package/packages/pi/extensions/context.ts +29 -116
- package/packages/pi/extensions/eval.ts +2 -1
- package/packages/pi/extensions/hub-tools.ts +31 -11
- package/packages/pi/extensions/hud-tool.ts +230 -69
- package/packages/pi/extensions/index.ts +39 -63
- package/packages/pi/extensions/jfl-resolve.ts +98 -0
- package/packages/pi/extensions/journal.ts +91 -6
- package/packages/pi/extensions/map-bridge.ts +31 -0
- package/packages/pi/extensions/onboarding-v2.ts +367 -399
- package/packages/pi/extensions/peter-parker.ts +2 -1
- package/packages/pi/extensions/policy-head-tool.ts +3 -2
- package/packages/pi/extensions/portfolio-bridge.ts +3 -4
- package/packages/pi/extensions/session.ts +91 -15
- package/packages/pi/extensions/stratus-bridge.ts +2 -1
- package/packages/pi/extensions/synopsis-tool.ts +6 -1
- package/packages/pi/extensions/training-buffer-tool.ts +3 -2
- package/packages/pi/extensions/types.ts +2 -0
- package/packages/pi/package.json +3 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
195
|
+
signal: AbortSignal.timeout(5000),
|
|
168
196
|
})
|
|
169
|
-
ctx.
|
|
197
|
+
ctx.ui.notify(" ✓ Hub session closed", { level: "info" })
|
|
170
198
|
} catch {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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(`
|
|
176
|
-
} catch
|
|
177
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
35
|
+
return await jflImport("tuple-miner")
|
|
35
36
|
} catch {
|
|
36
37
|
return null
|
|
37
38
|
}
|
package/packages/pi/package.json
CHANGED
|
@@ -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
|
+
```
|