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
@@ -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) =>
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Semi-static context source registry.
3
+ *
4
+ * One entry here = the only change needed to add a new source across all consumers:
5
+ * CLAUDE.md @imports, opencode instructions[], Cursor .mdc, Copilot .instructions.md,
6
+ * and the session-stop digest writer.
7
+ */
8
+
9
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { parse } from "./frontmatter";
12
+ import { readFailures } from "./learning-store";
13
+ import { loadOpinionContext } from "./opinions";
14
+ import { palHome, paths } from "./paths";
15
+ import { readFramePrinciples } from "./wisdom";
16
+
17
+ /** A single semi-static context source — built at session stop, loaded natively at session start. */
18
+ export interface SemiStaticSource {
19
+ /** Absolute path used in @imports (CLAUDE.md), instructions[] (opencode), and digest writes. */
20
+ readonly path: string;
21
+ /** When true, session-stop handler writes build result to path. */
22
+ readonly writesDigest: boolean;
23
+ /** Returns current content — builds fresh when writesDigest is true, reads the file otherwise. */
24
+ load(): string;
25
+ /** Slug for ~/.cursor/rules/pal-${slug}.mdc and ~/.copilot/instructions/pal-${slug}.instructions.md */
26
+ readonly slug: string;
27
+ /** Human-readable description for Cursor .mdc frontmatter. */
28
+ readonly description: string;
29
+ }
30
+
31
+ /** Returns the Cursor rules filename for a source. */
32
+ export function cursorFilename(src: SemiStaticSource): string {
33
+ return `pal-${src.slug}.mdc`;
34
+ }
35
+
36
+ /** Returns the Copilot instructions filename for a source. */
37
+ export function copilotFilename(src: SemiStaticSource): string {
38
+ return `pal-${src.slug}.instructions.md`;
39
+ }
40
+
41
+ function readFileSafe(path: string): string {
42
+ try {
43
+ if (!existsSync(path)) return "";
44
+ return readFileSync(path, "utf-8").trim();
45
+ } catch {
46
+ return "";
47
+ }
48
+ }
49
+
50
+ /** Build recommendations from the most recent synthesis report. */
51
+ export function loadSynthesisRecommendations(): string {
52
+ try {
53
+ const synthDir = paths.synthesis();
54
+ if (!existsSync(synthDir)) return "";
55
+
56
+ const months = readdirSync(synthDir).sort().reverse();
57
+ for (const month of months) {
58
+ const monthDir = resolve(synthDir, month);
59
+ try {
60
+ const files = readdirSync(monthDir)
61
+ .filter((f) => f.endsWith(".md"))
62
+ .sort()
63
+ .reverse();
64
+ if (files.length === 0) continue;
65
+
66
+ const content = readFileSync(resolve(monthDir, files[0]), "utf-8");
67
+
68
+ const recMatch = content.match(/## Recommendations\n\n([\s\S]*?)(?:\n##|\n$|$)/);
69
+ if (!recMatch?.[1]?.trim()) continue;
70
+
71
+ const recs = recMatch[1]
72
+ .trim()
73
+ .split("\n")
74
+ .filter((l) => l.trim())
75
+ .slice(0, 4);
76
+
77
+ if (recs.length === 0) continue;
78
+
79
+ const { meta } = parse<{ period?: string; average_rating?: string }>(content);
80
+ const period = meta.period ?? "";
81
+ const avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
82
+
83
+ const header = [
84
+ "## Pattern Synthesis",
85
+ period ? `*${period} — ${avgRating}*` : "",
86
+ ]
87
+ .filter(Boolean)
88
+ .join("\n");
89
+
90
+ return [header, ...recs].join("\n");
91
+ } catch {
92
+ /* try next month */
93
+ }
94
+ }
95
+ return "";
96
+ } catch {
97
+ return "";
98
+ }
99
+ }
100
+
101
+ /** Build the 5 most recent failure lessons as an avoid-list. */
102
+ export function loadFailurePatterns(): string {
103
+ try {
104
+ const entries = readFailures(paths.failures(), 5);
105
+ if (entries.length === 0) return "";
106
+
107
+ const lines = entries.map((e) => {
108
+ const label = e.rating ? `[${e.rating}/10]` : "";
109
+ const text = e.principle || e.context;
110
+ return `- ${label} ${text}`.trim();
111
+ });
112
+
113
+ return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
114
+ } catch {
115
+ return "";
116
+ }
117
+ }
118
+
119
+ /**
120
+ * All semi-static context sources in load order.
121
+ * Adding one entry here is the only change needed to extend coverage to all consumers.
122
+ */
123
+ export function getSemiStaticSources(): SemiStaticSource[] {
124
+ const memory = paths.memory();
125
+ const home = palHome();
126
+ return [
127
+ {
128
+ path: resolve(memory, "self-model", "current.md"),
129
+ writesDigest: false,
130
+ load: () => readFileSafe(resolve(memory, "self-model", "current.md")),
131
+ slug: "self-model",
132
+ description: "PAL self-model",
133
+ },
134
+ {
135
+ path: resolve(memory, "wisdom", "context.md"),
136
+ writesDigest: true,
137
+ load: () => {
138
+ try {
139
+ const principles = readFramePrinciples();
140
+ if (principles.length === 0) return "";
141
+ return ["## Crystallized Principles", ...principles.map((p) => `- ${p}`)].join(
142
+ "\n"
143
+ );
144
+ } catch {
145
+ return "";
146
+ }
147
+ },
148
+ slug: "wisdom",
149
+ description: "PAL wisdom",
150
+ },
151
+ {
152
+ path: resolve(memory, "relationship", "opinions-context.md"),
153
+ writesDigest: true,
154
+ load: () => {
155
+ try {
156
+ return loadOpinionContext();
157
+ } catch {
158
+ return "";
159
+ }
160
+ },
161
+ slug: "opinions",
162
+ description: "PAL opinions",
163
+ },
164
+ {
165
+ path: resolve(memory, "learning", "synthesis-digest.md"),
166
+ writesDigest: true,
167
+ load: loadSynthesisRecommendations,
168
+ slug: "synthesis",
169
+ description: "PAL pattern synthesis",
170
+ },
171
+ {
172
+ path: resolve(memory, "learning", "failures-digest.md"),
173
+ writesDigest: true,
174
+ load: loadFailurePatterns,
175
+ slug: "failures",
176
+ description: "PAL recent failure lessons",
177
+ },
178
+ {
179
+ path: resolve(home, "docs", "STEERING_RULES.md"),
180
+ writesDigest: false,
181
+ load: () => readFileSafe(resolve(home, "docs", "STEERING_RULES.md")),
182
+ slug: "steering",
183
+ description: "PAL steering rules",
184
+ },
185
+ ];
186
+ }
@@ -36,11 +36,6 @@ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
36
36
  "What are your current goals? (short-term, medium-term, long-term) (~/.pal/telos/GOALS.md)",
37
37
  hint: "e.g. Ship v2 by Q3, learn Rust, get promoted to staff engineer",
38
38
  },
39
- projects: {
40
- file: "telos/PROJECTS.md",
41
- question: "What projects are you currently working on? (~/.pal/telos/PROJECTS.md)",
42
- hint: "e.g. PAL (active, high priority), personal blog (paused), side SaaS (early stage)",
43
- },
44
39
  beliefs: {
45
40
  file: "telos/BELIEFS.md",
46
41
  question: "What principles or values guide your work? (~/.pal/telos/BELIEFS.md)",
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { autoGraduate } from "../handlers/auto-graduate";
9
9
  import { autoBackup } from "../handlers/backup";
10
+ import { writeContextDigests } from "../handlers/context-digests";
10
11
  import { notifyDesktop } from "../handlers/desktop-notify";
11
12
  import { captureFailure } from "../handlers/failure";
12
13
  import { projectTouch } from "../handlers/project-touch";
@@ -56,6 +57,7 @@ export async function runStopHandlers(
56
57
  autoGraduate(),
57
58
  projectTouch(options.lastAssistantMessage),
58
59
  notifyDesktop(options.sessionId),
60
+ Promise.resolve(writeContextDigests()),
59
61
  ]);
60
62
 
61
63
  const handlerNames = [
@@ -71,6 +73,7 @@ export async function runStopHandlers(
71
73
  "auto-graduate",
72
74
  "project-touch",
73
75
  "desktop-notify",
76
+ "context-digests",
74
77
  ];
75
78
  for (let i = 0; i < results.length; i++) {
76
79
  const r = results[i];
@@ -57,7 +57,7 @@ if (removedAgents.length > 0) {
57
57
  // --- Remove PAL system docs ---
58
58
  removePalDocs();
59
59
 
60
- // --- Remove AGENTS.md and CLAUDE.md symlink ---
60
+ // --- Remove AGENTS.md and CLAUDE.md ---
61
61
  const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
62
62
  const claudeMd = resolve(CLAUDE_DIR, "CLAUDE.md");
63
63
  try {
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * PAL — Copilot target installer
3
3
  * Writes hooks to ~/.copilot/hooks/pal-hooks.json.
4
- * Copies skills and agents. Symlinks copilot-instructions.md to AGENTS.md.
4
+ * Copies skills and agents. Writes ~/.copilot/instructions/pal-*.instructions.md.
5
+ * Enables ~/.copilot/instructions in VS Code chat.instructionsFilesLocations.
5
6
  */
6
7
 
7
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { mkdirSync, writeFileSync } from "node:fs";
8
9
  import { resolve } from "node:path";
10
+ import { writeContextDigests } from "../../hooks/handlers/context-digests";
9
11
  import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
10
12
  import { assets, palPkg, platform } from "../../hooks/lib/paths";
11
13
  import {
@@ -16,7 +18,10 @@ import {
16
18
  generateSkillIndex,
17
19
  loadCopilotHooksTemplate,
18
20
  log,
21
+ readJson,
19
22
  scaffoldPalSettings,
23
+ vscodeSettingsFile,
24
+ writeJson,
20
25
  } from "../lib";
21
26
 
22
27
  const PKG_ROOT = palPkg().replaceAll("\\", "/");
@@ -50,16 +55,42 @@ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
50
55
  // --- Scaffold PAL settings ---
51
56
  scaffoldPalSettings();
52
57
 
53
- // --- Generate AGENTS.md + copilot-instructions.md symlink ---
54
- // ensureSymlinks() inside regenerateIfNeeded() handles the symlink once ~/.copilot/ exists
58
+ // --- Generate AGENTS.md ---
55
59
  regenerateIfNeeded();
56
- const instructionsPath = resolve(COPILOT_DIR, "copilot-instructions.md");
60
+ log.success("Generated AGENTS.md");
61
+
62
+ // --- Write ~/.copilot/instructions/pal-*.instructions.md ---
63
+ mkdirSync(resolve(COPILOT_DIR, "instructions"), { recursive: true });
64
+ writeContextDigests();
57
65
  log.success(
58
- existsSync(instructionsPath)
59
- ? "copilot-instructions.md symlink present"
60
- : "Generated AGENTS.md (copilot-instructions.md symlink will be created on next session)"
66
+ "Written ~/.copilot/instructions/pal-self-model + pal-wisdom + pal-opinions.instructions.md"
61
67
  );
62
68
 
69
+ // --- Enable ~/.copilot/instructions in VS Code settings ---
70
+ const vsSettingsPath = vscodeSettingsFile();
71
+ const manualHint =
72
+ 'Add manually: { "chat.instructionsFilesLocations": { "~/.copilot/instructions": true } }';
73
+ if (vsSettingsPath) {
74
+ try {
75
+ const settings = readJson<Record<string, unknown>>(vsSettingsPath, {});
76
+ const existing =
77
+ typeof settings["chat.instructionsFilesLocations"] === "object" &&
78
+ settings["chat.instructionsFilesLocations"] !== null
79
+ ? (settings["chat.instructionsFilesLocations"] as Record<string, unknown>)
80
+ : {};
81
+ settings["chat.instructionsFilesLocations"] = {
82
+ ...existing,
83
+ "~/.copilot/instructions": true,
84
+ };
85
+ writeJson(vsSettingsPath, settings);
86
+ log.success("Enabled ~/.copilot/instructions in VS Code settings");
87
+ } catch {
88
+ log.warn(`Could not update VS Code settings — ${manualHint}`);
89
+ }
90
+ } else {
91
+ log.warn(`Could not detect VS Code settings path — ${manualHint}`);
92
+ }
93
+
63
94
  log.success("Copilot installation complete");
64
95
  console.log("");
65
96
  log.info(`Skills: ${countSkills()}`);