portable-agent-layer 0.21.0 → 0.23.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 (47) hide show
  1. package/README.md +3 -2
  2. package/assets/agents/gemini-researcher.md +17 -3
  3. package/assets/agents/grok-researcher.md +19 -5
  4. package/assets/agents/multi-perspective-researcher.md +16 -2
  5. package/assets/agents/perplexity-researcher.md +17 -3
  6. package/assets/skills/analyze-pdf/SKILL.md +1 -1
  7. package/assets/skills/analyze-youtube/SKILL.md +1 -1
  8. package/assets/skills/extract-entities/SKILL.md +1 -1
  9. package/assets/skills/fyzz-chat-api/SKILL.md +6 -6
  10. package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
  11. package/assets/skills/reflect/SKILL.md +2 -2
  12. package/assets/skills/telos/SKILL.md +6 -6
  13. package/assets/templates/AGENTS.md.template +2 -2
  14. package/assets/templates/PAL/ALGORITHM.md +139 -13
  15. package/assets/templates/PAL/CONTEXT_ROUTING.md +17 -17
  16. package/assets/templates/PAL/MEMORY_SYSTEM.md +5 -5
  17. package/assets/templates/PAL/README.md +12 -9
  18. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
  19. package/assets/templates/PAL/WORK_TRACKING.md +2 -9
  20. package/assets/templates/pal-settings.json +6 -3
  21. package/assets/templates/settings.claude.json +2 -2
  22. package/package.json +3 -1
  23. package/src/cli/index.ts +7 -14
  24. package/src/hooks/handlers/rating.ts +1 -1
  25. package/src/hooks/handlers/relationship.ts +3 -3
  26. package/src/hooks/handlers/session-intelligence.ts +324 -0
  27. package/src/hooks/handlers/session-name.ts +3 -3
  28. package/src/hooks/handlers/synthesis.ts +36 -0
  29. package/src/hooks/handlers/update-check.ts +2 -2
  30. package/src/hooks/handlers/work-learning.ts +1 -1
  31. package/src/hooks/lib/context.ts +123 -41
  32. package/src/hooks/lib/graduation.ts +1 -1
  33. package/src/hooks/lib/inference.ts +1 -1
  34. package/src/hooks/lib/paths.ts +4 -12
  35. package/src/hooks/lib/readme-sync.ts +3 -3
  36. package/src/hooks/lib/security.ts +41 -27
  37. package/src/hooks/lib/stop.ts +6 -6
  38. package/src/hooks/lib/token-usage.ts +1 -0
  39. package/src/hooks/lib/work-tracking.ts +1 -51
  40. package/src/targets/claude/install.ts +3 -1
  41. package/src/targets/cursor/install.ts +9 -1
  42. package/src/targets/cursor/uninstall.ts +7 -0
  43. package/src/targets/lib.ts +214 -111
  44. package/src/targets/opencode/install.ts +6 -4
  45. package/src/tools/agent/algorithm-reflect.ts +122 -0
  46. package/src/tools/agent/synthesize.ts +361 -0
  47. package/src/tools/agent/thread.ts +162 -0
@@ -15,13 +15,7 @@ import { readSessionNames } from "./session-names";
15
15
  import { buildSetupPrompt, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
16
16
  import { computeSignalTrends, formatTrends } from "./signal-trends";
17
17
  import { readFramePrinciples } from "./wisdom";
18
- import {
19
- activeProjects,
20
- readProjectHistory,
21
- readSessions,
22
- recentSessions,
23
- staleProjects,
24
- } from "./work-tracking";
18
+ import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
25
19
 
26
20
  interface PalSettings {
27
21
  loadAtStartup?: { files?: string[] };
@@ -83,44 +77,16 @@ export function loadActiveWork(): { text: string; summary: string | null } | nul
83
77
  try {
84
78
  const cwd = process.cwd();
85
79
  const allRecent = recentSessions(48);
86
- const projects = activeProjects();
87
- const stale = staleProjects(7);
88
80
 
89
- if (allRecent.length === 0 && projects.length === 0) return null;
81
+ if (allRecent.length === 0) return null;
90
82
 
91
83
  const lines: string[] = [];
92
84
 
93
- if (allRecent.length > 0) {
94
- lines.push("## Recent Work (last 48h)");
95
- for (const s of allRecent.slice(-10).reverse()) {
96
- const ago = formatAgo(s.ts);
97
- const here = s.cwd === cwd ? " *" : "";
98
- lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
99
- }
100
- }
101
-
102
- if (projects.length > 0) {
103
- lines.push("", "### Active Projects");
104
- for (const p of projects) {
105
- const sessionCount = p.sessions.length;
106
- const ago = formatAgo(p.updated);
107
- lines.push(`- **${p.name}** (${sessionCount} sessions, last: ${ago})`);
108
- if (p.nextSteps.length > 0) {
109
- lines.push(` Next: ${p.nextSteps[0]}`);
110
- }
111
- if (p.blockers.length > 0) {
112
- lines.push(` Blockers: ${p.blockers.join(", ")}`);
113
- } else {
114
- lines.push(" Blockers: None");
115
- }
116
- }
117
- }
118
-
119
- if (stale.length > 0) {
120
- lines.push("", "### Stale Projects (>7d inactive)");
121
- for (const p of stale) {
122
- lines.push(`- **${p.name}** — last active ${formatAgo(p.updated)}`);
123
- }
85
+ lines.push("## Recent Work (last 48h)");
86
+ for (const s of allRecent.slice(-10).reverse()) {
87
+ const ago = formatAgo(s.ts);
88
+ const here = s.cwd === cwd ? " *" : "";
89
+ lines.push(`- [${s.status}] ${s.name} ${ago}${here}`);
124
90
  }
125
91
 
126
92
  // Summary from most recent session
@@ -367,6 +333,116 @@ export function loadRelationshipContext(): string {
367
333
  }
368
334
  }
369
335
 
336
+ /** Load session intelligence from compact synthesis state */
337
+ export function loadSessionIntelligence(): string {
338
+ try {
339
+ const p = resolve(paths.state(), "synthesis.json");
340
+ if (!existsSync(p)) return "";
341
+ const state = JSON.parse(readFileSync(p, "utf-8"));
342
+
343
+ const lines: string[] = ["## Session Intelligence"];
344
+
345
+ // Open Threads — project-specific first, then global
346
+ if (state.threads?.length > 0) {
347
+ const cwd = process.cwd();
348
+ const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
349
+ const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
350
+
351
+ if (here.length > 0) {
352
+ lines.push("");
353
+ lines.push(`**Open threads — this project (${here.length}):**`);
354
+ for (const t of here) {
355
+ lines.push(`- ${t.title} (opened ${t.opened})`);
356
+ if (t.context) lines.push(` ${t.context}`);
357
+ }
358
+ lines.push("→ These are directly relevant to your current work.");
359
+ }
360
+ if (other.length > 0) {
361
+ lines.push("");
362
+ lines.push(`**Open threads — other projects (${other.length}):**`);
363
+ for (const t of other) {
364
+ lines.push(`- ${t.title} (opened ${t.opened})`);
365
+ }
366
+ }
367
+ }
368
+
369
+ // Rating Trend
370
+ if (state.ratings?.count > 0) {
371
+ const r = state.ratings;
372
+ lines.push("");
373
+ lines.push(
374
+ `**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : ""}`
375
+ );
376
+ if (r.trend === "declining") {
377
+ lines.push(
378
+ "→ Trend is declining. Be extra careful with assumptions. Confirm before acting."
379
+ );
380
+ } else if (r.trend === "improving") {
381
+ lines.push("→ Trend is improving. Maintain current approach.");
382
+ } else if (r.lowCount > 5) {
383
+ lines.push(
384
+ "→ Multiple low ratings. Slow down, verify before acting, ask when uncertain."
385
+ );
386
+ }
387
+ }
388
+
389
+ // Algorithm Performance
390
+ if (state.algorithm?.reflectionCount > 0) {
391
+ const a = state.algorithm;
392
+ lines.push("");
393
+ lines.push(
394
+ `**Algorithm:** ${a.reflectionCount} reflections, ${a.passRate}% criteria pass rate, ${a.avgSentiment}/10 sentiment.`
395
+ );
396
+ if (a.passRate < 80) {
397
+ lines.push(
398
+ "→ Criteria pass rate is low. Invest more time in OBSERVE and PLAN phases."
399
+ );
400
+ }
401
+ if (a.recentObservations?.length > 0) {
402
+ const cwd = process.cwd();
403
+ const relevant = a.recentObservations.filter(
404
+ (o: { cwd?: string }) => !o.cwd || o.cwd === cwd
405
+ );
406
+ if (relevant.length > 0) {
407
+ lines.push("Recent self-observations (this project):");
408
+ for (const o of relevant) {
409
+ lines.push(`- [${o.date}] ${o.task}: "${o.observation}"`);
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ return lines.length > 1 ? lines.join("\n") : "";
416
+ } catch {
417
+ return "";
418
+ }
419
+ }
420
+
421
+ /** Load handoff state for the current project */
422
+ export function loadHandoff(): string {
423
+ try {
424
+ const p = resolve(paths.state(), "last-handoff.json");
425
+ if (!existsSync(p)) return "";
426
+ const handoffs = JSON.parse(readFileSync(p, "utf-8"));
427
+ const cwd = process.cwd();
428
+ const entry = handoffs[cwd];
429
+ if (!entry?.handoff || entry.status !== "in-progress") return "";
430
+
431
+ const age = Date.now() - new Date(entry.timestamp).getTime();
432
+ if (age > 7 * 24 * 60 * 60 * 1000) return ""; // stale after 7 days
433
+
434
+ return [
435
+ "## Pick Up Where You Left Off",
436
+ `*Previous session: ${entry.title}*`,
437
+ "",
438
+ entry.handoff,
439
+ "→ Continue this work or explicitly close it before starting something new.",
440
+ ].join("\n");
441
+ } catch {
442
+ return "";
443
+ }
444
+ }
445
+
370
446
  /**
371
447
  * Build the <system-reminder> content for the AI.
372
448
  *
@@ -392,10 +468,16 @@ export function buildSystemReminder(): string {
392
468
  ? loadSynthesisRecommendations()
393
469
  : "";
394
470
  const opinions = isEnabled(settings, "opinions") ? loadOpinionContext() : "";
471
+ const intelligence = isEnabled(settings, "sessionIntelligence")
472
+ ? loadSessionIntelligence()
473
+ : "";
474
+ const handoff = isEnabled(settings, "handoff") ? loadHandoff() : "";
395
475
  const parts: string[] = [];
396
476
  if (startup) parts.push(startup);
477
+ if (handoff) parts.push(handoff);
397
478
  if (wisdom) parts.push(wisdom);
398
479
  if (opinions) parts.push(opinions);
480
+ if (intelligence) parts.push(intelligence);
399
481
  if (relationship) parts.push(relationship);
400
482
  if (projectHistory) parts.push(projectHistory);
401
483
  if (digest) parts.push(digest);
@@ -214,7 +214,7 @@ async function generateRecommendations(
214
214
  ratings: RatingsSummary | null
215
215
  ): Promise<string[]> {
216
216
  if (candidates.length === 0 && !ratings) return [];
217
- if (!process.env.ANTHROPIC_API_KEY) {
217
+ if (!process.env.PAL_ANTHROPIC_API_KEY) {
218
218
  return candidates
219
219
  .slice(0, 3)
220
220
  .map(
@@ -21,7 +21,7 @@ export interface InferenceResult {
21
21
  }
22
22
 
23
23
  export async function inference(opts: InferenceOptions): Promise<InferenceResult> {
24
- const apiKey = process.env.ANTHROPIC_API_KEY;
24
+ const apiKey = process.env.PAL_ANTHROPIC_API_KEY;
25
25
  if (!apiKey) return { success: false };
26
26
 
27
27
  const {
@@ -12,20 +12,12 @@ export function palPkg(): string {
12
12
  }
13
13
 
14
14
  /**
15
- * Root of the user's personal state (telos, memory, etc.).
16
- * In repo mode: same as palPkg() (the repo root).
17
- * In package mode: ~/.pal/ (or PAL_HOME override).
18
- *
19
- * Repo mode is detected by the presence of .palroot next to the package.
20
- * This file is not included in the npm package, so it only exists in cloned repos.
15
+ * Root of the user's personal state (telos, memory, docs, tools, skills).
16
+ * Always resolves to ~/.pal/ regardless of where the package lives.
17
+ * Power users who want memory/telos versioned in a repo can override via PAL_HOME.
21
18
  */
22
19
  export function palHome(): string {
23
- if (process.env.PAL_HOME) return process.env.PAL_HOME;
24
-
25
- const pkgRoot = palPkg();
26
- if (existsSync(resolve(pkgRoot, ".palroot"))) return pkgRoot;
27
-
28
- return resolve(homedir(), ".pal");
20
+ return process.env.PAL_HOME || resolve(homedir(), ".pal");
29
21
  }
30
22
 
31
23
  /** Ensure a directory exists, creating it recursively if needed */
@@ -58,12 +58,12 @@ function extractEnvVars(): string[] {
58
58
  }
59
59
  }
60
60
 
61
- // ANTHROPIC_API_KEY from inference.ts
61
+ // PAL_ANTHROPIC_API_KEY from inference.ts
62
62
  const inferenceFile = resolve(pkg, "src", "hooks", "lib", "inference.ts");
63
63
  if (existsSync(inferenceFile)) {
64
64
  const content = readFileSync(inferenceFile, "utf-8");
65
- if (content.includes("ANTHROPIC_API_KEY")) {
66
- vars.add("ANTHROPIC_API_KEY");
65
+ if (content.includes("PAL_ANTHROPIC_API_KEY")) {
66
+ vars.add("PAL_ANTHROPIC_API_KEY");
67
67
  }
68
68
  }
69
69
 
@@ -34,6 +34,8 @@ export const HOOK_MANAGED_FILES = [
34
34
  "debug.log.prev",
35
35
  "opinions.json",
36
36
  "pal-settings.json",
37
+ "skill-index.json",
38
+ "algorithm-reflections.jsonl",
37
39
  ];
38
40
 
39
41
  /** Hook-managed directories — AI must not write to or delete from these */
@@ -45,7 +47,6 @@ export const HOOK_MANAGED_DIRS = [
45
47
  "memory/relationship",
46
48
  "memory/wisdom/state",
47
49
  "memory/projects",
48
- ".agents/PAL",
49
50
  ];
50
51
 
51
52
  /** Escape a string for use in a RegExp */
@@ -60,8 +61,11 @@ export const PROTECTED_PATHS: RegExp[] = [
60
61
  /^\/System\//,
61
62
  /\.ssh\/(?!config)/,
62
63
  /\.gnupg\//,
63
- // Derived from HOOK_MANAGED_FILES
64
- ...HOOK_MANAGED_FILES.map((name) => new RegExp(`[/\\\\]${escapeRegExp(name)}$`)),
64
+ // Derived from HOOK_MANAGED_FILES — scoped to managed roots only
65
+ ...HOOK_MANAGED_FILES.map(
66
+ (name) =>
67
+ new RegExp(`[/\\\\]\\.(?:pal|claude|agents|cursor)[/\\\\].*${escapeRegExp(name)}$`)
68
+ ),
65
69
  ];
66
70
 
67
71
  /** Patterns that warrant a warning (logged but not blocked) */
@@ -72,38 +76,44 @@ export const WARN_COMMANDS: RegExp[] = [
72
76
  /truncate\s+table/i,
73
77
  ];
74
78
 
79
+ /** Roots where managed files/dirs are protected (user state, not repo templates) */
80
+ const MANAGED_ROOTS = [".pal/", ".claude/", ".agents/", ".config/opencode/", ".cursor/"];
81
+
82
+ function isUnderManagedRoot(path: string): boolean {
83
+ const normalized = path.replace(/\\/g, "/");
84
+ return MANAGED_ROOTS.some(
85
+ (root) => normalized.includes(`/${root}`) || normalized.includes(`\\.${root}`)
86
+ );
87
+ }
88
+
75
89
  /** Read-only commands allowed to reference protected files */
76
90
  const READ_ONLY_COMMANDS =
77
- /^\s*(?:cat|head|tail|less|more|grep|rg|wc|diff|stat|file|ls|dir|git\s+(?:log|diff|blame|show|status)|bat)\b/;
91
+ /^\s*(?:cat|head|tail|less|more|grep|rg|wc|diff|stat|file|ls|dir|find|git\s+(?:log|diff|blame|show|status)|bat)\b/;
78
92
 
79
93
  /** Check a bash command against blocked patterns. Returns reason string or null. */
80
94
  export function checkBashCommand(cmd: string): string | null {
81
95
  for (const [pattern, reason] of BLOCKED_COMMANDS) {
82
96
  if (pattern.test(cmd)) return reason;
83
97
  }
84
- // If command mentions a protected file, block unless it's read-only
98
+ // If command references a managed file in a managed root path, block unless read-only.
99
+ // The filename must appear IN the same path as the managed root (e.g. .pal/.../file.json).
100
+ const segments = cmd.split(/[|;&&]/).map((s) => s.trim());
85
101
  for (const name of HOOK_MANAGED_FILES) {
86
- if (cmd.includes(name)) {
87
- // Check each piped segment — if any segment is not read-only, block
88
- const segments = cmd.split(/[|;&&]/).map((s) => s.trim());
89
- const allReadOnly = segments
90
- .filter((s) => s.includes(name))
91
- .every((s) => READ_ONLY_COMMANDS.test(s));
92
- if (!allReadOnly) {
93
- return `${name} is managed automatically by hooks — do not edit directly`;
94
- }
102
+ const pattern = new RegExp(
103
+ `\\.(?:pal|claude|agents|cursor|config/opencode)[/\\\\]\\S*${escapeRegExp(name)}`
104
+ );
105
+ const managed = segments.filter((s) => pattern.test(s));
106
+ if (managed.length > 0 && !managed.every((s) => READ_ONLY_COMMANDS.test(s))) {
107
+ return `${name} is managed automatically by hooks — do not edit directly`;
95
108
  }
96
109
  }
97
- // If command mentions a hook-managed directory, block unless it's read-only
98
110
  for (const dir of HOOK_MANAGED_DIRS) {
99
- if (cmd.includes(dir)) {
100
- const segments = cmd.split(/[|;&&]/).map((s) => s.trim());
101
- const allReadOnly = segments
102
- .filter((s) => s.includes(dir))
103
- .every((s) => READ_ONLY_COMMANDS.test(s));
104
- if (!allReadOnly) {
105
- return `${dir} is managed automatically by hooks — do not edit directly`;
106
- }
111
+ const pattern = new RegExp(
112
+ `\\.(?:pal|claude|agents|cursor|config/opencode)[/\\\\]\\S*${escapeRegExp(dir)}`
113
+ );
114
+ const managed = segments.filter((s) => pattern.test(s));
115
+ if (managed.length > 0 && !managed.every((s) => READ_ONLY_COMMANDS.test(s))) {
116
+ return `${dir} is managed automatically by hooks — do not edit directly`;
107
117
  }
108
118
  }
109
119
  return null;
@@ -112,10 +122,14 @@ export function checkBashCommand(cmd: string): string | null {
112
122
  /** Check a file path against protected patterns. Returns a reason string or null. */
113
123
  export function checkFilePath(filePath: string): string | null {
114
124
  const normalized = filePath.replace(/\\/g, "/");
115
- // Check hook-managed files first (more specific message)
116
- const matchedFile = HOOK_MANAGED_FILES.find((name) => normalized.endsWith(`/${name}`));
117
- if (matchedFile) {
118
- return `${matchedFile} is managed automatically by hooks — do not edit directly`;
125
+ // Check hook-managed files only under managed roots (not repo templates)
126
+ if (isUnderManagedRoot(normalized)) {
127
+ const matchedFile = HOOK_MANAGED_FILES.find((name) =>
128
+ normalized.endsWith(`/${name}`)
129
+ );
130
+ if (matchedFile) {
131
+ return `${matchedFile} is managed automatically by hooks — do not edit directly`;
132
+ }
119
133
  }
120
134
  // Check hook-managed directories
121
135
  const matchedDir = HOOK_MANAGED_DIRS.find((dir) => normalized.includes(`/${dir}/`));
@@ -8,10 +8,10 @@ import { resolve } from "node:path";
8
8
  import { autoBackup } from "../handlers/backup";
9
9
  import { captureFailure } from "../handlers/failure";
10
10
  import { checkReflectTrigger } from "../handlers/reflect-trigger";
11
- import { captureRelationship } from "../handlers/relationship";
11
+ import { captureSessionIntelligence } from "../handlers/session-intelligence";
12
+ import { runSynthesis } from "../handlers/synthesis";
12
13
  import { resetTab } from "../handlers/tab";
13
14
  import { updateCounts } from "../handlers/update-counts";
14
- import { captureWorkLearning } from "../handlers/work-learning";
15
15
  import { captureWorkSession } from "../handlers/work-session";
16
16
  import { logDebug, logError } from "./log";
17
17
  import { ensureDir, paths } from "./paths";
@@ -39,23 +39,23 @@ export async function runStopHandlers(
39
39
  const results = await Promise.allSettled([
40
40
  captureWorkSession(transcript, options.sessionId),
41
41
  resetTab(),
42
- captureRelationship(transcript, options.sessionId),
43
- captureWorkLearning(transcript, options.sessionId),
42
+ captureSessionIntelligence(transcript, options.sessionId),
44
43
  checkPendingFailure(transcript),
45
44
  updateCounts(),
46
45
  autoBackup(),
47
46
  checkReflectTrigger(),
47
+ runSynthesis(),
48
48
  ]);
49
49
 
50
50
  const handlerNames = [
51
51
  "work-session",
52
52
  "tab",
53
- "relationship",
54
- "work-learning",
53
+ "session-intelligence",
55
54
  "pending-failure",
56
55
  "update-counts",
57
56
  "backup",
58
57
  "reflect-trigger",
58
+ "synthesis",
59
59
  ];
60
60
  for (let i = 0; i < results.length; i++) {
61
61
  const r = results[i];
@@ -13,6 +13,7 @@ export type TokenCaller =
13
13
  | "failure"
14
14
  | "work-learning"
15
15
  | "session-name"
16
+ | "session-intelligence"
16
17
  | "relationship";
17
18
 
18
19
  interface TokenUsageEntry {
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Structured work tracking: session history + persistent projects.
2
+ * Structured work tracking: session history + per-project history.
3
3
  * Used by both Claude Code (StopOrchestrator) and opencode (plugin).
4
4
  */
5
5
 
6
6
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { ensureDir, paths } from "./paths";
9
- import { now } from "./time";
10
9
 
11
10
  // ── Session Records ──────────────────────────────────────────────
12
11
 
@@ -179,52 +178,3 @@ export function readProjectHistory(cwd: string, limit = 15): ProjectHistoryEntry
179
178
  return [];
180
179
  }
181
180
  }
182
-
183
- // ── Persistent Projects ──────────────────────────────────────────
184
-
185
- export interface Project {
186
- id: string;
187
- name: string;
188
- created: string;
189
- updated: string;
190
- status: "active" | "paused" | "completed";
191
- objectives: string[];
192
- decisions: string[];
193
- completed: string[];
194
- blockers: string[];
195
- nextSteps: string[];
196
- handoff: string;
197
- sessions: string[];
198
- }
199
-
200
- function projectsPath(): string {
201
- return resolve(ensureDir(paths.state()), "projects.json");
202
- }
203
-
204
- export function readProjects(): Record<string, Project> {
205
- const p = projectsPath();
206
- if (!existsSync(p)) return {};
207
- try {
208
- return JSON.parse(readFileSync(p, "utf-8"));
209
- } catch {
210
- return {};
211
- }
212
- }
213
-
214
- export function writeProject(project: Project): void {
215
- const projects = readProjects();
216
- project.updated = now();
217
- projects[project.id] = project;
218
- writeFileSync(projectsPath(), JSON.stringify(projects, null, 2), "utf-8");
219
- }
220
-
221
- export function activeProjects(): Project[] {
222
- return Object.values(readProjects()).filter((p) => p.status === "active");
223
- }
224
-
225
- export function staleProjects(days = 7): Project[] {
226
- const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
227
- return Object.values(readProjects()).filter(
228
- (p) => p.status === "active" && new Date(p.updated).getTime() < cutoff
229
- );
230
- }
@@ -15,6 +15,7 @@ import {
15
15
  countAgents,
16
16
  countMd,
17
17
  countSkills,
18
+ generateSkillIndex,
18
19
  loadSettingsTemplate,
19
20
  log,
20
21
  mergeSettings,
@@ -50,13 +51,14 @@ log.success("Merged PAL settings into settings.json");
50
51
  // --- Copy skills ---
51
52
  const skillsDir = resolve(CLAUDE_DIR, "skills");
52
53
  copySkills(skillsDir);
54
+ generateSkillIndex();
53
55
 
54
56
  // --- Copy agents ---
55
57
  copyAgents();
56
58
 
57
59
  // --- Copy PAL system docs ---
58
60
  const palDocsCount = copyPalDocs();
59
- log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
61
+ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
60
62
 
61
63
  // --- Scaffold PAL settings ---
62
64
  scaffoldPalSettings();
@@ -9,9 +9,11 @@ import { resolve } from "node:path";
9
9
  import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
10
10
  import { assets, palPkg, platform } from "../../hooks/lib/paths";
11
11
  import {
12
+ copyAgentsForCursor,
12
13
  copyPalDocs,
13
14
  copySkills,
14
15
  countSkills,
16
+ generateSkillIndex,
15
17
  loadCursorHooksTemplate,
16
18
  log,
17
19
  mergeCursorHooks,
@@ -44,10 +46,16 @@ log.success("Merged PAL hooks into hooks.json");
44
46
  // --- Symlink skills to ~/.cursor/skills/ ---
45
47
  const cursorSkillsDir = resolve(CURSOR_DIR, "skills");
46
48
  copySkills(cursorSkillsDir);
49
+ generateSkillIndex();
50
+
51
+ // --- Copy agents to ~/.cursor/agents/ ---
52
+ const cursorAgentsDir = resolve(CURSOR_DIR, "agents");
53
+ const agentCount = copyAgentsForCursor(cursorAgentsDir);
54
+ if (agentCount > 0) log.success(`Installed ${agentCount} agents to ~/.cursor/agents/`);
47
55
 
48
56
  // --- Copy PAL system docs ---
49
57
  const palDocsCount = copyPalDocs();
50
- log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
58
+ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
51
59
 
52
60
  // --- Scaffold PAL settings ---
53
61
  scaffoldPalSettings();
@@ -11,6 +11,7 @@ import {
11
11
  loadCursorHooksTemplate,
12
12
  log,
13
13
  readJson,
14
+ removeAgentsFromCursor,
14
15
  removePalDocs,
15
16
  removeSkills,
16
17
  unmergeCursorHooks,
@@ -46,6 +47,12 @@ if (removed.length > 0) {
46
47
  log.info("No PAL skills found");
47
48
  }
48
49
 
50
+ // --- Remove PAL agents ---
51
+ const removedAgents = removeAgentsFromCursor(resolve(CURSOR_DIR, "agents"));
52
+ if (removedAgents.length > 0) {
53
+ log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
54
+ }
55
+
49
56
  // --- Remove PAL system docs ---
50
57
  removePalDocs();
51
58