portable-agent-layer 0.85.23 → 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 +136 -15
- package/src/cli/index.ts +1 -1
- package/src/cli/skill.ts +10 -2
- package/src/hooks/handlers/agenda.ts +4 -4
- package/src/hooks/lib/agent-definition.ts +57 -0
- package/src/hooks/lib/frontmatter.ts +21 -8
- package/src/hooks/lib/inference.ts +100 -52
- package/src/hooks/lib/log.ts +10 -4
- package/src/hooks/lib/models.ts +34 -0
- package/src/hooks/lib/opencode-config.ts +16 -5
- 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/self-model.ts +1 -1
- package/src/tools/subagent-doctor.ts +50 -40
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PAL — Codex uninstaller
|
|
3
3
|
* Removes only PAL-owned hooks from ~/.codex/hooks.json. Preserves user hooks.
|
|
4
|
-
* Removes PAL skill symlinks.
|
|
4
|
+
* Removes PAL skill symlinks and PAL agents.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadCodexHooksTemplate,
|
|
12
12
|
log,
|
|
13
13
|
readJson,
|
|
14
|
+
removeAgentsFromCodex,
|
|
14
15
|
removeCodexStatuslineConfig,
|
|
15
16
|
removeSkills,
|
|
16
17
|
unmergeCodexHooks,
|
|
@@ -87,6 +88,12 @@ if (removed.length > 0) {
|
|
|
87
88
|
log.info("No PAL skills found");
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// --- Remove PAL agents ---
|
|
92
|
+
const removedAgents = removeAgentsFromCodex(resolve(CODEX_DIR, "agents"));
|
|
93
|
+
if (removedAgents.length > 0) {
|
|
94
|
+
log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
// --- Disable hooks in config.toml ---
|
|
91
98
|
const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
|
|
92
99
|
disableCodexHooks(CONFIG_FILE);
|
package/src/targets/lib.ts
CHANGED
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
21
|
+
import { AGENT_PLATFORMS, type AgentPlatform } from "../hooks/lib/agent-definition";
|
|
21
22
|
import { assets, palHome, platform } from "../hooks/lib/paths";
|
|
22
23
|
import { declaredTriggers } from "../hooks/lib/skill-triggers";
|
|
24
|
+
import { agentFileName, renderAgentForPlatform } from "./agent-render";
|
|
23
25
|
|
|
24
26
|
// --- Colored logging ---
|
|
25
27
|
|
|
@@ -993,7 +995,7 @@ export function removeSkills(claudeSkillsDir: string): string[] {
|
|
|
993
995
|
|
|
994
996
|
// --- Agents ---
|
|
995
997
|
|
|
996
|
-
const claudeAgentsDir = () =>
|
|
998
|
+
const claudeAgentsDir = () => nativeAgentsDir("claude");
|
|
997
999
|
|
|
998
1000
|
/**
|
|
999
1001
|
* Install PAL agent definitions into ~/.claude/agents/.
|
|
@@ -1031,81 +1033,28 @@ export function countAgents(): number {
|
|
|
1031
1033
|
}
|
|
1032
1034
|
}
|
|
1033
1035
|
|
|
1034
|
-
// --- Agent platform
|
|
1036
|
+
// --- Agent platform rendering ---
|
|
1035
1037
|
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
* Extract a platform-specific agent file from the unified agent format.
|
|
1041
|
-
*
|
|
1042
|
-
* Each agent .md defines platform blocks at the top level:
|
|
1043
|
-
* claude: → fields for Claude Code
|
|
1044
|
-
* opencode: → fields for opencode
|
|
1045
|
-
* cursor: → fields for Cursor
|
|
1046
|
-
*
|
|
1047
|
-
* Global fields (name, description) are always included.
|
|
1048
|
-
* The target platform block is un-indented and merged into the root.
|
|
1049
|
-
* All other platform blocks are stripped.
|
|
1050
|
-
*/
|
|
1051
|
-
function extractAgentForPlatform(content: string, platform: AgentPlatform): string {
|
|
1052
|
-
const parts = content.split(/^---\s*$/m);
|
|
1053
|
-
if (parts.length < 3) return content;
|
|
1054
|
-
|
|
1055
|
-
const frontmatter = parts[1];
|
|
1056
|
-
const body = parts.slice(2).join("---");
|
|
1057
|
-
|
|
1058
|
-
const globalLines: string[] = [];
|
|
1059
|
-
const platformLines: Record<AgentPlatform, string[]> = {
|
|
1060
|
-
claude: [],
|
|
1061
|
-
opencode: [],
|
|
1062
|
-
cursor: [],
|
|
1063
|
-
copilot: [],
|
|
1064
|
-
};
|
|
1065
|
-
let currentPlatform: AgentPlatform | null = null;
|
|
1066
|
-
|
|
1067
|
-
for (const line of frontmatter.split("\n")) {
|
|
1068
|
-
if (!line.trim()) continue;
|
|
1069
|
-
|
|
1070
|
-
const platformMatch = new RegExp(/^(claude|opencode|cursor|copilot):\s*$/).exec(line);
|
|
1071
|
-
if (platformMatch) {
|
|
1072
|
-
currentPlatform = platformMatch[1] as AgentPlatform;
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
if (currentPlatform) {
|
|
1077
|
-
if (new RegExp(/^ {2}/).exec(line)) {
|
|
1078
|
-
platformLines[currentPlatform].push(line.slice(2)); // un-indent one level
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
currentPlatform = null; // end of platform block
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
globalLines.push(line);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const newFrontmatter = [...globalLines, ...platformLines[platform]]
|
|
1088
|
-
.filter((l) => l.trim())
|
|
1089
|
-
.join("\n");
|
|
1038
|
+
const shippedAgentStems = () =>
|
|
1039
|
+
readdirSync(assets.agents())
|
|
1040
|
+
.filter((f) => f.endsWith(".md"))
|
|
1041
|
+
.map((f) => f.replace(/\.md$/, ""));
|
|
1090
1042
|
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1043
|
+
const shippedAgentSource = (stem: string) =>
|
|
1044
|
+
readFileSync(resolve(assets.agents(), `${stem}.md`), "utf-8");
|
|
1093
1045
|
|
|
1094
1046
|
/** Install agents for a platform into a target directory. Always overwrites. */
|
|
1095
1047
|
function installAgents(targetDir: string, platform: AgentPlatform): number {
|
|
1096
|
-
|
|
1097
|
-
if (!existsSync(agentsDir)) return 0;
|
|
1048
|
+
if (!existsSync(assets.agents())) return 0;
|
|
1098
1049
|
|
|
1099
1050
|
mkdirSync(targetDir, { recursive: true });
|
|
1100
1051
|
let count = 0;
|
|
1101
1052
|
|
|
1102
|
-
for (const
|
|
1103
|
-
const
|
|
1104
|
-
const installed = reportOnlyOnFailure(`${platform} agent ${name}`, () => {
|
|
1105
|
-
const content = readFileSync(resolve(agentsDir, file), "utf-8");
|
|
1053
|
+
for (const stem of shippedAgentStems()) {
|
|
1054
|
+
const installed = reportOnlyOnFailure(`${platform} agent ${stem}`, () => {
|
|
1106
1055
|
writeFileSync(
|
|
1107
|
-
resolve(targetDir,
|
|
1108
|
-
|
|
1056
|
+
resolve(targetDir, agentFileName(stem, platform)),
|
|
1057
|
+
renderAgentForPlatform(shippedAgentSource(stem), platform),
|
|
1109
1058
|
"utf-8"
|
|
1110
1059
|
);
|
|
1111
1060
|
});
|
|
@@ -1114,18 +1063,31 @@ function installAgents(targetDir: string, platform: AgentPlatform): number {
|
|
|
1114
1063
|
return count;
|
|
1115
1064
|
}
|
|
1116
1065
|
|
|
1066
|
+
/** Shipped agents in targetDir that differ from what installAgents would write there. */
|
|
1067
|
+
export function staleShippedAgents(targetDir: string, agent: AgentPlatform): string[] {
|
|
1068
|
+
if (!existsSync(assets.agents())) return [];
|
|
1069
|
+
const isStale = (stem: string) => {
|
|
1070
|
+
const installed = resolve(targetDir, agentFileName(stem, agent));
|
|
1071
|
+
return (
|
|
1072
|
+
!existsSync(installed) ||
|
|
1073
|
+
readFileSync(installed, "utf-8") !==
|
|
1074
|
+
renderAgentForPlatform(shippedAgentSource(stem), agent)
|
|
1075
|
+
);
|
|
1076
|
+
};
|
|
1077
|
+
return shippedAgentStems().filter(isStale).sort();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1117
1080
|
/** Remove PAL agents from a directory. */
|
|
1118
|
-
function uninstallAgents(targetDir: string,
|
|
1119
|
-
|
|
1120
|
-
if (!existsSync(agentsDir)) return [];
|
|
1081
|
+
function uninstallAgents(targetDir: string, platform: AgentPlatform): string[] {
|
|
1082
|
+
if (!existsSync(assets.agents())) return [];
|
|
1121
1083
|
|
|
1122
1084
|
const removed: string[] = [];
|
|
1123
|
-
for (const
|
|
1124
|
-
const dst = resolve(targetDir,
|
|
1085
|
+
for (const stem of shippedAgentStems()) {
|
|
1086
|
+
const dst = resolve(targetDir, agentFileName(stem, platform));
|
|
1125
1087
|
if (existsSync(dst)) {
|
|
1126
1088
|
unlinkSync(dst);
|
|
1127
|
-
removed.push(
|
|
1128
|
-
log.detail(`Removed ${
|
|
1089
|
+
removed.push(stem);
|
|
1090
|
+
log.detail(`Removed ${platform} agent: ${stem}`);
|
|
1129
1091
|
}
|
|
1130
1092
|
}
|
|
1131
1093
|
return removed;
|
|
@@ -1147,6 +1109,10 @@ export function copyAgentsForCopilot(copilotAgentsDir: string): number {
|
|
|
1147
1109
|
return installAgents(copilotAgentsDir, "copilot");
|
|
1148
1110
|
}
|
|
1149
1111
|
|
|
1112
|
+
export function copyAgentsForCodex(codexAgentsDir: string): number {
|
|
1113
|
+
return installAgents(codexAgentsDir, "codex");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1150
1116
|
export function removeAgentsFromCursor(cursorAgentsDir: string): string[] {
|
|
1151
1117
|
return uninstallAgents(cursorAgentsDir, "cursor");
|
|
1152
1118
|
}
|
|
@@ -1155,6 +1121,10 @@ export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] {
|
|
|
1155
1121
|
return uninstallAgents(copilotAgentsDir, "copilot");
|
|
1156
1122
|
}
|
|
1157
1123
|
|
|
1124
|
+
export function removeAgentsFromCodex(codexAgentsDir: string): string[] {
|
|
1125
|
+
return uninstallAgents(codexAgentsDir, "codex");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1158
1128
|
// --- Personal subagents (user-authored, under ~/.pal/agents/) ---
|
|
1159
1129
|
|
|
1160
1130
|
/**
|
|
@@ -1169,12 +1139,19 @@ const palAgentsStore = () => resolve(palHome(), "agents");
|
|
|
1169
1139
|
* exists (mirrors linkPersonalSkill's per-agent gate).
|
|
1170
1140
|
*/
|
|
1171
1141
|
function personalSubagentTargets(): { agent: AgentPlatform; dir: string }[] {
|
|
1172
|
-
return
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1142
|
+
return AGENT_PLATFORMS.map((agent) => ({ agent, dir: nativeAgentsDir(agent) }));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** The directory each agent reads its subagent definitions from. */
|
|
1146
|
+
export function nativeAgentsDir(agent: AgentPlatform): string {
|
|
1147
|
+
const home = {
|
|
1148
|
+
claude: platform.claudeDir,
|
|
1149
|
+
opencode: platform.opencodeDir,
|
|
1150
|
+
cursor: platform.cursorDir,
|
|
1151
|
+
copilot: platform.copilotDir,
|
|
1152
|
+
codex: platform.codexDir,
|
|
1153
|
+
}[agent];
|
|
1154
|
+
return resolve(home(), "agents");
|
|
1178
1155
|
}
|
|
1179
1156
|
|
|
1180
1157
|
/** Names of the subagents PAL ships (assets/agents/*.md). */
|
|
@@ -1215,17 +1192,23 @@ export function installPersonalSubagent(name: string): string[] {
|
|
|
1215
1192
|
);
|
|
1216
1193
|
}
|
|
1217
1194
|
const content = readFileSync(src, "utf-8");
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
resolve(dir,
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1195
|
+
const renders = personalSubagentTargets()
|
|
1196
|
+
.filter(({ dir }) => existsSync(dir))
|
|
1197
|
+
.map(({ agent, dir }) => ({
|
|
1198
|
+
agent,
|
|
1199
|
+
file: resolve(dir, agentFileName(name, agent)),
|
|
1200
|
+
text: renderForPersonalSubagent(content, agent),
|
|
1201
|
+
}));
|
|
1202
|
+
for (const { file, text } of renders) writeFileSync(file, text, "utf-8");
|
|
1203
|
+
return renders.map(({ agent }) => agent);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function renderForPersonalSubagent(content: string, agent: AgentPlatform): string {
|
|
1207
|
+
try {
|
|
1208
|
+
return renderAgentForPlatform(content, agent);
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
throw new Error(`Cannot build the ${agent} subagent: ${(e as Error).message}`);
|
|
1227
1211
|
}
|
|
1228
|
-
return installed;
|
|
1229
1212
|
}
|
|
1230
1213
|
|
|
1231
1214
|
/** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { isInside } from "../../hooks/lib/paths";
|
|
10
11
|
|
|
11
12
|
const UI_DIST = resolve(import.meta.dir, "ui", "dist");
|
|
12
13
|
|
|
@@ -57,12 +58,6 @@ export function indexHtml(): Response {
|
|
|
57
58
|
*/
|
|
58
59
|
export function staticAsset(pathname: string): Response | null {
|
|
59
60
|
const target = resolve(UI_DIST, `.${pathname}`);
|
|
60
|
-
if (!
|
|
61
|
+
if (!isInside(UI_DIST, target)) return null;
|
|
61
62
|
return existsSync(target) ? new Response(Bun.file(target)) : null;
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
/** Compared as paths, not strings — a separator differs by platform. */
|
|
65
|
-
function isInsideDist(target: string): boolean {
|
|
66
|
-
const rel = relative(UI_DIST, target);
|
|
67
|
-
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
68
|
-
}
|
|
@@ -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 =
|
package/src/tools/self-model.ts
CHANGED
|
@@ -84,7 +84,7 @@ async function composeSelfModel(days: number): Promise<string> {
|
|
|
84
84
|
const result = await inference({
|
|
85
85
|
system: buildPrompt(ai.name, principal.name),
|
|
86
86
|
user: inferenceUserContent(rawData, readFileOrEmpty(selfModelPath())),
|
|
87
|
-
|
|
87
|
+
tier: "medium",
|
|
88
88
|
maxTokens: 1500,
|
|
89
89
|
timeout: 90000,
|
|
90
90
|
caller: "self-model",
|
|
@@ -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) {
|