portable-agent-layer 0.22.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 (39) hide show
  1. package/assets/agents/gemini-researcher.md +17 -3
  2. package/assets/agents/grok-researcher.md +19 -5
  3. package/assets/agents/multi-perspective-researcher.md +16 -2
  4. package/assets/agents/perplexity-researcher.md +17 -3
  5. package/assets/skills/analyze-pdf/SKILL.md +1 -1
  6. package/assets/skills/analyze-youtube/SKILL.md +1 -1
  7. package/assets/skills/extract-entities/SKILL.md +1 -1
  8. package/assets/skills/fyzz-chat-api/SKILL.md +3 -3
  9. package/assets/skills/reflect/SKILL.md +2 -2
  10. package/assets/skills/telos/SKILL.md +6 -6
  11. package/assets/templates/AGENTS.md.template +2 -2
  12. package/assets/templates/PAL/ALGORITHM.md +74 -10
  13. package/assets/templates/PAL/CONTEXT_ROUTING.md +17 -17
  14. package/assets/templates/PAL/MEMORY_SYSTEM.md +5 -5
  15. package/assets/templates/PAL/README.md +12 -9
  16. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
  17. package/assets/templates/pal-settings.json +6 -3
  18. package/assets/templates/settings.claude.json +2 -2
  19. package/package.json +3 -1
  20. package/src/cli/index.ts +4 -11
  21. package/src/hooks/handlers/relationship.ts +1 -1
  22. package/src/hooks/handlers/session-intelligence.ts +324 -0
  23. package/src/hooks/handlers/session-name.ts +2 -2
  24. package/src/hooks/handlers/synthesis.ts +36 -0
  25. package/src/hooks/handlers/update-check.ts +2 -2
  26. package/src/hooks/handlers/work-learning.ts +1 -1
  27. package/src/hooks/lib/context.ts +116 -0
  28. package/src/hooks/lib/paths.ts +4 -12
  29. package/src/hooks/lib/security.ts +39 -28
  30. package/src/hooks/lib/stop.ts +6 -6
  31. package/src/hooks/lib/token-usage.ts +1 -0
  32. package/src/targets/claude/install.ts +1 -1
  33. package/src/targets/cursor/install.ts +7 -1
  34. package/src/targets/cursor/uninstall.ts +7 -0
  35. package/src/targets/lib.ts +125 -115
  36. package/src/targets/opencode/install.ts +4 -4
  37. package/src/tools/agent/algorithm-reflect.ts +2 -0
  38. package/src/tools/agent/synthesize.ts +361 -0
  39. package/src/tools/agent/thread.ts +162 -0
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,7 +372,7 @@ 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
@@ -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("");
@@ -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,
@@ -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,
@@ -333,6 +333,116 @@ export function loadRelationshipContext(): string {
333
333
  }
334
334
  }
335
335
 
336
+ /** Load session intelligence from compact synthesis state */
337
+ export function loadSessionIntelligence(): string {
338
+ try {
339
+ const p = resolve(paths.state(), "synthesis.json");
340
+ if (!existsSync(p)) return "";
341
+ const state = JSON.parse(readFileSync(p, "utf-8"));
342
+
343
+ const lines: string[] = ["## Session Intelligence"];
344
+
345
+ // Open Threads — project-specific first, then global
346
+ if (state.threads?.length > 0) {
347
+ const cwd = process.cwd();
348
+ const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
349
+ const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
350
+
351
+ if (here.length > 0) {
352
+ lines.push("");
353
+ lines.push(`**Open threads — this project (${here.length}):**`);
354
+ for (const t of here) {
355
+ lines.push(`- ${t.title} (opened ${t.opened})`);
356
+ if (t.context) lines.push(` ${t.context}`);
357
+ }
358
+ lines.push("→ These are directly relevant to your current work.");
359
+ }
360
+ if (other.length > 0) {
361
+ lines.push("");
362
+ lines.push(`**Open threads — other projects (${other.length}):**`);
363
+ for (const t of other) {
364
+ lines.push(`- ${t.title} (opened ${t.opened})`);
365
+ }
366
+ }
367
+ }
368
+
369
+ // Rating Trend
370
+ if (state.ratings?.count > 0) {
371
+ const r = state.ratings;
372
+ lines.push("");
373
+ lines.push(
374
+ `**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : ""}`
375
+ );
376
+ if (r.trend === "declining") {
377
+ lines.push(
378
+ "→ Trend is declining. Be extra careful with assumptions. Confirm before acting."
379
+ );
380
+ } else if (r.trend === "improving") {
381
+ lines.push("→ Trend is improving. Maintain current approach.");
382
+ } else if (r.lowCount > 5) {
383
+ lines.push(
384
+ "→ Multiple low ratings. Slow down, verify before acting, ask when uncertain."
385
+ );
386
+ }
387
+ }
388
+
389
+ // Algorithm Performance
390
+ if (state.algorithm?.reflectionCount > 0) {
391
+ const a = state.algorithm;
392
+ lines.push("");
393
+ lines.push(
394
+ `**Algorithm:** ${a.reflectionCount} reflections, ${a.passRate}% criteria pass rate, ${a.avgSentiment}/10 sentiment.`
395
+ );
396
+ if (a.passRate < 80) {
397
+ lines.push(
398
+ "→ Criteria pass rate is low. Invest more time in OBSERVE and PLAN phases."
399
+ );
400
+ }
401
+ if (a.recentObservations?.length > 0) {
402
+ const cwd = process.cwd();
403
+ const relevant = a.recentObservations.filter(
404
+ (o: { cwd?: string }) => !o.cwd || o.cwd === cwd
405
+ );
406
+ if (relevant.length > 0) {
407
+ lines.push("Recent self-observations (this project):");
408
+ for (const o of relevant) {
409
+ lines.push(`- [${o.date}] ${o.task}: "${o.observation}"`);
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ return lines.length > 1 ? lines.join("\n") : "";
416
+ } catch {
417
+ return "";
418
+ }
419
+ }
420
+
421
+ /** Load handoff state for the current project */
422
+ export function loadHandoff(): string {
423
+ try {
424
+ const p = resolve(paths.state(), "last-handoff.json");
425
+ if (!existsSync(p)) return "";
426
+ const handoffs = JSON.parse(readFileSync(p, "utf-8"));
427
+ const cwd = process.cwd();
428
+ const entry = handoffs[cwd];
429
+ if (!entry?.handoff || entry.status !== "in-progress") return "";
430
+
431
+ const age = Date.now() - new Date(entry.timestamp).getTime();
432
+ if (age > 7 * 24 * 60 * 60 * 1000) return ""; // stale after 7 days
433
+
434
+ return [
435
+ "## Pick Up Where You Left Off",
436
+ `*Previous session: ${entry.title}*`,
437
+ "",
438
+ entry.handoff,
439
+ "→ Continue this work or explicitly close it before starting something new.",
440
+ ].join("\n");
441
+ } catch {
442
+ return "";
443
+ }
444
+ }
445
+
336
446
  /**
337
447
  * Build the <system-reminder> content for the AI.
338
448
  *
@@ -358,10 +468,16 @@ export function buildSystemReminder(): string {
358
468
  ? loadSynthesisRecommendations()
359
469
  : "";
360
470
  const opinions = isEnabled(settings, "opinions") ? loadOpinionContext() : "";
471
+ const intelligence = isEnabled(settings, "sessionIntelligence")
472
+ ? loadSessionIntelligence()
473
+ : "";
474
+ const handoff = isEnabled(settings, "handoff") ? loadHandoff() : "";
361
475
  const parts: string[] = [];
362
476
  if (startup) parts.push(startup);
477
+ if (handoff) parts.push(handoff);
363
478
  if (wisdom) parts.push(wisdom);
364
479
  if (opinions) parts.push(opinions);
480
+ if (intelligence) parts.push(intelligence);
365
481
  if (relationship) parts.push(relationship);
366
482
  if (projectHistory) parts.push(projectHistory);
367
483
  if (digest) parts.push(digest);
@@ -12,20 +12,12 @@ export function palPkg(): string {
12
12
  }
13
13
 
14
14
  /**
15
- * Root of the user's personal state (telos, memory, etc.).
16
- * In repo mode: same as palPkg() (the repo root).
17
- * In package mode: ~/.pal/ (or PAL_HOME override).
18
- *
19
- * Repo mode is detected by the presence of .palroot next to the package.
20
- * This file is not included in the npm package, so it only exists in cloned repos.
15
+ * Root of the user's personal state (telos, memory, docs, tools, skills).
16
+ * Always resolves to ~/.pal/ regardless of where the package lives.
17
+ * Power users who want memory/telos versioned in a repo can override via PAL_HOME.
21
18
  */
22
19
  export function palHome(): string {
23
- if (process.env.PAL_HOME) return process.env.PAL_HOME;
24
-
25
- const pkgRoot = palPkg();
26
- if (existsSync(resolve(pkgRoot, ".palroot"))) return pkgRoot;
27
-
28
- return resolve(homedir(), ".pal");
20
+ return process.env.PAL_HOME || resolve(homedir(), ".pal");
29
21
  }
30
22
 
31
23
  /** Ensure a directory exists, creating it recursively if needed */