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.
- package/README.md +1 -1
- package/assets/skills/presentation/SKILL.md +2 -0
- package/assets/skills/presentation/demo/slides/004-content.md +27 -1
- package/assets/skills/presentation/theme-base/base.css +206 -0
- package/assets/skills/presentation/theme-base/skeleton.html +49 -0
- package/assets/skills/presentation/tools/lib/lint-rules.ts +25 -0
- package/assets/skills/projects/SKILL.md +0 -1
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/templates/AGENTS.md.template +2 -1
- package/assets/templates/PAL/ALGORITHM.md +28 -3
- package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
- package/assets/templates/PAL/README.md +1 -1
- package/assets/templates/PAL/STEERING_RULES.md +4 -0
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
- package/assets/templates/PAL/WORK_TRACKING.md +1 -1
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +1 -1
- package/src/cli/setup-telos.ts +12 -79
- package/src/hooks/LoadContext.ts +22 -10
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/session-intelligence.ts +9 -86
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +57 -139
- package/src/hooks/lib/relationship.ts +3 -3
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/semi-static.ts +186 -0
- package/src/hooks/lib/setup.ts +0 -5
- package/src/hooks/lib/stop.ts +3 -0
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/copilot/install.ts +39 -8
- package/src/targets/copilot/uninstall.ts +58 -17
- package/src/targets/cursor/install.ts +8 -0
- package/src/targets/cursor/uninstall.ts +18 -1
- package/src/targets/lib.ts +26 -0
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +1 -1
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/relationship-reflect.ts +2 -2
- package/src/tools/self-model.ts +4 -4
- package/assets/templates/telos/PROJECTS.md +0 -7
package/src/hooks/lib/context.ts
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
* Used by LoadContext.ts (Claude Code) and the opencode plugin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync,
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
447
|
-
|
|
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 =
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
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
|
-
*
|
|
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" | "
|
|
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
|
-
/**
|
|
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
|
+
}
|
package/src/hooks/lib/setup.ts
CHANGED
|
@@ -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)",
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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 {
|
|
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
|
|
54
|
-
// ensureSymlinks() inside regenerateIfNeeded() handles the symlink once ~/.copilot/ exists
|
|
58
|
+
// --- Generate AGENTS.md ---
|
|
55
59
|
regenerateIfNeeded();
|
|
56
|
-
|
|
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
|
-
|
|
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()}`);
|