portable-agent-layer 0.22.0 → 0.23.1

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 (42) hide show
  1. package/assets/agents/gemini-researcher.md +17 -3
  2. package/assets/agents/grok-researcher.md +19 -5
  3. package/assets/agents/multi-perspective-researcher.md +16 -2
  4. package/assets/agents/perplexity-researcher.md +17 -3
  5. package/assets/skills/analyze-pdf/SKILL.md +1 -1
  6. package/assets/skills/analyze-youtube/SKILL.md +1 -1
  7. package/assets/skills/extract-entities/SKILL.md +1 -1
  8. package/assets/skills/fyzz-chat-api/SKILL.md +3 -3
  9. package/assets/skills/reflect/SKILL.md +2 -2
  10. package/assets/skills/telos/SKILL.md +6 -6
  11. package/assets/templates/AGENTS.md.template +2 -2
  12. package/assets/templates/PAL/ALGORITHM.md +93 -10
  13. package/assets/templates/PAL/CONTEXT_ROUTING.md +17 -17
  14. package/assets/templates/PAL/MEMORY_SYSTEM.md +5 -5
  15. package/assets/templates/PAL/README.md +12 -9
  16. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
  17. package/assets/templates/pal-settings.json +6 -3
  18. package/assets/templates/settings.claude.json +2 -2
  19. package/package.json +3 -1
  20. package/src/cli/index.ts +4 -11
  21. package/src/hooks/handlers/failure.ts +3 -1
  22. package/src/hooks/handlers/rating.ts +17 -2
  23. package/src/hooks/handlers/reflect-trigger.ts +4 -4
  24. package/src/hooks/handlers/relationship.ts +1 -1
  25. package/src/hooks/handlers/session-intelligence.ts +324 -0
  26. package/src/hooks/handlers/session-name.ts +2 -2
  27. package/src/hooks/handlers/synthesis.ts +36 -0
  28. package/src/hooks/handlers/update-check.ts +2 -2
  29. package/src/hooks/handlers/work-learning.ts +1 -1
  30. package/src/hooks/lib/context.ts +119 -2
  31. package/src/hooks/lib/paths.ts +4 -12
  32. package/src/hooks/lib/security.ts +39 -28
  33. package/src/hooks/lib/stop.ts +56 -7
  34. package/src/hooks/lib/token-usage.ts +1 -0
  35. package/src/targets/claude/install.ts +1 -1
  36. package/src/targets/cursor/install.ts +7 -1
  37. package/src/targets/cursor/uninstall.ts +7 -0
  38. package/src/targets/lib.ts +125 -115
  39. package/src/targets/opencode/install.ts +4 -4
  40. package/src/tools/agent/algorithm-reflect.ts +2 -0
  41. package/src/tools/agent/synthesize.ts +361 -0
  42. package/src/tools/agent/thread.ts +162 -0
@@ -253,11 +253,13 @@ export function scaffoldPalSettings(): void {
253
253
 
254
254
  // --- PAL docs (modular context routing files) ---
255
255
 
256
- const PAL_DOCS_DIR = resolve(platform.agentsDir(), "PAL");
256
+ const PAL_DOCS_DIR = resolve(palHome(), "docs");
257
+ const PAL_TOOLS_DIR = resolve(palHome(), "tools");
257
258
 
258
259
  /**
259
- * Install PAL system docs into ~/.agents/PAL/.
260
+ * Install PAL system docs into ~/.pal/docs/.
260
261
  * Always overwrites — these are engine-managed, not user-editable.
262
+ * Also creates ~/.pal/tools/ → repo agent tools (symlink).
261
263
  */
262
264
  export function copyPalDocs(): number {
263
265
  const srcDir = assets.palDocs();
@@ -273,28 +275,25 @@ export function copyPalDocs(): number {
273
275
  count++;
274
276
  }
275
277
 
276
- // Symlink ~/.agents/PAL/{telos,memory,tools}source locations
278
+ // ~/.pal/tools/ → repo agent tools
277
279
  const linkType = process.platform === "win32" ? "junction" : "dir";
278
- ensureSymlink(resolve(PAL_DOCS_DIR, "telos"), resolve(palHome(), "telos"), linkType);
279
- ensureSymlink(resolve(PAL_DOCS_DIR, "memory"), resolve(palHome(), "memory"), linkType);
280
- ensureSymlink(resolve(PAL_DOCS_DIR, "tools"), assets.agentTools(), linkType);
280
+ ensureSymlink(PAL_TOOLS_DIR, assets.agentTools(), linkType);
281
281
 
282
282
  return count;
283
283
  }
284
284
 
285
- /** Remove PAL system docs from ~/.agents/PAL/ */
285
+ /** Remove PAL system docs from ~/.pal/docs/ */
286
286
  export function removePalDocs(): void {
287
- if (!existsSync(PAL_DOCS_DIR)) return;
288
- for (const file of readdirSync(PAL_DOCS_DIR).filter((f) => f.endsWith(".md"))) {
289
- try {
290
- unlinkSync(resolve(PAL_DOCS_DIR, file));
291
- } catch {
292
- /* gone */
293
- }
287
+ // Remove tools symlink
288
+ try {
289
+ unlinkSync(PAL_TOOLS_DIR);
290
+ } catch {
291
+ /* gone */
294
292
  }
293
+ if (!existsSync(PAL_DOCS_DIR)) return;
295
294
  try {
296
295
  rmSync(PAL_DOCS_DIR, { recursive: true });
297
- log.info("Removed ~/.agents/PAL/");
296
+ log.info("Removed ~/.pal/docs/");
298
297
  } catch {
299
298
  /* gone */
300
299
  }
@@ -302,12 +301,12 @@ export function removePalDocs(): void {
302
301
 
303
302
  // --- Skills ---
304
303
 
305
- const AGENTS_SKILLS_DIR = resolve(platform.agentsDir(), "skills");
304
+ const PAL_SKILLS_DIR = resolve(palHome(), "skills");
306
305
 
307
306
  /**
308
307
  * Install PAL skills by symlinking:
309
- * ~/.agents/skills/<name> → <repo>/assets/skills/<name> (source of truth)
310
- * ~/.claude/skills/<name> → ~/.agents/skills/<name> (Claude Code discovery)
308
+ * ~/.pal/skills/<name> → <repo>/assets/skills/<name> (source of truth)
309
+ * ~/.claude/skills/<name> → ~/.pal/skills/<name> (agent discovery)
311
310
  *
312
311
  * Symlinks mean tools inside skills can import from the repo (src/hooks/lib/*)
313
312
  * and everything resolves naturally. Additive — skips skills already installed.
@@ -316,7 +315,7 @@ export function copySkills(claudeSkillsDir: string): number {
316
315
  const skillsDir = assets.skills();
317
316
  if (!existsSync(skillsDir)) return 0;
318
317
 
319
- mkdirSync(AGENTS_SKILLS_DIR, { recursive: true });
318
+ mkdirSync(PAL_SKILLS_DIR, { recursive: true });
320
319
  mkdirSync(claudeSkillsDir, { recursive: true });
321
320
  const linkType = process.platform === "win32" ? "junction" : "dir";
322
321
  let count = 0;
@@ -325,17 +324,22 @@ export function copySkills(claudeSkillsDir: string): number {
325
324
  const srcDir = resolve(skillsDir, name);
326
325
  if (!existsSync(resolve(srcDir, "SKILL.md"))) continue;
327
326
 
328
- // ~/.agents/skills/<name> → <repo>/assets/skills/<name>
329
- const agentLink = resolve(AGENTS_SKILLS_DIR, name);
330
- ensureSymlink(agentLink, srcDir, linkType);
327
+ // ~/.pal/skills/<name> → <repo>/assets/skills/<name>
328
+ const palLink = resolve(PAL_SKILLS_DIR, name);
329
+ ensureSymlink(palLink, srcDir, linkType);
331
330
 
332
- // ~/.claude/skills/<name> → ~/.agents/skills/<name>
331
+ // ~/.claude/skills/<name> → ~/.pal/skills/<name>
333
332
  const claudeLink = resolve(claudeSkillsDir, name);
334
- ensureSymlink(claudeLink, agentLink, linkType);
333
+ ensureSymlink(claudeLink, palLink, linkType);
335
334
 
336
335
  log.info(`Linked skill: ${name}`);
337
336
  count++;
338
337
  }
338
+
339
+ // ~/.agents/skills/ → ~/.pal/skills/
340
+ mkdirSync(platform.agentsDir(), { recursive: true });
341
+ ensureSymlink(resolve(platform.agentsDir(), "skills"), PAL_SKILLS_DIR, linkType);
342
+
339
343
  return count;
340
344
  }
341
345
 
@@ -356,7 +360,7 @@ function ensureSymlink(link: string, target: string, type: "dir" | "junction"):
356
360
  symlinkSync(target, link, type);
357
361
  }
358
362
 
359
- /** Remove PAL skill symlinks from ~/.agents/skills/ and ~/.claude/skills/ */
363
+ /** Remove PAL skill symlinks from ~/.pal/skills/ and ~/.claude/skills/ */
360
364
  export function removeSkills(claudeSkillsDir: string): string[] {
361
365
  const skillsDir = assets.skills();
362
366
  if (!existsSync(skillsDir)) return [];
@@ -365,10 +369,7 @@ export function removeSkills(claudeSkillsDir: string): string[] {
365
369
  for (const name of readdirSync(skillsDir)) {
366
370
  if (!existsSync(resolve(skillsDir, name, "SKILL.md"))) continue;
367
371
 
368
- for (const link of [
369
- resolve(AGENTS_SKILLS_DIR, name),
370
- resolve(claudeSkillsDir, name),
371
- ]) {
372
+ for (const link of [resolve(PAL_SKILLS_DIR, name), resolve(claudeSkillsDir, name)]) {
372
373
  try {
373
374
  unlinkSync(link);
374
375
  } catch {
@@ -378,6 +379,14 @@ export function removeSkills(claudeSkillsDir: string): string[] {
378
379
  removed.push(name);
379
380
  log.info(`Removed skill: ${name}`);
380
381
  }
382
+
383
+ // Remove ~/.agents/skills/ → ~/.pal/skills/ symlink
384
+ try {
385
+ unlinkSync(resolve(platform.agentsDir(), "skills"));
386
+ } catch {
387
+ /* gone */
388
+ }
389
+
381
390
  return removed;
382
391
  }
383
392
 
@@ -387,28 +396,10 @@ const CLAUDE_AGENTS_DIR = resolve(platform.claudeDir(), "agents");
387
396
 
388
397
  /**
389
398
  * Install PAL agent definitions into ~/.claude/agents/.
390
- * Additiveskips agents already installed.
399
+ * Always overwrites engine-managed, not user-editable.
391
400
  */
392
401
  export function copyAgents(): number {
393
- const agentsDir = assets.agents();
394
- if (!existsSync(agentsDir)) return 0;
395
-
396
- mkdirSync(CLAUDE_AGENTS_DIR, { recursive: true });
397
- let count = 0;
398
-
399
- for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md"))) {
400
- const src = resolve(agentsDir, file);
401
- const dst = resolve(CLAUDE_AGENTS_DIR, file);
402
-
403
- if (!existsSync(dst)) {
404
- copyFileSync(src, dst);
405
- log.info(`Added agent: ${file.replace(/\.md$/, "")}`);
406
- count++;
407
- } else {
408
- log.warn(`Agent exists, skipping: ${file.replace(/\.md$/, "")}`);
409
- }
410
- }
411
- return count;
402
+ return installAgents(CLAUDE_AGENTS_DIR, "claude");
412
403
  }
413
404
 
414
405
  /** Remove PAL agents from ~/.claude/agents/ */
@@ -439,103 +430,122 @@ export function countAgents(): number {
439
430
  }
440
431
  }
441
432
 
442
- // --- Opencode agent translation ---
433
+ // --- Agent platform extraction ---
443
434
 
444
- /** Map Claude Code tool names to opencode permission keys */
445
- const TOOL_TO_PERMISSION: Record<string, string> = {
446
- WebSearch: "webfetch",
447
- WebFetch: "webfetch",
448
- Read: "read",
449
- Grep: "read",
450
- Glob: "read",
451
- Write: "edit",
452
- Edit: "edit",
453
- Bash: "bash",
454
- };
435
+ const AGENT_PLATFORMS = ["claude", "opencode", "cursor"] as const;
436
+ type AgentPlatform = (typeof AGENT_PLATFORMS)[number];
455
437
 
456
438
  /**
457
- * Translate a Claude Code agent .md file to opencode format.
458
- * - `tools: X, Y` → `permission: { x: allow, ... }`
459
- * - Adds `mode: subagent`
460
- * - Keeps everything else (name, description, model, body)
439
+ * Extract a platform-specific agent file from the unified agent format.
440
+ *
441
+ * Each agent .md defines platform blocks at the top level:
442
+ * claude: → fields for Claude Code
443
+ * opencode: → fields for opencode
444
+ * cursor: → fields for Cursor
445
+ *
446
+ * Global fields (name, description) are always included.
447
+ * The target platform block is un-indented and merged into the root.
448
+ * All other platform blocks are stripped.
461
449
  */
462
- export function translateAgentForOpencode(content: string): string {
450
+ export function extractAgentForPlatform(
451
+ content: string,
452
+ platform: AgentPlatform
453
+ ): string {
463
454
  const parts = content.split(/^---\s*$/m);
464
455
  if (parts.length < 3) return content;
465
456
 
466
457
  const frontmatter = parts[1];
467
458
  const body = parts.slice(2).join("---");
468
459
 
469
- // Extract tools line
470
- const toolsMatch = frontmatter.match(/^tools:\s*(.+)$/m);
471
- const tools = toolsMatch ? toolsMatch[1].split(",").map((t) => t.trim()) : [];
460
+ const globalLines: string[] = [];
461
+ const platformLines: Record<AgentPlatform, string[]> = {
462
+ claude: [],
463
+ opencode: [],
464
+ cursor: [],
465
+ };
466
+ let currentPlatform: AgentPlatform | null = null;
472
467
 
473
- // Build opencode permissions (deduplicated)
474
- const perms = new Set<string>();
475
- for (const tool of tools) {
476
- const perm = TOOL_TO_PERMISSION[tool];
477
- if (perm) perms.add(perm);
478
- }
468
+ for (const line of frontmatter.split("\n")) {
469
+ if (!line.trim()) continue;
479
470
 
480
- // Build new frontmatter: remove tools line, add mode + permission
481
- let newFrontmatter = frontmatter.replace(/^tools:\s*.+$/m, "").trim();
482
- newFrontmatter += "\nmode: subagent";
483
- if (perms.size > 0) {
484
- const permObj = Object.fromEntries([...perms].map((p) => [p, "allow"]));
485
- // Inline YAML object
486
- const permYaml = Object.entries(permObj)
487
- .map(([k, v]) => ` ${k}: ${v}`)
488
- .join("\n");
489
- newFrontmatter += `\npermission:\n${permYaml}`;
471
+ const platformMatch = line.match(/^(claude|opencode|cursor):\s*$/);
472
+ if (platformMatch) {
473
+ currentPlatform = platformMatch[1] as AgentPlatform;
474
+ continue;
475
+ }
476
+
477
+ if (currentPlatform) {
478
+ if (line.match(/^ {2}/)) {
479
+ platformLines[currentPlatform].push(line.slice(2)); // un-indent one level
480
+ continue;
481
+ }
482
+ currentPlatform = null; // end of platform block
483
+ }
484
+
485
+ globalLines.push(line);
490
486
  }
491
487
 
488
+ const newFrontmatter = [...globalLines, ...platformLines[platform]]
489
+ .filter((l) => l.trim())
490
+ .join("\n");
491
+
492
492
  return `---\n${newFrontmatter}\n---\n${body}`;
493
493
  }
494
494
 
495
- /**
496
- * Install PAL agent definitions into an opencode agents directory.
497
- * Translates frontmatter from Claude Code format to opencode format.
498
- */
499
- export function copyAgentsForOpencode(ocAgentsDir: string): number {
495
+ /** Install agents for a platform into a target directory. Always overwrites. */
496
+ function installAgents(targetDir: string, platform: AgentPlatform): number {
500
497
  const agentsDir = assets.agents();
501
498
  if (!existsSync(agentsDir)) return 0;
502
499
 
503
- mkdirSync(ocAgentsDir, { recursive: true });
500
+ mkdirSync(targetDir, { recursive: true });
504
501
  let count = 0;
505
502
 
506
503
  for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md"))) {
507
- const dst = resolve(ocAgentsDir, file);
508
- if (!existsSync(dst)) {
509
- const src = resolve(agentsDir, file);
510
- const content = readFileSync(src, "utf-8");
511
- writeFileSync(dst, translateAgentForOpencode(content), "utf-8");
512
- log.info(`Added opencode agent: ${file.replace(/\.md$/, "")}`);
513
- count++;
514
- } else {
515
- log.warn(`Opencode agent exists, skipping: ${file.replace(/\.md$/, "")}`);
516
- }
504
+ const content = readFileSync(resolve(agentsDir, file), "utf-8");
505
+ writeFileSync(
506
+ resolve(targetDir, file),
507
+ extractAgentForPlatform(content, platform),
508
+ "utf-8"
509
+ );
510
+ log.info(`Installed ${platform} agent: ${file.replace(/\.md$/, "")}`);
511
+ count++;
517
512
  }
518
513
  return count;
519
514
  }
520
515
 
521
- /** Remove PAL agents from an opencode agents directory */
522
- export function removeAgentsFromOpencode(ocAgentsDir: string): string[] {
516
+ /** Remove PAL agents from a directory. */
517
+ function uninstallAgents(targetDir: string, label: string): string[] {
523
518
  const agentsDir = assets.agents();
524
519
  if (!existsSync(agentsDir)) return [];
525
520
 
526
521
  const removed: string[] = [];
527
522
  for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md"))) {
528
- const dst = resolve(ocAgentsDir, file);
523
+ const dst = resolve(targetDir, file);
529
524
  if (existsSync(dst)) {
530
525
  unlinkSync(dst);
531
- const name = file.replace(/\.md$/, "");
532
- removed.push(name);
533
- log.info(`Removed opencode agent: ${name}`);
526
+ removed.push(file.replace(/\.md$/, ""));
527
+ log.info(`Removed ${label} agent: ${file.replace(/\.md$/, "")}`);
534
528
  }
535
529
  }
536
530
  return removed;
537
531
  }
538
532
 
533
+ export function copyAgentsForOpencode(ocAgentsDir: string): number {
534
+ return installAgents(ocAgentsDir, "opencode");
535
+ }
536
+
537
+ export function removeAgentsFromOpencode(ocAgentsDir: string): string[] {
538
+ return uninstallAgents(ocAgentsDir, "opencode");
539
+ }
540
+
541
+ export function copyAgentsForCursor(cursorAgentsDir: string): number {
542
+ return installAgents(cursorAgentsDir, "cursor");
543
+ }
544
+
545
+ export function removeAgentsFromCursor(cursorAgentsDir: string): string[] {
546
+ return uninstallAgents(cursorAgentsDir, "cursor");
547
+ }
548
+
539
549
  // --- Skill Index ---
540
550
 
541
551
  interface SkillIndexEntry {
@@ -580,11 +590,11 @@ function extractTriggers(description: string): string[] {
580
590
  }
581
591
 
582
592
  /**
583
- * Generate skill-index.json from installed skills in ~/.agents/skills/.
593
+ * Generate skill-index.json from installed skills in ~/.pal/skills/.
584
594
  * Called during install after skills are symlinked.
585
595
  */
586
596
  export function generateSkillIndex(): number {
587
- if (!existsSync(AGENTS_SKILLS_DIR)) return 0;
597
+ if (!existsSync(PAL_SKILLS_DIR)) return 0;
588
598
 
589
599
  const index: SkillIndex = {
590
600
  generated: new Date().toISOString(),
@@ -592,8 +602,8 @@ export function generateSkillIndex(): number {
592
602
  skills: {},
593
603
  };
594
604
 
595
- for (const name of readdirSync(AGENTS_SKILLS_DIR)) {
596
- const skillMd = resolve(AGENTS_SKILLS_DIR, name, "SKILL.md");
605
+ for (const name of readdirSync(PAL_SKILLS_DIR)) {
606
+ const skillMd = resolve(PAL_SKILLS_DIR, name, "SKILL.md");
597
607
  if (!existsSync(skillMd)) continue;
598
608
 
599
609
  try {
@@ -629,12 +639,12 @@ export function generateSkillIndex(): number {
629
639
  return index.totalSkills;
630
640
  }
631
641
 
632
- /** Count skill subdirectories in ~/.agents/skills/ */
642
+ /** Count skill subdirectories in ~/.pal/skills/ */
633
643
  export function countSkills(): number {
634
- if (!existsSync(AGENTS_SKILLS_DIR)) return 0;
644
+ if (!existsSync(PAL_SKILLS_DIR)) return 0;
635
645
  try {
636
- return readdirSync(AGENTS_SKILLS_DIR).filter((f) =>
637
- existsSync(resolve(AGENTS_SKILLS_DIR, f, "SKILL.md"))
646
+ return readdirSync(PAL_SKILLS_DIR).filter((f) =>
647
+ existsSync(resolve(PAL_SKILLS_DIR, f, "SKILL.md"))
638
648
  ).length;
639
649
  } catch {
640
650
  return 0;
@@ -52,11 +52,11 @@ try {
52
52
  log.warn(`Could not install plugin deps — run 'bun install' in ${OC_PLUGINS_DIR}`);
53
53
  }
54
54
 
55
- // --- 3. Install skills into ~/.agents/skills/ ---
55
+ // --- 3. Install skills into ~/.pal/skills/ ---
56
56
  const claudeSkillsDir = resolve(platform.claudeDir(), "skills");
57
57
  copySkills(claudeSkillsDir);
58
58
  generateSkillIndex();
59
- log.success("Installed skills to ~/.agents/skills/");
59
+ log.success("Installed skills to ~/.pal/skills/");
60
60
 
61
61
  // --- 4. Install agents into ~/.config/opencode/agents/ ---
62
62
  const ocAgentsDir = resolve(OC_GLOBAL_DIR, "agents");
@@ -64,7 +64,7 @@ copyAgentsForOpencode(ocAgentsDir);
64
64
 
65
65
  // --- 5. Copy PAL system docs ---
66
66
  const palDocsCount = copyPalDocs();
67
- log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
67
+ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
68
68
 
69
69
  // --- 6. Generate ~/.config/opencode/AGENTS.md ---
70
70
  regenerateIfNeeded();
@@ -73,4 +73,4 @@ log.success("Generated ~/.config/opencode/AGENTS.md");
73
73
  log.success("opencode installation complete");
74
74
  console.log("");
75
75
  log.info(`Plugin: ${pluginDst}`);
76
- log.info(`Skills: ${countSkills()} (native via ~/.agents/skills/)`);
76
+ log.info(`Skills: ${countSkills()} (native via ~/.pal/skills/)`);
@@ -21,6 +21,7 @@ import { paths } from "../../hooks/lib/paths";
21
21
 
22
22
  interface AlgorithmReflection {
23
23
  timestamp: string;
24
+ cwd: string;
24
25
  task: string;
25
26
  criteria_count: number;
26
27
  criteria_passed: number;
@@ -103,6 +104,7 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/
103
104
 
104
105
  const reflection: AlgorithmReflection = {
105
106
  timestamp: new Date().toISOString(),
107
+ cwd: process.cwd(),
106
108
  task: values.task,
107
109
  criteria_count: parseInt(values.criteria || "0", 10),
108
110
  criteria_passed: parseInt(values.passed || "0", 10),