jfl 0.7.2 → 0.8.0

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 (93) hide show
  1. package/dist/commands/context-hub.d.ts.map +1 -1
  2. package/dist/commands/context-hub.js +26 -0
  3. package/dist/commands/context-hub.js.map +1 -1
  4. package/dist/commands/migrate-tenet.d.ts +25 -0
  5. package/dist/commands/migrate-tenet.d.ts.map +1 -0
  6. package/dist/commands/migrate-tenet.js +252 -0
  7. package/dist/commands/migrate-tenet.js.map +1 -0
  8. package/dist/commands/peter.d.ts.map +1 -1
  9. package/dist/commands/peter.js +47 -5
  10. package/dist/commands/peter.js.map +1 -1
  11. package/dist/commands/pi.d.ts +1 -0
  12. package/dist/commands/pi.d.ts.map +1 -1
  13. package/dist/commands/pi.js +5 -1
  14. package/dist/commands/pi.js.map +1 -1
  15. package/dist/commands/pivot.d.ts +28 -0
  16. package/dist/commands/pivot.d.ts.map +1 -0
  17. package/dist/commands/pivot.js +219 -0
  18. package/dist/commands/pivot.js.map +1 -0
  19. package/dist/commands/services-create.js +348 -0
  20. package/dist/commands/services-create.js.map +1 -1
  21. package/dist/dashboard-static/assets/index-BVrmW-ZI.js +154 -0
  22. package/dist/dashboard-static/assets/{index-CW8oWAdr.css → index-DtruPD44.css} +1 -1
  23. package/dist/dashboard-static/index.html +2 -2
  24. package/dist/index.js +24 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/agent-generator.d.ts.map +1 -1
  27. package/dist/lib/agent-generator.js +15 -0
  28. package/dist/lib/agent-generator.js.map +1 -1
  29. package/dist/lib/counterfactual-engine.d.ts +136 -0
  30. package/dist/lib/counterfactual-engine.d.ts.map +1 -0
  31. package/dist/lib/counterfactual-engine.js +417 -0
  32. package/dist/lib/counterfactual-engine.js.map +1 -0
  33. package/dist/lib/dynamics-model.d.ts +107 -0
  34. package/dist/lib/dynamics-model.d.ts.map +1 -0
  35. package/dist/lib/dynamics-model.js +363 -0
  36. package/dist/lib/dynamics-model.js.map +1 -0
  37. package/dist/lib/eval-snapshot.d.ts.map +1 -1
  38. package/dist/lib/eval-snapshot.js +15 -4
  39. package/dist/lib/eval-snapshot.js.map +1 -1
  40. package/dist/lib/invariant-monitor.d.ts +50 -0
  41. package/dist/lib/invariant-monitor.d.ts.map +1 -0
  42. package/dist/lib/invariant-monitor.js +400 -0
  43. package/dist/lib/invariant-monitor.js.map +1 -0
  44. package/dist/lib/meta-orchestrator.d.ts +40 -3
  45. package/dist/lib/meta-orchestrator.d.ts.map +1 -1
  46. package/dist/lib/meta-orchestrator.js +181 -2
  47. package/dist/lib/meta-orchestrator.js.map +1 -1
  48. package/dist/lib/openclaw-sdk.d.ts +8 -0
  49. package/dist/lib/openclaw-sdk.d.ts.map +1 -1
  50. package/dist/lib/openclaw-sdk.js +11 -0
  51. package/dist/lib/openclaw-sdk.js.map +1 -1
  52. package/dist/lib/peter-parker-bridge.d.ts +37 -1
  53. package/dist/lib/peter-parker-bridge.d.ts.map +1 -1
  54. package/dist/lib/peter-parker-bridge.js +201 -1
  55. package/dist/lib/peter-parker-bridge.js.map +1 -1
  56. package/dist/lib/service-detector.d.ts +1 -1
  57. package/dist/lib/service-detector.d.ts.map +1 -1
  58. package/dist/lib/service-detector.js +26 -6
  59. package/dist/lib/service-detector.js.map +1 -1
  60. package/dist/lib/service-gtm.d.ts +1 -1
  61. package/dist/lib/service-gtm.d.ts.map +1 -1
  62. package/dist/lib/state-capture.d.ts +36 -0
  63. package/dist/lib/state-capture.d.ts.map +1 -0
  64. package/dist/lib/state-capture.js +541 -0
  65. package/dist/lib/state-capture.js.map +1 -0
  66. package/dist/lib/stratus-client.d.ts +78 -2
  67. package/dist/lib/stratus-client.d.ts.map +1 -1
  68. package/dist/lib/stratus-client.js +432 -1
  69. package/dist/lib/stratus-client.js.map +1 -1
  70. package/dist/lib/world-model-store.d.ts +172 -0
  71. package/dist/lib/world-model-store.d.ts.map +1 -0
  72. package/dist/lib/world-model-store.js +487 -0
  73. package/dist/lib/world-model-store.js.map +1 -0
  74. package/dist/types/world-model.d.ts +478 -0
  75. package/dist/types/world-model.d.ts.map +1 -0
  76. package/dist/types/world-model.js +80 -0
  77. package/dist/types/world-model.js.map +1 -0
  78. package/dist/utils/jfl-config.d.ts +5 -0
  79. package/dist/utils/jfl-config.d.ts.map +1 -1
  80. package/dist/utils/jfl-config.js +13 -1
  81. package/dist/utils/jfl-config.js.map +1 -1
  82. package/package.json +1 -1
  83. package/packages/pi/extensions/hud-tool.ts +2 -24
  84. package/packages/pi/extensions/index.ts +48 -25
  85. package/packages/pi/extensions/onboarding-v1.ts +455 -0
  86. package/packages/pi/extensions/onboarding-v2.ts +516 -0
  87. package/packages/pi/extensions/onboarding-v3.ts +675 -0
  88. package/packages/pi/extensions/pivot-tool.ts +59 -0
  89. package/packages/pi/extensions/session.ts +58 -0
  90. package/packages/pi/extensions/types.ts +2 -0
  91. package/packages/pi/skills/pivot/SKILL.md +91 -0
  92. package/template/.claude/settings.json +9 -0
  93. package/dist/dashboard-static/assets/index-Ck8f9dcM.js +0 -121
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Onboarding V2 — CINEMATIC (Sator Square)
3
+ *
4
+ * Full-screen animated onboarding sequence for the Tenet rebrand.
5
+ * Progressive reveal: Sator Square -> subsystem awakening -> mission briefing.
6
+ * Gold (#FFD700) on dark, theatrical pacing, mystery and gravitas.
7
+ *
8
+ * @purpose Cinematic onboarding overlay — Sator Square, subsystem init, mission brief
9
+ */
10
+
11
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs"
12
+ import { join } from "path"
13
+ import { execSync } from "child_process"
14
+ import type { PiContext, PiTheme, JflConfig } from "./types.js"
15
+
16
+ const GOLD = "#FFD700"
17
+ const GOLD_DIM = "#B8960F"
18
+ const DARK_GOLD = "#7A6400"
19
+ const WARM_WHITE = "#F5E6D3"
20
+ const CHARCOAL = "#1a1a1a"
21
+ const EMBER = "#CC5500"
22
+
23
+ const SATOR_SQUARE = [
24
+ ["S", "A", "T", "O", "R"],
25
+ ["A", "R", "E", "P", "O"],
26
+ ["T", "E", "N", "E", "T"],
27
+ ["O", "P", "E", "R", "A"],
28
+ ["R", "O", "T", "A", "S"],
29
+ ]
30
+
31
+ const TENET_POSITIONS = [
32
+ [2, 0], [2, 1], [2, 2], [2, 3], [2, 4],
33
+ ]
34
+
35
+ interface SubsystemState {
36
+ name: string
37
+ icon: string
38
+ status: "waiting" | "connecting" | "ready" | "error"
39
+ detail: string
40
+ }
41
+
42
+ interface MissionData {
43
+ lastSessionSummary: string[]
44
+ pendingDecisions: number
45
+ currentPhase: string
46
+ projectName: string
47
+ projectType: string
48
+ branch: string
49
+ journalCount: number
50
+ activeAgents: number
51
+ }
52
+
53
+ function gatherMissionData(root: string, config: JflConfig): MissionData {
54
+ const projectName = config.name ?? root.split("/").pop() ?? "TENET"
55
+ const projectType = config.type ?? "gtm"
56
+
57
+ let currentPhase = ""
58
+ const roadmapPath = join(root, "knowledge", "ROADMAP.md")
59
+ if (existsSync(roadmapPath)) {
60
+ try {
61
+ const content = readFileSync(roadmapPath, "utf-8")
62
+ const phaseMatch = content.match(/## (?:Phase|Current Phase)[:\s]*([^\n]+)/i)
63
+ if (phaseMatch) currentPhase = phaseMatch[1].trim()
64
+ } catch {}
65
+ }
66
+
67
+ const lastSessionSummary: string[] = []
68
+ let journalCount = 0
69
+ const journalDir = join(root, ".jfl", "journal")
70
+ if (existsSync(journalDir)) {
71
+ try {
72
+ const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort()
73
+ for (const f of files) {
74
+ try {
75
+ const lines = readFileSync(join(journalDir, f), "utf-8").trim().split("\n").filter(Boolean)
76
+ journalCount += lines.length
77
+ for (const l of lines.slice(-5)) {
78
+ try {
79
+ const e = JSON.parse(l)
80
+ lastSessionSummary.push(e.title ?? e.summary ?? "")
81
+ } catch {}
82
+ }
83
+ } catch {}
84
+ }
85
+ } catch {}
86
+ }
87
+
88
+ let pendingDecisions = 0
89
+ const tasksPath = join(root, "knowledge", "TASKS.md")
90
+ if (existsSync(tasksPath)) {
91
+ try {
92
+ const content = readFileSync(tasksPath, "utf-8")
93
+ const pending = content.match(/\[ \]/g)
94
+ pendingDecisions = pending?.length ?? 0
95
+ } catch {}
96
+ }
97
+
98
+ let branch = "main"
99
+ try {
100
+ branch = execSync("git branch --show-current", { cwd: root, stdio: ["pipe", "pipe", "ignore"] })
101
+ .toString().trim() || "main"
102
+ } catch {}
103
+
104
+ let activeAgents = 0
105
+ if (existsSync(journalDir)) {
106
+ const now = Date.now()
107
+ try {
108
+ for (const f of readdirSync(journalDir)) {
109
+ if (!f.startsWith("session-") || !f.endsWith(".jsonl")) continue
110
+ try {
111
+ const stat = statSync(join(journalDir, f))
112
+ if (now - stat.mtimeMs < 300000) activeAgents++
113
+ } catch {}
114
+ }
115
+ } catch {}
116
+ }
117
+
118
+ return {
119
+ lastSessionSummary: lastSessionSummary.slice(-4),
120
+ pendingDecisions,
121
+ currentPhase,
122
+ projectName,
123
+ projectType,
124
+ branch,
125
+ journalCount,
126
+ activeAgents,
127
+ }
128
+ }
129
+
130
+ async function probeSubsystem(
131
+ name: string,
132
+ probeFn: () => Promise<string>
133
+ ): Promise<{ ok: boolean; detail: string }> {
134
+ try {
135
+ const detail = await probeFn()
136
+ return { ok: true, detail }
137
+ } catch {
138
+ return { ok: false, detail: "offline" }
139
+ }
140
+ }
141
+
142
+ async function probeContextHub(root: string): Promise<string> {
143
+ const portFile = join(root, ".jfl", "context-hub.port")
144
+ if (!existsSync(portFile)) return "not found"
145
+ const port = readFileSync(portFile, "utf-8").trim()
146
+ const resp = await fetch(`http://localhost:${port}/api/health`, {
147
+ signal: AbortSignal.timeout(2000),
148
+ })
149
+ if (!resp.ok) throw new Error("unhealthy")
150
+ const data = await resp.json() as { status?: string; itemCount?: number }
151
+ const count = data.itemCount ?? 0
152
+ return count > 0 ? `${count} items` : "ready"
153
+ }
154
+
155
+ async function probeMemory(root: string): Promise<string> {
156
+ const dbPath = join(root, ".jfl", "memory.db")
157
+ if (!existsSync(dbPath)) return "no database"
158
+ const journalDir = join(root, ".jfl", "journal")
159
+ if (!existsSync(journalDir)) return "0 memories"
160
+ let count = 0
161
+ for (const f of readdirSync(journalDir).filter(f => f.endsWith(".jsonl"))) {
162
+ try {
163
+ count += readFileSync(join(journalDir, f), "utf-8").trim().split("\n").filter(Boolean).length
164
+ } catch {}
165
+ }
166
+ return `${count} memories`
167
+ }
168
+
169
+ async function probeSubway(root: string): Promise<string> {
170
+ const envPath = join(root, ".env")
171
+ if (!existsSync(envPath)) return "no key"
172
+ try {
173
+ const env = readFileSync(envPath, "utf-8")
174
+ if (env.includes("STRATUS_API_KEY") || env.includes("stratus_sk")) {
175
+ return "relay online"
176
+ }
177
+ } catch {}
178
+ return "no relay"
179
+ }
180
+
181
+ export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promise<void> {
182
+ const root = ctx.session.projectRoot
183
+
184
+ if (!ctx.ui.hasUI) {
185
+ ctx.log("Onboarding v2: no TUI, skipping cinematic", "debug")
186
+ return
187
+ }
188
+
189
+ const mission = gatherMissionData(root, config)
190
+
191
+ const subsystemProbes = await Promise.allSettled([
192
+ probeSubsystem("Context Hub", () => probeContextHub(root)),
193
+ probeSubsystem("Memory", () => probeMemory(root)),
194
+ probeSubsystem("Subway", () => probeSubway(root)),
195
+ ])
196
+
197
+ const subsystems: SubsystemState[] = [
198
+ {
199
+ name: "Context Hub",
200
+ icon: "◆",
201
+ status: "waiting",
202
+ detail: "",
203
+ },
204
+ {
205
+ name: "Memory",
206
+ icon: "◆",
207
+ status: "waiting",
208
+ detail: "",
209
+ },
210
+ {
211
+ name: "Subway",
212
+ icon: "◆",
213
+ status: "waiting",
214
+ detail: "",
215
+ },
216
+ ]
217
+
218
+ const probeResults = subsystemProbes.map(p =>
219
+ p.status === "fulfilled" ? p.value : { ok: false, detail: "probe failed" }
220
+ )
221
+
222
+ await ctx.ui.custom<void>((tui: any, theme: PiTheme, _kb: any, done: (r: void) => void) => {
223
+ let phase: "square" | "subsystems" | "briefing" | "ready" = "square"
224
+ let squareProgress = 0
225
+ let tenetGlow = false
226
+ let subsystemIndex = -1
227
+ let briefingRevealed = 0
228
+ let readyFade = 0
229
+ let frameTimer: ReturnType<typeof setTimeout> | null = null
230
+ let disposed = false
231
+
232
+ const TOTAL_CELLS = 25
233
+ const SQUARE_FRAME_MS = 50
234
+ const SUBSYSTEM_FRAME_MS = 300
235
+ const BRIEFING_FRAME_MS = 200
236
+ const READY_DELAY_MS = 600
237
+ const TENET_GLOW_DELAY_MS = 400
238
+
239
+ function scheduleNext(fn: () => void, ms: number) {
240
+ if (disposed) return
241
+ frameTimer = setTimeout(() => {
242
+ if (disposed) return
243
+ fn()
244
+ tui.requestRender()
245
+ }, ms)
246
+ }
247
+
248
+ function advanceSquare() {
249
+ if (squareProgress < TOTAL_CELLS) {
250
+ squareProgress++
251
+ scheduleNext(advanceSquare, SQUARE_FRAME_MS)
252
+ } else if (!tenetGlow) {
253
+ scheduleNext(() => {
254
+ tenetGlow = true
255
+ scheduleNext(startSubsystems, TENET_GLOW_DELAY_MS)
256
+ }, TENET_GLOW_DELAY_MS)
257
+ }
258
+ }
259
+
260
+ function startSubsystems() {
261
+ phase = "subsystems"
262
+ subsystemIndex = 0
263
+ advanceSubsystem()
264
+ }
265
+
266
+ function advanceSubsystem() {
267
+ if (subsystemIndex < subsystems.length) {
268
+ const result = probeResults[subsystemIndex]
269
+ subsystems[subsystemIndex].status = result.ok ? "ready" : "error"
270
+ subsystems[subsystemIndex].detail = result.detail
271
+ subsystemIndex++
272
+ if (subsystemIndex < subsystems.length) {
273
+ scheduleNext(advanceSubsystem, SUBSYSTEM_FRAME_MS)
274
+ } else {
275
+ scheduleNext(startBriefing, SUBSYSTEM_FRAME_MS)
276
+ }
277
+ }
278
+ }
279
+
280
+ function startBriefing() {
281
+ phase = "briefing"
282
+ briefingRevealed = 0
283
+ advanceBriefing()
284
+ }
285
+
286
+ function advanceBriefing() {
287
+ const totalLines = getBriefingLines().length
288
+ if (briefingRevealed < totalLines) {
289
+ briefingRevealed++
290
+ scheduleNext(advanceBriefing, BRIEFING_FRAME_MS)
291
+ } else {
292
+ scheduleNext(startReady, READY_DELAY_MS)
293
+ }
294
+ }
295
+
296
+ function getBriefingLines(): string[] {
297
+ const lines: string[] = []
298
+ if (mission.lastSessionSummary.length > 0) {
299
+ for (const s of mission.lastSessionSummary.slice(-3)) {
300
+ if (s.trim()) lines.push(s.trim())
301
+ }
302
+ }
303
+ if (mission.pendingDecisions > 0) {
304
+ lines.push(`${mission.pendingDecisions} tasks pending`)
305
+ }
306
+ if (mission.currentPhase) {
307
+ lines.push(mission.currentPhase)
308
+ }
309
+ if (lines.length === 0) {
310
+ lines.push("First session. No prior context.")
311
+ }
312
+ return lines
313
+ }
314
+
315
+ function startReady() {
316
+ phase = "ready"
317
+ readyFade = 0
318
+ scheduleNext(() => {
319
+ readyFade = 1
320
+ scheduleNext(() => {
321
+ if (!disposed) done(undefined)
322
+ }, 500)
323
+ }, 300)
324
+ }
325
+
326
+ scheduleNext(advanceSquare, 300)
327
+
328
+ function gold(t: PiTheme, text: string): string {
329
+ return t.fg(GOLD, text)
330
+ }
331
+ function dimGold(t: PiTheme, text: string): string {
332
+ return t.fg(GOLD_DIM, text)
333
+ }
334
+ function darkGold(t: PiTheme, text: string): string {
335
+ return t.fg(DARK_GOLD, text)
336
+ }
337
+ function warm(t: PiTheme, text: string): string {
338
+ return t.fg(WARM_WHITE, text)
339
+ }
340
+
341
+ function renderSquare(theme: PiTheme, width: number): string[] {
342
+ const lines: string[] = []
343
+ const squareWidth = 5 * 4 - 1
344
+ const leftPad = Math.max(0, Math.floor((width - squareWidth) / 2))
345
+ const pad = " ".repeat(leftPad)
346
+
347
+ for (let row = 0; row < 5; row++) {
348
+ let line = ""
349
+ for (let col = 0; col < 5; col++) {
350
+ const cellIndex = row * 5 + col
351
+ const char = SATOR_SQUARE[row][col]
352
+ const isTenet = TENET_POSITIONS.some(([r, c]) => r === row && c === col)
353
+
354
+ if (cellIndex < squareProgress) {
355
+ if (tenetGlow && isTenet) {
356
+ line += theme.bold(gold(theme, char))
357
+ } else if (tenetGlow) {
358
+ line += darkGold(theme, char)
359
+ } else {
360
+ line += dimGold(theme, char)
361
+ }
362
+ } else {
363
+ line += darkGold(theme, ".")
364
+ }
365
+
366
+ if (col < 4) line += " "
367
+ }
368
+ lines.push(pad + line)
369
+ if (row < 4) lines.push("")
370
+ }
371
+
372
+ return lines
373
+ }
374
+
375
+ function renderSubsystems(theme: PiTheme, width: number): string[] {
376
+ const lines: string[] = []
377
+ const innerWidth = Math.min(width - 8, 50)
378
+ const leftPad = Math.max(0, Math.floor((width - innerWidth) / 2))
379
+ const pad = " ".repeat(leftPad)
380
+
381
+ for (let i = 0; i < subsystems.length; i++) {
382
+ const sys = subsystems[i]
383
+ let icon: string
384
+ let nameStr: string
385
+ let detailStr: string
386
+
387
+ if (i > subsystemIndex - 1 && i !== subsystemIndex) {
388
+ icon = darkGold(theme, "○")
389
+ nameStr = darkGold(theme, sys.name)
390
+ detailStr = darkGold(theme, "waiting...")
391
+ } else if (sys.status === "ready") {
392
+ icon = gold(theme, "●")
393
+ nameStr = warm(theme, sys.name)
394
+ detailStr = gold(theme, sys.detail)
395
+ } else if (sys.status === "error") {
396
+ icon = theme.fg(EMBER, "●")
397
+ nameStr = theme.fg(EMBER, sys.name)
398
+ detailStr = theme.fg(EMBER, sys.detail)
399
+ } else {
400
+ icon = dimGold(theme, "◌")
401
+ nameStr = dimGold(theme, sys.name)
402
+ detailStr = dimGold(theme, "connecting...")
403
+ }
404
+
405
+ const namePadded = (sys.name + " ".repeat(Math.max(0, 16 - sys.name.length)))
406
+ if (sys.status === "ready") {
407
+ lines.push(pad + `${icon} ${warm(theme, namePadded)} ${detailStr}`)
408
+ } else if (sys.status === "error") {
409
+ lines.push(pad + `${icon} ${theme.fg(EMBER, namePadded)} ${detailStr}`)
410
+ } else if (i > subsystemIndex - 1) {
411
+ lines.push(pad + `${icon} ${darkGold(theme, namePadded)} ${detailStr}`)
412
+ } else {
413
+ lines.push(pad + `${icon} ${dimGold(theme, namePadded)} ${detailStr}`)
414
+ }
415
+ }
416
+
417
+ return lines
418
+ }
419
+
420
+ function renderBriefing(theme: PiTheme, width: number): string[] {
421
+ const lines: string[] = []
422
+ const innerWidth = Math.min(width - 8, 56)
423
+ const leftPad = Math.max(0, Math.floor((width - innerWidth) / 2))
424
+ const pad = " ".repeat(leftPad)
425
+
426
+ const divider = darkGold(theme, "───") + dimGold(theme, " Last Session ") + darkGold(theme, "─".repeat(Math.max(0, innerWidth - 17)))
427
+ lines.push(pad + divider)
428
+ lines.push("")
429
+
430
+ const briefingLines = getBriefingLines()
431
+ for (let i = 0; i < briefingLines.length; i++) {
432
+ if (i < briefingRevealed) {
433
+ lines.push(pad + " " + warm(theme, briefingLines[i]))
434
+ }
435
+ }
436
+
437
+ return lines
438
+ }
439
+
440
+ function renderReady(theme: PiTheme, width: number): string[] {
441
+ const lines: string[] = []
442
+ const leftPad = Math.max(0, Math.floor((width - 20) / 2))
443
+ const pad = " ".repeat(leftPad)
444
+
445
+ if (readyFade >= 1) {
446
+ lines.push(pad + theme.bold(gold(theme, mission.projectName)))
447
+ lines.push(pad + dimGold(theme, `${mission.projectType} · ${mission.branch}`))
448
+ } else {
449
+ lines.push(pad + darkGold(theme, mission.projectName))
450
+ }
451
+
452
+ return lines
453
+ }
454
+
455
+ return {
456
+ handleInput(key: string) {
457
+ if (key === "\x1b" || key === "q" || key === " " || key === "\r") {
458
+ if (frameTimer) clearTimeout(frameTimer)
459
+ disposed = true
460
+ done(undefined)
461
+ }
462
+ },
463
+
464
+ render(width: number): string[] {
465
+ const lines: string[] = []
466
+ const w = Math.min(width, 80)
467
+
468
+ lines.push("")
469
+ lines.push("")
470
+
471
+ if (phase === "square" || phase === "subsystems" || phase === "briefing" || phase === "ready") {
472
+ const squareLines = renderSquare(theme, w)
473
+ lines.push(...squareLines)
474
+ }
475
+
476
+ lines.push("")
477
+
478
+ if (phase === "subsystems" || phase === "briefing" || phase === "ready") {
479
+ const sysLines = renderSubsystems(theme, w)
480
+ lines.push(...sysLines)
481
+ lines.push("")
482
+ }
483
+
484
+ if (phase === "briefing" || phase === "ready") {
485
+ const briefLines = renderBriefing(theme, w)
486
+ lines.push(...briefLines)
487
+ lines.push("")
488
+ }
489
+
490
+ if (phase === "ready") {
491
+ const readyLines = renderReady(theme, w)
492
+ lines.push(...readyLines)
493
+ lines.push("")
494
+ }
495
+
496
+ if (phase !== "ready") {
497
+ const hint = darkGold(theme, "Esc to skip")
498
+ const hintPad = " ".repeat(Math.max(0, Math.floor((w - 11) / 2)))
499
+ lines.push("")
500
+ lines.push(hintPad + hint)
501
+ }
502
+
503
+ lines.push("")
504
+
505
+ return lines
506
+ },
507
+
508
+ invalidate() {},
509
+ }
510
+ }, {
511
+ overlay: true,
512
+ overlayOptions: {
513
+ dismissOnEscape: false,
514
+ },
515
+ })
516
+ }