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.
Files changed (71) hide show
  1. package/dist/commands/context-hub.d.ts.map +1 -1
  2. package/dist/commands/context-hub.js +23 -1
  3. package/dist/commands/context-hub.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +6 -0
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/peter.d.ts.map +1 -1
  8. package/dist/commands/peter.js +11 -15
  9. package/dist/commands/peter.js.map +1 -1
  10. package/dist/commands/pivot.d.ts.map +1 -1
  11. package/dist/commands/pivot.js +22 -25
  12. package/dist/commands/pivot.js.map +1 -1
  13. package/dist/commands/repair.d.ts.map +1 -1
  14. package/dist/commands/repair.js +26 -0
  15. package/dist/commands/repair.js.map +1 -1
  16. package/dist/commands/session.d.ts.map +1 -1
  17. package/dist/commands/session.js +39 -0
  18. package/dist/commands/session.js.map +1 -1
  19. package/dist/commands/start.d.ts.map +1 -1
  20. package/dist/commands/start.js +60 -0
  21. package/dist/commands/start.js.map +1 -1
  22. package/dist/commands/update.d.ts.map +1 -1
  23. package/dist/commands/update.js +3 -1
  24. package/dist/commands/update.js.map +1 -1
  25. package/dist/lib/agent-session.d.ts.map +1 -1
  26. package/dist/lib/agent-session.js +6 -3
  27. package/dist/lib/agent-session.js.map +1 -1
  28. package/dist/lib/gtm-generator.js +7 -0
  29. package/dist/lib/gtm-generator.js.map +1 -1
  30. package/dist/lib/memory-db.d.ts +8 -0
  31. package/dist/lib/memory-db.d.ts.map +1 -1
  32. package/dist/lib/memory-db.js +24 -0
  33. package/dist/lib/memory-db.js.map +1 -1
  34. package/dist/lib/memory-indexer.d.ts +8 -0
  35. package/dist/lib/memory-indexer.d.ts.map +1 -1
  36. package/dist/lib/memory-indexer.js +30 -1
  37. package/dist/lib/memory-indexer.js.map +1 -1
  38. package/dist/lib/memory-search.d.ts.map +1 -1
  39. package/dist/lib/memory-search.js +2 -7
  40. package/dist/lib/memory-search.js.map +1 -1
  41. package/dist/lib/service-detector.js +2 -2
  42. package/dist/lib/service-detector.js.map +1 -1
  43. package/dist/lib/telemetry/physical-world-collector.js +1 -1
  44. package/dist/lib/telemetry/physical-world-collector.js.map +1 -1
  45. package/dist/utils/git.d.ts +1 -1
  46. package/dist/utils/git.d.ts.map +1 -1
  47. package/dist/utils/git.js +9 -6
  48. package/dist/utils/git.js.map +1 -1
  49. package/dist/utils/provenance.d.ts +65 -0
  50. package/dist/utils/provenance.d.ts.map +1 -0
  51. package/dist/utils/provenance.js +213 -0
  52. package/dist/utils/provenance.js.map +1 -0
  53. package/package.json +1 -1
  54. package/packages/pi/extensions/context.ts +11 -0
  55. package/packages/pi/extensions/header.ts +171 -0
  56. package/packages/pi/extensions/hud-tool.ts +1 -1
  57. package/packages/pi/extensions/index.ts +28 -3
  58. package/packages/pi/extensions/memory-tool.ts +3 -3
  59. package/packages/pi/extensions/onboarding-v2.ts +70 -185
  60. package/packages/pi/extensions/onboarding-v3.ts +32 -21
  61. package/packages/pi/extensions/service-skills.ts +6 -1
  62. package/packages/pi/extensions/session.ts +7 -1
  63. package/packages/pi/extensions/startup-briefing.ts +313 -0
  64. package/packages/pi/extensions/subway-mesh.ts +893 -0
  65. package/packages/pi/extensions/types.ts +1 -0
  66. package/packages/pi/package.json +1 -0
  67. package/scripts/pp-branch-pr.sh +24 -6
  68. package/scripts/pp-branch-pr.sh.bak +115 -0
  69. package/template/.pi/settings.json +5 -0
  70. package/template/CLAUDE.md +82 -1738
  71. 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
+ }