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,516 +1,369 @@
1
1
  /**
2
- * Onboarding V2 — CINEMATIC (Sator Square)
2
+ * Onboarding V2 — Cinematic boot sequence
3
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.
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 Cinematic onboarding overlay Sator Square, subsystem init, mission brief
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
- import { existsSync, readFileSync, readdirSync, statSync } from "fs"
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"
22
+
23
+ // ─── Colors ──────────────────────────────────────────────────────────────────
15
24
 
16
25
  const GOLD = "warning"
17
- const GOLD_DIM = "muted"
18
- const DARK_GOLD = "dim"
19
- const WARM_WHITE = "text"
20
- const CHARCOAL = "#1a1a1a"
21
- const EMBER = "error"
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"],
26
+ const DIM = "muted"
27
+ const WARM = "text"
28
+ const RED = "error"
29
+ const GREEN = "success"
30
+
31
+ // ─── Sator Square ────────────────────────────────────────────────────────────
32
+
33
+ const SATOR = [
34
+ ["S","A","T","O","R"],
35
+ ["A","R","E","P","O"],
36
+ ["T","E","N","E","T"],
37
+ ["O","P","E","R","A"],
38
+ ["R","O","T","A","S"],
29
39
  ]
40
+ const TENET_ROW = 2
30
41
 
31
- const TENET_POSITIONS = [
32
- [2, 0], [2, 1], [2, 2], [2, 3], [2, 4],
33
- ]
42
+ // ─── Types ───────────────────────────────────────────────────────────────────
34
43
 
35
- interface SubsystemState {
44
+ interface Sys {
36
45
  name: string
37
- icon: string
38
- status: "waiting" | "connecting" | "ready" | "error"
46
+ status: "wait" | "probe" | "ok" | "err"
39
47
  detail: string
40
48
  }
41
49
 
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
- }
50
+ // ─── Probes ──────────────────────────────────────────────────────────────────
66
51
 
67
- const lastSessionSummary: string[] = []
68
- let journalCount = 0
69
- const journalDir = join(root, ".jfl", "journal")
70
- if (existsSync(journalDir)) {
52
+ async function probeHub(root: string): Promise<{ok: boolean; detail: string}> {
53
+ const port = readPort(root)
54
+ for (let i = 0; i < 4; i++) {
71
55
  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
- }
56
+ const r = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(2000) })
57
+ if (r.ok) { const d = await r.json() as any; if (d.status === "ok") return {ok:true, detail:`:${port}`} }
85
58
  } catch {}
59
+ if (i < 3) await sleep(700)
86
60
  }
61
+ return {ok:false, detail:"offline"}
62
+ }
87
63
 
88
- let pendingDecisions = 0
89
- const tasksPath = join(root, "knowledge", "TASKS.md")
90
- if (existsSync(tasksPath)) {
64
+ async function probeContext(root: string): Promise<{ok: boolean; detail: string}> {
65
+ const port = readPort(root), token = readToken(root)
66
+ for (let i = 0; i < 3; i++) {
91
67
  try {
92
- const content = readFileSync(tasksPath, "utf-8")
93
- const pending = content.match(/\[ \]/g)
94
- pendingDecisions = pending?.length ?? 0
68
+ const r = await fetch(`http://localhost:${port}/api/context`, {
69
+ method: "POST",
70
+ headers: {"Content-Type":"application/json", ...(token ? {Authorization:`Bearer ${token}`} : {})},
71
+ body: JSON.stringify({maxItems:1}),
72
+ signal: AbortSignal.timeout(2000),
73
+ })
74
+ if (r.ok) return {ok:true, detail:"loaded"}
95
75
  } catch {}
76
+ if (i < 2) await sleep(700)
96
77
  }
78
+ return {ok:false, detail:"waiting…"}
79
+ }
97
80
 
98
- let branch = "main"
81
+ async function probeMemory(root: string): Promise<{ok: boolean; detail: string}> {
82
+ const dir = join(root, ".jfl", "journal")
83
+ if (!existsSync(dir)) return {ok:false, detail:"none"}
99
84
  try {
100
- branch = execSync("git branch --show-current", { cwd: root, stdio: ["pipe", "pipe", "ignore"] })
101
- .toString().trim() || "main"
102
- } catch {}
85
+ let n = 0
86
+ for (const f of readdirSync(dir).filter(f => f.endsWith(".jsonl")))
87
+ n += readFileSync(join(dir, f), "utf-8").trim().split("\n").filter(Boolean).length
88
+ return {ok:true, detail:`${n} entries`}
89
+ } catch { return {ok:false, detail:"err"} }
90
+ }
103
91
 
104
- let activeAgents = 0
105
- if (existsSync(journalDir)) {
106
- const now = Date.now()
92
+ async function probeAutoCommit(): Promise<{ok: boolean; detail: string}> {
93
+ for (let i = 0; i < 4; i++) {
107
94
  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
- }
95
+ if (execSync("ps aux", {timeout:2000, encoding:"utf-8", stdio:["pipe","pipe","ignore"]}).includes("auto-commit.sh"))
96
+ return {ok:true, detail:"watching"}
115
97
  } catch {}
98
+ if (i < 3) await sleep(1000)
116
99
  }
117
-
118
- return {
119
- lastSessionSummary: lastSessionSummary.slice(-4),
120
- pendingDecisions,
121
- currentPhase,
122
- projectName,
123
- projectType,
124
- branch,
125
- journalCount,
126
- activeAgents,
127
- }
100
+ return {ok:false, detail:"–"}
128
101
  }
129
102
 
130
- async function probeSubsystem(
131
- name: string,
132
- probeFn: () => Promise<string>
133
- ): Promise<{ ok: boolean; detail: string }> {
103
+ async function probeEval(root: string): Promise<{ok: boolean; detail: string}> {
104
+ const p = join(root, ".jfl", "eval.jsonl")
105
+ if (!existsSync(p)) return {ok:false, detail:"—"}
134
106
  try {
135
- const detail = await probeFn()
136
- return { ok: true, detail }
137
- } catch {
138
- return { ok: false, detail: "offline" }
139
- }
107
+ const lines = readFileSync(p, "utf-8").trim().split("\n").filter(Boolean)
108
+ for (let i = lines.length-1; i >= Math.max(0, lines.length-20); i--) {
109
+ try { const e = JSON.parse(lines[i]); const s = e.composite ?? e.score; if (s != null) return {ok:true, detail:Number(s).toFixed(2)} } catch {}
110
+ }
111
+ return {ok:true, detail:`${lines.length} runs`}
112
+ } catch { return {ok:false, detail:"err"} }
140
113
  }
141
114
 
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"
115
+ async function probeTraining(root: string): Promise<{ok: boolean; detail: string}> {
116
+ const p = join(root, ".jfl", "training-buffer.jsonl")
117
+ if (!existsSync(p)) return {ok:false, detail:"—"}
118
+ try {
119
+ return {ok:true, detail:`${readFileSync(p,"utf-8").trim().split("\n").filter(Boolean).length} tuples`}
120
+ } catch { return {ok:false, detail:"err"} }
153
121
  }
154
122
 
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`
123
+ function readPort(root: string): string {
124
+ const f = join(root, ".jfl", "context-hub.port")
125
+ return existsSync(f) ? readFileSync(f, "utf-8").trim() : "4360"
167
126
  }
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"
127
+ function readToken(root: string): string {
128
+ const f = join(root, ".jfl", "context-hub.token")
129
+ return existsSync(f) ? readFileSync(f, "utf-8").trim() : ""
179
130
  }
131
+ function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
132
+
133
+ // ─── Main ────────────────────────────────────────────────────────────────────
180
134
 
181
135
  export async function setupOnboarding(ctx: PiContext, config: JflConfig): Promise<void> {
182
136
  const root = ctx.session.projectRoot
137
+ if (!ctx.ui.hasUI) return
183
138
 
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
- },
139
+ const projectName = config.name ?? root.split("/").pop() ?? "TENET"
140
+ const branch = ctx.session.branch
141
+
142
+ const systems: Sys[] = [
143
+ {name:"Hub", status:"wait", detail:""},
144
+ {name:"Context", status:"wait", detail:""},
145
+ {name:"Memory", status:"wait", detail:""},
146
+ {name:"Auto-commit", status:"wait", detail:""},
147
+ {name:"Eval", status:"wait", detail:""},
148
+ {name:"Training", status:"wait", detail:""},
216
149
  ]
217
150
 
218
- const probeResults = subsystemProbes.map(p =>
219
- p.status === "fulfilled" ? p.value : { ok: false, detail: "probe failed" }
220
- )
151
+ const probes = [
152
+ () => probeHub(root),
153
+ () => probeContext(root),
154
+ () => probeMemory(root),
155
+ () => probeAutoCommit(),
156
+ () => probeEval(root),
157
+ () => probeTraining(root),
158
+ ]
221
159
 
222
160
  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)
161
+ let phase: "square"|"glow"|"systems"|"ready" = "square"
162
+ let sqProg = 0
163
+ let glow = false
164
+ let sysRevealed = 0
165
+ let readyTick = 0
166
+ let timer: ReturnType<typeof setTimeout>|null = null
167
+ let dead = false
168
+
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
176
+
177
+ function tick(fn: () => void, ms: number) {
178
+ if (dead) return
179
+ timer = setTimeout(() => { if (!dead) { fn(); tui.requestRender() } }, ms)
246
180
  }
247
181
 
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
- }
182
+ // ── Color helpers ──
183
+ const g = (s: string) => theme.fg(GOLD, s)
184
+ const d = (s: string) => theme.fg(DIM, s)
185
+ const w = (s: string) => theme.fg(WARM, s)
186
+ const gr = (s: string) => theme.fg(GREEN, s)
187
+ const rd = (s: string) => theme.fg(RED, s)
188
+
189
+ // ── ANSI helpers ──
190
+ function stripLen(s: string) { return s.replace(/\x1b\[[0-9;]*m/g, "").length }
191
+ function pad(line: string, width: number) {
192
+ return line + " ".repeat(Math.max(0, width - stripLen(line)))
258
193
  }
259
-
260
- function startSubsystems() {
261
- phase = "subsystems"
262
- subsystemIndex = 0
263
- advanceSubsystem()
194
+ function center(text: string, width: number) {
195
+ const vis = stripLen(text)
196
+ const left = Math.max(0, Math.floor((width - vis) / 2))
197
+ return " ".repeat(left) + text
264
198
  }
265
199
 
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
- }
200
+ // ── Phase: Square ──
201
+ function doSquare() {
202
+ if (sqProg < 25) { sqProg++; tick(doSquare, SQ_MS) }
203
+ else tick(() => { glow = true; phase = "glow"; tick(doSystems, GLW2SYS) }, GLOW_MS)
278
204
  }
279
205
 
280
- function startBriefing() {
281
- phase = "briefing"
282
- briefingRevealed = 0
283
- advanceBriefing()
206
+ // ── Phase: Systems ──
207
+ function doSystems() {
208
+ phase = "systems"
209
+ // Fire all probes in parallel
210
+ systems.forEach((s, i) => {
211
+ s.status = "probe"
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
+ })
224
+ })
225
+ revealSys()
284
226
  }
285
227
 
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
- }
228
+ function revealSys() {
229
+ if (sysRevealed < systems.length) { sysRevealed++; tick(revealSys, SYS_MS) }
230
+ else waitProbes()
294
231
  }
295
232
 
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
- }
233
+ function waitProbes() {
234
+ if (systems.some(s => s.status === "probe")) tick(waitProbes, SETTLE)
235
+ else {
236
+ // Refresh header with final probe data
237
+ refreshHeaderData(root, config)
238
+ tick(doReady, 200)
302
239
  }
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
240
  }
314
241
 
315
- function startReady() {
242
+ // ── Phase: Ready ──
243
+ function doReady() {
316
244
  phase = "ready"
317
- readyFade = 0
318
- scheduleNext(() => {
319
- readyFade = 1
320
- scheduleNext(() => {
321
- if (!disposed) done(undefined)
322
- }, 500)
323
- }, 300)
245
+ readyTick = 0
246
+ tickReady()
324
247
  }
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)
248
+ function tickReady() {
249
+ if (readyTick < 3) { readyTick++; tick(tickReady, 250) }
250
+ else tick(() => { if (!dead) done(undefined) }, RDY_HOLD)
339
251
  }
340
252
 
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)
253
+ // Kick off
254
+ tick(doSquare, 100)
346
255
 
347
- for (let row = 0; row < 5; row++) {
256
+ // ── Renderers ──
257
+
258
+ function renderSquare(W: number): string[] {
259
+ const out: string[] = []
260
+ const cw = 5 * 4 - 1 // "S A T O R"
261
+ const lp = " ".repeat(Math.max(0, Math.floor((W - cw) / 2)))
262
+ for (let r = 0; r < 5; r++) {
348
263
  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
- }
264
+ for (let c = 0; c < 5; c++) {
265
+ const idx = r * 5 + c
266
+ const ch = SATOR[r][c]
267
+ if (idx < sqProg) {
268
+ if (glow && r === TENET_ROW) line += theme.bold(g(ch))
269
+ else if (glow) line += d(ch)
270
+ else line += d(ch)
362
271
  } else {
363
- line += darkGold(theme, ".")
272
+ line += d("·")
364
273
  }
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}`)
274
+ if (c < 4) line += " "
414
275
  }
276
+ out.push(lp + line)
277
+ if (r < 4) out.push("") // spacing between rows
415
278
  }
416
-
417
- return lines
279
+ return out
418
280
  }
419
281
 
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
- }
282
+ function renderSystems(W: number): string[] {
283
+ const out: string[] = []
284
+ const inner = Math.min(W - 4, 48)
285
+ const lp = " ".repeat(Math.max(0, Math.floor((W - inner) / 2)))
286
+
287
+ for (let i = 0; i < Math.min(sysRevealed, systems.length); i++) {
288
+ const s = systems[i]
289
+ const nm = s.name.padEnd(13)
290
+ let icon: string, name: string, det: string
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) }
293
+ else if (s.status === "probe") { icon = d("◌"); name = d(nm); det = d("…") }
294
+ else { icon = d("○"); name = d(nm); det = d("") }
295
+ out.push(`${lp} ${icon} ${name} ${det}`)
435
296
  }
436
-
437
- return lines
297
+ return out
438
298
  }
439
299
 
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))
300
+ function renderReady(W: number): string[] {
301
+ const out: string[] = []
302
+ const sep = d(" · ")
303
+ out.push("")
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))
450
315
  }
451
-
452
- return lines
316
+ return out
453
317
  }
454
318
 
319
+ // ── Main render ──
455
320
  return {
456
321
  handleInput(key: string) {
457
322
  if (key === "\x1b" || key === "q" || key === " " || key === "\r") {
458
- if (frameTimer) clearTimeout(frameTimer)
459
- disposed = true
323
+ if (timer) clearTimeout(timer)
324
+ dead = true
460
325
  done(undefined)
461
326
  }
462
327
  },
463
328
 
464
329
  render(width: number): string[] {
465
- const lines: string[] = []
466
- const w = Math.min(width, 80)
467
-
468
- lines.push("")
469
- lines.push("")
330
+ const W = width
331
+ const H = process.stdout?.rows || 40
332
+ const content: string[] = []
470
333
 
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
- }
334
+ // Square
335
+ content.push(...renderSquare(W))
336
+ if (phase !== "square" && phase !== "glow") content.push("")
483
337
 
484
- if (phase === "briefing" || phase === "ready") {
485
- const briefLines = renderBriefing(theme, w)
486
- lines.push(...briefLines)
487
- lines.push("")
338
+ // Systems
339
+ if (phase === "systems" || phase === "ready") {
340
+ content.push(...renderSystems(W))
488
341
  }
489
342
 
343
+ // Ready
490
344
  if (phase === "ready") {
491
- const readyLines = renderReady(theme, w)
492
- lines.push(...readyLines)
493
- lines.push("")
345
+ content.push(...renderReady(W))
494
346
  }
495
347
 
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)
348
+ // Skip hint (during animation only)
349
+ if (phase === "square" || phase === "glow" || phase === "systems") {
350
+ content.push("", center(d("press any key to skip"), W))
501
351
  }
502
352
 
503
- lines.push("")
353
+ // Vertically center, fill everything with blanks (covers Pi startup)
354
+ const topPad = Math.max(1, Math.floor((H - content.length) / 2))
355
+ const out: string[] = []
356
+ for (let i = 0; i < topPad; i++) out.push(" ".repeat(W))
357
+ for (const line of content) out.push(pad(line, W))
358
+ while (out.length < H) out.push(" ".repeat(W))
504
359
 
505
- return lines
360
+ return out
506
361
  },
507
362
 
508
363
  invalidate() {},
509
364
  }
510
365
  }, {
511
366
  overlay: true,
512
- overlayOptions: {
513
- dismissOnEscape: false,
514
- },
367
+ overlayOptions: { dismissOnEscape: false },
515
368
  })
516
369
  }