portable-agent-layer 0.35.0 → 0.37.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 (108) hide show
  1. package/README.md +2 -1
  2. package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
  3. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
  4. package/assets/skills/consulting-report/tools/dev.ts +2 -2
  5. package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
  6. package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
  7. package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
  8. package/assets/skills/opinion/tools/opinion.ts +3 -2
  9. package/assets/skills/presentation/SKILL.md +1 -1
  10. package/assets/skills/presentation/tools/doctor.ts +2 -5
  11. package/assets/skills/presentation/tools/lib/inline.ts +6 -11
  12. package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
  13. package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
  14. package/assets/skills/presentation/tools/setup-template.ts +10 -7
  15. package/assets/skills/projects/SKILL.md +44 -21
  16. package/assets/skills/research/tools/gemini-search.ts +2 -2
  17. package/assets/skills/research/tools/grok-search.ts +2 -2
  18. package/assets/skills/research/tools/perplexity-search.ts +2 -2
  19. package/assets/skills/telos/SKILL.md +7 -52
  20. package/assets/skills/telos/tools/update-telos.ts +0 -1
  21. package/assets/templates/PAL/ALGORITHM.md +54 -5
  22. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  23. package/assets/templates/PAL/README.md +1 -1
  24. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  25. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  26. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  27. package/assets/templates/hooks.codex.json +44 -0
  28. package/assets/templates/hooks.cursor.json +11 -5
  29. package/assets/templates/pal-settings.json +1 -3
  30. package/assets/templates/settings.claude.json +2 -1
  31. package/package.json +2 -1
  32. package/src/cli/index.ts +112 -14
  33. package/src/cli/migrate.ts +299 -0
  34. package/src/cli/setup-identity.ts +3 -3
  35. package/src/cli/setup-telos.ts +12 -80
  36. package/src/hooks/CompactRecover.ts +11 -5
  37. package/src/hooks/LoadContext.ts +35 -11
  38. package/src/hooks/PreCompactPersist.ts +26 -34
  39. package/src/hooks/SecurityValidator.ts +43 -21
  40. package/src/hooks/StopOrchestrator.ts +4 -1
  41. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  42. package/src/hooks/handlers/auto-graduate.ts +2 -2
  43. package/src/hooks/handlers/backup.ts +3 -3
  44. package/src/hooks/handlers/context-digests.ts +74 -0
  45. package/src/hooks/handlers/failure.ts +5 -3
  46. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  47. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  48. package/src/hooks/handlers/rating.ts +2 -1
  49. package/src/hooks/handlers/readme-sync.ts +3 -2
  50. package/src/hooks/handlers/session-intelligence.ts +17 -93
  51. package/src/hooks/handlers/session-name.ts +2 -2
  52. package/src/hooks/handlers/synthesis.ts +5 -2
  53. package/src/hooks/handlers/update-counts.ts +3 -2
  54. package/src/hooks/lib/agent.ts +20 -18
  55. package/src/hooks/lib/claude-md.ts +69 -14
  56. package/src/hooks/lib/context.ts +92 -246
  57. package/src/hooks/lib/entities.ts +7 -7
  58. package/src/hooks/lib/frontmatter.ts +4 -4
  59. package/src/hooks/lib/graduation.ts +7 -6
  60. package/src/hooks/lib/inference.ts +6 -2
  61. package/src/hooks/lib/learning-category.ts +1 -1
  62. package/src/hooks/lib/learning-store.ts +6 -1
  63. package/src/hooks/lib/notify.ts +2 -2
  64. package/src/hooks/lib/opinions.ts +3 -3
  65. package/src/hooks/lib/paths.ts +2 -0
  66. package/src/hooks/lib/projects.ts +142 -74
  67. package/src/hooks/lib/readme-sync.ts +1 -1
  68. package/src/hooks/lib/relationship.ts +4 -16
  69. package/src/hooks/lib/retrieval-index.ts +5 -3
  70. package/src/hooks/lib/retrieval.ts +11 -12
  71. package/src/hooks/lib/security.ts +24 -18
  72. package/src/hooks/lib/semi-static.ts +188 -0
  73. package/src/hooks/lib/session-names.ts +1 -1
  74. package/src/hooks/lib/settings.ts +1 -1
  75. package/src/hooks/lib/setup.ts +2 -65
  76. package/src/hooks/lib/signals.ts +2 -2
  77. package/src/hooks/lib/stdin.ts +1 -1
  78. package/src/hooks/lib/stop.ts +16 -6
  79. package/src/hooks/lib/token-usage.ts +1 -2
  80. package/src/hooks/lib/transcript.ts +1 -1
  81. package/src/hooks/lib/wisdom.ts +5 -5
  82. package/src/hooks/lib/work-tracking.ts +8 -14
  83. package/src/targets/claude/uninstall.ts +1 -1
  84. package/src/targets/codex/install.ts +95 -0
  85. package/src/targets/codex/uninstall.ts +70 -0
  86. package/src/targets/copilot/install.ts +39 -8
  87. package/src/targets/copilot/uninstall.ts +58 -17
  88. package/src/targets/cursor/install.ts +8 -0
  89. package/src/targets/cursor/uninstall.ts +18 -1
  90. package/src/targets/lib.ts +166 -14
  91. package/src/targets/opencode/install.ts +29 -1
  92. package/src/targets/opencode/plugin.ts +23 -12
  93. package/src/targets/opencode/uninstall.ts +30 -3
  94. package/src/tools/agent/algorithm-reflect.ts +1 -1
  95. package/src/tools/agent/analyze.ts +18 -18
  96. package/src/tools/agent/handoff-note.ts +116 -0
  97. package/src/tools/agent/project.ts +375 -75
  98. package/src/tools/agent/relationship-note.ts +51 -0
  99. package/src/tools/agent/synthesize.ts +6 -42
  100. package/src/tools/agent/thread.ts +15 -14
  101. package/src/tools/agent/wisdom-frame.ts +9 -3
  102. package/src/tools/import.ts +1 -1
  103. package/src/tools/relationship-reflect.ts +15 -13
  104. package/src/tools/self-model.ts +23 -19
  105. package/src/tools/session-summary.ts +3 -3
  106. package/src/tools/token-cost.ts +15 -16
  107. package/assets/skills/telos/tools/update-projects.ts +0 -106
  108. package/assets/templates/telos/PROJECTS.md +0 -7
@@ -3,21 +3,19 @@
3
3
  * Used by LoadContext.ts (Claude Code) and the opencode plugin.
4
4
  */
5
5
 
6
- import { existsSync, readdirSync, readFileSync } from "node:fs";
6
+ import { existsSync, readFileSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
8
  import { resolve } from "node:path";
9
- import { parse } from "./frontmatter";
10
- import { readFailures, readLearnings } from "./learning-store";
9
+ import { readLearnings } from "./learning-store";
11
10
  import { loadOpinionContext } from "./opinions";
12
11
  import { paths } from "./paths";
13
12
  import { loadActiveProjectsContext } from "./projects";
14
13
  import { loadRecentNotes } from "./relationship";
15
- import { readSessionNames } from "./session-names";
14
+ import { loadFailurePatterns, loadSynthesisRecommendations } from "./semi-static";
16
15
  import * as settings from "./settings";
17
- import { isSetupComplete, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
18
16
  import { computeSignalTrends, formatTrends } from "./signal-trends";
19
17
  import { readFramePrinciples } from "./wisdom";
20
- import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
18
+ import { readProjectHistory } from "./work-tracking";
21
19
 
22
20
  /** Load and concatenate loadAtStartup files */
23
21
  function loadStartupFiles(): string {
@@ -41,135 +39,6 @@ function loadStartupFiles(): string {
41
39
  return sections.join("\n\n---\n\n");
42
40
  }
43
41
 
44
- /** Count lines in a signals JSONL file */
45
- export function countSignals(filename: string): number {
46
- const filepath = resolve(paths.signals(), filename);
47
- if (!existsSync(filepath)) return 0;
48
- try {
49
- const content = readFileSync(filepath, "utf-8").trim();
50
- return content ? content.split("\n").length : 0;
51
- } catch {
52
- return 0;
53
- }
54
- }
55
-
56
- /** Load structured session history + project dashboard */
57
- export function loadActiveWork(): { text: string; summary: string | null } | null {
58
- try {
59
- const cwd = process.cwd();
60
- const allRecent = recentSessions(48);
61
-
62
- if (allRecent.length === 0) return null;
63
-
64
- const lines: string[] = [];
65
-
66
- lines.push("## Recent Work (last 48h)");
67
- for (const s of allRecent.slice(-10).reverse()) {
68
- const ago = formatAgo(s.ts);
69
- const here = s.cwd === cwd ? " *" : "";
70
- lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
71
- }
72
-
73
- // Summary from most recent session
74
- const cwdSessions = allRecent.filter((s) => s.cwd === cwd);
75
- const last = cwdSessions.length > 0 ? cwdSessions[cwdSessions.length - 1] : null;
76
- const summary = last?.summary?.slice(0, 60) || null;
77
-
78
- return {
79
- text: lines.join("\n"),
80
- summary: summary ? `"${summary}"` : null,
81
- };
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- /** Format a timestamp as a human-readable "X ago" string */
88
- function formatAgo(ts: string): string {
89
- const diff = Date.now() - new Date(ts).getTime();
90
- const hours = Math.floor(diff / (1000 * 60 * 60));
91
- if (hours < 1) return "just now";
92
- if (hours < 24) return `${hours}h ago`;
93
- const days = Math.floor(hours / 24);
94
- return `${days}d ago`;
95
- }
96
-
97
- /** Load the N most recent session names (fallback for greeting) */
98
- export function loadRecentSessions(count: number): string[] {
99
- try {
100
- const sessions = readSessions();
101
- if (sessions.length > 0) {
102
- return sessions
103
- .slice(-count)
104
- .reverse()
105
- .map((s) => s.name);
106
- }
107
- // Fallback to session-names.json for backwards compat
108
- const names = readSessionNames();
109
- const entries = Object.values(names);
110
- return entries.slice(-count).reverse();
111
- } catch {
112
- return [];
113
- }
114
- }
115
-
116
- /** Read cached counts from counts.json, falling back to live counting */
117
- function loadCachedCounts(): {
118
- signals: number;
119
- telos: number;
120
- skills: number;
121
- sessions: number;
122
- } {
123
- try {
124
- const countsPath = resolve(paths.state(), "counts.json");
125
- if (existsSync(countsPath)) {
126
- return JSON.parse(readFileSync(countsPath, "utf-8"));
127
- }
128
- } catch {
129
- /* fall through */
130
- }
131
- // Fallback: count live (first session before any stop has run)
132
- return {
133
- signals: countSignals("ratings.jsonl"),
134
- telos: 0,
135
- skills: 0,
136
- sessions: 0,
137
- };
138
- }
139
-
140
- /** Build the visible greeting lines for stderr */
141
- export function buildGreeting(): string[] {
142
- const counts = loadCachedCounts();
143
- const work = loadActiveWork();
144
- const setupState = readSetupState();
145
- const setupIncomplete = setupState && !isSetupComplete(setupState);
146
-
147
- const greeting: string[] = [];
148
-
149
- if (setupIncomplete) {
150
- const done = STEP_ORDER.length - remainingSteps(setupState).length;
151
- greeting.push(
152
- `🔧 PAL setup ${done}/${STEP_ORDER.length} | ${counts.signals} signals`
153
- );
154
- } else {
155
- greeting.push(
156
- `✅ PAL ready | ${counts.telos} TELOS | ${counts.skills} skills | ${counts.signals} signals | ${counts.sessions} sessions`
157
- );
158
- }
159
-
160
- if (work?.summary) {
161
- greeting.push(`📋 Previous: ${work.summary}`);
162
- }
163
-
164
- // Show recent session names for quick context
165
- const recent = loadRecentSessions(3);
166
- if (recent.length > 0) {
167
- greeting.push(`📂 Recent: ${recent.join(" | ")}`);
168
- }
169
-
170
- return greeting;
171
- }
172
-
173
42
  /** Load high-confidence wisdom principles for injection into system-reminder */
174
43
  export function loadWisdomContext(): string {
175
44
  try {
@@ -205,7 +74,7 @@ export function loadLearningDigest(): string {
205
74
  }
206
75
 
207
76
  /** Load self-model for session context injection */
208
- export function loadSelfModel(): string {
77
+ function loadSelfModel(): string {
209
78
  try {
210
79
  const p = resolve(paths.memory(), "self-model", "current.md");
211
80
  if (!existsSync(p)) return "";
@@ -217,78 +86,6 @@ export function loadSelfModel(): string {
217
86
  }
218
87
  }
219
88
 
220
- /** Load 5 most recent failure contexts as an "avoid" list */
221
- export function loadFailurePatterns(): string {
222
- try {
223
- const entries = readFailures(paths.failures(), 5);
224
- if (entries.length === 0) return "";
225
-
226
- const lines = entries.map((e) => {
227
- const label = e.rating ? `[${e.rating}/10]` : "";
228
- const text = e.principle || e.context;
229
- return `- ${label} ${text}`.trim();
230
- });
231
-
232
- return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
233
- } catch {
234
- return "";
235
- }
236
- }
237
-
238
- /** Load recommendations from the most recent synthesis report */
239
- export function loadSynthesisRecommendations(): string {
240
- try {
241
- const synthDir = paths.synthesis();
242
- if (!existsSync(synthDir)) return "";
243
-
244
- // Find most recent month directory
245
- const months = readdirSync(synthDir).sort().reverse();
246
- for (const month of months) {
247
- const monthDir = resolve(synthDir, month);
248
- try {
249
- const files = readdirSync(monthDir)
250
- .filter((f) => f.endsWith(".md"))
251
- .sort()
252
- .reverse();
253
- if (files.length === 0) continue;
254
-
255
- const content = readFileSync(resolve(monthDir, files[0]), "utf-8");
256
-
257
- // Extract recommendations section
258
- const recMatch = content.match(/## Recommendations\n\n([\s\S]*?)(?:\n##|\n$|$)/);
259
- if (!recMatch?.[1]?.trim()) continue;
260
-
261
- const recs = recMatch[1]
262
- .trim()
263
- .split("\n")
264
- .filter((l) => l.trim())
265
- .slice(0, 4);
266
-
267
- if (recs.length === 0) continue;
268
-
269
- const { meta } = parse<{
270
- period?: string;
271
- average_rating?: string;
272
- }>(content);
273
- const period = meta.period || "";
274
- const avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
275
-
276
- const header = [
277
- "## Pattern Synthesis",
278
- period ? `*${period} — ${avgRating}*` : "",
279
- ]
280
- .filter(Boolean)
281
- .join("\n");
282
-
283
- return [header, ...recs].join("\n");
284
- } catch {}
285
- }
286
- return "";
287
- } catch {
288
- return "";
289
- }
290
- }
291
-
292
89
  /** Load signal trends as a formatted string */
293
90
  export function loadSignalTrends(): string {
294
91
  try {
@@ -299,7 +96,7 @@ export function loadSignalTrends(): string {
299
96
  }
300
97
 
301
98
  /** Load per-project session history for the current working directory */
302
- export function loadProjectHistoryContext(): string {
99
+ function loadProjectHistoryContext(): string {
303
100
  try {
304
101
  const cwd = process.cwd();
305
102
  const entries = readProjectHistory(cwd, 3);
@@ -317,19 +114,54 @@ export function loadProjectHistoryContext(): string {
317
114
  }
318
115
  }
319
116
 
320
- /** Load recent relationship notes (today + yesterday) */
117
+ /**
118
+ * Filter raw relationship note lines:
119
+ * - O entries: stripped (loaded natively via digest)
120
+ * - HTML comments: stripped, but cwd is extracted from session comments
121
+ * - Session entries: kept only if block cwd matches current project (or legacy with no cwd)
122
+ * - W entries and structural lines: always kept
123
+ */
124
+ function filterRelationshipNotes(notes: string, cwd: string): string {
125
+ const lines = notes.split("\n");
126
+ const out: string[] = [];
127
+ let blockCwd: string | null = null;
128
+
129
+ for (const line of lines) {
130
+ if (/^## \d{2}:\d{2}/.test(line)) {
131
+ blockCwd = null;
132
+ out.push(line);
133
+ continue;
134
+ }
135
+ const cwdMatch = new RegExp(/<!--.*cwd:(\S+)/).exec(line);
136
+ if (cwdMatch) {
137
+ blockCwd = cwdMatch[1];
138
+ continue;
139
+ }
140
+ if (/^\s*<!--/.test(line)) continue;
141
+ if (/^\s*- O\(/.test(line)) continue;
142
+ if (/^\s*- Session:/.test(line)) {
143
+ if (blockCwd === null || blockCwd === cwd) out.push(line);
144
+ continue;
145
+ }
146
+ out.push(line);
147
+ }
148
+ return out.join("\n");
149
+ }
150
+
151
+ /** Load recent relationship notes (today + yesterday), scoped to current project */
321
152
  export function loadRelationshipContext(): string {
322
153
  try {
323
154
  const notes = loadRecentNotes(2);
324
155
  if (!notes) return "";
325
- return `## Recent Interaction Notes\n${notes}`;
156
+ const filtered = filterRelationshipNotes(notes, process.cwd());
157
+ return capSection(`## Recent Interaction Notes\n${filtered}`, 1500);
326
158
  } catch {
327
159
  return "";
328
160
  }
329
161
  }
330
162
 
331
163
  /** Load session intelligence from compact synthesis state */
332
- export function loadSessionIntelligence(): string {
164
+ function loadSessionIntelligence(): string {
333
165
  try {
334
166
  const p = resolve(paths.state(), "synthesis.json");
335
167
  if (!existsSync(p)) return "";
@@ -337,32 +169,13 @@ export function loadSessionIntelligence(): string {
337
169
 
338
170
  const lines: string[] = ["## Session Intelligence"];
339
171
 
340
- // Open Threads — project-specific first, then global
341
- if (state.threads?.length > 0) {
342
- const cwd = process.cwd();
343
- const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
344
- const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
345
-
346
- if (here.length > 0) {
347
- lines.push("");
348
- lines.push(`**Open threads — this project (${here.length}):**`);
349
- for (const t of here) {
350
- lines.push(`- ${t.title} (opened ${t.opened})`);
351
- if (t.context) lines.push(` ${t.context}`);
352
- }
353
- lines.push("→ These are directly relevant to your current work.");
354
- }
355
- // Cross-project threads intentionally omitted — they were noise in 90%+ of sessions.
356
- // To surface them on demand, add a `threads` slash-command or a flag in pal-settings.
357
- void other;
358
- }
359
-
360
172
  // Rating Trend
361
173
  if (state.ratings?.count > 0) {
362
174
  const r = state.ratings;
363
- lines.push("");
175
+ const lowNote = r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : "";
364
176
  lines.push(
365
- `**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : ""}`
177
+ "",
178
+ `**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${lowNote}`
366
179
  );
367
180
  if (r.trend === "declining") {
368
181
  lines.push(
@@ -380,8 +193,8 @@ export function loadSessionIntelligence(): string {
380
193
  // Algorithm Performance
381
194
  if (state.algorithm?.reflectionCount > 0) {
382
195
  const a = state.algorithm;
383
- lines.push("");
384
196
  lines.push(
197
+ "",
385
198
  `**Algorithm:** ${a.reflectionCount} reflections, ${a.passRate}% criteria pass rate, ${a.avgSentiment}/10 sentiment.`
386
199
  );
387
200
  if (a.passRate < 80) {
@@ -403,14 +216,14 @@ export function loadSessionIntelligence(): string {
403
216
  }
404
217
  }
405
218
 
406
- return lines.length > 1 ? lines.join("\n") : "";
219
+ return lines.length > 1 ? capSection(lines.join("\n"), 2000) : "";
407
220
  } catch {
408
221
  return "";
409
222
  }
410
223
  }
411
224
 
412
225
  /** Load handoff state for the current project */
413
- export function loadHandoff(): string {
226
+ function loadHandoff(): string {
414
227
  try {
415
228
  const p = resolve(paths.state(), "last-handoff.json");
416
229
  if (!existsSync(p)) return "";
@@ -434,17 +247,46 @@ export function loadHandoff(): string {
434
247
  }
435
248
  }
436
249
 
250
+ /** Truncate text to maxChars at the last complete line boundary */
251
+ function capSection(text: string, maxChars: number): string {
252
+ if (text.length <= maxChars) return text;
253
+ const lines = text.split("\n");
254
+ const kept: string[] = [];
255
+ let total = 0;
256
+ for (const line of lines) {
257
+ const next = total + line.length + 1;
258
+ if (next > maxChars) break;
259
+ kept.push(line);
260
+ total = next;
261
+ }
262
+ return kept.join("\n");
263
+ }
264
+
265
+ /** Agent targets — determines which context sections are skipped due to native loading. */
266
+ export type AgentTarget = "claude" | "opencode" | "cursor" | "copilot";
267
+
437
268
  /**
438
269
  * Build the <system-reminder> content for the AI.
439
270
  *
440
271
  * Static context (TELOS, setup prompt) lives in AGENTS.md / CLAUDE.md and is
441
272
  * loaded natively by Claude Code / opencode. This injects dynamic context only —
442
273
  * things that change per-session and can't live in a static file.
274
+ *
275
+ * opts.agent — agent target; Claude Code skips semi-static sections (self-model,
276
+ * wisdom, opinions) that load natively via @imports in CLAUDE.md.
443
277
  */
444
- export function buildSystemReminder(): string {
278
+ export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string {
279
+ // Semi-static sections loaded natively via @imports (Claude Code) or
280
+ // instructions[] (opencode). Skip them from hook output for those agents.
281
+ const skipSemiStatic =
282
+ opts.agent === "claude" ||
283
+ opts.agent === "opencode" ||
284
+ opts.agent === "cursor" ||
285
+ opts.agent === "copilot";
286
+
445
287
  const startup = loadStartupFiles();
446
- const work = settings.isEnabled("activeWork") ? loadActiveWork() : null;
447
- const wisdom = settings.isEnabled("wisdom") ? loadWisdomContext() : "";
288
+ const wisdom =
289
+ !skipSemiStatic && settings.isEnabled("wisdom") ? loadWisdomContext() : "";
448
290
  const relationship = settings.isEnabled("relationship")
449
291
  ? loadRelationshipContext()
450
292
  : "";
@@ -456,10 +298,16 @@ export function buildSystemReminder(): string {
456
298
  ? loadActiveProjectsContext()
457
299
  : "";
458
300
  const trends = settings.isEnabled("signalTrends") ? loadSignalTrends() : "";
459
- const failures = settings.isEnabled("failurePatterns") ? loadFailurePatterns() : "";
460
- const synthesis = settings.isEnabled("synthesis") ? loadSynthesisRecommendations() : "";
461
- const opinions = settings.isEnabled("opinions") ? loadOpinionContext() : "";
462
- const selfModel = settings.isEnabled("selfModel") ? loadSelfModel() : "";
301
+ const failures =
302
+ settings.isEnabled("failurePatterns") && !skipSemiStatic ? loadFailurePatterns() : "";
303
+ const synthesis =
304
+ settings.isEnabled("synthesis") && !skipSemiStatic
305
+ ? loadSynthesisRecommendations()
306
+ : "";
307
+ const opinions =
308
+ !skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
309
+ const selfModel =
310
+ !skipSemiStatic && settings.isEnabled("selfModel") ? loadSelfModel() : "";
463
311
  const intelligence = settings.isEnabled("sessionIntelligence")
464
312
  ? loadSessionIntelligence()
465
313
  : "";
@@ -478,8 +326,6 @@ export function buildSystemReminder(): string {
478
326
  if (synthesis) parts.push(synthesis);
479
327
  if (trends) parts.push(trends);
480
328
  if (failures) parts.push(failures);
481
- if (work) parts.push(work.text);
482
-
483
329
  if (parts.length === 0) return "";
484
330
 
485
331
  const now = new Date();
@@ -12,7 +12,7 @@ import { ensureDir, paths } from "./paths";
12
12
 
13
13
  // --- Types ---
14
14
 
15
- export interface PersonEntity {
15
+ interface PersonEntity {
16
16
  id: string;
17
17
  name: string;
18
18
  first_seen: string;
@@ -20,7 +20,7 @@ export interface PersonEntity {
20
20
  source_ids: string[];
21
21
  }
22
22
 
23
- export interface CompanyEntity {
23
+ interface CompanyEntity {
24
24
  id: string;
25
25
  name: string;
26
26
  domain: string | null;
@@ -29,7 +29,7 @@ export interface CompanyEntity {
29
29
  source_ids: string[];
30
30
  }
31
31
 
32
- export interface LinkEntity {
32
+ interface LinkEntity {
33
33
  id: string;
34
34
  url: string;
35
35
  first_seen: string;
@@ -37,7 +37,7 @@ export interface LinkEntity {
37
37
  source_ids: string[];
38
38
  }
39
39
 
40
- export interface SourceEntity {
40
+ interface SourceEntity {
41
41
  id: string;
42
42
  url: string | null;
43
43
  author: string | null;
@@ -47,7 +47,7 @@ export interface SourceEntity {
47
47
  source_ids: string[];
48
48
  }
49
49
 
50
- export interface EntityIndex {
50
+ interface EntityIndex {
51
51
  version: string;
52
52
  last_updated: string;
53
53
  people: Record<string, PersonEntity>;
@@ -100,8 +100,8 @@ function emptyIndex(): EntityIndex {
100
100
 
101
101
  /** Migrate older indexes that lack links/sources. */
102
102
  function ensureShape(index: EntityIndex): EntityIndex {
103
- if (!index.links) index.links = {};
104
- if (!index.sources) index.sources = {};
103
+ index.links ??= {};
104
+ index.sources ??= {};
105
105
  return index;
106
106
  }
107
107
 
@@ -5,7 +5,7 @@
5
5
  * Supports strings, numbers, booleans, and inline JSON arrays.
6
6
  */
7
7
 
8
- export interface Parsed<T = Record<string, string>> {
8
+ interface Parsed<T = Record<string, string>> {
9
9
  meta: T;
10
10
  body: string;
11
11
  }
@@ -30,7 +30,7 @@ export function parse<T = Record<string, string>>(content: string): Parsed<T> {
30
30
 
31
31
  const meta: Record<string, unknown> = {};
32
32
  for (const line of rawMeta.split("\n")) {
33
- const match = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
33
+ const match = new RegExp(/^(\w[\w-]*)\s*:\s*(.*)$/).exec(line);
34
34
  if (!match) continue;
35
35
  const [, key, rawValue] = match;
36
36
  const value = rawValue.trim();
@@ -50,7 +50,7 @@ export function parse<T = Record<string, string>>(content: string): Parsed<T> {
50
50
  (value.startsWith('"') && value.endsWith('"')) ||
51
51
  (value.startsWith("'") && value.endsWith("'"))
52
52
  ) {
53
- meta[key] = value.slice(1, -1).replace(/\\"/g, '"');
53
+ meta[key] = value.slice(1, -1).replaceAll('\\"', '"');
54
54
  continue;
55
55
  }
56
56
 
@@ -77,7 +77,7 @@ export function stringify(meta: Record<string, unknown>, body: string): string {
77
77
  if (Array.isArray(value)) {
78
78
  lines.push(`${key}: ${JSON.stringify(value)}`);
79
79
  } else if (typeof value === "string") {
80
- lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
80
+ lines.push(`${key}: "${value.replaceAll('"', '\\"')}"`);
81
81
  } else {
82
82
  lines.push(`${key}: ${String(value)}`);
83
83
  }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { resolve } from "node:path";
15
+ import { hasApiKey } from "./inference";
15
16
  import {
16
17
  type FailureEntry,
17
18
  type LearningEntry,
@@ -24,14 +25,14 @@ import { extractKeywords, similarity } from "./text-similarity";
24
25
 
25
26
  // ── Types ──
26
27
 
27
- export interface AnalysisEntry {
28
+ interface AnalysisEntry {
28
29
  source: string;
29
30
  path: string;
30
31
  text: string;
31
32
  date: string;
32
33
  }
33
34
 
34
- export interface PatternGroup {
35
+ interface PatternGroup {
35
36
  pattern: string;
36
37
  entries: AnalysisEntry[];
37
38
  domain: string;
@@ -51,7 +52,7 @@ interface GraduationState {
51
52
  graduated: GraduatedEntry[];
52
53
  }
53
54
 
54
- export interface RatingsSummary {
55
+ interface RatingsSummary {
55
56
  total: number;
56
57
  average: number;
57
58
  low: { count: number; examples: string[] };
@@ -214,7 +215,7 @@ async function generateRecommendations(
214
215
  ratings: RatingsSummary | null
215
216
  ): Promise<string[]> {
216
217
  if (candidates.length === 0 && !ratings) return [];
217
- if (!process.env.PAL_ANTHROPIC_API_KEY) {
218
+ if (!hasApiKey()) {
218
219
  return candidates
219
220
  .slice(0, 3)
220
221
  .map(
@@ -298,7 +299,7 @@ function writeState(state: GraduationState): void {
298
299
  function synthesizePrinciple(group: PatternGroup): string {
299
300
  const sorted = [...group.entries].sort((a, b) => a.text.length - b.text.length);
300
301
  let principle = sorted[0].text;
301
- const firstSentence = principle.match(/^[^.!?]+[.!?]?/);
302
+ const firstSentence = new RegExp(/^[^.!?]+[.!?]?/).exec(principle);
302
303
  if (firstSentence) principle = firstSentence[0];
303
304
  if (principle.length > 120) principle = `${principle.slice(0, 117)}...`;
304
305
  return principle.trim();
@@ -306,7 +307,7 @@ function synthesizePrinciple(group: PatternGroup): string {
306
307
 
307
308
  // ── Main Analysis ──
308
309
 
309
- export interface AnalyzeOptions {
310
+ interface AnalyzeOptions {
310
311
  /** Generate actionable recommendations via inference. Default: false (patterns only). */
311
312
  actionable?: boolean;
312
313
  }
@@ -4,7 +4,11 @@
4
4
 
5
5
  import { HAIKU_MODEL } from "./models";
6
6
 
7
- export interface InferenceOptions {
7
+ export function hasApiKey(): boolean {
8
+ return !!process.env.PAL_ANTHROPIC_API_KEY;
9
+ }
10
+
11
+ interface InferenceOptions {
8
12
  system?: string;
9
13
  user: string;
10
14
  model?: string;
@@ -14,7 +18,7 @@ export interface InferenceOptions {
14
18
  jsonSchema?: Record<string, unknown>;
15
19
  }
16
20
 
17
- export interface InferenceResult {
21
+ interface InferenceResult {
18
22
  success: boolean;
19
23
  output?: string;
20
24
  usage?: { inputTokens: number; outputTokens: number };
@@ -3,7 +3,7 @@
3
3
  * Used by both learning.ts and work-learning.ts handlers.
4
4
  */
5
5
 
6
- export type LearningCategory = "system" | "algorithm";
6
+ type LearningCategory = "system" | "algorithm";
7
7
 
8
8
  const SYSTEM_KEYWORDS =
9
9
  /\b(config|setting|install|deploy|build|lint|format|biome|typescript|tsc|hook|plugin|ci|cd|pipeline|docker|package|dependency|migration|schema|database|env|permission|security|git|commit|branch|merge)\b/i;
@@ -19,6 +19,7 @@ export interface FailureEntry {
19
19
  principle: string;
20
20
  date: string;
21
21
  ts: string;
22
+ cwd: string;
22
23
  }
23
24
 
24
25
  export interface LearningEntry {
@@ -76,6 +77,7 @@ export function readFailures(baseDir: string, limit?: number): FailureEntry[] {
76
77
  date?: string;
77
78
  ts?: string;
78
79
  slug?: string;
80
+ cwd?: string;
79
81
  }>(content);
80
82
 
81
83
  if (!meta.context) continue;
@@ -88,6 +90,7 @@ export function readFailures(baseDir: string, limit?: number): FailureEntry[] {
88
90
  principle: meta.principle || "",
89
91
  date: meta.date || (meta.ts ? String(meta.ts).slice(0, 10) : ""),
90
92
  ts: meta.ts ? String(meta.ts) : "",
93
+ cwd: meta.cwd || "",
91
94
  });
92
95
 
93
96
  if (limit && entries.length >= limit) return entries;
@@ -128,7 +131,9 @@ export function readLearnings(baseDir: string, limit?: number): LearningEntry[]
128
131
 
129
132
  if (!meta.title) continue;
130
133
 
131
- const insightsMatch = body.match(/## Insights\n([\s\S]*?)(?=\n##|$)/);
134
+ const insightsMatch = new RegExp(/## Insights\n([\s\S]*?)(?=\n##|$)/).exec(
135
+ body
136
+ );
132
137
 
133
138
  entries.push({
134
139
  filename: file,
@@ -20,11 +20,11 @@ function spawnSilent(cmd: string, args: string[]): Promise<void> {
20
20
  }
21
21
 
22
22
  function escapeAppleScript(s: string): string {
23
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
23
+ return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
24
24
  }
25
25
 
26
26
  function escapePowerShellSingle(s: string): string {
27
- return s.replace(/'/g, "''");
27
+ return s.replaceAll("'", "''");
28
28
  }
29
29
 
30
30
  export async function notify(title: string, body: string): Promise<void> {