jfl 0.5.0 → 0.6.1

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 (135) hide show
  1. package/dist/commands/context-hub.d.ts +1 -0
  2. package/dist/commands/context-hub.d.ts.map +1 -1
  3. package/dist/commands/context-hub.js +246 -2
  4. package/dist/commands/context-hub.js.map +1 -1
  5. package/dist/commands/peter.d.ts +2 -0
  6. package/dist/commands/peter.d.ts.map +1 -1
  7. package/dist/commands/peter.js +242 -52
  8. package/dist/commands/peter.js.map +1 -1
  9. package/dist/commands/setup.d.ts +12 -0
  10. package/dist/commands/setup.d.ts.map +1 -0
  11. package/dist/commands/setup.js +322 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/commands/train.d.ts +33 -0
  14. package/dist/commands/train.d.ts.map +1 -0
  15. package/dist/commands/train.js +510 -0
  16. package/dist/commands/train.js.map +1 -0
  17. package/dist/commands/verify.d.ts +14 -0
  18. package/dist/commands/verify.d.ts.map +1 -0
  19. package/dist/commands/verify.js +276 -0
  20. package/dist/commands/verify.js.map +1 -0
  21. package/dist/dashboard-static/assets/index-CW9ZxqX8.css +1 -0
  22. package/dist/dashboard-static/assets/index-DNN__p4K.js +121 -0
  23. package/dist/dashboard-static/index.html +2 -2
  24. package/dist/index.js +99 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/agent-session.d.ts.map +1 -1
  27. package/dist/lib/agent-session.js +12 -4
  28. package/dist/lib/agent-session.js.map +1 -1
  29. package/dist/lib/eval-snapshot.js +1 -1
  30. package/dist/lib/eval-snapshot.js.map +1 -1
  31. package/dist/lib/pi-sky/bridge.d.ts +55 -0
  32. package/dist/lib/pi-sky/bridge.d.ts.map +1 -0
  33. package/dist/lib/pi-sky/bridge.js +264 -0
  34. package/dist/lib/pi-sky/bridge.js.map +1 -0
  35. package/dist/lib/pi-sky/cost-monitor.d.ts +21 -0
  36. package/dist/lib/pi-sky/cost-monitor.d.ts.map +1 -0
  37. package/dist/lib/pi-sky/cost-monitor.js +126 -0
  38. package/dist/lib/pi-sky/cost-monitor.js.map +1 -0
  39. package/dist/lib/pi-sky/eval-sweep.d.ts +27 -0
  40. package/dist/lib/pi-sky/eval-sweep.d.ts.map +1 -0
  41. package/dist/lib/pi-sky/eval-sweep.js +141 -0
  42. package/dist/lib/pi-sky/eval-sweep.js.map +1 -0
  43. package/dist/lib/pi-sky/event-router.d.ts +32 -0
  44. package/dist/lib/pi-sky/event-router.d.ts.map +1 -0
  45. package/dist/lib/pi-sky/event-router.js +176 -0
  46. package/dist/lib/pi-sky/event-router.js.map +1 -0
  47. package/dist/lib/pi-sky/experiment.d.ts +9 -0
  48. package/dist/lib/pi-sky/experiment.d.ts.map +1 -0
  49. package/dist/lib/pi-sky/experiment.js +83 -0
  50. package/dist/lib/pi-sky/experiment.js.map +1 -0
  51. package/dist/lib/pi-sky/index.d.ts +16 -0
  52. package/dist/lib/pi-sky/index.d.ts.map +1 -0
  53. package/dist/lib/pi-sky/index.js +16 -0
  54. package/dist/lib/pi-sky/index.js.map +1 -0
  55. package/dist/lib/pi-sky/stratus-gate.d.ts +28 -0
  56. package/dist/lib/pi-sky/stratus-gate.d.ts.map +1 -0
  57. package/dist/lib/pi-sky/stratus-gate.js +61 -0
  58. package/dist/lib/pi-sky/stratus-gate.js.map +1 -0
  59. package/dist/lib/pi-sky/swarm.d.ts +28 -0
  60. package/dist/lib/pi-sky/swarm.d.ts.map +1 -0
  61. package/dist/lib/pi-sky/swarm.js +208 -0
  62. package/dist/lib/pi-sky/swarm.js.map +1 -0
  63. package/dist/lib/pi-sky/types.d.ts +139 -0
  64. package/dist/lib/pi-sky/types.d.ts.map +1 -0
  65. package/dist/lib/pi-sky/types.js +2 -0
  66. package/dist/lib/pi-sky/types.js.map +1 -0
  67. package/dist/lib/pi-sky/voice-bridge.d.ts +20 -0
  68. package/dist/lib/pi-sky/voice-bridge.d.ts.map +1 -0
  69. package/dist/lib/pi-sky/voice-bridge.js +91 -0
  70. package/dist/lib/pi-sky/voice-bridge.js.map +1 -0
  71. package/dist/lib/policy-head.d.ts +16 -1
  72. package/dist/lib/policy-head.d.ts.map +1 -1
  73. package/dist/lib/policy-head.js +117 -19
  74. package/dist/lib/policy-head.js.map +1 -1
  75. package/dist/lib/predictor.d.ts +10 -0
  76. package/dist/lib/predictor.d.ts.map +1 -1
  77. package/dist/lib/predictor.js +46 -7
  78. package/dist/lib/predictor.js.map +1 -1
  79. package/dist/lib/setup/agent-generator.d.ts +18 -0
  80. package/dist/lib/setup/agent-generator.d.ts.map +1 -0
  81. package/dist/lib/setup/agent-generator.js +114 -0
  82. package/dist/lib/setup/agent-generator.js.map +1 -0
  83. package/dist/lib/setup/context-analyzer.d.ts +16 -0
  84. package/dist/lib/setup/context-analyzer.d.ts.map +1 -0
  85. package/dist/lib/setup/context-analyzer.js +112 -0
  86. package/dist/lib/setup/context-analyzer.js.map +1 -0
  87. package/dist/lib/setup/doc-auditor.d.ts +54 -0
  88. package/dist/lib/setup/doc-auditor.d.ts.map +1 -0
  89. package/dist/lib/setup/doc-auditor.js +629 -0
  90. package/dist/lib/setup/doc-auditor.js.map +1 -0
  91. package/dist/lib/setup/domain-generator.d.ts +7 -0
  92. package/dist/lib/setup/domain-generator.d.ts.map +1 -0
  93. package/dist/lib/setup/domain-generator.js +58 -0
  94. package/dist/lib/setup/domain-generator.js.map +1 -0
  95. package/dist/lib/setup/smart-eval-generator.d.ts +38 -0
  96. package/dist/lib/setup/smart-eval-generator.d.ts.map +1 -0
  97. package/dist/lib/setup/smart-eval-generator.js +378 -0
  98. package/dist/lib/setup/smart-eval-generator.js.map +1 -0
  99. package/dist/lib/setup/smart-recommender.d.ts +63 -0
  100. package/dist/lib/setup/smart-recommender.d.ts.map +1 -0
  101. package/dist/lib/setup/smart-recommender.js +329 -0
  102. package/dist/lib/setup/smart-recommender.js.map +1 -0
  103. package/dist/lib/setup/spec-generator.d.ts +63 -0
  104. package/dist/lib/setup/spec-generator.d.ts.map +1 -0
  105. package/dist/lib/setup/spec-generator.js +310 -0
  106. package/dist/lib/setup/spec-generator.js.map +1 -0
  107. package/dist/lib/setup/violation-agent-generator.d.ts +32 -0
  108. package/dist/lib/setup/violation-agent-generator.d.ts.map +1 -0
  109. package/dist/lib/setup/violation-agent-generator.js +255 -0
  110. package/dist/lib/setup/violation-agent-generator.js.map +1 -0
  111. package/package.json +1 -1
  112. package/packages/pi/extensions/context.ts +88 -55
  113. package/packages/pi/extensions/hub-resolver.ts +63 -0
  114. package/packages/pi/extensions/index.ts +16 -3
  115. package/packages/pi/extensions/memory-tool.ts +9 -4
  116. package/packages/pi/extensions/session.ts +68 -16
  117. package/packages/pi/extensions/tool-renderers.ts +23 -8
  118. package/scripts/train/requirements.txt +5 -0
  119. package/scripts/train/train-policy-head.py +477 -0
  120. package/scripts/train/v2/dataset.py +81 -0
  121. package/scripts/train/v2/domain.json +18 -0
  122. package/scripts/train/v2/eval.py +196 -0
  123. package/scripts/train/v2/generate_data.py +219 -0
  124. package/scripts/train/v2/infer.py +188 -0
  125. package/scripts/train/v2/model.py +112 -0
  126. package/scripts/train/v2/precompute.py +132 -0
  127. package/scripts/train/v2/train.py +302 -0
  128. package/scripts/train/v2/transform_buffer.py +227 -0
  129. package/scripts/train/v2/validate_data.py +115 -0
  130. package/template/.claude/settings.json +2 -15
  131. package/template/scripts/session/session-cleanup.sh +2 -11
  132. package/template/scripts/session/session-end-hub.sh +72 -0
  133. package/template/scripts/session/session-start-hub.sh +105 -0
  134. package/dist/dashboard-static/assets/index-B6b867Pv.js +0 -121
  135. package/dist/dashboard-static/assets/index-Y4BrqxV-.css +0 -1
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Context Extension
3
3
  *
4
- * Ensures Context Hub is running, injects recent context before each agent turn,
5
- * and registers the jfl_context tool with custom TUI rendering.
6
- * Context results show type-colored headers and collapsible sections.
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.
7
7
  *
8
- * @purpose Context Hub integration inject context, register themed jfl_context tool
8
+ * @purpose Context Hub + system prompt injection via Hub API (parity with Claude Code)
9
9
  */
10
10
 
11
11
  import { existsSync, readFileSync } from "fs"
@@ -13,55 +13,35 @@ import { join } from "path"
13
13
  import { execSync } from "child_process"
14
14
  import type { PiContext, JflConfig, AgentStartEvent } from "./types.js"
15
15
  import { contextRenderCall, contextRenderResult } from "./tool-renderers.js"
16
+ import { readHubUrl, readToken } from "./hub-resolver.js"
16
17
 
17
18
  let hubBaseUrl = "http://localhost:4242"
18
19
  let hubToken: string | null = null
19
20
  let projectRoot = ""
20
-
21
- function readToken(root: string): string | null {
22
- const tokenPath = join(root, ".jfl", "context-hub.token")
23
- if (existsSync(tokenPath)) {
24
- return readFileSync(tokenPath, "utf-8").trim()
25
- }
26
- return null
27
- }
28
-
29
- function getHubUrl(root: string): string {
30
- // 1. Runtime port file (written by context-hub when it starts)
31
- const portFile = join(root, ".jfl", "context-hub.port")
32
- if (existsSync(portFile)) {
33
- const port = readFileSync(portFile, "utf-8").trim()
34
- if (port) return `http://localhost:${port}`
35
- }
36
-
37
- // 2. Project config (static port assignment)
38
- const configFile = join(root, ".jfl", "config.json")
39
- if (existsSync(configFile)) {
40
- try {
41
- const config = JSON.parse(readFileSync(configFile, "utf-8"))
42
- const port = config.contextHub?.port
43
- if (port) return `http://localhost:${port}`
44
- } catch {}
45
- }
46
-
47
- return "http://localhost:4242"
48
- }
21
+ let cachedPrompt: string | null = null
49
22
 
50
23
  function refreshHubUrl(): void {
51
- hubBaseUrl = getHubUrl(projectRoot)
24
+ hubBaseUrl = readHubUrl(projectRoot)
52
25
  hubToken = readToken(projectRoot)
53
26
  }
54
27
 
55
28
  async function fetchContext(query?: string, limit = 10): Promise<string> {
56
- // Try current URL first, then refresh and retry once on failure
57
29
  for (let attempt = 0; attempt < 2; attempt++) {
58
30
  try {
59
31
  const params = new URLSearchParams()
60
32
  if (query) params.set("query", query)
61
33
  params.set("limit", String(limit))
62
34
 
63
- const resp = await fetch(`${hubBaseUrl}/api/context?${params}`, {
64
- headers: hubToken ? { Authorization: `Bearer ${hubToken}` } : {},
35
+ const body: Record<string, unknown> = { maxItems: limit }
36
+ if (query) body.query = query
37
+
38
+ const resp = await fetch(`${hubBaseUrl}/api/context`, {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ ...(hubToken ? { Authorization: `Bearer ${hubToken}` } : {}),
43
+ },
44
+ body: JSON.stringify(body),
65
45
  signal: AbortSignal.timeout(5000),
66
46
  })
67
47
 
@@ -87,6 +67,26 @@ async function fetchContext(query?: string, limit = 10): Promise<string> {
87
67
  return ""
88
68
  }
89
69
 
70
+ async function fetchPrompt(taskType?: string): Promise<string> {
71
+ try {
72
+ const resp = await fetch(`${hubBaseUrl}/api/prompt`, {
73
+ method: "POST",
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ ...(hubToken ? { Authorization: `Bearer ${hubToken}` } : {}),
77
+ },
78
+ body: JSON.stringify({ taskType: taskType ?? "general", maxItems: 20 }),
79
+ signal: AbortSignal.timeout(10000),
80
+ })
81
+
82
+ if (!resp.ok) return ""
83
+ const data = await resp.json() as { prompt?: string }
84
+ return data.prompt ?? ""
85
+ } catch {
86
+ return ""
87
+ }
88
+ }
89
+
90
90
  export async function setupContext(ctx: PiContext, _config: JflConfig): Promise<void> {
91
91
  const root = ctx.session.projectRoot
92
92
  projectRoot = root
@@ -101,31 +101,54 @@ export async function setupContext(ctx: PiContext, _config: JflConfig): Promise<
101
101
  }
102
102
 
103
103
  // Now read the port (hub may have written .jfl/context-hub.port during ensure)
104
- hubBaseUrl = getHubUrl(root)
104
+ hubBaseUrl = readHubUrl(root)
105
105
  hubToken = readToken(root)
106
106
  ctx.log(`Context Hub URL: ${hubBaseUrl}`, "debug")
107
107
 
108
+ // Pre-fetch prompt for first turn (cache it)
109
+ cachedPrompt = await fetchPrompt("general")
110
+ if (cachedPrompt) {
111
+ ctx.log(`System prompt loaded via Hub (${cachedPrompt.length} chars)`, "debug")
112
+ }
113
+
108
114
  ctx.registerTool({
109
115
  name: "jfl_context",
110
- description: "Search JFL project context: journal entries, knowledge docs, memory. Use this to look up what happened in previous sessions, project decisions, or any project-specific knowledge.",
111
- promptSnippet: "Search project context: journals, knowledge docs, decisions",
116
+ description: "Get unified project context: journal entries, knowledge docs, code headers. Use at session start and when you need project state. Equivalent to MCP context_get.",
117
+ promptSnippet: "Get project context: journals, knowledge, code — use at session start",
118
+ promptGuidelines: [
119
+ "Call this tool at the start of every session to understand project state",
120
+ "Use taskType to prioritize context for your current task",
121
+ "Call without query to get general project overview",
122
+ ],
112
123
  inputSchema: {
113
124
  type: "object",
114
125
  properties: {
115
126
  query: {
116
127
  type: "string",
117
- description: "Search query to find relevant context",
128
+ description: "Optional search query to filter results",
129
+ },
130
+ taskType: {
131
+ type: "string",
132
+ description: "Type of task for context prioritization",
133
+ enum: ["code", "spec", "content", "strategy", "general"],
118
134
  },
119
135
  limit: {
120
136
  type: "number",
121
- description: "Maximum number of results to return (default: 10)",
137
+ description: "Maximum number of results to return (default: 30)",
122
138
  },
123
139
  },
124
- required: ["query"],
125
140
  },
126
141
  async handler(input) {
127
- const { query, limit } = input as { query: string; limit?: number }
128
- const result = await fetchContext(query, limit ?? 10)
142
+ const { query, limit, taskType } = input as { query?: string; limit?: number; taskType?: string }
143
+
144
+ // If asking for general context with no query, use the prompt endpoint
145
+ if (!query && taskType) {
146
+ const prompt = await fetchPrompt(taskType)
147
+ return prompt || "No context available."
148
+ }
149
+
150
+ // Otherwise use the search endpoint
151
+ const result = await fetchContext(query, limit ?? 30)
129
152
  return result || "No relevant context found."
130
153
  },
131
154
  renderCall: contextRenderCall,
@@ -137,15 +160,25 @@ export async function injectContext(
137
160
  _ctx: PiContext,
138
161
  _event: AgentStartEvent
139
162
  ): Promise<{ systemPromptAddition?: string } | void> {
140
- const context = await fetchContext(undefined, 10)
141
- if (!context) return
142
-
143
- return {
144
- systemPromptAddition: [
145
- "## JFL Project Context",
146
- "(Recent journal entries and project knowledge — use this to maintain continuity across sessions)",
147
- "",
148
- context,
149
- ].join("\n"),
163
+ // Use cached prompt from setup, or fetch fresh
164
+ const prompt = cachedPrompt || await fetchPrompt("general")
165
+
166
+ // Clear cache after first use — subsequent turns get fresh context
167
+ cachedPrompt = null
168
+
169
+ if (!prompt) {
170
+ // Fallback: just inject recent context
171
+ const context = await fetchContext(undefined, 10)
172
+ if (!context) return
173
+ return {
174
+ systemPromptAddition: [
175
+ "## JFL Project Context",
176
+ "(Recent journal entries and project knowledge)",
177
+ "",
178
+ context,
179
+ ].join("\n"),
180
+ }
150
181
  }
182
+
183
+ return { systemPromptAddition: prompt }
151
184
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Hub Resolver
3
+ *
4
+ * Resolves the active Context Hub URL for a project using runtime artifacts,
5
+ * persisted config, MCP config, and deterministic per-project fallback.
6
+ *
7
+ * @purpose Shared Context Hub URL/token resolution for Pi extensions
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "fs"
11
+ import { join, resolve } from "path"
12
+
13
+ const PORT_MIN = 4200
14
+ const PORT_MAX = 4999
15
+ const PORT_RANGE = PORT_MAX - PORT_MIN + 1
16
+
17
+ function computePortFromPath(projectPath: string): number {
18
+ const normalized = resolve(projectPath)
19
+ let hash = 5381
20
+ for (let i = 0; i < normalized.length; i++) {
21
+ hash = ((hash << 5) + hash + normalized.charCodeAt(i)) >>> 0
22
+ }
23
+ return PORT_MIN + (hash % PORT_RANGE)
24
+ }
25
+
26
+ function readJson(filePath: string): any | null {
27
+ if (!existsSync(filePath)) return null
28
+ try {
29
+ return JSON.parse(readFileSync(filePath, "utf-8"))
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ export function readToken(root: string): string | null {
36
+ const tokenPath = join(root, ".jfl", "context-hub.token")
37
+ if (existsSync(tokenPath)) {
38
+ return readFileSync(tokenPath, "utf-8").trim() || null
39
+ }
40
+ return null
41
+ }
42
+
43
+ export function readHubUrl(root: string): string {
44
+ const portFile = join(root, ".jfl", "context-hub.port")
45
+ if (existsSync(portFile)) {
46
+ const port = readFileSync(portFile, "utf-8").trim()
47
+ if (port) return `http://localhost:${port}`
48
+ }
49
+
50
+ const projectConfig = readJson(join(root, ".jfl", "config.json"))
51
+ const configuredPort = projectConfig?.contextHub?.port
52
+ if (configuredPort) {
53
+ return `http://localhost:${configuredPort}`
54
+ }
55
+
56
+ const mcpConfig = readJson(join(root, ".mcp.json"))
57
+ const mcpUrl = mcpConfig?.mcpServers?.["jfl-context"]?.env?.CONTEXT_HUB_URL
58
+ if (typeof mcpUrl === "string" && mcpUrl.trim()) {
59
+ return mcpUrl.trim()
60
+ }
61
+
62
+ return `http://localhost:${computePortFromPath(root)}`
63
+ }
@@ -319,9 +319,22 @@ export default async function jflExtension(pi: any): Promise<void> {
319
319
 
320
320
  if (config.pi?.auto_start !== false && pi.sendUserMessage) {
321
321
  setTimeout(() => {
322
- pi.sendUserMessage(
323
- `JFL session started in "${projectName}". Use the jfl_context tool to read recent project context, then show a brief status update with current focus and any blocking issues.`
324
- )
322
+ pi.sendUserMessage([
323
+ `JFL session started in "${projectName}" on branch ${ctx.session.branch}.`,
324
+ "",
325
+ "Complete these steps before responding to the user:",
326
+ "1. Use jfl_context to get recent project context (journal, knowledge, decisions)",
327
+ "2. Use jfl_memory_search to check for any recent decisions or blockers",
328
+ "3. Use jfl_hud to get the project dashboard (timeline, phase, pipeline)",
329
+ "4. Show a brief status update with:",
330
+ " - What was worked on recently (from journal)",
331
+ " - Current phase and focus",
332
+ " - Any blocking issues or warnings",
333
+ " - Suggested next action",
334
+ "",
335
+ "Follow the CLAUDE.md instructions injected in your system prompt.",
336
+ "Write journal entries as you work. Capture decisions immediately.",
337
+ ].join("\n"))
325
338
  }, 500)
326
339
  }
327
340
  })
@@ -40,11 +40,16 @@ export function setupMemoryTool(ctx: PiContext): void {
40
40
  const { query, limit, type } = input as { query: string; limit?: number; type?: string }
41
41
 
42
42
  try {
43
- const params = new URLSearchParams({ query, limit: String(limit ?? 10) })
44
- if (type && type !== "all") params.set("type", type)
43
+ const body: Record<string, unknown> = { query, maxItems: limit ?? 10 }
44
+ if (type && type !== "all") body.type = type
45
45
 
46
- const resp = await fetch(`${hubUrl}/api/memory/search?${params}`, {
47
- headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},
46
+ const resp = await fetch(`${hubUrl}/api/memory/search`, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
51
+ },
52
+ body: JSON.stringify(body),
48
53
  })
49
54
 
50
55
  if (!resp.ok) return "Memory search unavailable."
@@ -1,20 +1,22 @@
1
1
  /**
2
2
  * JFL Session Extension
3
3
  *
4
- * Handles auto-commit and session cleanup within a Pi session.
5
- * Does NOT run session-init.sh that script is a Claude Code pre-hook that
6
- * launches the AI CLI, which would cause a double-launch inside Pi.
7
- * Pi IS the AI runtime; we only handle the persistent background tasks here.
4
+ * Pi-native session lifecycle via Context Hub API.
5
+ * Calls POST /api/session/init on start and POST /api/session/end on shutdown.
6
+ * Hub handles sync, branch creation, doctor single source of truth.
7
+ * Pi only manages the auto-commit daemon locally (needs a detached process).
8
8
  *
9
- * @purpose Pi session lifecycle — auto-commit daemon + cleanup script delegation
9
+ * @purpose Pi session lifecycle — Hub API for init/end, local auto-commit daemon
10
10
  */
11
11
 
12
12
  import { execSync, spawn } from "child_process"
13
13
  import { existsSync, readFileSync } from "fs"
14
14
  import { join } from "path"
15
15
  import type { PiContext, JflConfig } from "./types.js"
16
+ import { hubUrl, authToken } from "./map-bridge.js"
16
17
 
17
18
  let autoCommitProcess: ReturnType<typeof spawn> | null = null
19
+ let sessionBranch = ""
18
20
 
19
21
  function findScript(root: string, scriptName: string): string | null {
20
22
  const candidates = [
@@ -30,7 +32,39 @@ function findScript(root: string, scriptName: string): string | null {
30
32
  export async function setupSession(ctx: PiContext, _config: JflConfig): Promise<void> {
31
33
  const root = ctx.session.projectRoot
32
34
 
33
- // Start auto-commit daemondetached so it outlives this call
35
+ // Call Hub session init handles sync, doctor, branch creation
36
+ try {
37
+ const resp = await fetch(`${hubUrl}/api/session/init`, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
42
+ },
43
+ body: JSON.stringify({ runtime: "pi" }),
44
+ signal: AbortSignal.timeout(90000),
45
+ })
46
+
47
+ if (resp.ok) {
48
+ const data = await resp.json() as { branch?: string; syncOk?: boolean; warnings?: string[]; doctor?: { errors: number; warnings: number } }
49
+ sessionBranch = data.branch ?? ctx.session.branch
50
+ if (data.warnings?.length) {
51
+ for (const w of data.warnings) ctx.log(w, "warn")
52
+ }
53
+ if (data.doctor?.errors) {
54
+ ctx.ui.notify(`Doctor: ${data.doctor.errors} errors`, { level: "warn" })
55
+ }
56
+ ctx.log(`Session init via Hub: branch=${sessionBranch}, sync=${data.syncOk}`, "debug")
57
+ } else {
58
+ ctx.log("Hub session init failed, falling back to local", "warn")
59
+ sessionBranch = ctx.session.branch
60
+ }
61
+ } catch {
62
+ // Hub not available — fall back to local branch detection
63
+ ctx.log("Hub unavailable for session init, using local branch", "debug")
64
+ sessionBranch = ctx.session.branch
65
+ }
66
+
67
+ // Start auto-commit daemon — must be local (detached process)
34
68
  const autoCommitScript = findScript(root, "auto-commit.sh")
35
69
  if (autoCommitScript) {
36
70
  autoCommitProcess = spawn("bash", [autoCommitScript, "start", "120"], {
@@ -44,28 +78,46 @@ export async function setupSession(ctx: PiContext, _config: JflConfig): Promise<
44
78
 
45
79
  ctx.emit("hook:session-start", {
46
80
  session: ctx.session.id,
47
- branch: ctx.session.branch,
81
+ branch: sessionBranch,
48
82
  projectRoot: root,
49
83
  ts: new Date().toISOString(),
50
84
  })
51
85
  }
52
86
 
87
+ export function getSessionBranch(): string {
88
+ return sessionBranch
89
+ }
90
+
53
91
  export async function onShutdown(ctx: PiContext): Promise<void> {
54
92
  const root = ctx.session.projectRoot
55
93
 
94
+ // Kill auto-commit daemon
56
95
  if (autoCommitProcess) {
57
- try {
58
- autoCommitProcess.kill()
59
- } catch {}
96
+ try { autoCommitProcess.kill() } catch {}
60
97
  autoCommitProcess = null
61
98
  }
62
99
 
63
- const cleanupScript = findScript(root, "session-cleanup.sh")
64
- if (cleanupScript) {
65
- try {
66
- execSync(`bash "${cleanupScript}"`, { cwd: root, stdio: "inherit" })
67
- } catch (err) {
68
- ctx.log(`session-cleanup.sh failed: ${err}`, "warn")
100
+ // Call Hub session end — handles journal check + cleanup
101
+ try {
102
+ await fetch(`${hubUrl}/api/session/end`, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
107
+ },
108
+ body: JSON.stringify({ runtime: "pi" }),
109
+ signal: AbortSignal.timeout(90000),
110
+ })
111
+ ctx.log("Session ended via Hub", "debug")
112
+ } catch {
113
+ // Fallback: run cleanup script directly
114
+ const cleanupScript = findScript(root, "session-cleanup.sh")
115
+ if (cleanupScript) {
116
+ try {
117
+ execSync(`bash "${cleanupScript}"`, { cwd: root, stdio: "inherit" })
118
+ } catch (err) {
119
+ ctx.log(`session-cleanup.sh failed: ${err}`, "warn")
120
+ }
69
121
  }
70
122
  }
71
123
 
@@ -12,10 +12,24 @@ import type { PiTheme } from "./types.js"
12
12
 
13
13
  // ─── Shared helpers ──────────────────────────────────────────────────────────
14
14
 
15
+ function visibleLen(text: string): number {
16
+ return text.replace(/\x1b\[[0-9;]*m/g, "").length
17
+ }
18
+
15
19
  function truncLine(text: string, maxW: number): string {
16
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, "")
17
- if (stripped.length <= maxW) return text
18
- return text.slice(0, maxW - 1) + "…"
20
+ if (visibleLen(text) <= maxW) return text
21
+ // Walk char-by-char, skipping ANSI sequences, until we hit maxW visible chars
22
+ let visible = 0
23
+ let i = 0
24
+ while (i < text.length && visible < maxW - 1) {
25
+ if (text[i] === "\x1b" && text[i + 1] === "[") {
26
+ const end = text.indexOf("m", i)
27
+ if (end !== -1) { i = end + 1; continue }
28
+ }
29
+ visible++
30
+ i++
31
+ }
32
+ return text.slice(0, i) + "\x1b[0m…"
19
33
  }
20
34
 
21
35
  function wrapText(text: string, width: number): string[] {
@@ -73,8 +87,9 @@ export function contextRenderCall(args: Record<string, any>, theme: PiTheme): an
73
87
  }
74
88
 
75
89
  export function contextRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
90
+ const MAX_W = 140
76
91
  const raw = extractText(result)
77
- if (raw === "No relevant context found.") {
92
+ if (raw === "No relevant context found." || raw === "No context available.") {
78
93
  return { render: () => [theme.fg("dim", "No relevant context found")], invalidate() {} }
79
94
  }
80
95
 
@@ -90,17 +105,17 @@ export function contextRenderResult(result: any, opts: { expanded: boolean }, th
90
105
  const typeMatch = firstLine.match(/^\[(\w+)\]\s*(.*)/)
91
106
  if (typeMatch) {
92
107
  const typeColor = typeMatch[1] === "decision" ? "warning" : typeMatch[1] === "feature" ? "success" : "muted"
93
- lines.push(`${theme.fg(typeColor, `[${typeMatch[1]}]`)} ${theme.fg("text", typeMatch[2])}`)
108
+ lines.push(truncLine(`${theme.fg(typeColor, `[${typeMatch[1]}]`)} ${theme.fg("text", typeMatch[2])}`, MAX_W))
94
109
  } else {
95
- lines.push(theme.fg("text", firstLine))
110
+ lines.push(truncLine(theme.fg("text", firstLine), MAX_W))
96
111
  }
97
112
  } else {
98
- lines.push(theme.fg("text", firstLine))
113
+ lines.push(truncLine(theme.fg("text", firstLine), MAX_W))
99
114
  }
100
115
 
101
116
  if (opts.expanded) {
102
117
  const rest = section.split("\n").slice(1)
103
- for (const l of rest) lines.push(theme.fg("muted", l))
118
+ for (const l of rest) lines.push(truncLine(theme.fg("muted", l), MAX_W))
104
119
  lines.push("")
105
120
  }
106
121
  }
@@ -0,0 +1,5 @@
1
+ torch>=2.0
2
+ numpy>=1.24
3
+ requests>=2.28
4
+ scikit-learn>=1.3
5
+ tqdm>=4.65