portable-agent-layer 0.35.0 → 0.36.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 (37) hide show
  1. package/README.md +1 -1
  2. package/assets/skills/projects/SKILL.md +0 -1
  3. package/assets/skills/telos/SKILL.md +7 -52
  4. package/assets/templates/PAL/ALGORITHM.md +28 -3
  5. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  6. package/assets/templates/PAL/README.md +1 -1
  7. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  8. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  9. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  10. package/assets/templates/pal-settings.json +1 -3
  11. package/assets/templates/settings.claude.json +2 -1
  12. package/package.json +1 -1
  13. package/src/cli/setup-telos.ts +12 -79
  14. package/src/hooks/LoadContext.ts +22 -10
  15. package/src/hooks/handlers/context-digests.ts +74 -0
  16. package/src/hooks/handlers/session-intelligence.ts +9 -86
  17. package/src/hooks/lib/claude-md.ts +69 -14
  18. package/src/hooks/lib/context.ts +57 -139
  19. package/src/hooks/lib/relationship.ts +3 -3
  20. package/src/hooks/lib/security.ts +2 -0
  21. package/src/hooks/lib/semi-static.ts +186 -0
  22. package/src/hooks/lib/setup.ts +0 -5
  23. package/src/hooks/lib/stop.ts +3 -0
  24. package/src/targets/claude/uninstall.ts +1 -1
  25. package/src/targets/copilot/install.ts +39 -8
  26. package/src/targets/copilot/uninstall.ts +58 -17
  27. package/src/targets/cursor/install.ts +8 -0
  28. package/src/targets/cursor/uninstall.ts +18 -1
  29. package/src/targets/lib.ts +26 -0
  30. package/src/targets/opencode/install.ts +29 -1
  31. package/src/targets/opencode/plugin.ts +1 -1
  32. package/src/targets/opencode/uninstall.ts +30 -3
  33. package/src/tools/agent/handoff-note.ts +116 -0
  34. package/src/tools/agent/relationship-note.ts +51 -0
  35. package/src/tools/relationship-reflect.ts +2 -2
  36. package/src/tools/self-model.ts +4 -4
  37. package/assets/templates/telos/PROJECTS.md +0 -7
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Stop handler: unified session intelligence capture.
3
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.
4
+ * Produces: title, summary, insights via Haiku.
5
+ * Writes: session learning file, project history.
7
6
  *
8
- * Replaces: work-learning.ts + relationship.ts (both still exist but are bypassed).
7
+ * Relationship notes written in ALGORITHM LEARN phase via relationship-note.ts
8
+ * Handoff notes → written in ALGORITHM LEARN phase via handoff-note.ts
9
+ *
10
+ * Replaces: work-learning.ts (still exists but is bypassed).
9
11
  */
10
12
 
11
13
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
@@ -15,7 +17,6 @@ import { inference } from "../lib/inference";
15
17
  import { categorizeLearning } from "../lib/learning-category";
16
18
  import { logDebug, logError } from "../lib/log";
17
19
  import { ensureDir, paths } from "../lib/paths";
18
- import { appendNotes, hasSessionNotes, type RelationshipNote } from "../lib/relationship";
19
20
  import { fileTimestamp, monthPath } from "../lib/time";
20
21
  import { logTokenUsage } from "../lib/token-usage";
21
22
  import {
@@ -110,25 +111,8 @@ const INTELLIGENCE_SCHEMA = {
110
111
  description:
111
112
  "If status is in-progress: what remains to be done, key decisions made, blockers. If completed: empty string.",
112
113
  },
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
114
  },
131
- required: ["title", "summary", "insights", "handoff", "observations"] as const,
115
+ required: ["title", "summary", "insights", "handoff"] as const,
132
116
  };
133
117
 
134
118
  interface IntelligenceOutput {
@@ -136,7 +120,6 @@ interface IntelligenceOutput {
136
120
  summary: string;
137
121
  insights: string;
138
122
  handoff: string;
139
- observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
140
123
  }
141
124
 
142
125
  // ── Main handler ──
@@ -160,9 +143,6 @@ export async function captureSessionIntelligence(
160
143
  return;
161
144
  }
162
145
 
163
- // Relationship dedup — skip relationship capture if already done for this session
164
- const skipRelationship = sessionId ? hasSessionNotes(sessionId) : false;
165
-
166
146
  // Extract transcript windows
167
147
  const userMessages = messages
168
148
  .filter((m) => m.role === "user")
@@ -174,8 +154,7 @@ export async function captureSessionIntelligence(
174
154
  const lastUser = extractLastUser(messages);
175
155
  const status = detectStatus(lastAssistantText);
176
156
 
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));
157
+ const userWindow = userMessages.slice(-10).map((t) => t.slice(0, 200));
179
158
  const assistantWindow = lastAssistantText.slice(0, 600);
180
159
 
181
160
  if (userWindow.length < 3) return;
@@ -195,12 +174,9 @@ export async function captureSessionIntelligence(
195
174
  status === "in-progress"
196
175
  ? "4. handoff: what remains unfinished — decisions made so far, next steps, blockers (2-4 sentences)"
197
176
  : "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
177
  ].join("\n"),
202
178
  user: `User messages:\n${userWindow.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n\nLast AI response:\n${assistantWindow}`,
203
- maxTokens: 500,
179
+ maxTokens: 350,
204
180
  timeout: 15000,
205
181
  jsonSchema: INTELLIGENCE_SCHEMA,
206
182
  });
@@ -218,8 +194,6 @@ export async function captureSessionIntelligence(
218
194
  const title = output?.title || extractContent(lastUser).slice(0, 80) || "session";
219
195
  const summary = output?.summary || lastAssistantText.slice(0, 600);
220
196
  const insights = output?.insights || "";
221
- const handoff = output?.handoff || "";
222
-
223
197
  // ── Write session learning file ──
224
198
 
225
199
  const category = categorizeLearning(title, summary);
@@ -241,7 +215,6 @@ export async function captureSessionIntelligence(
241
215
  "",
242
216
  "## Insights",
243
217
  insights || "*No insights captured.*",
244
- ...(handoff ? ["", "## Handoff", handoff] : []),
245
218
  ].join("\n");
246
219
 
247
220
  const content = stringify(meta, body);
@@ -271,54 +244,4 @@ export async function captureSessionIntelligence(
271
244
 
272
245
  if (sessionId) markCaptured(sessionId, filepath, messages.length);
273
246
  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
247
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Dynamic AGENTS.md generation.
2
+ * Dynamic AGENTS.md / CLAUDE.md generation.
3
3
  *
4
- * AGENTS.md is regenerated when setup.json or any telos file is newer than
5
- * the existing AGENTS.md. The template lives at AGENTS.md.template.
6
- * CLAUDE.md is kept as a symlink pointing to AGENTS.md.
4
+ * AGENTS.md (opencode, codex, copilot) is regenerated when setup.json or any
5
+ * telos file is newer. CLAUDE.md (Claude Code) is a real file — not a symlink —
6
+ * and prepends an @import for the self-model so that large static context loads
7
+ * natively rather than through the hook's stdout.
7
8
  */
8
9
 
9
10
  import {
@@ -19,6 +20,7 @@ import {
19
20
  } from "node:fs";
20
21
  import { dirname, relative, resolve } from "node:path";
21
22
  import { assets, ensureDir, paths, platform } from "./paths";
23
+ import { getSemiStaticSources } from "./semi-static";
22
24
 
23
25
  const TEMPLATE_PATH = assets.agentsMdTemplate();
24
26
 
@@ -70,16 +72,12 @@ function ensureOneSymlink(linkPath: string, targetPath: string): void {
70
72
  }
71
73
  }
72
74
 
73
- /** Ensure all agent symlinks point to the canonical AGENTS.md */
75
+ /** Ensure codex symlink points to the canonical AGENTS.md.
76
+ * CLAUDE.md for Claude Code is a real file written by ensureClaudeCodeMd().
77
+ * Copilot uses ~/.copilot/instructions/*.instructions.md — no symlink needed. */
74
78
  function ensureSymlinks(): void {
75
- const { outputPath, symlinkPath } = getOutputPaths();
76
- ensureOneSymlink(symlinkPath, outputPath);
79
+ const { outputPath } = getOutputPaths();
77
80
  ensureOneSymlink(resolve(platform.codexDir(), "AGENTS.md"), outputPath);
78
- // Copilot instructions — only create if ~/.copilot/ already exists (i.e. Copilot is installed)
79
- const copilotDir = platform.copilotDir();
80
- if (existsSync(copilotDir)) {
81
- ensureOneSymlink(resolve(copilotDir, "copilot-instructions.md"), outputPath);
82
- }
83
81
  }
84
82
 
85
83
  /** Returns true if AGENTS.md needs to be regenerated */
@@ -89,11 +87,12 @@ export function needsRebuild(): boolean {
89
87
 
90
88
  const outputMtime = statSync(outputPath).mtimeMs;
91
89
 
92
- // Collect source files: template + setup.json + identity + PAL docs
90
+ // Collect source files: template + setup.json + identity + PAL docs + @import candidates
93
91
  const sources: string[] = [
94
92
  TEMPLATE_PATH,
95
93
  resolve(paths.state(), "setup.json"),
96
94
  resolve(paths.memory(), "pal-settings.json"),
95
+ ...getSemiStaticSources().map((s) => s.path),
97
96
  ];
98
97
 
99
98
  // Track PAL doc sources for rebuild detection
@@ -124,15 +123,71 @@ export function buildClaudeMd(): string {
124
123
  .replaceAll("{{PRINCIPAL_NAME}}", id.principal.name);
125
124
  }
126
125
 
127
- /** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
126
+ /** Build @import header lines for CLAUDE.md one line per semi-static file that exists. */
127
+ function buildClaudeCodeImports(): string {
128
+ const claudeDir = platform.claudeDir();
129
+
130
+ const lines = getSemiStaticSources()
131
+ .map((s) => s.path)
132
+ .filter((p) => existsSync(p))
133
+ .map((p) => `@${relative(claudeDir, p).replaceAll("\\", "/")}`);
134
+
135
+ return lines.length > 0 ? `${lines.join("\n")}\n\n` : "";
136
+ }
137
+
138
+ /** Build CLAUDE.md content for Claude Code — prepends @import for self-model. */
139
+ export function buildClaudeCodeMd(): string {
140
+ return buildClaudeCodeImports() + buildClaudeMd();
141
+ }
142
+
143
+ /** Write ~/.claude/CLAUDE.md as a real file (upgrading from symlink if needed).
144
+ * Also rewrites if the @import header has changed (new digest files appeared). */
145
+ function ensureClaudeCodeMd(): void {
146
+ const claudeDir = platform.claudeDir();
147
+ if (!claudeDir) return;
148
+ const claudeMdPath = resolve(claudeDir, "CLAUDE.md");
149
+ const expected = buildClaudeCodeMd();
150
+ try {
151
+ if (existsSync(claudeMdPath) && !lstatSync(claudeMdPath).isSymbolicLink()) {
152
+ const current = readFileSync(claudeMdPath, "utf-8");
153
+ if (current === expected) return; // no change needed
154
+ // @imports changed — rewrite
155
+ } else if (existsSync(claudeMdPath)) {
156
+ unlinkSync(claudeMdPath); // remove symlink
157
+ }
158
+ } catch {
159
+ /* fall through */
160
+ }
161
+ try {
162
+ ensureDir(claudeDir);
163
+ writeFileSync(claudeMdPath, expected, "utf-8");
164
+ } catch {
165
+ /* ignore write errors — non-fatal */
166
+ }
167
+ }
168
+
169
+ /** Regenerate AGENTS.md if any source file is newer, write real CLAUDE.md, ensure other symlinks. Returns true if rebuilt. */
128
170
  export function regenerateIfNeeded(): boolean {
129
171
  const { outputPath } = getOutputPaths();
130
172
  if (!needsRebuild()) {
131
173
  ensureSymlinks();
174
+ ensureClaudeCodeMd();
132
175
  return false;
133
176
  }
134
177
  ensureDir(dirname(outputPath));
135
178
  writeFileSync(outputPath, buildClaudeMd(), "utf-8");
179
+ // Write Claude Code's CLAUDE.md as a real file (removing any existing symlink)
180
+ const claudeDir = platform.claudeDir();
181
+ if (claudeDir) {
182
+ const claudeMdPath = resolve(claudeDir, "CLAUDE.md");
183
+ try {
184
+ if (existsSync(claudeMdPath)) unlinkSync(claudeMdPath);
185
+ ensureDir(claudeDir);
186
+ writeFileSync(claudeMdPath, buildClaudeCodeMd(), "utf-8");
187
+ } catch {
188
+ /* ignore — CLAUDE.md write failure is non-fatal */
189
+ }
190
+ }
136
191
  ensureSymlinks();
137
192
  return true;
138
193
  }
@@ -3,21 +3,21 @@
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";
14
+ import { loadFailurePatterns, loadSynthesisRecommendations } from "./semi-static";
15
15
  import { readSessionNames } from "./session-names";
16
16
  import * as settings from "./settings";
17
17
  import { isSetupComplete, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
18
18
  import { computeSignalTrends, formatTrends } from "./signal-trends";
19
19
  import { readFramePrinciples } from "./wisdom";
20
- import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
20
+ import { readProjectHistory, readSessions } from "./work-tracking";
21
21
 
22
22
  /** Load and concatenate loadAtStartup files */
23
23
  function loadStartupFiles(): string {
@@ -53,47 +53,6 @@ export function countSignals(filename: string): number {
53
53
  }
54
54
  }
55
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
56
  /** Load the N most recent session names (fallback for greeting) */
98
57
  export function loadRecentSessions(count: number): string[] {
99
58
  try {
@@ -140,7 +99,6 @@ function loadCachedCounts(): {
140
99
  /** Build the visible greeting lines for stderr */
141
100
  export function buildGreeting(): string[] {
142
101
  const counts = loadCachedCounts();
143
- const work = loadActiveWork();
144
102
  const setupState = readSetupState();
145
103
  const setupIncomplete = setupState && !isSetupComplete(setupState);
146
104
 
@@ -157,10 +115,6 @@ export function buildGreeting(): string[] {
157
115
  );
158
116
  }
159
117
 
160
- if (work?.summary) {
161
- greeting.push(`📋 Previous: ${work.summary}`);
162
- }
163
-
164
118
  // Show recent session names for quick context
165
119
  const recent = loadRecentSessions(3);
166
120
  if (recent.length > 0) {
@@ -217,78 +171,6 @@ export function loadSelfModel(): string {
217
171
  }
218
172
  }
219
173
 
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
174
  /** Load signal trends as a formatted string */
293
175
  export function loadSignalTrends(): string {
294
176
  try {
@@ -322,7 +204,12 @@ export function loadRelationshipContext(): string {
322
204
  try {
323
205
  const notes = loadRecentNotes(2);
324
206
  if (!notes) return "";
325
- return `## Recent Interaction Notes\n${notes}`;
207
+ // Strip O entries (opinions loaded natively via digest) and HTML comment lines
208
+ const filtered = notes
209
+ .split("\n")
210
+ .filter((l) => !/^\s*- O\(/.test(l) && !/^\s*<!--/.test(l))
211
+ .join("\n");
212
+ return capSection(`## Recent Interaction Notes\n${filtered}`, 1500);
326
213
  } catch {
327
214
  return "";
328
215
  }
@@ -337,11 +224,10 @@ export function loadSessionIntelligence(): string {
337
224
 
338
225
  const lines: string[] = ["## Session Intelligence"];
339
226
 
340
- // Open Threads — project-specific first, then global
227
+ // Open Threads — project-specific only
341
228
  if (state.threads?.length > 0) {
342
229
  const cwd = process.cwd();
343
230
  const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
344
- const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
345
231
 
346
232
  if (here.length > 0) {
347
233
  lines.push("");
@@ -350,11 +236,10 @@ export function loadSessionIntelligence(): string {
350
236
  lines.push(`- ${t.title} (opened ${t.opened})`);
351
237
  if (t.context) lines.push(` ${t.context}`);
352
238
  }
353
- lines.push("→ These are directly relevant to your current work.");
239
+ lines.push(
240
+ "→ Continue this work or explicitly close it before starting something new."
241
+ );
354
242
  }
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
243
  }
359
244
 
360
245
  // Rating Trend
@@ -403,7 +288,7 @@ export function loadSessionIntelligence(): string {
403
288
  }
404
289
  }
405
290
 
406
- return lines.length > 1 ? lines.join("\n") : "";
291
+ return lines.length > 1 ? capSection(lines.join("\n"), 2000) : "";
407
292
  } catch {
408
293
  return "";
409
294
  }
@@ -434,17 +319,46 @@ export function loadHandoff(): string {
434
319
  }
435
320
  }
436
321
 
322
+ /** Truncate text to maxChars at the last complete line boundary */
323
+ function capSection(text: string, maxChars: number): string {
324
+ if (text.length <= maxChars) return text;
325
+ const lines = text.split("\n");
326
+ const kept: string[] = [];
327
+ let total = 0;
328
+ for (const line of lines) {
329
+ const next = total + line.length + 1;
330
+ if (next > maxChars) break;
331
+ kept.push(line);
332
+ total = next;
333
+ }
334
+ return kept.join("\n");
335
+ }
336
+
337
+ /** Agent targets — determines which context sections are skipped due to native loading. */
338
+ export type AgentTarget = "claude" | "opencode" | "cursor" | "copilot";
339
+
437
340
  /**
438
341
  * Build the <system-reminder> content for the AI.
439
342
  *
440
343
  * Static context (TELOS, setup prompt) lives in AGENTS.md / CLAUDE.md and is
441
344
  * loaded natively by Claude Code / opencode. This injects dynamic context only —
442
345
  * things that change per-session and can't live in a static file.
346
+ *
347
+ * opts.agent — agent target; Claude Code skips semi-static sections (self-model,
348
+ * wisdom, opinions) that load natively via @imports in CLAUDE.md.
443
349
  */
444
- export function buildSystemReminder(): string {
350
+ export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string {
351
+ // Semi-static sections loaded natively via @imports (Claude Code) or
352
+ // instructions[] (opencode). Skip them from hook output for those agents.
353
+ const skipSemiStatic =
354
+ opts.agent === "claude" ||
355
+ opts.agent === "opencode" ||
356
+ opts.agent === "cursor" ||
357
+ opts.agent === "copilot";
358
+
445
359
  const startup = loadStartupFiles();
446
- const work = settings.isEnabled("activeWork") ? loadActiveWork() : null;
447
- const wisdom = settings.isEnabled("wisdom") ? loadWisdomContext() : "";
360
+ const wisdom =
361
+ !skipSemiStatic && settings.isEnabled("wisdom") ? loadWisdomContext() : "";
448
362
  const relationship = settings.isEnabled("relationship")
449
363
  ? loadRelationshipContext()
450
364
  : "";
@@ -456,10 +370,16 @@ export function buildSystemReminder(): string {
456
370
  ? loadActiveProjectsContext()
457
371
  : "";
458
372
  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() : "";
373
+ const failures =
374
+ settings.isEnabled("failurePatterns") && !skipSemiStatic ? loadFailurePatterns() : "";
375
+ const synthesis =
376
+ settings.isEnabled("synthesis") && !skipSemiStatic
377
+ ? loadSynthesisRecommendations()
378
+ : "";
379
+ const opinions =
380
+ !skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
381
+ const selfModel =
382
+ !skipSemiStatic && settings.isEnabled("selfModel") ? loadSelfModel() : "";
463
383
  const intelligence = settings.isEnabled("sessionIntelligence")
464
384
  ? loadSessionIntelligence()
465
385
  : "";
@@ -478,8 +398,6 @@ export function buildSystemReminder(): string {
478
398
  if (synthesis) parts.push(synthesis);
479
399
  if (trends) parts.push(trends);
480
400
  if (failures) parts.push(failures);
481
- if (work) parts.push(work.text);
482
-
483
401
  if (parts.length === 0) return "";
484
402
 
485
403
  const now = new Date();
@@ -5,7 +5,7 @@
5
5
  * Notes live at memory/relationship/YYYY-MM/YYYY-MM-DD.md
6
6
  * W = world (facts about user's situation)
7
7
  * O = opinion (preference with confidence)
8
- * B = biographical (what the AI did this session, first-person)
8
+ * Session = what the AI did this session (first-person)
9
9
  *
10
10
  * Extraction is handled by the relationship handler via Haiku inference.
11
11
  * This lib provides storage and reading utilities only.
@@ -15,7 +15,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
15
15
  import { resolve } from "node:path";
16
16
  import { ensureDir, paths } from "./paths";
17
17
 
18
- export type NoteType = "W" | "O" | "B";
18
+ export type NoteType = "W" | "O" | "Session";
19
19
 
20
20
  export interface RelationshipNote {
21
21
  type: NoteType;
@@ -31,7 +31,7 @@ function dailyFilePath(date: Date): string {
31
31
  return resolve(monthDir, `${yyyy}-${mm}-${dd}.md`);
32
32
  }
33
33
 
34
- /** Check if a session already has notes in today's file */
34
+ /** @deprecated No longer called relationship notes are written in ALGORITHM LEARN phase */
35
35
  export function hasSessionNotes(sessionId: string): boolean {
36
36
  const filepath = dailyFilePath(new Date());
37
37
  if (!existsSync(filepath)) return false;
@@ -63,6 +63,8 @@ export const PROTECTED_PATHS: RegExp[] = [
63
63
  /^\/System\//,
64
64
  /\.ssh\/(?!config)/,
65
65
  /\.gnupg\//,
66
+ // Claude Code auto-memory — PAL owns memory; writes here indicate wrong system is being used
67
+ /\.claude\/projects\/[^/]+\/memory\//,
66
68
  // Derived from HOOK_MANAGED_FILES — scoped to managed roots only
67
69
  ...HOOK_MANAGED_FILES.map(
68
70
  (name) =>