portable-agent-layer 0.34.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 (43) hide show
  1. package/README.md +1 -1
  2. package/assets/skills/presentation/SKILL.md +2 -0
  3. package/assets/skills/presentation/demo/slides/004-content.md +27 -1
  4. package/assets/skills/presentation/theme-base/base.css +206 -0
  5. package/assets/skills/presentation/theme-base/skeleton.html +49 -0
  6. package/assets/skills/presentation/tools/lib/lint-rules.ts +25 -0
  7. package/assets/skills/projects/SKILL.md +0 -1
  8. package/assets/skills/telos/SKILL.md +7 -52
  9. package/assets/templates/AGENTS.md.template +2 -1
  10. package/assets/templates/PAL/ALGORITHM.md +28 -3
  11. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  12. package/assets/templates/PAL/README.md +1 -1
  13. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  14. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  15. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  16. package/assets/templates/pal-settings.json +1 -3
  17. package/assets/templates/settings.claude.json +2 -1
  18. package/package.json +1 -1
  19. package/src/cli/setup-telos.ts +12 -79
  20. package/src/hooks/LoadContext.ts +22 -10
  21. package/src/hooks/handlers/context-digests.ts +74 -0
  22. package/src/hooks/handlers/session-intelligence.ts +9 -86
  23. package/src/hooks/lib/claude-md.ts +69 -14
  24. package/src/hooks/lib/context.ts +57 -139
  25. package/src/hooks/lib/relationship.ts +3 -3
  26. package/src/hooks/lib/security.ts +2 -0
  27. package/src/hooks/lib/semi-static.ts +186 -0
  28. package/src/hooks/lib/setup.ts +0 -5
  29. package/src/hooks/lib/stop.ts +3 -0
  30. package/src/targets/claude/uninstall.ts +1 -1
  31. package/src/targets/copilot/install.ts +39 -8
  32. package/src/targets/copilot/uninstall.ts +58 -17
  33. package/src/targets/cursor/install.ts +8 -0
  34. package/src/targets/cursor/uninstall.ts +18 -1
  35. package/src/targets/lib.ts +26 -0
  36. package/src/targets/opencode/install.ts +29 -1
  37. package/src/targets/opencode/plugin.ts +1 -1
  38. package/src/targets/opencode/uninstall.ts +30 -3
  39. package/src/tools/agent/handoff-note.ts +116 -0
  40. package/src/tools/agent/relationship-note.ts +51 -0
  41. package/src/tools/relationship-reflect.ts +2 -2
  42. package/src/tools/self-model.ts +4 -4
  43. package/assets/templates/telos/PROJECTS.md +0 -7
@@ -233,6 +233,7 @@ Brief description.
233
233
  │ │ - Work learning capture
234
234
  │ │ - Failure logging
235
235
  │ │ - Reflect trigger check
236
+ │ │ - Write context digests (semi-static sources)
236
237
  │ │ - Auto-backup
237
238
  │ │ - Count updates
238
239
  │ │ - Tab reset
@@ -357,34 +358,44 @@ Relationship notes (O/B types)
357
358
 
358
359
  ## Context Loading Architecture
359
360
 
360
- ### Two-Layer Design
361
+ ### Three-Tier Design
361
362
 
362
- **Static context** (loaded natively by the agent):
363
- - CLAUDE.md — identity, modes, context routing table
364
- - Loaded once at session start, always available
363
+ **Tier 1 — Operational** (loaded natively at agent startup):
364
+ - CLAUDE.md / AGENTS.md — identity, modes, context routing table
365
+ - Loaded once per session, always available, never re-fetched
365
366
 
366
- **Dynamic context** (injected by LoadContext hook):
367
- - Changes per-session, can't live in a static file
368
- - Injected as `<system-reminder>` block to stdout
367
+ **Tier 2 — Semi-static** (pre-compiled at previous session stop, loaded natively):
368
+ - Self-model, wisdom, opinions, synthesis, failures, steering rules
369
+ - Written to disk by `writeContextDigests()` at Stop time
370
+ - Loaded natively per-agent: `@imports` in CLAUDE.md (Claude Code), `instructions[]` in opencode config, `.mdc` rules in `~/.cursor/rules/` (Cursor), `.instructions.md` in `~/.copilot/instructions/` (Copilot)
371
+ - Content is global/user-level — safe to pre-compile (not project-scoped)
372
+
373
+ **Tier 3 — Dynamic** (injected fresh each session by LoadContext hook):
374
+ - Handoff notes, session intelligence, open threads, relationship notes, project history
375
+ - Changes per-session or is project-scoped — can't be pre-compiled
376
+ - Injected as `<system-reminder>` block via stdout
369
377
  - Each section independently toggleable in `pal-settings.json → dynamicContext`
370
378
 
371
- ### Injection Order
379
+ ### Semi-Static Registry
380
+
381
+ All semi-static sources are defined in `src/hooks/lib/semi-static.ts` via `getSemiStaticSources()`. Adding one entry there propagates automatically to every consumer — no other files need to change.
382
+
383
+ ### Dynamic Injection Order
372
384
 
373
385
  ```
374
386
  LoadContext.ts
375
387
 
376
388
  ├─► Regenerate CLAUDE.md if template/telos changed
377
389
 
378
- └─► Build system-reminder:
390
+ └─► Build system-reminder (dynamic sections only):
379
391
  1. loadAtStartup files (user-configured)
380
- 2. Crystallized principles (wisdom frames)
381
- 3. Tracked opinions (≥85% confidence)
382
- 4. Recent interaction notes (last 2 days)
383
- 5. Learning digest (this project + other recent)
384
- 6. Pattern synthesis recommendations
385
- 7. Signal trends (today/week/trend)
386
- 8. Failure patterns (last 5 low-rating contexts)
387
- 9. Active work summary (sessions + projects)
392
+ 2. Handoff note (in-progress work from last session)
393
+ 3. Session intelligence (rating trend, algorithm performance)
394
+ 4. Open threads (current project only)
395
+ 5. Recent interaction notes (last 2 days)
396
+ 6. Active projects
397
+ 7. Project session history (this project)
398
+ 8. Signal trends (today/week/trend)
388
399
  ```
389
400
 
390
401
  ### On-Demand Context
@@ -436,6 +447,9 @@ src/targets/
436
447
  ├── cursor/ # Cursor specific
437
448
  │ ├── install.ts # Register hooks + skills in ~/.cursor/
438
449
  │ └── uninstall.ts
450
+ ├── copilot/ # GitHub Copilot specific
451
+ │ ├── install.ts # Write instruction files + update VS Code settings
452
+ │ └── uninstall.ts
439
453
  └── lib.ts # Shared: JSON read/write, settings merge, TELOS scaffold
440
454
  ```
441
455
 
@@ -452,6 +466,7 @@ All paths resolve through `src/hooks/lib/paths.ts`:
452
466
  | Claude config | `~/.claude` | `PAL_CLAUDE_DIR` |
453
467
  | opencode config | `~/.config/opencode` | `PAL_OPENCODE_DIR` |
454
468
  | Cursor config | `~/.cursor` | `PAL_CURSOR_DIR` |
469
+ | Copilot config | `~/.copilot` | `PAL_COPILOT_DIR` |
455
470
  | Codex config | `~/.codex` | `PAL_CODEX_DIR` |
456
471
  | Agents dir | `~/.agents` | `PAL_AGENTS_DIR` |
457
472
 
@@ -4,4 +4,4 @@ PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captu
4
4
 
5
5
  ## Projects
6
6
 
7
- Projects are managed in `telos/PROJECTS.md` and force-loaded at session startup via `pal-settings.json loadAtStartup.files`.
7
+ Projects are managed via the `/projects` skill. State lives in `~/.pal/memory/state/progress/{slug}.json`, one file per project. Inspect with `bun ~/.pal/tools/project.ts list`; manage via `~/.pal/docs/PROJECT_LIFECYCLE.md`.
@@ -13,9 +13,7 @@
13
13
  },
14
14
  "loadAtStartup": {
15
15
  "_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
16
- "files": [
17
- "~/.pal/docs/STEERING_RULES.md"
18
- ]
16
+ "files": []
19
17
  },
20
18
  "dynamicContext": {
21
19
  "_docs": "Dynamic context sections injected at session start. Set to false to disable.",
@@ -97,5 +97,6 @@
97
97
  ]
98
98
  }
99
99
  ]
100
- }
100
+ },
101
+ "autoMemoryEnabled": false
101
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,72 +7,9 @@
7
7
  import { writeFileSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import * as clack from "@clack/prompts";
10
- import { upsertProject } from "../../assets/skills/telos/tools/update-projects";
11
10
  import { palHome } from "../hooks/lib/paths";
12
11
  import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
13
12
 
14
- function toKebabCase(name: string): string {
15
- return name
16
- .toLowerCase()
17
- .replace(/[^a-z0-9]+/g, "-")
18
- .replace(/^-|-$/g, "");
19
- }
20
-
21
- async function promptProjectsLoop(): Promise<void> {
22
- const addFirst = await clack.confirm({
23
- message: "Do you want to add any projects now?",
24
- initialValue: true,
25
- });
26
- if (clack.isCancel(addFirst) || !addFirst) return;
27
-
28
- let addMore = true;
29
- while (addMore) {
30
- const name = await clack.text({
31
- message: "Project name?",
32
- placeholder: "e.g. PAL, My SaaS, Work Dashboard",
33
- });
34
- if (clack.isCancel(name)) return;
35
-
36
- const status = await clack.select({
37
- message: "Status?",
38
- options: [
39
- { value: "Active", label: "Active" },
40
- { value: "Planning", label: "Planning" },
41
- { value: "Paused", label: "Paused" },
42
- { value: "Complete", label: "Complete" },
43
- ],
44
- });
45
- if (clack.isCancel(status)) return;
46
-
47
- const priority = await clack.select({
48
- message: "Priority?",
49
- options: [
50
- { value: "High", label: "High" },
51
- { value: "Medium", label: "Medium" },
52
- { value: "Low", label: "Low" },
53
- ],
54
- });
55
- if (clack.isCancel(priority)) return;
56
-
57
- const notes = await clack.text({
58
- message: "Notes? (optional — leave blank to skip)",
59
- placeholder: "e.g. Building the v2 API, blocked on design review",
60
- });
61
- if (clack.isCancel(notes)) return;
62
-
63
- const id = toKebabCase(name as string);
64
- const row = `| ${id} | ${name} | ${status} | ${priority} | ${notes || ""} |`;
65
- upsertProject(id, row, `Added ${name} during PAL setup`);
66
- clack.log.success(`Added: ${name}`);
67
-
68
- const again = await clack.confirm({
69
- message: "Add another project?",
70
- initialValue: false,
71
- });
72
- if (clack.isCancel(again) || !again) addMore = false;
73
- }
74
- }
75
-
76
13
  /** Prompt for missing TELOS context. Skips any step whose file already has real content. */
77
14
  export async function promptTelos(): Promise<void> {
78
15
  // Skip interactive prompts in non-TTY environments (tests, CI)
@@ -95,25 +32,21 @@ export async function promptTelos(): Promise<void> {
95
32
  );
96
33
 
97
34
  for (const key of pending) {
98
- if (key === "projects") {
99
- await promptProjectsLoop();
100
- } else {
101
- const step = SETUP_STEPS[key];
102
- const title = key.charAt(0).toUpperCase() + key.slice(1);
103
-
104
- const answer = await clack.text({
105
- message: step.question,
106
- placeholder: step.hint,
107
- });
35
+ const step = SETUP_STEPS[key];
36
+ const title = key.charAt(0).toUpperCase() + key.slice(1);
108
37
 
109
- if (clack.isCancel(answer)) {
110
- clack.cancel("Setup cancelled");
111
- return;
112
- }
38
+ const answer = await clack.text({
39
+ message: step.question,
40
+ placeholder: step.hint,
41
+ });
113
42
 
114
- const filePath = resolve(home, step.file);
115
- writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
43
+ if (clack.isCancel(answer)) {
44
+ clack.cancel("Setup cancelled");
45
+ return;
116
46
  }
47
+
48
+ const filePath = resolve(home, step.file);
49
+ writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
117
50
  }
118
51
 
119
52
  clack.outro("Personal context saved ✓");
@@ -9,10 +9,10 @@
9
9
  * context directly to ~/.copilot/copilot-instructions.md so it is picked up on load.
10
10
  */
11
11
 
12
- import { existsSync, lstatSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { mkdirSync, writeFileSync } from "node:fs";
13
13
  import { resolve } from "node:path";
14
14
  import { buildClaudeMd, regenerateIfNeeded } from "./lib/claude-md";
15
- import { buildSystemReminder } from "./lib/context";
15
+ import { type AgentTarget, buildSystemReminder } from "./lib/context";
16
16
  import { logDebug, logError } from "./lib/log";
17
17
  import { platform } from "./lib/paths";
18
18
 
@@ -36,21 +36,33 @@ try {
36
36
 
37
37
  // --- Context to stdout (or file for Copilot) ---
38
38
  try {
39
- const reminder = buildSystemReminder();
39
+ // Determine agent target — controls which sections are skipped (loaded natively instead).
40
+ let agent: AgentTarget = "claude";
41
+ if (process.env.PAL_AGENT === "copilot") agent = "copilot";
42
+ else if (process.env.CURSOR_VERSION) agent = "cursor";
43
+ const reminder = buildSystemReminder({ agent });
40
44
  if (!reminder) process.exit(0);
41
45
 
42
46
  if (process.env.PAL_AGENT === "copilot") {
43
- // Copilot: sessionStart output is ignored write merged context to copilot-instructions.md
44
- const instructionsPath = resolve(platform.copilotDir(), "copilot-instructions.md");
47
+ // Copilot: semi-static in ~/.copilot/instructions/pal-*.instructions.md (written at stop).
48
+ // Write AGENTS.md + dynamic context to pal-session.instructions.md on each session start.
49
+ const instructionsDir = resolve(platform.copilotDir(), "instructions");
50
+ mkdirSync(instructionsDir, { recursive: true });
45
51
  const agentsMd = buildClaudeMd();
46
52
  const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
47
- if (existsSync(instructionsPath) && lstatSync(instructionsPath).isSymbolicLink()) {
48
- unlinkSync(instructionsPath);
53
+ if (context) {
54
+ writeFileSync(
55
+ resolve(instructionsDir, "pal-session.instructions.md"),
56
+ `---\napplyTo: "**"\n---\n\n${context}`,
57
+ "utf-8"
58
+ );
49
59
  }
50
- writeFileSync(instructionsPath, context, "utf-8");
51
- logDebug("LoadContext", `Copilot instructions written: ${context.length} chars`);
60
+ logDebug(
61
+ "LoadContext",
62
+ `Copilot session instructions written: ${context.length} chars`
63
+ );
52
64
  } else if (process.env.CURSOR_VERSION) {
53
- // Cursor: no native user-level rules — inject AGENTS.md + dynamic context
65
+ // Cursor: semi-static in ~/.cursor/rules/pal-context.mdc; inject AGENTS.md + dynamic here
54
66
  const agentsMd = buildClaudeMd();
55
67
  const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
56
68
  process.stdout.write(JSON.stringify({ additional_context: context }));
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Handler: write pre-compiled context digest files for @import / instructions[].
3
+ *
4
+ * Runs at session stop so that CLAUDE.md can @import these files natively
5
+ * at the next session start, keeping hook stdout small.
6
+ *
7
+ * Sources are defined in src/hooks/lib/semi-static.ts — add one entry there
8
+ * to extend coverage to all consumers (CLAUDE.md, opencode, Cursor, Copilot).
9
+ */
10
+
11
+ import { existsSync, writeFileSync } from "node:fs";
12
+ import { dirname, resolve } from "node:path";
13
+ import { ensureDir, platform } from "../lib/paths";
14
+ import {
15
+ copilotFilename,
16
+ cursorFilename,
17
+ getSemiStaticSources,
18
+ } from "../lib/semi-static";
19
+
20
+ export function writeContextDigests(): void {
21
+ const sources = getSemiStaticSources();
22
+
23
+ // Resolve Cursor/Copilot destination dirs once (null if agent not installed)
24
+ let rulesDir: string | null = null;
25
+ let instructionsDir: string | null = null;
26
+
27
+ try {
28
+ const cursorDir = platform.cursorDir();
29
+ if (existsSync(cursorDir)) {
30
+ rulesDir = ensureDir(resolve(cursorDir, "rules"));
31
+ }
32
+ } catch {
33
+ /* non-fatal */
34
+ }
35
+
36
+ try {
37
+ const copilotDir = platform.copilotDir();
38
+ if (existsSync(copilotDir)) {
39
+ instructionsDir = ensureDir(resolve(copilotDir, "instructions"));
40
+ }
41
+ } catch {
42
+ /* non-fatal */
43
+ }
44
+
45
+ for (const src of sources) {
46
+ try {
47
+ const content = src.load();
48
+ if (!content) continue;
49
+
50
+ if (src.writesDigest) {
51
+ ensureDir(dirname(src.path));
52
+ writeFileSync(src.path, content, "utf-8");
53
+ }
54
+
55
+ if (rulesDir) {
56
+ writeFileSync(
57
+ resolve(rulesDir, cursorFilename(src)),
58
+ `---\ndescription: ${src.description}\nalwaysApply: true\n---\n\n${content}`,
59
+ "utf-8"
60
+ );
61
+ }
62
+
63
+ if (instructionsDir) {
64
+ writeFileSync(
65
+ resolve(instructionsDir, copilotFilename(src)),
66
+ `---\napplyTo: "**"\n---\n\n${content}`,
67
+ "utf-8"
68
+ );
69
+ }
70
+ } catch {
71
+ /* non-fatal */
72
+ }
73
+ }
74
+ }
@@ -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
  }