vskill 0.5.126 → 0.5.128

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 (228) hide show
  1. package/agents.json +1 -1
  2. package/dist/bin.js +0 -0
  3. package/dist/eval-server/api-routes.js +129 -12
  4. package/dist/eval-server/api-routes.js.map +1 -1
  5. package/dist/eval-server/install-engine-routes.js +1 -1
  6. package/dist/eval-server/install-engine-routes.js.map +1 -1
  7. package/dist/eval-server/utils/scan-install-locations.d.ts +18 -0
  8. package/dist/eval-server/utils/scan-install-locations.js +136 -0
  9. package/dist/eval-server/utils/scan-install-locations.js.map +1 -0
  10. package/dist/eval-ui/assets/{CommandPalette-CBrGufxz.js → CommandPalette-D_MdwCki.js} +1 -1
  11. package/dist/eval-ui/assets/{CreateSkillPage-DXrpM1On.js → CreateSkillPage-lLACfsOZ.js} +3 -3
  12. package/dist/eval-ui/assets/{FindSkillsPalette-D99SoJx3.js → FindSkillsPalette-VTksFrSO.js} +2 -2
  13. package/dist/eval-ui/assets/SearchPaletteCore-D49bVafl.js +14 -0
  14. package/dist/eval-ui/assets/{SkillDetailPanel-DzmlOsAc.js → SkillDetailPanel-Cg8lyDAl.js} +1 -1
  15. package/dist/eval-ui/assets/UpdateDropdown-CLt94M3u.js +1 -0
  16. package/dist/eval-ui/assets/{index-BAN03ugM.css → index-C8DXCPPg.css} +1 -1
  17. package/dist/eval-ui/assets/index-fbCfi37A.js +102 -0
  18. package/dist/eval-ui/assets/{skill-url-lxDpBkBU.js → skill-url-DXjZASn-.js} +1 -1
  19. package/dist/eval-ui/index.html +2 -2
  20. package/dist/index.js +0 -0
  21. package/package.json +1 -1
  22. package/dist/agents/agents-registry.test.d.ts +0 -1
  23. package/dist/agents/agents-registry.test.js +0 -248
  24. package/dist/agents/agents-registry.test.js.map +0 -1
  25. package/dist/api/client.test.d.ts +0 -1
  26. package/dist/api/client.test.js +0 -428
  27. package/dist/api/client.test.js.map +0 -1
  28. package/dist/audit/audit-integration.test.d.ts +0 -1
  29. package/dist/audit/audit-integration.test.js +0 -92
  30. package/dist/audit/audit-integration.test.js.map +0 -1
  31. package/dist/audit/audit-llm.test.d.ts +0 -1
  32. package/dist/audit/audit-llm.test.js +0 -110
  33. package/dist/audit/audit-llm.test.js.map +0 -1
  34. package/dist/audit/audit-patterns.test.d.ts +0 -1
  35. package/dist/audit/audit-patterns.test.js +0 -91
  36. package/dist/audit/audit-patterns.test.js.map +0 -1
  37. package/dist/audit/audit-scanner.test.d.ts +0 -1
  38. package/dist/audit/audit-scanner.test.js +0 -112
  39. package/dist/audit/audit-scanner.test.js.map +0 -1
  40. package/dist/audit/audit-types.test.d.ts +0 -1
  41. package/dist/audit/audit-types.test.js +0 -140
  42. package/dist/audit/audit-types.test.js.map +0 -1
  43. package/dist/audit/config.test.d.ts +0 -1
  44. package/dist/audit/config.test.js +0 -44
  45. package/dist/audit/config.test.js.map +0 -1
  46. package/dist/audit/file-discovery.test.d.ts +0 -1
  47. package/dist/audit/file-discovery.test.js +0 -120
  48. package/dist/audit/file-discovery.test.js.map +0 -1
  49. package/dist/audit/fix-suggestions.test.d.ts +0 -1
  50. package/dist/audit/fix-suggestions.test.js +0 -35
  51. package/dist/audit/fix-suggestions.test.js.map +0 -1
  52. package/dist/audit/formatters/json-formatter.test.d.ts +0 -1
  53. package/dist/audit/formatters/json-formatter.test.js +0 -49
  54. package/dist/audit/formatters/json-formatter.test.js.map +0 -1
  55. package/dist/audit/formatters/report-formatter.test.d.ts +0 -1
  56. package/dist/audit/formatters/report-formatter.test.js +0 -51
  57. package/dist/audit/formatters/report-formatter.test.js.map +0 -1
  58. package/dist/audit/formatters/sarif-formatter.test.d.ts +0 -1
  59. package/dist/audit/formatters/sarif-formatter.test.js +0 -71
  60. package/dist/audit/formatters/sarif-formatter.test.js.map +0 -1
  61. package/dist/audit/formatters/terminal-formatter.test.d.ts +0 -1
  62. package/dist/audit/formatters/terminal-formatter.test.js +0 -51
  63. package/dist/audit/formatters/terminal-formatter.test.js.map +0 -1
  64. package/dist/blocklist/blocklist-e2e.test.d.ts +0 -1
  65. package/dist/blocklist/blocklist-e2e.test.js +0 -346
  66. package/dist/blocklist/blocklist-e2e.test.js.map +0 -1
  67. package/dist/blocklist/blocklist.test.d.ts +0 -1
  68. package/dist/blocklist/blocklist.test.js +0 -259
  69. package/dist/blocklist/blocklist.test.js.map +0 -1
  70. package/dist/commands/__tests__/eval-router.test.d.ts +0 -1
  71. package/dist/commands/__tests__/eval-router.test.js +0 -60
  72. package/dist/commands/__tests__/eval-router.test.js.map +0 -1
  73. package/dist/commands/__tests__/eval-serve.test.d.ts +0 -1
  74. package/dist/commands/__tests__/eval-serve.test.js +0 -23
  75. package/dist/commands/__tests__/eval-serve.test.js.map +0 -1
  76. package/dist/commands/add-blocklist-e2e.test.d.ts +0 -1
  77. package/dist/commands/add-blocklist-e2e.test.js +0 -397
  78. package/dist/commands/add-blocklist-e2e.test.js.map +0 -1
  79. package/dist/commands/add-wizard.test.d.ts +0 -1
  80. package/dist/commands/add-wizard.test.js +0 -392
  81. package/dist/commands/add-wizard.test.js.map +0 -1
  82. package/dist/commands/add.test.d.ts +0 -1
  83. package/dist/commands/add.test.js +0 -2365
  84. package/dist/commands/add.test.js.map +0 -1
  85. package/dist/commands/audit.test.d.ts +0 -1
  86. package/dist/commands/audit.test.js +0 -79
  87. package/dist/commands/audit.test.js.map +0 -1
  88. package/dist/commands/blocklist.test.d.ts +0 -1
  89. package/dist/commands/blocklist.test.js +0 -158
  90. package/dist/commands/blocklist.test.js.map +0 -1
  91. package/dist/commands/eval/__tests__/coverage.test.d.ts +0 -1
  92. package/dist/commands/eval/__tests__/coverage.test.js +0 -122
  93. package/dist/commands/eval/__tests__/coverage.test.js.map +0 -1
  94. package/dist/commands/eval/__tests__/generate-all.test.d.ts +0 -1
  95. package/dist/commands/eval/__tests__/generate-all.test.js +0 -133
  96. package/dist/commands/eval/__tests__/generate-all.test.js.map +0 -1
  97. package/dist/commands/eval/__tests__/init.test.d.ts +0 -1
  98. package/dist/commands/eval/__tests__/init.test.js +0 -116
  99. package/dist/commands/eval/__tests__/init.test.js.map +0 -1
  100. package/dist/commands/eval/__tests__/run.test.d.ts +0 -1
  101. package/dist/commands/eval/__tests__/run.test.js +0 -186
  102. package/dist/commands/eval/__tests__/run.test.js.map +0 -1
  103. package/dist/commands/find.test.d.ts +0 -1
  104. package/dist/commands/find.test.js +0 -481
  105. package/dist/commands/find.test.js.map +0 -1
  106. package/dist/commands/marketplace.test.d.ts +0 -1
  107. package/dist/commands/marketplace.test.js +0 -129
  108. package/dist/commands/marketplace.test.js.map +0 -1
  109. package/dist/commands/remove.test.d.ts +0 -1
  110. package/dist/commands/remove.test.js +0 -164
  111. package/dist/commands/remove.test.js.map +0 -1
  112. package/dist/commands/should-skip.test.d.ts +0 -1
  113. package/dist/commands/should-skip.test.js +0 -56
  114. package/dist/commands/should-skip.test.js.map +0 -1
  115. package/dist/commands/submit.test.d.ts +0 -1
  116. package/dist/commands/submit.test.js +0 -83
  117. package/dist/commands/submit.test.js.map +0 -1
  118. package/dist/commands/update.test.d.ts +0 -1
  119. package/dist/commands/update.test.js +0 -250
  120. package/dist/commands/update.test.js.map +0 -1
  121. package/dist/discovery/github-tree.test.d.ts +0 -1
  122. package/dist/discovery/github-tree.test.js +0 -372
  123. package/dist/discovery/github-tree.test.js.map +0 -1
  124. package/dist/eval/__tests__/activation-tester.test.d.ts +0 -1
  125. package/dist/eval/__tests__/activation-tester.test.js +0 -203
  126. package/dist/eval/__tests__/activation-tester.test.js.map +0 -1
  127. package/dist/eval/__tests__/benchmark-history.test.d.ts +0 -1
  128. package/dist/eval/__tests__/benchmark-history.test.js +0 -422
  129. package/dist/eval/__tests__/benchmark-history.test.js.map +0 -1
  130. package/dist/eval/__tests__/benchmark.test.d.ts +0 -1
  131. package/dist/eval/__tests__/benchmark.test.js +0 -94
  132. package/dist/eval/__tests__/benchmark.test.js.map +0 -1
  133. package/dist/eval/__tests__/comparator.test.d.ts +0 -1
  134. package/dist/eval/__tests__/comparator.test.js +0 -282
  135. package/dist/eval/__tests__/comparator.test.js.map +0 -1
  136. package/dist/eval/__tests__/judge.test.d.ts +0 -1
  137. package/dist/eval/__tests__/judge.test.js +0 -122
  138. package/dist/eval/__tests__/judge.test.js.map +0 -1
  139. package/dist/eval/__tests__/llm.test.d.ts +0 -1
  140. package/dist/eval/__tests__/llm.test.js +0 -543
  141. package/dist/eval/__tests__/llm.test.js.map +0 -1
  142. package/dist/eval/__tests__/mcp-detector.test.d.ts +0 -1
  143. package/dist/eval/__tests__/mcp-detector.test.js +0 -180
  144. package/dist/eval/__tests__/mcp-detector.test.js.map +0 -1
  145. package/dist/eval/__tests__/prompt-builder.test.d.ts +0 -1
  146. package/dist/eval/__tests__/prompt-builder.test.js +0 -142
  147. package/dist/eval/__tests__/prompt-builder.test.js.map +0 -1
  148. package/dist/eval/__tests__/schema.test.d.ts +0 -1
  149. package/dist/eval/__tests__/schema.test.js +0 -247
  150. package/dist/eval/__tests__/schema.test.js.map +0 -1
  151. package/dist/eval/__tests__/skill-scanner.test.d.ts +0 -1
  152. package/dist/eval/__tests__/skill-scanner.test.js +0 -228
  153. package/dist/eval/__tests__/skill-scanner.test.js.map +0 -1
  154. package/dist/eval/__tests__/verdict.test.d.ts +0 -1
  155. package/dist/eval/__tests__/verdict.test.js +0 -47
  156. package/dist/eval/__tests__/verdict.test.js.map +0 -1
  157. package/dist/eval-server/__tests__/benchmark-runner.test.d.ts +0 -1
  158. package/dist/eval-server/__tests__/benchmark-runner.test.js +0 -301
  159. package/dist/eval-server/__tests__/benchmark-runner.test.js.map +0 -1
  160. package/dist/eval-server/__tests__/comparison-sse-events.test.d.ts +0 -1
  161. package/dist/eval-server/__tests__/comparison-sse-events.test.js +0 -278
  162. package/dist/eval-server/__tests__/comparison-sse-events.test.js.map +0 -1
  163. package/dist/eval-server/__tests__/sse-helpers.test.d.ts +0 -1
  164. package/dist/eval-server/__tests__/sse-helpers.test.js +0 -128
  165. package/dist/eval-server/__tests__/sse-helpers.test.js.map +0 -1
  166. package/dist/eval-ui/assets/SearchPaletteCore-CnRYmTuv.js +0 -6
  167. package/dist/eval-ui/assets/UpdateDropdown-BsAm4uMW.js +0 -1
  168. package/dist/eval-ui/assets/index-BGQolxHD.js +0 -102
  169. package/dist/installer/canonical.test.d.ts +0 -1
  170. package/dist/installer/canonical.test.js +0 -264
  171. package/dist/installer/canonical.test.js.map +0 -1
  172. package/dist/lockfile/lockfile.test.d.ts +0 -1
  173. package/dist/lockfile/lockfile.test.js +0 -204
  174. package/dist/lockfile/lockfile.test.js.map +0 -1
  175. package/dist/lockfile/project-root.test.d.ts +0 -1
  176. package/dist/lockfile/project-root.test.js +0 -49
  177. package/dist/lockfile/project-root.test.js.map +0 -1
  178. package/dist/marketplace/marketplace.test.d.ts +0 -1
  179. package/dist/marketplace/marketplace.test.js +0 -312
  180. package/dist/marketplace/marketplace.test.js.map +0 -1
  181. package/dist/resolvers/source-resolver.test.d.ts +0 -1
  182. package/dist/resolvers/source-resolver.test.js +0 -104
  183. package/dist/resolvers/source-resolver.test.js.map +0 -1
  184. package/dist/resolvers/url-resolver.test.d.ts +0 -1
  185. package/dist/resolvers/url-resolver.test.js +0 -49
  186. package/dist/resolvers/url-resolver.test.js.map +0 -1
  187. package/dist/scanner/dci-integration.test.d.ts +0 -1
  188. package/dist/scanner/dci-integration.test.js +0 -83
  189. package/dist/scanner/dci-integration.test.js.map +0 -1
  190. package/dist/scanner/patterns.test.d.ts +0 -1
  191. package/dist/scanner/patterns.test.js +0 -832
  192. package/dist/scanner/patterns.test.js.map +0 -1
  193. package/dist/scanner/tier1.test.d.ts +0 -1
  194. package/dist/scanner/tier1.test.js +0 -305
  195. package/dist/scanner/tier1.test.js.map +0 -1
  196. package/dist/security/platform-security.test.d.ts +0 -1
  197. package/dist/security/platform-security.test.js +0 -92
  198. package/dist/security/platform-security.test.js.map +0 -1
  199. package/dist/settings/settings.test.d.ts +0 -1
  200. package/dist/settings/settings.test.js +0 -103
  201. package/dist/settings/settings.test.js.map +0 -1
  202. package/dist/updater/source-fetcher.test.d.ts +0 -1
  203. package/dist/updater/source-fetcher.test.js +0 -192
  204. package/dist/updater/source-fetcher.test.js.map +0 -1
  205. package/dist/utils/__tests__/paths.test.d.ts +0 -1
  206. package/dist/utils/__tests__/paths.test.js +0 -22
  207. package/dist/utils/__tests__/paths.test.js.map +0 -1
  208. package/dist/utils/__tests__/resolve-binary.integration.test.d.ts +0 -1
  209. package/dist/utils/__tests__/resolve-binary.integration.test.js +0 -138
  210. package/dist/utils/__tests__/resolve-binary.integration.test.js.map +0 -1
  211. package/dist/utils/__tests__/resolve-binary.test.d.ts +0 -1
  212. package/dist/utils/__tests__/resolve-binary.test.js +0 -175
  213. package/dist/utils/__tests__/resolve-binary.test.js.map +0 -1
  214. package/dist/utils/__tests__/validation.test.d.ts +0 -1
  215. package/dist/utils/__tests__/validation.test.js +0 -107
  216. package/dist/utils/__tests__/validation.test.js.map +0 -1
  217. package/dist/utils/agent-filter.test.d.ts +0 -1
  218. package/dist/utils/agent-filter.test.js +0 -75
  219. package/dist/utils/agent-filter.test.js.map +0 -1
  220. package/dist/utils/output.test.d.ts +0 -1
  221. package/dist/utils/output.test.js +0 -28
  222. package/dist/utils/output.test.js.map +0 -1
  223. package/dist/utils/project-root.test.d.ts +0 -1
  224. package/dist/utils/project-root.test.js +0 -74
  225. package/dist/utils/project-root.test.js.map +0 -1
  226. package/dist/utils/prompts.test.d.ts +0 -1
  227. package/dist/utils/prompts.test.js +0 -285
  228. package/dist/utils/prompts.test.js.map +0 -1
package/agents.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-04-26T06:02:47.633Z",
3
+ "generatedAt": "2026-04-26T09:56:17.782Z",
4
4
  "agentPrefixes": [
5
5
  ".adal",
6
6
  ".agent",
package/dist/bin.js CHANGED
File without changes
@@ -2,7 +2,7 @@
2
2
  // api-routes.ts -- REST API route handlers for the eval UI
3
3
  // ---------------------------------------------------------------------------
4
4
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
5
- import { execSync } from "node:child_process";
5
+ import { execSync, execFileSync } from "node:child_process";
6
6
  import { join, resolve, dirname, basename } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { sendJson, readBody } from "./router.js";
@@ -1510,20 +1510,54 @@ export function registerRoutes(router, root, projectName) {
1510
1510
  // directly instead of shelling out to whatever `vskill` is on PATH (which
1511
1511
  // may be a different version than the studio is bundling). This guarantees
1512
1512
  // the disk-version reconcile from outdated.ts is applied uniformly.
1513
+ //
1514
+ // 0747 T-002: enrich each row with `installLocations[]`, `localPlugin`,
1515
+ // `localSkill` so the Studio's bell dropdown can render tooltips and route
1516
+ // smart clicks via `revealSkill` instead of guessing local fs identifiers
1517
+ // from the canonical platform name.
1513
1518
  router.get("/api/skills/updates", async (req, res) => {
1514
1519
  try {
1515
1520
  const { getOutdatedJson } = await import("../commands/outdated.js");
1521
+ const { scanSkillInstallLocations } = await import("./utils/scan-install-locations.js");
1516
1522
  const programmatic = await getOutdatedJson();
1517
1523
  if (!programmatic) {
1518
1524
  sendJson(res, [], 200, req);
1519
1525
  return;
1520
1526
  }
1521
- const enriched = programmatic.results.map((r) => ({
1522
- ...r,
1523
- ...(programmatic.pinMap.has(r.name)
1524
- ? { pinned: true, pinnedVersion: programmatic.pinMap.get(r.name) }
1525
- : {}),
1526
- }));
1527
+ // 0747 AC-US4-06 + code-review F-004: per-request memoization keyed by
1528
+ // canonical skill name. Each unique name is scanned exactly once even
1529
+ // if it appears in multiple rows, so the syscall cost is O(unique
1530
+ // names × agents × scopes), not O(rows × agents × scopes). For a
1531
+ // typical /updates response every row has a distinct name, so this
1532
+ // matches the previous one-call-per-row behavior on the happy path
1533
+ // while giving a correct lower bound when names ever duplicate.
1534
+ const scanCache = new Map();
1535
+ const scanOnce = (name) => {
1536
+ const hit = scanCache.get(name);
1537
+ if (hit !== undefined)
1538
+ return hit;
1539
+ const fresh = scanSkillInstallLocations(name, root);
1540
+ scanCache.set(name, fresh);
1541
+ return fresh;
1542
+ };
1543
+ const enriched = programmatic.results.map((r) => {
1544
+ const locations = scanOnce(r.name);
1545
+ const localSkill = r.name.split("/").pop();
1546
+ // Highest-precedence install wins for the local fs pair the click
1547
+ // handler reveals: project > personal > plugin.
1548
+ const precedence = { project: 0, personal: 1, plugin: 2 };
1549
+ const winner = [...locations].sort((a, b) => precedence[a.scope] -
1550
+ precedence[b.scope])[0];
1551
+ return {
1552
+ ...r,
1553
+ ...(programmatic.pinMap.has(r.name)
1554
+ ? { pinned: true, pinnedVersion: programmatic.pinMap.get(r.name) }
1555
+ : {}),
1556
+ installLocations: locations,
1557
+ localSkill,
1558
+ ...(winner?.pluginSlug ? { localPlugin: winner.pluginSlug } : {}),
1559
+ };
1560
+ });
1527
1561
  sendJson(res, enriched, 200, req);
1528
1562
  }
1529
1563
  catch {
@@ -1637,18 +1671,88 @@ export function registerRoutes(router, root, projectName) {
1637
1671
  }
1638
1672
  });
1639
1673
  // T-011: Single-skill update SSE endpoint
1674
+ //
1675
+ // 0747 T-003: optional `?agent=<id>` query param scopes the update to a
1676
+ // single agent's install. The id MUST be in AGENTS_REGISTRY (allowlist) —
1677
+ // it is interpolated into the execSync command, so any non-allowlisted
1678
+ // value is rejected before reaching the shell. Without `?agent`, behavior
1679
+ // is unchanged and `vskill update` does its built-in cross-agent fan-out.
1680
+ // 0747 code-review/grill F-002: validate :skill route params against a
1681
+ // strict slug regex. Without this, a value starting with `--` becomes a
1682
+ // CLI flag instead of a positional arg (e.g. `vskill update --force`
1683
+ // touches every skill including pinned ones). execFileSync prevents
1684
+ // shell injection but Commander still parses argv. Allowed: alphanum,
1685
+ // dot, underscore, hyphen, slash (for canonical owner/repo/skill names).
1686
+ const SKILL_SLUG_RE = /^[a-zA-Z0-9._/-]+$/;
1687
+ const isSafeSkillName = (s) => typeof s === "string" &&
1688
+ s.length > 0 &&
1689
+ s.length <= 200 &&
1690
+ !s.startsWith("-") &&
1691
+ SKILL_SLUG_RE.test(s);
1640
1692
  router.post("/api/skills/:plugin/:skill/update", async (req, res, params) => {
1641
1693
  initSSE(res, req);
1642
1694
  const skillName = params.skill;
1643
- sendSSE(res, "progress", { status: "updating", skill: skillName });
1695
+ // 0747 grill F-002: reject before doing anything so a malformed slug
1696
+ // can never reach Commander as a flag.
1697
+ if (!isSafeSkillName(skillName)) {
1698
+ sendSSE(res, "error", {
1699
+ error: `Invalid skill name: ${skillName}`,
1700
+ skill: skillName,
1701
+ });
1702
+ sendSSEDone(res, { status: "error", skill: skillName });
1703
+ return;
1704
+ }
1705
+ // Parse optional ?agent=<id>
1706
+ let agentId = null;
1644
1707
  try {
1645
- execSync(`vskill update ${skillName}`, {
1708
+ const reqUrl = req.url ?? "";
1709
+ const url = new URL(reqUrl, "http://localhost");
1710
+ const raw = url.searchParams.get("agent");
1711
+ if (raw !== null) {
1712
+ const allowed = AGENTS_REGISTRY.some((a) => a.id === raw);
1713
+ if (!allowed) {
1714
+ sendSSE(res, "error", {
1715
+ error: `Unknown agent id: ${raw}`,
1716
+ skill: skillName,
1717
+ });
1718
+ sendSSEDone(res, { status: "error", skill: skillName });
1719
+ return;
1720
+ }
1721
+ agentId = raw;
1722
+ }
1723
+ }
1724
+ catch {
1725
+ // URL parse error → behave as if ?agent was omitted
1726
+ }
1727
+ sendSSE(res, "progress", {
1728
+ status: "updating",
1729
+ skill: skillName,
1730
+ ...(agentId ? { agent: agentId } : {}),
1731
+ });
1732
+ try {
1733
+ // 0747 code-review F-001: use execFileSync with argv array — never
1734
+ // shell-interpolate user-controlled values. skillName comes from a
1735
+ // route param and could otherwise carry shell metacharacters via
1736
+ // decodeURIComponent. agentId is allowlisted above; we still pass it
1737
+ // as a separate arg for defense-in-depth.
1738
+ const args = ["update", skillName];
1739
+ if (agentId)
1740
+ args.push("--agent", agentId);
1741
+ execFileSync("vskill", args, {
1646
1742
  timeout: 60_000,
1647
1743
  encoding: "utf-8",
1648
1744
  stdio: ["ignore", "pipe", "pipe"],
1649
1745
  });
1650
- sendSSE(res, "progress", { status: "done", skill: skillName });
1651
- sendSSEDone(res, { status: "done", skill: skillName });
1746
+ sendSSE(res, "progress", {
1747
+ status: "done",
1748
+ skill: skillName,
1749
+ ...(agentId ? { agent: agentId } : {}),
1750
+ });
1751
+ sendSSEDone(res, {
1752
+ status: "done",
1753
+ skill: skillName,
1754
+ ...(agentId ? { agent: agentId } : {}),
1755
+ });
1652
1756
  }
1653
1757
  catch (err) {
1654
1758
  sendSSE(res, "error", { error: err.message, skill: skillName });
@@ -1671,8 +1775,21 @@ export function registerRoutes(router, root, projectName) {
1671
1775
  try {
1672
1776
  for (const skill of skills) {
1673
1777
  sendSSE(res, "skill:start", { skill });
1778
+ // 0747 grill F-002: reject body.skills[] entries that could be
1779
+ // misread by Commander as flags (e.g. `--force`).
1780
+ if (!isSafeSkillName(skill)) {
1781
+ sendSSE(res, "skill:error", {
1782
+ skill,
1783
+ error: `Invalid skill name: ${skill}`,
1784
+ });
1785
+ failed++;
1786
+ continue;
1787
+ }
1674
1788
  try {
1675
- execSync(`vskill update ${skill}`, {
1789
+ // 0747 code-review F-001 (defense-in-depth): batch endpoint also
1790
+ // shell-interpolated body.skills[]. Switch to execFileSync argv
1791
+ // form so request-body values can never reach the shell.
1792
+ execFileSync("vskill", ["update", skill], {
1676
1793
  timeout: 60_000,
1677
1794
  encoding: "utf-8",
1678
1795
  stdio: ["ignore", "pipe", "pipe"],