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
|
@@ -1,105 +1,250 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HUD Tool Extension
|
|
3
3
|
*
|
|
4
|
-
* Registers jfl_hud tool and /hud command with
|
|
5
|
-
* Shows
|
|
6
|
-
*
|
|
4
|
+
* Registers jfl_hud tool and /hud command with a real project dashboard.
|
|
5
|
+
* Shows: system health, recent work, eval scores, branch, phase.
|
|
6
|
+
* Fast — reads local files, one hub health check, no heavy API calls.
|
|
7
7
|
*
|
|
8
|
-
* @purpose jfl_hud tool + /hud command
|
|
8
|
+
* @purpose jfl_hud tool + /hud command — real dashboard, not a stub
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync } from "fs"
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from "fs"
|
|
12
12
|
import { join } from "path"
|
|
13
13
|
import { execSync } from "child_process"
|
|
14
14
|
import type { PiContext, PiTheme } from "./types.js"
|
|
15
15
|
import { hudRenderCall, hudRenderResult } from "./tool-renderers.js"
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
let projectRoot = ""
|
|
18
|
+
|
|
19
|
+
// ─── Data gathering (all fast, local reads) ─────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function getConfig(root: string): { name: string; type: string } {
|
|
22
|
+
const configPath = join(root, ".jfl", "config.json")
|
|
23
|
+
let name = root.split("/").pop() ?? "JFL"
|
|
24
|
+
let type = "gtm"
|
|
25
|
+
if (existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const c = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
28
|
+
if (c.name) name = c.name
|
|
29
|
+
if (c.type) type = c.type
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
return { name, type }
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
function getBranch(root: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return execSync("git branch --show-current", { cwd: root, timeout: 2000, stdio: ["pipe", "pipe", "ignore"] }).toString().trim()
|
|
38
|
+
} catch { return "unknown" }
|
|
39
|
+
}
|
|
24
40
|
|
|
25
41
|
function getDaysToLaunch(root: string): string | null {
|
|
26
42
|
const roadmapPath = join(root, "knowledge", "ROADMAP.md")
|
|
27
43
|
if (!existsSync(roadmapPath)) return null
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(roadmapPath, "utf-8")
|
|
46
|
+
const dateMatch = content.match(/(\d{4}-\d{2}-\d{2})/m)
|
|
47
|
+
if (!dateMatch) return null
|
|
48
|
+
const diff = Math.ceil((new Date(dateMatch[1]).getTime() - Date.now()) / 86400000)
|
|
49
|
+
return diff > 0 ? `${diff}d to launch` : `${Math.abs(diff)}d past launch`
|
|
50
|
+
} catch { return null }
|
|
51
|
+
}
|
|
28
52
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
53
|
+
function getPhase(root: string): string {
|
|
54
|
+
const roadmapPath = join(root, "knowledge", "ROADMAP.md")
|
|
55
|
+
if (!existsSync(roadmapPath)) return ""
|
|
56
|
+
try {
|
|
57
|
+
const content = readFileSync(roadmapPath, "utf-8")
|
|
58
|
+
const match = content.match(/## (?:Phase|Current Phase)[:\t ]*([^\n]+)/i)
|
|
59
|
+
if (match && match[1].trim() && !match[1].trim().startsWith("```")) return match[1].trim()
|
|
60
|
+
} catch {}
|
|
61
|
+
return ""
|
|
62
|
+
}
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
interface JournalEntry {
|
|
65
|
+
title: string
|
|
66
|
+
type: string
|
|
67
|
+
ts: string
|
|
68
|
+
status?: string
|
|
37
69
|
}
|
|
38
70
|
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
if (!existsSync(
|
|
71
|
+
function getRecentJournal(root: string, count: number = 5): JournalEntry[] {
|
|
72
|
+
const journalDir = join(root, ".jfl", "journal")
|
|
73
|
+
if (!existsSync(journalDir)) return []
|
|
42
74
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return sameLineMatch[1].trim()
|
|
47
|
-
}
|
|
75
|
+
try {
|
|
76
|
+
const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort()
|
|
77
|
+
const entries: JournalEntry[] = []
|
|
48
78
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
// Read from newest files first
|
|
80
|
+
for (let i = files.length - 1; i >= 0 && entries.length < count * 2; i--) {
|
|
81
|
+
try {
|
|
82
|
+
const lines = readFileSync(join(journalDir, files[i]), "utf-8").trim().split("\n").filter(Boolean)
|
|
83
|
+
for (let j = lines.length - 1; j >= 0 && entries.length < count * 2; j--) {
|
|
84
|
+
try {
|
|
85
|
+
const e = JSON.parse(lines[j])
|
|
86
|
+
if (e.title && e.type !== "pivot") {
|
|
87
|
+
entries.push({ title: e.title, type: e.type ?? "note", ts: e.ts ?? "", status: e.status })
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return entries.slice(0, count)
|
|
95
|
+
} catch { return [] }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getJournalStats(root: string): { files: number; entries: number } {
|
|
99
|
+
const journalDir = join(root, ".jfl", "journal")
|
|
100
|
+
if (!existsSync(journalDir)) return { files: 0, entries: 0 }
|
|
101
|
+
try {
|
|
102
|
+
const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl"))
|
|
103
|
+
let entries = 0
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
try {
|
|
106
|
+
entries += readFileSync(join(journalDir, f), "utf-8").trim().split("\n").filter(Boolean).length
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
return { files: files.length, entries }
|
|
110
|
+
} catch { return { files: 0, entries: 0 } }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface SystemHealth {
|
|
114
|
+
hub: "up" | "down"
|
|
115
|
+
hubPort: number
|
|
116
|
+
autoCommit: boolean
|
|
117
|
+
watchdog: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function checkSystemHealth(root: string): SystemHealth {
|
|
121
|
+
const portFile = join(root, ".jfl", "context-hub.port")
|
|
122
|
+
const port = existsSync(portFile)
|
|
123
|
+
? parseInt(readFileSync(portFile, "utf-8").trim()) || 4360
|
|
124
|
+
: 4360
|
|
56
125
|
|
|
57
|
-
|
|
126
|
+
let hubUp: "up" | "down" = "down"
|
|
127
|
+
try {
|
|
128
|
+
const resp = execSync(`curl -sf http://localhost:${port}/health`, { timeout: 2000, stdio: ["pipe", "pipe", "ignore"] }).toString()
|
|
129
|
+
if (resp.includes("ok")) hubUp = "up"
|
|
130
|
+
} catch {}
|
|
131
|
+
|
|
132
|
+
let autoCommit = false
|
|
133
|
+
try {
|
|
134
|
+
const ps = execSync("ps aux", { timeout: 2000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] })
|
|
135
|
+
autoCommit = ps.includes("auto-commit.sh")
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
let watchdog = false
|
|
139
|
+
try {
|
|
140
|
+
const pidFile = join(root, ".jfl", "hub-watchdog.pid")
|
|
141
|
+
if (existsSync(pidFile)) {
|
|
142
|
+
const pid = readFileSync(pidFile, "utf-8").trim()
|
|
143
|
+
execSync(`kill -0 ${pid}`, { stdio: "ignore" })
|
|
144
|
+
watchdog = true
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
|
|
148
|
+
return { hub: hubUp, hubPort: port, autoCommit, watchdog }
|
|
58
149
|
}
|
|
59
150
|
|
|
60
|
-
function
|
|
151
|
+
function getEvalSummary(root: string): string | null {
|
|
152
|
+
const evalPath = join(root, ".jfl", "eval.jsonl")
|
|
153
|
+
if (!existsSync(evalPath)) return null
|
|
61
154
|
try {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
155
|
+
const lines = readFileSync(evalPath, "utf-8").trim().split("\n").filter(Boolean)
|
|
156
|
+
if (lines.length === 0) return null
|
|
157
|
+
// Scan backwards for most recent entry with a composite/score
|
|
158
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 20); i--) {
|
|
159
|
+
try {
|
|
160
|
+
const entry = JSON.parse(lines[i])
|
|
161
|
+
const score = entry.composite ?? entry.score ?? null
|
|
162
|
+
if (score !== null && score !== undefined) {
|
|
163
|
+
const tests = entry.metrics?.tests_passed !== undefined
|
|
164
|
+
? `${entry.metrics.tests_passed}/${entry.metrics.tests_total} tests`
|
|
165
|
+
: ""
|
|
166
|
+
return `${Number(score).toFixed(2)}${tests ? ` (${tests})` : ""}`
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
return null
|
|
73
171
|
} catch { return null }
|
|
74
172
|
}
|
|
75
173
|
|
|
76
|
-
function
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
174
|
+
function getTrainingStats(root: string): string | null {
|
|
175
|
+
const bufferPath = join(root, ".jfl", "training-buffer.jsonl")
|
|
176
|
+
if (!existsSync(bufferPath)) return null
|
|
177
|
+
try {
|
|
178
|
+
const lines = readFileSync(bufferPath, "utf-8").trim().split("\n").filter(Boolean).length
|
|
179
|
+
return `${lines} tuples`
|
|
180
|
+
} catch { return null }
|
|
181
|
+
}
|
|
80
182
|
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8")) as { name?: string; type?: string }
|
|
84
|
-
if (config.name) projectName = config.name
|
|
85
|
-
if (config.type) projectType = config.type
|
|
86
|
-
} catch {}
|
|
87
|
-
}
|
|
183
|
+
// ─── Build the dashboard ────────────────────────────────────────────────────
|
|
88
184
|
|
|
185
|
+
function buildHud(root: string): string {
|
|
186
|
+
const config = getConfig(root)
|
|
187
|
+
const branch = getBranch(root)
|
|
188
|
+
const phase = getPhase(root)
|
|
89
189
|
const days = getDaysToLaunch(root)
|
|
90
|
-
const
|
|
91
|
-
const
|
|
190
|
+
const health = checkSystemHealth(root)
|
|
191
|
+
const journalStats = getJournalStats(root)
|
|
192
|
+
const recentWork = getRecentJournal(root, 5)
|
|
193
|
+
const evalScore = getEvalSummary(root)
|
|
194
|
+
const trainingStats = getTrainingStats(root)
|
|
195
|
+
|
|
196
|
+
const lines: string[] = []
|
|
92
197
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
198
|
+
// Header
|
|
199
|
+
lines.push(`◆ ${config.name}`)
|
|
200
|
+
lines.push(` Branch: ${branch}`)
|
|
201
|
+
if (phase) lines.push(` Phase: ${phase}`)
|
|
202
|
+
if (days) lines.push(` ${days}`)
|
|
203
|
+
lines.push("")
|
|
99
204
|
|
|
100
|
-
|
|
205
|
+
// System health
|
|
206
|
+
const hubIcon = health.hub === "up" ? "✓" : "✗"
|
|
207
|
+
const acIcon = health.autoCommit ? "✓" : "–"
|
|
208
|
+
const wdIcon = health.watchdog ? "✓" : "–"
|
|
209
|
+
lines.push(` Systems`)
|
|
210
|
+
lines.push(` ${hubIcon} Hub (:${health.hubPort}) ${acIcon} Auto-commit ${wdIcon} Watchdog`)
|
|
211
|
+
if (evalScore) lines.push(` Eval: ${evalScore}`)
|
|
212
|
+
if (trainingStats) lines.push(` Training: ${trainingStats}`)
|
|
213
|
+
lines.push(` Journal: ${journalStats.entries} entries across ${journalStats.files} sessions`)
|
|
214
|
+
lines.push("")
|
|
215
|
+
|
|
216
|
+
// Recent work
|
|
217
|
+
if (recentWork.length > 0) {
|
|
218
|
+
lines.push(" Recent")
|
|
219
|
+
for (const entry of recentWork) {
|
|
220
|
+
const typeTag = `[${entry.type}]`.padEnd(12)
|
|
221
|
+
const title = entry.title.length > 70 ? entry.title.slice(0, 67) + "…" : entry.title
|
|
222
|
+
lines.push(` ${typeTag} ${title}`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return lines.join("\n")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Also export a one-line version for the steer message
|
|
230
|
+
function buildHudCompact(root: string): string {
|
|
231
|
+
const config = getConfig(root)
|
|
232
|
+
const health = checkSystemHealth(root)
|
|
233
|
+
const journalStats = getJournalStats(root)
|
|
234
|
+
const recentWork = getRecentJournal(root, 1)
|
|
235
|
+
const evalScore = getEvalSummary(root)
|
|
236
|
+
|
|
237
|
+
const parts = [`◆ ${config.name}`]
|
|
238
|
+
parts.push(`Hub ${health.hub === "up" ? "✓" : "✗"}`)
|
|
239
|
+
if (evalScore) parts.push(`Eval ${evalScore}`)
|
|
240
|
+
parts.push(`${journalStats.entries} journal entries`)
|
|
241
|
+
if (recentWork.length > 0) parts.push(`Last: ${recentWork[0].title.slice(0, 50)}`)
|
|
242
|
+
|
|
243
|
+
return parts.join(" | ")
|
|
101
244
|
}
|
|
102
245
|
|
|
246
|
+
// ─── Tool + Command registration ───────────────────────────────────────────
|
|
247
|
+
|
|
103
248
|
export function setupHudTool(ctx: PiContext): void {
|
|
104
249
|
projectRoot = ctx.session.projectRoot
|
|
105
250
|
|
|
@@ -112,7 +257,7 @@ export function setupHudTool(ctx: PiContext): void {
|
|
|
112
257
|
properties: {},
|
|
113
258
|
},
|
|
114
259
|
async handler() {
|
|
115
|
-
return
|
|
260
|
+
return buildHud(projectRoot)
|
|
116
261
|
},
|
|
117
262
|
renderCall: hudRenderCall,
|
|
118
263
|
renderResult: hudRenderResult,
|
|
@@ -121,13 +266,29 @@ export function setupHudTool(ctx: PiContext): void {
|
|
|
121
266
|
ctx.registerCommand({
|
|
122
267
|
name: "hud",
|
|
123
268
|
description: "Show project dashboard",
|
|
124
|
-
async handler(_args,
|
|
125
|
-
const
|
|
126
|
-
ctx.ui.notify(
|
|
269
|
+
async handler(_args, _cmdCtx) {
|
|
270
|
+
const output = buildHud(projectRoot)
|
|
271
|
+
ctx.ui.notify(output, { level: "info" })
|
|
127
272
|
},
|
|
128
273
|
})
|
|
129
274
|
}
|
|
130
275
|
|
|
276
|
+
/** Build HUD output for embedding in session init steer message */
|
|
277
|
+
export function getHudForSteer(): string {
|
|
278
|
+
if (!projectRoot) return ""
|
|
279
|
+
try {
|
|
280
|
+
return buildHud(projectRoot)
|
|
281
|
+
} catch { return "" }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Compact HUD for status bars / footers */
|
|
285
|
+
export function getHudCompact(): string {
|
|
286
|
+
if (!projectRoot) return ""
|
|
287
|
+
try {
|
|
288
|
+
return buildHudCompact(projectRoot)
|
|
289
|
+
} catch { return "" }
|
|
290
|
+
}
|
|
291
|
+
|
|
131
292
|
export async function updateHudWidget(_ctx: PiContext): Promise<void> {
|
|
132
293
|
// Widget rendering moved to subway extension
|
|
133
294
|
}
|
|
@@ -13,12 +13,12 @@ import { readFileSync, existsSync } from "fs"
|
|
|
13
13
|
import { join } from "path"
|
|
14
14
|
import { execSync } from "child_process"
|
|
15
15
|
import type { PiContext, JflConfig, JflToolDef, JflCommandDef, PiTheme } from "./types.js"
|
|
16
|
-
import { setupSession, onShutdown } from "./session.js"
|
|
16
|
+
import { setupSession, onShutdown, getSessionBranch } from "./session.js"
|
|
17
17
|
import { setupContext, injectContext } from "./context.js"
|
|
18
|
-
import { setupJournal, checkJournalBeforeCompact, onJournalAgentEnd, onToolExecutionEnd } from "./journal.js"
|
|
18
|
+
import { setupJournal, checkJournalBeforeCompact, onJournalAgentEnd, onToolExecutionEnd, checkPurposeHeader, hasJournalEntryForSession } from "./journal.js"
|
|
19
19
|
import { setupMapBridge, onMapBridgeShutdown, onMapToolEnd } from "./map-bridge.js"
|
|
20
20
|
import { setupEval, onAgentEnd as onEvalEnd } from "./eval.js"
|
|
21
|
-
import { setupHudTool, updateHudWidget } from "./hud-tool.js"
|
|
21
|
+
import { setupHudTool, updateHudWidget, getHudForSteer } from "./hud-tool.js"
|
|
22
22
|
import { setupCrmTool } from "./crm-tool.js"
|
|
23
23
|
import { setupMemoryTool } from "./memory-tool.js"
|
|
24
24
|
import { setupSynopsisTool } from "./synopsis-tool.js"
|
|
@@ -119,11 +119,13 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
119
119
|
return {
|
|
120
120
|
projectRoot: projectCwd,
|
|
121
121
|
id: pi.getSessionName?.() ?? "jfl",
|
|
122
|
-
branch: getCurrentBranch(projectCwd),
|
|
122
|
+
branch: getSessionBranch() || getCurrentBranch(projectCwd),
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
|
|
126
126
|
log: (msg: string, level?: string) => {
|
|
127
|
+
// Only show debug messages if JFL_DEBUG is set
|
|
128
|
+
if (level === "debug" && !process.env.JFL_DEBUG) return
|
|
127
129
|
const prefix = level === "debug" ? "[JFL debug]" : "[JFL]"
|
|
128
130
|
console.log(`${prefix} ${msg}`)
|
|
129
131
|
},
|
|
@@ -305,10 +307,15 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
305
307
|
|
|
306
308
|
pi.setSessionName(`JFL: ${projectName}`)
|
|
307
309
|
|
|
308
|
-
|
|
310
|
+
// ─── Hub first, then animation + tools in parallel ─────────────
|
|
311
|
+
// Hub must be up before onboarding probes fire.
|
|
312
|
+
// setupContext starts hub + registers jfl_context tool.
|
|
313
|
+
await setupContext(ctx, config)
|
|
314
|
+
|
|
315
|
+
// Now hub is up — run animation + remaining setup in parallel
|
|
316
|
+
const doSetup = async () => {
|
|
309
317
|
await setupMapBridge(ctx, config)
|
|
310
318
|
await setupSession(ctx, config)
|
|
311
|
-
await setupContext(ctx, config)
|
|
312
319
|
await setupJournal(ctx, config)
|
|
313
320
|
await setupEval(ctx, config)
|
|
314
321
|
|
|
@@ -339,37 +346,10 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
339
346
|
const onboard = config.pi?.disable_onboarding ? null : selectOnboarding(config)
|
|
340
347
|
await Promise.all([
|
|
341
348
|
onboard ? onboard(ctx, config).catch(() => {}) : Promise.resolve(),
|
|
342
|
-
|
|
349
|
+
doSetup(),
|
|
343
350
|
])
|
|
344
351
|
|
|
345
352
|
ctx.log(`JFL: ${projectName} — session ready`)
|
|
346
|
-
|
|
347
|
-
if (config.pi?.auto_start !== false && pi.sendMessage) {
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
pi.sendMessage({
|
|
350
|
-
customType: "jfl-session-init",
|
|
351
|
-
content: [
|
|
352
|
-
`JFL session ready: "${projectName}" on branch ${ctx.session.branch}.`,
|
|
353
|
-
"",
|
|
354
|
-
"Your system prompt already contains full project context: CLAUDE.md, recent journal entries, knowledge docs, and code headers.",
|
|
355
|
-
"Everything was loaded during the boot screen. You do NOT need to call any tools.",
|
|
356
|
-
"",
|
|
357
|
-
"DO NOT call jfl_context, jfl_hud, jfl_memory_search, or run any bash commands at startup.",
|
|
358
|
-
"DO NOT run session-sync.sh, jfl-doctor.sh, or read journal files.",
|
|
359
|
-
"All of that is already done and injected into your system prompt.",
|
|
360
|
-
"",
|
|
361
|
-
"Just greet the user naturally with a brief status (3-5 lines):",
|
|
362
|
-
"- What was worked on recently (from the journal entries in your system prompt)",
|
|
363
|
-
"- Current phase and any blockers",
|
|
364
|
-
"- A suggested next action",
|
|
365
|
-
"",
|
|
366
|
-
"No tool calls. No setup noise. Just talk.",
|
|
367
|
-
"Write journal entries as you work. Capture decisions immediately.",
|
|
368
|
-
].join("\n"),
|
|
369
|
-
display: false,
|
|
370
|
-
}, { triggerTurn: true, deliverAs: "steer" })
|
|
371
|
-
}, 500)
|
|
372
|
-
}
|
|
373
353
|
})
|
|
374
354
|
|
|
375
355
|
pi.on("session_shutdown", async (_event: unknown, piCtx: any) => {
|
|
@@ -384,35 +364,10 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
384
364
|
return checkJournalBeforeCompact(ctx)
|
|
385
365
|
})
|
|
386
366
|
|
|
387
|
-
pi.on("before_agent_start", async (event: any,
|
|
388
|
-
latestPiCtx = piCtx
|
|
367
|
+
pi.on("before_agent_start", async (event: any, _piCtx: any) => {
|
|
389
368
|
const result = await injectContext(ctx, event)
|
|
390
369
|
if (result?.systemPromptAddition) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// Strip Path B (Claude Code manual startup) from system prompt.
|
|
394
|
-
// We're running in Pi with the extension — Path B instructions are
|
|
395
|
-
// noise that can confuse the LLM into running manual startup commands.
|
|
396
|
-
const pathBStart = "### Path B: Claude Code / No Extension"
|
|
397
|
-
const pathBEnd = "### How to Tell Which Path You're On"
|
|
398
|
-
const startIdx = current.indexOf(pathBStart)
|
|
399
|
-
const endIdx = current.indexOf(pathBEnd)
|
|
400
|
-
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
401
|
-
current = current.slice(0, startIdx) + current.slice(endIdx)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Also strip the "How to Tell" section — it references Path B
|
|
405
|
-
const howToTell = "### How to Tell Which Path You're On"
|
|
406
|
-
const howToTellIdx = current.indexOf(howToTell)
|
|
407
|
-
if (howToTellIdx !== -1) {
|
|
408
|
-
// Find the next ### or ## heading after it
|
|
409
|
-
const afterHowToTell = current.slice(howToTellIdx + howToTell.length)
|
|
410
|
-
const nextHeading = afterHowToTell.search(/\n###? /)
|
|
411
|
-
if (nextHeading !== -1) {
|
|
412
|
-
current = current.slice(0, howToTellIdx) + afterHowToTell.slice(nextHeading)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
370
|
+
const current = (event.systemPrompt as string) ?? ""
|
|
416
371
|
return {
|
|
417
372
|
systemPrompt: current
|
|
418
373
|
? `${current}\n\n${result.systemPromptAddition}`
|
|
@@ -448,10 +403,31 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
448
403
|
jflEmit("turn:end", { ...event, turnCount })
|
|
449
404
|
})
|
|
450
405
|
|
|
406
|
+
// UserPromptSubmit equivalent — detect approval/done patterns in user input
|
|
407
|
+
pi.on("input", async (event: any, _piCtx: any) => {
|
|
408
|
+
const text = (event.text ?? "").trim()
|
|
409
|
+
if (!text) return { action: "continue" }
|
|
410
|
+
|
|
411
|
+
const approvalPatterns = /\b(done|approved|ship it|looks good|lgtm|merge it|go ahead|good to go)\b/i
|
|
412
|
+
if (approvalPatterns.test(text)) {
|
|
413
|
+
const branch = getCurrentBranch(ctx.session.projectRoot)
|
|
414
|
+
const hasJournal = hasJournalEntryForSession(ctx.session.projectRoot, branch)
|
|
415
|
+
if (!hasJournal) {
|
|
416
|
+
// Transform: append journal reminder to the user's message
|
|
417
|
+
return {
|
|
418
|
+
action: "transform",
|
|
419
|
+
text: `${event.text}\n\n[System: The user just approved/completed something. Write a journal entry NOW before proceeding — capture what was approved, decisions made, and next steps. Use the journal format from CLAUDE.md.]`,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { action: "continue" }
|
|
424
|
+
})
|
|
425
|
+
|
|
451
426
|
pi.on("tool_execution_end", async (event: any, piCtx: any) => {
|
|
452
427
|
latestPiCtx = piCtx
|
|
453
|
-
await onToolExecutionEnd(ctx, event)
|
|
454
|
-
|
|
428
|
+
await onToolExecutionEnd(ctx, event) // git commit → journal nudge
|
|
429
|
+
checkPurposeHeader(ctx, event) // Write/Edit → @purpose check
|
|
430
|
+
await onMapToolEnd(ctx, event) // hub event bus
|
|
455
431
|
})
|
|
456
432
|
|
|
457
433
|
pi.on("model_select", async (event: any, piCtx: any) => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve jfl-cli library modules at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Extensions can't use relative imports like ../../src/lib/foo.js because
|
|
5
|
+
* the path only works when running from the jfl-cli source tree. When Pi
|
|
6
|
+
* loads us as a package, we need to find jfl's actual install location.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. JFL_CLI_DIR env var (explicit override)
|
|
10
|
+
* 2. `which jfl` → follow symlink → dist/index.js → repo root
|
|
11
|
+
* 3. npm global: /opt/homebrew/lib/node_modules/jfl/
|
|
12
|
+
* 4. Fallback: ../../ (source tree — dev only)
|
|
13
|
+
*
|
|
14
|
+
* @purpose Resolve jfl-cli library paths for Pi extension imports
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readlinkSync, realpathSync } from "fs"
|
|
18
|
+
import { join, dirname, resolve } from "path"
|
|
19
|
+
import { execSync } from "child_process"
|
|
20
|
+
|
|
21
|
+
let jflRoot: string | null = null
|
|
22
|
+
|
|
23
|
+
function findJflRoot(): string {
|
|
24
|
+
if (jflRoot) return jflRoot
|
|
25
|
+
|
|
26
|
+
// 1. Explicit env
|
|
27
|
+
if (process.env.JFL_CLI_DIR) {
|
|
28
|
+
const dir = process.env.JFL_CLI_DIR
|
|
29
|
+
if (existsSync(join(dir, "dist", "lib"))) {
|
|
30
|
+
jflRoot = dir
|
|
31
|
+
return dir
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Follow `which jfl` symlink
|
|
36
|
+
try {
|
|
37
|
+
const jflBin = execSync("which jfl", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim()
|
|
38
|
+
if (jflBin) {
|
|
39
|
+
const realPath = realpathSync(jflBin)
|
|
40
|
+
// realPath is like /path/to/jfl-cli/dist/index.js
|
|
41
|
+
const distDir = dirname(realPath)
|
|
42
|
+
const root = dirname(distDir)
|
|
43
|
+
if (existsSync(join(root, "dist", "lib"))) {
|
|
44
|
+
jflRoot = root
|
|
45
|
+
return root
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
// 3. npm global locations
|
|
51
|
+
const npmGlobalPaths = [
|
|
52
|
+
"/opt/homebrew/lib/node_modules/jfl",
|
|
53
|
+
"/usr/local/lib/node_modules/jfl",
|
|
54
|
+
]
|
|
55
|
+
for (const p of npmGlobalPaths) {
|
|
56
|
+
if (existsSync(join(p, "dist", "lib"))) {
|
|
57
|
+
jflRoot = p
|
|
58
|
+
return p
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 4. Fallback: source tree (dev only)
|
|
63
|
+
const devRoot = resolve(dirname(new URL(import.meta.url).pathname), "..", "..")
|
|
64
|
+
if (existsSync(join(devRoot, "dist", "lib"))) {
|
|
65
|
+
jflRoot = devRoot
|
|
66
|
+
return devRoot
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Last resort
|
|
70
|
+
jflRoot = devRoot
|
|
71
|
+
return devRoot
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Import a jfl-cli library module by name.
|
|
76
|
+
*
|
|
77
|
+
* Usage:
|
|
78
|
+
* const { PolicyHeadInference } = await jflImport("policy-head")
|
|
79
|
+
* const { TrainingBuffer } = await jflImport("training-buffer")
|
|
80
|
+
* const { phoneHomeToPortfolio } = await jflImport("service-gtm")
|
|
81
|
+
*/
|
|
82
|
+
export async function jflImport(moduleName: string): Promise<any> {
|
|
83
|
+
const root = findJflRoot()
|
|
84
|
+
const modulePath = join(root, "dist", "lib", `${moduleName}.js`)
|
|
85
|
+
|
|
86
|
+
if (!existsSync(modulePath)) {
|
|
87
|
+
throw new Error(`jfl module not found: ${moduleName} (looked in ${modulePath})`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return import(modulePath)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the jfl-cli root directory.
|
|
95
|
+
*/
|
|
96
|
+
export function getJflRoot(): string {
|
|
97
|
+
return findJflRoot()
|
|
98
|
+
}
|