portable-agent-layer 0.85.24 → 0.86.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 -1
- package/assets/agents/gemini-researcher.md +2 -2
- package/assets/agents/grok-researcher.md +2 -2
- package/assets/agents/perplexity-researcher.md +1 -1
- package/assets/agents/subagent-author.md +1 -1
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -0
- package/assets/skills/create-subagent/SKILL.md +2 -2
- package/assets/skills/create-subagent/authoring-guide.md +13 -1
- package/assets/skills/deep-research/SKILL.md +1 -0
- package/assets/skills/deep-research/tools/gemini-search.ts +1 -0
- package/assets/skills/deep-research/tools/grok-search.ts +1 -0
- package/assets/skills/deep-research/tools/perplexity-search.ts +1 -1
- package/assets/skills/fyzz-chat-api/SKILL.md +2 -2
- package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +1 -1
- package/package.json +1 -1
- package/src/cli/doctor/agents.ts +16 -1
- package/src/cli/doctor/health.ts +7 -6
- package/src/cli/doctor/inference.ts +89 -11
- package/src/cli/index.ts +0 -0
- package/src/cli/skill.ts +10 -2
- package/src/hooks/lib/agent-definition.ts +57 -0
- package/src/hooks/lib/frontmatter.ts +21 -8
- package/src/hooks/lib/inference.ts +40 -9
- package/src/hooks/lib/log.ts +10 -4
- package/src/hooks/lib/pal-env.ts +51 -0
- package/src/hooks/lib/paths.ts +7 -1
- package/src/targets/agent-render.ts +103 -0
- package/src/targets/codex/install.ts +6 -1
- package/src/targets/codex/uninstall.ts +8 -1
- package/src/targets/lib.ts +72 -89
- package/src/tools/control-room/static.ts +3 -8
- package/src/tools/lib/skill-doctor.ts +4 -4
- package/src/tools/subagent-doctor.ts +50 -40
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
14
14
|
import { basename, extname, relative, resolve } from "node:path";
|
|
15
|
+
import { splitFrontmatter } from "../../hooks/lib/frontmatter";
|
|
15
16
|
import { palHome, toPath } from "../../hooks/lib/paths";
|
|
16
17
|
import { declaredTriggers } from "../../hooks/lib/skill-triggers";
|
|
17
18
|
|
|
@@ -90,8 +91,8 @@ export function findAbsolutePaths(skillDir: string): string[] {
|
|
|
90
91
|
|
|
91
92
|
/** Split a SKILL.md into frontmatter fields and body. */
|
|
92
93
|
export function parseSkill(content: string): ParsedSkill {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
94
|
+
const split = splitFrontmatter(content);
|
|
95
|
+
if (!split) {
|
|
95
96
|
return {
|
|
96
97
|
name: null,
|
|
97
98
|
description: null,
|
|
@@ -103,8 +104,7 @@ export function parseSkill(content: string): ParsedSkill {
|
|
|
103
104
|
body: content,
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
|
-
const frontmatter =
|
|
107
|
-
const body = parts.slice(2).join("---");
|
|
107
|
+
const { frontmatter, body } = split;
|
|
108
108
|
const name = /^name:\s*"?(.+?)"?\s*$/m.exec(frontmatter)?.[1] ?? null;
|
|
109
109
|
const rawDescription = /^description:[ \t]*(.*?)\s*$/m.exec(frontmatter)?.[1] ?? null;
|
|
110
110
|
const descriptionQuoted =
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* subagent-doctor — static evaluator for a personal subagent definition
|
|
3
3
|
* (~/.pal/agents/<name>.md) with merged multi-platform frontmatter. Checks only
|
|
4
4
|
* what is mechanically verifiable: name/description constraints, per-platform
|
|
5
|
-
* blocks (claude/opencode/cursor/copilot), model/tools/permission shape, body
|
|
5
|
+
* blocks (claude/opencode/cursor/copilot/codex), model/tools/permission shape, body
|
|
6
6
|
* length, and absolute-path portability.
|
|
7
7
|
*
|
|
8
8
|
* Library: import { lintSubagent, formatSubagentReport } from ".../subagent-doctor"
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
15
15
|
import { basename, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
AGENT_PLATFORMS,
|
|
18
|
+
type AgentPlatform,
|
|
19
|
+
parseAgentDefinition,
|
|
20
|
+
} from "../hooks/lib/agent-definition";
|
|
16
21
|
import { assets, namesAPath, palHome, toPath } from "../hooks/lib/paths";
|
|
17
22
|
|
|
18
23
|
type Level = "pass" | "warn" | "error";
|
|
@@ -31,9 +36,6 @@ export interface SubagentReport {
|
|
|
31
36
|
warnings: number;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
const AGENT_PLATFORMS = ["claude", "opencode", "cursor", "copilot"] as const;
|
|
35
|
-
type AgentPlatform = (typeof AGENT_PLATFORMS)[number];
|
|
36
|
-
|
|
37
39
|
interface ParsedSubagent {
|
|
38
40
|
hasFrontmatter: boolean;
|
|
39
41
|
name: string | null;
|
|
@@ -53,6 +55,9 @@ const MAX_BODY_LINES = 500;
|
|
|
53
55
|
const KNOWN_MODELS = ["inherit", "fable", "sonnet", "opus", "haiku"];
|
|
54
56
|
const PERMISSION_VALUES = ["allow", "ask", "deny"];
|
|
55
57
|
const OPENCODE_MODES = ["subagent", "primary", "all"];
|
|
58
|
+
const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
59
|
+
const CODEX_TABLE_KEYS = ["tools", "skills"];
|
|
60
|
+
const SHARED_FIELDS = ["name", "description"];
|
|
56
61
|
|
|
57
62
|
/** Machine/user-specific absolute paths that will not survive an export. */
|
|
58
63
|
const ABSOLUTE_PATH_RE =
|
|
@@ -71,42 +76,11 @@ function shippedAgentNames(): Set<string> {
|
|
|
71
76
|
|
|
72
77
|
/** Split a merged subagent .md into global fields, per-platform blocks, and body. */
|
|
73
78
|
function parseSubagent(content: string): ParsedSubagent {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
return {
|
|
77
|
-
hasFrontmatter: false,
|
|
78
|
-
name: null,
|
|
79
|
-
description: null,
|
|
80
|
-
descriptionQuoted: false,
|
|
81
|
-
global: [],
|
|
82
|
-
platforms: {},
|
|
83
|
-
body: content,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const frontmatter = parts[1];
|
|
87
|
-
const body = parts.slice(2).join("---");
|
|
88
|
-
|
|
89
|
-
const global: string[] = [];
|
|
90
|
-
const platforms: Partial<Record<AgentPlatform, string[]>> = {};
|
|
91
|
-
let current: AgentPlatform | null = null;
|
|
92
|
-
|
|
93
|
-
for (const line of frontmatter.split("\n")) {
|
|
94
|
-
if (!line.trim()) continue;
|
|
95
|
-
const pm = /^(claude|opencode|cursor|copilot):\s*$/.exec(line);
|
|
96
|
-
if (pm) {
|
|
97
|
-
current = pm[1] as AgentPlatform;
|
|
98
|
-
platforms[current] ??= [];
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (current) {
|
|
102
|
-
if (/^ {2}/.test(line)) {
|
|
103
|
-
platforms[current]?.push(line.slice(2));
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
current = null;
|
|
107
|
-
}
|
|
108
|
-
global.push(line);
|
|
79
|
+
const definition = parseAgentDefinition(content);
|
|
80
|
+
if (!definition.hasFrontmatter) {
|
|
81
|
+
return { ...definition, name: null, description: null, descriptionQuoted: false };
|
|
109
82
|
}
|
|
83
|
+
const { global, platforms, body } = definition;
|
|
110
84
|
|
|
111
85
|
const globalText = global.join("\n");
|
|
112
86
|
const name = /^name:\s*"?(.+?)"?\s*$/m.exec(globalText)?.[1] ?? null;
|
|
@@ -139,6 +113,10 @@ function fieldValue(lines: string[], key: string): string | null {
|
|
|
139
113
|
return null;
|
|
140
114
|
}
|
|
141
115
|
|
|
116
|
+
function topLevelKeys(lines: string[]): string[] {
|
|
117
|
+
return lines.flatMap((line) => /^([A-Za-z0-9_-]+):/.exec(line)?.[1] ?? []);
|
|
118
|
+
}
|
|
119
|
+
|
|
142
120
|
/** True when a block declares `key:` (with or without an inline value). */
|
|
143
121
|
function hasField(lines: string[], key: string): boolean {
|
|
144
122
|
return lines.some((l) => new RegExp(`^${key}:`).test(l));
|
|
@@ -202,6 +180,26 @@ function checkCopilotBlock(lines: string[], add: AddFinding): void {
|
|
|
202
180
|
if (model) validateModel(model, "copilot", add);
|
|
203
181
|
}
|
|
204
182
|
|
|
183
|
+
function checkCodexBlock(lines: string[], add: AddFinding): void {
|
|
184
|
+
const model = fieldValue(lines, "model");
|
|
185
|
+
if (model) validateModel(model, "codex", add);
|
|
186
|
+
const sandbox = fieldValue(lines, "sandbox_mode");
|
|
187
|
+
if (sandbox !== null && !CODEX_SANDBOX_MODES.includes(sandbox)) {
|
|
188
|
+
add(
|
|
189
|
+
"warn",
|
|
190
|
+
"codex.sandbox_mode",
|
|
191
|
+
`sandbox_mode "${sandbox}" — expected one of ${CODEX_SANDBOX_MODES.join(", ")}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
for (const key of CODEX_TABLE_KEYS.filter((k) => hasField(lines, k))) {
|
|
195
|
+
add(
|
|
196
|
+
"warn",
|
|
197
|
+
`codex.${key}`,
|
|
198
|
+
`Codex reads \`${key}\` as a config table, not a list — the agent will not load; name tools or skills in the body instead`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
205
203
|
function validateModel(model: string, platform: string, add: AddFinding): void {
|
|
206
204
|
const known = KNOWN_MODELS.includes(model) || /^(claude-|gpt-|o\d|gemini-)/.test(model);
|
|
207
205
|
if (!known) {
|
|
@@ -311,6 +309,17 @@ export function lintSubagent(file: string): SubagentReport {
|
|
|
311
309
|
);
|
|
312
310
|
}
|
|
313
311
|
|
|
312
|
+
const unmappedGlobals = topLevelKeys(parsed.global).filter(
|
|
313
|
+
(key) => !SHARED_FIELDS.includes(key)
|
|
314
|
+
);
|
|
315
|
+
if (unmappedGlobals.length > 0) {
|
|
316
|
+
add(
|
|
317
|
+
"error",
|
|
318
|
+
"global.fields",
|
|
319
|
+
`global ${unmappedGlobals.join(", ")} has no Codex key, so linking fails — move it into the platform blocks that use it`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
314
323
|
// ── platform blocks ──
|
|
315
324
|
const present = AGENT_PLATFORMS.filter((p) => (platforms[p]?.length ?? 0) > 0);
|
|
316
325
|
present.length > 0
|
|
@@ -318,13 +327,14 @@ export function lintSubagent(file: string): SubagentReport {
|
|
|
318
327
|
: add(
|
|
319
328
|
"warn",
|
|
320
329
|
"platforms",
|
|
321
|
-
"no platform block — the subagent installs with only name/description; add a claude:/opencode:/cursor:/copilot: block to set model, tools, mode"
|
|
330
|
+
"no platform block — the subagent installs with only name/description; add a claude:/opencode:/cursor:/copilot:/codex: block to set model, tools, mode"
|
|
322
331
|
);
|
|
323
332
|
|
|
324
333
|
if (platforms.claude) checkClaudeBlock(platforms.claude, add);
|
|
325
334
|
if (platforms.opencode) checkOpencodeBlock(platforms.opencode, add);
|
|
326
335
|
if (platforms.cursor) checkCursorBlock(platforms.cursor, add);
|
|
327
336
|
if (platforms.copilot) checkCopilotBlock(platforms.copilot, add);
|
|
337
|
+
if (platforms.codex) checkCodexBlock(platforms.codex, add);
|
|
328
338
|
|
|
329
339
|
// ── skills field: Claude preloads it; the others have no such field ──
|
|
330
340
|
for (const p of ["opencode", "cursor", "copilot"] as const) {
|