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.
- package/dist/commands/context-hub.d.ts +1 -0
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +246 -2
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/peter.d.ts +2 -0
- package/dist/commands/peter.d.ts.map +1 -1
- package/dist/commands/peter.js +242 -52
- package/dist/commands/peter.js.map +1 -1
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +322 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/train.d.ts +33 -0
- package/dist/commands/train.d.ts.map +1 -0
- package/dist/commands/train.js +510 -0
- package/dist/commands/train.js.map +1 -0
- package/dist/commands/verify.d.ts +14 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +276 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/dashboard-static/assets/index-CW9ZxqX8.css +1 -0
- package/dist/dashboard-static/assets/index-DNN__p4K.js +121 -0
- package/dist/dashboard-static/index.html +2 -2
- package/dist/index.js +99 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-session.d.ts.map +1 -1
- package/dist/lib/agent-session.js +12 -4
- package/dist/lib/agent-session.js.map +1 -1
- package/dist/lib/eval-snapshot.js +1 -1
- package/dist/lib/eval-snapshot.js.map +1 -1
- package/dist/lib/pi-sky/bridge.d.ts +55 -0
- package/dist/lib/pi-sky/bridge.d.ts.map +1 -0
- package/dist/lib/pi-sky/bridge.js +264 -0
- package/dist/lib/pi-sky/bridge.js.map +1 -0
- package/dist/lib/pi-sky/cost-monitor.d.ts +21 -0
- package/dist/lib/pi-sky/cost-monitor.d.ts.map +1 -0
- package/dist/lib/pi-sky/cost-monitor.js +126 -0
- package/dist/lib/pi-sky/cost-monitor.js.map +1 -0
- package/dist/lib/pi-sky/eval-sweep.d.ts +27 -0
- package/dist/lib/pi-sky/eval-sweep.d.ts.map +1 -0
- package/dist/lib/pi-sky/eval-sweep.js +141 -0
- package/dist/lib/pi-sky/eval-sweep.js.map +1 -0
- package/dist/lib/pi-sky/event-router.d.ts +32 -0
- package/dist/lib/pi-sky/event-router.d.ts.map +1 -0
- package/dist/lib/pi-sky/event-router.js +176 -0
- package/dist/lib/pi-sky/event-router.js.map +1 -0
- package/dist/lib/pi-sky/experiment.d.ts +9 -0
- package/dist/lib/pi-sky/experiment.d.ts.map +1 -0
- package/dist/lib/pi-sky/experiment.js +83 -0
- package/dist/lib/pi-sky/experiment.js.map +1 -0
- package/dist/lib/pi-sky/index.d.ts +16 -0
- package/dist/lib/pi-sky/index.d.ts.map +1 -0
- package/dist/lib/pi-sky/index.js +16 -0
- package/dist/lib/pi-sky/index.js.map +1 -0
- package/dist/lib/pi-sky/stratus-gate.d.ts +28 -0
- package/dist/lib/pi-sky/stratus-gate.d.ts.map +1 -0
- package/dist/lib/pi-sky/stratus-gate.js +61 -0
- package/dist/lib/pi-sky/stratus-gate.js.map +1 -0
- package/dist/lib/pi-sky/swarm.d.ts +28 -0
- package/dist/lib/pi-sky/swarm.d.ts.map +1 -0
- package/dist/lib/pi-sky/swarm.js +208 -0
- package/dist/lib/pi-sky/swarm.js.map +1 -0
- package/dist/lib/pi-sky/types.d.ts +139 -0
- package/dist/lib/pi-sky/types.d.ts.map +1 -0
- package/dist/lib/pi-sky/types.js +2 -0
- package/dist/lib/pi-sky/types.js.map +1 -0
- package/dist/lib/pi-sky/voice-bridge.d.ts +20 -0
- package/dist/lib/pi-sky/voice-bridge.d.ts.map +1 -0
- package/dist/lib/pi-sky/voice-bridge.js +91 -0
- package/dist/lib/pi-sky/voice-bridge.js.map +1 -0
- package/dist/lib/policy-head.d.ts +16 -1
- package/dist/lib/policy-head.d.ts.map +1 -1
- package/dist/lib/policy-head.js +117 -19
- package/dist/lib/policy-head.js.map +1 -1
- package/dist/lib/predictor.d.ts +10 -0
- package/dist/lib/predictor.d.ts.map +1 -1
- package/dist/lib/predictor.js +46 -7
- package/dist/lib/predictor.js.map +1 -1
- package/dist/lib/setup/agent-generator.d.ts +18 -0
- package/dist/lib/setup/agent-generator.d.ts.map +1 -0
- package/dist/lib/setup/agent-generator.js +114 -0
- package/dist/lib/setup/agent-generator.js.map +1 -0
- package/dist/lib/setup/context-analyzer.d.ts +16 -0
- package/dist/lib/setup/context-analyzer.d.ts.map +1 -0
- package/dist/lib/setup/context-analyzer.js +112 -0
- package/dist/lib/setup/context-analyzer.js.map +1 -0
- package/dist/lib/setup/doc-auditor.d.ts +54 -0
- package/dist/lib/setup/doc-auditor.d.ts.map +1 -0
- package/dist/lib/setup/doc-auditor.js +629 -0
- package/dist/lib/setup/doc-auditor.js.map +1 -0
- package/dist/lib/setup/domain-generator.d.ts +7 -0
- package/dist/lib/setup/domain-generator.d.ts.map +1 -0
- package/dist/lib/setup/domain-generator.js +58 -0
- package/dist/lib/setup/domain-generator.js.map +1 -0
- package/dist/lib/setup/smart-eval-generator.d.ts +38 -0
- package/dist/lib/setup/smart-eval-generator.d.ts.map +1 -0
- package/dist/lib/setup/smart-eval-generator.js +378 -0
- package/dist/lib/setup/smart-eval-generator.js.map +1 -0
- package/dist/lib/setup/smart-recommender.d.ts +63 -0
- package/dist/lib/setup/smart-recommender.d.ts.map +1 -0
- package/dist/lib/setup/smart-recommender.js +329 -0
- package/dist/lib/setup/smart-recommender.js.map +1 -0
- package/dist/lib/setup/spec-generator.d.ts +63 -0
- package/dist/lib/setup/spec-generator.d.ts.map +1 -0
- package/dist/lib/setup/spec-generator.js +310 -0
- package/dist/lib/setup/spec-generator.js.map +1 -0
- package/dist/lib/setup/violation-agent-generator.d.ts +32 -0
- package/dist/lib/setup/violation-agent-generator.d.ts.map +1 -0
- package/dist/lib/setup/violation-agent-generator.js +255 -0
- package/dist/lib/setup/violation-agent-generator.js.map +1 -0
- package/package.json +1 -1
- package/packages/pi/extensions/context.ts +88 -55
- package/packages/pi/extensions/hub-resolver.ts +63 -0
- package/packages/pi/extensions/index.ts +16 -3
- package/packages/pi/extensions/memory-tool.ts +9 -4
- package/packages/pi/extensions/session.ts +68 -16
- package/packages/pi/extensions/tool-renderers.ts +23 -8
- package/scripts/train/requirements.txt +5 -0
- package/scripts/train/train-policy-head.py +477 -0
- package/scripts/train/v2/dataset.py +81 -0
- package/scripts/train/v2/domain.json +18 -0
- package/scripts/train/v2/eval.py +196 -0
- package/scripts/train/v2/generate_data.py +219 -0
- package/scripts/train/v2/infer.py +188 -0
- package/scripts/train/v2/model.py +112 -0
- package/scripts/train/v2/precompute.py +132 -0
- package/scripts/train/v2/train.py +302 -0
- package/scripts/train/v2/transform_buffer.py +227 -0
- package/scripts/train/v2/validate_data.py +115 -0
- package/template/.claude/settings.json +2 -15
- package/template/scripts/session/session-cleanup.sh +2 -11
- package/template/scripts/session/session-end-hub.sh +72 -0
- package/template/scripts/session/session-start-hub.sh +105 -0
- package/dist/dashboard-static/assets/index-B6b867Pv.js +0 -121
- 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
|
|
5
|
-
* and registers the jfl_context tool
|
|
6
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
64
|
-
|
|
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 =
|
|
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: "
|
|
111
|
-
promptSnippet: "
|
|
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: "
|
|
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:
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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}"
|
|
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
|
|
44
|
-
if (type && type !== "all")
|
|
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
|
|
47
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Pi
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
}
|