portable-agent-layer 0.63.3 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/assets/schema/pal-settings.schema.json +4 -0
- package/assets/skills/analyze-pdf/SKILL.md +11 -0
- package/assets/skills/analyze-youtube/SKILL.md +12 -0
- package/assets/skills/consulting-report/SKILL.md +9 -0
- package/assets/skills/consulting-report/tools/generate-pdf.mjs +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +5 -2
- package/assets/skills/council/SKILL.md +32 -0
- package/assets/skills/create-pdf/SKILL.md +13 -0
- package/assets/skills/create-skill/SKILL.md +14 -2
- package/assets/skills/create-skill/authoring-guide.md +10 -1
- package/assets/skills/create-subagent/SKILL.md +22 -4
- package/assets/skills/{research → deep-research}/SKILL.md +32 -1
- package/assets/skills/entities/SKILL.md +10 -0
- package/assets/skills/extract-wisdom/SKILL.md +12 -0
- package/assets/skills/first-principles/SKILL.md +8 -0
- package/assets/skills/frontend-design/SKILL.md +14 -0
- package/assets/skills/fyzz-chat-api/SKILL.md +10 -0
- package/assets/skills/humanize/SKILL.md +13 -1
- package/assets/skills/opinion/SKILL.md +11 -0
- package/assets/skills/pal-analyze/SKILL.md +11 -0
- package/assets/skills/pal-reflect/SKILL.md +10 -0
- package/assets/skills/playwright/SKILL.md +15 -2
- package/assets/skills/playwright/tools/shot.ts +6 -7
- package/assets/skills/presentation/SKILL.md +12 -0
- package/assets/skills/projects/SKILL.md +20 -1
- package/assets/skills/reflect/SKILL.md +13 -0
- package/assets/skills/telos/SKILL.md +12 -0
- package/assets/skills/think/SKILL.md +9 -0
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +3 -0
- package/assets/templates/pal-settings.json +1 -0
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +15 -4
- package/src/cli/index.ts +95 -9
- package/src/cli/migrate.ts +69 -3
- package/src/cli/skill.ts +47 -3
- package/src/hooks/handlers/inject-retrieval.ts +20 -10
- package/src/hooks/lib/anchor.ts +90 -0
- package/src/hooks/lib/bindings.ts +117 -0
- package/src/hooks/lib/export.ts +38 -1
- package/src/hooks/lib/import-merge.ts +220 -0
- package/src/hooks/lib/inference.ts +113 -72
- package/src/hooks/lib/machine.ts +176 -0
- package/src/hooks/lib/projects.ts +223 -15
- package/src/hooks/lib/readme-sync.ts +30 -10
- package/src/hooks/lib/relationship.ts +3 -1
- package/src/hooks/lib/remote.ts +58 -0
- package/src/hooks/lib/retrieval.ts +8 -2
- package/src/hooks/lib/signals.ts +2 -1
- package/src/hooks/lib/skill-match.ts +129 -0
- package/src/hooks/lib/skill-triggers.ts +82 -0
- package/src/hooks/lib/stop.ts +5 -2
- package/src/targets/lib.ts +137 -35
- package/src/targets/opencode/plugin.ts +2 -6
- package/src/tools/agent/algorithm-reflect.ts +45 -11
- package/src/tools/agent/project.ts +148 -23
- package/src/tools/agent/thread.ts +7 -2
- package/src/tools/skill-doctor.ts +130 -5
- package/assets/skills/playwright/tools/shot-lib.mjs +0 -44
- package/assets/skills/playwright/tools/shot.mjs +0 -89
- package/assets/skills/review/SKILL.md +0 -20
- package/assets/skills/summarize/SKILL.md +0 -16
- /package/assets/skills/{research → deep-research}/tools/gemini-search.ts +0 -0
- /package/assets/skills/{research → deep-research}/tools/grok-search.ts +0 -0
- /package/assets/skills/{research → deep-research}/tools/perplexity-search.ts +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md trigger declarations — the words and phrases a prompt carries when it
|
|
3
|
+
* wants a given skill.
|
|
4
|
+
*
|
|
5
|
+
* `metadata` is the only frontmatter key Anthropic's skill spec reserves for
|
|
6
|
+
* third-party tooling, so triggers live under it; a top-level `triggers:` key
|
|
7
|
+
* fails skill packaging with an unexpected-key error. Shared by the skill-index
|
|
8
|
+
* generator (which publishes them) and the skill doctor (which warns when a
|
|
9
|
+
* skill declares none).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Indented lines belonging to the frontmatter `metadata:` map, or [] when absent. */
|
|
13
|
+
function metadataBlock(frontmatter: string): string[] {
|
|
14
|
+
const lines = frontmatter.split("\n");
|
|
15
|
+
const start = lines.findIndex((line) => /^metadata:\s*$/.test(line));
|
|
16
|
+
if (start === -1) return [];
|
|
17
|
+
|
|
18
|
+
const block: string[] = [];
|
|
19
|
+
for (const line of lines.slice(start + 1)) {
|
|
20
|
+
if (line.trim() === "") continue;
|
|
21
|
+
if (!/^\s/.test(line)) break;
|
|
22
|
+
block.push(line);
|
|
23
|
+
}
|
|
24
|
+
return block;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Normalize one authored trigger: unquote, collapse whitespace, lowercase. */
|
|
28
|
+
function normalizeTrigger(raw: string): string {
|
|
29
|
+
return raw
|
|
30
|
+
.trim()
|
|
31
|
+
.replace(/^["']|["']$/g, "")
|
|
32
|
+
.replace(/\s+/g, " ")
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Split a YAML flow sequence — `["a", "b"]` or `[a, b]` — into its items. */
|
|
38
|
+
function splitFlowSequence(value: string): string[] {
|
|
39
|
+
try {
|
|
40
|
+
const parsed: unknown = JSON.parse(value);
|
|
41
|
+
if (Array.isArray(parsed)) return parsed.map((item) => String(item));
|
|
42
|
+
} catch {
|
|
43
|
+
/* not strict JSON — fall through to the permissive split */
|
|
44
|
+
}
|
|
45
|
+
return value.slice(1, -1).split(",");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Items of a YAML block sequence: the `- item` lines directly under a key. */
|
|
49
|
+
function blockSequenceItems(lines: string[]): string[] {
|
|
50
|
+
const items: string[] = [];
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const item = /^\s*-\s+(.*)$/.exec(line);
|
|
53
|
+
if (!item) break;
|
|
54
|
+
items.push(item[1]);
|
|
55
|
+
}
|
|
56
|
+
return items;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Author-declared `metadata.triggers` — the words and phrases a prompt is matched
|
|
61
|
+
* against. Accepts either YAML shape:
|
|
62
|
+
*
|
|
63
|
+
* metadata: | metadata:
|
|
64
|
+
* triggers: | triggers: ["make a deck", "slides"]
|
|
65
|
+
* - make a deck |
|
|
66
|
+
* - slides |
|
|
67
|
+
*
|
|
68
|
+
* `metadata` is the only frontmatter key Anthropic's skill spec reserves for
|
|
69
|
+
* third-party tooling; a top-level `triggers:` key fails skill packaging.
|
|
70
|
+
*/
|
|
71
|
+
export function declaredTriggers(frontmatter: string): string[] {
|
|
72
|
+
const block = metadataBlock(frontmatter);
|
|
73
|
+
const at = block.findIndex((line) => /^\s*triggers:/.test(line));
|
|
74
|
+
if (at === -1) return [];
|
|
75
|
+
|
|
76
|
+
const inline = /^\s*triggers:\s*(\S.*)$/.exec(block[at])?.[1];
|
|
77
|
+
const raw = inline
|
|
78
|
+
? splitFlowSequence(inline)
|
|
79
|
+
: blockSequenceItems(block.slice(at + 1));
|
|
80
|
+
|
|
81
|
+
return [...new Set(raw.map(normalizeTrigger).filter(Boolean))];
|
|
82
|
+
}
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Used by StopOrchestrator.ts (Claude Code) and opencode plugin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { mkdtemp, rename, writeFile } from "node:fs/promises";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
@@ -189,8 +190,10 @@ async function detachFailurePrinciple(transcript: string): Promise<void> {
|
|
|
189
190
|
// Rename to claim the pending file atomically — prevents two Stop hooks
|
|
190
191
|
// racing on the same low rating (opencode notably fires session.idle AND
|
|
191
192
|
// session.diff concurrently, so runStopHandlers runs twice in parallel).
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
// The claim stays inside the state directory: rename fails with EXDEV across
|
|
194
|
+
// devices, and the OS temp dir is on another volume often enough to matter.
|
|
195
|
+
const claimId: string = randomUUID();
|
|
196
|
+
const claimedPath = resolve(paths.state(), `pending-failure.${claimId}.json`);
|
|
194
197
|
try {
|
|
195
198
|
await rename(pendingPath, claimedPath);
|
|
196
199
|
} catch (err) {
|
package/src/targets/lib.ts
CHANGED
|
@@ -10,14 +10,16 @@ import {
|
|
|
10
10
|
mkdirSync,
|
|
11
11
|
readdirSync,
|
|
12
12
|
readFileSync,
|
|
13
|
+
readlinkSync,
|
|
13
14
|
rmSync,
|
|
14
15
|
symlinkSync,
|
|
15
16
|
unlinkSync,
|
|
16
17
|
writeFileSync,
|
|
17
18
|
} from "node:fs";
|
|
18
19
|
import { homedir } from "node:os";
|
|
19
|
-
import { resolve } from "node:path";
|
|
20
|
+
import { dirname, resolve, sep } from "node:path";
|
|
20
21
|
import { assets, palHome, platform } from "../hooks/lib/paths";
|
|
22
|
+
import { declaredTriggers } from "../hooks/lib/skill-triggers";
|
|
21
23
|
|
|
22
24
|
// --- Colored logging ---
|
|
23
25
|
|
|
@@ -48,14 +50,20 @@ export function writeJson(path: string, data: unknown): void {
|
|
|
48
50
|
/** Public PAL repository — the link surfaced in commit/PR co-author credits. */
|
|
49
51
|
export const PAL_REPO_URL = "https://github.com/kovrichard/portable-agent-layer";
|
|
50
52
|
|
|
51
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Build the commit footer (bare URL, autolinks on GitHub) and PR body line
|
|
55
|
+
* (markdown link). `sessionUrl` is off because Claude Code otherwise appends a
|
|
56
|
+
* claude.ai session link to commits made from web or Remote Control sessions,
|
|
57
|
+
* which puts a link to a private transcript in a public history.
|
|
58
|
+
*/
|
|
52
59
|
export function buildAttributionText(
|
|
53
60
|
name: string,
|
|
54
61
|
repoUrl: string = PAL_REPO_URL
|
|
55
|
-
): { commit: string; pr: string } {
|
|
62
|
+
): { commit: string; pr: string; sessionUrl: false } {
|
|
56
63
|
return {
|
|
57
64
|
commit: `Co-authored by ${name} · ${repoUrl}`,
|
|
58
65
|
pr: `Co-authored by [${name}](${repoUrl})`,
|
|
66
|
+
sessionUrl: false,
|
|
59
67
|
};
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -73,7 +81,7 @@ export function applyAttribution(
|
|
|
73
81
|
result.attribution = buildAttributionText(opts.name, opts.repoUrl);
|
|
74
82
|
result.includeCoAuthoredBy = false;
|
|
75
83
|
} else {
|
|
76
|
-
result.attribution = { commit: "", pr: "" };
|
|
84
|
+
result.attribution = { commit: "", pr: "", sessionUrl: false };
|
|
77
85
|
if (result.includeCoAuthoredBy === false) delete result.includeCoAuthoredBy;
|
|
78
86
|
}
|
|
79
87
|
return result;
|
|
@@ -691,8 +699,8 @@ export function scaffoldPalSettings(): void {
|
|
|
691
699
|
|
|
692
700
|
// --- PAL docs (modular context routing files) ---
|
|
693
701
|
|
|
694
|
-
const
|
|
695
|
-
const
|
|
702
|
+
const palDocsDir = () => resolve(palHome(), "docs");
|
|
703
|
+
const palToolsDir = () => resolve(palHome(), "tools");
|
|
696
704
|
|
|
697
705
|
/**
|
|
698
706
|
* Install PAL system docs into ~/.pal/docs/.
|
|
@@ -703,19 +711,19 @@ export function copyPalDocs(): number {
|
|
|
703
711
|
const srcDir = assets.palDocs();
|
|
704
712
|
if (!existsSync(srcDir)) return 0;
|
|
705
713
|
|
|
706
|
-
mkdirSync(
|
|
714
|
+
mkdirSync(palDocsDir(), { recursive: true });
|
|
707
715
|
let count = 0;
|
|
708
716
|
|
|
709
717
|
for (const file of readdirSync(srcDir).filter((f) => f.endsWith(".md"))) {
|
|
710
718
|
const src = resolve(srcDir, file);
|
|
711
|
-
const dst = resolve(
|
|
719
|
+
const dst = resolve(palDocsDir(), file);
|
|
712
720
|
copyFileSync(src, dst);
|
|
713
721
|
count++;
|
|
714
722
|
}
|
|
715
723
|
|
|
716
724
|
// ~/.pal/tools/ → repo agent tools
|
|
717
725
|
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
718
|
-
ensureSymlink(
|
|
726
|
+
ensureSymlink(palToolsDir(), assets.agentTools(), linkType);
|
|
719
727
|
|
|
720
728
|
return count;
|
|
721
729
|
}
|
|
@@ -724,13 +732,13 @@ export function copyPalDocs(): number {
|
|
|
724
732
|
export function removePalDocs(): void {
|
|
725
733
|
// Remove tools symlink
|
|
726
734
|
try {
|
|
727
|
-
unlinkSync(
|
|
735
|
+
unlinkSync(palToolsDir());
|
|
728
736
|
} catch {
|
|
729
737
|
/* gone */
|
|
730
738
|
}
|
|
731
|
-
if (!existsSync(
|
|
739
|
+
if (!existsSync(palDocsDir())) return;
|
|
732
740
|
try {
|
|
733
|
-
rmSync(
|
|
741
|
+
rmSync(palDocsDir(), { recursive: true });
|
|
734
742
|
log.info("Removed ~/.pal/docs/");
|
|
735
743
|
} catch {
|
|
736
744
|
/* gone */
|
|
@@ -739,7 +747,7 @@ export function removePalDocs(): void {
|
|
|
739
747
|
|
|
740
748
|
// --- Skills ---
|
|
741
749
|
|
|
742
|
-
const
|
|
750
|
+
const palSkillsDir = () => resolve(palHome(), "skills");
|
|
743
751
|
|
|
744
752
|
/**
|
|
745
753
|
* Run one step of a bulk install, naming it only when it fails.
|
|
@@ -770,16 +778,20 @@ export function copySkills(claudeSkillsDir: string): number {
|
|
|
770
778
|
const skillsDir = assets.skills();
|
|
771
779
|
if (!existsSync(skillsDir)) return 0;
|
|
772
780
|
|
|
773
|
-
mkdirSync(
|
|
781
|
+
mkdirSync(palSkillsDir(), { recursive: true });
|
|
774
782
|
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
775
783
|
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
776
784
|
let count = 0;
|
|
777
785
|
|
|
786
|
+
for (const name of pruneStaleSkillLinks(claudeSkillsDir)) {
|
|
787
|
+
log.info(`Removed stale skill link: ${name}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
778
790
|
for (const name of readdirSync(skillsDir)) {
|
|
779
791
|
const srcDir = resolve(skillsDir, name);
|
|
780
792
|
if (!existsSync(resolve(srcDir, "SKILL.md"))) continue;
|
|
781
793
|
|
|
782
|
-
const palLink = resolve(
|
|
794
|
+
const palLink = resolve(palSkillsDir(), name);
|
|
783
795
|
const claudeLink = resolve(claudeSkillsDir, name);
|
|
784
796
|
const linked = reportOnlyOnFailure(`skill ${name}`, () => {
|
|
785
797
|
// ~/.pal/skills/<name> → <repo>/assets/skills/<name>
|
|
@@ -792,11 +804,60 @@ export function copySkills(claudeSkillsDir: string): number {
|
|
|
792
804
|
|
|
793
805
|
// ~/.agents/skills/ → ~/.pal/skills/
|
|
794
806
|
mkdirSync(platform.agentsDir(), { recursive: true });
|
|
795
|
-
ensureSymlink(resolve(platform.agentsDir(), "skills"),
|
|
807
|
+
ensureSymlink(resolve(platform.agentsDir(), "skills"), palSkillsDir(), linkType);
|
|
796
808
|
|
|
797
809
|
return count;
|
|
798
810
|
}
|
|
799
811
|
|
|
812
|
+
/** True when `link` is a symlink whose target no longer exists. */
|
|
813
|
+
function isDanglingSymlink(link: string): boolean {
|
|
814
|
+
try {
|
|
815
|
+
return lstatSync(link).isSymbolicLink() && !existsSync(link);
|
|
816
|
+
} catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** True when the symlink at `link` points at `root` or somewhere beneath it. */
|
|
822
|
+
function symlinkPointsInto(link: string, root: string): boolean {
|
|
823
|
+
try {
|
|
824
|
+
const target = resolve(dirname(link), readlinkSync(link));
|
|
825
|
+
return target === root || target.startsWith(root + sep);
|
|
826
|
+
} catch {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Remove discovery links left behind when a shipped skill is renamed or
|
|
833
|
+
* retired. Ownership is read from where a link points, not from metadata:
|
|
834
|
+
* a dead link has no SKILL.md to read, but its target path still says
|
|
835
|
+
* whether PAL created it.
|
|
836
|
+
*
|
|
837
|
+
* ~/.pal/skills/<name> → pruned when dangling and pointing into assets/skills/
|
|
838
|
+
* <agent>/skills/<name> → pruned when dangling and pointing into ~/.pal/skills/
|
|
839
|
+
*
|
|
840
|
+
* Personal skills are real directories, so they are never candidates, and a
|
|
841
|
+
* user's own symlinks to anywhere else are left alone even when broken.
|
|
842
|
+
*/
|
|
843
|
+
function pruneStaleSkillLinks(agentSkillsDir: string): string[] {
|
|
844
|
+
const ownedTrees = [
|
|
845
|
+
{ dir: palSkillsDir(), root: assets.skills() },
|
|
846
|
+
{ dir: agentSkillsDir, root: palSkillsDir() },
|
|
847
|
+
];
|
|
848
|
+
const removed: string[] = [];
|
|
849
|
+
for (const { dir, root } of ownedTrees) {
|
|
850
|
+
if (!existsSync(dir)) continue;
|
|
851
|
+
for (const name of readdirSync(dir)) {
|
|
852
|
+
const link = resolve(dir, name);
|
|
853
|
+
if (!isDanglingSymlink(link) || !symlinkPointsInto(link, root)) continue;
|
|
854
|
+
unlinkSync(link);
|
|
855
|
+
removed.push(name);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return removed;
|
|
859
|
+
}
|
|
860
|
+
|
|
800
861
|
/**
|
|
801
862
|
* Agent skills directories that need a per-skill discovery link.
|
|
802
863
|
*
|
|
@@ -823,7 +884,7 @@ function perSkillAgentDirs(): { agent: string; dir: string }[] {
|
|
|
823
884
|
* is covered by the whole-dir ~/.agents/skills link).
|
|
824
885
|
*/
|
|
825
886
|
export function linkPersonalSkill(name: string): string[] {
|
|
826
|
-
const palLink = resolve(
|
|
887
|
+
const palLink = resolve(palSkillsDir(), name);
|
|
827
888
|
if (!existsSync(resolve(palLink, "SKILL.md"))) {
|
|
828
889
|
throw new Error(`No skill found at ${palLink}/SKILL.md`);
|
|
829
890
|
}
|
|
@@ -837,8 +898,47 @@ export function linkPersonalSkill(name: string): string[] {
|
|
|
837
898
|
return linked;
|
|
838
899
|
}
|
|
839
900
|
|
|
901
|
+
/**
|
|
902
|
+
* The agent config trees PAL writes into when no env override is set. These are
|
|
903
|
+
* the developer's own installed agents, so a test that forgets to point the
|
|
904
|
+
* PAL_*_DIR vars at a sandbox silently rewires their real setup.
|
|
905
|
+
*/
|
|
906
|
+
function realAgentRoots(): string[] {
|
|
907
|
+
const h = homedir();
|
|
908
|
+
return [
|
|
909
|
+
resolve(h, ".pal"),
|
|
910
|
+
resolve(h, ".claude"),
|
|
911
|
+
resolve(h, ".cursor"),
|
|
912
|
+
resolve(h, ".copilot"),
|
|
913
|
+
resolve(h, ".codex"),
|
|
914
|
+
resolve(h, ".agents"),
|
|
915
|
+
resolve(h, ".config", "opencode"),
|
|
916
|
+
];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Under `bun test` (PAL_TEST_SANDBOX, set by the test preload and inherited by
|
|
921
|
+
* spawned CLIs), refuse any link that would land in a real agent tree. Tests
|
|
922
|
+
* sandbox PAL_HOME far more reliably than they sandbox the per-agent dirs, and
|
|
923
|
+
* the failure is otherwise invisible: the suite passes while the developer's
|
|
924
|
+
* own agents accumulate links into a deleted test directory.
|
|
925
|
+
*/
|
|
926
|
+
function assertInsideTestSandbox(link: string): void {
|
|
927
|
+
if (!process.env.PAL_TEST_SANDBOX) return;
|
|
928
|
+
const escaped = realAgentRoots().find(
|
|
929
|
+
(root) => link === root || link.startsWith(root + sep)
|
|
930
|
+
);
|
|
931
|
+
if (!escaped) return;
|
|
932
|
+
throw new Error(
|
|
933
|
+
`Refusing to write ${link}: outside the test sandbox (${escaped} is a real agent directory). ` +
|
|
934
|
+
"Point PAL_CLAUDE_DIR, PAL_CURSOR_DIR, PAL_COPILOT_DIR, PAL_CODEX_DIR, " +
|
|
935
|
+
"PAL_OPENCODE_DIR and PAL_AGENTS_DIR at a temp directory in this test."
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
840
939
|
/** Create or update a symlink/junction, replacing any non-symlink entry. */
|
|
841
940
|
function ensureSymlink(link: string, target: string, type: "dir" | "junction"): void {
|
|
941
|
+
assertInsideTestSandbox(link);
|
|
842
942
|
try {
|
|
843
943
|
const st = lstatSync(link);
|
|
844
944
|
if (st.isSymbolicLink()) return; // already a symlink, leave it
|
|
@@ -886,7 +986,7 @@ export function removeSkills(claudeSkillsDir: string): string[] {
|
|
|
886
986
|
for (const name of readdirSync(skillsDir)) {
|
|
887
987
|
if (!existsSync(resolve(skillsDir, name, "SKILL.md"))) continue;
|
|
888
988
|
|
|
889
|
-
for (const link of [resolve(
|
|
989
|
+
for (const link of [resolve(palSkillsDir(), name), resolve(claudeSkillsDir, name)]) {
|
|
890
990
|
try {
|
|
891
991
|
unlinkSync(link);
|
|
892
992
|
} catch {
|
|
@@ -909,14 +1009,14 @@ export function removeSkills(claudeSkillsDir: string): string[] {
|
|
|
909
1009
|
|
|
910
1010
|
// --- Agents ---
|
|
911
1011
|
|
|
912
|
-
const
|
|
1012
|
+
const claudeAgentsDir = () => resolve(platform.claudeDir(), "agents");
|
|
913
1013
|
|
|
914
1014
|
/**
|
|
915
1015
|
* Install PAL agent definitions into ~/.claude/agents/.
|
|
916
1016
|
* Always overwrites — engine-managed, not user-editable.
|
|
917
1017
|
*/
|
|
918
1018
|
export function copyAgents(): number {
|
|
919
|
-
return installAgents(
|
|
1019
|
+
return installAgents(claudeAgentsDir(), "claude");
|
|
920
1020
|
}
|
|
921
1021
|
|
|
922
1022
|
/** Remove PAL agents from ~/.claude/agents/ */
|
|
@@ -926,7 +1026,7 @@ export function removeAgents(): string[] {
|
|
|
926
1026
|
|
|
927
1027
|
const removed: string[] = [];
|
|
928
1028
|
for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md"))) {
|
|
929
|
-
const dst = resolve(
|
|
1029
|
+
const dst = resolve(claudeAgentsDir(), file);
|
|
930
1030
|
if (existsSync(dst)) {
|
|
931
1031
|
unlinkSync(dst);
|
|
932
1032
|
const name = file.replace(/\.md$/, "");
|
|
@@ -939,9 +1039,9 @@ export function removeAgents(): string[] {
|
|
|
939
1039
|
|
|
940
1040
|
/** Count agent .md files in ~/.claude/agents/ */
|
|
941
1041
|
export function countAgents(): number {
|
|
942
|
-
if (!existsSync(
|
|
1042
|
+
if (!existsSync(claudeAgentsDir())) return 0;
|
|
943
1043
|
try {
|
|
944
|
-
return readdirSync(
|
|
1044
|
+
return readdirSync(claudeAgentsDir()).filter((f) => f.endsWith(".md")).length;
|
|
945
1045
|
} catch {
|
|
946
1046
|
return 0;
|
|
947
1047
|
}
|
|
@@ -1077,7 +1177,7 @@ export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] {
|
|
|
1077
1177
|
* Store for user-authored subagents: ~/.pal/agents/<name>.md — one merged
|
|
1078
1178
|
* multi-platform frontmatter file per subagent (same schema as assets/agents/).
|
|
1079
1179
|
*/
|
|
1080
|
-
const
|
|
1180
|
+
const palAgentsStore = () => resolve(palHome(), "agents");
|
|
1081
1181
|
|
|
1082
1182
|
/**
|
|
1083
1183
|
* Each installed agent and the native agents directory a personal subagent is
|
|
@@ -1106,8 +1206,8 @@ function shippedAgentNames(): Set<string> {
|
|
|
1106
1206
|
|
|
1107
1207
|
/** List the user-authored subagents in ~/.pal/agents/. */
|
|
1108
1208
|
export function listPersonalSubagents(): string[] {
|
|
1109
|
-
if (!existsSync(
|
|
1110
|
-
return readdirSync(
|
|
1209
|
+
if (!existsSync(palAgentsStore())) return [];
|
|
1210
|
+
return readdirSync(palAgentsStore())
|
|
1111
1211
|
.filter((f) => f.endsWith(".md"))
|
|
1112
1212
|
.map((f) => f.replace(/\.md$/, ""))
|
|
1113
1213
|
.sort();
|
|
@@ -1121,7 +1221,7 @@ export function listPersonalSubagents(): string[] {
|
|
|
1121
1221
|
* its own frontmatter shape. Returns the agents it was installed into.
|
|
1122
1222
|
*/
|
|
1123
1223
|
export function installPersonalSubagent(name: string): string[] {
|
|
1124
|
-
const src = resolve(
|
|
1224
|
+
const src = resolve(palAgentsStore(), `${name}.md`);
|
|
1125
1225
|
if (!existsSync(src)) {
|
|
1126
1226
|
throw new Error(`No subagent found at ${src}`);
|
|
1127
1227
|
}
|
|
@@ -1319,7 +1419,7 @@ interface SkillIndex {
|
|
|
1319
1419
|
skills: Record<string, SkillIndexEntry>;
|
|
1320
1420
|
}
|
|
1321
1421
|
|
|
1322
|
-
/**
|
|
1422
|
+
/** Fallback triggers for a skill that declares none: keywords mined from its description. */
|
|
1323
1423
|
function extractTriggers(description: string): string[] {
|
|
1324
1424
|
// Extract "Use when ..." phrases and key terms
|
|
1325
1425
|
const triggers = new Set<string>();
|
|
@@ -1353,7 +1453,7 @@ function extractTriggers(description: string): string[] {
|
|
|
1353
1453
|
* Called during install after skills are symlinked.
|
|
1354
1454
|
*/
|
|
1355
1455
|
export function generateSkillIndex(): number {
|
|
1356
|
-
if (!existsSync(
|
|
1456
|
+
if (!existsSync(palSkillsDir())) return 0;
|
|
1357
1457
|
|
|
1358
1458
|
const index: SkillIndex = {
|
|
1359
1459
|
generated: new Date().toISOString(),
|
|
@@ -1361,8 +1461,8 @@ export function generateSkillIndex(): number {
|
|
|
1361
1461
|
skills: {},
|
|
1362
1462
|
};
|
|
1363
1463
|
|
|
1364
|
-
for (const name of readdirSync(
|
|
1365
|
-
const skillMd = resolve(
|
|
1464
|
+
for (const name of readdirSync(palSkillsDir())) {
|
|
1465
|
+
const skillMd = resolve(palSkillsDir(), name, "SKILL.md");
|
|
1366
1466
|
if (!existsSync(skillMd)) continue;
|
|
1367
1467
|
|
|
1368
1468
|
try {
|
|
@@ -1378,10 +1478,12 @@ export function generateSkillIndex(): number {
|
|
|
1378
1478
|
const skillName = nameMatch[1].trim();
|
|
1379
1479
|
const description = descMatch?.[1]?.trim() ?? "";
|
|
1380
1480
|
|
|
1481
|
+
const declared = declaredTriggers(fm);
|
|
1482
|
+
|
|
1381
1483
|
index.skills[skillName] = {
|
|
1382
1484
|
name: skillName,
|
|
1383
1485
|
description,
|
|
1384
|
-
triggers: extractTriggers(description),
|
|
1486
|
+
triggers: declared.length > 0 ? declared : extractTriggers(description),
|
|
1385
1487
|
};
|
|
1386
1488
|
index.totalSkills++;
|
|
1387
1489
|
} catch {
|
|
@@ -1399,10 +1501,10 @@ export function generateSkillIndex(): number {
|
|
|
1399
1501
|
|
|
1400
1502
|
/** Count skill subdirectories in ~/.pal/skills/ */
|
|
1401
1503
|
export function countSkills(): number {
|
|
1402
|
-
if (!existsSync(
|
|
1504
|
+
if (!existsSync(palSkillsDir())) return 0;
|
|
1403
1505
|
try {
|
|
1404
|
-
return readdirSync(
|
|
1405
|
-
existsSync(resolve(
|
|
1506
|
+
return readdirSync(palSkillsDir()).filter((f) =>
|
|
1507
|
+
existsSync(resolve(palSkillsDir(), f, "SKILL.md"))
|
|
1406
1508
|
).length;
|
|
1407
1509
|
} catch {
|
|
1408
1510
|
return 0;
|
|
@@ -63,11 +63,9 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
63
63
|
const { captureRating } = await lib<typeof import("../../hooks/handlers/rating")>(
|
|
64
64
|
"../handlers/rating.ts"
|
|
65
65
|
);
|
|
66
|
-
const {
|
|
66
|
+
const { getPromptContext } = await lib<
|
|
67
67
|
typeof import("../../hooks/handlers/inject-retrieval")
|
|
68
68
|
>("../handlers/inject-retrieval.ts");
|
|
69
|
-
const { getSteeringReminder } =
|
|
70
|
-
await lib<typeof import("../../hooks/lib/steering")>("steering.ts");
|
|
71
69
|
|
|
72
70
|
function partsToText(parts: Array<Record<string, unknown>>): string {
|
|
73
71
|
return parts
|
|
@@ -163,9 +161,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
163
161
|
const text = partsToText(output.parts ?? []);
|
|
164
162
|
if (!text.trim()) return;
|
|
165
163
|
|
|
166
|
-
const
|
|
167
|
-
const steering = getSteeringReminder(text);
|
|
168
|
-
const injectedText = [steering, retrieval].filter(Boolean).join("\n\n");
|
|
164
|
+
const injectedText = (await getPromptContext(text)) ?? "";
|
|
169
165
|
logPromptSnapshot(text, injectedText || null);
|
|
170
166
|
|
|
171
167
|
await Promise.allSettled([
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* AlgorithmReflect — Append structured algorithm reflections to JSONL.
|
|
4
5
|
*
|
|
@@ -14,6 +15,8 @@
|
|
|
14
15
|
|
|
15
16
|
import { appendFileSync } from "node:fs";
|
|
16
17
|
import { parseArgs } from "node:util";
|
|
18
|
+
import { encodeAnchor } from "../../hooks/lib/anchor";
|
|
19
|
+
import { loadMachine } from "../../hooks/lib/machine";
|
|
17
20
|
import { paths } from "../../hooks/lib/paths";
|
|
18
21
|
import { emit } from "../lib/emit";
|
|
19
22
|
|
|
@@ -22,6 +25,7 @@ import { emit } from "../lib/emit";
|
|
|
22
25
|
interface AlgorithmReflection {
|
|
23
26
|
timestamp: string;
|
|
24
27
|
cwd: string;
|
|
28
|
+
m: string;
|
|
25
29
|
task: string;
|
|
26
30
|
criteria_count: number;
|
|
27
31
|
criteria_passed: number;
|
|
@@ -41,6 +45,40 @@ function reflectionsPath(): string {
|
|
|
41
45
|
return paths.reflectionsFile();
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Assemble a reflection record from CLI-style input, stamping the current
|
|
50
|
+
* cwd (anchored) and this machine's id. Exported so the stamping logic is
|
|
51
|
+
* directly testable without going through argv parsing.
|
|
52
|
+
*/
|
|
53
|
+
export function buildReflection(input: {
|
|
54
|
+
task: string;
|
|
55
|
+
q1: string;
|
|
56
|
+
q2: string;
|
|
57
|
+
q3: string;
|
|
58
|
+
criteria_count?: number;
|
|
59
|
+
criteria_passed?: number;
|
|
60
|
+
criteria_failed?: number;
|
|
61
|
+
sentiment?: number;
|
|
62
|
+
scope?: string;
|
|
63
|
+
}): AlgorithmReflection {
|
|
64
|
+
return {
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
cwd: encodeAnchor(process.cwd()),
|
|
67
|
+
m: loadMachine().id,
|
|
68
|
+
task: input.task,
|
|
69
|
+
criteria_count: input.criteria_count ?? 0,
|
|
70
|
+
criteria_passed: input.criteria_passed ?? 0,
|
|
71
|
+
criteria_failed: input.criteria_failed ?? 0,
|
|
72
|
+
sentiment: Math.max(1, Math.min(10, input.sentiment ?? 5)),
|
|
73
|
+
q1: input.q1,
|
|
74
|
+
q2: input.q2,
|
|
75
|
+
q3: input.q3,
|
|
76
|
+
// Default to general (the ~94% case); only "task-specific" suppresses it
|
|
77
|
+
// from algorithm-update clustering.
|
|
78
|
+
scope: input.scope === "task-specific" ? "task-specific" : "general",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
function appendReflection(reflection: AlgorithmReflection): {
|
|
45
83
|
success: boolean;
|
|
46
84
|
message: string;
|
|
@@ -105,21 +143,17 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/
|
|
|
105
143
|
process.exit(1);
|
|
106
144
|
}
|
|
107
145
|
|
|
108
|
-
const reflection
|
|
109
|
-
timestamp: new Date().toISOString(),
|
|
110
|
-
cwd: process.cwd(),
|
|
146
|
+
const reflection = buildReflection({
|
|
111
147
|
task: values.task,
|
|
112
|
-
criteria_count: parseInt(values.criteria || "0", 10),
|
|
113
|
-
criteria_passed: parseInt(values.passed || "0", 10),
|
|
114
|
-
criteria_failed: parseInt(values.failed || "0", 10),
|
|
115
|
-
sentiment: Math.max(1, Math.min(10, parseInt(values.sentiment || "5", 10))),
|
|
116
148
|
q1: values.q1,
|
|
117
149
|
q2: values.q2,
|
|
118
150
|
q3: values.q3,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
criteria_count: parseInt(values.criteria || "0", 10),
|
|
152
|
+
criteria_passed: parseInt(values.passed || "0", 10),
|
|
153
|
+
criteria_failed: parseInt(values.failed || "0", 10),
|
|
154
|
+
sentiment: parseInt(values.sentiment || "5", 10),
|
|
155
|
+
scope: values.scope,
|
|
156
|
+
});
|
|
123
157
|
|
|
124
158
|
const result = appendReflection(reflection);
|
|
125
159
|
emit.ok(result.message);
|