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
@@ -1,105 +1,250 @@
1
1
  /**
2
2
  * HUD Tool Extension
3
3
  *
4
- * Registers jfl_hud tool and /hud command with custom TUI rendering.
5
- * Shows a themed, collapsible dashboard in the tool output.
6
- * Updates the aboveEditor widget after each agent turn.
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 + themed widget + custom rendering
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
- function clipLine(text: string, maxW: number): string {
18
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, "")
19
- if (stripped.length <= maxW) return text
20
- return text.slice(0, maxW - 1) + "…"
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
- let projectRoot = ""
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
- const content = readFileSync(roadmapPath, "utf-8")
30
- const dateMatch = content.match(/(\d{4}-\d{2}-\d{2})/m)
31
- if (!dateMatch) return null
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
- const launchDate = new Date(dateMatch[1])
34
- const now = new Date()
35
- const diff = Math.ceil((launchDate.getTime() - now.getTime()) / 86400000)
36
- return diff > 0 ? `${diff}d to launch` : `${Math.abs(diff)}d past launch`
64
+ interface JournalEntry {
65
+ title: string
66
+ type: string
67
+ ts: string
68
+ status?: string
37
69
  }
38
70
 
39
- function getProjectPhase(root: string): string {
40
- const roadmapPath = join(root, "knowledge", "ROADMAP.md")
41
- if (!existsSync(roadmapPath)) return "unknown"
71
+ function getRecentJournal(root: string, count: number = 5): JournalEntry[] {
72
+ const journalDir = join(root, ".jfl", "journal")
73
+ if (!existsSync(journalDir)) return []
42
74
 
43
- const content = readFileSync(roadmapPath, "utf-8")
44
- const sameLineMatch = content.match(/## (?:Phase|Current Phase)[:\t ]*([^\n]+)/i)
45
- if (sameLineMatch && sameLineMatch[1].trim() && !sameLineMatch[1].trim().startsWith("```")) {
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
- const sectionMatch = content.match(/## (?:Phase|Current Phase)[^\n]*\n+([^\n]+)/i)
50
- if (sectionMatch) {
51
- let line = sectionMatch[1].trim()
52
- if (line.startsWith("```")) return "unknown"
53
- line = line.replace(/^[`*_[\]]+|[`*_[\]]+$/g, "")
54
- return line || "unknown"
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
- return "unknown"
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 getLastJournalTitle(root: string): string | null {
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 journalDir = join(root, ".jfl", "journal")
63
- if (!existsSync(journalDir)) return null
64
- const files = execSync(`ls -t "${journalDir}"/*.jsonl 2>/dev/null | head -1`, {
65
- cwd: root, timeout: 3000, encoding: "utf-8",
66
- }).trim()
67
- if (!files) return null
68
- const content = readFileSync(files, "utf-8").trim()
69
- const lastLine = content.split("\n").pop()
70
- if (!lastLine) return null
71
- const entry = JSON.parse(lastLine)
72
- return entry.title ?? null
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 buildHudLines(root: string): string[] {
77
- const configPath = join(root, ".jfl", "config.json")
78
- let projectName = root.split("/").pop() ?? "JFL"
79
- let projectType = "gtm"
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
- if (existsSync(configPath)) {
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 phase = getProjectPhase(root)
91
- const lastWork = getLastJournalTitle(root)
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
- const lines = [
94
- `◆ ${projectName}`,
95
- phase !== "unknown" ? ` ${phase}` : "",
96
- days ? ` ${days}` : "",
97
- lastWork ? ` Last: ${lastWork.slice(0, 60)}` : "",
98
- ].filter(Boolean)
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
- return lines
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 buildHudLines(projectRoot).join("\n")
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, ctx) {
125
- const lines = buildHudLines(projectRoot)
126
- ctx.ui.notify(lines.join("\n"), { level: "info" })
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
- const setupWork = async () => {
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
- setupWork(),
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, piCtx: 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
- let current = piCtx.getSystemPrompt?.() ?? ""
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
- await onMapToolEnd(ctx, event)
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
+ }