jfl 0.9.2 → 0.9.4

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 (71) hide show
  1. package/dist/commands/context-hub.d.ts.map +1 -1
  2. package/dist/commands/context-hub.js +23 -1
  3. package/dist/commands/context-hub.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +6 -0
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/peter.d.ts.map +1 -1
  8. package/dist/commands/peter.js +11 -15
  9. package/dist/commands/peter.js.map +1 -1
  10. package/dist/commands/pivot.d.ts.map +1 -1
  11. package/dist/commands/pivot.js +22 -25
  12. package/dist/commands/pivot.js.map +1 -1
  13. package/dist/commands/repair.d.ts.map +1 -1
  14. package/dist/commands/repair.js +26 -0
  15. package/dist/commands/repair.js.map +1 -1
  16. package/dist/commands/session.d.ts.map +1 -1
  17. package/dist/commands/session.js +39 -0
  18. package/dist/commands/session.js.map +1 -1
  19. package/dist/commands/start.d.ts.map +1 -1
  20. package/dist/commands/start.js +60 -0
  21. package/dist/commands/start.js.map +1 -1
  22. package/dist/commands/update.d.ts.map +1 -1
  23. package/dist/commands/update.js +3 -1
  24. package/dist/commands/update.js.map +1 -1
  25. package/dist/lib/agent-session.d.ts.map +1 -1
  26. package/dist/lib/agent-session.js +6 -3
  27. package/dist/lib/agent-session.js.map +1 -1
  28. package/dist/lib/gtm-generator.js +7 -0
  29. package/dist/lib/gtm-generator.js.map +1 -1
  30. package/dist/lib/memory-db.d.ts +8 -0
  31. package/dist/lib/memory-db.d.ts.map +1 -1
  32. package/dist/lib/memory-db.js +24 -0
  33. package/dist/lib/memory-db.js.map +1 -1
  34. package/dist/lib/memory-indexer.d.ts +8 -0
  35. package/dist/lib/memory-indexer.d.ts.map +1 -1
  36. package/dist/lib/memory-indexer.js +30 -1
  37. package/dist/lib/memory-indexer.js.map +1 -1
  38. package/dist/lib/memory-search.d.ts.map +1 -1
  39. package/dist/lib/memory-search.js +2 -7
  40. package/dist/lib/memory-search.js.map +1 -1
  41. package/dist/lib/service-detector.js +2 -2
  42. package/dist/lib/service-detector.js.map +1 -1
  43. package/dist/lib/telemetry/physical-world-collector.js +1 -1
  44. package/dist/lib/telemetry/physical-world-collector.js.map +1 -1
  45. package/dist/utils/git.d.ts +1 -1
  46. package/dist/utils/git.d.ts.map +1 -1
  47. package/dist/utils/git.js +9 -6
  48. package/dist/utils/git.js.map +1 -1
  49. package/dist/utils/provenance.d.ts +65 -0
  50. package/dist/utils/provenance.d.ts.map +1 -0
  51. package/dist/utils/provenance.js +213 -0
  52. package/dist/utils/provenance.js.map +1 -0
  53. package/package.json +1 -1
  54. package/packages/pi/extensions/context.ts +11 -0
  55. package/packages/pi/extensions/header.ts +171 -0
  56. package/packages/pi/extensions/hud-tool.ts +1 -1
  57. package/packages/pi/extensions/index.ts +43 -3
  58. package/packages/pi/extensions/memory-tool.ts +3 -3
  59. package/packages/pi/extensions/onboarding-v2.ts +70 -185
  60. package/packages/pi/extensions/onboarding-v3.ts +32 -21
  61. package/packages/pi/extensions/service-skills.ts +6 -1
  62. package/packages/pi/extensions/session.ts +7 -1
  63. package/packages/pi/extensions/startup-briefing.ts +313 -0
  64. package/packages/pi/extensions/subway-mesh.ts +893 -0
  65. package/packages/pi/extensions/types.ts +1 -0
  66. package/packages/pi/package.json +1 -0
  67. package/scripts/pp-branch-pr.sh +24 -6
  68. package/scripts/pp-branch-pr.sh.bak +115 -0
  69. package/template/.pi/settings.json +2 -0
  70. package/template/CLAUDE.md +82 -1738
  71. package/template/CLAUDE.md.bak +0 -1187
@@ -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"
@@ -82,9 +85,24 @@ function getCurrentBranch(root: string): string {
82
85
  }
83
86
  }
84
87
 
88
+ // ─── Dedup guard ─────────────────────────────────────────────────────────────
89
+ // When developing jfl-cli locally, Pi discovers the extension from BOTH the
90
+ // local packages/pi/ AND the global npm-installed jfl package. Both run as
91
+ // separate factory calls in the same process. This guard ensures only the
92
+ // first one initializes — which is the local dev version (Pi loads project-
93
+ // local extensions before global packages).
94
+
95
+ const JFL_LOADED = Symbol.for("jfl-pi-extension-loaded")
96
+
85
97
  // ─── Pi extension factory function ───────────────────────────────────────────
86
98
 
87
99
  export default async function jflExtension(pi: any): Promise<void> {
100
+ // Skip if already loaded from another source (dedup)
101
+ if ((globalThis as any)[JFL_LOADED]) {
102
+ return
103
+ }
104
+ (globalThis as any)[JFL_LOADED] = true
105
+
88
106
  let projectCwd = process.cwd()
89
107
  let latestPiCtx: any = null
90
108
 
@@ -235,6 +253,10 @@ export default async function jflExtension(pi: any): Promise<void> {
235
253
  if (latestPiCtx?.ui?.setFooter) latestPiCtx.ui.setFooter(factory)
236
254
  },
237
255
 
256
+ setHeader: (factory: any) => {
257
+ if (latestPiCtx?.ui?.setHeader) latestPiCtx.ui.setHeader(factory)
258
+ },
259
+
238
260
  setEditorText: (text: string) => {
239
261
  if (latestPiCtx?.ui?.setEditorText) latestPiCtx.ui.setEditorText(text)
240
262
  },
@@ -307,6 +329,9 @@ export default async function jflExtension(pi: any): Promise<void> {
307
329
 
308
330
  pi.setSessionName(`JFL: ${projectName}`)
309
331
 
332
+ // ─── Header first — replace Pi's default before anything renders ──
333
+ setupHeader(ctx, config)
334
+
310
335
  // ─── Hub first, then animation + tools in parallel ─────────────
311
336
  // Hub must be up before onboarding probes fire.
312
337
  // setupContext starts hub + registers jfl_context tool.
@@ -331,6 +356,8 @@ export default async function jflExtension(pi: any): Promise<void> {
331
356
  await setupServiceSkills(ctx, config)
332
357
  await setupHubTools(ctx, config)
333
358
 
359
+ await setupSubwayMesh(ctx, config)
360
+
334
361
  initStratusBridge(projectCwd)
335
362
  initAgentNames(projectCwd)
336
363
  await setupPeterParker(ctx, config)
@@ -350,10 +377,17 @@ export default async function jflExtension(pi: any): Promise<void> {
350
377
  ])
351
378
 
352
379
  ctx.log(`JFL: ${projectName} — session ready`)
380
+
381
+ // Fire startup briefing — gathers synopsis, PRs, team activity, next actions
382
+ // and steers the model to produce a concise "here's where things stand" greeting
383
+ if (config.pi?.auto_start !== false) {
384
+ await fireStartupBriefing(ctx, config)
385
+ }
353
386
  })
354
387
 
355
388
  pi.on("session_shutdown", async (_event: unknown, piCtx: any) => {
356
389
  latestPiCtx = piCtx
390
+ onMeshShutdown()
357
391
  await onPortfolioShutdown(ctx)
358
392
  await onShutdown(ctx)
359
393
  await onMapBridgeShutdown(ctx)
@@ -366,12 +400,18 @@ export default async function jflExtension(pi: any): Promise<void> {
366
400
 
367
401
  pi.on("before_agent_start", async (event: any, _piCtx: any) => {
368
402
  const result = await injectContext(ctx, event)
369
- if (result?.systemPromptAddition) {
403
+ const meshState = injectMeshContext()
404
+ const additions = [
405
+ result?.systemPromptAddition,
406
+ meshState,
407
+ ].filter(Boolean).join("\n\n")
408
+
409
+ if (additions) {
370
410
  const current = (event.systemPrompt as string) ?? ""
371
411
  return {
372
412
  systemPrompt: current
373
- ? `${current}\n\n${result.systemPromptAddition}`
374
- : result.systemPromptAddition,
413
+ ? `${current}\n\n${additions}`
414
+ : additions,
375
415
  }
376
416
  }
377
417
  })
@@ -110,9 +110,9 @@ export function setupMemoryTool(ctx: PiContext): void {
110
110
  })
111
111
 
112
112
  if (!resp.ok) return "Failed to add memory — hub returned error."
113
- const data = await resp.json() as { ok?: boolean; id?: string }
114
- return data.ok
115
- ? `Memory added: "${title}" (${type ?? "note"})`
113
+ const data = await resp.json() as { ok?: boolean; id?: string | number }
114
+ return (data.ok || data.id)
115
+ ? `Memory added: "${title}" (${type ?? "note"})${data.id ? ` [id: ${data.id}]` : ""}`
116
116
  : "Memory add returned unexpected response."
117
117
  } catch {
118
118
  return "Memory add unavailable — Context Hub may not be running."
@@ -1,17 +1,24 @@
1
1
  /**
2
- * Onboarding V2 — Full-screen cinematic startup
2
+ * Onboarding V2 — Cinematic boot sequence
3
3
  *
4
- * Owns the entire terminal. Covers Pi's startup noise with full-width blanking.
5
- * Flow: TENET square system probes mission brief ready
6
- * The overlay IS the greeting no model auto-trigger needed.
4
+ * Visual-only startup overlay. Shows the SATOR square animation and
5
+ * live system probes, then dismisses. No project data that comes
6
+ * from the startup briefing steer which fires after this overlay closes.
7
7
  *
8
- * @purpose Full-screen startup overlay with live probes and mission briefing
8
+ * Flow: SATOR square system probes project name → dismiss
9
+ *
10
+ * The header (header.ts) owns persistent identity. This overlay owns
11
+ * the first-impression cinematic moment. The startup briefing (steer)
12
+ * owns the data-rich greeting that follows.
13
+ *
14
+ * @purpose Cinematic boot overlay — SATOR square + system health probes
9
15
  */
10
16
 
11
17
  import { existsSync, readFileSync, readdirSync } from "fs"
12
18
  import { join } from "path"
13
19
  import { execSync } from "child_process"
14
20
  import type { PiContext, PiTheme, JflConfig } from "./types.js"
21
+ import { setHeaderHubStatus, setHeaderAutoCommit, refreshHeaderData } from "./header.js"
15
22
 
16
23
  // ─── Colors ──────────────────────────────────────────────────────────────────
17
24
 
@@ -40,25 +47,6 @@ interface Sys {
40
47
  detail: string
41
48
  }
42
49
 
43
- interface Brief {
44
- label: string
45
- value: string
46
- color: "gold" | "warm" | "dim" | "green" | "red"
47
- }
48
-
49
- interface Mission {
50
- name: string
51
- branch: string
52
- recentWork: string[]
53
- issuesActive: string[]
54
- issuesBacklog: string[]
55
- policyHead: string
56
- training: string
57
- journals: number
58
- sessions: number
59
- warnings: string[]
60
- }
61
-
62
50
  // ─── Probes ──────────────────────────────────────────────────────────────────
63
51
 
64
52
  async function probeHub(root: string): Promise<{ok: boolean; detail: string}> {
@@ -109,7 +97,7 @@ async function probeAutoCommit(): Promise<{ok: boolean; detail: string}> {
109
97
  } catch {}
110
98
  if (i < 3) await sleep(1000)
111
99
  }
112
- return {ok:false, detail:"starting…"}
100
+ return {ok:false, detail:""}
113
101
  }
114
102
 
115
103
  async function probeEval(root: string): Promise<{ok: boolean; detail: string}> {
@@ -142,89 +130,14 @@ function readToken(root: string): string {
142
130
  }
143
131
  function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
144
132
 
145
- // ─── Mission data ────────────────────────────────────────────────────────────
146
-
147
- function gather(root: string, config: JflConfig): Mission {
148
- const name = config.name ?? root.split("/").pop() ?? "project"
149
-
150
- let branch = "main"
151
- try { branch = execSync("git branch --show-current", {cwd:root, stdio:["pipe","pipe","ignore"]}).toString().trim() || "main" } catch {}
152
-
153
- const recentWork: string[] = []
154
- let journals = 0, sessions = 0
155
- const jDir = join(root, ".jfl", "journal")
156
- if (existsSync(jDir)) {
157
- const files = readdirSync(jDir).filter(f => f.endsWith(".jsonl"))
158
- sessions = files.length
159
- const all: Array<{ts:string,title:string}> = []
160
- for (const f of files) {
161
- try {
162
- const lines = readFileSync(join(jDir, f), "utf-8").trim().split("\n").filter(Boolean)
163
- journals += lines.length
164
- for (const l of lines) { try { const e = JSON.parse(l); if (e.title) all.push({ts:e.ts||"",title:e.title}) } catch {} }
165
- } catch {}
166
- }
167
- all.sort((a,b) => b.ts.localeCompare(a.ts))
168
- for (const e of all.slice(0,4)) recentWork.push(e.title.length > 55 ? e.title.slice(0,52)+"…" : e.title)
169
- }
170
-
171
- const issuesActive: string[] = [], issuesBacklog: string[] = []
172
- try {
173
- const r = execSync('gh issue list --label jfl/in-progress --limit 3 --json number,title 2>/dev/null', {cwd:root, timeout:5000, encoding:"utf-8", stdio:["pipe","pipe","ignore"]})
174
- for (const i of JSON.parse(r||"[]") as any[]) issuesActive.push(`#${i.number} ${i.title}`)
175
- } catch {}
176
- try {
177
- const r = execSync('gh issue list --label jfl/backlog --limit 3 --json number,title 2>/dev/null', {cwd:root, timeout:5000, encoding:"utf-8", stdio:["pipe","pipe","ignore"]})
178
- for (const i of JSON.parse(r||"[]") as any[]) issuesBacklog.push(`#${i.number} ${i.title}`)
179
- } catch {}
180
-
181
- let policyHead = ""
182
- try {
183
- const p = join(root, ".jfl", "checkpoints", "policy-head-v2.json")
184
- if (existsSync(p)) { const d = JSON.parse(readFileSync(p,"utf-8")); policyHead = `${((d.val_accuracy||0)*100).toFixed(1)}% · ${d.training_examples||"?"} examples` }
185
- } catch {}
186
-
187
- let training = ""
188
- try {
189
- const p = join(root, ".jfl", "training-buffer.jsonl")
190
- if (existsSync(p)) training = `${readFileSync(p,"utf-8").trim().split("\n").filter(Boolean).length} tuples`
191
- } catch {}
192
-
193
- const warnings: string[] = []
194
- try {
195
- const b = execSync("git branch", {cwd:root, timeout:3000, encoding:"utf-8", stdio:["pipe","pipe","ignore"]})
196
- const unmerged = b.split("\n").filter(l => l.trim().startsWith("session-")).length
197
- if (unmerged > 0) warnings.push(`${unmerged} unmerged branches`)
198
- } catch {}
199
- try {
200
- if (execSync("git status --porcelain", {cwd:root, timeout:3000, encoding:"utf-8", stdio:["pipe","pipe","ignore"]}).trim())
201
- warnings.push("uncommitted changes")
202
- } catch {}
203
-
204
- return { name, branch, recentWork, issuesActive, issuesBacklog, policyHead, training, journals, sessions, warnings }
205
- }
206
-
207
- /** Structured briefing for steer/context injection */
208
- export function buildStartupBriefing(mission: Mission): string {
209
- const l: string[] = ["## Session Briefing",""]
210
- if (mission.issuesActive.length) { l.push("IN PROGRESS:"); mission.issuesActive.forEach(i => l.push(` ${i}`)); l.push("") }
211
- if (mission.issuesBacklog.length) { l.push("BACKLOG:"); mission.issuesBacklog.forEach(i => l.push(` ${i}`)); l.push("") }
212
- if (mission.policyHead) l.push(`PolicyHead: ${mission.policyHead}`)
213
- if (mission.training) l.push(`Training: ${mission.training}`)
214
- if (mission.journals) l.push(`Journal: ${mission.journals} entries · ${mission.sessions} sessions`)
215
- l.push("")
216
- if (mission.recentWork.length) { l.push("RECENT:"); mission.recentWork.forEach(w => l.push(` - ${w}`)); l.push("") }
217
- if (mission.warnings.length) { l.push("WARNINGS:"); mission.warnings.forEach(w => l.push(` ⚠ ${w}`)) }
218
- return l.join("\n")
219
- }
220
-
221
133
  // ─── Main ────────────────────────────────────────────────────────────────────
222
134
 
223
135
  export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promise<void> {
224
136
  const root = ctx.session.projectRoot
225
137
  if (!ctx.ui.hasUI) return
226
138
 
227
- const mission = gather(root, config)
139
+ const projectName = config.name ?? root.split("/").pop() ?? "TENET"
140
+ const branch = ctx.session.branch
228
141
 
229
142
  const systems: Sys[] = [
230
143
  {name:"Hub", status:"wait", detail:""},
@@ -244,35 +157,22 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
244
157
  () => probeTraining(root),
245
158
  ]
246
159
 
247
- // Build brief lines
248
- const brief: Brief[] = []
249
- if (mission.policyHead) brief.push({label:"PolicyHead", value:mission.policyHead, color:"gold"})
250
- if (mission.training) brief.push({label:"Training", value:mission.training, color:"gold"})
251
- if (mission.journals) brief.push({label:"Journal", value:`${mission.journals} entries · ${mission.sessions} sessions`, color:"dim"})
252
- for (const i of mission.issuesActive.slice(0,2)) brief.push({label:"▸ Active", value:i, color:"green"})
253
- for (const i of mission.issuesBacklog.slice(0,2)) brief.push({label:" Backlog", value:i, color:"dim"})
254
- for (const w of mission.recentWork.slice(0,3)) brief.push({label:"", value:w, color:"warm"})
255
- for (const w of mission.warnings) brief.push({label:"⚠", value:w, color:"red"})
256
-
257
160
  await ctx.ui.custom<void>((tui: any, theme: PiTheme, _kb: any, done: (r: void) => void) => {
258
- let phase: "square"|"glow"|"systems"|"brief"|"ready" = "square"
161
+ let phase: "square"|"glow"|"systems"|"ready" = "square"
259
162
  let sqProg = 0
260
163
  let glow = false
261
164
  let sysRevealed = 0
262
- let briefRevealed = 0
263
165
  let readyTick = 0
264
166
  let timer: ReturnType<typeof setTimeout>|null = null
265
167
  let dead = false
266
168
 
267
- // Timing
268
- const SQ_MS = 50 // per letter
269
- const GLOW_MS = 700 // hold after square
270
- const GLW2SYS = 500 // glow → systems
271
- const SYS_MS = 250 // per system row
272
- const SETTLE = 200 // probe settle poll
273
- const BRF_MS = 100 // per brief line
274
- const BRF2RDY = 400 // brief → ready
275
- const RDY_HOLD = 2000 // show ready then dismiss
169
+ // ── Timing ──
170
+ const SQ_MS = 40 // per letter (faster)
171
+ const GLOW_MS = 500 // hold after square
172
+ const GLW2SYS = 300 // glow → systems
173
+ const SYS_MS = 180 // per system row reveal
174
+ const SETTLE = 150 // probe settle poll
175
+ const RDY_HOLD = 1200 // show ready then auto-dismiss
276
176
 
277
177
  function tick(fn: () => void, ms: number) {
278
178
  if (dead) return
@@ -287,12 +187,12 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
287
187
  const rd = (s: string) => theme.fg(RED, s)
288
188
 
289
189
  // ── ANSI helpers ──
290
- function strip(s: string) { return s.replace(/\x1b\[[0-9;]*m/g, "") }
190
+ function stripLen(s: string) { return s.replace(/\x1b\[[0-9;]*m/g, "").length }
291
191
  function pad(line: string, width: number) {
292
- return line + " ".repeat(Math.max(0, width - strip(line).length))
192
+ return line + " ".repeat(Math.max(0, width - stripLen(line)))
293
193
  }
294
194
  function center(text: string, width: number) {
295
- const vis = strip(text).length
195
+ const vis = stripLen(text)
296
196
  const left = Math.max(0, Math.floor((width - vis) / 2))
297
197
  return " ".repeat(left) + text
298
198
  }
@@ -306,32 +206,37 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
306
206
  // ── Phase: Systems ──
307
207
  function doSystems() {
308
208
  phase = "systems"
309
- // Fire all probes
209
+ // Fire all probes in parallel
310
210
  systems.forEach((s, i) => {
311
211
  s.status = "probe"
312
- probes[i]().then(r => { s.status = r.ok ? "ok" : "err"; s.detail = r.detail; tui.requestRender() })
313
- .catch(() => { s.status = "err"; s.detail = "timeout"; tui.requestRender() })
212
+ probes[i]().then(r => {
213
+ s.status = r.ok ? "ok" : "err"
214
+ s.detail = r.detail
215
+ // Feed results back to the persistent header
216
+ if (i === 0) setHeaderHubStatus(r.ok) // Hub
217
+ if (i === 3) setHeaderAutoCommit(r.ok) // Auto-commit
218
+ tui.requestRender()
219
+ }).catch(() => {
220
+ s.status = "err"
221
+ s.detail = "timeout"
222
+ tui.requestRender()
223
+ })
314
224
  })
315
225
  revealSys()
316
226
  }
227
+
317
228
  function revealSys() {
318
229
  if (sysRevealed < systems.length) { sysRevealed++; tick(revealSys, SYS_MS) }
319
230
  else waitProbes()
320
231
  }
232
+
321
233
  function waitProbes() {
322
234
  if (systems.some(s => s.status === "probe")) tick(waitProbes, SETTLE)
323
- else tick(doBrief, 300)
324
- }
325
-
326
- // ── Phase: Brief ──
327
- function doBrief() {
328
- phase = "brief"
329
- briefRevealed = 0
330
- tickBrief()
331
- }
332
- function tickBrief() {
333
- if (briefRevealed < brief.length) { briefRevealed++; tick(tickBrief, BRF_MS) }
334
- else tick(doReady, BRF2RDY)
235
+ else {
236
+ // Refresh header with final probe data
237
+ refreshHeaderData(root, config)
238
+ tick(doReady, 200)
239
+ }
335
240
  }
336
241
 
337
242
  // ── Phase: Ready ──
@@ -341,12 +246,12 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
341
246
  tickReady()
342
247
  }
343
248
  function tickReady() {
344
- if (readyTick < 3) { readyTick++; tick(tickReady, 350) }
249
+ if (readyTick < 3) { readyTick++; tick(tickReady, 250) }
345
250
  else tick(() => { if (!dead) done(undefined) }, RDY_HOLD)
346
251
  }
347
252
 
348
- // Kick
349
- tick(doSquare, 150)
253
+ // Kick off
254
+ tick(doSquare, 100)
350
255
 
351
256
  // ── Renderers ──
352
257
 
@@ -376,46 +281,38 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
376
281
 
377
282
  function renderSystems(W: number): string[] {
378
283
  const out: string[] = []
379
- const inner = Math.min(W - 4, 52)
284
+ const inner = Math.min(W - 4, 48)
380
285
  const lp = " ".repeat(Math.max(0, Math.floor((W - inner) / 2)))
381
286
 
382
287
  for (let i = 0; i < Math.min(sysRevealed, systems.length); i++) {
383
288
  const s = systems[i]
384
289
  const nm = s.name.padEnd(13)
385
290
  let icon: string, name: string, det: string
386
- if (s.status === "ok") { icon = gr("✓"); name = w(nm); det = g(s.detail) }
387
- else if (s.status === "err") { icon = rd("✗"); name = rd(nm); det = rd(s.detail) }
291
+ if (s.status === "ok") { icon = gr("✓"); name = w(nm); det = d(s.detail) }
292
+ else if (s.status === "err") { icon = rd("✗"); name = rd(nm); det = rd(s.detail) }
388
293
  else if (s.status === "probe") { icon = d("◌"); name = d(nm); det = d("…") }
389
- else { icon = d("○"); name = d(nm); det = d("") }
294
+ else { icon = d("○"); name = d(nm); det = d("") }
390
295
  out.push(`${lp} ${icon} ${name} ${det}`)
391
296
  }
392
297
  return out
393
298
  }
394
299
 
395
- function renderBrief(W: number): string[] {
396
- const out: string[] = []
397
- const inner = Math.min(W - 4, 52)
398
- const lp = " ".repeat(Math.max(0, Math.floor((W - inner) / 2)))
399
-
400
- // Divider
401
- out.push(lp + d("─".repeat(Math.max(8, inner - 10))))
402
- out.push("")
403
-
404
- for (let i = 0; i < Math.min(briefRevealed, brief.length); i++) {
405
- const b = brief[i]
406
- const color = b.color === "gold" ? g : b.color === "green" ? gr : b.color === "red" ? rd : b.color === "warm" ? w : d
407
- if (b.label) out.push(`${lp} ${d(b.label.padEnd(12))} ${color(b.value)}`)
408
- else out.push(`${lp} ${color(b.value)}`)
409
- }
410
- return out
411
- }
412
-
413
300
  function renderReady(W: number): string[] {
414
301
  const out: string[] = []
302
+ const sep = d(" · ")
415
303
  out.push("")
416
- if (readyTick >= 1) out.push(center(theme.bold(g(mission.name.toUpperCase())), W))
417
- if (readyTick >= 2) out.push(center(d(mission.branch), W))
418
- if (readyTick >= 3) out.push(center(d("ready"), W))
304
+ if (readyTick >= 1) out.push(center(theme.bold(g(projectName.toUpperCase())), W))
305
+ if (readyTick >= 2) out.push(center(d(branch), W))
306
+ if (readyTick >= 3) {
307
+ // Count how many probes passed
308
+ const ok = systems.filter(s => s.status === "ok").length
309
+ const total = systems.length
310
+ const allGood = ok === total
311
+ const statusText = allGood
312
+ ? gr("all systems nominal")
313
+ : g(`${ok}/${total} systems`)
314
+ out.push(center(statusText, W))
315
+ }
419
316
  return out
420
317
  }
421
318
 
@@ -439,14 +336,8 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
439
336
  if (phase !== "square" && phase !== "glow") content.push("")
440
337
 
441
338
  // Systems
442
- if (phase === "systems" || phase === "brief" || phase === "ready") {
339
+ if (phase === "systems" || phase === "ready") {
443
340
  content.push(...renderSystems(W))
444
- content.push("")
445
- }
446
-
447
- // Brief
448
- if (phase === "brief" || phase === "ready") {
449
- content.push(...renderBrief(W))
450
341
  }
451
342
 
452
343
  // Ready
@@ -454,22 +345,16 @@ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promis
454
345
  content.push(...renderReady(W))
455
346
  }
456
347
 
457
- // Skip hint
458
- if (phase !== "ready") {
348
+ // Skip hint (during animation only)
349
+ if (phase === "square" || phase === "glow" || phase === "systems") {
459
350
  content.push("", center(d("press any key to skip"), W))
460
351
  }
461
352
 
462
- // Build full-screen output: vertically center content, fill everything else with blanks
353
+ // Vertically center, fill everything with blanks (covers Pi startup)
463
354
  const topPad = Math.max(1, Math.floor((H - content.length) / 2))
464
355
  const out: string[] = []
465
-
466
- // Fill top with blank full-width lines (covers Pi startup text)
467
356
  for (let i = 0; i < topPad; i++) out.push(" ".repeat(W))
468
-
469
- // Content lines, each padded to full width
470
357
  for (const line of content) out.push(pad(line, W))
471
-
472
- // Fill bottom
473
358
  while (out.length < H) out.push(" ".repeat(W))
474
359
 
475
360
  return out
@@ -14,6 +14,7 @@ import { join } from "path"
14
14
  import { execSync } from "child_process"
15
15
  import type { PiContext, JflConfig, PiTheme } from "./types.js"
16
16
  import { readHubUrl, readToken } from "./hub-resolver.js"
17
+ import { getMeshClient, isStandaloneSubwayLoaded } from "./subway-mesh.js"
17
18
 
18
19
  // ─── Subsystem status tracking ──────────────────────────────────────────────
19
20
 
@@ -454,7 +455,7 @@ async function checkMemory(
454
455
  }
455
456
 
456
457
  async function checkSubway(
457
- root: string,
458
+ _root: string,
458
459
  sys: Subsystem,
459
460
  onUpdate: () => void,
460
461
  ): Promise<void> {
@@ -462,30 +463,40 @@ async function checkSubway(
462
463
  sys.status = "connecting"
463
464
  onUpdate()
464
465
 
465
- const hubBaseUrl = readHubUrl(root)
466
- const token = readToken(root)
466
+ if (isStandaloneSubwayLoaded()) {
467
+ // Standalone extension is handling mesh — report as ready
468
+ sys.progress = 1
469
+ sys.status = "ready"
470
+ sys.detail = "standalone ext"
471
+ onUpdate()
472
+ return
473
+ }
467
474
 
468
- try {
469
- sys.progress = 0.5
475
+ const client = getMeshClient()
476
+ if (!client) {
477
+ sys.progress = 1
478
+ sys.status = "warning"
479
+ sys.detail = "not initialized"
470
480
  onUpdate()
481
+ return
482
+ }
471
483
 
472
- const resp = await fetch(`${hubBaseUrl}/api/events?pattern=*&limit=1`, {
473
- headers: token ? { Authorization: `Bearer ${token}` } : {},
474
- signal: AbortSignal.timeout(3000),
475
- })
484
+ sys.progress = 0.5
485
+ onUpdate()
476
486
 
477
- if (resp.ok) {
478
- sys.progress = 1
479
- sys.status = "ready"
480
- sys.detail = "connected"
481
- onUpdate()
482
- } else {
483
- sys.progress = 1
484
- sys.status = "warning"
485
- sys.detail = "degraded"
486
- onUpdate()
487
- }
488
- } catch {
487
+ // Wait up to 3s for the mesh client to register
488
+ const deadline = Date.now() + 3000
489
+ while (Date.now() < deadline) {
490
+ if (client.registered) break
491
+ await new Promise(r => setTimeout(r, 100))
492
+ }
493
+
494
+ if (client.registered) {
495
+ sys.progress = 1
496
+ sys.status = "ready"
497
+ sys.detail = client.name
498
+ onUpdate()
499
+ } else {
489
500
  sys.progress = 1
490
501
  sys.status = "warning"
491
502
  sys.detail = "offline"
@@ -199,8 +199,13 @@ export async function setupServiceSkills(ctx: PiContext, _config: JflConfig): Pr
199
199
  },
200
200
  })
201
201
 
202
- // Register per-service commands (e.g., /jfl-cli status, /jfl-platform logs)
202
+ // Register per-service commands (e.g., /subclaw status, /subway-fe logs)
203
+ // Skip "subway" — /subway is owned by the mesh command (subway-mesh.ts or standalone ext).
204
+ // The jfl_service tool still covers all services including subway.
205
+ const reservedNames = new Set(["subway"])
206
+
203
207
  for (const svc of services) {
208
+ if (reservedNames.has(svc.name)) continue
204
209
  ctx.registerCommand({
205
210
  name: svc.name,
206
211
  description: `${svc.description} — commands: ${svc.commands.join(", ")}`,
@@ -201,7 +201,13 @@ export async function onShutdown(ctx: PiContext): Promise<void> {
201
201
 
202
202
  // ── 5. Merge session branch back (parity with CC session-cleanup.sh) ─
203
203
  if (branch.startsWith("session-")) {
204
- try {
204
+ // Guard: if git is already on a DIFFERENT branch (e.g. a new session started),
205
+ // don't merge — the old session branch may already be gone.
206
+ const currentGitBranch = getCurrentBranch(root)
207
+ if (currentGitBranch !== branch) {
208
+ ctx.log(`Skipping merge — git is on ${currentGitBranch}, not ${branch} (new session likely started)`, "debug")
209
+ ctx.ui.notify(` ⚠ Skipped merge — already on ${currentGitBranch}`, { level: "info" })
210
+ } else try {
205
211
  // Get working branch from config or default to main
206
212
  let workingBranch = "main"
207
213
  const configPath = join(root, ".jfl", "config.json")