@skillsmith/core 0.11.4 → 0.11.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 (240) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +10 -7
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/api/client.d.ts.map +1 -1
  5. package/dist/src/api/client.js +20 -5
  6. package/dist/src/api/client.js.map +1 -1
  7. package/dist/src/api/client.private-registry.d.ts +80 -0
  8. package/dist/src/api/client.private-registry.d.ts.map +1 -0
  9. package/dist/src/api/client.private-registry.js +97 -0
  10. package/dist/src/api/client.private-registry.js.map +1 -0
  11. package/dist/src/api/client.private-registry.test.d.ts +9 -0
  12. package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
  13. package/dist/src/api/client.private-registry.test.js +127 -0
  14. package/dist/src/api/client.private-registry.test.js.map +1 -0
  15. package/dist/src/api/client.test.js +39 -0
  16. package/dist/src/api/client.test.js.map +1 -1
  17. package/dist/src/api/client.toSkill.test.d.ts +25 -0
  18. package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
  19. package/dist/src/api/client.toSkill.test.js +97 -0
  20. package/dist/src/api/client.toSkill.test.js.map +1 -0
  21. package/dist/src/api/client.token-refresh.d.ts +8 -0
  22. package/dist/src/api/client.token-refresh.d.ts.map +1 -1
  23. package/dist/src/api/client.token-refresh.js +26 -0
  24. package/dist/src/api/client.token-refresh.js.map +1 -1
  25. package/dist/src/api/client.token-refresh.test.js +82 -0
  26. package/dist/src/api/client.token-refresh.test.js.map +1 -1
  27. package/dist/src/api/index.d.ts +2 -0
  28. package/dist/src/api/index.d.ts.map +1 -1
  29. package/dist/src/api/index.js +9 -0
  30. package/dist/src/api/index.js.map +1 -1
  31. package/dist/src/api/security-summary.d.ts +73 -0
  32. package/dist/src/api/security-summary.d.ts.map +1 -0
  33. package/dist/src/api/security-summary.js +88 -0
  34. package/dist/src/api/security-summary.js.map +1 -0
  35. package/dist/src/api/security-summary.test.d.ts +9 -0
  36. package/dist/src/api/security-summary.test.d.ts.map +1 -0
  37. package/dist/src/api/security-summary.test.js +94 -0
  38. package/dist/src/api/security-summary.test.js.map +1 -0
  39. package/dist/src/compatibility/slugs.d.ts +7 -3
  40. package/dist/src/compatibility/slugs.d.ts.map +1 -1
  41. package/dist/src/compatibility/slugs.js +11 -4
  42. package/dist/src/compatibility/slugs.js.map +1 -1
  43. package/dist/src/config/config-atomic-write.d.ts +30 -10
  44. package/dist/src/config/config-atomic-write.d.ts.map +1 -1
  45. package/dist/src/config/config-atomic-write.js +33 -80
  46. package/dist/src/config/config-atomic-write.js.map +1 -1
  47. package/dist/src/config/config-atomic-write.test.d.ts +8 -0
  48. package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
  49. package/dist/src/config/config-atomic-write.test.js +64 -12
  50. package/dist/src/config/config-atomic-write.test.js.map +1 -1
  51. package/dist/src/config/owned-lock.acquire.d.ts +53 -0
  52. package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
  53. package/dist/src/config/owned-lock.acquire.js +140 -0
  54. package/dist/src/config/owned-lock.acquire.js.map +1 -0
  55. package/dist/src/config/owned-lock.claim.d.ts +84 -0
  56. package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
  57. package/dist/src/config/owned-lock.claim.js +310 -0
  58. package/dist/src/config/owned-lock.claim.js.map +1 -0
  59. package/dist/src/config/owned-lock.d.ts +113 -0
  60. package/dist/src/config/owned-lock.d.ts.map +1 -0
  61. package/dist/src/config/owned-lock.js +121 -0
  62. package/dist/src/config/owned-lock.js.map +1 -0
  63. package/dist/src/config/owned-lock.test.d.ts +12 -0
  64. package/dist/src/config/owned-lock.test.d.ts.map +1 -0
  65. package/dist/src/config/owned-lock.test.js +412 -0
  66. package/dist/src/config/owned-lock.test.js.map +1 -0
  67. package/dist/src/config/owned-lock.types.d.ts +78 -0
  68. package/dist/src/config/owned-lock.types.d.ts.map +1 -0
  69. package/dist/src/config/owned-lock.types.js +30 -0
  70. package/dist/src/config/owned-lock.types.js.map +1 -0
  71. package/dist/src/db/createDatabase.d.ts.map +1 -1
  72. package/dist/src/db/createDatabase.js +19 -7
  73. package/dist/src/db/createDatabase.js.map +1 -1
  74. package/dist/src/db/drivers/sqljsDriver.d.ts +8 -0
  75. package/dist/src/db/drivers/sqljsDriver.d.ts.map +1 -1
  76. package/dist/src/db/drivers/sqljsDriver.js +24 -2
  77. package/dist/src/db/drivers/sqljsDriver.js.map +1 -1
  78. package/dist/src/db/migration-runner.d.ts.map +1 -1
  79. package/dist/src/db/migration-runner.js +23 -2
  80. package/dist/src/db/migration-runner.js.map +1 -1
  81. package/dist/src/db/schema.d.ts.map +1 -1
  82. package/dist/src/db/schema.js +29 -3
  83. package/dist/src/db/schema.js.map +1 -1
  84. package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
  85. package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
  86. package/dist/src/embeddings/hnsw-store.exports.js +10 -0
  87. package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
  88. package/dist/src/embeddings/index.d.ts +1 -2
  89. package/dist/src/embeddings/index.d.ts.map +1 -1
  90. package/dist/src/embeddings/index.js +17 -3
  91. package/dist/src/embeddings/index.js.map +1 -1
  92. package/dist/src/embeddings/probe.d.ts.map +1 -1
  93. package/dist/src/embeddings/probe.js +4 -8
  94. package/dist/src/embeddings/probe.js.map +1 -1
  95. package/dist/src/exports/services.d.ts +5 -1
  96. package/dist/src/exports/services.d.ts.map +1 -1
  97. package/dist/src/exports/services.js +27 -0
  98. package/dist/src/exports/services.js.map +1 -1
  99. package/dist/src/index.d.ts +3 -3
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +9 -3
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/install/index.d.ts +2 -2
  104. package/dist/src/install/index.d.ts.map +1 -1
  105. package/dist/src/install/index.js +1 -1
  106. package/dist/src/install/index.js.map +1 -1
  107. package/dist/src/install/paths.d.ts +165 -1
  108. package/dist/src/install/paths.d.ts.map +1 -1
  109. package/dist/src/install/paths.js +254 -1
  110. package/dist/src/install/paths.js.map +1 -1
  111. package/dist/src/install/paths.test.js +174 -2
  112. package/dist/src/install/paths.test.js.map +1 -1
  113. package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
  114. package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
  115. package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
  116. package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
  117. package/dist/src/services/context-words.d.ts +40 -0
  118. package/dist/src/services/context-words.d.ts.map +1 -0
  119. package/dist/src/services/context-words.js +178 -0
  120. package/dist/src/services/context-words.js.map +1 -0
  121. package/dist/src/services/context-words.test.d.ts +2 -0
  122. package/dist/src/services/context-words.test.d.ts.map +1 -0
  123. package/dist/src/services/context-words.test.js +94 -0
  124. package/dist/src/services/context-words.test.js.map +1 -0
  125. package/dist/src/services/recommend-guard.d.ts +28 -0
  126. package/dist/src/services/recommend-guard.d.ts.map +1 -0
  127. package/dist/src/services/recommend-guard.js +34 -0
  128. package/dist/src/services/recommend-guard.js.map +1 -0
  129. package/dist/src/services/recommend-guard.test.d.ts +2 -0
  130. package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
  131. package/dist/src/services/recommend-guard.test.js +28 -0
  132. package/dist/src/services/recommend-guard.test.js.map +1 -0
  133. package/dist/src/services/skill-installation.content.d.ts +96 -0
  134. package/dist/src/services/skill-installation.content.d.ts.map +1 -0
  135. package/dist/src/services/skill-installation.content.js +319 -0
  136. package/dist/src/services/skill-installation.content.js.map +1 -0
  137. package/dist/src/services/skill-installation.helpers.d.ts +32 -1
  138. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  139. package/dist/src/services/skill-installation.helpers.js +62 -7
  140. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  141. package/dist/src/services/skill-installation.helpers.test.js +108 -2
  142. package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
  143. package/dist/src/services/skill-installation.io.d.ts +22 -1
  144. package/dist/src/services/skill-installation.io.d.ts.map +1 -1
  145. package/dist/src/services/skill-installation.io.js +110 -6
  146. package/dist/src/services/skill-installation.io.js.map +1 -1
  147. package/dist/src/services/skill-installation.service.d.ts +22 -1
  148. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  149. package/dist/src/services/skill-installation.service.js +30 -5
  150. package/dist/src/services/skill-installation.service.js.map +1 -1
  151. package/dist/src/services/skill-installation.types.d.ts +42 -1
  152. package/dist/src/services/skill-installation.types.d.ts.map +1 -1
  153. package/dist/src/services/skill-installation.types.js.map +1 -1
  154. package/dist/src/services/skill-manifest.d.ts +21 -0
  155. package/dist/src/services/skill-manifest.d.ts.map +1 -1
  156. package/dist/src/services/skill-manifest.js +51 -6
  157. package/dist/src/services/skill-manifest.js.map +1 -1
  158. package/dist/src/services/skill-manifest.test.d.ts +2 -0
  159. package/dist/src/services/skill-manifest.test.d.ts.map +1 -0
  160. package/dist/src/services/skill-manifest.test.js +202 -0
  161. package/dist/src/services/skill-manifest.test.js.map +1 -0
  162. package/dist/src/services/skill-resolution.d.ts +67 -0
  163. package/dist/src/services/skill-resolution.d.ts.map +1 -0
  164. package/dist/src/services/skill-resolution.js +67 -0
  165. package/dist/src/services/skill-resolution.js.map +1 -0
  166. package/dist/src/services/skill-resolution.test.d.ts +2 -0
  167. package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
  168. package/dist/src/services/skill-resolution.test.js +143 -0
  169. package/dist/src/services/skill-resolution.test.js.map +1 -0
  170. package/dist/src/types/skill.d.ts +10 -0
  171. package/dist/src/types/skill.d.ts.map +1 -1
  172. package/dist/src/types.d.ts +9 -5
  173. package/dist/src/types.d.ts.map +1 -1
  174. package/dist/src/utils/index.d.ts +1 -0
  175. package/dist/src/utils/index.d.ts.map +1 -1
  176. package/dist/src/utils/index.js +4 -0
  177. package/dist/src/utils/index.js.map +1 -1
  178. package/dist/src/utils/quiet-mode.d.ts +21 -0
  179. package/dist/src/utils/quiet-mode.d.ts.map +1 -0
  180. package/dist/src/utils/quiet-mode.js +26 -0
  181. package/dist/src/utils/quiet-mode.js.map +1 -0
  182. package/dist/tests/EmbeddingService.test.js +58 -1
  183. package/dist/tests/EmbeddingService.test.js.map +1 -1
  184. package/dist/tests/SecurityScanner.performance.test.js +5 -2
  185. package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
  186. package/dist/tests/db/database-abstraction.test.js +45 -1
  187. package/dist/tests/db/database-abstraction.test.js.map +1 -1
  188. package/dist/tests/db/migration-runner-race.test.d.ts +2 -0
  189. package/dist/tests/db/migration-runner-race.test.d.ts.map +1 -0
  190. package/dist/tests/db/migration-runner-race.test.js +75 -0
  191. package/dist/tests/db/migration-runner-race.test.js.map +1 -0
  192. package/dist/tests/db/schema-async.test.js +37 -1
  193. package/dist/tests/db/schema-async.test.js.map +1 -1
  194. package/dist/tests/db/sqljsDriver.test.js +63 -1
  195. package/dist/tests/db/sqljsDriver.test.js.map +1 -1
  196. package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
  197. package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
  198. package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
  199. package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
  200. package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
  201. package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
  202. package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
  203. package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
  204. package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
  205. package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
  206. package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
  207. package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
  208. package/dist/tests/schema.test.js +43 -1
  209. package/dist/tests/schema.test.js.map +1 -1
  210. package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
  211. package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
  212. package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
  213. package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
  214. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts +34 -0
  215. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts.map +1 -0
  216. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +246 -0
  217. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -0
  218. package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
  219. package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
  220. package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
  221. package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
  222. package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts +17 -0
  223. package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts.map +1 -0
  224. package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js +112 -0
  225. package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js.map +1 -0
  226. package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
  227. package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
  228. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
  229. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
  230. package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
  231. package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
  232. package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
  233. package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
  234. package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
  235. package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
  236. package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
  237. package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
  238. package/dist/tests/utils/quiet-mode.test.js +42 -0
  239. package/dist/tests/utils/quiet-mode.test.js.map +1 -0
  240. package/package.json +17 -5
@@ -0,0 +1,246 @@
1
+ /**
2
+ * SMI-5980 (Wave 3): writeInstallFiles must route the companion-subagent
3
+ * write through COMPANION_AGENT_TARGETS (install/paths.ts) for the caller's
4
+ * `client`, not a hardcoded `path.join(os.homedir(), '.claude', 'agents')`
5
+ * literal.
6
+ *
7
+ * `homedir()` is mocked to a controlled tmp directory BEFORE `paths.ts` is
8
+ * first imported — COMPANION_AGENT_TARGETS is a module-level constant
9
+ * computed once at import time from `homedir()`, so every assertion below
10
+ * compares against that SAME already-resolved map rather than a second,
11
+ * independently-hardcoded expectation. This also keeps the test from ever
12
+ * writing to a real developer/CI machine's actual home directory.
13
+ *
14
+ * SMI-5982 (Wave 6): antigravity's `COMPANION_AGENT_TARGETS` entry uses a
15
+ * RELATIVE `dir` (resolved against `process.cwd()`, not `homedir()`) — its
16
+ * own dedicated describe block below chdir's into an isolated tmp directory
17
+ * per test (never TEST_HOME, never the real repo checkout) rather than
18
+ * relying on the homedir() mock every other client's assertions use.
19
+ *
20
+ * SMI-5982 code-review fix #1 (BLOCKING, cwd-dependent resolution):
21
+ * `writeInstallFiles()` now resolves antigravity's relative `dir` via
22
+ * `resolveCompanionAgentPath()`'s explicit `baseDir` param (defaulting to
23
+ * `process.cwd()` at the call site, not deferred to whatever `fs` call
24
+ * eventually consumes the path) — so `result.subagentPath` is now an
25
+ * ABSOLUTE path even for antigravity, not the raw relative string. The
26
+ * `it.each` below covers both the omitted-`companionBaseDir` (falls back to
27
+ * `process.cwd()`, which this describe block's chdir controls) and an
28
+ * explicit `companionBaseDir` override.
29
+ *
30
+ * Split out of skill-installation.io.test.ts to stay under the 500-line
31
+ * CI gate (same rationale as skill-installation.io.symlink.test.ts).
32
+ */
33
+ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
34
+ import * as fs from 'fs/promises';
35
+ import * as path from 'path';
36
+ import * as os from 'node:os';
37
+ const { TEST_HOME, homedirMock } = vi.hoisted(() => {
38
+ const home = '/tmp/skillsmith-wif-client-agent-path-test-' + process.pid;
39
+ return { TEST_HOME: home, homedirMock: vi.fn(() => home) };
40
+ });
41
+ vi.mock('node:os', async (importOriginal) => {
42
+ const actual = await importOriginal();
43
+ return { ...actual, homedir: homedirMock };
44
+ });
45
+ import { writeInstallFiles } from '../../../src/services/skill-installation.io.js';
46
+ import { CLIENT_IDS, COMPANION_AGENT_TARGETS } from '../../../src/install/paths.js';
47
+ afterAll(async () => {
48
+ await fs.rm(TEST_HOME, { recursive: true, force: true }).catch(() => { });
49
+ });
50
+ /**
51
+ * Fresh skillsDir/installPath per test so parallel `it.each` iterations
52
+ * (all sharing TEST_HOME) never collide with each other.
53
+ */
54
+ async function freshInstallPath(label) {
55
+ const skillsDir = path.join(TEST_HOME, 'src', label, 'skills');
56
+ await fs.mkdir(skillsDir, { recursive: true });
57
+ return { skillsDir, installPath: path.join(skillsDir, 'my-skill') };
58
+ }
59
+ describe('writeInstallFiles companion-subagent path per client (SMI-5980 regression)', () => {
60
+ beforeEach(() => {
61
+ homedirMock.mockReturnValue(TEST_HOME);
62
+ });
63
+ // SMI-5982 (Wave 6): antigravity is excluded from this generic 'flat'
64
+ // it.each — its dir is a RELATIVE path (resolved against process.cwd(),
65
+ // NOT the mocked homedir()), so writing it here would land outside
66
+ // TEST_HOME, pollute the real working tree, and never get cleaned up by
67
+ // this file's afterAll. It gets its own cwd-isolated describe block below.
68
+ it.each(CLIENT_IDS.filter((id) => id !== 'antigravity'))('writes the companion subagent for client=%s at exactly COMPANION_AGENT_TARGETS[client]', async (client) => {
69
+ const { skillsDir, installPath } = await freshInstallPath(client);
70
+ const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], '---\nname: my-skill-specialist\n---\nbody', client);
71
+ // Every remaining client uses 'flat' mode (copilot's own
72
+ // '{name}.agent.md' pattern, SMI-5980 PR-review finding, included).
73
+ const target = COMPANION_AGENT_TARGETS[client];
74
+ const expectedPath = path.join(target.dir, target.filenamePattern.replace('{name}', 'my-skill'));
75
+ expect(result.subagentPath).toBe(expectedPath);
76
+ expect(await fs.readFile(expectedPath, 'utf8')).toBe('---\nname: my-skill-specialist\n---\nbody');
77
+ });
78
+ it('defaults to the canonical (claude-code) target when client is omitted (regression: unchanged)', async () => {
79
+ const { skillsDir, installPath } = await freshInstallPath('default-client');
80
+ const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], 'subagent body'
81
+ // client omitted — must resolve exactly like the old hardcoded default.
82
+ );
83
+ const expectedPath = path.join(COMPANION_AGENT_TARGETS['claude-code'].dir, 'my-skill-specialist.md');
84
+ expect(result.subagentPath).toBe(expectedPath);
85
+ expect(await fs.readFile(expectedPath, 'utf8')).toBe('subagent body');
86
+ });
87
+ it('claude-code and cursor land in the SAME directory (both default, no independent evidence for cursor)', async () => {
88
+ const claudeCode = await freshInstallPath('cc-vs-cursor-claude');
89
+ const cursor = await freshInstallPath('cc-vs-cursor-cursor');
90
+ const claudeResult = await writeInstallFiles(claudeCode.installPath, claudeCode.skillsDir, 'my-skill', '# hello', [], 'body', 'claude-code');
91
+ const cursorResult = await writeInstallFiles(cursor.installPath, cursor.skillsDir, 'my-skill', '# hello', [], 'body', 'cursor');
92
+ expect(path.dirname(cursorResult.subagentPath ?? '')).toBe(path.dirname(claudeResult.subagentPath ?? ''));
93
+ });
94
+ it('copilot and opencode land at their own real, literal directories (code-review finding: not just "different from claude-code")', async () => {
95
+ const copilot = await freshInstallPath('copilot-diff');
96
+ const opencode = await freshInstallPath('opencode-diff');
97
+ const copilotResult = await writeInstallFiles(copilot.installPath, copilot.skillsDir, 'my-skill', '# hello', [], 'body', 'copilot');
98
+ const opencodeResult = await writeInstallFiles(opencode.installPath, opencode.skillsDir, 'my-skill', '# hello', [], 'body', 'opencode');
99
+ // Independently-constructed literal expected paths (via the SAME mocked
100
+ // TEST_HOME this file's own homedir() mock uses, but NOT derived from
101
+ // COMPANION_AGENT_TARGETS itself) — a code-review pass on an earlier
102
+ // version of this test caught that comparing only against
103
+ // COMPANION_AGENT_TARGETS['claude-code'].dir proves "different from
104
+ // claude-code," not "equals the actual intended copilot/opencode value";
105
+ // a typo in the map (e.g. singular '.copilot/agent') would have still
106
+ // passed the old assertion.
107
+ expect(path.dirname(copilotResult.subagentPath ?? '')).toBe(path.join(TEST_HOME, '.copilot', 'agents'));
108
+ expect(path.dirname(opencodeResult.subagentPath ?? '')).toBe(path.join(TEST_HOME, '.config', 'opencode', 'agents'));
109
+ // Belt-and-suspenders: still confirm both differ from claude-code's dir.
110
+ expect(path.dirname(copilotResult.subagentPath ?? '')).not.toBe(COMPANION_AGENT_TARGETS['claude-code'].dir);
111
+ expect(path.dirname(opencodeResult.subagentPath ?? '')).not.toBe(COMPANION_AGENT_TARGETS['claude-code'].dir);
112
+ });
113
+ it('does not write any companion-subagent file when subagentContent is undefined, regardless of client', async () => {
114
+ const { skillsDir, installPath } = await freshInstallPath('no-subagent');
115
+ const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], undefined, 'copilot');
116
+ expect(result.subagentPath).toBeUndefined();
117
+ });
118
+ });
119
+ /**
120
+ * SMI-5982 (Wave 6): antigravity's companion-agent dir is a RELATIVE path
121
+ * (`.agents/agents`, project-scoped per the plan's Step 1 — no existing
122
+ * global-vs-project distinction to hook into) resolved against
123
+ * `process.cwd()`, unlike every other client's `homedir()`-anchored
124
+ * absolute dir. This describe block is isolated from the rest of the file:
125
+ * each test chdir's into its own fresh tmp directory (never TEST_HOME, and
126
+ * never the real repo checkout) and restores the original cwd in
127
+ * `afterEach` even on assertion failure, so a broken assertion can never
128
+ * leave the whole test PROCESS's cwd corrupted for later tests/files.
129
+ */
130
+ describe('writeInstallFiles companion-subagent path for antigravity (directory-package, SMI-5982 Wave 6)', () => {
131
+ let originalCwd;
132
+ let projectDir;
133
+ beforeEach(async () => {
134
+ originalCwd = process.cwd();
135
+ projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skillsmith-antigravity-cwd-'));
136
+ process.chdir(projectDir);
137
+ });
138
+ afterEach(async () => {
139
+ process.chdir(originalCwd);
140
+ await fs.rm(projectDir, { recursive: true, force: true }).catch(() => { });
141
+ });
142
+ it('writes agent.md inside a skill-named subdirectory when companionBaseDir is passed explicitly (SMI-5982 code-review fix #1)', async () => {
143
+ const { skillsDir, installPath } = await freshInstallPath('antigravity-write');
144
+ const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], '---\nname: my-skill-specialist\n---\nbody', 'antigravity',
145
+ // PR-review follow-up: writeInstallFiles() no longer falls back to
146
+ // process.cwd() itself when companionBaseDir is omitted — an explicit
147
+ // value (here, this describe block's chdir target) is required for
148
+ // directory-package clients. See the dedicated "omitted
149
+ // companionBaseDir" test below for the fail-closed case.
150
+ projectDir);
151
+ // SMI-5982 code-review fix #1: resolveCompanionAgentPath() resolves
152
+ // antigravity's relative dir against the explicit baseDir BEFORE
153
+ // returning — so subagentPath is ABSOLUTE, not the raw relative string.
154
+ // Independently verify the REAL file via an absolute path built from the
155
+ // known projectDir (not derived from result.subagentPath, so a bug that
156
+ // silently mis-resolved the value would still be caught).
157
+ const expectedRelativePath = path.join('.agents', 'agents', 'my-skill', 'agent.md');
158
+ const expectedAbsolutePath = path.join(projectDir, expectedRelativePath);
159
+ expect(result.subagentPath).toBe(expectedAbsolutePath);
160
+ expect(await fs.readFile(expectedAbsolutePath, 'utf8')).toBe('---\nname: my-skill-specialist\n---\nbody');
161
+ // Directory-package structure: agent.md lives INSIDE a subdirectory
162
+ // named after the skill, not directly inside the agents dir.
163
+ expect(path.basename(path.dirname(expectedAbsolutePath))).toBe('my-skill');
164
+ });
165
+ it('omitting companionBaseDir for antigravity now rejects the install instead of silently defaulting to process.cwd() (PR-review follow-up)', async () => {
166
+ const { skillsDir, installPath } = await freshInstallPath('antigravity-omitted-basedir');
167
+ await expect(writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], '---\nname: my-skill-specialist\n---\nbody', 'antigravity'
168
+ // companionBaseDir omitted — must now throw (fail closed), not
169
+ // resolve against process.cwd(). SKILL.md IS written before this
170
+ // point (the companion-agent step runs last), but writeInstallFiles'
171
+ // existing rollback-on-failure logic unwinds installPath entirely on
172
+ // any thrown error — so nothing survives on disk after the throw.
173
+ )).rejects.toThrow(/directory-package mode.*explicit baseDir is required/s);
174
+ await expect(fs.access(installPath)).rejects.toThrow();
175
+ });
176
+ it('writes agent.md resolved against an explicit companionBaseDir, not process.cwd() (SMI-5982 code-review fix #1)', async () => {
177
+ // Deliberately DIFFERENT from projectDir (this block's chdir target) to
178
+ // prove companionBaseDir — not the ambient cwd — drives resolution. This
179
+ // is exactly the MCP-server scenario the fix targets: a long-running
180
+ // process whose own cwd does not track the caller's real project.
181
+ const explicitBaseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skillsmith-antigravity-explicit-basedir-'));
182
+ try {
183
+ const { skillsDir, installPath } = await freshInstallPath('antigravity-explicit-basedir');
184
+ const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], '---\nname: my-skill-specialist\n---\nbody', 'antigravity', explicitBaseDir);
185
+ const expectedAbsolutePath = path.join(explicitBaseDir, '.agents', 'agents', 'my-skill', 'agent.md');
186
+ expect(result.subagentPath).toBe(expectedAbsolutePath);
187
+ expect(result.subagentPath?.startsWith(projectDir)).toBe(false);
188
+ expect(await fs.readFile(expectedAbsolutePath, 'utf8')).toBe('---\nname: my-skill-specialist\n---\nbody');
189
+ }
190
+ finally {
191
+ await fs.rm(explicitBaseDir, { recursive: true, force: true }).catch(() => { });
192
+ }
193
+ });
194
+ it('a safeWriteFile failure on the companion-agent path does not crash, and never force-removes a non-empty per-skill directory', async () => {
195
+ // Reachable, non-mocked failure mode for the LAST step in
196
+ // writeInstallFiles's try block (companion-agent write): a symlink
197
+ // already sitting at the exact target path. safeWriteFile's O_NOFOLLOW
198
+ // open() refuses it (SymlinkError), so the rollback branch added in
199
+ // SMI-5982 runs with subagentPath already assigned. The per-skill
200
+ // directory is NOT empty (the symlink itself is still inside it), so
201
+ // the rollback's rmdir() must be a safe no-op here, never a force-delete
202
+ // — this proves the fix degrades safely rather than that it always
203
+ // reclaims disk space (an EMPTY orphaned directory can only arise from a
204
+ // transient I/O failure between mkdir and the write, not reproducible
205
+ // deterministically without mocking fs internals).
206
+ const { skillsDir, installPath } = await freshInstallPath('antigravity-symlink');
207
+ const agentSkillDir = path.join(projectDir, '.agents', 'agents', 'my-skill');
208
+ await fs.mkdir(agentSkillDir, { recursive: true });
209
+ await fs.symlink('/nonexistent-target', path.join(agentSkillDir, 'agent.md'));
210
+ await expect(writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [], '---\nname: my-skill-specialist\n---\nbody', 'antigravity',
211
+ // PR-review follow-up: explicit now that writeInstallFiles() no
212
+ // longer defaults a missing companionBaseDir to process.cwd() —
213
+ // required so this test still reaches the symlink failure it is
214
+ // actually exercising, rather than the (also-now-thrown)
215
+ // required-baseDir guard.
216
+ projectDir)).rejects.toThrow(/symlink/i);
217
+ // The directory (and its symlink) must still exist — rollback did not
218
+ // blindly force-delete non-empty content.
219
+ const stats = await fs.lstat(agentSkillDir);
220
+ expect(stats.isDirectory()).toBe(true);
221
+ });
222
+ it('rejects a path-traversal skillName ("..") for the companion-agent path and rolls back the already-written SKILL.md (SMI-5982 code-review fix #2)', async () => {
223
+ // resolveCompanionAgentPath()'s own directory-package validation (fix #2)
224
+ // is the last line of defense here: this test passes a malicious
225
+ // skillName distinct from what installPath was built from, exactly the
226
+ // "a future caller bypasses upstream sanitization" scenario the fix's
227
+ // doc comment calls out.
228
+ const { skillsDir, installPath } = await freshInstallPath('antigravity-traversal');
229
+ await expect(writeInstallFiles(installPath, skillsDir, '..', '# hello', [], '---\nname: specialist\n---\nbody', 'antigravity',
230
+ // PR-review follow-up: explicit now that writeInstallFiles() no
231
+ // longer defaults a missing companionBaseDir to process.cwd() —
232
+ // required so this test still reaches the skillName traversal
233
+ // guard it is actually exercising, rather than the
234
+ // (also-now-thrown) required-baseDir guard.
235
+ projectDir)).rejects.toThrow(/Unsafe skill name for directory-package companion path/);
236
+ // Rollback: the existing catch block in writeInstallFiles() already
237
+ // covers this — SKILL.md was written before the throw (subagentPath is
238
+ // resolved last), so it (and installPath itself) must be gone. Nothing
239
+ // was ever created under the companion-agent dir, since the throw
240
+ // happens BEFORE any fs.mkdir/write for that path.
241
+ await expect(fs.access(installPath)).rejects.toThrow();
242
+ const agentsParentDir = path.join(projectDir, '.agents');
243
+ await expect(fs.access(agentsParentDir)).rejects.toThrow();
244
+ });
245
+ });
246
+ //# sourceMappingURL=skill-installation.io.client-agent-path.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.io.client-agent-path.test.js","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.io.client-agent-path.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview writeInstallFiles nested sub-skill path + symlink-safety tests
3
+ * @see SMI-5905 final code review (Sol, GPT-5.6-Sol via NEEDLE) findings #2/#4
4
+ *
5
+ * Split out of skill-installation.io.test.ts (515/500 lines once these tests landed).
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=skill-installation.io.symlink.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.io.symlink.test.d.ts","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.io.symlink.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @fileoverview writeInstallFiles nested sub-skill path + symlink-safety tests
3
+ * @see SMI-5905 final code review (Sol, GPT-5.6-Sol via NEEDLE) findings #2/#4
4
+ *
5
+ * Split out of skill-installation.io.test.ts (515/500 lines once these tests landed).
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import * as fs from 'fs/promises';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ import { writeInstallFiles } from '../../../src/services/skill-installation.io.js';
12
+ /**
13
+ * SMI-5905 Sol final-code-review findings #2/#4: nested sub-skill filenames (e.g.
14
+ * "scripts/run.sh", used by private-registry content installs) previously had no parent
15
+ * directory created (ENOENT on any clean install) and, once fixed, must not silently write
16
+ * through a pre-existing symlink at an intermediate path component.
17
+ */
18
+ describe('writeInstallFiles nested sub-skill paths (SMI-5905 final review #2/#4)', () => {
19
+ it('creates the parent directory for a nested sub-skill filename', async () => {
20
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wif-nested-'));
21
+ try {
22
+ const skillsDir = path.join(root, 'skills');
23
+ await fs.mkdir(skillsDir, { recursive: true });
24
+ const installPath = path.join(skillsDir, 'my-skill');
25
+ const result = await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [{ filename: 'scripts/run.sh', content: 'echo hi' }], undefined);
26
+ expect(await fs.readFile(path.join(installPath, 'scripts', 'run.sh'), 'utf8')).toBe('echo hi');
27
+ expect(result.writtenFiles).toContain(path.join(installPath, 'scripts', 'run.sh'));
28
+ }
29
+ finally {
30
+ await fs.rm(root, { recursive: true, force: true }).catch(() => { });
31
+ }
32
+ });
33
+ it('creates parents for two nested files sharing the same new subdirectory (concurrent-write race)', async () => {
34
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wif-nested-race-'));
35
+ try {
36
+ const skillsDir = path.join(root, 'skills');
37
+ await fs.mkdir(skillsDir, { recursive: true });
38
+ const installPath = path.join(skillsDir, 'my-skill');
39
+ await writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [
40
+ { filename: 'scripts/a.sh', content: 'a' },
41
+ { filename: 'scripts/b.sh', content: 'b' },
42
+ ], undefined);
43
+ expect(await fs.readFile(path.join(installPath, 'scripts', 'a.sh'), 'utf8')).toBe('a');
44
+ expect(await fs.readFile(path.join(installPath, 'scripts', 'b.sh'), 'utf8')).toBe('b');
45
+ }
46
+ finally {
47
+ await fs.rm(root, { recursive: true, force: true }).catch(() => { });
48
+ }
49
+ });
50
+ it('refuses to write through a pre-existing symlinked intermediate directory', async () => {
51
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wif-symlink-'));
52
+ try {
53
+ const skillsDir = path.join(root, 'skills');
54
+ await fs.mkdir(skillsDir, { recursive: true });
55
+ const installPath = path.join(skillsDir, 'my-skill');
56
+ await fs.mkdir(installPath, { recursive: true });
57
+ // A directory OUTSIDE skillsDir the attack would try to write into.
58
+ const outsideTarget = path.join(root, 'outside-target');
59
+ await fs.mkdir(outsideTarget, { recursive: true });
60
+ // Plant "scripts" as a symlink pointing outside skillsDir, as if left behind by a
61
+ // prior force-reinstall or planted directly by an attacker with local write access.
62
+ await fs.symlink(outsideTarget, path.join(installPath, 'scripts'));
63
+ await expect(writeInstallFiles(installPath, skillsDir, 'my-skill', '# hello', [{ filename: 'scripts/run.sh', content: 'malicious' }], undefined)).rejects.toThrow(/symlink/);
64
+ // Nothing was written to the symlink target.
65
+ await expect(fs.access(path.join(outsideTarget, 'run.sh'))).rejects.toThrow();
66
+ }
67
+ finally {
68
+ await fs.rm(root, { recursive: true, force: true }).catch(() => { });
69
+ }
70
+ });
71
+ });
72
+ //# sourceMappingURL=skill-installation.io.symlink.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.io.symlink.test.js","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.io.symlink.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * SMI-5982 code-review fix #1 (BLOCKING, cwd-dependent resolution):
3
+ * `SkillInstallationService`'s `companionBaseDir` constructor param must
4
+ * thread all the way through `install()` -> `writeInstallFiles()` ->
5
+ * `resolveCompanionAgentPath()` so the Antigravity companion-agent file
6
+ * lands under an explicitly-passed root, not whatever `process.cwd()`
7
+ * happens to be at the moment the write actually runs — the exact hazard
8
+ * for a long-running MCP server whose cwd is fixed at launch and does not
9
+ * track the calling editor/agent's real project.
10
+ *
11
+ * `applyOptimization` (skill-installation.helpers.ts) is mocked to force a
12
+ * deterministic `subagentContent` without depending on the real
13
+ * TransformationService's content-shape heuristics — this test is about
14
+ * PATH WIRING, not optimization behavior, which is covered elsewhere.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=skill-installation.service.companion-basedir.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.service.companion-basedir.test.d.ts","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.companion-basedir.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * SMI-5982 code-review fix #1 (BLOCKING, cwd-dependent resolution):
3
+ * `SkillInstallationService`'s `companionBaseDir` constructor param must
4
+ * thread all the way through `install()` -> `writeInstallFiles()` ->
5
+ * `resolveCompanionAgentPath()` so the Antigravity companion-agent file
6
+ * lands under an explicitly-passed root, not whatever `process.cwd()`
7
+ * happens to be at the moment the write actually runs — the exact hazard
8
+ * for a long-running MCP server whose cwd is fixed at launch and does not
9
+ * track the calling editor/agent's real project.
10
+ *
11
+ * `applyOptimization` (skill-installation.helpers.ts) is mocked to force a
12
+ * deterministic `subagentContent` without depending on the real
13
+ * TransformationService's content-shape heuristics — this test is about
14
+ * PATH WIRING, not optimization behavior, which is covered elsewhere.
15
+ */
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
17
+ import * as fs from 'fs/promises';
18
+ import * as path from 'path';
19
+ import * as os from 'node:os';
20
+ vi.mock('../../../src/services/skill-installation.helpers.js', async (importOriginal) => {
21
+ const actual = await importOriginal();
22
+ return {
23
+ ...actual,
24
+ applyOptimization: vi.fn(async (_db, _skillId, skillName, skillMdContent) => ({
25
+ finalSkillContent: skillMdContent,
26
+ subSkillFiles: [],
27
+ subagentContent: `---\nname: ${skillName}-specialist\n---\nCompanion subagent body`,
28
+ claudeMdSnippet: undefined,
29
+ optimizationInfo: {
30
+ optimized: true,
31
+ subSkills: [],
32
+ subagentGenerated: true,
33
+ },
34
+ })),
35
+ };
36
+ });
37
+ import { SkillInstallationService } from '../../../src/services/skill-installation.service.js';
38
+ import { SkillRepository } from '../../../src/repositories/SkillRepository.js';
39
+ import { SkillDependencyRepository } from '../../../src/repositories/SkillDependencyRepository.js';
40
+ import { createTestDatabase } from '../../helpers/database.js';
41
+ const VALID_SKILL_MD = '---\nname: my-skill\ndescription: test skill for SMI-5982 companionBaseDir wiring\n---\n\n' +
42
+ '# My Skill\n\nEnough content here to clear the 100-character minimum validation threshold ' +
43
+ 'the installer enforces before proceeding with the rest of the install pipeline.';
44
+ describe('SkillInstallationService companionBaseDir wiring (SMI-5982 code-review fix #1)', () => {
45
+ let db;
46
+ let originalCwd;
47
+ let projectDir;
48
+ let skillsDir;
49
+ let manifestPath;
50
+ beforeEach(async () => {
51
+ db = await createTestDatabase();
52
+ originalCwd = process.cwd();
53
+ projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skillsmith-service-companion-basedir-'));
54
+ process.chdir(projectDir);
55
+ skillsDir = path.join(projectDir, 'skills');
56
+ manifestPath = path.join(projectDir, 'manifest.json');
57
+ await fs.mkdir(skillsDir, { recursive: true });
58
+ vi.stubGlobal('fetch', vi.fn());
59
+ vi.mocked(fetch).mockImplementation(async (url) => {
60
+ const urlStr = typeof url === 'string' ? url : url.toString();
61
+ if (urlStr.includes('SKILL.md')) {
62
+ return new Response(VALID_SKILL_MD, { status: 200 });
63
+ }
64
+ return new Response('Not found', { status: 404 });
65
+ });
66
+ });
67
+ afterEach(async () => {
68
+ process.chdir(originalCwd);
69
+ db.close();
70
+ vi.restoreAllMocks();
71
+ await fs.rm(projectDir, { recursive: true, force: true }).catch(() => { });
72
+ });
73
+ function buildService(overrides = {}) {
74
+ return new SkillInstallationService({
75
+ db,
76
+ skillRepo: new SkillRepository(db),
77
+ skillDependencyRepo: new SkillDependencyRepository(db),
78
+ skillsDir,
79
+ manifestPath,
80
+ client: 'antigravity',
81
+ ...overrides,
82
+ });
83
+ }
84
+ it('lands the antigravity companion file under an explicit companionBaseDir, not process.cwd()', async () => {
85
+ const explicitBaseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skillsmith-service-explicit-basedir-'));
86
+ try {
87
+ const service = buildService({ companionBaseDir: explicitBaseDir });
88
+ const result = await service.install('https://github.com/owner/my-skill');
89
+ expect(result.success).toBe(true);
90
+ const expectedPath = path.join(explicitBaseDir, '.agents', 'agents', 'my-skill', 'agent.md');
91
+ expect(result.optimization?.subagentPath).toBe(expectedPath);
92
+ await expect(fs.access(expectedPath)).resolves.toBeUndefined();
93
+ // Must NOT have landed under process.cwd() (this describe block's projectDir) —
94
+ // the whole point of the fix.
95
+ expect(result.optimization?.subagentPath?.startsWith(projectDir)).toBe(false);
96
+ }
97
+ finally {
98
+ await fs.rm(explicitBaseDir, { recursive: true, force: true }).catch(() => { });
99
+ }
100
+ });
101
+ it('PR-review follow-up: omitting companionBaseDir now fails the install closed instead of silently defaulting to process.cwd()', async () => {
102
+ const service = buildService(); // companionBaseDir omitted
103
+ const result = await service.install('https://github.com/owner/my-skill');
104
+ expect(result.success).toBe(false);
105
+ expect(result.error).toMatch(/directory-package mode.*explicit baseDir is required/s);
106
+ // Nothing must have been written anywhere under projectDir (this describe
107
+ // block's process.cwd()) — the whole point of the fix is that a missing
108
+ // companionBaseDir can no longer resolve here at all, not even by accident.
109
+ await expect(fs.access(path.join(projectDir, '.agents'))).rejects.toThrow();
110
+ });
111
+ });
112
+ //# sourceMappingURL=skill-installation.service.companion-basedir.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.service.companion-basedir.test.js","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.companion-basedir.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACxE,OAAO,KAAK,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAC5B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAE7B,EAAE,CAAC,IAAI,CAAC,qDAAqD,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IACtF,MAAM,MAAM,GACV,MAAM,cAAc,EAAwE,CAAA;IAC9F,OAAO;QACL,GAAG,MAAM;QACT,iBAAiB,EAAE,EAAE,CAAC,EAAE,CACtB,KAAK,EAAE,GAAY,EAAE,QAAgB,EAAE,SAAiB,EAAE,cAAsB,EAAE,EAAE,CAAC,CAAC;YACpF,iBAAiB,EAAE,cAAc;YACjC,aAAa,EAAE,EAAE;YACjB,eAAe,EAAE,cAAc,SAAS,2CAA2C;YACnF,eAAe,EAAE,SAAS;YAC1B,gBAAgB,EAAE;gBAChB,SAAS,EAAE,IAAI;gBACf,SAAS,EAAE,EAAE;gBACb,iBAAiB,EAAE,IAAI;aACxB;SACF,CAAC,CACH;KACF,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,OAAO,EAAE,wBAAwB,EAAE,MAAM,qDAAqD,CAAA;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,8CAA8C,CAAA;AAC9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,wDAAwD,CAAA;AAClG,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAG9D,MAAM,cAAc,GAClB,4FAA4F;IAC5F,4FAA4F;IAC5F,iFAAiF,CAAA;AAEnF,QAAQ,CAAC,gFAAgF,EAAE,GAAG,EAAE;IAC9F,IAAI,EAAY,CAAA;IAChB,IAAI,WAAmB,CAAA;IACvB,IAAI,UAAkB,CAAA;IACtB,IAAI,SAAiB,CAAA;IACrB,IAAI,YAAoB,CAAA;IAExB,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,EAAE,GAAG,MAAM,kBAAkB,EAAE,CAAA;QAC/B,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;QAC3B,UAAU,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,uCAAuC,CAAC,CAAC,CAAA;QAC9F,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACzB,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;QAC3C,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAA;QACrD,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAE9C,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;QAC/B,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAChD,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAC7D,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChC,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;YACtD,CAAC;YACD,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;QAC1B,EAAE,CAAC,KAAK,EAAE,CAAA;QACV,EAAE,CAAC,eAAe,EAAE,CAAA;QACpB,MAAM,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,SAAS,YAAY,CACnB,YAAgF,EAAE;QAElF,OAAO,IAAI,wBAAwB,CAAC;YAClC,EAAE;YACF,SAAS,EAAE,IAAI,eAAe,CAAC,EAAE,CAAC;YAClC,mBAAmB,EAAE,IAAI,yBAAyB,CAAC,EAAE,CAAC;YACtD,SAAS;YACT,YAAY;YACZ,MAAM,EAAE,aAAa;YACrB,GAAG,SAAS;SACb,CAAC,CAAA;IACJ,CAAC;IAED,EAAE,CAAC,4FAA4F,EAAE,KAAK,IAAI,EAAE;QAC1G,MAAM,eAAe,GAAG,MAAM,EAAE,CAAC,OAAO,CACtC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,sCAAsC,CAAC,CAC/D,CAAA;QACD,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,YAAY,CAAC,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC,CAAA;YAEnE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;YAEzE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACjC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;YAC5F,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YAC5D,MAAM,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;YAC9D,gFAAgF;YAChF,8BAA8B;YAC9B,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/E,CAAC;gBAAS,CAAC;YACT,MAAM,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAChF,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6HAA6H,EAAE,KAAK,IAAI,EAAE;QAC3I,MAAM,OAAO,GAAG,YAAY,EAAE,CAAA,CAAC,2BAA2B;QAE1D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAEzE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,uDAAuD,CAAC,CAAA;QACrF,0EAA0E;QAC1E,wEAAwE;QACxE,4EAA4E;QAC5E,MAAM,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;IAC7E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * SMI-4795: errorCode taxonomy on install failures
3
+ *
4
+ * Each failing return path inside install() must populate `errorCode` with
5
+ * a code from InstallErrorCode. Telemetry (emitInstallEvent) reads this
6
+ * field; without it the install funnel cannot classify failures.
7
+ *
8
+ * Split out of skill-installation.service.test.ts (CLAUDE.md's 500-line
9
+ * file cap) after SMI-5894 Wave 1 added its own multi-client suite there —
10
+ * this block predates that change and is unrelated to it, but moving it
11
+ * out (rather than the newly-added block alone) was the smallest change
12
+ * that got the original file back under the cap.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=skill-installation.service.error-codes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.service.error-codes.test.d.ts","sourceRoot":"","sources":["../../../../tests/unit/services/skill-installation.service.error-codes.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}