portable-agent-layer 0.63.2 → 0.64.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 +7 -3
- 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/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 +13 -0
- package/assets/skills/presentation/SKILL.md +12 -0
- package/assets/skills/projects/SKILL.md +16 -0
- 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/package.json +1 -1
- package/src/cli/index.ts +2 -2
- package/src/cli/skill.ts +47 -3
- package/src/hooks/handlers/inject-retrieval.ts +41 -27
- package/src/hooks/lib/readme-sync.ts +30 -10
- package/src/hooks/lib/skill-match.ts +129 -0
- package/src/hooks/lib/skill-triggers.ts +82 -0
- package/src/targets/lib.ts +60 -3
- package/src/targets/opencode/plugin.ts +2 -6
- package/src/tools/skill-doctor.ts +130 -5
- 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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
13
13
|
import { basename, extname, relative, resolve } from "node:path";
|
|
14
14
|
import { palHome } from "../hooks/lib/paths";
|
|
15
|
+
import { declaredTriggers } from "../hooks/lib/skill-triggers";
|
|
15
16
|
|
|
16
17
|
type Level = "pass" | "warn" | "error";
|
|
17
18
|
|
|
@@ -33,13 +34,20 @@ interface ParsedSkill {
|
|
|
33
34
|
name: string | null;
|
|
34
35
|
description: string | null;
|
|
35
36
|
descriptionQuoted: boolean;
|
|
37
|
+
triggers: string[];
|
|
38
|
+
shipped: boolean;
|
|
39
|
+
license: string | null;
|
|
40
|
+
derivedFrom: string | null;
|
|
36
41
|
body: string;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
const SHIPPED_SOURCE = "portable-agent-layer";
|
|
45
|
+
|
|
39
46
|
const RESERVED_WORDS = ["anthropic", "claude"];
|
|
40
47
|
const MAX_NAME = 64;
|
|
41
48
|
const MAX_DESCRIPTION = 1024;
|
|
42
49
|
const MAX_BODY_LINES = 500;
|
|
50
|
+
const MIN_TRIGGERS = 3;
|
|
43
51
|
|
|
44
52
|
/** File extensions worth scanning for hardcoded paths (SKILL.md + its scripts). */
|
|
45
53
|
const SCANNABLE_EXT = new Set([".md", ".ts", ".js", ".mjs", ".cjs", ".sh", ".py"]);
|
|
@@ -83,7 +91,16 @@ function findAbsolutePaths(skillDir: string): string[] {
|
|
|
83
91
|
function parseSkill(content: string): ParsedSkill {
|
|
84
92
|
const parts = content.split(/^---\s*$/m);
|
|
85
93
|
if (parts.length < 3) {
|
|
86
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
name: null,
|
|
96
|
+
description: null,
|
|
97
|
+
descriptionQuoted: false,
|
|
98
|
+
triggers: [],
|
|
99
|
+
shipped: false,
|
|
100
|
+
license: null,
|
|
101
|
+
derivedFrom: null,
|
|
102
|
+
body: content,
|
|
103
|
+
};
|
|
87
104
|
}
|
|
88
105
|
const frontmatter = parts[1];
|
|
89
106
|
const body = parts.slice(2).join("---");
|
|
@@ -95,7 +112,46 @@ function parseSkill(content: string): ParsedSkill {
|
|
|
95
112
|
rawDescription.startsWith('"') &&
|
|
96
113
|
rawDescription.endsWith('"');
|
|
97
114
|
const description = descriptionQuoted ? rawDescription.slice(1, -1) : rawDescription;
|
|
98
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
description,
|
|
118
|
+
descriptionQuoted,
|
|
119
|
+
triggers: declaredTriggers(frontmatter),
|
|
120
|
+
shipped: metadataField(frontmatter, "source") === SHIPPED_SOURCE,
|
|
121
|
+
license: topLevelField(frontmatter, "license"),
|
|
122
|
+
derivedFrom: metadataField(frontmatter, "derived-from"),
|
|
123
|
+
body,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Value of a top-level `key:` line in the frontmatter, unquoted. */
|
|
128
|
+
function topLevelField(frontmatter: string, key: string): string | null {
|
|
129
|
+
return (
|
|
130
|
+
new RegExp(String.raw`^${key}:\s*"?(.+?)"?\s*$`, "m").exec(frontmatter)?.[1] ?? null
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Value of an indented `key:` line under the `metadata:` block, unquoted. */
|
|
135
|
+
function metadataField(frontmatter: string, key: string): string | null {
|
|
136
|
+
return (
|
|
137
|
+
new RegExp(String.raw`^[ \t]+${key}:\s*"?(.+?)"?\s*$`, "m").exec(frontmatter)?.[1] ??
|
|
138
|
+
null
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Render triggers for a report line: `"a", "b"` or `"a" then "b"`. */
|
|
143
|
+
function quoteList(triggers: string[], separator: string): string {
|
|
144
|
+
return triggers.map((trigger) => `"${trigger}"`).join(separator);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The triggers every skill must declare first: its own name, then the
|
|
149
|
+
* de-hyphenated form a user would actually type. A single-word name has only
|
|
150
|
+
* the one form, and the parser dedupes anyway, so it requires just itself.
|
|
151
|
+
*/
|
|
152
|
+
function leadTriggers(name: string): string[] {
|
|
153
|
+
const spaced = name.replaceAll("-", " ");
|
|
154
|
+
return spaced === name ? [name] : [name, spaced];
|
|
99
155
|
}
|
|
100
156
|
|
|
101
157
|
/** Remove fenced and inline code so prose checks don't trip on examples. */
|
|
@@ -134,9 +190,29 @@ export function lintSkill(skillDir: string): DoctorReport {
|
|
|
134
190
|
`skill file is "${skillFile}" — must be exactly "SKILL.md" or the skill is silently ignored`
|
|
135
191
|
);
|
|
136
192
|
|
|
137
|
-
const {
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
const {
|
|
194
|
+
name,
|
|
195
|
+
description,
|
|
196
|
+
descriptionQuoted,
|
|
197
|
+
triggers,
|
|
198
|
+
shipped,
|
|
199
|
+
license,
|
|
200
|
+
derivedFrom,
|
|
201
|
+
body,
|
|
202
|
+
} = parseSkill(readFileSync(resolve(skillDir, skillFile), "utf-8"));
|
|
203
|
+
|
|
204
|
+
// ── provenance (shipped skills only) ──
|
|
205
|
+
if (shipped) {
|
|
206
|
+
if (license) add("pass", "license", `licensed ${license}`);
|
|
207
|
+
else if (derivedFrom)
|
|
208
|
+
add("pass", "license", `unlicensed by design — derived from ${derivedFrom}`);
|
|
209
|
+
else
|
|
210
|
+
add(
|
|
211
|
+
"warn",
|
|
212
|
+
"license",
|
|
213
|
+
"shipped skill declares no license — add `license: MIT`, or `metadata.derived-from: <origin>` when the idea comes from another project"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
140
216
|
|
|
141
217
|
// The runtime keys a skill by its folder name; a mismatched frontmatter `name`
|
|
142
218
|
// makes the skill silently fail to load.
|
|
@@ -218,6 +294,37 @@ export function lintSkill(skillDir: string): DoctorReport {
|
|
|
218
294
|
: add("pass", "description.pov", "third person");
|
|
219
295
|
}
|
|
220
296
|
|
|
297
|
+
// ── triggers ──
|
|
298
|
+
if (triggers.length === 0) {
|
|
299
|
+
add(
|
|
300
|
+
"warn",
|
|
301
|
+
"metadata.triggers",
|
|
302
|
+
"no metadata.triggers declared — add the words and phrases a prompt would contain so the prompt-time matcher can surface this skill; without them it falls back to keywords mined from the description"
|
|
303
|
+
);
|
|
304
|
+
} else if (triggers.length < MIN_TRIGGERS) {
|
|
305
|
+
add(
|
|
306
|
+
"warn",
|
|
307
|
+
"metadata.triggers",
|
|
308
|
+
`only ${triggers.length} trigger(s) declared — aim for at least ${MIN_TRIGGERS}, mostly multi-word phrases`
|
|
309
|
+
);
|
|
310
|
+
} else {
|
|
311
|
+
add("pass", "metadata.triggers", `${triggers.length} triggers declared`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (name && triggers.length > 0) {
|
|
315
|
+
const lead = leadTriggers(name);
|
|
316
|
+
const actual = triggers.slice(0, lead.length);
|
|
317
|
+
const wanted = quoteList(lead, " then ");
|
|
318
|
+
const found = quoteList(actual, ", ") || "nothing";
|
|
319
|
+
actual.join("\u0000") === lead.join("\u0000")
|
|
320
|
+
? add("pass", "metadata.triggers.lead", `leads with ${wanted}`)
|
|
321
|
+
: add(
|
|
322
|
+
"warn",
|
|
323
|
+
"metadata.triggers.lead",
|
|
324
|
+
`triggers must lead with ${wanted} — found ${found}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
221
328
|
// ── body ──
|
|
222
329
|
const bodyLines = body.split("\n").length;
|
|
223
330
|
bodyLines <= MAX_BODY_LINES
|
|
@@ -299,6 +406,24 @@ export function lintSkill(skillDir: string): DoctorReport {
|
|
|
299
406
|
}
|
|
300
407
|
|
|
301
408
|
/** Render a report as a human-readable string. */
|
|
409
|
+
/** One scannable line per skill for a whole-store run: verdict plus the checks that fired. */
|
|
410
|
+
export function formatSummary(r: DoctorReport): string {
|
|
411
|
+
const fired = (level: Level) =>
|
|
412
|
+
r.findings.filter((f) => f.level === level).map((f) => f.check);
|
|
413
|
+
// The folder name, not the frontmatter name — the folder is what the reader
|
|
414
|
+
// passes back to `pal cli skill doctor <name>`, and a mismatch between the two
|
|
415
|
+
// is itself one of the errors this line reports.
|
|
416
|
+
const name = basename(r.dir).padEnd(20);
|
|
417
|
+
|
|
418
|
+
if (r.errors > 0) {
|
|
419
|
+
return `✗ ${name} ${r.errors} error(s): ${fired("error").join(", ")}`;
|
|
420
|
+
}
|
|
421
|
+
if (r.warnings > 0) {
|
|
422
|
+
return `⚠ ${name} ${r.warnings} warning(s): ${fired("warn").join(", ")}`;
|
|
423
|
+
}
|
|
424
|
+
return `✓ ${name} clean`;
|
|
425
|
+
}
|
|
426
|
+
|
|
302
427
|
export function formatReport(r: DoctorReport): string {
|
|
303
428
|
const icon = { pass: "✓", warn: "⚠", error: "✗" } as const;
|
|
304
429
|
const lines = [`skill-doctor: ${r.name ?? "(unparsed)"} — ${r.dir}`];
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: review
|
|
3
|
-
description: "Security-focused code review with severity ratings. Use when reviewing code for security issues, vulnerabilities, or OWASP concerns."
|
|
4
|
-
argument-hint: [file or directory]
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
When the user invokes /review <file, diff, or PR>:
|
|
8
|
-
|
|
9
|
-
1. Read the target code or diff in full
|
|
10
|
-
2. Analyze for:
|
|
11
|
-
- **Security** — OWASP top 10, injection, auth, data exposure
|
|
12
|
-
- **Logic** — edge cases, off-by-one, null handling, race conditions
|
|
13
|
-
- **Performance** — N+1 queries, unnecessary allocations, blocking calls
|
|
14
|
-
- **Style** — consistency with surrounding code (not your preferences)
|
|
15
|
-
3. Output findings grouped by severity:
|
|
16
|
-
- CRITICAL — must fix before merge
|
|
17
|
-
- WARNING — should fix, creates risk
|
|
18
|
-
- SUGGESTION — nice to have
|
|
19
|
-
4. Each finding includes: file:line, what's wrong, concrete fix
|
|
20
|
-
5. End with a one-line verdict: APPROVE / REQUEST CHANGES / NEEDS DISCUSSION
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: summarize
|
|
3
|
-
description: "Structured summarization of documents, URLs, or conversations. Use when summarizing content, creating overviews, or distilling key points."
|
|
4
|
-
argument-hint: <document, URL, or topic>
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Summarize $ARGUMENTS:
|
|
8
|
-
|
|
9
|
-
1. Fetch or read the target content
|
|
10
|
-
2. Produce:
|
|
11
|
-
- **TLDR** (1 sentence)
|
|
12
|
-
- **Key points** (5-7 bullets)
|
|
13
|
-
- **Action items** (if any exist in the content)
|
|
14
|
-
- **Notable quotes/data** (verbatim, with attribution)
|
|
15
|
-
3. Keep total output under 500 words
|
|
16
|
-
4. If the content is very long, note what was covered vs skipped
|
|
File without changes
|
|
File without changes
|
|
File without changes
|