jfl 0.9.1 → 0.9.3

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 (126) hide show
  1. package/dist/commands/context-hub.d.ts.map +1 -1
  2. package/dist/commands/context-hub.js +141 -3
  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/init.d.ts.map +1 -1
  8. package/dist/commands/init.js +6 -0
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/linear.d.ts.map +1 -1
  11. package/dist/commands/linear.js +24 -0
  12. package/dist/commands/linear.js.map +1 -1
  13. package/dist/commands/peter.d.ts.map +1 -1
  14. package/dist/commands/peter.js +11 -15
  15. package/dist/commands/peter.js.map +1 -1
  16. package/dist/commands/pi.d.ts +3 -0
  17. package/dist/commands/pi.d.ts.map +1 -1
  18. package/dist/commands/pi.js +19 -0
  19. package/dist/commands/pi.js.map +1 -1
  20. package/dist/commands/pivot.d.ts.map +1 -1
  21. package/dist/commands/pivot.js +22 -25
  22. package/dist/commands/pivot.js.map +1 -1
  23. package/dist/commands/repair.d.ts.map +1 -1
  24. package/dist/commands/repair.js +26 -0
  25. package/dist/commands/repair.js.map +1 -1
  26. package/dist/commands/session.d.ts.map +1 -1
  27. package/dist/commands/session.js +39 -0
  28. package/dist/commands/session.js.map +1 -1
  29. package/dist/commands/start.d.ts.map +1 -1
  30. package/dist/commands/start.js +60 -0
  31. package/dist/commands/start.js.map +1 -1
  32. package/dist/commands/update.d.ts.map +1 -1
  33. package/dist/commands/update.js +3 -1
  34. package/dist/commands/update.js.map +1 -1
  35. package/dist/index.js +3 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/lib/advanced-setup.js +7 -7
  38. package/dist/lib/advanced-setup.js.map +1 -1
  39. package/dist/lib/agent-session.d.ts.map +1 -1
  40. package/dist/lib/agent-session.js +6 -3
  41. package/dist/lib/agent-session.js.map +1 -1
  42. package/dist/lib/discovery-agent.js +1 -1
  43. package/dist/lib/discovery-agent.js.map +1 -1
  44. package/dist/lib/gtm-generator.js +7 -0
  45. package/dist/lib/gtm-generator.js.map +1 -1
  46. package/dist/lib/linear-webhook.d.ts +50 -0
  47. package/dist/lib/linear-webhook.d.ts.map +1 -0
  48. package/dist/lib/linear-webhook.js +92 -0
  49. package/dist/lib/linear-webhook.js.map +1 -0
  50. package/dist/lib/memory-db.d.ts +8 -0
  51. package/dist/lib/memory-db.d.ts.map +1 -1
  52. package/dist/lib/memory-db.js +24 -0
  53. package/dist/lib/memory-db.js.map +1 -1
  54. package/dist/lib/memory-indexer.d.ts +8 -0
  55. package/dist/lib/memory-indexer.d.ts.map +1 -1
  56. package/dist/lib/memory-indexer.js +30 -1
  57. package/dist/lib/memory-indexer.js.map +1 -1
  58. package/dist/lib/memory-search.d.ts.map +1 -1
  59. package/dist/lib/memory-search.js +2 -7
  60. package/dist/lib/memory-search.js.map +1 -1
  61. package/dist/lib/onboarding.js +1 -1
  62. package/dist/lib/onboarding.js.map +1 -1
  63. package/dist/lib/rl-manager.d.ts +1 -1
  64. package/dist/lib/rl-manager.d.ts.map +1 -1
  65. package/dist/lib/rl-manager.js +3 -3
  66. package/dist/lib/rl-manager.js.map +1 -1
  67. package/dist/lib/service-detector.js +2 -2
  68. package/dist/lib/service-detector.js.map +1 -1
  69. package/dist/lib/telemetry/physical-world-collector.js +1 -1
  70. package/dist/lib/telemetry/physical-world-collector.js.map +1 -1
  71. package/dist/lib/tool-schemas.d.ts +35 -0
  72. package/dist/lib/tool-schemas.d.ts.map +1 -0
  73. package/dist/lib/tool-schemas.js +246 -0
  74. package/dist/lib/tool-schemas.js.map +1 -0
  75. package/dist/lib/workspace/data-pipeline.d.ts.map +1 -1
  76. package/dist/lib/workspace/data-pipeline.js +29 -20
  77. package/dist/lib/workspace/data-pipeline.js.map +1 -1
  78. package/dist/lib/workspace/engine.d.ts +1 -0
  79. package/dist/lib/workspace/engine.d.ts.map +1 -1
  80. package/dist/lib/workspace/engine.js +10 -0
  81. package/dist/lib/workspace/engine.js.map +1 -1
  82. package/dist/mcp/context-hub-mcp.js +7 -1
  83. package/dist/mcp/context-hub-mcp.js.map +1 -1
  84. package/dist/types/telemetry.d.ts +1 -0
  85. package/dist/types/telemetry.d.ts.map +1 -1
  86. package/dist/utils/git.d.ts +1 -1
  87. package/dist/utils/git.d.ts.map +1 -1
  88. package/dist/utils/git.js +9 -6
  89. package/dist/utils/git.js.map +1 -1
  90. package/dist/utils/provenance.d.ts +65 -0
  91. package/dist/utils/provenance.d.ts.map +1 -0
  92. package/dist/utils/provenance.js +213 -0
  93. package/dist/utils/provenance.js.map +1 -0
  94. package/package.json +1 -1
  95. package/packages/pi/assets/boot.mp3 +0 -0
  96. package/packages/pi/extensions/autoresearch.ts +3 -2
  97. package/packages/pi/extensions/context.ts +38 -114
  98. package/packages/pi/extensions/eval.ts +2 -1
  99. package/packages/pi/extensions/header.ts +171 -0
  100. package/packages/pi/extensions/hub-tools.ts +31 -11
  101. package/packages/pi/extensions/hud-tool.ts +231 -70
  102. package/packages/pi/extensions/index.ts +65 -64
  103. package/packages/pi/extensions/jfl-resolve.ts +98 -0
  104. package/packages/pi/extensions/journal.ts +91 -6
  105. package/packages/pi/extensions/map-bridge.ts +31 -0
  106. package/packages/pi/extensions/memory-tool.ts +3 -3
  107. package/packages/pi/extensions/onboarding-v2.ts +263 -410
  108. package/packages/pi/extensions/onboarding-v3.ts +32 -21
  109. package/packages/pi/extensions/peter-parker.ts +2 -1
  110. package/packages/pi/extensions/policy-head-tool.ts +3 -2
  111. package/packages/pi/extensions/portfolio-bridge.ts +3 -4
  112. package/packages/pi/extensions/service-skills.ts +6 -1
  113. package/packages/pi/extensions/session.ts +97 -15
  114. package/packages/pi/extensions/startup-briefing.ts +313 -0
  115. package/packages/pi/extensions/stratus-bridge.ts +2 -1
  116. package/packages/pi/extensions/subway-mesh.ts +893 -0
  117. package/packages/pi/extensions/synopsis-tool.ts +6 -1
  118. package/packages/pi/extensions/training-buffer-tool.ts +3 -2
  119. package/packages/pi/extensions/types.ts +3 -0
  120. package/packages/pi/package.json +4 -1
  121. package/packages/pi/skills/viz/SKILL.md +204 -0
  122. package/scripts/pp-branch-pr.sh +24 -6
  123. package/scripts/pp-branch-pr.sh.bak +115 -0
  124. package/template/.pi/settings.json +5 -0
  125. package/template/CLAUDE.md +82 -1738
  126. package/template/CLAUDE.md.bak +0 -1187
@@ -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
- // Widget rendering moved to subway extension
293
+ // Widget rendering handled by subway-mesh.ts
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"
@@ -38,6 +38,9 @@ import { setupFooter } from "./footer.js"
38
38
  import { setupShortcuts } from "./shortcuts.js"
39
39
  import { setupNotifications } from "./notifications.js"
40
40
  import { setupBookmarks } from "./bookmarks.js"
41
+ import { setupSubwayMesh, injectMeshContext, onMeshShutdown } from "./subway-mesh.js"
42
+ import { fireStartupBriefing } from "./startup-briefing.js"
43
+ import { setupHeader, setHeaderHubStatus, setHeaderAutoCommit, setHeaderBranch, refreshHeaderData } from "./header.js"
41
44
  import { setupOnboarding as setupOnboardingV1 } from "./onboarding-v1.js"
42
45
  import { setupOnboarding as setupOnboardingV2 } from "./onboarding-v2.js"
43
46
  import { setupOnboarding as setupOnboardingV3 } from "./onboarding-v3.js"
@@ -119,11 +122,13 @@ export default async function jflExtension(pi: any): Promise<void> {
119
122
  return {
120
123
  projectRoot: projectCwd,
121
124
  id: pi.getSessionName?.() ?? "jfl",
122
- branch: getCurrentBranch(projectCwd),
125
+ branch: getSessionBranch() || getCurrentBranch(projectCwd),
123
126
  }
124
127
  },
125
128
 
126
129
  log: (msg: string, level?: string) => {
130
+ // Only show debug messages if JFL_DEBUG is set
131
+ if (level === "debug" && !process.env.JFL_DEBUG) return
127
132
  const prefix = level === "debug" ? "[JFL debug]" : "[JFL]"
128
133
  console.log(`${prefix} ${msg}`)
129
134
  },
@@ -233,6 +238,10 @@ export default async function jflExtension(pi: any): Promise<void> {
233
238
  if (latestPiCtx?.ui?.setFooter) latestPiCtx.ui.setFooter(factory)
234
239
  },
235
240
 
241
+ setHeader: (factory: any) => {
242
+ if (latestPiCtx?.ui?.setHeader) latestPiCtx.ui.setHeader(factory)
243
+ },
244
+
236
245
  setEditorText: (text: string) => {
237
246
  if (latestPiCtx?.ui?.setEditorText) latestPiCtx.ui.setEditorText(text)
238
247
  },
@@ -305,10 +314,18 @@ export default async function jflExtension(pi: any): Promise<void> {
305
314
 
306
315
  pi.setSessionName(`JFL: ${projectName}`)
307
316
 
308
- const setupWork = async () => {
317
+ // ─── Header first replace Pi's default before anything renders ──
318
+ setupHeader(ctx, config)
319
+
320
+ // ─── Hub first, then animation + tools in parallel ─────────────
321
+ // Hub must be up before onboarding probes fire.
322
+ // setupContext starts hub + registers jfl_context tool.
323
+ await setupContext(ctx, config)
324
+
325
+ // Now hub is up — run animation + remaining setup in parallel
326
+ const doSetup = async () => {
309
327
  await setupMapBridge(ctx, config)
310
328
  await setupSession(ctx, config)
311
- await setupContext(ctx, config)
312
329
  await setupJournal(ctx, config)
313
330
  await setupEval(ctx, config)
314
331
 
@@ -324,6 +341,8 @@ export default async function jflExtension(pi: any): Promise<void> {
324
341
  await setupServiceSkills(ctx, config)
325
342
  await setupHubTools(ctx, config)
326
343
 
344
+ await setupSubwayMesh(ctx, config)
345
+
327
346
  initStratusBridge(projectCwd)
328
347
  initAgentNames(projectCwd)
329
348
  await setupPeterParker(ctx, config)
@@ -339,41 +358,21 @@ export default async function jflExtension(pi: any): Promise<void> {
339
358
  const onboard = config.pi?.disable_onboarding ? null : selectOnboarding(config)
340
359
  await Promise.all([
341
360
  onboard ? onboard(ctx, config).catch(() => {}) : Promise.resolve(),
342
- setupWork(),
361
+ doSetup(),
343
362
  ])
344
363
 
345
364
  ctx.log(`JFL: ${projectName} — session ready`)
346
365
 
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)
366
+ // Fire startup briefing gathers synopsis, PRs, team activity, next actions
367
+ // and steers the model to produce a concise "here's where things stand" greeting
368
+ if (config.pi?.auto_start !== false) {
369
+ await fireStartupBriefing(ctx, config)
372
370
  }
373
371
  })
374
372
 
375
373
  pi.on("session_shutdown", async (_event: unknown, piCtx: any) => {
376
374
  latestPiCtx = piCtx
375
+ onMeshShutdown()
377
376
  await onPortfolioShutdown(ctx)
378
377
  await onShutdown(ctx)
379
378
  await onMapBridgeShutdown(ctx)
@@ -384,39 +383,20 @@ export default async function jflExtension(pi: any): Promise<void> {
384
383
  return checkJournalBeforeCompact(ctx)
385
384
  })
386
385
 
387
- pi.on("before_agent_start", async (event: any, piCtx: any) => {
388
- latestPiCtx = piCtx
386
+ pi.on("before_agent_start", async (event: any, _piCtx: any) => {
389
387
  const result = await injectContext(ctx, event)
390
- 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
-
388
+ const meshState = injectMeshContext()
389
+ const additions = [
390
+ result?.systemPromptAddition,
391
+ meshState,
392
+ ].filter(Boolean).join("\n\n")
393
+
394
+ if (additions) {
395
+ const current = (event.systemPrompt as string) ?? ""
416
396
  return {
417
397
  systemPrompt: current
418
- ? `${current}\n\n${result.systemPromptAddition}`
419
- : result.systemPromptAddition,
398
+ ? `${current}\n\n${additions}`
399
+ : additions,
420
400
  }
421
401
  }
422
402
  })
@@ -448,10 +428,31 @@ export default async function jflExtension(pi: any): Promise<void> {
448
428
  jflEmit("turn:end", { ...event, turnCount })
449
429
  })
450
430
 
431
+ // UserPromptSubmit equivalent — detect approval/done patterns in user input
432
+ pi.on("input", async (event: any, _piCtx: any) => {
433
+ const text = (event.text ?? "").trim()
434
+ if (!text) return { action: "continue" }
435
+
436
+ const approvalPatterns = /\b(done|approved|ship it|looks good|lgtm|merge it|go ahead|good to go)\b/i
437
+ if (approvalPatterns.test(text)) {
438
+ const branch = getCurrentBranch(ctx.session.projectRoot)
439
+ const hasJournal = hasJournalEntryForSession(ctx.session.projectRoot, branch)
440
+ if (!hasJournal) {
441
+ // Transform: append journal reminder to the user's message
442
+ return {
443
+ action: "transform",
444
+ 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.]`,
445
+ }
446
+ }
447
+ }
448
+ return { action: "continue" }
449
+ })
450
+
451
451
  pi.on("tool_execution_end", async (event: any, piCtx: any) => {
452
452
  latestPiCtx = piCtx
453
- await onToolExecutionEnd(ctx, event)
454
- await onMapToolEnd(ctx, event)
453
+ await onToolExecutionEnd(ctx, event) // git commit → journal nudge
454
+ checkPurposeHeader(ctx, event) // Write/Edit → @purpose check
455
+ await onMapToolEnd(ctx, event) // hub event bus
455
456
  })
456
457
 
457
458
  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
+ }