portable-agent-layer 0.21.0 → 0.23.0

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 (47) hide show
  1. package/README.md +3 -2
  2. package/assets/agents/gemini-researcher.md +17 -3
  3. package/assets/agents/grok-researcher.md +19 -5
  4. package/assets/agents/multi-perspective-researcher.md +16 -2
  5. package/assets/agents/perplexity-researcher.md +17 -3
  6. package/assets/skills/analyze-pdf/SKILL.md +1 -1
  7. package/assets/skills/analyze-youtube/SKILL.md +1 -1
  8. package/assets/skills/extract-entities/SKILL.md +1 -1
  9. package/assets/skills/fyzz-chat-api/SKILL.md +6 -6
  10. package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
  11. package/assets/skills/reflect/SKILL.md +2 -2
  12. package/assets/skills/telos/SKILL.md +6 -6
  13. package/assets/templates/AGENTS.md.template +2 -2
  14. package/assets/templates/PAL/ALGORITHM.md +139 -13
  15. package/assets/templates/PAL/CONTEXT_ROUTING.md +17 -17
  16. package/assets/templates/PAL/MEMORY_SYSTEM.md +5 -5
  17. package/assets/templates/PAL/README.md +12 -9
  18. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
  19. package/assets/templates/PAL/WORK_TRACKING.md +2 -9
  20. package/assets/templates/pal-settings.json +6 -3
  21. package/assets/templates/settings.claude.json +2 -2
  22. package/package.json +3 -1
  23. package/src/cli/index.ts +7 -14
  24. package/src/hooks/handlers/rating.ts +1 -1
  25. package/src/hooks/handlers/relationship.ts +3 -3
  26. package/src/hooks/handlers/session-intelligence.ts +324 -0
  27. package/src/hooks/handlers/session-name.ts +3 -3
  28. package/src/hooks/handlers/synthesis.ts +36 -0
  29. package/src/hooks/handlers/update-check.ts +2 -2
  30. package/src/hooks/handlers/work-learning.ts +1 -1
  31. package/src/hooks/lib/context.ts +123 -41
  32. package/src/hooks/lib/graduation.ts +1 -1
  33. package/src/hooks/lib/inference.ts +1 -1
  34. package/src/hooks/lib/paths.ts +4 -12
  35. package/src/hooks/lib/readme-sync.ts +3 -3
  36. package/src/hooks/lib/security.ts +41 -27
  37. package/src/hooks/lib/stop.ts +6 -6
  38. package/src/hooks/lib/token-usage.ts +1 -0
  39. package/src/hooks/lib/work-tracking.ts +1 -51
  40. package/src/targets/claude/install.ts +3 -1
  41. package/src/targets/cursor/install.ts +9 -1
  42. package/src/targets/cursor/uninstall.ts +7 -0
  43. package/src/targets/lib.ts +214 -111
  44. package/src/targets/opencode/install.ts +6 -4
  45. package/src/tools/agent/algorithm-reflect.ts +122 -0
  46. package/src/tools/agent/synthesize.ts +361 -0
  47. package/src/tools/agent/thread.ts +162 -0
@@ -6,18 +6,21 @@ PAL is a persistent, cross-platform, cross-agent layer for portable AI workflows
6
6
 
7
7
  **CLAUDE.md** (or the agent equivalent) is the entry point — generated from a template by the CLI installer. It defines execution modes, The Algorithm routing, and the context routing table. The agent loads it natively every session. A SessionStart hook keeps it fresh automatically.
8
8
 
9
- **The PAL home directory (`~/.agents/PAL/`)** contains all system documentation, user context (TELOS), and routing files. The rest of the system lives in the PAL package (`src/`) and the agent's config directory (`~/.claude/`, `~/.config/opencode/`, `~/.cursor/`, or `~/.codex/`).
9
+ **The PAL home directory (`~/.pal/`)** contains all system documentation, user context (TELOS), memory, and tools. The rest of the system lives in the PAL package (`src/`) and the agent's config directory (`~/.claude/`, `~/.config/opencode/`, `~/.cursor/`, or `~/.codex/`).
10
10
 
11
11
  ## Directory Structure
12
12
 
13
13
  ```
14
- ~/.agents/PAL/ # PAL home — user context + routing
15
- ALGORITHM.md # The execution engine (4-phase)
16
- CONTEXT_ROUTING.md # On-demand context routing table
17
- MEMORY_SYSTEM.md # Memory guidelines
18
- OPINION_TRACKING.md # Opinion system reference
19
- STEERING_RULES.md # Behavioral rules
20
- WORK_TRACKING.md # Work tracking reference
14
+ ~/.pal/ # PAL home
15
+ docs/ # System documentation (engine-managed)
16
+ ALGORITHM.md # The execution engine (4-phase)
17
+ CONTEXT_ROUTING.md # On-demand context routing table
18
+ MEMORY_SYSTEM.md # Memory guidelines
19
+ OPINION_TRACKING.md # Opinion system reference
20
+ STEERING_RULES.md # Behavioral rules
21
+ WORK_TRACKING.md # Work tracking reference
22
+ tools/ # Agent CLI tools (symlink → repo src/tools/agent/)
23
+ skills/ # Installed skills (symlinks → assets/skills/)
21
24
  telos/ # User life context (TELOS)
22
25
  MISSION.md, GOALS.md, PROJECTS.md, BELIEFS.md,
23
26
  CHALLENGES.md, STRATEGIES.md, IDEAS.md, LEARNED.md,
@@ -124,5 +127,5 @@ PAL is designed to work identically across:
124
127
 
125
128
  - **Add a skill:** Use the `create-skill` skill or manually create `assets/skills/<name>/SKILL.md`
126
129
  - **Add startup files:** Append to `pal-settings.json → loadAtStartup.files`
127
- - **Add user context:** Create files in `~/.agents/PAL/telos/`
130
+ - **Add user context:** Create files in `~/.pal/telos/`
128
131
  - **Toggle dynamic context:** Set keys in `pal-settings.json → dynamicContext` to `false`
@@ -447,7 +447,7 @@ All paths resolve through `src/hooks/lib/paths.ts`:
447
447
 
448
448
  | Path | Default | Override |
449
449
  |------|---------|----------|
450
- | PAL home | `~/.agents/PAL` | `PAL_HOME` |
450
+ | PAL home | `~/.pal` | `PAL_HOME` |
451
451
  | PAL package | Auto-detected from source | `PAL_PKG` |
452
452
  | Claude config | `~/.claude` | `PAL_CLAUDE_DIR` |
453
453
  | opencode config | `~/.config/opencode` | `PAL_OPENCODE_DIR` |
@@ -1,14 +1,7 @@
1
1
  # Work Tracking
2
2
 
3
- PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured) and `memory/state/projects.json` (AI-managed).
3
+ PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured).
4
4
 
5
5
  ## Projects
6
6
 
7
- Update `projects.json` via the work-tracking library when:
8
- - **Starting sustained multi-session work** → create a project with objectives and an id (slugified, e.g. "pdf-template-engine")
9
- - **Making a key decision** → add to the project's `decisions` array
10
- - **Completing a milestone** → add to `completed`, remove from `nextSteps`
11
- - **Session ends with open work** → update `nextSteps` and `handoff`
12
- - **Work is done** → set status to "completed"
13
-
14
- Do not create projects for one-off questions or quick fixes.
7
+ Projects are managed in `telos/PROJECTS.md` and force-loaded at session startup via `pal-settings.json loadAtStartup.files`.
@@ -14,8 +14,8 @@
14
14
  "loadAtStartup": {
15
15
  "_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
16
16
  "files": [
17
- "~/.agents/PAL/STEERING_RULES.md",
18
- "~/.agents/PAL/telos/PROJECTS.md"
17
+ "~/.pal/docs/STEERING_RULES.md",
18
+ "~/.pal/telos/PROJECTS.md"
19
19
  ]
20
20
  },
21
21
  "dynamicContext": {
@@ -27,6 +27,9 @@
27
27
  "synthesis": true,
28
28
  "signalTrends": true,
29
29
  "failurePatterns": true,
30
- "activeWork": true
30
+ "activeWork": true,
31
+ "projectHistory": true,
32
+ "sessionIntelligence": true,
33
+ "handoff": true
31
34
  }
32
35
  }
@@ -19,8 +19,8 @@
19
19
  "Bash(file //*)",
20
20
  "Bash(stat //*)",
21
21
  "Bash(readlink //*)",
22
- "Bash(bun ~/.agents/skills/*/tools/*.ts *)",
23
- "Bash(bun ~/.agents/PAL/tools/*.ts *)"
22
+ "Bash(bun ~/.pal/skills/*/tools/*.ts *)",
23
+ "Bash(bun ~/.pal/tools/*.ts *)"
24
24
  ]
25
25
  },
26
26
  "hooks": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,8 @@
44
44
  "prepare": "husky",
45
45
  "install:all": "bun run src/cli/index.ts cli install",
46
46
  "uninstall": "bun run src/cli/index.ts cli uninstall",
47
+ "tool:synthesize": "bun run src/tools/agent/synthesize.ts",
48
+ "tool:thread": "bun run src/tools/agent/thread.ts",
47
49
  "tool:analyze": "bun run src/tools/agent/analyze.ts",
48
50
  "tool:wisdom-frame": "bun run src/tools/agent/wisdom-frame.ts",
49
51
  "tool:reflect": "bun run src/tools/relationship-reflect.ts",
package/src/cli/index.ts CHANGED
@@ -347,7 +347,6 @@ function doctor(silent = false): DoctorResult {
347
347
  const hasAgent = claude.available || opencode.available || cursor.available;
348
348
 
349
349
  const home = palHome();
350
- const isRepo = existsSync(resolve(palPkg(), ".palroot"));
351
350
  const telosCount = (() => {
352
351
  try {
353
352
  return readdirSync(resolve(home, "telos")).filter((f) => f.endsWith(".md")).length;
@@ -373,13 +372,13 @@ function doctor(silent = false): DoctorResult {
373
372
  cursor.available
374
373
  ? ok(`Cursor ${cursor.version || ""}`.trim())
375
374
  : fail("Cursor — not found");
376
- ok(`PAL home: ${home} (${isRepo ? "repo" : "package"} mode)`);
375
+ ok(`PAL home: ${home}`);
377
376
  telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
378
377
 
379
378
  // API key checks
380
- process.env.ANTHROPIC_API_KEY
381
- ? ok("ANTHROPIC_API_KEY is set")
382
- : fail("ANTHROPIC_API_KEY — not set (hooks need it for inference)");
379
+ process.env.PAL_ANTHROPIC_API_KEY
380
+ ? ok("PAL_ANTHROPIC_API_KEY is set")
381
+ : fail("PAL_ANTHROPIC_API_KEY — not set (hooks need it for inference)");
383
382
  process.env.PAL_GEMINI_API_KEY
384
383
  ? ok("PAL_GEMINI_API_KEY is set")
385
384
  : warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
@@ -420,13 +419,9 @@ async function init(args: string[]) {
420
419
  }
421
420
 
422
421
  const home = palHome();
423
- const isRepo = existsSync(resolve(palPkg(), ".palroot"));
424
-
425
- if (!isRepo) {
426
- log.info(`Creating PAL home at ${home}`);
427
- mkdirSync(resolve(home, "telos"), { recursive: true });
428
- mkdirSync(resolve(home, "memory"), { recursive: true });
429
- }
422
+ log.info(`Creating PAL home at ${home}`);
423
+ mkdirSync(resolve(home, "telos"), { recursive: true });
424
+ mkdirSync(resolve(home, "memory"), { recursive: true });
430
425
 
431
426
  scaffoldTelos();
432
427
  ensureSetupState();
@@ -670,13 +665,11 @@ async function update() {
670
665
  async function status() {
671
666
  const home = palHome();
672
667
  const pkg = palPkg();
673
- const isRepo = existsSync(resolve(pkg, ".palroot"));
674
668
 
675
669
  const pkgJson = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8"));
676
670
 
677
671
  console.log("");
678
672
  log.info(`Version: ${pkgJson.version}`);
679
- log.info(`Mode: ${isRepo ? "repo" : "package"}`);
680
673
  log.info(`Package: ${pkg}`);
681
674
  log.info(`Home: ${home}`);
682
675
  console.log("");
@@ -358,6 +358,6 @@ export async function captureRating(message: string, sessionId?: string): Promis
358
358
  return;
359
359
  }
360
360
 
361
- // Path 2: Implicit sentiment (requires ANTHROPIC_API_KEY — inference silently no-ops without it)
361
+ // Path 2: Implicit sentiment (requires PAL_ANTHROPIC_API_KEY — inference silently no-ops without it)
362
362
  await handleImplicitSentiment(cleaned, sessionId);
363
363
  }
@@ -52,8 +52,8 @@ export async function captureRelationship(
52
52
  return;
53
53
  }
54
54
 
55
- if (!process.env.ANTHROPIC_API_KEY) {
56
- logDebug("relationship", "Skipped: no ANTHROPIC_API_KEY");
55
+ if (!process.env.PAL_ANTHROPIC_API_KEY) {
56
+ logDebug("relationship", "Skipped: no PAL_ANTHROPIC_API_KEY");
57
57
  return;
58
58
  }
59
59
 
@@ -74,7 +74,7 @@ export async function captureRelationship(
74
74
  logDebug("relationship", "Calling inference...");
75
75
  const result = await inference({
76
76
  system:
77
- "You analyze messages from an AI coding session to extract relationship observations. " +
77
+ "You analyze messages from an AI assistant session to extract relationship observations. " +
78
78
  "Types: O=opinions/preferences (how the user likes to work, what they want), " +
79
79
  "B=biographical (what the AI accomplished this session, written in first-person), " +
80
80
  "W=world facts (user's situation, projects, tools they use). " +
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Stop handler: unified session intelligence capture.
3
+ *
4
+ * Merges work-learning + relationship + handoff into a single Haiku call.
5
+ * Produces: title, summary, insights, handoff, relationship observations.
6
+ * Writes: session learning file, project history, relationship notes, last-handoff.
7
+ *
8
+ * Replaces: work-learning.ts + relationship.ts (both still exist but are bypassed).
9
+ */
10
+
11
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import { stringify } from "../lib/frontmatter";
14
+ import { inference } from "../lib/inference";
15
+ import { categorizeLearning } from "../lib/learning-category";
16
+ import { logDebug, logError } from "../lib/log";
17
+ import { ensureDir, paths } from "../lib/paths";
18
+ import { appendNotes, hasSessionNotes, type RelationshipNote } from "../lib/relationship";
19
+ import { fileTimestamp, monthPath } from "../lib/time";
20
+ import { logTokenUsage } from "../lib/token-usage";
21
+ import {
22
+ extractContent,
23
+ extractLastAssistant,
24
+ extractLastUser,
25
+ parseMessages,
26
+ } from "../lib/transcript";
27
+ import { appendProjectHistory, detectStatus } from "../lib/work-tracking";
28
+
29
+ // ── Dedup tracking (same as work-learning) ──
30
+
31
+ interface CaptureEntry {
32
+ filepath: string;
33
+ messageCount: number;
34
+ }
35
+
36
+ const MIN_NEW_MESSAGES = 10;
37
+
38
+ function capturedPath(): string {
39
+ return resolve(paths.state(), "captured-learnings.json");
40
+ }
41
+
42
+ function getPreviousCapture(sessionId: string): CaptureEntry | null {
43
+ const p = capturedPath();
44
+ if (!existsSync(p)) return null;
45
+ try {
46
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
47
+ if (Array.isArray(raw)) return null;
48
+ const entry = raw[sessionId];
49
+ if (!entry) return null;
50
+ if (typeof entry === "string") return { filepath: entry, messageCount: 0 };
51
+ return entry as CaptureEntry;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function markCaptured(sessionId: string, filepath: string, messageCount: number): void {
58
+ const p = capturedPath();
59
+ let data: Record<string, CaptureEntry> = {};
60
+ try {
61
+ if (existsSync(p)) {
62
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
63
+ if (!Array.isArray(raw) && typeof raw === "object") {
64
+ for (const [k, v] of Object.entries(raw)) {
65
+ data[k] =
66
+ typeof v === "string"
67
+ ? { filepath: v, messageCount: 0 }
68
+ : (v as CaptureEntry);
69
+ }
70
+ }
71
+ }
72
+ } catch {
73
+ /* start fresh */
74
+ }
75
+ data[sessionId] = { filepath, messageCount };
76
+ const entries = Object.entries(data);
77
+ if (entries.length > 50) data = Object.fromEntries(entries.slice(-50));
78
+ writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
79
+ }
80
+
81
+ function slugify(text: string): string {
82
+ return text
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9\s]/g, "")
85
+ .trim()
86
+ .split(/\s+/)
87
+ .slice(0, 4)
88
+ .join("-");
89
+ }
90
+
91
+ // ── JSON schema for merged Haiku call ──
92
+
93
+ const INTELLIGENCE_SCHEMA = {
94
+ type: "object" as const,
95
+ additionalProperties: false,
96
+ properties: {
97
+ title: { type: "string" as const, description: "Short session title, 5-10 words" },
98
+ summary: {
99
+ type: "string" as const,
100
+ description:
101
+ "What the AI did for the user, 2-4 sentences, AI perspective using 'we'",
102
+ },
103
+ insights: {
104
+ type: "string" as const,
105
+ description:
106
+ "What worked, what was surprising, what to do differently, 2-3 bullet points",
107
+ },
108
+ handoff: {
109
+ type: "string" as const,
110
+ description:
111
+ "If status is in-progress: what remains to be done, key decisions made, blockers. If completed: empty string.",
112
+ },
113
+ observations: {
114
+ type: "array" as const,
115
+ items: {
116
+ type: "object" as const,
117
+ additionalProperties: false,
118
+ properties: {
119
+ type: {
120
+ type: "string" as const,
121
+ enum: ["O", "W", "B"],
122
+ description: "O=preference, W=world fact, B=what AI did",
123
+ },
124
+ text: { type: "string" as const },
125
+ confidence: { type: "number" as const },
126
+ },
127
+ required: ["type", "text", "confidence"] as const,
128
+ },
129
+ },
130
+ },
131
+ required: ["title", "summary", "insights", "handoff", "observations"] as const,
132
+ };
133
+
134
+ interface IntelligenceOutput {
135
+ title: string;
136
+ summary: string;
137
+ insights: string;
138
+ handoff: string;
139
+ observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
140
+ }
141
+
142
+ // ── Main handler ──
143
+
144
+ export async function captureSessionIntelligence(
145
+ transcript: string,
146
+ sessionId?: string
147
+ ): Promise<void> {
148
+ const messages = parseMessages(transcript);
149
+ if (messages.length < 6 || transcript.length < 2000) return;
150
+
151
+ // Dedup check
152
+ if (sessionId) {
153
+ const prev = getPreviousCapture(sessionId);
154
+ if (prev && messages.length - prev.messageCount < MIN_NEW_MESSAGES) return;
155
+ }
156
+
157
+ // Skip if no API key
158
+ if (!process.env.PAL_ANTHROPIC_API_KEY) {
159
+ logDebug("session-intelligence", "Skipped: no PAL_ANTHROPIC_API_KEY");
160
+ return;
161
+ }
162
+
163
+ // Relationship dedup — skip relationship capture if already done for this session
164
+ const skipRelationship = sessionId ? hasSessionNotes(sessionId) : false;
165
+
166
+ // Extract transcript windows
167
+ const userMessages = messages
168
+ .filter((m) => m.role === "user")
169
+ .map((m) => extractContent(m))
170
+ .filter((t) => t.length > 0);
171
+
172
+ const lastAssistant = extractLastAssistant(messages);
173
+ const lastAssistantText = extractContent(lastAssistant);
174
+ const lastUser = extractLastUser(messages);
175
+ const status = detectStatus(lastAssistantText);
176
+
177
+ // Wider window: 15 user msgs at 200 chars (relationship needs more context)
178
+ const userWindow = userMessages.slice(-15).map((t) => t.slice(0, 200));
179
+ const assistantWindow = lastAssistantText.slice(0, 600);
180
+
181
+ if (userWindow.length < 3) return;
182
+
183
+ // Single Haiku call
184
+ logDebug("session-intelligence", "Calling inference...");
185
+ let output: IntelligenceOutput | null = null;
186
+ try {
187
+ const result = await inference({
188
+ system: [
189
+ "You analyze a session between a human user and an AI assistant. Sessions may involve coding, research, writing, planning, analysis, or any other task.",
190
+ `Session status: ${status}.`,
191
+ "Produce ALL of the following:",
192
+ "1. title: short title (5-10 words) describing what was accomplished",
193
+ "2. summary: what the AI did for the user (2-4 sentences, AI perspective using 'we')",
194
+ "3. insights: what worked, what was surprising, what to do differently (2-3 points, no markdown)",
195
+ status === "in-progress"
196
+ ? "4. handoff: what remains unfinished — decisions made so far, next steps, blockers (2-4 sentences)"
197
+ : "4. handoff: empty string (session completed)",
198
+ skipRelationship
199
+ ? "5. observations: empty array (already captured)"
200
+ : "5. observations: 0-3 relationship observations. O=preference/opinion, W=world fact, B=what AI did this session (first-person). Be concise.",
201
+ ].join("\n"),
202
+ user: `User messages:\n${userWindow.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n\nLast AI response:\n${assistantWindow}`,
203
+ maxTokens: 500,
204
+ timeout: 15000,
205
+ jsonSchema: INTELLIGENCE_SCHEMA,
206
+ });
207
+
208
+ if (result.usage) logTokenUsage("session-intelligence", result.usage);
209
+
210
+ if (result.success && result.output) {
211
+ output = JSON.parse(result.output) as IntelligenceOutput;
212
+ }
213
+ } catch (err) {
214
+ logError("session-intelligence", err);
215
+ }
216
+
217
+ // Fallbacks
218
+ const title = output?.title || extractContent(lastUser).slice(0, 80) || "session";
219
+ const summary = output?.summary || lastAssistantText.slice(0, 600);
220
+ const insights = output?.insights || "";
221
+ const handoff = output?.handoff || "";
222
+
223
+ // ── Write session learning file ──
224
+
225
+ const category = categorizeLearning(title, summary);
226
+ const slug = slugify(title);
227
+ const dir = ensureDir(resolve(paths.sessionLearning(), monthPath()));
228
+ const filename = `${fileTimestamp()}_${category}_${slug}.md`;
229
+
230
+ const meta: Record<string, unknown> = {
231
+ title,
232
+ category,
233
+ date: new Date().toISOString().slice(0, 10),
234
+ cwd: process.cwd(),
235
+ };
236
+ if (sessionId) meta.session = sessionId;
237
+
238
+ const body = [
239
+ "## What Was Done",
240
+ summary,
241
+ "",
242
+ "## Insights",
243
+ insights || "*No insights captured.*",
244
+ ...(handoff ? ["", "## Handoff", handoff] : []),
245
+ ].join("\n");
246
+
247
+ const content = stringify(meta, body);
248
+
249
+ // Remove previous capture for this session
250
+ if (sessionId) {
251
+ const prev = getPreviousCapture(sessionId);
252
+ if (prev?.filepath && existsSync(prev.filepath)) {
253
+ try {
254
+ unlinkSync(prev.filepath);
255
+ } catch {
256
+ /* ignore */
257
+ }
258
+ }
259
+ }
260
+
261
+ const filepath = resolve(dir, filename);
262
+ writeFileSync(filepath, content, "utf-8");
263
+
264
+ // Append to per-project history
265
+ appendProjectHistory(process.cwd(), {
266
+ date: new Date().toISOString().slice(0, 10),
267
+ title,
268
+ summary,
269
+ insights,
270
+ });
271
+
272
+ if (sessionId) markCaptured(sessionId, filepath, messages.length);
273
+ logDebug("session-intelligence", `Learning captured: ${title}`);
274
+
275
+ // ── Write relationship notes ──
276
+
277
+ if (!skipRelationship && output?.observations && output.observations.length > 0) {
278
+ try {
279
+ const notes: RelationshipNote[] = output.observations.map((o) => ({
280
+ type: o.type,
281
+ text: o.text,
282
+ confidence: o.confidence,
283
+ }));
284
+ appendNotes(notes, sessionId);
285
+ logDebug(
286
+ "session-intelligence",
287
+ `${notes.length} relationship observations captured`
288
+ );
289
+ } catch (err) {
290
+ logError("session-intelligence:relationship", err);
291
+ }
292
+ }
293
+
294
+ // ── Write handoff state ──
295
+
296
+ if (handoff && status === "in-progress") {
297
+ try {
298
+ const handoffPath = resolve(ensureDir(paths.state()), "last-handoff.json");
299
+ let handoffs: Record<string, unknown> = {};
300
+ if (existsSync(handoffPath)) {
301
+ try {
302
+ handoffs = JSON.parse(readFileSync(handoffPath, "utf-8"));
303
+ } catch {
304
+ /* fresh */
305
+ }
306
+ }
307
+ handoffs[process.cwd()] = {
308
+ timestamp: new Date().toISOString(),
309
+ sessionId,
310
+ title,
311
+ status,
312
+ handoff,
313
+ artifacts: [],
314
+ };
315
+ // Keep last 20 projects
316
+ const entries = Object.entries(handoffs);
317
+ if (entries.length > 20) handoffs = Object.fromEntries(entries.slice(-20));
318
+ writeFileSync(handoffPath, JSON.stringify(handoffs, null, 2), "utf-8");
319
+ logDebug("session-intelligence", "Handoff state written");
320
+ } catch (err) {
321
+ logError("session-intelligence:handoff", err);
322
+ }
323
+ }
324
+ }
@@ -20,9 +20,9 @@ import {
20
20
  import { logTokenUsage } from "../lib/token-usage";
21
21
 
22
22
  const NAME_PROMPT =
23
- "You generate concise 4-word session titles for AI coding sessions. " +
23
+ "You generate concise 4-word session titles for AI assistant sessions. " +
24
24
  "Output EXACTLY 4 words in Title Case, no punctuation. Describe the specific task. " +
25
- 'Example: "Fix Session Name Generation", "Debug Auth Token Refresh"';
25
+ 'Example: "Fix Session Name Generation", "Research Market Entry Strategy"';
26
26
 
27
27
  export async function captureSessionName(
28
28
  message: string,
@@ -42,7 +42,7 @@ export async function captureSessionName(
42
42
  logDebug("session-name", `Named from prompt: "${name}"`);
43
43
 
44
44
  // Spawn detached background process to upgrade with Haiku inference
45
- if (!process.env.ANTHROPIC_API_KEY) return;
45
+ if (!process.env.PAL_ANTHROPIC_API_KEY) return;
46
46
  try {
47
47
  const promptB64 = Buffer.from(message.slice(0, 800)).toString("base64");
48
48
  const child = spawn(
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Stop handler: Run synthesis if 24h+ since last run.
3
+ * Imports synthesize logic directly — no subprocess needed.
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { logDebug } from "../lib/log";
9
+ import { paths } from "../lib/paths";
10
+
11
+ const SYNTHESIS_TTL_MS = 24 * 60 * 60 * 1000;
12
+
13
+ export async function runSynthesis(): Promise<void> {
14
+ const statePath = resolve(paths.state(), "synthesis.json");
15
+
16
+ // Check 24h guard
17
+ if (existsSync(statePath)) {
18
+ try {
19
+ const data = JSON.parse(readFileSync(statePath, "utf-8")) as { timestamp: string };
20
+ if (Date.now() - new Date(data.timestamp).getTime() < SYNTHESIS_TTL_MS) {
21
+ logDebug("synthesis", "Skipped — last synthesis < 24h ago");
22
+ return;
23
+ }
24
+ } catch {
25
+ // Corrupted state — run anyway
26
+ }
27
+ }
28
+
29
+ logDebug("synthesis", "Running synthesis...");
30
+
31
+ const { synthesize, writeSynthesis } = await import("../../tools/agent/synthesize");
32
+ const state = synthesize(7);
33
+ writeSynthesis(state);
34
+
35
+ logDebug("synthesis", "Synthesis complete");
36
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Update checker — detects if a newer version of PAL is available.
3
3
  *
4
- * Repo mode (.palroot exists): git fetch + compare HEAD vs origin/main
4
+ * Repo mode (.git exists next to package): git fetch + compare HEAD vs origin/main
5
5
  * Package mode: fetch npm registry for latest version vs installed
6
6
  *
7
7
  * Caches result in state/update-available.json. Checked at most once per hour.
@@ -47,7 +47,7 @@ function writeCache(cache: UpdateCache): void {
47
47
  }
48
48
 
49
49
  function isRepoMode(): boolean {
50
- return existsSync(resolve(palPkg(), ".palroot"));
50
+ return existsSync(resolve(palPkg(), ".git"));
51
51
  }
52
52
 
53
53
  function getInstalledVersion(): string {
@@ -116,7 +116,7 @@ export async function captureWorkLearning(
116
116
  .join("\n");
117
117
  const result = await inference({
118
118
  system:
119
- "You summarize AI coding sessions between a human user and an AI assistant. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown).",
119
+ "You summarize sessions between a human user and an AI assistant. Sessions may involve coding, research, writing, planning, analysis, or any other task. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown).",
120
120
  user: `Human messages:\n${userMessages}\n\nAI response:\n${rawSummary.slice(0, 400)}`,
121
121
  maxTokens: 300,
122
122
  timeout: 15000,