portable-agent-layer 0.35.0 → 0.37.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 (108) hide show
  1. package/README.md +2 -1
  2. package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
  3. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
  4. package/assets/skills/consulting-report/tools/dev.ts +2 -2
  5. package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
  6. package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
  7. package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
  8. package/assets/skills/opinion/tools/opinion.ts +3 -2
  9. package/assets/skills/presentation/SKILL.md +1 -1
  10. package/assets/skills/presentation/tools/doctor.ts +2 -5
  11. package/assets/skills/presentation/tools/lib/inline.ts +6 -11
  12. package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
  13. package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
  14. package/assets/skills/presentation/tools/setup-template.ts +10 -7
  15. package/assets/skills/projects/SKILL.md +44 -21
  16. package/assets/skills/research/tools/gemini-search.ts +2 -2
  17. package/assets/skills/research/tools/grok-search.ts +2 -2
  18. package/assets/skills/research/tools/perplexity-search.ts +2 -2
  19. package/assets/skills/telos/SKILL.md +7 -52
  20. package/assets/skills/telos/tools/update-telos.ts +0 -1
  21. package/assets/templates/PAL/ALGORITHM.md +54 -5
  22. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  23. package/assets/templates/PAL/README.md +1 -1
  24. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  25. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  26. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  27. package/assets/templates/hooks.codex.json +44 -0
  28. package/assets/templates/hooks.cursor.json +11 -5
  29. package/assets/templates/pal-settings.json +1 -3
  30. package/assets/templates/settings.claude.json +2 -1
  31. package/package.json +2 -1
  32. package/src/cli/index.ts +112 -14
  33. package/src/cli/migrate.ts +299 -0
  34. package/src/cli/setup-identity.ts +3 -3
  35. package/src/cli/setup-telos.ts +12 -80
  36. package/src/hooks/CompactRecover.ts +11 -5
  37. package/src/hooks/LoadContext.ts +35 -11
  38. package/src/hooks/PreCompactPersist.ts +26 -34
  39. package/src/hooks/SecurityValidator.ts +43 -21
  40. package/src/hooks/StopOrchestrator.ts +4 -1
  41. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  42. package/src/hooks/handlers/auto-graduate.ts +2 -2
  43. package/src/hooks/handlers/backup.ts +3 -3
  44. package/src/hooks/handlers/context-digests.ts +74 -0
  45. package/src/hooks/handlers/failure.ts +5 -3
  46. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  47. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  48. package/src/hooks/handlers/rating.ts +2 -1
  49. package/src/hooks/handlers/readme-sync.ts +3 -2
  50. package/src/hooks/handlers/session-intelligence.ts +17 -93
  51. package/src/hooks/handlers/session-name.ts +2 -2
  52. package/src/hooks/handlers/synthesis.ts +5 -2
  53. package/src/hooks/handlers/update-counts.ts +3 -2
  54. package/src/hooks/lib/agent.ts +20 -18
  55. package/src/hooks/lib/claude-md.ts +69 -14
  56. package/src/hooks/lib/context.ts +92 -246
  57. package/src/hooks/lib/entities.ts +7 -7
  58. package/src/hooks/lib/frontmatter.ts +4 -4
  59. package/src/hooks/lib/graduation.ts +7 -6
  60. package/src/hooks/lib/inference.ts +6 -2
  61. package/src/hooks/lib/learning-category.ts +1 -1
  62. package/src/hooks/lib/learning-store.ts +6 -1
  63. package/src/hooks/lib/notify.ts +2 -2
  64. package/src/hooks/lib/opinions.ts +3 -3
  65. package/src/hooks/lib/paths.ts +2 -0
  66. package/src/hooks/lib/projects.ts +142 -74
  67. package/src/hooks/lib/readme-sync.ts +1 -1
  68. package/src/hooks/lib/relationship.ts +4 -16
  69. package/src/hooks/lib/retrieval-index.ts +5 -3
  70. package/src/hooks/lib/retrieval.ts +11 -12
  71. package/src/hooks/lib/security.ts +24 -18
  72. package/src/hooks/lib/semi-static.ts +188 -0
  73. package/src/hooks/lib/session-names.ts +1 -1
  74. package/src/hooks/lib/settings.ts +1 -1
  75. package/src/hooks/lib/setup.ts +2 -65
  76. package/src/hooks/lib/signals.ts +2 -2
  77. package/src/hooks/lib/stdin.ts +1 -1
  78. package/src/hooks/lib/stop.ts +16 -6
  79. package/src/hooks/lib/token-usage.ts +1 -2
  80. package/src/hooks/lib/transcript.ts +1 -1
  81. package/src/hooks/lib/wisdom.ts +5 -5
  82. package/src/hooks/lib/work-tracking.ts +8 -14
  83. package/src/targets/claude/uninstall.ts +1 -1
  84. package/src/targets/codex/install.ts +95 -0
  85. package/src/targets/codex/uninstall.ts +70 -0
  86. package/src/targets/copilot/install.ts +39 -8
  87. package/src/targets/copilot/uninstall.ts +58 -17
  88. package/src/targets/cursor/install.ts +8 -0
  89. package/src/targets/cursor/uninstall.ts +18 -1
  90. package/src/targets/lib.ts +166 -14
  91. package/src/targets/opencode/install.ts +29 -1
  92. package/src/targets/opencode/plugin.ts +23 -12
  93. package/src/targets/opencode/uninstall.ts +30 -3
  94. package/src/tools/agent/algorithm-reflect.ts +1 -1
  95. package/src/tools/agent/analyze.ts +18 -18
  96. package/src/tools/agent/handoff-note.ts +116 -0
  97. package/src/tools/agent/project.ts +375 -75
  98. package/src/tools/agent/relationship-note.ts +51 -0
  99. package/src/tools/agent/synthesize.ts +6 -42
  100. package/src/tools/agent/thread.ts +15 -14
  101. package/src/tools/agent/wisdom-frame.ts +9 -3
  102. package/src/tools/import.ts +1 -1
  103. package/src/tools/relationship-reflect.ts +15 -13
  104. package/src/tools/self-model.ts +23 -19
  105. package/src/tools/session-summary.ts +3 -3
  106. package/src/tools/token-cost.ts +15 -16
  107. package/assets/skills/telos/tools/update-projects.ts +0 -106
  108. package/assets/templates/telos/PROJECTS.md +0 -7
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PAL — Codex uninstaller
3
+ * Removes only PAL-owned hooks from ~/.codex/hooks.json. Preserves user hooks.
4
+ * Removes PAL skill symlinks.
5
+ */
6
+
7
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
10
+ import {
11
+ loadCodexHooksTemplate,
12
+ log,
13
+ readJson,
14
+ removeSkills,
15
+ unmergeCodexHooks,
16
+ writeJson,
17
+ } from "../lib";
18
+
19
+ /**
20
+ * Remove `hooks = true` from config.toml.
21
+ * Also removes a now-empty [features] section header.
22
+ */
23
+ function disableCodexHooks(configPath: string): void {
24
+ if (!existsSync(configPath)) return;
25
+ let content = readFileSync(configPath, "utf-8");
26
+ if (!/^\s*hooks\s*=\s*true/m.test(content)) return;
27
+
28
+ // Remove the hooks line
29
+ content = content.replace(/^[ \t]*hooks\s*=\s*true[ \t]*\n?/m, "");
30
+
31
+ // Remove [features] header if it's now empty (nothing between it and next section / EOF)
32
+ content = content.replace(/\[features\]\n(?=\[|$)/m, "");
33
+
34
+ writeFileSync(configPath, content, "utf-8");
35
+ log.success("Removed hooks from ~/.codex/config.toml");
36
+ }
37
+
38
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
39
+ const CODEX_DIR = platform.codexDir();
40
+ const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
41
+
42
+ // --- Remove PAL hooks from hooks.json ---
43
+ if (existsSync(HOOKS_FILE)) {
44
+ copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
45
+ log.info("Backed up hooks.json");
46
+
47
+ const template = loadCodexHooksTemplate(assets.codexHooksTemplate(), PKG_ROOT);
48
+ const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
49
+ const cleaned = unmergeCodexHooks(existing, template);
50
+
51
+ writeJson(HOOKS_FILE, cleaned);
52
+ log.success("Removed PAL hooks from ~/.codex/hooks.json");
53
+ } else {
54
+ log.info("No hooks.json found, nothing to do");
55
+ }
56
+
57
+ // --- Remove PAL skill symlinks ---
58
+ const codexSkillsDir = resolve(CODEX_DIR, "skills");
59
+ const removed = removeSkills(codexSkillsDir);
60
+ if (removed.length > 0) {
61
+ log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
62
+ } else {
63
+ log.info("No PAL skills found");
64
+ }
65
+
66
+ // --- Disable hooks in config.toml ---
67
+ const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
68
+ disableCodexHooks(CONFIG_FILE);
69
+
70
+ log.success("Codex uninstall complete");
@@ -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()}`);
@@ -1,12 +1,22 @@
1
1
  /**
2
2
  * PAL — Copilot uninstaller
3
- * Removes pal-hooks.json, skill symlinks, agents, and copilot-instructions.md symlink.
3
+ * Removes pal-hooks.json, skill symlinks, agents, instruction files,
4
+ * and the VS Code chat.instructionsFilesLocations entry.
4
5
  */
5
6
 
6
7
  import { copyFileSync, existsSync, lstatSync, readlinkSync, unlinkSync } from "node:fs";
7
8
  import { resolve } from "node:path";
8
9
  import { platform } from "../../hooks/lib/paths";
9
- import { log, removeAgentsFromCopilot, removePalDocs, removeSkills } from "../lib";
10
+ import { copilotFilename, getSemiStaticSources } from "../../hooks/lib/semi-static";
11
+ import {
12
+ log,
13
+ readJson,
14
+ removeAgentsFromCopilot,
15
+ removePalDocs,
16
+ removeSkills,
17
+ vscodeSettingsFile,
18
+ writeJson,
19
+ } from "../lib";
10
20
 
11
21
  const COPILOT_DIR = platform.copilotDir();
12
22
  const HOOKS_FILE = resolve(COPILOT_DIR, "hooks", "pal-hooks.json");
@@ -35,26 +45,57 @@ if (removedAgents.length > 0) {
35
45
  log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
36
46
  }
37
47
 
38
- // --- Remove copilot-instructions.md symlink (only if it points to AGENTS.md) ---
39
- const instructionsPath = resolve(COPILOT_DIR, "copilot-instructions.md");
40
- if (existsSync(instructionsPath)) {
48
+ // --- Remove PAL docs ---
49
+ removePalDocs();
50
+
51
+ // --- Remove ~/.copilot/instructions/pal-*.instructions.md ---
52
+ for (const src of getSemiStaticSources()) {
53
+ try {
54
+ unlinkSync(resolve(COPILOT_DIR, "instructions", copilotFilename(src)));
55
+ } catch {
56
+ /* gone */
57
+ }
58
+ }
59
+ // pal-session.instructions.md is written by LoadContext (not a semi-static source)
60
+ try {
61
+ unlinkSync(resolve(COPILOT_DIR, "instructions", "pal-session.instructions.md"));
62
+ } catch {
63
+ /* gone */
64
+ }
65
+ log.success("Removed ~/.copilot/instructions/pal-*.instructions.md");
66
+
67
+ // --- Backward compat: remove old copilot-instructions.md symlink if present ---
68
+ const legacyPath = resolve(COPILOT_DIR, "copilot-instructions.md");
69
+ if (existsSync(legacyPath)) {
41
70
  try {
42
- const stat = lstatSync(instructionsPath);
43
- if (stat.isSymbolicLink()) {
44
- const target = readlinkSync(instructionsPath);
45
- if (target.includes("AGENTS.md")) {
46
- unlinkSync(instructionsPath);
47
- log.success("Removed copilot-instructions.md symlink");
48
- } else {
49
- log.info("copilot-instructions.md is not a PAL symlink — leaving it");
50
- }
71
+ if (
72
+ lstatSync(legacyPath).isSymbolicLink() &&
73
+ readlinkSync(legacyPath).includes("AGENTS.md")
74
+ ) {
75
+ unlinkSync(legacyPath);
76
+ log.success("Removed legacy copilot-instructions.md symlink");
51
77
  }
52
78
  } catch {
53
- // ignore
79
+ /* ignore */
54
80
  }
55
81
  }
56
82
 
57
- // --- Remove PAL docs ---
58
- removePalDocs();
83
+ // --- Remove ~/.copilot/instructions entry from VS Code settings ---
84
+ const vsSettingsPath = vscodeSettingsFile();
85
+ if (vsSettingsPath && existsSync(vsSettingsPath)) {
86
+ const settings = readJson<Record<string, unknown>>(vsSettingsPath, {});
87
+ const locs = settings["chat.instructionsFilesLocations"];
88
+ if (typeof locs === "object" && locs !== null) {
89
+ const obj = locs as Record<string, unknown>;
90
+ delete obj["~/.copilot/instructions"];
91
+ if (Object.keys(obj).length === 0) {
92
+ delete settings["chat.instructionsFilesLocations"];
93
+ } else {
94
+ settings["chat.instructionsFilesLocations"] = obj;
95
+ }
96
+ writeJson(vsSettingsPath, settings);
97
+ log.success("Removed ~/.copilot/instructions from VS Code settings");
98
+ }
99
+ }
59
100
 
60
101
  log.success("Copilot uninstall complete");
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { copyFileSync, existsSync, mkdirSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
+ import { writeContextDigests } from "../../hooks/handlers/context-digests";
9
10
  import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
10
11
  import { assets, palPkg, platform } from "../../hooks/lib/paths";
11
12
  import {
@@ -64,6 +65,13 @@ scaffoldPalSettings();
64
65
  regenerateIfNeeded();
65
66
  log.success("Generated AGENTS.md");
66
67
 
68
+ // --- Write ~/.cursor/rules/pal-*.mdc ---
69
+ mkdirSync(resolve(CURSOR_DIR, "rules"), { recursive: true });
70
+ writeContextDigests();
71
+ log.success(
72
+ "Written ~/.cursor/rules/pal-self-model.mdc + pal-wisdom.mdc + pal-opinions.mdc"
73
+ );
74
+
67
75
  log.success("Cursor installation complete");
68
76
  console.log("");
69
77
  log.info(`Skills: ${countSkills()}`);
@@ -4,9 +4,10 @@
4
4
  * Removes PAL skill symlinks.
5
5
  */
6
6
 
7
- import { copyFileSync, existsSync } from "node:fs";
7
+ import { copyFileSync, existsSync, unlinkSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { assets, palPkg, platform } from "../../hooks/lib/paths";
10
+ import { cursorFilename, getSemiStaticSources } from "../../hooks/lib/semi-static";
10
11
  import {
11
12
  loadCursorHooksTemplate,
12
13
  log,
@@ -56,4 +57,20 @@ if (removedAgents.length > 0) {
56
57
  // --- Remove PAL system docs ---
57
58
  removePalDocs();
58
59
 
60
+ // --- Remove ~/.cursor/rules/pal-*.mdc ---
61
+ for (const src of getSemiStaticSources()) {
62
+ try {
63
+ unlinkSync(resolve(CURSOR_DIR, "rules", cursorFilename(src)));
64
+ } catch {
65
+ /* gone */
66
+ }
67
+ }
68
+ // Backward compat: remove legacy merged file if present
69
+ try {
70
+ unlinkSync(resolve(CURSOR_DIR, "rules", "pal-context.mdc"));
71
+ } catch {
72
+ /* gone */
73
+ }
74
+ log.success("Removed ~/.cursor/rules/pal-*.mdc");
75
+
59
76
  log.success("Cursor uninstall complete");
@@ -14,6 +14,7 @@ import {
14
14
  unlinkSync,
15
15
  writeFileSync,
16
16
  } from "node:fs";
17
+ import { homedir } from "node:os";
17
18
  import { resolve } from "node:path";
18
19
  import { assets, palHome, platform } from "../hooks/lib/paths";
19
20
 
@@ -41,6 +42,31 @@ export function writeJson(path: string, data: unknown): void {
41
42
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
42
43
  }
43
44
 
45
+ /** Resolve the VS Code user settings.json path cross-platform. Returns null on unknown platforms. */
46
+ export function vscodeSettingsFile(): string | null {
47
+ const h = homedir();
48
+ if (process.platform === "darwin") {
49
+ return resolve(h, "Library", "Application Support", "Code", "User", "settings.json");
50
+ }
51
+ if (process.platform === "linux") {
52
+ return resolve(
53
+ process.env.XDG_CONFIG_HOME ?? resolve(h, ".config"),
54
+ "Code",
55
+ "User",
56
+ "settings.json"
57
+ );
58
+ }
59
+ if (process.platform === "win32") {
60
+ return resolve(
61
+ process.env.APPDATA ?? resolve(h, "AppData", "Roaming"),
62
+ "Code",
63
+ "User",
64
+ "settings.json"
65
+ );
66
+ }
67
+ return null;
68
+ }
69
+
44
70
  // --- Settings template merge/unmerge ---
45
71
 
46
72
  type HookEntry = { matcher?: string; hooks?: Array<{ type: string; command: string }> };
@@ -55,7 +81,11 @@ type Settings = Record<string, unknown> & {
55
81
  export function loadSettingsTemplate(templatePath: string, pkgRoot: string): Settings {
56
82
  const raw = readFileSync(templatePath, "utf-8");
57
83
  const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
58
- return JSON.parse(resolved) as Settings;
84
+ try {
85
+ return JSON.parse(resolved) as Settings;
86
+ } catch (e) {
87
+ throw new Error(`Failed to parse settings template at ${templatePath}: ${e}`);
88
+ }
59
89
  }
60
90
 
61
91
  /**
@@ -69,7 +99,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
69
99
 
70
100
  // Merge hooks (deduplicate by command)
71
101
  if (template.hooks) {
72
- if (!result.hooks) result.hooks = {};
102
+ result.hooks ??= {};
73
103
  for (const [event, entries] of Object.entries(template.hooks)) {
74
104
  const current = result.hooks[event] ?? [];
75
105
  for (const entry of entries) {
@@ -84,8 +114,8 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
84
114
 
85
115
  // Merge permissions.allow (deduplicate)
86
116
  if (template.permissions?.allow) {
87
- if (!result.permissions) result.permissions = {};
88
- if (!result.permissions.allow) result.permissions.allow = [];
117
+ result.permissions ??= {};
118
+ result.permissions.allow ??= [];
89
119
  for (const perm of template.permissions.allow) {
90
120
  if (!result.permissions.allow.includes(perm)) {
91
121
  result.permissions.allow.push(perm);
@@ -158,7 +188,11 @@ export function loadCursorHooksTemplate(
158
188
  ): CursorHooks {
159
189
  const raw = readFileSync(templatePath, "utf-8");
160
190
  const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
161
- return JSON.parse(resolved) as CursorHooks;
191
+ try {
192
+ return JSON.parse(resolved) as CursorHooks;
193
+ } catch (e) {
194
+ throw new Error(`Failed to parse Cursor hooks template at ${templatePath}: ${e}`);
195
+ }
162
196
  }
163
197
 
164
198
  /**
@@ -172,7 +206,7 @@ export function mergeCursorHooks(
172
206
  const result: CursorHooks = { ...existing, version: existing.version ?? 1 };
173
207
 
174
208
  if (template.hooks) {
175
- if (!result.hooks) result.hooks = {};
209
+ result.hooks ??= {};
176
210
  for (const [event, entries] of Object.entries(template.hooks)) {
177
211
  const current = result.hooks[event] ?? [];
178
212
  for (const entry of entries) {
@@ -215,6 +249,103 @@ export function unmergeCursorHooks(
215
249
  return result;
216
250
  }
217
251
 
252
+ // --- Codex hooks (nested group format, distinct from Cursor's flat format) ---
253
+
254
+ type CodexHookCommand = { type: string; command: string; timeout?: number };
255
+ type CodexHookGroup = { matcher?: string; hooks: CodexHookCommand[] };
256
+ type CodexHooks = { hooks?: Record<string, CodexHookGroup[]> };
257
+
258
+ /** Strip leading env-var assignments so "PAL_AGENT=x bun run ..." → "bun run ..." */
259
+ function canonicalCmd(cmd: string): string {
260
+ return cmd.replace(/^(?:\w+=\S+\s+)+/, "");
261
+ }
262
+
263
+ export function loadCodexHooksTemplate(
264
+ templatePath: string,
265
+ pkgRoot: string
266
+ ): CodexHooks {
267
+ const raw = readFileSync(templatePath, "utf-8");
268
+ const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
269
+ try {
270
+ return JSON.parse(resolved) as CodexHooks;
271
+ } catch (e) {
272
+ throw new Error(`Failed to parse Codex hooks template at ${templatePath}: ${e}`);
273
+ }
274
+ }
275
+
276
+ /** Merge PAL hooks into an existing Codex hooks.json. Deduplicates by canonical command path. */
277
+ export function mergeCodexHooks(existing: CodexHooks, template: CodexHooks): CodexHooks {
278
+ const result: CodexHooks = { ...existing };
279
+ if (!template.hooks) return result;
280
+ result.hooks ??= {};
281
+
282
+ // Collect canonical paths of PAL template commands so we can evict stale variants
283
+ const palCanonical = new Set(
284
+ Object.values(template.hooks).flatMap((groups) =>
285
+ groups.flatMap((g) => g.hooks.map((h) => canonicalCmd(h.command)))
286
+ )
287
+ );
288
+
289
+ // Strip any existing entries (nested or flat) whose canonical path matches a PAL command
290
+ for (const event of Object.keys(result.hooks)) {
291
+ result.hooks[event] = (result.hooks[event] ?? [])
292
+ .map((g) => {
293
+ const flat = g as unknown as CodexHookCommand;
294
+ if (!g.hooks && flat.command && palCanonical.has(canonicalCmd(flat.command))) {
295
+ return null;
296
+ }
297
+ const filtered = (g.hooks ?? []).filter(
298
+ (h) => !palCanonical.has(canonicalCmd(h.command))
299
+ );
300
+ return filtered.length > 0 ? { ...g, hooks: filtered } : null;
301
+ })
302
+ .filter((g): g is CodexHookGroup => g !== null);
303
+ if (result.hooks[event].length === 0) delete result.hooks[event];
304
+ }
305
+
306
+ // Add fresh template entries
307
+ for (const [event, groups] of Object.entries(template.hooks)) {
308
+ const current = result.hooks[event] ?? [];
309
+ for (const group of groups) current.push(group);
310
+ result.hooks[event] = current;
311
+ }
312
+ return result;
313
+ }
314
+
315
+ /** Remove PAL hooks from an existing Codex hooks.json. Preserves user hooks. */
316
+ export function unmergeCodexHooks(
317
+ existing: CodexHooks,
318
+ template: CodexHooks
319
+ ): CodexHooks {
320
+ const result: CodexHooks = { ...existing };
321
+ if (!template.hooks || !result.hooks) return result;
322
+
323
+ // Match by canonical path so prefix variants (PAL_AGENT=codex, etc.) are all removed
324
+ const palCanonical = new Set(
325
+ Object.values(template.hooks).flatMap((groups) =>
326
+ groups.flatMap((g) => g.hooks.map((h) => canonicalCmd(h.command)))
327
+ )
328
+ );
329
+
330
+ for (const event of Object.keys(result.hooks)) {
331
+ result.hooks[event] = (result.hooks[event] ?? [])
332
+ .map((g) => {
333
+ const flat = g as unknown as CodexHookCommand;
334
+ if (!g.hooks && flat.command && palCanonical.has(canonicalCmd(flat.command))) {
335
+ return null;
336
+ }
337
+ const filtered = (g.hooks ?? []).filter(
338
+ (h) => !palCanonical.has(canonicalCmd(h.command))
339
+ );
340
+ return filtered.length > 0 ? { ...g, hooks: filtered } : null;
341
+ })
342
+ .filter((g): g is CodexHookGroup => g !== null);
343
+ if (result.hooks[event].length === 0) delete result.hooks[event];
344
+ }
345
+ if (Object.keys(result.hooks).length === 0) delete result.hooks;
346
+ return result;
347
+ }
348
+
218
349
  // --- TELOS scaffolding ---
219
350
 
220
351
  /** Copy template files into telos/ without overwriting existing ones */
@@ -249,6 +380,21 @@ export function scaffoldPalSettings(): void {
249
380
  copyFileSync(src, dst);
250
381
  log.info("Created pal-settings.json from template");
251
382
  }
383
+
384
+ // Strip deprecated loadAtStartup.files entries from existing installs.
385
+ // mergeSettings only adds, never removes — deprecated entries persist indefinitely otherwise.
386
+ try {
387
+ const raw = JSON.parse(readFileSync(dst, "utf-8"));
388
+ const files: string[] = raw?.loadAtStartup?.files ?? [];
389
+ const cleaned = files.filter((f: string) => !f.endsWith("PROJECTS.md"));
390
+ if (cleaned.length !== files.length) {
391
+ raw.loadAtStartup.files = cleaned;
392
+ writeFileSync(dst, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
393
+ log.info("Removed deprecated PROJECTS.md from loadAtStartup.files");
394
+ }
395
+ } catch {
396
+ /* non-fatal — malformed settings left as-is */
397
+ }
252
398
  }
253
399
 
254
400
  // --- PAL docs (modular context routing files) ---
@@ -466,14 +612,14 @@ function extractAgentForPlatform(content: string, platform: AgentPlatform): stri
466
612
  for (const line of frontmatter.split("\n")) {
467
613
  if (!line.trim()) continue;
468
614
 
469
- const platformMatch = line.match(/^(claude|opencode|cursor):\s*$/);
615
+ const platformMatch = new RegExp(/^(claude|opencode|cursor):\s*$/).exec(line);
470
616
  if (platformMatch) {
471
617
  currentPlatform = platformMatch[1] as AgentPlatform;
472
618
  continue;
473
619
  }
474
620
 
475
621
  if (currentPlatform) {
476
- if (line.match(/^ {2}/)) {
622
+ if (new RegExp(/^ {2}/).exec(line)) {
477
623
  platformLines[currentPlatform].push(line.slice(2)); // un-indent one level
478
624
  continue;
479
625
  }
@@ -554,9 +700,15 @@ export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] {
554
700
 
555
701
  /** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
556
702
  export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
557
- return JSON.parse(
558
- readFileSync(templatePath, "utf-8").replaceAll("{{PKG_ROOT}}", pkgRoot)
703
+ const resolved = readFileSync(templatePath, "utf-8").replaceAll(
704
+ "{{PKG_ROOT}}",
705
+ pkgRoot
559
706
  );
707
+ try {
708
+ return JSON.parse(resolved);
709
+ } catch (e) {
710
+ throw new Error(`Failed to parse Copilot hooks template at ${templatePath}: ${e}`);
711
+ }
560
712
  }
561
713
 
562
714
  // --- Skill Index ---
@@ -578,7 +730,7 @@ function extractTriggers(description: string): string[] {
578
730
  // Extract "Use when ..." phrases and key terms
579
731
  const triggers = new Set<string>();
580
732
 
581
- const useWhen = description.match(/Use when\s+(.+?)(?:\.|$)/i);
733
+ const useWhen = new RegExp(/Use when\s+(.+?)(?:\.|$)/i).exec(description);
582
734
  if (useWhen) {
583
735
  const words = useWhen[1]
584
736
  .toLowerCase()
@@ -621,12 +773,12 @@ export function generateSkillIndex(): number {
621
773
 
622
774
  try {
623
775
  const content = readFileSync(skillMd, "utf-8");
624
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
776
+ const fmMatch = new RegExp(/^---\n([\s\S]*?)\n---/).exec(content);
625
777
  if (!fmMatch) continue;
626
778
 
627
779
  const fm = fmMatch[1];
628
- const nameMatch = fm.match(/^name:\s*(.+)$/m);
629
- const descMatch = fm.match(/^description:\s*"?(.+?)"?\s*$/m);
780
+ const nameMatch = new RegExp(/^name:\s*(.+)$/m).exec(fm);
781
+ const descMatch = new RegExp(/^description:\s*"?(.+?)"?\s*$/m).exec(fm);
630
782
  if (!nameMatch) continue;
631
783
 
632
784
  const skillName = nameMatch[1].trim();
@@ -3,10 +3,18 @@
3
3
  * Deploys plugin, installs skills, generates AGENTS.md.
4
4
  */
5
5
 
6
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ statSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
7
14
  import { resolve } from "node:path";
8
15
  import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
9
16
  import { palPkg, platform } from "../../hooks/lib/paths";
17
+ import { getSemiStaticSources } from "../../hooks/lib/semi-static";
10
18
  import {
11
19
  copyAgentsForOpencode,
12
20
  copyPalDocs,
@@ -70,6 +78,26 @@ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
70
78
  regenerateIfNeeded();
71
79
  log.success("Generated ~/.config/opencode/AGENTS.md");
72
80
 
81
+ // --- 7. Add semi-static digest files to instructions[] in config.json ---
82
+ const configPath = resolve(OC_GLOBAL_DIR, "config.json");
83
+ const staticFiles = getSemiStaticSources().map((s) => s.path);
84
+ let ocConfig: Record<string, unknown> = {};
85
+ if (existsSync(configPath) && statSync(configPath).size > 0) {
86
+ try {
87
+ ocConfig = JSON.parse(readFileSync(configPath, "utf-8"));
88
+ } catch {
89
+ /* start fresh */
90
+ }
91
+ }
92
+ const existingInstructions = Array.isArray(ocConfig.instructions)
93
+ ? (ocConfig.instructions as string[])
94
+ : [];
95
+ ocConfig.instructions = [...new Set([...existingInstructions, ...staticFiles])];
96
+ writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
97
+ log.success(
98
+ `Updated config.json: ${(ocConfig.instructions as string[]).length} instructions`
99
+ );
100
+
73
101
  log.success("opencode installation complete");
74
102
  console.log("");
75
103
  log.info(`Plugin: ${pluginDst}`);
@@ -34,6 +34,9 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
34
34
  const { captureRating } = await lib<typeof import("../../hooks/handlers/rating")>(
35
35
  "../handlers/rating.ts"
36
36
  );
37
+ const { getRetrievalReminder } = await lib<
38
+ typeof import("../../hooks/handlers/inject-retrieval")
39
+ >("../handlers/inject-retrieval.ts");
37
40
 
38
41
  function partsToText(parts: Array<Record<string, unknown>>): string {
39
42
  return parts
@@ -74,7 +77,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
74
77
  return {
75
78
  // --- Per-message: Inject dynamic system reminder ---
76
79
  "experimental.chat.system.transform": async (_input, output) => {
77
- const reminder = buildSystemReminder();
80
+ const reminder = buildSystemReminder({ agent: "opencode" });
78
81
  if (reminder) output.system.push(reminder);
79
82
  },
80
83
 
@@ -120,17 +123,25 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
120
123
 
121
124
  // --- Capture ratings + session naming from user messages (shared handlers) ---
122
125
  "chat.message": async (input, output) => {
123
- const text =
124
- output.parts
125
- ?.filter((p) => p.type === "text")
126
- .map((p) => p.text || "")
127
- .join(" ") ?? "";
128
-
129
- if (text.trim()) {
130
- await Promise.allSettled([
131
- captureRating(text, input.sessionID),
132
- captureSessionName(text, input.sessionID),
133
- ]);
126
+ const text = partsToText(output.parts ?? []);
127
+ if (!text.trim()) return;
128
+
129
+ const [, , retrievalResult] = await Promise.allSettled([
130
+ captureRating(text, input.sessionID),
131
+ captureSessionName(text, input.sessionID),
132
+ getRetrievalReminder(text),
133
+ ]);
134
+
135
+ if (retrievalResult.status === "fulfilled" && retrievalResult.value) {
136
+ const injected = {
137
+ id: `pal-retrieval-${Date.now()}`,
138
+ sessionID: input.sessionID,
139
+ messageID: input.messageID ?? `pal-msg-${Date.now()}`,
140
+ type: "text" as const,
141
+ text: retrievalResult.value,
142
+ synthetic: true,
143
+ };
144
+ output.parts = [injected, ...(output.parts ?? [])];
134
145
  }
135
146
  },
136
147