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.
Files changed (34) hide show
  1. package/README.md +7 -1
  2. package/assets/agents/gemini-researcher.md +2 -2
  3. package/assets/agents/grok-researcher.md +2 -2
  4. package/assets/agents/perplexity-researcher.md +1 -1
  5. package/assets/agents/subagent-author.md +1 -1
  6. package/assets/skills/analyze-youtube/SKILL.md +1 -1
  7. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -0
  8. package/assets/skills/create-subagent/SKILL.md +2 -2
  9. package/assets/skills/create-subagent/authoring-guide.md +13 -1
  10. package/assets/skills/deep-research/SKILL.md +1 -0
  11. package/assets/skills/deep-research/tools/gemini-search.ts +1 -0
  12. package/assets/skills/deep-research/tools/grok-search.ts +1 -0
  13. package/assets/skills/deep-research/tools/perplexity-search.ts +1 -1
  14. package/assets/skills/fyzz-chat-api/SKILL.md +2 -2
  15. package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +1 -1
  16. package/package.json +1 -1
  17. package/src/cli/doctor/agents.ts +16 -1
  18. package/src/cli/doctor/health.ts +7 -6
  19. package/src/cli/doctor/inference.ts +89 -11
  20. package/src/cli/index.ts +0 -0
  21. package/src/cli/skill.ts +10 -2
  22. package/src/hooks/lib/agent-definition.ts +57 -0
  23. package/src/hooks/lib/frontmatter.ts +21 -8
  24. package/src/hooks/lib/inference.ts +40 -9
  25. package/src/hooks/lib/log.ts +10 -4
  26. package/src/hooks/lib/pal-env.ts +51 -0
  27. package/src/hooks/lib/paths.ts +7 -1
  28. package/src/targets/agent-render.ts +103 -0
  29. package/src/targets/codex/install.ts +6 -1
  30. package/src/targets/codex/uninstall.ts +8 -1
  31. package/src/targets/lib.ts +72 -89
  32. package/src/tools/control-room/static.ts +3 -8
  33. package/src/tools/lib/skill-doctor.ts +4 -4
  34. 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 parts = content.split(/^---\s*$/m);
94
- if (parts.length < 3) {
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 = parts[1];
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 parts = content.split(/^---\s*$/m);
75
- if (parts.length < 3) {
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) {