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,11 +1,12 @@
1
1
  /**
2
2
  * Context Extension
3
3
  *
4
- * Ensures Context Hub is running, injects CLAUDE.md + recent context before
5
- * each agent turn via POST /api/prompt, and registers the jfl_context tool.
6
- * Hub is the single source of truth no local CLAUDE.md reading.
4
+ * Ensures Context Hub is running and registers the jfl_context tool.
5
+ * On first turn, injects a small amount of recent journal context so
6
+ * the model's greeting is contextual. The model reads CLAUDE.md itself
7
+ * via AGENTS.md instructions — no 45K system prompt injection needed.
7
8
  *
8
- * @purpose Context Hub + system prompt injection via Hub API (parity with Claude Code)
9
+ * @purpose Context Hub startup + jfl_context tool registration
9
10
  */
10
11
 
11
12
  import { existsSync, readFileSync } from "fs"
@@ -18,7 +19,11 @@ import { readHubUrl, readToken } from "./hub-resolver.js"
18
19
  let hubBaseUrl = "http://localhost:4242"
19
20
  let hubToken: string | null = null
20
21
  let projectRoot = ""
21
- let cachedPrompt: string | null = null
22
+ let promptInjected = false
23
+
24
+ export function resetPromptInjected(): void {
25
+ promptInjected = false
26
+ }
22
27
 
23
28
  function refreshHubUrl(): void {
24
29
  hubBaseUrl = readHubUrl(projectRoot)
@@ -28,10 +33,6 @@ function refreshHubUrl(): void {
28
33
  async function fetchContext(query?: string, limit = 10): Promise<string> {
29
34
  for (let attempt = 0; attempt < 2; attempt++) {
30
35
  try {
31
- const params = new URLSearchParams()
32
- if (query) params.set("query", query)
33
- params.set("limit", String(limit))
34
-
35
36
  const body: Record<string, unknown> = { maxItems: limit }
36
37
  if (query) body.query = query
37
38
 
@@ -67,81 +68,12 @@ async function fetchContext(query?: string, limit = 10): Promise<string> {
67
68
  return ""
68
69
  }
69
70
 
70
- /**
71
- * Strip Claude-Code-specific instructions from CLAUDE.md prompt.
72
- * Pi handles these via extensions (session.ts, map-bridge.ts, journal.ts)
73
- * so the LLM shouldn't be told to do them manually.
74
- */
75
- function stripClaudeCodeInstructions(prompt: string): string {
76
- let cleaned = prompt
77
-
78
- // Strip "Session Sync" section — Pi's session.ts handles via Hub API
79
- cleaned = cleaned.replace(
80
- /## CRITICAL: Session Sync[\s\S]*?(?=\n## (?!.*Session Sync))/,
81
- "## Session Management\n\nSession sync, doctor checks, and branch management are handled automatically by Pi extensions. No manual action needed.\n\n"
82
- )
83
-
84
- // Strip MCP tool references — Pi has native tools instead
85
- cleaned = cleaned.replace(/Call:\s*mcp__jfl-context__\w+/g, "Use the jfl_context tool")
86
- cleaned = cleaned.replace(/mcp__jfl-context__context_get/g, "jfl_context")
87
- cleaned = cleaned.replace(/mcp__jfl-context__context_search/g, "jfl_context (with query)")
88
- cleaned = cleaned.replace(/mcp__jfl-context__memory_search/g, "jfl_memory_search")
89
-
90
- // Strip .claude/settings.json references — Pi uses extensions
91
- cleaned = cleaned.replace(/Hooks in `\.claude\/settings\.json`[^\n]*/g, "Hooks are managed by Pi extensions automatically.")
92
- cleaned = cleaned.replace(/`\.claude\/settings\.json`[^`\n]*hooks[^.\n]*/gi, "Pi extension hooks")
93
-
94
- // Strip session-sync.sh and jfl-doctor.sh instructions — session.ts handles
95
- cleaned = cleaned.replace(/\n.*session-sync\.sh.*/g, "")
96
- cleaned = cleaned.replace(/\n.*jfl-doctor\.sh.*/g, "")
97
- cleaned = cleaned.replace(/\.\/scripts\/session\/session-sync\.sh/g, "(handled automatically)")
98
- cleaned = cleaned.replace(/\.\/scripts\/session\/jfl-doctor\.sh/g, "(handled automatically)")
99
-
100
- // Strip "Path B" sections (Claude Code manual startup)
101
- const pathBPatterns = [
102
- /### Path B: Claude Code \/ No Extension[\s\S]*?(?=\n### (?!.*Path B))/,
103
- /### How to Tell Which Path You're On[\s\S]*?(?=\n###? )/,
104
- ]
105
- for (const pattern of pathBPatterns) {
106
- cleaned = cleaned.replace(pattern, "")
107
- }
108
-
109
- // Strip file structure references to .claude/
110
- cleaned = cleaned.replace(/├── \.claude\/settings\.json[^\n]*/g, "")
111
- cleaned = cleaned.replace(/- `\.claude\/settings\.json`[^\n]*/g, "")
112
- cleaned = cleaned.replace(/- `\.claude\/skills\/`[^\n]*/g, "")
113
-
114
- return cleaned
115
- }
116
-
117
- async function fetchPrompt(taskType?: string): Promise<string> {
118
- try {
119
- const resp = await fetch(`${hubBaseUrl}/api/prompt`, {
120
- method: "POST",
121
- headers: {
122
- "Content-Type": "application/json",
123
- ...(hubToken ? { Authorization: `Bearer ${hubToken}` } : {}),
124
- },
125
- body: JSON.stringify({ taskType: taskType ?? "general", maxItems: 20 }),
126
- signal: AbortSignal.timeout(10000),
127
- })
128
-
129
- if (!resp.ok) return ""
130
- const data = await resp.json() as { prompt?: string }
131
- const raw = data.prompt ?? ""
132
-
133
- // Clean CC-specific instructions — Pi handles these via extensions
134
- return stripClaudeCodeInstructions(raw)
135
- } catch {
136
- return ""
137
- }
138
- }
139
-
140
71
  export async function setupContext(ctx: PiContext, _config: JflConfig): Promise<void> {
141
72
  const root = ctx.session.projectRoot
142
73
  projectRoot = root
74
+ promptInjected = false // reset on each new session
143
75
 
144
- // Start Context Hub FIRST, then read the port it wrote
76
+ // Start Context Hub
145
77
  try {
146
78
  execSync("jfl context-hub ensure", { cwd: root, stdio: "pipe", timeout: 15000 })
147
79
  ctx.log("Context Hub ensured", "debug")
@@ -150,15 +82,20 @@ export async function setupContext(ctx: PiContext, _config: JflConfig): Promise<
150
82
  ctx.log(`Context Hub ensure failed: ${msg}`, "debug")
151
83
  }
152
84
 
153
- // Now read the port (hub may have written .jfl/context-hub.port during ensure)
85
+ // Read the port hub wrote
154
86
  hubBaseUrl = readHubUrl(root)
155
87
  hubToken = readToken(root)
156
88
  ctx.log(`Context Hub URL: ${hubBaseUrl}`, "debug")
157
89
 
158
- // Pre-fetch prompt for first turn (cache it)
159
- cachedPrompt = await fetchPrompt("general")
160
- if (cachedPrompt) {
161
- ctx.log(`System prompt loaded via Hub (${cachedPrompt.length} chars)`, "debug")
90
+ // Wait for hub to actually accept connections (startDaemon only waits 800ms —
91
+ // not enough for the HTTP server to bind. Without this, session/init races and
92
+ // falls back to ctx.session.branch = "main".)
93
+ for (let i = 0; i < 20; i++) {
94
+ try {
95
+ const resp = await fetch(`${hubBaseUrl}/health`, { signal: AbortSignal.timeout(500) })
96
+ if (resp.ok) { ctx.log("Context Hub ready", "debug"); break }
97
+ } catch {}
98
+ await new Promise(resolve => setTimeout(resolve, 300))
162
99
  }
163
100
 
164
101
  ctx.registerTool({
@@ -189,15 +126,7 @@ export async function setupContext(ctx: PiContext, _config: JflConfig): Promise<
189
126
  },
190
127
  },
191
128
  async handler(input) {
192
- const { query, limit, taskType } = input as { query?: string; limit?: number; taskType?: string }
193
-
194
- // If asking for general context with no query, use the prompt endpoint
195
- if (!query && taskType) {
196
- const prompt = await fetchPrompt(taskType)
197
- return prompt || "No context available."
198
- }
199
-
200
- // Otherwise use the search endpoint
129
+ const { query, limit } = input as { query?: string; limit?: number; taskType?: string }
201
130
  const result = await fetchContext(query, limit ?? 30)
202
131
  return result || "No relevant context found."
203
132
  },
@@ -210,25 +139,20 @@ export async function injectContext(
210
139
  _ctx: PiContext,
211
140
  _event: AgentStartEvent
212
141
  ): Promise<{ systemPromptAddition?: string } | void> {
213
- // Use cached prompt from setup, or fetch fresh
214
- const prompt = cachedPrompt || await fetchPrompt("general")
215
-
216
- // Clear cache after first use — subsequent turns get fresh context
217
- cachedPrompt = null
218
-
219
- if (!prompt) {
220
- // Fallback: just inject recent context
221
- const context = await fetchContext(undefined, 10)
222
- if (!context) return
223
- return {
224
- systemPromptAddition: [
225
- "## JFL Project Context",
226
- "(Recent journal entries and project knowledge)",
227
- "",
228
- context,
229
- ].join("\n"),
230
- }
142
+ // Only on first turn inject recent journal context so greeting is contextual.
143
+ // The model reads CLAUDE.md itself (AGENTS.md tells it to). No 45K dump needed.
144
+ if (promptInjected) return
145
+ promptInjected = true
146
+
147
+ const context = await fetchContext(undefined, 10)
148
+ if (!context) return
149
+
150
+ return {
151
+ systemPromptAddition: [
152
+ "## JFL Project Context",
153
+ "(Recent journal entries and project knowledge)",
154
+ "",
155
+ context,
156
+ ].join("\n"),
231
157
  }
232
-
233
- return { systemPromptAddition: prompt }
234
158
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { randomUUID } from "crypto"
11
11
  import type { PiContext, JflConfig, AgentEndEvent } from "./types.js"
12
+ import { jflImport } from "./jfl-resolve.js"
12
13
  import { emitCustomEvent } from "./map-bridge.js"
13
14
 
14
15
  interface EvalEntry {
@@ -50,7 +51,7 @@ export async function onAgentEnd(ctx: PiContext, event: AgentEndEvent): Promise<
50
51
 
51
52
  try {
52
53
  // @ts-ignore — resolved from jfl package at runtime
53
- const { appendEval } = await import("../../src/lib/eval-store.js")
54
+ const { appendEval } = await jflImport("eval-store")
54
55
  appendEval(entry as Parameters<typeof appendEval>[0], projectRoot)
55
56
  } catch {
56
57
  // eval-store may not be available in all contexts — non-fatal
@@ -0,0 +1,171 @@
1
+ /**
2
+ * TENET Header
3
+ *
4
+ * Replaces Pi's default startup header (logo + keybinding hints) with a
5
+ * branded TENET identity line. Shows project name, branch, and key system
6
+ * indicators in a single compact row. This owns "above the fold" —
7
+ * everything the user sees before any messages.
8
+ *
9
+ * @purpose Branded header replacing Pi's default — owns above-the-fold identity
10
+ */
11
+
12
+ import { existsSync, readFileSync, readdirSync } from "fs"
13
+ import { join } from "path"
14
+ import type { PiContext, PiTheme, JflConfig } from "./types.js"
15
+
16
+ // ─── Cached state ────────────────────────────────────────────────────────────
17
+
18
+ let projectRoot = ""
19
+ let projectName = ""
20
+ let branch = ""
21
+ let journalCount = 0
22
+ let tupleCount = 0
23
+ let sessionCount = 0
24
+ let hubOnline = false
25
+ let autoCommitRunning = false
26
+ let evalScore: string | null = null
27
+
28
+ // ─── Data helpers ────────────────────────────────────────────────────────────
29
+
30
+ function countJournals(root: string): { entries: number; sessions: number } {
31
+ const dir = join(root, ".jfl", "journal")
32
+ if (!existsSync(dir)) return { entries: 0, sessions: 0 }
33
+ try {
34
+ const files = readdirSync(dir).filter(f => f.endsWith(".jsonl"))
35
+ let entries = 0
36
+ for (const f of files) {
37
+ entries += readFileSync(join(dir, f), "utf-8").trim().split("\n").filter(Boolean).length
38
+ }
39
+ return { entries, sessions: files.length }
40
+ } catch {
41
+ return { entries: 0, sessions: 0 }
42
+ }
43
+ }
44
+
45
+ function countTuples(root: string): number {
46
+ const p = join(root, ".jfl", "training-buffer.jsonl")
47
+ if (!existsSync(p)) return 0
48
+ try {
49
+ return readFileSync(p, "utf-8").trim().split("\n").filter(Boolean).length
50
+ } catch {
51
+ return 0
52
+ }
53
+ }
54
+
55
+ function getLatestEval(root: string): string | null {
56
+ try {
57
+ const p = join(root, ".jfl", "eval.jsonl")
58
+ if (!existsSync(p)) return null
59
+ const lines = readFileSync(p, "utf-8").trim().split("\n").filter(Boolean)
60
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 20); i--) {
61
+ try {
62
+ const e = JSON.parse(lines[i])
63
+ const s = e.composite ?? e.score
64
+ if (s != null) return Number(s).toFixed(2)
65
+ } catch {}
66
+ }
67
+ return null
68
+ } catch {
69
+ return null
70
+ }
71
+ }
72
+
73
+ // ─── Refresh on-demand (called from onboarding probes or session_start) ──────
74
+
75
+ export function refreshHeaderData(root: string, config: JflConfig): void {
76
+ projectRoot = root
77
+ projectName = config.name ?? root.split("/").pop() ?? "TENET"
78
+ const { entries, sessions } = countJournals(root)
79
+ journalCount = entries
80
+ sessionCount = sessions
81
+ tupleCount = countTuples(root)
82
+ evalScore = getLatestEval(root)
83
+ }
84
+
85
+ export function setHeaderHubStatus(online: boolean): void {
86
+ hubOnline = online
87
+ }
88
+
89
+ export function setHeaderAutoCommit(running: boolean): void {
90
+ autoCommitRunning = running
91
+ }
92
+
93
+ export function setHeaderBranch(b: string): void {
94
+ branch = b
95
+ }
96
+
97
+ // ─── ANSI helpers ────────────────────────────────────────────────────────────
98
+
99
+ function visLen(s: string): number {
100
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length
101
+ }
102
+
103
+ function truncAnsi(s: string, max: number): string {
104
+ let vis = 0
105
+ let i = 0
106
+ while (i < s.length && vis < max) {
107
+ if (s[i] === "\x1b") {
108
+ const end = s.indexOf("m", i)
109
+ if (end !== -1) { i = end + 1; continue }
110
+ }
111
+ vis++
112
+ i++
113
+ }
114
+ return vis >= max ? s.slice(0, i) : s
115
+ }
116
+
117
+ // ─── Render ──────────────────────────────────────────────────────────────────
118
+
119
+ function renderHeader(width: number, theme: PiTheme): string[] {
120
+ // Line 1: ◆ TENET · project-name · branch
121
+ const diamond = theme.fg("warning", "◆")
122
+ const brand = theme.bold(theme.fg("accent", "TENET"))
123
+ const name = theme.fg("text", projectName)
124
+ const branchStr = theme.fg("muted", branch || "main")
125
+
126
+ // Line 2: compact system indicators
127
+ const hubIcon = hubOnline
128
+ ? theme.fg("success", "✓") + theme.fg("dim", " Hub")
129
+ : theme.fg("error", "✗") + theme.fg("dim", " Hub")
130
+
131
+ const acIcon = autoCommitRunning
132
+ ? theme.fg("success", "✓") + theme.fg("dim", " Auto-commit")
133
+ : theme.fg("dim", "– Auto-commit")
134
+
135
+ const parts: string[] = [hubIcon, acIcon]
136
+
137
+ if (journalCount > 0) {
138
+ parts.push(theme.fg("dim", `${journalCount} entries · ${sessionCount} sessions`))
139
+ }
140
+ if (tupleCount > 0) {
141
+ parts.push(theme.fg("dim", `${tupleCount} tuples`))
142
+ }
143
+ if (evalScore) {
144
+ parts.push(theme.fg("warning", evalScore) + theme.fg("dim", " eval"))
145
+ }
146
+
147
+ const sep = theme.fg("dim", " · ")
148
+ const line1 = truncAnsi(` ${diamond} ${brand}${sep}${name}${sep}${branchStr}`, width)
149
+ const line2 = truncAnsi(` ${parts.join(theme.fg("dim", " │ "))}`, width)
150
+
151
+ return [line1, line2]
152
+ }
153
+
154
+ // ─── Setup ───────────────────────────────────────────────────────────────────
155
+
156
+ export function setupHeader(ctx: PiContext, config: JflConfig): void {
157
+ if (!ctx.ui.hasUI) return
158
+
159
+ projectRoot = ctx.session.projectRoot
160
+ branch = ctx.session.branch
161
+ refreshHeaderData(projectRoot, config)
162
+
163
+ // Use the raw Pi setHeader API (available on latestPiCtx.ui)
164
+ // We access it through the shim's setHeader if available
165
+ ctx.ui.setHeader?.((tui: any, theme: PiTheme) => ({
166
+ render(width: number): string[] {
167
+ return renderHeader(width, theme)
168
+ },
169
+ invalidate() {},
170
+ }))
171
+ }
@@ -12,22 +12,42 @@
12
12
 
13
13
  import type { PiContext, JflConfig } from "./types.js"
14
14
  import { hubUrl, authToken } from "./map-bridge.js"
15
+ import { readHubUrl, readToken } from "./hub-resolver.js"
16
+
17
+ let projectRoot = ""
15
18
 
16
19
  async function hubFetch(path: string, method: "GET" | "POST" = "GET", body?: unknown): Promise<any> {
17
- const resp = await fetch(`${hubUrl}${path}`, {
18
- method,
19
- headers: {
20
- "Content-Type": "application/json",
21
- ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
22
- },
23
- ...(body ? { body: JSON.stringify(body) } : {}),
24
- signal: AbortSignal.timeout(5000),
25
- })
26
- if (!resp.ok) throw new Error(`Hub ${path}: ${resp.status}`)
27
- return resp.json()
20
+ // Try current hubUrl, then fall back to fresh port file read
21
+ const urls = [hubUrl]
22
+ if (projectRoot) {
23
+ const fresh = readHubUrl(projectRoot)
24
+ if (fresh && fresh !== hubUrl) urls.push(fresh)
25
+ }
26
+
27
+ let lastErr: Error | null = null
28
+ for (const url of urls) {
29
+ const token = url === hubUrl ? authToken : (projectRoot ? readToken(projectRoot) : authToken)
30
+ try {
31
+ const resp = await fetch(`${url}${path}`, {
32
+ method,
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
36
+ },
37
+ ...(body ? { body: JSON.stringify(body) } : {}),
38
+ signal: AbortSignal.timeout(5000),
39
+ })
40
+ if (!resp.ok) throw new Error(`Hub ${path}: ${resp.status}`)
41
+ return await resp.json()
42
+ } catch (err) {
43
+ lastErr = err as Error
44
+ }
45
+ }
46
+ throw lastErr ?? new Error(`Hub ${path}: unreachable`)
28
47
  }
29
48
 
30
49
  export async function setupHubTools(ctx: PiContext, _config: JflConfig): Promise<void> {
50
+ projectRoot = ctx.session.projectRoot
31
51
 
32
52
  // ─── jfl_events_publish ──────────────────────────────────────────────────
33
53
  ctx.registerTool({