cue-ai 0.9.0 → 0.9.2

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 (310) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +82 -33
  3. package/bin/cue-review-progress +107 -0
  4. package/bin/cue-review-watch +98 -0
  5. package/dist/cue.js +7352 -3744
  6. package/package.json +16 -5
  7. package/profiles/_types.ts +9 -0
  8. package/profiles/backend/profile.yaml +2 -0
  9. package/profiles/blog-writer/profile.yaml +10 -0
  10. package/profiles/browser/profile.yaml +9 -2
  11. package/profiles/builder/profile.yaml +3 -6
  12. package/profiles/career/profile.yaml +13 -2
  13. package/profiles/claude-api/profile.yaml +1 -1
  14. package/profiles/commerce/profile.yaml +27 -3
  15. package/profiles/core/logo.png +0 -0
  16. package/profiles/core/profile.yaml +62 -2
  17. package/profiles/dash-merge-test/profile.yaml +109 -0
  18. package/profiles/designer/profile.yaml +2 -0
  19. package/profiles/designer-medusa-next/profile.yaml +4 -1
  20. package/profiles/designer-medusa-vite/profile.yaml +4 -1
  21. package/profiles/docs-writer/profile.yaml +3 -1
  22. package/profiles/eu-tender-research/README.md +48 -0
  23. package/profiles/eu-tender-research/logo.png +0 -0
  24. package/profiles/eu-tender-research/profile.yaml +108 -0
  25. package/profiles/finance/logo.png +0 -0
  26. package/profiles/finance/profile.yaml +46 -0
  27. package/profiles/frontend/profile.yaml +5 -9
  28. package/profiles/growth/profile.yaml +2 -3
  29. package/profiles/gstack/profile.yaml +15 -0
  30. package/profiles/higgsfield/profile.yaml +3 -0
  31. package/profiles/hyperframes/logo.png +0 -0
  32. package/profiles/hyperframes/profile.yaml +59 -0
  33. package/profiles/improver/profile.yaml +88 -0
  34. package/profiles/marketing/profile.yaml +0 -3
  35. package/profiles/medusa-dev/profile.yaml +2 -0
  36. package/profiles/medusa-next/profile.yaml +2 -3
  37. package/profiles/medusa-vite/profile.yaml +2 -3
  38. package/profiles/n8n/logo.png +0 -0
  39. package/profiles/n8n/profile.yaml +50 -0
  40. package/profiles/nextjs/profile.yaml +2 -3
  41. package/profiles/ops/profile.yaml +2 -0
  42. package/profiles/postizz/profile.yaml +13 -3
  43. package/profiles/python/profile.yaml +3 -0
  44. package/profiles/research/profile.yaml +3 -1
  45. package/profiles/schema.json +10 -0
  46. package/profiles/secops/profile.yaml +2 -0
  47. package/profiles/seo/profile.yaml +56 -0
  48. package/profiles/skill-writer/profile.yaml +8 -0
  49. package/profiles/ssh/profile.yaml +32 -0
  50. package/profiles/strapi/logo.png +0 -0
  51. package/profiles/strapi/profile.yaml +45 -0
  52. package/profiles/stripe/logo.png +0 -0
  53. package/profiles/stripe/profile.yaml +1 -0
  54. package/profiles/supabase/logo.png +0 -0
  55. package/profiles/supabase/profile.yaml +85 -0
  56. package/profiles/vercel/logo.png +0 -0
  57. package/profiles/vercel/profile.yaml +25 -1
  58. package/profiles/vite/profile.yaml +4 -3
  59. package/profiles/web-frontend-base/profile.yaml +5 -4
  60. package/profiles/webshop/profile.yaml +23 -5
  61. package/profiles/x-growth-bot/profile.yaml +44 -0
  62. package/resources/icons/generate-icons.py +128 -2
  63. package/resources/mcps/configs/claude.sanitized.json +42 -0
  64. package/resources/mcps/configs/codex.sanitized.json +7 -0
  65. package/resources/skills/skills/career/resume-version-manager/SKILL.md +351 -0
  66. package/resources/skills/skills/career/salary-negotiation-prep/SKILL.md +378 -0
  67. package/resources/skills/skills/content/pdf/SKILL.md +2 -0
  68. package/resources/skills/skills/content/postiz-cards/SKILL.md +48 -0
  69. package/resources/skills/skills/content/postiz-cards/scripts/analytics.sh +38 -0
  70. package/resources/skills/skills/content/postiz-cards/scripts/card.sh +42 -0
  71. package/resources/skills/skills/content/postiz-cards/scripts/lint.py +38 -0
  72. package/resources/skills/skills/design/headless-gif-demo/SKILL.md +1 -1
  73. package/resources/skills/skills/design/readme-svg-design/SKILL.md +1 -1
  74. package/resources/skills/skills/eu-funding/grant-outreach/SKILL.md +70 -0
  75. package/resources/skills/skills/eu-funding/hu-grant-finder/SKILL.md +114 -0
  76. package/resources/skills/skills/eu-funding/hu-grant-finder/evals.md +26 -0
  77. package/resources/skills/skills/eu-funding/ted-tender-search/SKILL.md +80 -0
  78. package/resources/skills/skills/eu-funding/ted-tender-search/evals.md +26 -0
  79. package/resources/skills/skills/eu-funding/ted-tender-search/scripts/ted-search.sh +46 -0
  80. package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +1 -1
  81. package/resources/skills/skills/github/gx-agents/SKILL.md +96 -0
  82. package/resources/skills/skills/gstack/design-shotgun/SKILL.md +1 -1
  83. package/resources/skills/skills/marketing/ab-test-analyzer/SKILL.md +1 -1
  84. package/resources/skills/skills/marketing/ab-test-setup-and-analysis/SKILL.md +1 -1
  85. package/resources/skills/skills/marketing/account-structure-review/SKILL.md +1 -1
  86. package/resources/skills/skills/marketing/ad-copy-variant-generator/SKILL.md +1 -1
  87. package/resources/skills/skills/marketing/ad-extension-audit/SKILL.md +1 -1
  88. package/resources/skills/skills/marketing/ad-spend-allocator/SKILL.md +1 -1
  89. package/resources/skills/skills/marketing/anomaly-detection/SKILL.md +1 -1
  90. package/resources/skills/skills/marketing/attribution-model-comparison/SKILL.md +1 -1
  91. package/resources/skills/skills/marketing/audience-overlap-analysis/SKILL.md +7 -1
  92. package/resources/skills/skills/marketing/bid-strategy-recommendations/SKILL.md +7 -1
  93. package/resources/skills/skills/marketing/budget-scenario-planner/SKILL.md +6 -1
  94. package/resources/skills/skills/marketing/campaign-naming-convention-builder/SKILL.md +7 -1
  95. package/resources/skills/skills/marketing/channel-mix-optimizer/SKILL.md +7 -1
  96. package/resources/skills/skills/marketing/client-report-narratives/SKILL.md +6 -1
  97. package/resources/skills/skills/marketing/competitor-creative-analysis/SKILL.md +1 -1
  98. package/resources/skills/skills/marketing/competitor-teardown/SKILL.md +1 -1
  99. package/resources/skills/skills/marketing/content-repurposer/SKILL.md +1 -1
  100. package/resources/skills/skills/marketing/conversion-path-analysis/SKILL.md +1 -1
  101. package/resources/skills/skills/marketing/cpa-diagnostics/SKILL.md +1 -1
  102. package/resources/skills/skills/marketing/creative-fatigue-detection/SKILL.md +1 -1
  103. package/resources/skills/skills/marketing/day-hour-performance-breakdown/SKILL.md +1 -1
  104. package/resources/skills/skills/marketing/device-performance-split/SKILL.md +1 -1
  105. package/resources/skills/skills/marketing/e2e-seo-assistant/SKILL.md +1 -1
  106. package/resources/skills/skills/marketing/email-sequence-writer/SKILL.md +1 -1
  107. package/resources/skills/skills/marketing/frequency-cap-recommendations/SKILL.md +1 -1
  108. package/resources/skills/skills/marketing/geo-performance-analysis/SKILL.md +1 -1
  109. package/resources/skills/skills/marketing/google-ads-audit/SKILL.md +1 -1
  110. package/resources/skills/skills/marketing/icp-research-assistant/SKILL.md +1 -1
  111. package/resources/skills/skills/marketing/keyword-cannibalization-check/SKILL.md +1 -1
  112. package/resources/skills/skills/marketing/landing-page-audit/SKILL.md +1 -1
  113. package/resources/skills/skills/marketing/landing-page-audit-quick/SKILL.md +1 -1
  114. package/resources/skills/skills/marketing/linkedin-ads-audit/SKILL.md +1 -1
  115. package/resources/skills/skills/marketing/meta-ads-audit/SKILL.md +1 -1
  116. package/resources/skills/skills/marketing/pacing-monitor/SKILL.md +1 -1
  117. package/resources/skills/skills/marketing/performance-benchmarking/SKILL.md +1 -1
  118. package/resources/skills/skills/marketing/programmatic-seo-builder/SKILL.md +1 -1
  119. package/resources/skills/skills/marketing/quality-score-breakdown/SKILL.md +1 -1
  120. package/resources/skills/skills/marketing/reddit-ads-audit/SKILL.md +1 -1
  121. package/resources/skills/skills/marketing/retargeting-window-analysis/SKILL.md +1 -1
  122. package/resources/skills/skills/marketing/roas-forecasting/SKILL.md +1 -1
  123. package/resources/skills/skills/marketing/search-term-mining/SKILL.md +1 -1
  124. package/resources/skills/skills/marketing/utm-tracking-generator/SKILL.md +1 -1
  125. package/resources/skills/skills/marketing/wasted-spend-finder/SKILL.md +1 -1
  126. package/resources/skills/skills/marketing/weekly-account-summary/SKILL.md +1 -1
  127. package/resources/skills/skills/meta/awesome-list-submit/SKILL.md +4 -4
  128. package/resources/skills/skills/meta/cue-dashboard/SKILL.md +109 -0
  129. package/resources/skills/skills/meta/cue-developer/SKILL.md +161 -0
  130. package/resources/skills/skills/meta/cue-developer/evals/evals.json +57 -0
  131. package/resources/skills/skills/meta/cue-developer/references/architecture.md +65 -0
  132. package/resources/skills/skills/meta/cue-developer/references/build_and_test.md +72 -0
  133. package/resources/skills/skills/meta/cue-developer/references/contributing.md +75 -0
  134. package/resources/skills/skills/meta/cue-developer/references/conventions.md +57 -0
  135. package/resources/skills/skills/meta/cue-developer/references/first_time_setup.md +51 -0
  136. package/resources/skills/skills/meta/cue-developer/references/skill_and_mcp_authoring.md +84 -0
  137. package/resources/skills/skills/meta/cue-developer/references/troubleshooting.md +42 -0
  138. package/resources/skills/skills/meta/delegation-check/SKILL.md +148 -0
  139. package/resources/skills/skills/meta/delegation-check/specs/scan-algorithm.md +125 -0
  140. package/resources/skills/skills/meta/delegation-check/specs/separation-rules.md +190 -0
  141. package/resources/skills/skills/meta/focus/SKILL.md +62 -0
  142. package/resources/skills/skills/meta/help/SKILL.md +1 -1
  143. package/resources/skills/skills/meta/integrity-tags/SKILL.md +2 -0
  144. package/resources/skills/skills/meta/next-steps/SKILL.md +124 -0
  145. package/resources/skills/skills/meta/next-steps/evals/eval-set.json +92 -0
  146. package/resources/skills/skills/meta/profile-from-docs/SKILL.md +141 -0
  147. package/resources/skills/skills/meta/ralph-loop/SKILL.md +83 -0
  148. package/resources/skills/skills/meta/ralph-loop/scripts/loop.sh +73 -0
  149. package/resources/skills/skills/meta/skill-simplify/SKILL.md +136 -0
  150. package/resources/skills/skills/meta/skill-simplify/phases/01-analysis.md +173 -0
  151. package/resources/skills/skills/meta/skill-simplify/phases/02-optimize.md +104 -0
  152. package/resources/skills/skills/meta/skill-simplify/phases/03-check.md +145 -0
  153. package/resources/skills/skills/meta/smart-loader/scripts/smart-lookup.sh +13 -4
  154. package/resources/skills/skills/meta/verify-council/SKILL.md +182 -0
  155. package/resources/skills/skills/meta/verify-council/references/lane-prompts.md +103 -0
  156. package/resources/skills/skills/meta/verify-council/references/workflow.js +217 -0
  157. package/resources/skills/skills/nvidia/aiq-research/SKILL.md +1 -1
  158. package/resources/skills/skills/nvidia/cuopt-developer/SKILL.md +16 -1
  159. package/resources/skills/skills/nvidia/cuopt-developer/resources/contributing.md +2 -2
  160. package/resources/skills/skills/nvidia/cuopt-developer/resources/numerical_debugging.md +128 -0
  161. package/resources/skills/skills/nvidia/cuopt-developer/resources/python_bindings.md +2 -9
  162. package/resources/skills/skills/nvidia/cuopt-developer/resources/vrp_skills.md +166 -0
  163. package/resources/skills/skills/nvidia/cuopt-install/SKILL.md +2 -10
  164. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/SKILL.md +3 -23
  165. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/resources/examples.md +40 -20
  166. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-python/SKILL.md +5 -1
  167. package/resources/skills/skills/nvidia/skill-evolution/SKILL.md +4 -5
  168. package/resources/skills/skills/research/trendradar/SKILL.md +1 -1
  169. package/resources/skills/skills/ssh/ssh-config/SKILL.md +94 -0
  170. package/resources/skills/skills/ssh/ssh-copy/SKILL.md +92 -0
  171. package/resources/skills/skills/ssh/ssh-harden/SKILL.md +108 -0
  172. package/resources/skills/skills/ssh/ssh-keys/SKILL.md +82 -0
  173. package/resources/skills/skills/ssh/ssh-paste-image/LICENSE +28 -0
  174. package/resources/skills/skills/ssh/ssh-paste-image/SKILL.md +149 -0
  175. package/resources/skills/skills/ssh/ssh-paste-image/scripts/build.sh +29 -0
  176. package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/go.mod +3 -0
  177. package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/main.go +79 -0
  178. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/ccimgd.service +12 -0
  179. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/com.ccimgd.plist +20 -0
  180. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/go.mod +3 -0
  181. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/main.go +98 -0
  182. package/resources/skills/skills/ssh/ssh-tunnel/SKILL.md +96 -0
  183. package/resources/skills/skills/strapi/building-with-strapi/SKILL.md +112 -0
  184. package/resources/skills/skills/strapi/strapi-cli/SKILL.md +93 -0
  185. package/resources/skills/skills/strapi/strapi-content-api/SKILL.md +115 -0
  186. package/resources/skills/skills/strapi/strapi-deploy/SKILL.md +89 -0
  187. package/resources/skills/skills/strapi/strapi-mcp-setup/SKILL.md +101 -0
  188. package/resources/skills/skills/strapi/strapi-plugins/SKILL.md +97 -0
  189. package/resources/skills/skills/tools/context7/SKILL.md +101 -0
  190. package/resources/skills/skills/tools/opensrc/SKILL.md +1 -1
  191. package/resources/skills/skills/tools/portless/SKILL.md +186 -0
  192. package/resources/skills/skills/xbot/operate/SKILL.md +229 -0
  193. package/src/commands/_index.ts +8 -0
  194. package/src/commands/ai-score.e2e.test.ts +11 -4
  195. package/src/commands/ai.ts +3 -4
  196. package/src/commands/auto-detect.ts +1 -1
  197. package/src/commands/cli.test.ts +1 -2
  198. package/src/commands/cli.ts +1 -1
  199. package/src/commands/cloud.ts +1 -1
  200. package/src/commands/current.ts +1 -4
  201. package/src/commands/dash.test.ts +110 -0
  202. package/src/commands/dash.ts +194 -0
  203. package/src/commands/dashboard.ts +26 -0
  204. package/src/commands/diff.ts +1 -1
  205. package/src/commands/discover.test.ts +1 -1
  206. package/src/commands/discover.ts +90 -40
  207. package/src/commands/doctor.test.ts +58 -0
  208. package/src/commands/doctor.ts +79 -3
  209. package/src/commands/eval-behavior.ts +1 -1
  210. package/src/commands/eval.ts +2 -2
  211. package/src/commands/evolve.ts +4 -3
  212. package/src/commands/failures.test.ts +1 -1
  213. package/src/commands/features-batch1.test.ts +6 -1
  214. package/src/commands/icon.ts +1 -5
  215. package/src/commands/import-profile.ts +1 -1
  216. package/src/commands/init.ts +50 -7
  217. package/src/commands/install-sh.e2e.test.ts +65 -0
  218. package/src/commands/launch-handoff.e2e.test.ts +88 -0
  219. package/src/commands/launch.e2e.test.ts +8 -1
  220. package/src/commands/launch.test.ts +29 -0
  221. package/src/commands/launch.ts +185 -131
  222. package/src/commands/lock.ts +0 -1
  223. package/src/commands/marketplace.ts +0 -4
  224. package/src/commands/materialize.ts +1 -1
  225. package/src/commands/mem.ts +341 -0
  226. package/src/commands/optimizer.ts +0 -3
  227. package/src/commands/playground.ts +1 -2
  228. package/src/commands/profile-draft-skill.ts +1 -1
  229. package/src/commands/replay-whatif.ts +1 -6
  230. package/src/commands/score.ts +2 -2
  231. package/src/commands/security.test.ts +88 -0
  232. package/src/commands/security.ts +74 -28
  233. package/src/commands/shell.test.ts +65 -4
  234. package/src/commands/shell.ts +67 -7
  235. package/src/commands/skills-test.ts +0 -1
  236. package/src/commands/skills.ts +28 -2
  237. package/src/commands/sources.ts +1 -2
  238. package/src/commands/status.ts +2 -6
  239. package/src/commands/submit-profile.ts +1 -1
  240. package/src/commands/suggest.ts +35 -10
  241. package/src/commands/trigger-gaps.test.ts +50 -0
  242. package/src/commands/trigger-gaps.ts +63 -29
  243. package/src/commands/update.ts +1 -1
  244. package/src/commands/validate.ts +16 -4
  245. package/src/commands/watch-live.ts +1 -1
  246. package/src/commands/workspace.ts +1 -1
  247. package/src/index.ts +26 -10
  248. package/src/lib/active-sessions.ts +1 -1
  249. package/src/lib/agent-adapters.test.ts +100 -0
  250. package/src/lib/agent-adapters.ts +2 -2
  251. package/src/lib/analytics.test.ts +88 -0
  252. package/src/lib/analytics.ts +82 -1
  253. package/src/lib/auto-detect.test.ts +10 -4
  254. package/src/lib/auto-detect.ts +19 -23
  255. package/src/lib/brand-icons.ts +0 -1
  256. package/src/lib/cache.ts +2 -3
  257. package/src/lib/claude-mem-env.test.ts +148 -0
  258. package/src/lib/claude-mem-env.ts +172 -0
  259. package/src/lib/combo-history.test.ts +53 -0
  260. package/src/lib/combo-history.ts +83 -0
  261. package/src/lib/companion-detect.test.ts +108 -0
  262. package/src/lib/companion-detect.ts +140 -0
  263. package/src/lib/companion-fetch.ts +4 -6
  264. package/src/lib/conditional-skills.test.ts +1 -1
  265. package/src/lib/config-paths.test.ts +53 -0
  266. package/src/lib/config-paths.ts +33 -0
  267. package/src/lib/dashboard-server.test.ts +351 -0
  268. package/src/lib/dashboard-server.ts +1476 -27
  269. package/src/lib/debug-log.test.ts +66 -0
  270. package/src/lib/debug-log.ts +45 -0
  271. package/src/lib/mcp-catalog.test.ts +102 -0
  272. package/src/lib/mcp-catalog.ts +193 -0
  273. package/src/lib/pair-suggestions.test.ts +111 -0
  274. package/src/lib/pair-suggestions.ts +98 -5
  275. package/src/lib/permissions.test.ts +76 -0
  276. package/src/lib/permissions.ts +125 -0
  277. package/src/lib/picker.test.ts +1106 -1
  278. package/src/lib/picker.ts +1230 -142
  279. package/src/lib/plugin-discovery.ts +126 -0
  280. package/src/lib/pr-poster.ts +1 -1
  281. package/src/lib/pr-throttle.ts +2 -6
  282. package/src/lib/profile-linter.test.ts +67 -1
  283. package/src/lib/profile-linter.ts +59 -14
  284. package/src/lib/profile-loader.test.ts +21 -0
  285. package/src/lib/profile-loader.ts +22 -3
  286. package/src/lib/profile-metrics.ts +2 -6
  287. package/src/lib/profile-names.test.ts +58 -0
  288. package/src/lib/repos.test.ts +57 -0
  289. package/src/lib/repos.ts +167 -0
  290. package/src/lib/resolver-npx.ts +10 -1
  291. package/src/lib/runtime-materializer.test.ts +200 -3
  292. package/src/lib/runtime-materializer.ts +129 -20
  293. package/src/lib/shared-profiles.ts +2 -3
  294. package/src/lib/skill-clis.test.ts +113 -0
  295. package/src/lib/skill-clis.ts +232 -0
  296. package/src/lib/skill-dependencies.ts +9 -1
  297. package/src/lib/skill-deps.ts +1 -1
  298. package/src/lib/skill-linter.ts +1 -1
  299. package/src/lib/skill-quality.ts +0 -1
  300. package/src/lib/skill-sandbox.test.ts +1 -1
  301. package/src/lib/skills-lock.test.ts +1 -1
  302. package/src/lib/telemetry-consent.ts +3 -5
  303. package/src/lib/telemetry-report.test.ts +2 -2
  304. package/src/lib/token-budget.ts +111 -0
  305. package/src/lib/trigger-gaps.test.ts +70 -0
  306. package/src/lib/trigger-gaps.ts +48 -6
  307. package/src/lib/tui/data.ts +1 -5
  308. package/src/lib/workflow-store.ts +150 -0
  309. package/src/lib/workspace-secrets.ts +0 -4
  310. package/src/lib/workspaces.ts +1 -1
@@ -0,0 +1,58 @@
1
+ /**
2
+ * D9 activation check (checkActivation). Injectable opts let us drive it
3
+ * against a throwaway HOME/PATH without touching the real machine.
4
+ */
5
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ import { checkActivation } from "./doctor";
11
+
12
+ let home: string;
13
+ let binDir: string;
14
+ beforeEach(() => {
15
+ home = mkdtempSync(join(tmpdir(), "cue-doctor-"));
16
+ binDir = join(home, ".local", "bin");
17
+ mkdirSync(binDir, { recursive: true });
18
+ });
19
+ afterEach(() => rmSync(home, { recursive: true, force: true }));
20
+
21
+ function writeShim() {
22
+ writeFileSync(join(binDir, "claude"), '#!/usr/bin/env bash\nexec cue launch claude "$@"\n');
23
+ }
24
+
25
+ describe("checkActivation (D9)", () => {
26
+ test("no shim → D9 warning (gating)", () => {
27
+ const issues = checkActivation({ homeDir: home, realBin: "/usr/bin/claude", pathDirs: [binDir, "/usr/bin"] });
28
+ expect(issues).toHaveLength(1);
29
+ expect(issues[0]!.code).toBe("D9");
30
+ expect(issues[0]!.severity).toBe("warning");
31
+ expect(issues[0]!.message).toContain("shim missing");
32
+ expect(issues[0]!.fix).toBe("cue shell install");
33
+ });
34
+
35
+ test("shim + real bin + ~/.local/bin first on PATH → healthy", () => {
36
+ writeShim();
37
+ const issues = checkActivation({ homeDir: home, realBin: "/usr/bin/claude", pathDirs: [binDir, "/usr/bin"] });
38
+ expect(issues).toHaveLength(0);
39
+ });
40
+
41
+ test("shim + real bin shadowing the shim on PATH → D9 error", () => {
42
+ writeShim();
43
+ const issues = checkActivation({ homeDir: home, realBin: "/usr/bin/claude", pathDirs: ["/usr/bin", binDir] });
44
+ expect(issues).toHaveLength(1);
45
+ expect(issues[0]!.code).toBe("D9");
46
+ expect(issues[0]!.severity).toBe("error");
47
+ expect(issues[0]!.message).toContain("shadowed");
48
+ });
49
+
50
+ test("shim but no real claude binary → D9 warning", () => {
51
+ writeShim();
52
+ const issues = checkActivation({ homeDir: home, realBin: null, pathDirs: [binDir] });
53
+ expect(issues).toHaveLength(1);
54
+ expect(issues[0]!.code).toBe("D9");
55
+ expect(issues[0]!.severity).toBe("warning");
56
+ expect(issues[0]!.message).toContain("not found");
57
+ });
58
+ });
@@ -16,16 +16,18 @@
16
16
  */
17
17
 
18
18
  import { readFileSync, existsSync, lstatSync, readlinkSync, readdirSync } from "node:fs";
19
- import { readFile, writeFile, readdir, lstat, rm } from "node:fs/promises";
19
+ import { readFile, writeFile, rm } from "node:fs/promises";
20
20
  import { resolve, dirname, join } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
22
- import { createHash } from "node:crypto";
23
22
  import { spawnSync } from "node:child_process";
24
23
 
25
24
  import { listProfiles, loadProfile } from "../lib/profile-loader";
26
25
  import { listAllSkillIds } from "../lib/resolver-local";
27
- import { findIncompleteSkills, fetchCompanionFiles, detectSkillPath, readSourceFile } from "../lib/companion-fetch";
26
+ import { findIncompleteSkills, fetchCompanionFiles, readSourceFile } from "../lib/companion-fetch";
28
27
  import { detectMissingDependencies } from "../lib/skill-dependencies";
28
+ import { shimInstalled, runInstall } from "./shell";
29
+ import { findRealClaudeBin } from "../lib/claude-binary";
30
+ import { homedir } from "node:os";
29
31
 
30
32
  const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
31
33
  const PROFILES_DIR = process.env.CUE_PROFILES_DIR ?? join(REPO_ROOT, "profiles");
@@ -202,6 +204,67 @@ async function checkProfile(profileName: string, allSkillIds: Set<string>, allMc
202
204
  return issues;
203
205
  }
204
206
 
207
+ /**
208
+ * D9 — Activation health (environment-scoped, runs once, not per profile).
209
+ * Verifies the claude shim is installed, the real binary resolves, and
210
+ * ~/.local/bin precedes it on PATH. Injectable for testing.
211
+ */
212
+ export function checkActivation(
213
+ opts: { homeDir?: string; pathDirs?: string[]; realBin?: string | null } = {},
214
+ ): Issue[] {
215
+ const issues: Issue[] = [];
216
+ const PROF = "(activation)";
217
+
218
+ // 1. The claude shim must be installed (and be a cue shim). Gating check —
219
+ // the rest is moot without it.
220
+ if (!shimInstalled(opts.homeDir)) {
221
+ // Warning, not error: a user may legitimately not have run `cue shell
222
+ // install` yet (or use `cue launch` directly). Surfacing it shouldn't flip
223
+ // `cue doctor`'s exit code, which should track actual profile breakage.
224
+ issues.push({
225
+ code: "D9",
226
+ severity: "warning",
227
+ profile: PROF,
228
+ message: "~/.local/bin/claude shim missing or not a cue shim — `claude` won't load profiles (run `cue shell install`)",
229
+ fix: "cue shell install",
230
+ });
231
+ return issues;
232
+ }
233
+
234
+ // 2. The real claude binary must resolve.
235
+ const realBin = opts.realBin !== undefined ? opts.realBin : findRealClaudeBin();
236
+ if (!realBin) {
237
+ issues.push({
238
+ code: "D9",
239
+ severity: "warning",
240
+ profile: PROF,
241
+ message: "Real claude binary not found on PATH (only the cue shim resolves)",
242
+ fix: "Install Claude Code, or set CUE_REAL_CLAUDE",
243
+ });
244
+ return issues;
245
+ }
246
+
247
+ // 3. ~/.local/bin must precede the real binary's dir on PATH (else the real
248
+ // binary shadows the shim). Only meaningful when both dirs are on PATH.
249
+ const home = opts.homeDir ?? homedir();
250
+ const shimDir = join(home, ".local", "bin");
251
+ const pathDirs = opts.pathDirs ?? (process.env.PATH ?? "").split(":");
252
+ const shimIdx = pathDirs.indexOf(shimDir);
253
+ const realDir = resolve(realBin, "..");
254
+ const realIdx = pathDirs.indexOf(realDir);
255
+ if (shimIdx >= 0 && realIdx >= 0 && shimIdx > realIdx) {
256
+ issues.push({
257
+ code: "D9",
258
+ severity: "error",
259
+ profile: PROF,
260
+ message: `Real binary dir ${realDir} precedes ${shimDir} on PATH — the shim is shadowed`,
261
+ fix: "Reorder PATH so ~/.local/bin comes before the real claude/codex",
262
+ });
263
+ }
264
+
265
+ return issues;
266
+ }
267
+
205
268
  async function applyFix(issue: Issue): Promise<boolean> {
206
269
  const yamlPath = join(PROFILES_DIR, issue.profile, "profile.yaml");
207
270
 
@@ -287,6 +350,14 @@ async function applyFix(issue: Issue): Promise<boolean> {
287
350
  });
288
351
  return fetched.length > 0;
289
352
  }
353
+ case "D9": {
354
+ // Install/repair the shim. runInstall returns 1 (no throw) when PATH
355
+ // ordering is wrong — that case can't be auto-fixed (user must reorder
356
+ // their shell PATH), so success is keyed on rc === 0.
357
+ const realBin = findRealClaudeBin();
358
+ const rc = await runInstall({ realClaude: realBin ?? undefined });
359
+ return rc === 0;
360
+ }
290
361
  default:
291
362
  return false;
292
363
  }
@@ -311,6 +382,7 @@ Checks:
311
382
  D6 Broken symlink in runtime
312
383
  D7 Incomplete skill (companions declared but missing)
313
384
  D8 Quality gate declared but the .sh under resources/quality-gates/ is missing
385
+ D9 Activation: claude shim installed, real claude resolves, ~/.local/bin precedes it
314
386
 
315
387
  Flags:
316
388
  --fix Auto-repair issues
@@ -363,6 +435,10 @@ Examples:
363
435
  } catch { /* skip */ }
364
436
  }
365
437
 
438
+ // D9: activation health (shim + real binary + PATH order). Environment-
439
+ // scoped, so run once regardless of --profile.
440
+ issues.push(...checkActivation());
441
+
366
442
  // D3 only when checking all profiles, and only if no profile globs everything.
367
443
  if (!targetProfile && !hasGlobAll) {
368
444
  for (const id of allSkillIds) {
@@ -13,7 +13,7 @@
13
13
  * Output: per-scenario pass/fail + a single summary score per profile.
14
14
  */
15
15
 
16
- import { readFileSync, existsSync } from "node:fs";
16
+ import { readFileSync, } from "node:fs";
17
17
  import { join, resolve, dirname, isAbsolute } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { homedir } from "node:os";
@@ -15,8 +15,8 @@
15
15
  * cost-per-message and the score now use perMessage so they reflect reality.
16
16
  */
17
17
 
18
- import { resolve, join, dirname, basename, isAbsolute } from "node:path";
19
- import { readFileSync, existsSync } from "node:fs";
18
+ import { resolve, join, dirname, isAbsolute } from "node:path";
19
+ import { readFileSync, } from "node:fs";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { homedir } from "node:os";
22
22
 
@@ -19,8 +19,9 @@ import {
19
19
  } from "node:fs";
20
20
  import { join, resolve, dirname } from "node:path";
21
21
  import { homedir } from "node:os";
22
+ import { configDir } from "../lib/config-paths";
22
23
 
23
- import { loadProfile, listProfiles } from "../lib/profile-loader";
24
+ import { loadProfile, } from "../lib/profile-loader";
24
25
  import { resolveProfileForCwd } from "../lib/cwd-resolver";
25
26
 
26
27
  const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cue");
@@ -249,11 +250,11 @@ export async function run(args: string[]): Promise<number> {
249
250
  if (args.includes("--history")) { showHistory(); return 0; }
250
251
 
251
252
  // Resolve profile
252
- const configDir = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cue");
253
+ const cfgDir = configDir();
253
254
  const explicit = args.find(a => !a.startsWith("-"));
254
255
  let profileName = explicit;
255
256
  if (!profileName) {
256
- const resolved = await resolveProfileForCwd({ cwd: process.cwd(), homeDir: homedir(), configDir });
257
+ const resolved = await resolveProfileForCwd({ cwd: process.cwd(), homeDir: homedir(), configDir: cfgDir });
257
258
  profileName = "profile" in resolved ? resolved.profile : "core";
258
259
  }
259
260
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, expect, test, beforeEach, afterEach } from "bun:test";
7
7
  import { mkdirSync, writeFileSync, rmSync } from "node:fs";
8
- import { tmpdir, homedir } from "node:os";
8
+ import { tmpdir, } from "node:os";
9
9
  import { join } from "node:path";
10
10
 
11
11
  import { run as failuresRun } from "./failures";
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, test, beforeEach, afterEach } from "bun:test";
6
- import { mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
6
+ import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
 
@@ -16,9 +16,14 @@ let tmp: string;
16
16
  beforeEach(() => {
17
17
  tmp = `${tmpdir()}/cue-features-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18
18
  mkdirSync(tmp, { recursive: true });
19
+ // Point `cue suggest`'s session scan at the empty tmp dir so the suggest
20
+ // tests stay hermetic + fast — otherwise they scan the real ~/.claude
21
+ // history (minutes on a heavy machine), which hung the whole suite.
22
+ process.env.CUE_SUGGEST_SESSIONS_DIR = tmp;
19
23
  });
20
24
 
21
25
  afterEach(() => {
26
+ delete process.env.CUE_SUGGEST_SESSIONS_DIR;
22
27
  try { rmSync(tmp, { recursive: true, force: true }); } catch {}
23
28
  });
24
29
 
@@ -10,6 +10,7 @@ import * as p from "@clack/prompts";
10
10
  import { resolveProfileForCwd } from "../lib/cwd-resolver";
11
11
  import { loadProfile } from "../lib/profile-loader";
12
12
  import { homedir } from "node:os";
13
+ import { configDir } from "../lib/config-paths";
13
14
 
14
15
  const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
15
16
 
@@ -19,11 +20,6 @@ const ICONS = [
19
20
  "🦈", "🐊", "🦅", "🐎", "🦁", "🐘",
20
21
  ];
21
22
 
22
- function configDir(): string {
23
- return process.env.XDG_CONFIG_HOME
24
- ? join(process.env.XDG_CONFIG_HOME, "cue")
25
- : join(homedir(), ".config", "cue");
26
- }
27
23
 
28
24
  export async function run(args: string[]): Promise<number> {
29
25
  const profileName = args[0] ?? await resolveCurrentProfile();
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
7
- import { resolve, dirname, join, basename } from "node:path";
7
+ import { resolve, dirname, join, } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
10
  import { loadProfile } from "../lib/profile-loader";
@@ -18,6 +18,8 @@ import { detectProfile } from "../lib/auto-detect";
18
18
  import { scanProject } from "../lib/project-scanner";
19
19
  import { listProfiles } from "../lib/profile-loader";
20
20
  import { getCachedGemsForProfile, autoInstallClis } from "./discover";
21
+ import { shimInstalled, runInstall } from "./shell";
22
+ import { gateFreshSkill } from "./security";
21
23
  import {
22
24
  configDir,
23
25
  enable as enableTelemetry,
@@ -67,11 +69,6 @@ export async function runGlobalOnboarding(): Promise<boolean> {
67
69
  hint: "recommended — minimal base plus skill management",
68
70
  },
69
71
  { value: "core", label: "core only", hint: "smallest — just the base" },
70
- {
71
- value: "core+skill-writer+ecc",
72
- label: "core + skill-writer + ecc",
73
- hint: "+ workspace conventions (CLAUDE.md / AGENTS.md)",
74
- },
75
72
  { value: "__custom", label: "Custom…", hint: "type a +-separated composite" },
76
73
  { value: "__skip", label: "Skip for now", hint: "falls back to plain `core`" },
77
74
  ],
@@ -82,7 +79,7 @@ export async function runGlobalOnboarding(): Promise<boolean> {
82
79
  let defaultComposite: string | null = null;
83
80
  if (defaultPick === "__custom") {
84
81
  const custom = await p.text({
85
- message: "Composite (e.g., core+skill-writer+ecc):",
82
+ message: "Composite (e.g., core+skill-writer+backend):",
86
83
  placeholder: "core+skill-writer",
87
84
  validate: (v) => {
88
85
  const parts = (v ?? "").split("+").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -160,14 +157,58 @@ async function offerDiscoverGems(profile: string): Promise<void> {
160
157
  const install = await p.confirm({ message: "Install these gems?" });
161
158
  if (p.isCancel(install) || !install) return;
162
159
 
160
+ let flagged = 0;
163
161
  for (const g of gems) {
164
162
  p.log.step(`Installing ${g.full_name}...`);
165
163
  spawnSync("npx", ["skills", "add", g.full_name, "-a", "claude-code", "-y"], {
166
164
  encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"],
167
165
  });
166
+ // Security gate: flag a just-fetched skill with critical findings and skip
167
+ // its CLI auto-install (the gem is installed to ~/.claude/skills, but the
168
+ // wizard does not auto-register it to a profile).
169
+ const gate = gateFreshSkill(g.name);
170
+ if (!gate.ok) {
171
+ flagged++;
172
+ p.log.error(`${g.full_name}: ${gate.critical.length} critical security finding(s) — review before use.`);
173
+ for (const c of gate.critical) p.log.message(` [${c.code}] ${c.message}`);
174
+ continue;
175
+ }
176
+ if (!gate.scanned) {
177
+ p.log.warn(`${g.full_name}: no SKILL.md found to scan — review manually.`);
178
+ }
168
179
  autoInstallClis(g.name);
169
180
  }
170
- p.log.success(`Installed ${gems.length} gem(s).`);
181
+ p.log.success(`Installed ${gems.length} gem(s) to ~/.claude/skills${flagged > 0 ? ` (${flagged} flagged by the security scan — review before use)` : ""}.`);
182
+ }
183
+
184
+ /**
185
+ * Offer to install the shell shims if they're missing. Without the
186
+ * `~/.local/bin/claude` shim, typing `claude` runs vanilla Claude Code and
187
+ * the pinned profile is never loaded — the #1 "I followed the docs and
188
+ * nothing happened" failure. Detect it here and offer the one-time fix.
189
+ */
190
+ async function ensureShim(): Promise<void> {
191
+ if (shimInstalled()) return;
192
+ p.log.warn(
193
+ "The `claude`/`codex` shim isn't installed yet — without it, launching `claude` runs vanilla Claude Code and won't load this profile.",
194
+ );
195
+ const install = await p.confirm({
196
+ message: "Install the shell shim now? (writes ~/.local/bin/claude)",
197
+ });
198
+ if (p.isCancel(install) || !install) {
199
+ p.log.message("Skipped — run `cue shell install` later to activate profile loading.");
200
+ return;
201
+ }
202
+ try {
203
+ const code = await runInstall();
204
+ if (code === 0) {
205
+ p.log.success("Shim installed to ~/.local/bin. Make sure it's earlier on your PATH than the real claude/codex.");
206
+ } else {
207
+ p.log.warn("Shim install reported an issue — run `cue shell install` manually for details.");
208
+ }
209
+ } catch {
210
+ p.log.warn("Couldn't install the shim automatically — run `cue shell install` manually.");
211
+ }
171
212
  }
172
213
 
173
214
  export async function run(args: string[]): Promise<number> {
@@ -264,6 +305,7 @@ export async function run(args: string[]): Promise<number> {
264
305
 
265
306
  writeFileSync(join(cwd, ".cue-profile"), (name as string) + "\n");
266
307
  await offerDiscoverGems(name as string);
308
+ await ensureShim();
267
309
  p.outro(`✅ Created profile "${name}" and pinned to this directory.`);
268
310
  return 0;
269
311
  }
@@ -271,6 +313,7 @@ export async function run(args: string[]): Promise<number> {
271
313
  // Pin the chosen profile
272
314
  writeFileSync(join(cwd, ".cue-profile"), (choice as string) + "\n");
273
315
  await offerDiscoverGems(choice as string);
316
+ await ensureShim();
274
317
  p.outro(`✅ Pinned "${choice}" to this directory. Next \`claude\` launch will use it.`);
275
318
  return 0;
276
319
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Smoke test for install.sh — proves the shell install path that every new
3
+ * user depends on: it symlinks `cue` onto PATH, writes a working `claude`
4
+ * shim, and `cue --version` runs through the shim dir.
5
+ *
6
+ * Hermetic: installs into a throwaway SHIM_DIR with a stub `authmux` on PATH so
7
+ * install.sh's Step 5 never runs `npm install -g authmux` (no network).
8
+ */
9
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
10
+ import { spawnSync } from "node:child_process";
11
+ import { mkdtempSync, writeFileSync, chmodSync, existsSync, readFileSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ const REPO_ROOT = join(import.meta.dir, "..", "..");
16
+ // CI-only: install.sh does `cd CUE_DIR && bun install`, which would mutate the
17
+ // developer's real node_modules and may hit the network on a cold cache. CI
18
+ // runs on a fresh checkout where that's fine and is the canonical place to
19
+ // prove the shell install path; locally we skip to keep the working tree clean.
20
+ const CAN_RUN =
21
+ !!process.env.CI &&
22
+ process.platform !== "win32" &&
23
+ spawnSync("bash", ["--version"], { encoding: "utf8" }).status === 0;
24
+
25
+ describe.skipIf(!CAN_RUN)("install.sh smoke", () => {
26
+ let shimDir: string;
27
+ beforeEach(() => {
28
+ shimDir = mkdtempSync(join(tmpdir(), "cue-installsh-"));
29
+ // Stub authmux so `command -v authmux` short-circuits Step 5 (no npm -g).
30
+ const stub = join(shimDir, "authmux");
31
+ writeFileSync(stub, "#!/usr/bin/env bash\necho 0.0.0\n");
32
+ chmodSync(stub, 0o755);
33
+ });
34
+ afterEach(() => rmSync(shimDir, { recursive: true, force: true }));
35
+
36
+ test("symlinks cue, writes a working claude shim, and cue --version runs through it", () => {
37
+ const env = {
38
+ ...process.env,
39
+ SHIM_DIR: shimDir,
40
+ CUE_DIR: REPO_ROOT,
41
+ PATH: `${shimDir}:${process.env.PATH ?? ""}`,
42
+ };
43
+ const res = spawnSync("bash", [join(REPO_ROOT, "install.sh"), "--yes"], {
44
+ encoding: "utf8",
45
+ timeout: 120000,
46
+ env,
47
+ });
48
+ expect(res.status).toBe(0);
49
+
50
+ // cue is exposed on PATH (symlink to bin/cue).
51
+ expect(existsSync(join(shimDir, "cue"))).toBe(true);
52
+
53
+ // claude shim routes through cue.
54
+ const claudeShim = join(shimDir, "claude");
55
+ expect(existsSync(claudeShim)).toBe(true);
56
+ expect(readFileSync(claudeShim, "utf8")).toContain("exec cue launch claude");
57
+
58
+ // `cue --version` works through the installed symlink and matches package.json.
59
+ const pkgVersion = JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf8")).version;
60
+ const ver = spawnSync(join(shimDir, "cue"), ["--version"], { encoding: "utf8", env, timeout: 20000 });
61
+ expect(ver.status).toBe(0);
62
+ expect(ver.stdout.trim()).toBe(pkgVersion);
63
+ expect(ver.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
64
+ });
65
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * E2e coverage for the launch EXEC HANDOFF — the lines that decide whether
3
+ * `claude`/`codex` actually starts with the right env. Uses `--dry-run` (which
4
+ * builds childEnv + the exec plan and prints it as JSON without exec'ing) and a
5
+ * direct recursion-guard probe.
6
+ *
7
+ * Kept in its own file (not launch.e2e.test.ts) to stay additive while that
8
+ * file is being edited concurrently.
9
+ */
10
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
11
+ import { mkdtemp, rm } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { spawnSync } from "node:child_process";
15
+
16
+ const CUE_BIN = join(import.meta.dir, "../index.ts");
17
+ const BUN_SPAWNABLE = spawnSync("bun", ["--version"], { encoding: "utf8" }).status === 0;
18
+
19
+ function cue(args: string[], env: Record<string, string> = {}): { status: number; stdout: string; stderr: string } {
20
+ const cleanEnv = { ...process.env, ...env };
21
+ delete cleanEnv.CUE_LAUNCHING;
22
+ delete cleanEnv.CLAUDE_CONFIG_DIR;
23
+ const res = spawnSync("bun", ["run", CUE_BIN, ...args], { encoding: "utf8", timeout: 20000, env: cleanEnv });
24
+ return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
25
+ }
26
+
27
+ function plan(stdout: string): any {
28
+ return JSON.parse(stdout.match(/\{[\s\S]*\}/)![0]);
29
+ }
30
+
31
+ describe.skipIf(!BUN_SPAWNABLE)("cue launch --dry-run exec handoff", () => {
32
+ let xdg: string;
33
+ beforeEach(async () => {
34
+ xdg = await mkdtemp(join(tmpdir(), "cue-handoff-"));
35
+ });
36
+ afterEach(async () => {
37
+ await rm(xdg, { recursive: true, force: true });
38
+ });
39
+
40
+ test("claude → CLAUDE_CONFIG_DIR points at the materialized runtime", () => {
41
+ const r = cue(["launch", "claude", "--cue-profile", "core", "--dry-run"], { XDG_CONFIG_HOME: xdg });
42
+ expect(r.status).toBe(0);
43
+ const p = plan(r.stdout);
44
+ const expected = join(xdg, "cue", "runtime", "core", "claude");
45
+ expect(p.agent).toBe("claude-code");
46
+ expect(p.env.CLAUDE_CONFIG_DIR).toBe(expected);
47
+ expect(p.runtimeDir).toBe(expected);
48
+ expect(p.command).toEqual(["claude"]);
49
+ expect(p.env.CODEX_HOME).toBeUndefined();
50
+ // NOTE: CUE_LAUNCHING is intentionally absent from the dry-run JSON (only
51
+ // env[envKey] is serialized); it's covered by the recursion-guard test.
52
+ });
53
+
54
+ test("codex → CODEX_HOME points at the codex runtime", () => {
55
+ const r = cue(["launch", "codex", "--cue-profile", "core", "--dry-run"], { XDG_CONFIG_HOME: xdg });
56
+ expect(r.status).toBe(0);
57
+ const p = plan(r.stdout);
58
+ const expected = join(xdg, "cue", "runtime", "core", "codex");
59
+ expect(p.agent).toBe("codex");
60
+ expect(p.env.CODEX_HOME).toBe(expected);
61
+ expect(p.command).toEqual(["codex"]);
62
+ expect(p.env.CLAUDE_CONFIG_DIR).toBeUndefined();
63
+ });
64
+
65
+ test("passthrough args flow into the exec command", () => {
66
+ const r = cue(["launch", "claude", "--cue-profile", "core", "--dry-run", "--resume", "foo"], { XDG_CONFIG_HOME: xdg });
67
+ expect(r.status).toBe(0);
68
+ const p = plan(r.stdout);
69
+ expect(p.command).toEqual(["claude", "--resume", "foo"]);
70
+ });
71
+ });
72
+
73
+ describe.skipIf(!BUN_SPAWNABLE)("cue launch recursion guard", () => {
74
+ test("CUE_LAUNCHING=1 aborts with exit 2 (shim recursion)", () => {
75
+ // Must NOT use the cue() helper — it strips CUE_LAUNCHING. Spawn directly
76
+ // with CUE_LAUNCHING=1 set (and CLAUDE_CONFIG_DIR cleared to avoid the
77
+ // unrelated account-alias → picker path).
78
+ const env = { ...process.env, CUE_LAUNCHING: "1" };
79
+ delete env.CLAUDE_CONFIG_DIR;
80
+ const res = spawnSync("bun", ["run", CUE_BIN, "launch", "claude", "--cue-profile", "core", "--dry-run"], {
81
+ encoding: "utf8",
82
+ timeout: 15000,
83
+ env,
84
+ });
85
+ expect(res.status).toBe(2);
86
+ expect(res.stderr).toContain("shim recursion detected");
87
+ });
88
+ });
@@ -11,6 +11,13 @@ import { spawnSync } from "node:child_process";
11
11
 
12
12
  const CUE_BIN = join(import.meta.dir, "../index.ts");
13
13
 
14
+ // These e2e tests shell out to `bun run`. In some sandboxes (and odd PATH
15
+ // setups) a spawned child can't find `bun`, which would hard-fail the suite
16
+ // with "Executable not found in $PATH: bun" — unrelated to what's under test.
17
+ // Skip the whole describe when a child `bun` can't be spawned. CI installs bun
18
+ // via setup-bun, so this only skips in constrained local/sandbox runs.
19
+ const BUN_SPAWNABLE = spawnSync("bun", ["--version"], { encoding: "utf8" }).status === 0;
20
+
14
21
  function cue(args: string[], opts: { cwd?: string; env?: Record<string, string> } = {}): { status: number; stdout: string; stderr: string } {
15
22
  // Strip env vars set when the test runner itself is running inside a cue
16
23
  // session — they propagate to the child cue invocation and break it in
@@ -29,7 +36,7 @@ function cue(args: string[], opts: { cwd?: string; env?: Record<string, string>
29
36
  return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
30
37
  }
31
38
 
32
- describe("cue launch e2e", () => {
39
+ describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
33
40
  let tmpDir: string;
34
41
 
35
42
  beforeEach(async () => {
@@ -219,6 +219,22 @@ describe("buildPickerSections", () => {
219
219
  expect(composite?.label).toBe("backend + designer");
220
220
  });
221
221
 
222
+ test("recent composite reuses each part's icon label, not bare names", () => {
223
+ // Combined recent rows should carry every part's icon (emoji or kitty image
224
+ // placeholder), pulled from each part's own option label.
225
+ const all = [
226
+ { value: "improver", label: "📈 improver", hint: "" },
227
+ { value: "secops", label: "🔒 secops", hint: "" },
228
+ { value: "builder", label: "🐻 builder", hint: "" },
229
+ ];
230
+ const recent = [
231
+ { name: "improver+secops+builder", sessions: 4, lastUsed: "2026-05-26T09:00:00Z" },
232
+ ];
233
+ const out = buildPickerSections(opt("__default"), all, recent, 3, now);
234
+ const composite = out.find((o) => o.value === "improver+secops+builder");
235
+ expect(composite?.label).toBe("📈 improver + 🔒 secops + 🐻 builder");
236
+ });
237
+
222
238
  test("featured: composites synthesized, single profiles reuse their option and leave All", () => {
223
239
  const all = [opt("backend"), opt("designer"), opt("webshop")];
224
240
  const out = buildPickerSections(
@@ -510,6 +526,19 @@ describe("formatProfileSummary", () => {
510
526
  expect(out[0]).toBe("skills 6 ← 🧬 writer:2 + 🐢 core:2");
511
527
  });
512
528
 
529
+ test("caps a fat composite breakdown at 6 parts with a '+N more' suffix", () => {
530
+ const names = ["a", "b", "c", "d", "e", "f", "g"]; // 7 parts → cap kicks in
531
+ const main = makeProfile({
532
+ name: names.join("+"),
533
+ skills: { local: names.map((n) => ({ id: `${n}/1` })), npx: [] },
534
+ });
535
+ const parts = names.map((n) =>
536
+ makeProfile({ name: n, skills: { local: [{ id: `${n}/1` }], npx: [] } }),
537
+ );
538
+ const out = formatProfileSummary(main, parts);
539
+ expect(out[0]).toBe("skills 7 ← a:1 + b:1 + c:1 + d:1 + e:1 + f:1 +1 more");
540
+ });
541
+
513
542
  test("adds category line below skills when localCount >= 5", () => {
514
543
  const profile = makeProfile({
515
544
  skills: {