portable-agent-layer 0.36.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 (89) hide show
  1. package/README.md +1 -0
  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 -20
  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/tools/update-telos.ts +0 -1
  20. package/assets/templates/PAL/ALGORITHM.md +27 -3
  21. package/assets/templates/hooks.codex.json +44 -0
  22. package/assets/templates/hooks.cursor.json +11 -5
  23. package/package.json +2 -1
  24. package/src/cli/index.ts +112 -14
  25. package/src/cli/migrate.ts +299 -0
  26. package/src/cli/setup-identity.ts +3 -3
  27. package/src/cli/setup-telos.ts +0 -1
  28. package/src/hooks/CompactRecover.ts +11 -5
  29. package/src/hooks/LoadContext.ts +14 -2
  30. package/src/hooks/PreCompactPersist.ts +26 -34
  31. package/src/hooks/SecurityValidator.ts +43 -21
  32. package/src/hooks/StopOrchestrator.ts +4 -1
  33. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  34. package/src/hooks/handlers/auto-graduate.ts +2 -2
  35. package/src/hooks/handlers/backup.ts +3 -3
  36. package/src/hooks/handlers/failure.ts +5 -3
  37. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  38. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  39. package/src/hooks/handlers/rating.ts +2 -1
  40. package/src/hooks/handlers/readme-sync.ts +3 -2
  41. package/src/hooks/handlers/session-intelligence.ts +9 -8
  42. package/src/hooks/handlers/session-name.ts +2 -2
  43. package/src/hooks/handlers/synthesis.ts +5 -2
  44. package/src/hooks/handlers/update-counts.ts +3 -2
  45. package/src/hooks/lib/agent.ts +20 -18
  46. package/src/hooks/lib/context.ts +45 -117
  47. package/src/hooks/lib/entities.ts +7 -7
  48. package/src/hooks/lib/frontmatter.ts +4 -4
  49. package/src/hooks/lib/graduation.ts +7 -6
  50. package/src/hooks/lib/inference.ts +6 -2
  51. package/src/hooks/lib/learning-category.ts +1 -1
  52. package/src/hooks/lib/learning-store.ts +6 -1
  53. package/src/hooks/lib/notify.ts +2 -2
  54. package/src/hooks/lib/opinions.ts +3 -3
  55. package/src/hooks/lib/paths.ts +2 -0
  56. package/src/hooks/lib/projects.ts +142 -74
  57. package/src/hooks/lib/readme-sync.ts +1 -1
  58. package/src/hooks/lib/relationship.ts +3 -15
  59. package/src/hooks/lib/retrieval-index.ts +5 -3
  60. package/src/hooks/lib/retrieval.ts +11 -12
  61. package/src/hooks/lib/security.ts +22 -18
  62. package/src/hooks/lib/semi-static.ts +4 -2
  63. package/src/hooks/lib/session-names.ts +1 -1
  64. package/src/hooks/lib/settings.ts +1 -1
  65. package/src/hooks/lib/setup.ts +2 -60
  66. package/src/hooks/lib/signals.ts +2 -2
  67. package/src/hooks/lib/stdin.ts +1 -1
  68. package/src/hooks/lib/stop.ts +13 -6
  69. package/src/hooks/lib/token-usage.ts +1 -2
  70. package/src/hooks/lib/transcript.ts +1 -1
  71. package/src/hooks/lib/wisdom.ts +5 -5
  72. package/src/hooks/lib/work-tracking.ts +8 -14
  73. package/src/targets/codex/install.ts +95 -0
  74. package/src/targets/codex/uninstall.ts +70 -0
  75. package/src/targets/lib.ts +140 -14
  76. package/src/targets/opencode/plugin.ts +22 -11
  77. package/src/tools/agent/algorithm-reflect.ts +1 -1
  78. package/src/tools/agent/analyze.ts +18 -18
  79. package/src/tools/agent/handoff-note.ts +1 -1
  80. package/src/tools/agent/project.ts +375 -75
  81. package/src/tools/agent/synthesize.ts +6 -42
  82. package/src/tools/agent/thread.ts +15 -14
  83. package/src/tools/agent/wisdom-frame.ts +9 -3
  84. package/src/tools/import.ts +1 -1
  85. package/src/tools/relationship-reflect.ts +13 -11
  86. package/src/tools/self-model.ts +20 -16
  87. package/src/tools/session-summary.ts +3 -3
  88. package/src/tools/token-cost.ts +15 -16
  89. package/assets/skills/telos/tools/update-projects.ts +0 -106
@@ -81,7 +81,11 @@ type Settings = Record<string, unknown> & {
81
81
  export function loadSettingsTemplate(templatePath: string, pkgRoot: string): Settings {
82
82
  const raw = readFileSync(templatePath, "utf-8");
83
83
  const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
84
- 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
+ }
85
89
  }
86
90
 
87
91
  /**
@@ -95,7 +99,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
95
99
 
96
100
  // Merge hooks (deduplicate by command)
97
101
  if (template.hooks) {
98
- if (!result.hooks) result.hooks = {};
102
+ result.hooks ??= {};
99
103
  for (const [event, entries] of Object.entries(template.hooks)) {
100
104
  const current = result.hooks[event] ?? [];
101
105
  for (const entry of entries) {
@@ -110,8 +114,8 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
110
114
 
111
115
  // Merge permissions.allow (deduplicate)
112
116
  if (template.permissions?.allow) {
113
- if (!result.permissions) result.permissions = {};
114
- if (!result.permissions.allow) result.permissions.allow = [];
117
+ result.permissions ??= {};
118
+ result.permissions.allow ??= [];
115
119
  for (const perm of template.permissions.allow) {
116
120
  if (!result.permissions.allow.includes(perm)) {
117
121
  result.permissions.allow.push(perm);
@@ -184,7 +188,11 @@ export function loadCursorHooksTemplate(
184
188
  ): CursorHooks {
185
189
  const raw = readFileSync(templatePath, "utf-8");
186
190
  const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
187
- 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
+ }
188
196
  }
189
197
 
190
198
  /**
@@ -198,7 +206,7 @@ export function mergeCursorHooks(
198
206
  const result: CursorHooks = { ...existing, version: existing.version ?? 1 };
199
207
 
200
208
  if (template.hooks) {
201
- if (!result.hooks) result.hooks = {};
209
+ result.hooks ??= {};
202
210
  for (const [event, entries] of Object.entries(template.hooks)) {
203
211
  const current = result.hooks[event] ?? [];
204
212
  for (const entry of entries) {
@@ -241,6 +249,103 @@ export function unmergeCursorHooks(
241
249
  return result;
242
250
  }
243
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
+
244
349
  // --- TELOS scaffolding ---
245
350
 
246
351
  /** Copy template files into telos/ without overwriting existing ones */
@@ -275,6 +380,21 @@ export function scaffoldPalSettings(): void {
275
380
  copyFileSync(src, dst);
276
381
  log.info("Created pal-settings.json from template");
277
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
+ }
278
398
  }
279
399
 
280
400
  // --- PAL docs (modular context routing files) ---
@@ -492,14 +612,14 @@ function extractAgentForPlatform(content: string, platform: AgentPlatform): stri
492
612
  for (const line of frontmatter.split("\n")) {
493
613
  if (!line.trim()) continue;
494
614
 
495
- const platformMatch = line.match(/^(claude|opencode|cursor):\s*$/);
615
+ const platformMatch = new RegExp(/^(claude|opencode|cursor):\s*$/).exec(line);
496
616
  if (platformMatch) {
497
617
  currentPlatform = platformMatch[1] as AgentPlatform;
498
618
  continue;
499
619
  }
500
620
 
501
621
  if (currentPlatform) {
502
- if (line.match(/^ {2}/)) {
622
+ if (new RegExp(/^ {2}/).exec(line)) {
503
623
  platformLines[currentPlatform].push(line.slice(2)); // un-indent one level
504
624
  continue;
505
625
  }
@@ -580,9 +700,15 @@ export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] {
580
700
 
581
701
  /** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
582
702
  export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
583
- return JSON.parse(
584
- readFileSync(templatePath, "utf-8").replaceAll("{{PKG_ROOT}}", pkgRoot)
703
+ const resolved = readFileSync(templatePath, "utf-8").replaceAll(
704
+ "{{PKG_ROOT}}",
705
+ pkgRoot
585
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
+ }
586
712
  }
587
713
 
588
714
  // --- Skill Index ---
@@ -604,7 +730,7 @@ function extractTriggers(description: string): string[] {
604
730
  // Extract "Use when ..." phrases and key terms
605
731
  const triggers = new Set<string>();
606
732
 
607
- const useWhen = description.match(/Use when\s+(.+?)(?:\.|$)/i);
733
+ const useWhen = new RegExp(/Use when\s+(.+?)(?:\.|$)/i).exec(description);
608
734
  if (useWhen) {
609
735
  const words = useWhen[1]
610
736
  .toLowerCase()
@@ -647,12 +773,12 @@ export function generateSkillIndex(): number {
647
773
 
648
774
  try {
649
775
  const content = readFileSync(skillMd, "utf-8");
650
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
776
+ const fmMatch = new RegExp(/^---\n([\s\S]*?)\n---/).exec(content);
651
777
  if (!fmMatch) continue;
652
778
 
653
779
  const fm = fmMatch[1];
654
- const nameMatch = fm.match(/^name:\s*(.+)$/m);
655
- 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);
656
782
  if (!nameMatch) continue;
657
783
 
658
784
  const skillName = nameMatch[1].trim();
@@ -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
@@ -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
 
@@ -40,7 +40,7 @@ function reflectionsPath(): string {
40
40
  return resolve(dir, "algorithm-reflections.jsonl");
41
41
  }
42
42
 
43
- export function appendReflection(reflection: AlgorithmReflection): {
43
+ function appendReflection(reflection: AlgorithmReflection): {
44
44
  success: boolean;
45
45
  message: string;
46
46
  path: string;
@@ -34,25 +34,26 @@ export function printReport(result: AnalysisResult): void {
34
34
 
35
35
  if (result.ratings) {
36
36
  const r = result.ratings;
37
- const avgColor = r.average >= 7 ? c.green : r.average <= 4 ? c.red : c.yellow;
37
+ const lowOrMid = r.average <= 4 ? c.red : c.yellow;
38
+ const avgColor = r.average >= 7 ? c.green : lowOrMid;
39
+ const ratingStr = `${r.average.toFixed(1)}/10`;
40
+ const lowStr = `Low (≤4): ${r.low.count}`;
41
+ const highStr = `High (≥7): ${r.high.count}`;
38
42
  console.log(
39
- `\n ${c.bold("Ratings:")} ${avgColor(`${r.average.toFixed(1)}/10`)} avg (${r.total} total)`
40
- );
41
- console.log(
42
- ` ${c.red(`Low (≤4): ${r.low.count}`)} | ${c.green(`High (≥7): ${r.high.count}`)}`
43
+ `\n ${c.bold("Ratings:")} ${avgColor(ratingStr)} avg (${r.total} total)`
43
44
  );
45
+ console.log(` ${c.red(lowStr)} | ${c.green(highStr)}`);
44
46
  }
45
47
 
46
48
  if (result.candidates.length > 0) {
47
- console.log(
48
- `\n ${c.bold(c.green(`Graduation Report — ${result.candidates.length} pattern(s) detected`))}\n`
49
- );
49
+ const graduationHeader = `Graduation Report — ${result.candidates.length} pattern(s) detected`;
50
+ console.log(`\n ${c.bold(c.green(graduationHeader))}\n`);
50
51
  console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
51
52
 
52
53
  for (const candidate of result.candidates) {
53
- console.log(
54
- ` ${c.cyan(`[${candidate.domain}]`)} ${c.bold(`${candidate.entries.length}x`)} occurrences`
55
- );
54
+ const domain = `[${candidate.domain}]`;
55
+ const count = `${candidate.entries.length}x`;
56
+ console.log(` ${c.cyan(domain)} ${c.bold(count)} occurrences`);
56
57
  console.log("");
57
58
 
58
59
  for (const entry of candidate.entries) {
@@ -72,9 +73,8 @@ export function printReport(result: AnalysisResult): void {
72
73
  }
73
74
 
74
75
  console.log("");
75
- console.log(
76
- ` Target frame: ${c.magenta(`memory/wisdom/frames/${candidate.domain}.md`)}`
77
- );
76
+ const framePath = `memory/wisdom/frames/${candidate.domain}.md`;
77
+ console.log(` Target frame: ${c.magenta(framePath)}`);
78
78
  console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
79
79
  }
80
80
  }
@@ -82,9 +82,9 @@ export function printReport(result: AnalysisResult): void {
82
82
  if (result.emerging.length > 0) {
83
83
  console.log(` ${c.bold(c.yellow("Emerging (2x — one more to graduate)"))}\n`);
84
84
  for (const group of result.emerging) {
85
- console.log(
86
- ` ${c.cyan(`[${group.domain}]`)} ${c.bold(`${group.entries.length}x`)}`
87
- );
85
+ const domain = `[${group.domain}]`;
86
+ const count = `${group.entries.length}x`;
87
+ console.log(` ${c.cyan(domain)} ${c.bold(count)}`);
88
88
  for (const entry of group.entries) {
89
89
  const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
90
90
  const tag =
@@ -154,4 +154,4 @@ async function run() {
154
154
  printReport(result);
155
155
  }
156
156
 
157
- if (import.meta.main) run();
157
+ if (import.meta.main) await run();
@@ -43,7 +43,7 @@ function writeHandoffs(handoffs: Record<string, HandoffEntry>): void {
43
43
  writeFileSync(handoffPath(), JSON.stringify(trimmed, null, 2), "utf-8");
44
44
  }
45
45
 
46
- export function writeHandoffNote(
46
+ function writeHandoffNote(
47
47
  cwd: string,
48
48
  title: string,
49
49
  text: string,