portable-agent-layer 0.22.0 → 0.23.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.
Files changed (42) 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 +93 -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/failure.ts +3 -1
  22. package/src/hooks/handlers/rating.ts +17 -2
  23. package/src/hooks/handlers/reflect-trigger.ts +4 -4
  24. package/src/hooks/handlers/relationship.ts +1 -1
  25. package/src/hooks/handlers/session-intelligence.ts +324 -0
  26. package/src/hooks/handlers/session-name.ts +2 -2
  27. package/src/hooks/handlers/synthesis.ts +36 -0
  28. package/src/hooks/handlers/update-check.ts +2 -2
  29. package/src/hooks/handlers/work-learning.ts +1 -1
  30. package/src/hooks/lib/context.ts +119 -2
  31. package/src/hooks/lib/paths.ts +4 -12
  32. package/src/hooks/lib/security.ts +39 -28
  33. package/src/hooks/lib/stop.ts +56 -7
  34. package/src/hooks/lib/token-usage.ts +1 -0
  35. package/src/targets/claude/install.ts +1 -1
  36. package/src/targets/cursor/install.ts +7 -1
  37. package/src/targets/cursor/uninstall.ts +7 -0
  38. package/src/targets/lib.ts +125 -115
  39. package/src/targets/opencode/install.ts +4 -4
  40. package/src/tools/agent/algorithm-reflect.ts +2 -0
  41. package/src/tools/agent/synthesize.ts +361 -0
  42. 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("");
@@ -30,7 +30,8 @@ export async function captureFailure(
30
30
  rating: number,
31
31
  context: string,
32
32
  transcript: string,
33
- detailedContext?: string
33
+ detailedContext?: string,
34
+ principle?: string
34
35
  ): Promise<void> {
35
36
  const messages = parseMessages(transcript);
36
37
 
@@ -55,6 +56,7 @@ export async function captureFailure(
55
56
  ts: new Date().toISOString(),
56
57
  slug,
57
58
  };
59
+ if (principle) meta.principle = principle;
58
60
 
59
61
  const body = [
60
62
  "## What Happened",
@@ -166,8 +166,16 @@ const SENTIMENT_SCHEMA = {
166
166
  confidence: { type: "number" },
167
167
  summary: { type: "string" },
168
168
  detailed_context: { type: "string" },
169
+ principle: { type: "string" },
169
170
  },
170
- required: ["rating", "sentiment", "confidence", "summary", "detailed_context"],
171
+ required: [
172
+ "rating",
173
+ "sentiment",
174
+ "confidence",
175
+ "summary",
176
+ "detailed_context",
177
+ "principle",
178
+ ],
171
179
  additionalProperties: false,
172
180
  } as const;
173
181
 
@@ -177,6 +185,7 @@ interface SentimentResult {
177
185
  confidence: number;
178
186
  summary: string;
179
187
  detailed_context: string;
188
+ principle: string;
180
189
  }
181
190
 
182
191
  const SENTIMENT_SYSTEM_PROMPT = `Analyze the user's message for emotional sentiment toward the AI assistant.
@@ -187,7 +196,8 @@ OUTPUT FORMAT (JSON only):
187
196
  "sentiment": "positive" | "negative" | "neutral",
188
197
  "confidence": <0.0-1.0>,
189
198
  "summary": "<brief explanation, 10 words max>",
190
- "detailed_context": "<comprehensive analysis, 50-150 words>"
199
+ "detailed_context": "<comprehensive analysis, 50-150 words>",
200
+ "principle": "<one actionable rule the AI should follow to avoid this failure or repeat this success, 10-20 words. Start with a verb: 'Verify...', 'Always...', 'Never...', 'Ask before...'>"
191
201
  }
192
202
 
193
203
  DETAILED_CONTEXT REQUIREMENTS:
@@ -240,6 +250,7 @@ function handleRating(
240
250
  context: string,
241
251
  source: string,
242
252
  detailedContext?: string,
253
+ principle?: string,
243
254
  sessionId?: string,
244
255
  userMessage?: string
245
256
  ): void {
@@ -257,6 +268,7 @@ function handleRating(
257
268
  context,
258
269
  source,
259
270
  detailedContext,
271
+ principle,
260
272
  responsePreview,
261
273
  userPreview,
262
274
  ts: now(),
@@ -284,6 +296,7 @@ async function handleImplicitSentiment(
284
296
  `Direct praise: "${trimmed}"`,
285
297
  "implicit",
286
298
  undefined,
299
+ undefined,
287
300
  sessionId,
288
301
  trimmed
289
302
  );
@@ -328,6 +341,7 @@ async function handleImplicitSentiment(
328
341
  `${parsed.summary}: ${trimmed.slice(0, 200)}`,
329
342
  "implicit",
330
343
  parsed.detailed_context,
344
+ parsed.principle,
331
345
  sessionId,
332
346
  trimmed
333
347
  );
@@ -352,6 +366,7 @@ export async function captureRating(message: string, sessionId?: string): Promis
352
366
  explicit.comment || cleaned.slice(0, 200),
353
367
  "explicit",
354
368
  undefined,
369
+ undefined,
355
370
  sessionId,
356
371
  cleaned
357
372
  );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-trigger for relationship reflect — runs when conditions are met:
3
- * - 7+ days since last reflect
4
- * - 10+ new relationship notes since last reflect
3
+ * - 1+ days since last reflect
4
+ * - 5+ new relationship notes since last reflect
5
5
  *
6
6
  * Spawns `bun run tool:reflect` as a detached background process.
7
7
  */
@@ -12,8 +12,8 @@ import { logDebug } from "../lib/log";
12
12
  import { getLastReflectDate } from "../lib/opinions";
13
13
  import { palPkg, paths } from "../lib/paths";
14
14
 
15
- const MIN_DAYS_BETWEEN = 7;
16
- const MIN_NEW_NOTES = 10;
15
+ const MIN_DAYS_BETWEEN = 1;
16
+ const MIN_NEW_NOTES = 5;
17
17
 
18
18
  function countNotesSince(since: string): number {
19
19
  const relDir = paths.relationship();
@@ -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,