@vellumai/assistant 0.12.2-staging.5 → 0.12.2-staging.6

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 (53) hide show
  1. package/Dockerfile +7 -7
  2. package/openapi.yaml +151 -53
  3. package/package.json +5 -4
  4. package/scripts/bundled-plugin-packages.ts +154 -0
  5. package/scripts/generate-bundled-plugin-packages.ts +29 -0
  6. package/scripts/test.ts +15 -13
  7. package/src/__tests__/managed-store.test.ts +121 -0
  8. package/src/__tests__/scaffold-managed-skill-tool.test.ts +88 -0
  9. package/src/calls/__tests__/voice-control-protocol.test.ts +24 -2
  10. package/src/calls/__tests__/voice-session-bridge.test.ts +22 -2
  11. package/src/calls/voice-control-protocol.ts +13 -4
  12. package/src/calls/voice-session-bridge.ts +27 -6
  13. package/src/cli/commands/__tests__/plugins.test.ts +66 -0
  14. package/src/cli/commands/plugins.ts +50 -18
  15. package/src/cli/lib/__tests__/install-from-github.test.ts +67 -0
  16. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +169 -0
  17. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +57 -0
  18. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +14 -0
  19. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +27 -1
  20. package/src/cli/lib/__tests__/plugin-details.test.ts +9 -2
  21. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +20 -0
  22. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +43 -0
  23. package/src/cli/lib/__tests__/search-plugins.test.ts +31 -0
  24. package/src/cli/lib/bundled-plugin-packages.json +4 -0
  25. package/src/cli/lib/bundled-plugin-packages.ts +87 -0
  26. package/src/cli/lib/diff-plugin.ts +1 -1
  27. package/src/cli/lib/inspect-plugin.ts +80 -12
  28. package/src/cli/lib/install-from-github.ts +133 -76
  29. package/src/cli/lib/plugin-catalog-cache.ts +22 -3
  30. package/src/cli/lib/plugin-catalog-local.ts +13 -3
  31. package/src/cli/lib/plugin-catalog-platform.ts +6 -1
  32. package/src/cli/lib/plugin-catalog-resolve.ts +20 -0
  33. package/src/cli/lib/plugin-details.ts +12 -0
  34. package/src/cli/lib/plugin-marketplace.ts +121 -21
  35. package/src/cli/lib/plugin-pin-history.ts +5 -2
  36. package/src/cli/lib/search-plugins.ts +58 -16
  37. package/src/cli/lib/upgrade-plugin.ts +48 -3
  38. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  39. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -6
  40. package/src/daemon/conversation-tool-setup.ts +4 -22
  41. package/src/live-voice/__tests__/live-voice-vad.test.ts +624 -3
  42. package/src/live-voice/__tests__/session-controls.test.ts +18 -0
  43. package/src/live-voice/live-voice-session.ts +575 -53
  44. package/src/live-voice/session-controls.ts +7 -3
  45. package/src/monitoring/plugin-auto-update.ts +6 -0
  46. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  47. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  48. package/src/runtime/routes/__tests__/plugins-routes.test.ts +102 -0
  49. package/src/runtime/routes/plugins-routes.ts +69 -49
  50. package/src/skills/managed-store.ts +98 -32
  51. package/src/tools/skills/find-similar-skills.test.ts +3 -0
  52. package/src/tools/skills/resolve-execute-invocation.ts +24 -0
  53. package/src/tools/skills/scaffold-managed.ts +16 -13
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * Read the curated plugin marketplace manifest from the canonical repo.
3
3
  *
4
- * The manifest at `plugins/marketplace.json` whitelists external
5
- * ecosystem plugins so they appear in `assistant plugins search` / the web
6
- * catalog and become installable by name. Its shape is a subset of the
7
- * Claude Code marketplace manifest
8
- * (https://code.claude.com/docs/en/plugin-marketplaces) — `name` + `owner` +
9
- * a `plugins` array where each entry carries a `name` and a `source`. Only
10
- * `github` sources are resolved today.
4
+ * The manifest at `plugins/marketplace.json` whitelists reviewed plugins so
5
+ * they appear in `assistant plugins search` / the web catalog and become
6
+ * installable by name. Its shape extends the Claude Code marketplace manifest
7
+ * (https://code.claude.com/docs/en/plugin-marketplaces) with packages embedded
8
+ * in the assistant distribution.
11
9
  *
12
- * The manifest is fetched from the repo at a git ref (via the GitHub Contents
13
- * API) rather than bundled into the assistant build — so the whitelist can
14
- * grow without shipping a new release.
10
+ * The manifest is both fetched from the repo at a git ref and bundled into the
11
+ * assistant build. GitHub entries may grow without a new release. Local entries
12
+ * can only resolve when their exact path and version are embedded in that
13
+ * release.
15
14
  * Every external source pins an explicit `ref` that MUST be a full commit SHA:
16
15
  * the fetched code is locked to an immutable revision. Tags and branches are
17
16
  * rejected because they are mutable — an upstream owner could retag/repoint
@@ -60,7 +59,7 @@ const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
60
59
  const COMMIT_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
61
60
 
62
61
  export const githubSourceSchema = z.object({
63
- /** Discriminator. Only GitHub sources are resolved today. */
62
+ /** Discriminator for a reviewed external repository. */
64
63
  source: z.literal("github"),
65
64
  /** `owner/repo` of the external plugin repository. */
66
65
  repo: z.string().regex(REPO_SLUG_RE, "expected an `owner/repo` slug"),
@@ -89,10 +88,53 @@ export const githubSourceSchema = z.object({
89
88
  ),
90
89
  });
91
90
 
91
+ /**
92
+ * A plugin package shipped inside the assistant distribution. The path is a
93
+ * key into the generated bundled-package map, never a path resolved from the
94
+ * current working directory.
95
+ */
96
+ export const localSourceSchema = z.object({
97
+ source: z.literal("local"),
98
+ path: z
99
+ .string()
100
+ .regex(/^plugins\/[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/)
101
+ .refine(
102
+ (path) =>
103
+ !path
104
+ .split(/[/\\]/)
105
+ .some((segment) => segment === "." || segment === ".."),
106
+ "path must be a clean bundled-package path",
107
+ ),
108
+ version: z.string().min(1),
109
+ repo: z.never().optional(),
110
+ ref: z.never().optional(),
111
+ });
112
+
113
+ export const marketplaceSourceSchema = z.discriminatedUnion("source", [
114
+ githubSourceSchema,
115
+ localSourceSchema,
116
+ ]);
117
+
118
+ export const mcpIntegrationSchema = z.object({
119
+ kind: z.literal("mcp"),
120
+ displayName: z.string().min(1),
121
+ documentationUrl: z.string().url(),
122
+ verifiedAt: z.iso.date(),
123
+ verification: z.literal("documentation-only"),
124
+ setup: z.object({
125
+ mode: z.enum(["oauth", "manual"]),
126
+ instructions: z.string().min(1),
127
+ }),
128
+ /** Filename under the web client's bundled integration-image directory. */
129
+ logo: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/),
130
+ /** Existing main OAuth provider used to group a related MCP connection. */
131
+ oauthProvider: z.string().regex(PLUGIN_NAME_RE).optional(),
132
+ });
133
+
92
134
  const marketplaceEntrySchema = z.object({
93
135
  /** Install name. `assistant plugins install <name>` resolves to this entry. */
94
136
  name: z.string().regex(PLUGIN_NAME_RE, "expected a kebab-case install name"),
95
- source: githubSourceSchema,
137
+ source: marketplaceSourceSchema,
96
138
  description: z.string().optional(),
97
139
  /** Free-form grouping hint (e.g. `productivity`). Informational. */
98
140
  category: z.string().optional(),
@@ -111,9 +153,10 @@ const marketplaceEntrySchema = z.object({
111
153
  "expected a short emoji, not a URL or path",
112
154
  )
113
155
  .optional(),
156
+ integration: mcpIntegrationSchema.optional(),
114
157
  });
115
158
 
116
- export const marketplaceManifestSchema = z.object({
159
+ const marketplaceManifestBaseSchema = z.object({
117
160
  name: z.string(),
118
161
  owner: z
119
162
  .object({
@@ -122,14 +165,33 @@ export const marketplaceManifestSchema = z.object({
122
165
  email: z.string().optional(),
123
166
  })
124
167
  .optional(),
168
+ });
169
+
170
+ export const marketplaceManifestSchema = marketplaceManifestBaseSchema.extend({
125
171
  plugins: z.array(marketplaceEntrySchema),
126
172
  });
127
173
 
128
- /** A single whitelisted external plugin entry. */
174
+ const fetchedMarketplaceManifestSchema = marketplaceManifestBaseSchema.extend({
175
+ plugins: z.array(z.unknown()),
176
+ });
177
+
178
+ function readSourceDiscriminator(entry: unknown): unknown {
179
+ if (typeof entry !== "object" || entry === null) {
180
+ return null;
181
+ }
182
+ const source = (entry as Record<string, unknown>).source;
183
+ if (typeof source !== "object" || source === null) {
184
+ return null;
185
+ }
186
+ return (source as Record<string, unknown>).source;
187
+ }
188
+
189
+ /** A single reviewed plugin entry. */
129
190
  export type MarketplaceEntry = z.infer<typeof marketplaceEntrySchema>;
130
191
 
131
192
  /** Concrete GitHub coordinates an entry resolves to for install. */
132
- export interface ResolvedPluginSource {
193
+ export interface ResolvedGitHubPluginSource {
194
+ readonly kind: "github";
133
195
  readonly owner: string;
134
196
  readonly repo: string;
135
197
  /** Directory within the repo holding the plugin root; `""` = repo root. */
@@ -138,6 +200,17 @@ export interface ResolvedPluginSource {
138
200
  readonly ref: string;
139
201
  }
140
202
 
203
+ /** Concrete bundled-package coordinates an entry resolves to for install. */
204
+ export interface ResolvedLocalPluginSource {
205
+ readonly kind: "local";
206
+ readonly path: string;
207
+ readonly version: string;
208
+ }
209
+
210
+ export type ResolvedPluginSource =
211
+ | ResolvedGitHubPluginSource
212
+ | ResolvedLocalPluginSource;
213
+
141
214
  /** Options controlling which marketplace revision to read. */
142
215
  export interface FetchMarketplaceOptions {
143
216
  /** Ref of the canonical repo to read the manifest from. */
@@ -236,23 +309,42 @@ export async function fetchMarketplaceEntries(
236
309
  json = JSON.parse(await res.text());
237
310
  } catch (err) {
238
311
  throw new MarketplaceFetchError(
239
- `Marketplace manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
312
+ `Marketplace manifest is not valid JSON: ${
313
+ err instanceof Error ? err.message : String(err)
314
+ }`,
240
315
  );
241
316
  }
242
317
 
243
- const parsed = marketplaceManifestSchema.safeParse(json);
318
+ const parsed = fetchedMarketplaceManifestSchema.safeParse(json);
244
319
  if (!parsed.success) {
245
320
  throw new MarketplaceFetchError(
246
321
  `Marketplace manifest failed validation: ${parsed.error.message}`,
247
322
  );
248
323
  }
249
-
250
- return parsed.data.plugins;
324
+ const entries: MarketplaceEntry[] = [];
325
+ for (const rawEntry of parsed.data.plugins) {
326
+ const sourceKind = readSourceDiscriminator(rawEntry);
327
+ if (
328
+ typeof sourceKind === "string" &&
329
+ sourceKind !== "github" &&
330
+ sourceKind !== "local"
331
+ ) {
332
+ continue;
333
+ }
334
+ const entry = marketplaceEntrySchema.safeParse(rawEntry);
335
+ if (!entry.success) {
336
+ throw new MarketplaceFetchError(
337
+ `Marketplace manifest failed validation: ${entry.error.message}`,
338
+ );
339
+ }
340
+ entries.push(entry.data);
341
+ }
342
+ return entries;
251
343
  }
252
344
 
253
345
  /**
254
- * Resolve a plugin name to concrete GitHub coordinates using the supplied
255
- * marketplace entries. Returns `null` when no entry claims the name.
346
+ * Resolve a plugin name to its exact source using the supplied marketplace
347
+ * entries. Returns `null` when no entry claims the name.
256
348
  */
257
349
  export function resolveMarketplaceSource(
258
350
  name: string,
@@ -262,8 +354,16 @@ export function resolveMarketplaceSource(
262
354
  if (!entry) {
263
355
  return null;
264
356
  }
357
+ if (entry.source.source === "local") {
358
+ return {
359
+ kind: "local",
360
+ path: entry.source.path,
361
+ version: entry.source.version,
362
+ };
363
+ }
265
364
  const [owner, repo] = entry.source.repo.split("/", 2) as [string, string];
266
365
  return {
366
+ kind: "github",
267
367
  owner,
268
368
  repo,
269
369
  path: entry.source.path ?? "",
@@ -130,7 +130,9 @@ async function listManifestCommits(
130
130
  body = JSON.parse(await res.text());
131
131
  } catch (err) {
132
132
  throw new PluginPinHistoryError(
133
- `Marketplace commit history is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
133
+ `Marketplace commit history is not valid JSON: ${
134
+ err instanceof Error ? err.message : String(err)
135
+ }`,
134
136
  );
135
137
  }
136
138
  if (!Array.isArray(body)) {
@@ -166,7 +168,8 @@ async function pinAtCommit(
166
168
  { fetch: fetchFn },
167
169
  { ref: marketplaceCommit },
168
170
  );
169
- return entries.find((e) => e.name === name)?.source.ref ?? null;
171
+ const source = entries.find((entry) => entry.name === name)?.source;
172
+ return source?.source === "github" ? source.ref : null;
170
173
  }
171
174
 
172
175
  /**
@@ -22,26 +22,49 @@ export interface SearchPluginsDeps {
22
22
  }
23
23
 
24
24
  /** Where a catalog match comes from. */
25
- export type PluginMatchSource = {
26
- readonly kind: "github";
27
- /** `owner/repo` of the external plugin repository. */
28
- readonly repo: string;
29
- /** Directory within the repo, when the plugin is not at the root. */
30
- readonly path?: string;
31
- /** Pinned git ref the plugin is fetched from. */
32
- readonly ref: string;
33
- };
25
+ export type PluginMatchSource =
26
+ | {
27
+ readonly kind: "github";
28
+ /** `owner/repo` of the external plugin repository. */
29
+ readonly repo: string;
30
+ /** Directory within the repo, when the plugin is not at the root. */
31
+ readonly path?: string;
32
+ /** Pinned git ref the plugin is fetched from. */
33
+ readonly ref: string;
34
+ }
35
+ | {
36
+ readonly kind: "local";
37
+ /** Exact package key in the assistant's embedded plugin bundle. */
38
+ readonly path: string;
39
+ readonly version: string;
40
+ readonly repo?: undefined;
41
+ readonly ref?: undefined;
42
+ };
43
+
44
+ export interface McpPluginIntegration {
45
+ readonly kind: "mcp";
46
+ readonly displayName: string;
47
+ readonly documentationUrl: string;
48
+ readonly verifiedAt: string;
49
+ readonly verification: "documentation-only";
50
+ readonly setup: {
51
+ readonly mode: "oauth" | "manual";
52
+ readonly instructions: string;
53
+ };
54
+ readonly logo: string;
55
+ readonly oauthProvider?: string;
56
+ }
34
57
 
35
58
  /** One matching catalog entry. */
36
59
  export interface PluginSearchMatch {
37
60
  /** Install name — `assistant plugins install <name>` resolves to it. */
38
61
  readonly name: string;
39
62
  /**
40
- * Human-readable origin of the entry: a `github:owner/repo[/path]@ref`
41
- * locator for the external plugin source.
63
+ * Human-readable origin: either `github:owner/repo[/path]@ref` or
64
+ * `local:plugins/path@version`.
42
65
  */
43
66
  readonly path: string;
44
- /** Short description, when known (external entries only today). */
67
+ /** Short description, when known. */
45
68
  readonly description?: string;
46
69
  /**
47
70
  * Plugin icon: a curated emoji from the marketplace entry, or an icon URL
@@ -57,6 +80,7 @@ export interface PluginSearchMatch {
57
80
  readonly homepage?: string;
58
81
  /** License identifier, from the curated marketplace entry when present. */
59
82
  readonly license?: string;
83
+ readonly integration?: McpPluginIntegration;
60
84
  /** Discriminated origin, so callers can render/install accordingly. */
61
85
  readonly source: PluginMatchSource;
62
86
  }
@@ -121,14 +145,31 @@ export interface PluginCatalog {
121
145
 
122
146
  /**
123
147
  * Project a marketplace entry onto the catalog match shape, building a
124
- * `github:owner/repo[/path]@ref` locator for display.
148
+ * a source-specific locator for display.
125
149
  *
126
150
  * Shared by the platform fetcher and bundled reader so all catalog sources
127
151
  * project entries identically.
128
152
  */
129
153
  export function marketplaceMatch(entry: MarketplaceEntry): PluginSearchMatch {
130
- const { repo, path, ref } = entry.source;
131
- const locator = `github:${repo}${path ? `/${path}` : ""}@${ref}`;
154
+ const source: PluginMatchSource =
155
+ entry.source.source === "github"
156
+ ? {
157
+ kind: "github",
158
+ repo: entry.source.repo,
159
+ path: entry.source.path,
160
+ ref: entry.source.ref,
161
+ }
162
+ : {
163
+ kind: "local",
164
+ path: entry.source.path,
165
+ version: entry.source.version,
166
+ };
167
+ const locator =
168
+ source.kind === "github"
169
+ ? `github:${source.repo}${source.path ? `/${source.path}` : ""}@${
170
+ source.ref
171
+ }`
172
+ : `local:${source.path}@${source.version}`;
132
173
  return {
133
174
  name: entry.name,
134
175
  path: locator,
@@ -137,7 +178,8 @@ export function marketplaceMatch(entry: MarketplaceEntry): PluginSearchMatch {
137
178
  category: entry.category ?? null,
138
179
  homepage: entry.homepage,
139
180
  license: entry.license,
140
- source: { kind: "github", repo, path, ref },
181
+ integration: entry.integration,
182
+ source,
141
183
  };
142
184
  }
143
185
 
@@ -64,6 +64,7 @@ import {
64
64
  finalizeStagedInstall,
65
65
  type GitRunner,
66
66
  installPlugin,
67
+ type InstallPluginDeps,
67
68
  isFullCommitSha,
68
69
  materializePluginTree,
69
70
  type PluginFetchSource,
@@ -94,6 +95,7 @@ import {
94
95
  type PluginUpgradeStrategy,
95
96
  } from "./plugin-constants.js";
96
97
  import { computeFingerprint, fingerprintsEqual } from "./plugin-fingerprint.js";
98
+ import type { PluginCatalog } from "./search-plugins.js";
97
99
  import { PluginNotInstalledError } from "./uninstall-plugin.js";
98
100
 
99
101
  /**
@@ -167,6 +169,8 @@ export interface UpgradePluginDeps {
167
169
  * upgrades alike; dry runs and no-ops never stage, so it is not invoked.
168
170
  */
169
171
  readonly confirmStaged?: ConfirmStagedInstall;
172
+ readonly materializeLocalPackage?: InstallPluginDeps["materializeLocalPackage"];
173
+ readonly localCatalog?: PluginCatalog;
170
174
  }
171
175
 
172
176
  /** Result of an upgrade attempt. */
@@ -317,7 +321,11 @@ export async function upgradePlugin(
317
321
  try {
318
322
  inspection = await inspectPlugin(
319
323
  { name },
320
- { fetch: deps.fetch, workspacePluginsDir: deps.workspacePluginsDir },
324
+ {
325
+ fetch: deps.fetch,
326
+ workspacePluginsDir: deps.workspacePluginsDir,
327
+ localCatalog: deps.localCatalog,
328
+ },
321
329
  );
322
330
  } catch (err) {
323
331
  if (err instanceof PluginInspectNotFoundError) {
@@ -373,6 +381,19 @@ export async function upgradePlugin(
373
381
  const toTimestamp = remote.committedAt;
374
382
  const provenanceWasUnknown = inspection.status === "unknown-provenance";
375
383
 
384
+ const localSource = local.source;
385
+ const sourceChanged =
386
+ remote.kind === "local"
387
+ ? localSource?.kind !== "local" || localSource.path !== remote.path
388
+ : localSource?.kind === "local";
389
+ if (sourceChanged) {
390
+ throw new PluginNotUpgradableError(
391
+ name,
392
+ "its marketplace source changed; reinstall explicitly with 'plugins install " +
393
+ `${name} --force' to accept the new source`,
394
+ );
395
+ }
396
+
376
397
  if (inspection.status === "up-to-date") {
377
398
  return {
378
399
  name,
@@ -417,6 +438,12 @@ export async function upgradePlugin(
417
438
  strategy === "theirs" ||
418
439
  strategy === "assistant"
419
440
  ) {
441
+ if (remote.kind === "local") {
442
+ throw new PluginMergeBaselineError(
443
+ name,
444
+ "bundled packages do not retain the previous package version needed for a three-way merge",
445
+ );
446
+ }
420
447
  const [remoteOwner, remoteRepo] = remote.repo.split("/");
421
448
  return mergeUpgrade(
422
449
  {
@@ -445,7 +472,19 @@ export async function upgradePlugin(
445
472
  }
446
473
 
447
474
  const result = await installPlugin(
448
- { name, force: true },
475
+ {
476
+ name,
477
+ force: true,
478
+ ...(remote.kind === "local"
479
+ ? {
480
+ trustedSource: {
481
+ kind: "local" as const,
482
+ path: remote.path,
483
+ version: remote.version ?? remote.commit,
484
+ },
485
+ }
486
+ : {}),
487
+ },
449
488
  {
450
489
  fetch: deps.fetch,
451
490
  workspacePluginsDir: deps.workspacePluginsDir,
@@ -454,6 +493,7 @@ export async function upgradePlugin(
454
493
  runInstallDeps: deps.runInstallDeps,
455
494
  beforeSwap: deps.beforeSwap,
456
495
  confirmStaged: deps.confirmStaged,
496
+ materializeLocalPackage: deps.materializeLocalPackage,
457
497
  },
458
498
  );
459
499
 
@@ -716,7 +756,12 @@ async function mergeUpgrade(
716
756
  };
717
757
 
718
758
  const meta = readInstallMeta(local.target);
719
- if (!meta || !meta.commit || !meta.fingerprint) {
759
+ if (
760
+ !meta ||
761
+ meta.source.kind !== "github" ||
762
+ !meta.commit ||
763
+ !meta.fingerprint
764
+ ) {
720
765
  throw new PluginMergeBaselineError(
721
766
  name,
722
767
  "no install commit or fingerprint was recorded (an older or manually-copied install)",
@@ -28,7 +28,7 @@ Do NOT use this skill when the user just wants to run an existing skill. That is
28
28
  ## Capabilities
29
29
 
30
30
  - **Scaffold** a new managed skill with YAML frontmatter and markdown body
31
- - **Edit** an existing skill by scaffolding over it (rewrites the SKILL.md in place)
31
+ - **Edit** an existing skill by scaffolding over it (replaces the body and needs `activation_hints` restated; every other frontmatter field you leave out keeps its current value, and an empty value clears one)
32
32
  - **Delete** an existing managed skill directory
33
33
 
34
34
  Skills created via `scaffold_managed_skill` become available for `skill_load` when a valid top-level `SKILL.md` is written under the skill directory.
@@ -27,15 +27,15 @@
27
27
  },
28
28
  "emoji": {
29
29
  "type": "string",
30
- "description": "Optional emoji icon for the skill."
30
+ "description": "Optional emoji icon for the skill. On an overwrite, omit to keep the current one; pass an empty string to clear it."
31
31
  },
32
32
  "category": {
33
33
  "type": "string",
34
- "description": "Optional single lowercase category for grouping the skill in the Skills UI sidebar. Must be one of the published categories — a value outside this list shows under All with no sidebar bucket, so always pick the closest fit: browsing, calendar, commerce, content, development, email, health, integrations, messaging, productivity, system, voice."
34
+ "description": "Optional single lowercase category for grouping the skill in the Skills UI sidebar. Must be one of the published categories: a value outside this list shows under All with no sidebar bucket, so always pick the closest fit: browsing, calendar, commerce, content, development, email, health, integrations, messaging, productivity, system, voice. On an overwrite, omit to keep the current category; pass an empty string to clear it."
35
35
  },
36
36
  "overwrite": {
37
37
  "type": "boolean",
38
- "description": "Whether to overwrite an existing skill with the same ID (default: false)."
38
+ "description": "Whether to overwrite an existing skill with the same ID (default: false). An overwrite always replaces the body and must restate activation_hints; every other frontmatter field keeps its current value when omitted (emoji, category, includes, avoid_when, and anything else the file carries), and an empty value clears one."
39
39
  },
40
40
  "change_summary": {
41
41
  "type": "string",
@@ -44,17 +44,17 @@
44
44
  "includes": {
45
45
  "type": "array",
46
46
  "items": { "type": "string" },
47
- "description": "Optional list of child skill IDs this skill composes. When this skill is loaded via skill_load, each child's body is inlined after the parent's and the child's tools are projected, so the parent can rely on the child's procedure without re-stating it. Does not affect which skills get selected for a turn."
47
+ "description": "Optional list of child skill IDs this skill composes. When this skill is loaded via skill_load, each child's body is inlined after the parent's and the child's tools are projected, so the parent can rely on the child's procedure without re-stating it. Does not affect which skills get selected for a turn. On an overwrite, omit to keep the current list; pass an empty list to clear it."
48
48
  },
49
49
  "activation_hints": {
50
50
  "type": "array",
51
51
  "items": { "type": "string" },
52
- "description": "Required. 1-4 short trigger phrases describing the situations where this skill should activate, phrased as the observed intent (e.g. \"user asks to deploy staging\"). Surfaced in memory as a \"Use when: …\" clause so the skill is retrievable by intent, not just by name. Scaffolding rewrites the whole SKILL.md, so an overwrite must pass the hints the skill should keep, revised if the procedure changed."
52
+ "description": "Required. 1-4 short trigger phrases describing the situations where this skill should activate, phrased as the observed intent (e.g. \"user asks to deploy staging\"). Surfaced in memory as a \"Use when: …\" clause so the skill is retrievable by intent, not just by name. Required on an overwrite too: the hints should track the body, so restate them, revised if the procedure changed."
53
53
  },
54
54
  "avoid_when": {
55
55
  "type": "array",
56
56
  "items": { "type": "string" },
57
- "description": "Optional situations where this skill should NOT be used. Surfaced in memory as an \"Avoid when: …\" clause to steer retrieval away from the wrong contexts."
57
+ "description": "Optional situations where this skill should NOT be used. Surfaced in memory as an \"Avoid when: …\" clause to steer retrieval away from the wrong contexts. On an overwrite, omit to keep the current list; pass an empty list to clear it."
58
58
  },
59
59
  "files": {
60
60
  "type": "array",
@@ -45,11 +45,8 @@ import {
45
45
  injectActivityField,
46
46
  stripActivityField,
47
47
  } from "../tools/schema-transforms.js";
48
- import {
49
- augmentSkillExecuteError,
50
- recoverSkillExecuteEnvelope,
51
- resolveSkillExecuteInput,
52
- } from "../tools/skills/execute.js";
48
+ import { augmentSkillExecuteError } from "../tools/skills/execute.js";
49
+ import { resolveSkillExecuteInvocation } from "../tools/skills/resolve-execute-invocation.js";
53
50
  import { resolveToolInvocationAlias } from "../tools/tool-name-aliases.js";
54
51
  import type {
55
52
  ProxyApprovalCallback,
@@ -535,23 +532,8 @@ export function createToolExecutor(
535
532
  // risk level, permission checks, hooks, and lifecycle events all fire
536
533
  // with the real tool name.
537
534
  if (executionName === "skill_execute") {
538
- // Recover an envelope the provider wrapped as unparseable when MiniMax's
539
- // coercion failed to JSON-decode a bare-string `input` (see
540
- // recoverSkillExecuteEnvelope), then resolve the inner tool + params.
541
- const envelope = recoverSkillExecuteEnvelope(executionInput);
542
- const rawToolName =
543
- typeof envelope.tool === "string" ? envelope.tool : "";
544
- const innerSchema = rawToolName
545
- ? getTool(rawToolName)?.input_schema
546
- : undefined;
547
- const rawToolInput = resolveSkillExecuteInput(envelope, innerSchema);
548
-
549
- // Clone to avoid mutating shared input objects
550
- const { name: toolName, input: toolInput } = resolveToolInvocationAlias(
551
- rawToolName,
552
- { ...rawToolInput },
553
- ctx.allowedToolNames,
554
- );
535
+ const { name: toolName, input: toolInput } =
536
+ resolveSkillExecuteInvocation(executionInput, ctx.allowedToolNames);
555
537
 
556
538
  if (!toolName) {
557
539
  return {