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.
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +23 -1
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/peter.d.ts.map +1 -1
- package/dist/commands/peter.js +11 -15
- package/dist/commands/peter.js.map +1 -1
- package/dist/commands/pivot.d.ts.map +1 -1
- package/dist/commands/pivot.js +22 -25
- package/dist/commands/pivot.js.map +1 -1
- package/dist/commands/repair.d.ts.map +1 -1
- package/dist/commands/repair.js +26 -0
- package/dist/commands/repair.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +39 -0
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +60 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +3 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/agent-session.d.ts.map +1 -1
- package/dist/lib/agent-session.js +6 -3
- package/dist/lib/agent-session.js.map +1 -1
- package/dist/lib/gtm-generator.js +7 -0
- package/dist/lib/gtm-generator.js.map +1 -1
- package/dist/lib/memory-db.d.ts +8 -0
- package/dist/lib/memory-db.d.ts.map +1 -1
- package/dist/lib/memory-db.js +24 -0
- package/dist/lib/memory-db.js.map +1 -1
- package/dist/lib/memory-indexer.d.ts +8 -0
- package/dist/lib/memory-indexer.d.ts.map +1 -1
- package/dist/lib/memory-indexer.js +30 -1
- package/dist/lib/memory-indexer.js.map +1 -1
- package/dist/lib/memory-search.d.ts.map +1 -1
- package/dist/lib/memory-search.js +2 -7
- package/dist/lib/memory-search.js.map +1 -1
- package/dist/lib/service-detector.js +2 -2
- package/dist/lib/service-detector.js.map +1 -1
- package/dist/lib/telemetry/physical-world-collector.js +1 -1
- package/dist/lib/telemetry/physical-world-collector.js.map +1 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +9 -6
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/provenance.d.ts +65 -0
- package/dist/utils/provenance.d.ts.map +1 -0
- package/dist/utils/provenance.js +213 -0
- package/dist/utils/provenance.js.map +1 -0
- package/package.json +1 -1
- package/packages/pi/extensions/context.ts +11 -0
- package/packages/pi/extensions/header.ts +171 -0
- package/packages/pi/extensions/hud-tool.ts +1 -1
- package/packages/pi/extensions/index.ts +43 -3
- package/packages/pi/extensions/memory-tool.ts +3 -3
- package/packages/pi/extensions/onboarding-v2.ts +70 -185
- package/packages/pi/extensions/onboarding-v3.ts +32 -21
- package/packages/pi/extensions/service-skills.ts +6 -1
- package/packages/pi/extensions/session.ts +7 -1
- package/packages/pi/extensions/startup-briefing.ts +313 -0
- package/packages/pi/extensions/subway-mesh.ts +893 -0
- package/packages/pi/extensions/types.ts +1 -0
- package/packages/pi/package.json +1 -0
- package/scripts/pp-branch-pr.sh +24 -6
- package/scripts/pp-branch-pr.sh.bak +115 -0
- package/template/.pi/settings.json +2 -0
- package/template/CLAUDE.md +82 -1738
- 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
|
-
|
|
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${
|
|
374
|
-
:
|
|
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 —
|
|
2
|
+
* Onboarding V2 — Cinematic boot sequence
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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:"
|
|
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
|
|
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"|"
|
|
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 =
|
|
269
|
-
const GLOW_MS =
|
|
270
|
-
const GLW2SYS =
|
|
271
|
-
const SYS_MS =
|
|
272
|
-
const SETTLE =
|
|
273
|
-
const
|
|
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
|
|
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 -
|
|
192
|
+
return line + " ".repeat(Math.max(0, width - stripLen(line)))
|
|
293
193
|
}
|
|
294
194
|
function center(text: string, width: number) {
|
|
295
|
-
const vis =
|
|
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 => {
|
|
313
|
-
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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")
|
|
387
|
-
else if (s.status === "err")
|
|
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
|
|
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(
|
|
417
|
-
if (readyTick >= 2) out.push(center(d(
|
|
418
|
-
if (readyTick >= 3)
|
|
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 === "
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
signal: AbortSignal.timeout(3000),
|
|
475
|
-
})
|
|
484
|
+
sys.progress = 0.5
|
|
485
|
+
onUpdate()
|
|
476
486
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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., /
|
|
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
|
-
|
|
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")
|