Skip to content

Commit 63cee1a

Browse files
authored
fix(sdk): warn for silently dropped skill, command, and hook options (#8)
1 parent 73c11a0 commit 63cee1a

15 files changed

Lines changed: 517 additions & 26 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
packages:
3+
"@jalco/ap-sdk": patch
4+
---
5+
6+
### Warn for silently dropped skill, command, and hook options
7+
8+
Emitters that cannot represent a declared optional field now record an `unsupported-option` warning instead of dropping the field silently, the same contract the Codex subagent warnings already followed. This covers `allowedTools`, `disableModelInvocation`, `license`, and `metadata` on skills, `allowedTools`, `argumentHint`, and passthrough `frontmatter` on commands, and the `powershell` hook command variant on harnesses with a single command slot. Output files are unchanged: the build still degrades, and `warnings[]` now tells the caller what was lost.

‎apps/docs/content/docs/commands.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export default definePlugin({
3535

3636
## Fields
3737

38-
- `name`, `description`, `argumentHint`, and `body` define the command.
39-
- `allowedTools` is honored by Claude Code.
40-
- `frontmatter` is honored by YAML-frontmatter harnesses; Gemini TOML and Cursor plain markdown ignore it.
38+
- `name`, `description`, `argumentHint`, and `body` define the command. `argumentHint` is emitted on Claude Code, Codex, Pi, and Copilot. The other harnesses drop it with an `unsupported-option` warning.
39+
- `allowedTools` is honored by Claude Code. Codex, Pi, OpenCode, Gemini, Cursor, and Windsurf drop it with an `unsupported-option` warning. The Copilot emitter does not emit it yet.
40+
- `frontmatter` is honored by YAML-frontmatter harnesses. Gemini TOML and Cursor plain markdown drop it with an `unsupported-option` warning.
4141
- `harness.claude.model`, `harness.opencode.model`, and `harness.copilot.model` / `agent` set native model hints.
4242

4343
## Portability notes

‎apps/docs/content/docs/hooks.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default definePlugin({
1515
defineHook({
1616
event: "pre-tool-use",
1717
matcher: "ExitPlanMode",
18-
command: { unix: "node hooks/plan-review.mjs", powershell: "node hooks/plan-review.mjs" },
18+
command: { bash: "node hooks/plan-review.mjs", powershell: "node hooks/plan-review.mjs" },
1919
timeout: 120,
2020
async: true,
2121
harness: { codex: { event: "stop", matcher: undefined } },
@@ -43,7 +43,7 @@ export default definePlugin({
4343

4444
- `event`, `matcher`, `command`, and `timeout` define the hook.
4545
- `async` is native in Claude and dropped with a warning elsewhere.
46-
- Object commands can provide `unix` and `powershell`; PowerShell is used by Copilot.
46+
- Object commands can provide `bash` and `powershell`. Copilot emits the PowerShell variant as `windows`. Claude Code, Codex, and Gemini have a single command slot, so they use `bash` and drop `powershell` with an `unsupported-option` warning.
4747
- `harness.<target>.event` and `matcher` override native mapping. Use `matcher: undefined` to explicitly clear a matcher.
4848

4949
<Callout variant="note" title="Code-only harnesses">

‎apps/docs/content/docs/skills.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ export default definePlugin({
3131
- `name` — kebab-case id, max 64 characters.
3232
- `description` — max 1024 characters, the tightest harness limit; front-load trigger conditions because agents use this to decide when to load the skill.
3333
- `instructions` — the skill body.
34-
- `allowedTools` — honored by Claude Code and Pi.
35-
- `disableModelInvocation` — Pi-specific.
36-
- `license` — emitted where native skill metadata supports it.
37-
- `metadata` — honored by OpenCode and Pi.
34+
- `allowedTools` — honored by Claude Code and Pi. Other skill-capable harnesses drop it with an `unsupported-option` warning.
35+
- `disableModelInvocation` — honored by Pi and Copilot. Codex, OpenCode, Gemini, and Windsurf drop it with an `unsupported-option` warning.
36+
- `license` — emitted where native skill metadata supports it (Pi, OpenCode). Codex, Copilot, Gemini, and Windsurf drop it with an `unsupported-option` warning.
37+
- `metadata` — honored by OpenCode and Pi. Copilot, Gemini, and Windsurf drop it with an `unsupported-option` warning.
3838
- `frontmatter` — escape hatch merged into YAML-frontmatter harnesses; SDK-owned fields win on conflicts.
3939
- `resources` — extra files emitted inside the skill directory.
4040

‎packages/agent-plugin-sdk/src/harnesses/codex.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
emitContextFile,
1010
emitSkillDir,
1111
json,
12+
warnCommandAllowedTools,
13+
warnSkillAllowedTools,
14+
warnSkillDisableModelInvocation,
15+
warnSkillLicense,
1216
} from "./shared.js";
1317
import { toCodexEntry } from "./mcp.js";
1418
import { buildMatcherHooks } from "./hooks.js";
@@ -79,6 +83,11 @@ export const codex: Harness = {
7983
name: skill.name,
8084
description: skill.description,
8185
};
86+
if (skill.allowedTools?.length)
87+
warnSkillAllowedTools(ctx, "codex", skill.name);
88+
if (skill.disableModelInvocation)
89+
warnSkillDisableModelInvocation(ctx, "codex", skill.name);
90+
if (skill.license) warnSkillLicense(ctx, "codex", skill.name);
8291
files.push(...emitSkillDir(skill, frontmatter, "skills"));
8392
}
8493

@@ -92,6 +101,8 @@ export const codex: Harness = {
92101
description: command.description,
93102
"argument-hint": command.argumentHint,
94103
});
104+
if (command.allowedTools?.length)
105+
warnCommandAllowedTools(ctx, "codex", command.name);
95106
files.push(
96107
emitCommandFile(
97108
command.name,

‎packages/agent-plugin-sdk/src/harnesses/copilot.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { join } from "node:path";
33
import type { OutputFile, Plugin } from "../types.js";
44
import { compact, mapValues, mergeFrontmatter, renderFrontmatterDoc } from "../util/frontmatter.js";
55
import type { EmitContext, Harness, InstallScope } from "./types.js";
6-
import { emitContextFile, emitSkillDir, json } from "./shared.js";
6+
import {
7+
emitContextFile,
8+
emitSkillDir,
9+
json,
10+
warnSkillAllowedTools,
11+
warnSkillLicense,
12+
warnSkillMetadata,
13+
} from "./shared.js";
714
import { toCopilotEntry } from "./mcp.js";
815
import { buildCopilotHooks } from "./hooks.js";
916
import { mcpServerEntry, toolServerMcp } from "./tools.js";
@@ -56,6 +63,11 @@ export const copilot: Harness = {
5663
description: skill.description,
5764
"disable-model-invocation": skill.disableModelInvocation || undefined,
5865
});
66+
if (skill.allowedTools?.length)
67+
warnSkillAllowedTools(ctx, "copilot", skill.name);
68+
if (skill.license) warnSkillLicense(ctx, "copilot", skill.name);
69+
if (skill.metadata && Object.keys(skill.metadata).length > 0)
70+
warnSkillMetadata(ctx, "copilot", skill.name);
5971
files.push(...emitSkillDir(skill, frontmatter, ".github/skills"));
6072
}
6173

‎packages/agent-plugin-sdk/src/harnesses/cursor.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { homedir } from "node:os";
22
import { join } from "node:path";
33
import type { OutputFile, Plugin } from "../types.js";
44
import { compact, mapValues } from "../util/frontmatter.js";
5-
import type { Harness, InstallScope } from "./types.js";
6-
import { emitContextFile, json } from "./shared.js";
5+
import type { EmitContext, Harness, InstallScope } from "./types.js";
6+
import {
7+
emitContextFile,
8+
json,
9+
warnCommandAllowedTools,
10+
warnCommandArgumentHint,
11+
warnCommandFrontmatter,
12+
} from "./shared.js";
713
import { toCursorEntry } from "./mcp.js";
814
import { mcpServerEntry, toolServerMcp } from "./tools.js";
915

@@ -47,7 +53,7 @@ export const cursor: Harness = {
4753

4854
contextFileName: "AGENTS.md",
4955

50-
emit(plugin: Plugin): OutputFile[] {
56+
emit(plugin: Plugin, ctx: EmitContext): OutputFile[] {
5157
const files: OutputFile[] = [];
5258

5359
if (plugin.instructions?.trim()) {
@@ -60,6 +66,12 @@ export const cursor: Harness = {
6066
// slash-command name; Cursor appends the user's input, so the body is passed
6167
// through untouched.
6268
for (const command of plugin.commands ?? []) {
69+
if (command.argumentHint)
70+
warnCommandArgumentHint(ctx, "cursor", command.name);
71+
if (command.allowedTools?.length)
72+
warnCommandAllowedTools(ctx, "cursor", command.name);
73+
if (command.frontmatter && Object.keys(command.frontmatter).length > 0)
74+
warnCommandFrontmatter(ctx, "cursor", command.name);
6375
files.push({
6476
path: `.cursor/commands/${command.name}.md`,
6577
content: command.body.trimEnd() + "\n",

‎packages/agent-plugin-sdk/src/harnesses/gemini.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import {
1010
hasPositionalArgs,
1111
json,
1212
rewriteArgsToGemini,
13+
warnCommandAllowedTools,
14+
warnCommandArgumentHint,
15+
warnCommandFrontmatter,
16+
warnSkillAllowedTools,
17+
warnSkillDisableModelInvocation,
18+
warnSkillLicense,
19+
warnSkillMetadata,
1320
} from "./shared.js";
1421
import { toGeminiEntry } from "./mcp.js";
1522
import { buildMatcherHooks } from "./hooks.js";
@@ -96,6 +103,13 @@ export const gemini: Harness = {
96103
name: skill.name,
97104
description: skill.description,
98105
});
106+
if (skill.allowedTools?.length)
107+
warnSkillAllowedTools(ctx, "gemini", skill.name);
108+
if (skill.disableModelInvocation)
109+
warnSkillDisableModelInvocation(ctx, "gemini", skill.name);
110+
if (skill.license) warnSkillLicense(ctx, "gemini", skill.name);
111+
if (skill.metadata && Object.keys(skill.metadata).length > 0)
112+
warnSkillMetadata(ctx, "gemini", skill.name);
99113
files.push(...emitSkillDir(skill, frontmatter, "skills"));
100114
}
101115

@@ -115,6 +129,12 @@ export const gemini: Harness = {
115129
"Gemini commands only support `{{args}}` (all arguments); positional tokens are left literal.",
116130
});
117131
}
132+
if (command.argumentHint)
133+
warnCommandArgumentHint(ctx, "gemini", command.name);
134+
if (command.allowedTools?.length)
135+
warnCommandAllowedTools(ctx, "gemini", command.name);
136+
if (command.frontmatter && Object.keys(command.frontmatter).length > 0)
137+
warnCommandFrontmatter(ctx, "gemini", command.name);
118138
files.push({
119139
path: `commands/${command.name}.toml`,
120140
content: geminiCommandToml({

‎packages/agent-plugin-sdk/src/harnesses/hooks.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ function warnAsync(ctx: EmitContext | undefined, harness: HarnessId): void {
9090
});
9191
}
9292

93+
/** Warn (once per hook) that a harness dropped the `powershell` command variant. */
94+
function warnPowershell(ctx: EmitContext | undefined, harness: HarnessId): void {
95+
ctx?.warn({
96+
type: "unsupported-option",
97+
harness,
98+
feature: "hooks",
99+
option: "powershell",
100+
items: ["powershell"],
101+
details:
102+
`${harness} hook entries have a single command slot with no ` +
103+
"per-platform variant, so the `bash` command is used everywhere.",
104+
});
105+
}
106+
93107
/** Warn (once per hook) that a harness has no native form for this event. */
94108
function warnEvent(
95109
ctx: EmitContext | undefined,
@@ -131,6 +145,9 @@ export function buildMatcherHooks(
131145
warnEvent(ctx, harness, hook.event);
132146
continue;
133147
}
148+
if (typeof hook.command !== "string" && hook.command.powershell) {
149+
warnPowershell(ctx, harness);
150+
}
134151
const inner: Record<string, unknown> = {
135152
type: "command",
136153
command: bashOf(hook.command),

‎packages/agent-plugin-sdk/src/harnesses/opencode.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
emitContextFile,
1414
emitSkillDir,
1515
json,
16+
warnCommandAllowedTools,
17+
warnCommandArgumentHint,
18+
warnSkillAllowedTools,
19+
warnSkillDisableModelInvocation,
1620
} from "./shared.js";
1721
import { toOpenCodeEntry } from "./mcp.js";
1822
import { openCodePluginEntry } from "./tools.js";
@@ -84,6 +88,10 @@ export const opencode: Harness = {
8488
compatibility: "opencode",
8589
metadata: skill.metadata,
8690
});
91+
if (skill.allowedTools?.length)
92+
warnSkillAllowedTools(ctx, "opencode", skill.name);
93+
if (skill.disableModelInvocation)
94+
warnSkillDisableModelInvocation(ctx, "opencode", skill.name);
8795
files.push(...emitSkillDir(skill, frontmatter, "skills"));
8896
}
8997

@@ -95,6 +103,10 @@ export const opencode: Harness = {
95103
description: command.description,
96104
model: command.harness?.opencode?.model,
97105
});
106+
if (command.argumentHint)
107+
warnCommandArgumentHint(ctx, "opencode", command.name);
108+
if (command.allowedTools?.length)
109+
warnCommandAllowedTools(ctx, "opencode", command.name);
98110
files.push(
99111
emitCommandFile(
100112
command.name,

0 commit comments

Comments
 (0)