jfl 0.9.2 → 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.
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +23 -1
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/peter.d.ts.map +1 -1
- package/dist/commands/peter.js +11 -15
- package/dist/commands/peter.js.map +1 -1
- package/dist/commands/pivot.d.ts.map +1 -1
- package/dist/commands/pivot.js +22 -25
- package/dist/commands/pivot.js.map +1 -1
- package/dist/commands/repair.d.ts.map +1 -1
- package/dist/commands/repair.js +26 -0
- package/dist/commands/repair.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +39 -0
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +60 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +3 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/agent-session.d.ts.map +1 -1
- package/dist/lib/agent-session.js +6 -3
- package/dist/lib/agent-session.js.map +1 -1
- package/dist/lib/gtm-generator.js +7 -0
- package/dist/lib/gtm-generator.js.map +1 -1
- package/dist/lib/memory-db.d.ts +8 -0
- package/dist/lib/memory-db.d.ts.map +1 -1
- package/dist/lib/memory-db.js +24 -0
- package/dist/lib/memory-db.js.map +1 -1
- package/dist/lib/memory-indexer.d.ts +8 -0
- package/dist/lib/memory-indexer.d.ts.map +1 -1
- package/dist/lib/memory-indexer.js +30 -1
- package/dist/lib/memory-indexer.js.map +1 -1
- package/dist/lib/memory-search.d.ts.map +1 -1
- package/dist/lib/memory-search.js +2 -7
- package/dist/lib/memory-search.js.map +1 -1
- package/dist/lib/service-detector.js +2 -2
- package/dist/lib/service-detector.js.map +1 -1
- package/dist/lib/telemetry/physical-world-collector.js +1 -1
- package/dist/lib/telemetry/physical-world-collector.js.map +1 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +9 -6
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/provenance.d.ts +65 -0
- package/dist/utils/provenance.d.ts.map +1 -0
- package/dist/utils/provenance.js +213 -0
- package/dist/utils/provenance.js.map +1 -0
- package/package.json +1 -1
- package/packages/pi/extensions/context.ts +11 -0
- package/packages/pi/extensions/header.ts +171 -0
- package/packages/pi/extensions/hud-tool.ts +1 -1
- package/packages/pi/extensions/index.ts +28 -3
- package/packages/pi/extensions/memory-tool.ts +3 -3
- package/packages/pi/extensions/onboarding-v2.ts +70 -185
- package/packages/pi/extensions/onboarding-v3.ts +32 -21
- package/packages/pi/extensions/service-skills.ts +6 -1
- package/packages/pi/extensions/session.ts +7 -1
- package/packages/pi/extensions/startup-briefing.ts +313 -0
- package/packages/pi/extensions/subway-mesh.ts +893 -0
- package/packages/pi/extensions/types.ts +1 -0
- package/packages/pi/package.json +1 -0
- package/scripts/pp-branch-pr.sh +24 -6
- package/scripts/pp-branch-pr.sh.bak +115 -0
- package/template/.pi/settings.json +5 -0
- package/template/CLAUDE.md +82 -1738
- package/template/CLAUDE.md.bak +0 -1187
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup Briefing
|
|
3
|
+
*
|
|
4
|
+
* Gathers real data from synopsis, HUD, journal, git, and GitHub PRs
|
|
5
|
+
* to build a rich steer message on session start. The model uses this
|
|
6
|
+
* to produce a concise "here's where things stand" greeting.
|
|
7
|
+
*
|
|
8
|
+
* @purpose Startup steer message with real project data — synopsis, PRs, team activity
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from "fs"
|
|
12
|
+
import { join } from "path"
|
|
13
|
+
import { execSync } from "child_process"
|
|
14
|
+
import type { PiContext, JflConfig } from "./types.js"
|
|
15
|
+
import { getHudForSteer } from "./hud-tool.js"
|
|
16
|
+
|
|
17
|
+
// ─── Data gatherers (all async-safe, fail gracefully) ────────────────────────
|
|
18
|
+
|
|
19
|
+
function getSynopsis(root: string, hours: number = 24): string {
|
|
20
|
+
try {
|
|
21
|
+
const result = execSync(`jfl synopsis ${hours}`, {
|
|
22
|
+
cwd: root,
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
26
|
+
})
|
|
27
|
+
return result.trim()
|
|
28
|
+
} catch {
|
|
29
|
+
return ""
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getRecentJournalEntries(root: string, count: number = 8): Array<{
|
|
34
|
+
title: string
|
|
35
|
+
type: string
|
|
36
|
+
ts: string
|
|
37
|
+
session: string
|
|
38
|
+
summary?: string
|
|
39
|
+
}> {
|
|
40
|
+
const journalDir = join(root, ".jfl", "journal")
|
|
41
|
+
if (!existsSync(journalDir)) return []
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort()
|
|
45
|
+
const entries: Array<{ title: string; type: string; ts: string; session: string; summary?: string }> = []
|
|
46
|
+
|
|
47
|
+
for (let i = files.length - 1; i >= 0 && entries.length < count * 2; i--) {
|
|
48
|
+
try {
|
|
49
|
+
const lines = readFileSync(join(journalDir, files[i]), "utf-8").trim().split("\n").filter(Boolean)
|
|
50
|
+
for (let j = lines.length - 1; j >= 0 && entries.length < count * 2; j--) {
|
|
51
|
+
try {
|
|
52
|
+
const e = JSON.parse(lines[j])
|
|
53
|
+
if (e.title && e.type !== "pivot") {
|
|
54
|
+
entries.push({
|
|
55
|
+
title: e.title,
|
|
56
|
+
type: e.type ?? "note",
|
|
57
|
+
ts: e.ts ?? "",
|
|
58
|
+
session: e.session ?? files[i].replace(".jsonl", ""),
|
|
59
|
+
summary: e.summary,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return entries.slice(0, count)
|
|
68
|
+
} catch {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getTeamActivity(root: string, hours: number = 48): string[] {
|
|
74
|
+
const activity: string[] = []
|
|
75
|
+
|
|
76
|
+
// Git commits by others (non-goose authors in last 48h)
|
|
77
|
+
try {
|
|
78
|
+
const log = execSync(
|
|
79
|
+
`git log --oneline --since="${hours} hours ago" --format="%an|||%s" --all`,
|
|
80
|
+
{ cwd: root, timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
81
|
+
)
|
|
82
|
+
const lines = log.trim().split("\n").filter(Boolean)
|
|
83
|
+
const byAuthor = new Map<string, string[]>()
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const [author, ...msgParts] = line.split("|||")
|
|
87
|
+
const msg = msgParts.join("|||")
|
|
88
|
+
if (!author || !msg) continue
|
|
89
|
+
// Skip auto-save commits
|
|
90
|
+
if (msg.startsWith("auto:")) continue
|
|
91
|
+
const list = byAuthor.get(author) || []
|
|
92
|
+
list.push(msg)
|
|
93
|
+
byAuthor.set(author, list)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const [author, commits] of byAuthor) {
|
|
97
|
+
if (commits.length <= 2) {
|
|
98
|
+
activity.push(`${author}: ${commits.join("; ")}`)
|
|
99
|
+
} else {
|
|
100
|
+
activity.push(`${author}: ${commits.length} commits — ${commits.slice(0, 3).join("; ")}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
|
|
105
|
+
return activity
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface PullRequest {
|
|
109
|
+
number: number
|
|
110
|
+
title: string
|
|
111
|
+
author: string
|
|
112
|
+
state: string
|
|
113
|
+
createdAt?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getOpenPRs(root: string): PullRequest[] {
|
|
117
|
+
try {
|
|
118
|
+
const result = execSync(
|
|
119
|
+
"gh pr list --limit 10 --json number,title,author,state,createdAt",
|
|
120
|
+
{ cwd: root, timeout: 10000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
121
|
+
)
|
|
122
|
+
const prs = JSON.parse(result) as Array<{
|
|
123
|
+
number: number
|
|
124
|
+
title: string
|
|
125
|
+
author: { login: string; name?: string }
|
|
126
|
+
state: string
|
|
127
|
+
createdAt?: string
|
|
128
|
+
}>
|
|
129
|
+
return prs.map(pr => ({
|
|
130
|
+
number: pr.number,
|
|
131
|
+
title: pr.title.length > 80 ? pr.title.slice(0, 77) + "…" : pr.title,
|
|
132
|
+
author: pr.author.name || pr.author.login,
|
|
133
|
+
state: pr.state,
|
|
134
|
+
createdAt: pr.createdAt,
|
|
135
|
+
}))
|
|
136
|
+
} catch {
|
|
137
|
+
return []
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getNextActions(root: string): string[] {
|
|
142
|
+
const actions: string[] = []
|
|
143
|
+
|
|
144
|
+
// Check journal "next" fields from recent entries
|
|
145
|
+
const journalDir = join(root, ".jfl", "journal")
|
|
146
|
+
if (existsSync(journalDir)) {
|
|
147
|
+
try {
|
|
148
|
+
const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort()
|
|
149
|
+
for (let i = files.length - 1; i >= 0 && actions.length < 3; i--) {
|
|
150
|
+
try {
|
|
151
|
+
const lines = readFileSync(join(journalDir, files[i]), "utf-8").trim().split("\n").filter(Boolean)
|
|
152
|
+
for (let j = lines.length - 1; j >= 0 && actions.length < 3; j--) {
|
|
153
|
+
try {
|
|
154
|
+
const e = JSON.parse(lines[j])
|
|
155
|
+
if (e.next && typeof e.next === "string" && e.next.trim()) {
|
|
156
|
+
actions.push(e.next.trim())
|
|
157
|
+
} else if (Array.isArray(e.incomplete) && e.incomplete.length > 0) {
|
|
158
|
+
actions.push(`Incomplete: ${e.incomplete.slice(0, 2).join(", ")}`)
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return actions
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Cross-repo awareness ────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function getRegisteredServices(root: string): Array<{ name: string; path: string }> {
|
|
173
|
+
const configPath = join(root, ".jfl", "config.json")
|
|
174
|
+
if (!existsSync(configPath)) return []
|
|
175
|
+
try {
|
|
176
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
177
|
+
return (config.registered_services || [])
|
|
178
|
+
.filter((s: any) => s.path && existsSync(s.path))
|
|
179
|
+
.map((s: any) => ({ name: s.name, path: s.path }))
|
|
180
|
+
} catch {
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getCrossRepoPRs(root: string): PullRequest[] {
|
|
186
|
+
const services = getRegisteredServices(root)
|
|
187
|
+
const allPRs: PullRequest[] = []
|
|
188
|
+
|
|
189
|
+
for (const svc of services) {
|
|
190
|
+
try {
|
|
191
|
+
const prs = getOpenPRs(svc.path)
|
|
192
|
+
for (const pr of prs) {
|
|
193
|
+
allPRs.push({ ...pr, title: `[${svc.name}] ${pr.title}` })
|
|
194
|
+
}
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return allPRs
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Build the briefing ──────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export async function buildStartupBriefing(ctx: PiContext, config: JflConfig): Promise<string> {
|
|
204
|
+
const root = ctx.session.projectRoot
|
|
205
|
+
const sections: string[] = []
|
|
206
|
+
|
|
207
|
+
// 1. HUD — compact project status
|
|
208
|
+
const hud = getHudForSteer()
|
|
209
|
+
if (hud) {
|
|
210
|
+
sections.push("## Project Dashboard\n" + hud)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 2. Synopsis — recent work summary
|
|
214
|
+
const synopsis = getSynopsis(root, 48)
|
|
215
|
+
if (synopsis) {
|
|
216
|
+
sections.push("## Recent Work (48h)\n" + synopsis)
|
|
217
|
+
} else {
|
|
218
|
+
// Fallback to journal entries
|
|
219
|
+
const entries = getRecentJournalEntries(root, 6)
|
|
220
|
+
if (entries.length > 0) {
|
|
221
|
+
const entryLines = entries.map(e => {
|
|
222
|
+
const timeAgo = getTimeAgo(e.ts)
|
|
223
|
+
return `- [${e.type}] ${e.title}${timeAgo ? ` (${timeAgo})` : ""}${e.summary ? ` — ${e.summary}` : ""}`
|
|
224
|
+
})
|
|
225
|
+
sections.push("## Recent Work\n" + entryLines.join("\n"))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. Team activity — commits by other authors
|
|
230
|
+
const teamActivity = getTeamActivity(root, 48)
|
|
231
|
+
if (teamActivity.length > 0) {
|
|
232
|
+
sections.push("## Team Activity (48h)\n" + teamActivity.map(a => `- ${a}`).join("\n"))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 4. Open PRs — this repo + registered services
|
|
236
|
+
const localPRs = getOpenPRs(root)
|
|
237
|
+
const crossPRs = getCrossRepoPRs(root)
|
|
238
|
+
const allPRs = [...localPRs, ...crossPRs]
|
|
239
|
+
if (allPRs.length > 0) {
|
|
240
|
+
const prLines = allPRs.slice(0, 8).map(pr =>
|
|
241
|
+
`- #${pr.number} ${pr.title} (${pr.author})`
|
|
242
|
+
)
|
|
243
|
+
sections.push("## Open PRs\n" + prLines.join("\n"))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 5. Next actions — from journal "next" fields
|
|
247
|
+
const nextActions = getNextActions(root)
|
|
248
|
+
if (nextActions.length > 0) {
|
|
249
|
+
const unique = [...new Set(nextActions)].slice(0, 3)
|
|
250
|
+
sections.push("## Suggested Next Actions\n" + unique.map(a => `- ${a}`).join("\n"))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return sections.join("\n\n")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getTimeAgo(ts: string): string {
|
|
257
|
+
if (!ts) return ""
|
|
258
|
+
try {
|
|
259
|
+
const diff = Date.now() - new Date(ts).getTime()
|
|
260
|
+
const hours = Math.floor(diff / 3600000)
|
|
261
|
+
if (hours < 1) return "just now"
|
|
262
|
+
if (hours < 24) return `${hours}h ago`
|
|
263
|
+
const days = Math.floor(hours / 24)
|
|
264
|
+
return `${days}d ago`
|
|
265
|
+
} catch {
|
|
266
|
+
return ""
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Fire the steer ──────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
export async function fireStartupBriefing(ctx: PiContext, config: JflConfig): Promise<void> {
|
|
273
|
+
if (config.pi?.disable_briefing) return
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const briefingData = await buildStartupBriefing(ctx, config)
|
|
277
|
+
if (!briefingData) return
|
|
278
|
+
|
|
279
|
+
const projectName = config.name || ctx.session.projectRoot.split("/").pop() || "project"
|
|
280
|
+
|
|
281
|
+
const steerContent = [
|
|
282
|
+
`JFL session ready: "${projectName}" on branch ${ctx.session.branch}.`,
|
|
283
|
+
"",
|
|
284
|
+
"Here is the current project state. Use this to greet the user with a concise briefing:",
|
|
285
|
+
"",
|
|
286
|
+
briefingData,
|
|
287
|
+
"",
|
|
288
|
+
"---",
|
|
289
|
+
"INSTRUCTIONS: Give a concise briefing (5-10 lines). Include:",
|
|
290
|
+
"1. Current phase / what's hot right now",
|
|
291
|
+
"2. Key recent wins or changes (from synopsis/journal above)",
|
|
292
|
+
"3. What others on the team have been working on (if any team activity)",
|
|
293
|
+
"4. Open PRs that need attention (if any)",
|
|
294
|
+
"5. A suggested next action based on the journal 'next' fields",
|
|
295
|
+
"",
|
|
296
|
+
"End with: What do you want to hit?",
|
|
297
|
+
"",
|
|
298
|
+
"Be direct and useful. No setup noise. No tool calls needed — everything is above.",
|
|
299
|
+
].join("\n")
|
|
300
|
+
|
|
301
|
+
// Fire immediately — called after onboarding overlay has already dismissed
|
|
302
|
+
ctx.pi.sendMessage(
|
|
303
|
+
{
|
|
304
|
+
customType: "jfl-startup-briefing",
|
|
305
|
+
content: steerContent,
|
|
306
|
+
display: false,
|
|
307
|
+
},
|
|
308
|
+
{ triggerTurn: true, deliverAs: "steer" }
|
|
309
|
+
)
|
|
310
|
+
} catch (err) {
|
|
311
|
+
ctx.log(`Startup briefing failed: ${err}`, "debug")
|
|
312
|
+
}
|
|
313
|
+
}
|