@kensaurus/skills 0.0.0-stage → 2.1.0

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 (425) hide show
  1. package/.claude-plugin/marketplace.json +53 -0
  2. package/.claude-plugin/plugin.json +40 -0
  3. package/.cursor-plugin/plugin.json +38 -0
  4. package/.mcp.json +28 -0
  5. package/CHANGELOG.md +1790 -0
  6. package/LICENSE +21 -0
  7. package/LICENSE-APACHE +62 -0
  8. package/NOTICE +13 -0
  9. package/README.md +821 -2
  10. package/SECURITY.md +55 -0
  11. package/agents/code-reviewer.md +60 -0
  12. package/agents/completion-judge.md +89 -0
  13. package/agents/db-migrator.md +125 -0
  14. package/agents/debugger.md +47 -0
  15. package/agents/deploy-checker.md +100 -0
  16. package/agents/perf-monitor.md +74 -0
  17. package/assets/favicon.png +0 -0
  18. package/assets/logo-light.png +0 -0
  19. package/assets/logo.png +0 -0
  20. package/assets/logo.svg +6 -0
  21. package/assets/og.png +0 -0
  22. package/bin/install.mjs +1127 -0
  23. package/bin/kenji.js +2 -0
  24. package/commands/adr.md +17 -0
  25. package/commands/aeo-plan.md +18 -0
  26. package/commands/arch-boundaries.md +17 -0
  27. package/commands/aso-plan.md +18 -0
  28. package/commands/auth-flows.md +19 -0
  29. package/commands/backup-plan.md +17 -0
  30. package/commands/burndown-full.md +25 -0
  31. package/commands/capacitor-plan.md +18 -0
  32. package/commands/codemod-safety.md +22 -0
  33. package/commands/commit.md +18 -0
  34. package/commands/complete-everything.md +40 -0
  35. package/commands/cost-plan.md +19 -0
  36. package/commands/deadcode-plan.md +26 -0
  37. package/commands/deadcode.md +32 -0
  38. package/commands/debug-issue.md +17 -0
  39. package/commands/deps-plan.md +18 -0
  40. package/commands/docs-plan.md +17 -0
  41. package/commands/doctrine.md +19 -0
  42. package/commands/error-plan.md +19 -0
  43. package/commands/feedback-to-closure.md +36 -0
  44. package/commands/fix-issue.md +76 -0
  45. package/commands/gate-logic.md +26 -0
  46. package/commands/green-repo.md +36 -0
  47. package/commands/grill-me.md +19 -0
  48. package/commands/gtm-plan.md +21 -0
  49. package/commands/gtm-weekly.md +17 -0
  50. package/commands/gtm.md +22 -0
  51. package/commands/handoff.md +15 -0
  52. package/commands/housekeep-backlog.md +18 -0
  53. package/commands/housekeep-files.md +22 -0
  54. package/commands/housekeep-gates.md +18 -0
  55. package/commands/instant-nav.md +11 -0
  56. package/commands/integrity-plan.md +19 -0
  57. package/commands/launch-kit.md +16 -0
  58. package/commands/mcp-guide.md +40 -0
  59. package/commands/mobile-plan.md +19 -0
  60. package/commands/native-rn-monorepo/README.md +78 -0
  61. package/commands/native-rn-monorepo/android-build.md +26 -0
  62. package/commands/native-rn-monorepo/android-install.md +32 -0
  63. package/commands/native-rn-monorepo/android-logcat.md +37 -0
  64. package/commands/native-rn-monorepo/ios-ci-logs.md +56 -0
  65. package/commands/native-rn-monorepo/ios-ci-status.md +52 -0
  66. package/commands/native-rn-monorepo/ios-ci-trigger.md +59 -0
  67. package/commands/native-rn-monorepo/rn-reset.md +50 -0
  68. package/commands/native-rn-monorepo/rn-ship-ios.md +67 -0
  69. package/commands/native-rn-monorepo/rn-verify.md +53 -0
  70. package/commands/perf-plan.md +18 -0
  71. package/commands/plan-mode.md +74 -0
  72. package/commands/pr.md +16 -0
  73. package/commands/pricing-plan.md +20 -0
  74. package/commands/privacy-plan.md +18 -0
  75. package/commands/readability.md +12 -0
  76. package/commands/readme.md +15 -0
  77. package/commands/refactor.md +15 -0
  78. package/commands/release-prep.md +17 -0
  79. package/commands/research.md +25 -0
  80. package/commands/responsive-audit.md +21 -0
  81. package/commands/review-code.md +18 -0
  82. package/commands/rls-plan.md +18 -0
  83. package/commands/secrets-plan.md +18 -0
  84. package/commands/security-plan.md +19 -0
  85. package/commands/ship-and-observe.md +36 -0
  86. package/commands/skill-conflicts.md +19 -0
  87. package/commands/slop-plan.md +18 -0
  88. package/commands/stub-plan.md +18 -0
  89. package/commands/test-mutation.md +16 -0
  90. package/commands/test-plan.md +17 -0
  91. package/commands/test.md +29 -0
  92. package/commands/thirdparty-web-interface-guidelines.md +185 -0
  93. package/commands/uiux-plan.md +18 -0
  94. package/commands/uiux.md +45 -0
  95. package/commands/update-deps.md +21 -0
  96. package/commands/validation-plan.md +19 -0
  97. package/commands-portable/fix-issue.md +72 -0
  98. package/commands-portable/plan-mode.md +92 -0
  99. package/commands-portable/research.md +91 -0
  100. package/docs/screenshots/README.md +5 -0
  101. package/docs/screenshots/audit-dark.png +0 -0
  102. package/docs/screenshots/build-dark.png +0 -0
  103. package/docs/screenshots/grill-dark.png +0 -0
  104. package/docs/screenshots/hero-dark.png +0 -0
  105. package/docs/screenshots/hero-light.png +0 -0
  106. package/docs/screenshots/ship-dark.png +0 -0
  107. package/docs/screenshots/src/showcase.html +320 -0
  108. package/hooks/completion-gate.mjs +258 -0
  109. package/hooks/cursor-hooks.json +13 -0
  110. package/hooks/hooks.json +15 -0
  111. package/install.sh +21 -0
  112. package/llms.txt +40 -0
  113. package/mcp/README.md +266 -0
  114. package/mcp/VERSIONS.md +41 -0
  115. package/mcp/mcp-full.json.template +124 -0
  116. package/mcp/mcp.json.template +29 -0
  117. package/mcp/pinned-versions.json +27 -0
  118. package/package.json +94 -4
  119. package/rules/approved-plan-execution.mdc +65 -0
  120. package/rules/full-stack-ship-discipline.mdc +37 -0
  121. package/rules/native-rn-monorepo/README.md +63 -0
  122. package/rules/native-rn-monorepo/_project.mdc +69 -0
  123. package/rules/native-rn-monorepo/native-android.mdc +72 -0
  124. package/rules/native-rn-monorepo/native-ios.mdc +61 -0
  125. package/rules/native-rn-monorepo/react-native-js.mdc +78 -0
  126. package/rules/native-rn-monorepo/web.mdc +60 -0
  127. package/rules/project-starter/components.mdc +54 -0
  128. package/rules/project-starter/data-fetching.mdc +77 -0
  129. package/rules/project-starter/git.mdc +41 -0
  130. package/rules/project-starter/supabase.mdc +37 -0
  131. package/rules/project-starter/tailwind.mdc +48 -0
  132. package/rules/project-starter/typescript.mdc +36 -0
  133. package/rules/project-starter/web-performance.mdc +42 -0
  134. package/rules/senior-engineer.mdc +30 -0
  135. package/rules/shell-first-search.mdc +19 -0
  136. package/rules/skill-workflows.mdc +35 -0
  137. package/rules/verification-before-completion.mdc +57 -0
  138. package/skills/audit-accessibility/SKILL.md +441 -0
  139. package/skills/audit-agent-speed/SKILL.md +181 -0
  140. package/skills/audit-agent-speed/scripts/stop-typecheck.mjs +151 -0
  141. package/skills/audit-analytics/SKILL.md +138 -0
  142. package/skills/audit-auth-flows/SKILL.md +267 -0
  143. package/skills/audit-backend-architecture/SKILL.md +266 -0
  144. package/skills/audit-backend-architecture/references/patterns.md +386 -0
  145. package/skills/audit-bundle-size/SKILL.md +295 -0
  146. package/skills/audit-cicd/SKILL.md +218 -0
  147. package/skills/audit-code-quality/SKILL.md +314 -0
  148. package/skills/audit-code-review/SKILL.md +289 -0
  149. package/skills/audit-codemod-safety/SKILL.md +159 -0
  150. package/skills/audit-db-schema/SKILL.md +465 -0
  151. package/skills/audit-db-schema/references/details.md +110 -0
  152. package/skills/audit-doctrine/SKILL.md +189 -0
  153. package/skills/audit-env-parity/SKILL.md +133 -0
  154. package/skills/audit-fe-api/SKILL.md +458 -0
  155. package/skills/audit-gate-logic/SKILL.md +219 -0
  156. package/skills/audit-i18n/SKILL.md +339 -0
  157. package/skills/audit-infra-cost/SKILL.md +142 -0
  158. package/skills/audit-langfuse-llm/SKILL.md +468 -0
  159. package/skills/audit-langfuse-llm/references/details.md +226 -0
  160. package/skills/audit-llm-security/SKILL.md +147 -0
  161. package/skills/audit-monetization-iap/SKILL.md +137 -0
  162. package/skills/audit-payment-system/SKILL.md +268 -0
  163. package/skills/audit-payment-system/references/checklist.md +283 -0
  164. package/skills/audit-performance/SKILL.md +383 -0
  165. package/skills/audit-performance/references/loading-priority-2026.md +81 -0
  166. package/skills/audit-realworld/SKILL.md +287 -0
  167. package/skills/audit-registry-listing/SKILL.md +122 -0
  168. package/skills/audit-resilience/SKILL.md +153 -0
  169. package/skills/audit-responsive/SKILL.md +221 -0
  170. package/skills/audit-responsive/references/checklist.md +166 -0
  171. package/skills/audit-security/SKILL.md +289 -0
  172. package/skills/audit-skill-conflicts/SKILL.md +178 -0
  173. package/skills/audit-ui-states/SKILL.md +146 -0
  174. package/skills/audit-uiux-design-system/SKILL.md +475 -0
  175. package/skills/audit-uiux-design-system/references/details.md +71 -0
  176. package/skills/audit-ux/SKILL.md +381 -0
  177. package/skills/audit-ux/references/details.md +245 -0
  178. package/skills/audit-ux-journeys/SKILL.md +215 -0
  179. package/skills/audit-ux-journeys/references/checklist.md +179 -0
  180. package/skills/backend-db-performance/SKILL.md +441 -0
  181. package/skills/backend-error-handling/SKILL.md +489 -0
  182. package/skills/backend-error-handling/references/details.md +58 -0
  183. package/skills/backend-observability/SKILL.md +88 -0
  184. package/skills/backend-patterns/SKILL.md +499 -0
  185. package/skills/backend-patterns/references/architecture-patterns.md +298 -0
  186. package/skills/backend-realtime/SKILL.md +403 -0
  187. package/skills/backend-realtime/references/patterns.md +74 -0
  188. package/skills/burndown-full/SKILL.md +174 -0
  189. package/skills/complete-everything/SKILL.md +295 -0
  190. package/skills/data-pipeline/SKILL.md +109 -0
  191. package/skills/data-visualization/SKILL.md +488 -0
  192. package/skills/debug-error/SKILL.md +322 -0
  193. package/skills/debug-fe-be-integration/SKILL.md +459 -0
  194. package/skills/debug-sentry-monitor/SKILL.md +497 -0
  195. package/skills/debug-sentry-monitor/references/details.md +165 -0
  196. package/skills/deploy-npm/SKILL.md +394 -0
  197. package/skills/deploy-npm/references/example-mushi-mushi.md +52 -0
  198. package/skills/deploy-verify/SKILL.md +489 -0
  199. package/skills/design-api/SKILL.md +379 -0
  200. package/skills/design-canvas/SKILL.md +155 -0
  201. package/skills/design-email/SKILL.md +370 -0
  202. package/skills/design-frontend/SKILL.md +143 -0
  203. package/skills/design-generative-art/SKILL.md +474 -0
  204. package/skills/design-mobile-first/SKILL.md +506 -0
  205. package/skills/design-motion/SKILL.md +333 -0
  206. package/skills/design-motion/references/delight-interactions.md +191 -0
  207. package/skills/design-prd/SKILL.md +443 -0
  208. package/skills/design-system/SKILL.md +457 -0
  209. package/skills/design-theme/SKILL.md +226 -0
  210. package/skills/design-theme/themes/tsumagoi-ranch.md +150 -0
  211. package/skills/docs-adr/SKILL.md +168 -0
  212. package/skills/docs-coauthor/SKILL.md +368 -0
  213. package/skills/docs-comparison-pages/SKILL.md +117 -0
  214. package/skills/docs-domain-modeling/SKILL.md +97 -0
  215. package/skills/docs-launch-kit/SKILL.md +139 -0
  216. package/skills/docs-writer/SKILL.md +469 -0
  217. package/skills/enhance-agent-guardrails/SKILL.md +164 -0
  218. package/skills/enhance-arch-boundaries/SKILL.md +154 -0
  219. package/skills/enhance-capacitor-ui/SKILL.md +463 -0
  220. package/skills/enhance-capacitor-ui/references/details.md +750 -0
  221. package/skills/enhance-email-deliverability/SKILL.md +143 -0
  222. package/skills/enhance-growth-loops/SKILL.md +122 -0
  223. package/skills/enhance-lifecycle-email/SKILL.md +130 -0
  224. package/skills/enhance-motion/SKILL.md +193 -0
  225. package/skills/enhance-onboarding/SKILL.md +148 -0
  226. package/skills/enhance-pwa/SKILL.md +303 -0
  227. package/skills/enhance-readability/SKILL.md +146 -0
  228. package/skills/enhance-readme/SKILL.md +496 -0
  229. package/skills/enhance-readme/package-lock.json +187 -0
  230. package/skills/enhance-readme/package.json +17 -0
  231. package/skills/enhance-readme/scripts/generate-readme-blocks.mjs +199 -0
  232. package/skills/enhance-readme/scripts/record-readme-tour.mjs +442 -0
  233. package/skills/enhance-skill-prompts/SKILL.md +167 -0
  234. package/skills/enhance-skill-prompts/references/exemplar-audit-auth-flows.md +311 -0
  235. package/skills/enhance-ux-laws/SKILL.md +409 -0
  236. package/skills/enhance-web-conversion/SKILL.md +155 -0
  237. package/skills/enhance-web-forms/SKILL.md +154 -0
  238. package/skills/enhance-web-instant-nav/SKILL.md +138 -0
  239. package/skills/enhance-web-instant-nav/references/bfcache-blockers.md +23 -0
  240. package/skills/enhance-web-instant-nav/references/early-hints.md +33 -0
  241. package/skills/enhance-web-instant-nav/references/speculation-rules.md +44 -0
  242. package/skills/enhance-web-landing/SKILL.md +459 -0
  243. package/skills/enhance-web-landing/references/details.md +773 -0
  244. package/skills/enhance-web-redesign/SKILL.md +228 -0
  245. package/skills/enhance-web-seo/SKILL.md +276 -0
  246. package/skills/enhance-web-ui/SKILL.md +473 -0
  247. package/skills/enhance-web-ui/references/details.md +674 -0
  248. package/skills/enhance-web-ux/HEURISTICS.md +248 -0
  249. package/skills/enhance-web-ux/PATTERNS.md +375 -0
  250. package/skills/enhance-web-ux/SKILL.md +464 -0
  251. package/skills/enhance-web-ux/examples.md +222 -0
  252. package/skills/enhance-web-ux/references/details.md +406 -0
  253. package/skills/enhance-web-web3d/SKILL.md +397 -0
  254. package/skills/enhance-web-web3d/references/css-canvas-effects.md +180 -0
  255. package/skills/handoff/SKILL.md +66 -0
  256. package/skills/housekeep-backlog/SKILL.md +149 -0
  257. package/skills/housekeep-dead-code/SKILL.md +387 -0
  258. package/skills/housekeep-dead-code/references/ratchet-ci.md +205 -0
  259. package/skills/housekeep-dead-code/references/supabase-hygiene.md +152 -0
  260. package/skills/housekeep-design/SKILL.md +207 -0
  261. package/skills/housekeep-files/SKILL.md +220 -0
  262. package/skills/housekeep-files/references/naming-and-catalog.md +86 -0
  263. package/skills/housekeep-files/scripts/housekeep-files.ps1 +360 -0
  264. package/skills/housekeep-files/scripts/housekeep-files.sh +238 -0
  265. package/skills/housekeep-gates/SKILL.md +174 -0
  266. package/skills/iterate-agent-harness/SKILL.md +137 -0
  267. package/skills/iterate-gtm-weekly/SKILL.md +103 -0
  268. package/skills/iterate-post-launch/SKILL.md +292 -0
  269. package/skills/meta-mcp-builder/SKILL.md +313 -0
  270. package/skills/meta-skill-creator/SKILL.md +304 -0
  271. package/skills/mobile-capacitor-platform/SKILL.md +103 -0
  272. package/skills/mobile-emulator-start/SKILL.md +296 -0
  273. package/skills/mobile-emulator-test/SKILL.md +491 -0
  274. package/skills/mobile-emulator-test/references/details.md +478 -0
  275. package/skills/mobile-rn-performance/SKILL.md +107 -0
  276. package/skills/mobile-rn-screen/SKILL.md +476 -0
  277. package/skills/mobile-rn-screen/references/details.md +785 -0
  278. package/skills/mushi-health/SKILL.md +206 -0
  279. package/skills/mushi-integration/SKILL.md +257 -0
  280. package/skills/plan-aeo-readiness/SKILL.md +166 -0
  281. package/skills/plan-antislop/SKILL.md +281 -0
  282. package/skills/plan-aso/SKILL.md +149 -0
  283. package/skills/plan-backup-dr/SKILL.md +131 -0
  284. package/skills/plan-capacitor-hardening/SKILL.md +217 -0
  285. package/skills/plan-data-integrity/SKILL.md +187 -0
  286. package/skills/plan-dead-code/SKILL.md +386 -0
  287. package/skills/plan-dead-code/references/knip-config.md +214 -0
  288. package/skills/plan-dead-code/references/output-templates.md +133 -0
  289. package/skills/plan-dead-code/references/preservation-contract.md +50 -0
  290. package/skills/plan-dead-code/references/residue-greps.md +84 -0
  291. package/skills/plan-dependency-provenance/SKILL.md +200 -0
  292. package/skills/plan-docs-sync/SKILL.md +143 -0
  293. package/skills/plan-docs-sync/references/drift-taxonomy.md +43 -0
  294. package/skills/plan-docs-sync/references/output-templates.md +33 -0
  295. package/skills/plan-docs-sync/references/preservation-contract.md +17 -0
  296. package/skills/plan-error-handling/SKILL.md +205 -0
  297. package/skills/plan-gtm/SKILL.md +276 -0
  298. package/skills/plan-gtm/references/benchmarks-2026.md +183 -0
  299. package/skills/plan-input-validation/SKILL.md +179 -0
  300. package/skills/plan-llm-cost-guardrails/SKILL.md +176 -0
  301. package/skills/plan-mobile-readiness/SKILL.md +171 -0
  302. package/skills/plan-perf-audit/SKILL.md +145 -0
  303. package/skills/plan-perf-audit/references/audit-scope.md +51 -0
  304. package/skills/plan-perf-audit/references/output-templates.md +33 -0
  305. package/skills/plan-perf-audit/references/preservation-contract.md +13 -0
  306. package/skills/plan-pricing/SKILL.md +173 -0
  307. package/skills/plan-privacy-compliance/SKILL.md +148 -0
  308. package/skills/plan-rls-audit/SKILL.md +231 -0
  309. package/skills/plan-secrets-audit/SKILL.md +181 -0
  310. package/skills/plan-security-audit/SKILL.md +168 -0
  311. package/skills/plan-security-audit/references/output-templates.md +36 -0
  312. package/skills/plan-security-audit/references/owasp-supabase-scope.md +55 -0
  313. package/skills/plan-security-audit/references/preservation-contract.md +18 -0
  314. package/skills/plan-stub-checker/SKILL.md +216 -0
  315. package/skills/plan-stub-checker/references/detection-methodology.md +75 -0
  316. package/skills/plan-stub-checker/references/detection-taxonomy.md +34 -0
  317. package/skills/plan-stub-checker/references/output-templates.md +63 -0
  318. package/skills/plan-stub-checker/references/preservation-contract.md +24 -0
  319. package/skills/plan-test-coverage/SKILL.md +170 -0
  320. package/skills/plan-test-coverage/references/methodology.md +54 -0
  321. package/skills/plan-test-coverage/references/output-templates.md +34 -0
  322. package/skills/plan-test-coverage/references/preservation-contract.md +15 -0
  323. package/skills/plan-uiux-unification/SKILL.md +230 -0
  324. package/skills/plan-uiux-unification/references/output-templates.md +67 -0
  325. package/skills/plan-uiux-unification/references/phase-workbook.md +85 -0
  326. package/skills/plan-uiux-unification/references/preservation-contract.md +24 -0
  327. package/skills/protocol-browser-anti-stall/SKILL.md +211 -0
  328. package/skills/protocol-browser-anti-stall/references/mcp-to-cli-map.md +113 -0
  329. package/skills/protocol-browser-anti-stall/references/playwright-session-coordination.md +170 -0
  330. package/skills/research/SKILL.md +422 -0
  331. package/skills/test-exploratory/SKILL.md +165 -0
  332. package/skills/test-exploratory/references/charter-template.md +29 -0
  333. package/skills/test-load/SKILL.md +126 -0
  334. package/skills/test-mutation/SKILL.md +160 -0
  335. package/skills/test-playwright/SKILL.md +354 -0
  336. package/skills/test-qa/SKILL.md +364 -0
  337. package/skills/test-qa/references/details.md +268 -0
  338. package/skills/test-red-team/SKILL.md +386 -0
  339. package/skills/test-red-team/references/owasp-attack-checklist.md +193 -0
  340. package/skills/test-unit/SKILL.md +259 -0
  341. package/skills/test-unit/references/details.md +267 -0
  342. package/skills/test-visual-regression/SKILL.md +132 -0
  343. package/skills/thirdparty-emil-design-eng/ATTRIBUTION.md +20 -0
  344. package/skills/thirdparty-emil-design-eng/SKILL.md +21 -0
  345. package/skills/thirdparty-emil-design-eng/references/emil-design-eng.md +676 -0
  346. package/skills/thirdparty-ui-ux-pro-max/ATTRIBUTION.md +22 -0
  347. package/skills/thirdparty-ui-ux-pro-max/SKILL.md +304 -0
  348. package/skills/thirdparty-ui-ux-pro-max/data/charts.csv +26 -0
  349. package/skills/thirdparty-ui-ux-pro-max/data/colors.csv +97 -0
  350. package/skills/thirdparty-ui-ux-pro-max/data/icons.csv +101 -0
  351. package/skills/thirdparty-ui-ux-pro-max/data/landing.csv +31 -0
  352. package/skills/thirdparty-ui-ux-pro-max/data/products.csv +97 -0
  353. package/skills/thirdparty-ui-ux-pro-max/data/react-performance.csv +45 -0
  354. package/skills/thirdparty-ui-ux-pro-max/data/stacks/astro.csv +54 -0
  355. package/skills/thirdparty-ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  356. package/skills/thirdparty-ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  357. package/skills/thirdparty-ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  358. package/skills/thirdparty-ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  359. package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  360. package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  361. package/skills/thirdparty-ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  362. package/skills/thirdparty-ui-ux-pro-max/data/stacks/react.csv +54 -0
  363. package/skills/thirdparty-ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  364. package/skills/thirdparty-ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  365. package/skills/thirdparty-ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  366. package/skills/thirdparty-ui-ux-pro-max/data/stacks/vue.csv +50 -0
  367. package/skills/thirdparty-ui-ux-pro-max/data/styles.csv +68 -0
  368. package/skills/thirdparty-ui-ux-pro-max/data/typography.csv +58 -0
  369. package/skills/thirdparty-ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  370. package/skills/thirdparty-ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  371. package/skills/thirdparty-ui-ux-pro-max/data/web-interface.csv +31 -0
  372. package/skills/thirdparty-ui-ux-pro-max/scripts/core.py +253 -0
  373. package/skills/thirdparty-ui-ux-pro-max/scripts/design_system.py +1067 -0
  374. package/skills/thirdparty-ui-ux-pro-max/scripts/search.py +114 -0
  375. package/skills/thirdparty-web-interface-guidelines/ATTRIBUTION.md +23 -0
  376. package/skills/thirdparty-web-interface-guidelines/SKILL.md +190 -0
  377. package/skills/workflow-build-feature/SKILL.md +118 -0
  378. package/skills/workflow-coding-discipline/SKILL.md +140 -0
  379. package/skills/workflow-environment-ready/SKILL.md +128 -0
  380. package/skills/workflow-feature-flag/SKILL.md +262 -0
  381. package/skills/workflow-feedback-to-closure/SKILL.md +165 -0
  382. package/skills/workflow-fix-and-ship/SKILL.md +136 -0
  383. package/skills/workflow-git-commit/SKILL.md +200 -0
  384. package/skills/workflow-green-repo/SKILL.md +166 -0
  385. package/skills/workflow-grilling/SKILL.md +73 -0
  386. package/skills/workflow-gtm/SKILL.md +153 -0
  387. package/skills/workflow-housekeep/SKILL.md +453 -0
  388. package/skills/workflow-housekeep/references/templates.md +109 -0
  389. package/skills/workflow-launch-ready/SKILL.md +145 -0
  390. package/skills/workflow-merge-conflicts/SKILL.md +62 -0
  391. package/skills/workflow-onboard/SKILL.md +99 -0
  392. package/skills/workflow-parallel-agents/SKILL.md +164 -0
  393. package/skills/workflow-pr/SKILL.md +197 -0
  394. package/skills/workflow-quality-gate/SKILL.md +147 -0
  395. package/skills/workflow-refactor/SKILL.md +274 -0
  396. package/skills/workflow-release-prep/SKILL.md +207 -0
  397. package/skills/workflow-ship-and-observe/SKILL.md +164 -0
  398. package/skills/workflow-spec-tdd/SKILL.md +141 -0
  399. package/skills/workflow-spec-tdd/references/spec-template.md +126 -0
  400. package/skills/workflow-spec-tdd/references/tdd-patterns.md +167 -0
  401. package/skills-cursor/babysit/SKILL.md +17 -0
  402. package/skills-cursor/canvas/SKILL.md +142 -0
  403. package/skills-cursor/canvas/sdk/canvas-tokens.d.ts +235 -0
  404. package/skills-cursor/canvas/sdk/chart-primitives.d.ts +200 -0
  405. package/skills-cursor/canvas/sdk/dag-layout.d.ts +102 -0
  406. package/skills-cursor/canvas/sdk/diff-view.d.ts +130 -0
  407. package/skills-cursor/canvas/sdk/form-primitives.d.ts +194 -0
  408. package/skills-cursor/canvas/sdk/hooks.d.ts +117 -0
  409. package/skills-cursor/canvas/sdk/index.d.ts +47 -0
  410. package/skills-cursor/canvas/sdk/theme.d.ts +61 -0
  411. package/skills-cursor/canvas/sdk/todo-list.d.ts +49 -0
  412. package/skills-cursor/canvas/sdk/ui-primitives.d.ts +549 -0
  413. package/skills-cursor/canvas/sdk/ui-primitives.test.d.ts +2 -0
  414. package/skills-cursor/create-hook/SKILL.md +238 -0
  415. package/skills-cursor/create-rule/SKILL.md +185 -0
  416. package/skills-cursor/create-skill/SKILL.md +269 -0
  417. package/skills-cursor/create-skill/references/authoring-guide.md +182 -0
  418. package/skills-cursor/create-subagent/SKILL.md +228 -0
  419. package/skills-cursor/migrate-to-skills/SKILL.md +121 -0
  420. package/skills-cursor/shell/SKILL.md +22 -0
  421. package/skills-cursor/split-to-prs/SKILL.md +47 -0
  422. package/skills-cursor/statusline/SKILL.md +193 -0
  423. package/skills-cursor/update-cli-config/SKILL.md +85 -0
  424. package/skills-cursor/update-cursor-settings/SKILL.md +137 -0
  425. package/skills.sh.json +297 -0
@@ -0,0 +1,1127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * kenji installer
4
+ *
5
+ * Usage:
6
+ * npx @kensaurus/skills Merge-install into ~/.cursor/ AND ~/.agents/skills/
7
+ * npx @kensaurus/skills --auto Detect installed tools and install to each
8
+ * npx @kensaurus/skills --claude Install for Claude Code (~/.claude/) instead
9
+ * npx @kensaurus/skills --codex Install for Codex CLI (~/.codex/AGENTS.md + prompts)
10
+ * npx @kensaurus/skills --gemini Install for Gemini CLI (~/.gemini/GEMINI.md + commands)
11
+ * npx @kensaurus/skills --all Install for all four supported tools
12
+ * npx @kensaurus/skills --clean Mirror: make target paths match this repo exactly
13
+ * npx @kensaurus/skills --only skills Install only some groups (csv)
14
+ * npx @kensaurus/skills --skill audit-ux Install a single skill
15
+ * npx @kensaurus/skills --link Dev mode: symlink instead of copy
16
+ * npx @kensaurus/skills --restore [stamp] Restore a previous --clean backup
17
+ * npx @kensaurus/skills --dry-run Preview without changing anything
18
+ * npx @kensaurus/skills --verify Hash-check dests against this package (no writes)
19
+ * npx @kensaurus/skills --help
20
+ *
21
+ * Why two Cursor paths?
22
+ * ~/.cursor/skills/ — read by the Cursor agent at runtime
23
+ * ~/.agents/skills/ — historically indexed by the Cursor Skills UI panel
24
+ * Default still mirrors both. Pass --no-agents-mirror to skip the UI copy.
25
+ * skills-cursor names that Cursor already ships as managed builtins are not
26
+ * copied into ~/.cursor/skills (Claude still gets the portable copies).
27
+ * RENAMED_SKILLS prunes old skill directories after a rename so existing
28
+ * installs do not keep two near-identical copies. Honors --dry-run; skipped
29
+ * when --skill is scoped. RENAMED_RULES does the same for renamed rule files.
30
+ *
31
+ * Claude Code paths:
32
+ * ~/.claude/skills/ — global skills, appear as /slash-commands
33
+ * ~/.claude/commands/ — custom slash commands
34
+ * ~/.claude/agents/ — subagent definitions
35
+ * ~/.claude/rules/ — rules (.mdc sources installed as .md)
36
+ * ~/.claude/settings.json — merged completion Stop hook (same script as Cursor)
37
+ */
38
+
39
+ import {
40
+ existsSync, mkdirSync, cpSync, rmSync, symlinkSync, readdirSync, statSync,
41
+ readFileSync, writeFileSync,
42
+ } from 'node:fs';
43
+ import { createHash } from 'node:crypto';
44
+ import { join, resolve, dirname } from 'node:path';
45
+ import { homedir, platform } from 'node:os';
46
+ import { fileURLToPath } from 'node:url';
47
+
48
+ const __dir = fileURLToPath(new URL('..', import.meta.url));
49
+ const argv = process.argv.slice(2);
50
+
51
+ // ---- tiny arg parser (supports `--flag`, `--key value`, `--key=value`) ----
52
+ const flags = new Set();
53
+ const opts = {};
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (!a.startsWith('--')) continue;
57
+ const [k, inlineVal] = a.slice(2).split('=');
58
+ if (inlineVal !== undefined) { opts[k] = inlineVal; continue; }
59
+ const next = argv[i + 1];
60
+ if (next && !next.startsWith('--')) { opts[k] = next; i++; } else { flags.add(k); }
61
+ }
62
+ const has = (...names) => names.some((n) => flags.has(n) || n in opts);
63
+
64
+ if (has('help', 'h')) {
65
+ console.log(`
66
+ kenji installer
67
+
68
+ Usage:
69
+ npx @kensaurus/skills Merge-install into ~/.cursor/ + ~/.agents/skills/
70
+ npx @kensaurus/skills --auto Detect installed tools and install to each
71
+ npx @kensaurus/skills --claude Install for Claude Code (~/.claude/) instead
72
+ npx @kensaurus/skills --codex Install for Codex CLI (~/.codex/AGENTS.md)
73
+ npx @kensaurus/skills --gemini Install for Gemini CLI (~/.gemini/GEMINI.md)
74
+ npx @kensaurus/skills --all Install for all four supported tools
75
+ npx @kensaurus/skills --clean Mirror: wipe and rebuild target paths from this repo
76
+ npx @kensaurus/skills --only skills Install only some groups (skills,commands,agents,rules,hooks)
77
+ npx @kensaurus/skills --skill <name> Install one skill by name
78
+ npx @kensaurus/skills --link Dev mode: symlink repo into ~/.cursor (live edits)
79
+ npx @kensaurus/skills --restore [stamp] Restore a previous --clean backup (latest if omitted)
80
+ npx @kensaurus/skills --dry-run Preview without changing anything
81
+ npx @kensaurus/skills --verify Hash-check dests against this package (no writes)
82
+
83
+ Flags:
84
+ --auto Detect installed tools (~/.cursor, ~/.claude, ~/.codex, ~/.gemini) and
85
+ install to each; falls back to Cursor if none are found.
86
+ --cursor Target Cursor explicitly (same as the bare default).
87
+ --claude Target Claude Code (~/.claude/) instead of Cursor.
88
+ --codex Target Codex CLI — merged rules → ~/.codex/AGENTS.md + prompt ports.
89
+ --gemini Target Gemini CLI — merged rules → ~/.gemini/GEMINI.md + command ports.
90
+ --all Target all four supported tools in one run.
91
+ --clean, --mirror Wipe managed dirs first so target paths exactly mirror this repo.
92
+ --no-backup Skip the timestamped backup taken before a --clean wipe.
93
+ --only <csv> Limit to a subset of: skills, commands, agents, rules, hooks.
94
+ --skill <name> Install a single skill (implies --only skills).
95
+ --link Symlink (junction on Windows) instead of copying — for repo dev.
96
+ --restore [stamp] Copy a backup under <target>/.kenji-backups/ back into place.
97
+ --typecheck-hook [script]
98
+ Run from a repo root: add a background typecheck Stop hook
99
+ (asyncRewake) to ./.claude/settings.json. Script defaults to
100
+ typecheck or type-check. See skill audit-agent-speed.
101
+ --dry-run Show what would happen; make no changes.
102
+ --verify Read-only: fail if any packaged file is missing or
103
+ hash-mismatched at the destination (merge-compatible:
104
+ extra personal files are allowed).
105
+ --quiet Suppress install logs (errors still print).
106
+ --no-agents-mirror Skip copying skills to ~/.agents/skills.
107
+
108
+ What gets installed (Cursor):
109
+ ~/.cursor/skills/ ← agent skills at runtime (skills/ + non-builtin skills-cursor/)
110
+ ~/.agents/skills/ ← optional Skills UI mirror (same content; skip with --no-agents-mirror)
111
+ ~/.cursor/commands/ ← slash commands
112
+ ~/.cursor/agents/ ← subagent definitions
113
+ ~/.cursor/rules/ ← project rules starter pack
114
+ ~/.cursor/hooks.json ← merged completion stop hook (preserves existing hooks)
115
+ ~/.cursor/mcp.json ← MCP server template (only if missing; never overwritten)
116
+
117
+ What gets installed (Claude Code, with --claude or --all):
118
+ ~/.claude/skills/ ← global skills (appear as /slash-commands)
119
+ ~/.claude/commands/ ← custom slash commands
120
+ ~/.claude/agents/ ← subagent definitions
121
+ ~/.claude/rules/ ← rules (.mdc installed as .md)
122
+ ~/.claude/settings.json ← merged completion Stop hook (preserves existing hooks)
123
+
124
+ What gets installed (Codex CLI / Gemini CLI — no skills system):
125
+ ~/.codex/AGENTS.md ← rules merged into one context file (auto-loaded)
126
+ ~/.codex/prompts/ ← portable commands as custom prompts (plan, research, fix-issue)
127
+ ~/.gemini/GEMINI.md ← rules merged into one context file (auto-loaded)
128
+ ~/.gemini/commands/ ← portable commands as TOML commands
129
+ These tools have no skills loader, so skills/agents are not written there.
130
+ Existing AGENTS.md/GEMINI.md is backed up (.bak-<stamp>) before an update.
131
+ `.trim());
132
+ process.exit(0);
133
+ }
134
+
135
+ if (has('quiet', 'q')) {
136
+ console.log = () => {};
137
+ }
138
+
139
+ const isDryRun = has('dry-run');
140
+ const isVerifyOnly = has('verify');
141
+ const isClean = has('clean', 'mirror', 'force');
142
+ const noBackup = has('no-backup');
143
+ const useLink = has('link');
144
+ const noAgentsMirror = has('no-agents-mirror');
145
+ const verifyFailures = [];
146
+
147
+ /** Cursor already ships these as managed builtins. Do not copy into
148
+ * ~/.cursor/skills — a same-name user copy shadows the builtin and
149
+ * shows up twice in the `/` picker. Claude still gets portable copies. */
150
+ const CURSOR_MANAGED_BUILTIN_SKILLS = new Set([
151
+ 'babysit',
152
+ 'canvas',
153
+ 'create-hook',
154
+ 'create-rule',
155
+ 'create-skill',
156
+ 'create-subagent',
157
+ 'migrate-to-skills',
158
+ 'shell',
159
+ 'split-to-prs',
160
+ 'statusline',
161
+ 'update-cli-config',
162
+ 'update-cursor-settings',
163
+ ]);
164
+
165
+ /** Old skill directory → new skill directory. Empty until a rename ships.
166
+ * Tests may inject pairs via KENJI_RENAMED_SKILLS=old:new,old2:new2. */
167
+ const RENAMED_SKILLS = {
168
+ 'domain-modeling': 'docs-domain-modeling',
169
+ 'grilling': 'workflow-grilling',
170
+ };
171
+
172
+ function renamedSkillMap() {
173
+ const map = { ...RENAMED_SKILLS };
174
+ const extra = process.env.KENJI_RENAMED_SKILLS;
175
+ if (!extra) return map;
176
+ for (const pair of extra.split(',')) {
177
+ const [oldName, newName] = pair.split(':').map((s) => s.trim());
178
+ if (oldName && newName) map[oldName] = newName;
179
+ }
180
+ return map;
181
+ }
182
+
183
+ function shouldPruneRenamedSkills() {
184
+ if (skillName) return false;
185
+ if (onlyGroups && !onlyGroups.has('skills')) return false;
186
+ return true;
187
+ }
188
+
189
+ function pruneRenamedSkills(skillsDest, label) {
190
+ if (!shouldPruneRenamedSkills() || !skillsDest) return;
191
+ const map = renamedSkillMap();
192
+ for (const [oldName, newName] of Object.entries(map)) {
193
+ const stale = join(skillsDest, oldName);
194
+ if (!existsSync(stale)) continue;
195
+ if (isDryRun) {
196
+ console.log(` [dry-run] prune renamed ${label} skill ${oldName} → ${newName}`);
197
+ continue;
198
+ }
199
+ rmSync(stale, { recursive: true, force: true });
200
+ }
201
+ }
202
+
203
+ /** Old rule file → new rule file. Pruned in ~/.cursor/rules (.mdc) and
204
+ * ~/.claude/rules (.md) on merge installs, mirroring RENAMED_SKILLS. */
205
+ const RENAMED_RULES = {
206
+ 'composer-2.5-execution.mdc': 'approved-plan-execution.mdc',
207
+ };
208
+
209
+ function pruneRenamedRules(rulesDest, label, { renameMdc = false } = {}) {
210
+ if (skillName || (onlyGroups && !onlyGroups.has('rules')) || !rulesDest) return;
211
+ for (const [oldName, newName] of Object.entries(RENAMED_RULES)) {
212
+ const file = renameMdc ? oldName.replace(/\.mdc$/, '.md') : oldName;
213
+ const stale = join(rulesDest, file);
214
+ if (!existsSync(stale)) continue;
215
+ if (isDryRun) { console.log(` [dry-run] prune renamed ${label} rule ${file} → ${newName}`); continue; }
216
+ rmSync(stale, { force: true });
217
+ }
218
+ }
219
+
220
+ /** Old command file → new name. A stale copy is pruned only while it still
221
+ * carries a description this pack shipped, so a user's own file of the same
222
+ * name stays. */
223
+ const RENAMED_COMMANDS = {
224
+ 'plan.md': {
225
+ to: 'plan-mode.md',
226
+ descriptions: [
227
+ 'Research, clarify requirements, and produce an approved implementation plan before writing code (Cursor Plan Mode)',
228
+ ],
229
+ },
230
+ };
231
+
232
+ // Merge installs never delete, so leftovers from older releases stay unless
233
+ // pruned: renamed commands, and per-project command bundles that older
234
+ // installers copied into the global commands dir (where they register as
235
+ // /<bundle>:<name> in every project). Only files this pack ships are removed.
236
+ function pruneStaleCommands(commandsDest, label) {
237
+ if (skillName || (onlyGroups && !onlyGroups.has('commands')) || !commandsDest || !existsSync(commandsDest)) return;
238
+ for (const [oldName, { to, descriptions }] of Object.entries(RENAMED_COMMANDS)) {
239
+ const stale = join(commandsDest, oldName);
240
+ if (!existsSync(stale)) continue;
241
+ const { description } = parseFrontmatter(readFileSync(stale, 'utf8'));
242
+ if (!descriptions.includes(description)) continue;
243
+ if (isDryRun) { console.log(` [dry-run] prune renamed ${label} command ${oldName} → ${to}`); continue; }
244
+ rmSync(stale, { force: true });
245
+ }
246
+ for (const bundle of PROJECT_RULE_BUNDLES) {
247
+ const src = resolve(__dir, 'commands', bundle);
248
+ const dest = join(commandsDest, bundle);
249
+ if (!existsSync(src) || !existsSync(dest) || !statSync(dest).isDirectory()) continue;
250
+ for (const f of readdirSync(src)) {
251
+ const p = join(dest, f);
252
+ if (!existsSync(p)) continue;
253
+ if (isDryRun) { console.log(` [dry-run] prune per-project ${label} command ${bundle}/${f}`); continue; }
254
+ rmSync(p, { force: true });
255
+ }
256
+ if (!isDryRun && readdirSync(dest).length === 0) rmSync(dest, { recursive: true, force: true });
257
+ }
258
+ }
259
+
260
+ // ---- target resolution -----------------------------------------------------
261
+ // Backward compatible: a bare invocation still installs Cursor only.
262
+ // --auto detect installed tools (~/.cursor, ~/.claude, ~/.codex, ~/.gemini)
263
+ // and install to each; falls back to Cursor if none are found.
264
+ // --all force all four supported targets, installed or not.
265
+ // --cursor/--claude/--codex/--gemini explicit target(s); combine freely.
266
+ const codexBase = join(homedir(), '.codex'); // Codex CLI reads ~/.codex/AGENTS.md
267
+ const geminiBase = join(homedir(), '.gemini'); // Gemini CLI reads ~/.gemini/GEMINI.md
268
+ const cursorBase = join(homedir(), '.cursor');
269
+ const agentsBase = join(homedir(), '.agents'); // Cursor Skills UI reads ~/.agents/skills/
270
+ const claudeBase = join(homedir(), '.claude');
271
+
272
+ const explicitTargets = ['cursor', 'claude', 'codex', 'gemini'].filter((t) => has(t));
273
+ let targets;
274
+ if (has('all')) {
275
+ targets = new Set(['cursor', 'claude', 'codex', 'gemini']);
276
+ } else if (has('auto')) {
277
+ targets = detectTools();
278
+ if (targets.size === 0) targets = new Set(['cursor']);
279
+ } else if (explicitTargets.length) {
280
+ targets = new Set(explicitTargets);
281
+ } else {
282
+ targets = new Set(['cursor']); // backward-compatible default
283
+ }
284
+ const wantCursor = targets.has('cursor');
285
+ const wantClaude = targets.has('claude');
286
+ const wantCodex = targets.has('codex');
287
+ const wantGemini = targets.has('gemini');
288
+
289
+ if (has('auto')) {
290
+ console.log(`Auto-detected targets: ${[...targets].join(', ') || '(none — defaulting to Cursor)'}`);
291
+ }
292
+
293
+ const ALL_DIRS = [
294
+ { src: 'skills', dest: 'skills' },
295
+ { src: 'skills-cursor', dest: 'skills' }, // merge cursor-specific skills
296
+ { src: 'commands', dest: 'commands' },
297
+ { src: 'agents', dest: 'agents' },
298
+ { src: 'rules', dest: 'rules' },
299
+ ];
300
+
301
+ // Per-project bundles: copied into a specific repo's .cursor/rules and
302
+ // .cursor/commands by the user (see each bundle's README), never into the
303
+ // global dirs — their always/glob rules would fire in every project, and a
304
+ // global commands copy registers the bundle's README.md as /<bundle>:README.
305
+ const PROJECT_RULE_BUNDLES = new Set(['native-rn-monorepo', 'project-starter']);
306
+
307
+ // Installed folder names. Releases before 2.0.0 shipped as cursor-kenji and
308
+ // used the legacy names; installs migrate them (ADR-0013).
309
+ const HOOK_DIR = 'kenji-hooks';
310
+ const LEGACY_HOOK_DIR = 'cursor-kenji-hooks';
311
+ const BACKUPS_DIR = '.kenji-backups';
312
+ const LEGACY_BACKUPS_DIR = '.cursor-kenji-backups';
313
+ // Matches the managed completion-gate entry under either folder name.
314
+ const MANAGED_GATE = /(?:cursor-)?kenji-hooks[\\/]completion-gate\.mjs/;
315
+
316
+ // ---- typecheck-hook mode: per-repo background typecheck (audit-agent-speed) --
317
+ // Writes into the current repo only, never a global config: a global Stop hook
318
+ // would run in every repo, including ones without the script.
319
+ if (has('typecheck-hook')) {
320
+ const repo = process.cwd();
321
+ const pkgPath = join(repo, 'package.json');
322
+ if (!existsSync(join(repo, '.git')) || !existsSync(pkgPath)) {
323
+ console.error('--typecheck-hook: run it from a repo root that has .git and package.json.');
324
+ process.exit(1);
325
+ }
326
+ const scripts = JSON.parse(readFileSync(pkgPath, 'utf8')).scripts ?? {};
327
+ const script = typeof opts['typecheck-hook'] === 'string'
328
+ ? opts['typecheck-hook']
329
+ : ['typecheck', 'type-check'].find((name) => name in scripts);
330
+ if (!script || !(script in scripts)) {
331
+ console.error(`--typecheck-hook: package.json has no ${script ? `"${script}"` : '"typecheck" or "type-check"'} script. Pass the name: --typecheck-hook <script>.`);
332
+ process.exit(1);
333
+ }
334
+
335
+ const source = resolve(__dir, 'skills', 'audit-agent-speed', 'scripts', 'stop-typecheck.mjs');
336
+ const destScript = join(repo, '.claude', 'hooks', 'stop-typecheck.mjs');
337
+ const settingsPath = join(repo, '.claude', 'settings.json');
338
+ let settings = {};
339
+ if (existsSync(settingsPath)) {
340
+ try {
341
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
342
+ } catch {
343
+ console.error(`--typecheck-hook: ${settingsPath} is not valid JSON; nothing changed.`);
344
+ process.exit(1);
345
+ }
346
+ }
347
+ if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
348
+ const managed = /stop-typecheck\.mjs/;
349
+ const groups = (Array.isArray(settings.hooks.Stop) ? settings.hooks.Stop : [])
350
+ .map((group) => ({ ...group, hooks: (group?.hooks ?? []).filter((h) => !managed.test(String(h?.command ?? ''))) }))
351
+ .filter((group) => group.hooks.length > 0);
352
+ const blocking = groups.flatMap((g) => g.hooks).filter((h) => !h.async && !h.asyncRewake && /typecheck|type-check|tsc\b/.test(String(h.command ?? '')));
353
+ settings.hooks.Stop = [
354
+ ...groups,
355
+ { hooks: [{ type: 'command', command: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-typecheck.mjs" ${script}`, asyncRewake: true, timeout: 600 }] },
356
+ ];
357
+
358
+ if (isDryRun) {
359
+ console.log(` [dry-run] ${source} → ${destScript}`);
360
+ } else {
361
+ mkdirSync(dirname(destScript), { recursive: true });
362
+ writeFileSync(destScript, readFileSync(source));
363
+ }
364
+ const status = writeManagedFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
365
+ console.log(`${isDryRun ? '[dry-run] ' : '✓ '}Background typecheck Stop hook (${script}) → ${settingsPath} (${status})`);
366
+ for (const h of blocking) {
367
+ console.log(` [!] A blocking typecheck Stop hook is still registered: ${h.command}\n Remove it; the background hook replaces it.`);
368
+ }
369
+ process.exit(0);
370
+ }
371
+
372
+ // ---- restore mode ----------------------------------------------------------
373
+ if (has('restore')) {
374
+ const restoreBase = wantClaude && !wantCursor ? claudeBase : cursorBase;
375
+ const backupsRoot = [BACKUPS_DIR, LEGACY_BACKUPS_DIR]
376
+ .map((dir) => join(restoreBase, dir))
377
+ .find((dir) => existsSync(dir)) ?? join(restoreBase, BACKUPS_DIR);
378
+ const stamp = typeof opts.restore === 'string'
379
+ ? opts.restore
380
+ : (existsSync(backupsRoot)
381
+ ? readdirSync(backupsRoot).filter((n) => statSync(join(backupsRoot, n)).isDirectory()).sort().pop()
382
+ : null);
383
+ if (!stamp) { console.error('No backups found under ' + backupsRoot); process.exit(1); }
384
+ const snap = join(backupsRoot, stamp);
385
+ if (!existsSync(snap)) { console.error('Backup not found: ' + snap); process.exit(1); }
386
+
387
+ let restored = 0;
388
+ for (const dest of readdirSync(snap)) {
389
+ const from = join(snap, dest);
390
+ const to = join(restoreBase, dest);
391
+ if (isDryRun) {
392
+ console.log(` [dry-run] restore ${from} → ${to}`);
393
+ } else {
394
+ rmSync(to, { recursive: true, force: true });
395
+ cpSync(from, to, { recursive: true });
396
+ }
397
+ restored++;
398
+ }
399
+ console.log(`${isDryRun ? '[dry-run] ' : '✓ '}Restored ${restored} dir(s) from ${snap}`);
400
+ process.exit(0);
401
+ }
402
+
403
+ // ---- selection (--only / --skill) ------------------------------------------
404
+ const skillName = typeof opts.skill === 'string' ? opts.skill : null;
405
+ let onlyGroups = null;
406
+ if (typeof opts.only === 'string') onlyGroups = new Set(opts.only.split(',').map((s) => s.trim()).filter(Boolean));
407
+ if (skillName) onlyGroups = new Set(['skills']);
408
+
409
+ const DIRS = ALL_DIRS.filter((d) => !onlyGroups || onlyGroups.has(d.dest));
410
+ const managedDests = [...new Set(DIRS.map((d) => d.dest))];
411
+
412
+ // Text files are hashed with line endings normalized: a Windows checkout with
413
+ // core.autocrlf=true carries CRLF while the npm tarball (built on Linux) is
414
+ // LF, and --verify must not report identical content as a mismatch.
415
+ const TEXT_EXT = /\.(md|mdc|mjs|js|cjs|ts|tsx|json|jsonc|txt|toml|ya?ml|sh|cmd|svg|css|html)$/i;
416
+ function fileHash(p) {
417
+ const buf = readFileSync(p);
418
+ const data = TEXT_EXT.test(p) ? buf.toString('utf8').replace(/\r\n/g, '\n') : buf;
419
+ return createHash('sha256').update(data).digest('hex');
420
+ }
421
+
422
+ function walkRelFiles(root) {
423
+ const out = [];
424
+ const walk = (rel) => {
425
+ const p = rel ? join(root, rel) : root;
426
+ if (!existsSync(p)) return;
427
+ const st = statSync(p);
428
+ if (st.isDirectory()) {
429
+ for (const name of readdirSync(p).sort()) {
430
+ walk(rel ? `${rel}/${name}` : name);
431
+ }
432
+ return;
433
+ }
434
+ out.push(rel.replace(/\\/g, '/'));
435
+ };
436
+ walk('');
437
+ return out;
438
+ }
439
+
440
+ /** Source tree must be a subset of dest with matching file hashes. Extra dest files are allowed (merge). */
441
+ function treesMatch(src, dest) {
442
+ if (!existsSync(dest)) return { ok: false, reason: 'missing destination' };
443
+ const sDir = statSync(src).isDirectory();
444
+ const dDir = statSync(dest).isDirectory();
445
+ if (sDir !== dDir) return { ok: false, reason: 'file/directory type mismatch' };
446
+ if (!sDir) {
447
+ return fileHash(src) === fileHash(dest)
448
+ ? { ok: true }
449
+ : { ok: false, reason: 'content hash mismatch' };
450
+ }
451
+ for (const rel of walkRelFiles(src)) {
452
+ const from = join(src, rel);
453
+ const to = join(dest, rel);
454
+ if (!existsSync(to)) return { ok: false, reason: `missing ${rel}` };
455
+ if (statSync(to).isDirectory()) return { ok: false, reason: `${rel} is a directory at dest` };
456
+ if (fileHash(from) !== fileHash(to)) return { ok: false, reason: `content hash mismatch in ${rel}` };
457
+ }
458
+ return { ok: true };
459
+ }
460
+
461
+ function listInstallItems({ renameMdc = false, skipCursorBuiltins = false } = {}) {
462
+ const items = [];
463
+ for (const { src, dest } of DIRS) {
464
+ const srcPath = resolve(__dir, src);
465
+ if (!existsSync(srcPath)) continue;
466
+ for (const item of readdirSync(srcPath)) {
467
+ if (skillName && item !== skillName) continue;
468
+ if (
469
+ skipCursorBuiltins &&
470
+ src === 'skills-cursor' &&
471
+ dest === 'skills' &&
472
+ CURSOR_MANAGED_BUILTIN_SKILLS.has(item) &&
473
+ item !== skillName
474
+ ) {
475
+ continue;
476
+ }
477
+ const itemSrc = join(srcPath, item);
478
+ const isDir = statSync(itemSrc).isDirectory();
479
+ if (isDir && PROJECT_RULE_BUNDLES.has(item) && (dest === 'rules' || dest === 'commands')) continue;
480
+ if (!renameMdc && dest === 'rules' && !isDir && item.endsWith('.md')) continue;
481
+ let outName = item;
482
+ if (renameMdc && dest === 'rules' && !isDir && item.endsWith('.mdc')) {
483
+ outName = item.slice(0, -4) + '.md';
484
+ }
485
+ items.push({ dest, outName, itemSrc, isDir });
486
+ }
487
+ }
488
+ return items;
489
+ }
490
+
491
+ function recordVerify(src, dest, label) {
492
+ const match = treesMatch(src, dest);
493
+ if (!match.ok) verifyFailures.push(`${label}: ${match.reason}`);
494
+ return match.ok;
495
+ }
496
+
497
+ function dualSkillCommandNames() {
498
+ const skillNames = new Set();
499
+ for (const group of ['skills', 'skills-cursor']) {
500
+ const dir = resolve(__dir, group);
501
+ if (!existsSync(dir)) continue;
502
+ for (const name of readdirSync(dir)) skillNames.add(name);
503
+ }
504
+ const cmdDir = resolve(__dir, 'commands');
505
+ if (!existsSync(cmdDir)) return [];
506
+ return readdirSync(cmdDir)
507
+ .filter((f) => f.endsWith('.md'))
508
+ .map((f) => f.slice(0, -3))
509
+ .filter((n) => skillNames.has(n))
510
+ .sort();
511
+ }
512
+
513
+ function assertNoCursorBuiltins(skillsDir) {
514
+ if (!existsSync(skillsDir)) return;
515
+ for (const name of CURSOR_MANAGED_BUILTIN_SKILLS) {
516
+ if (skillName && name === skillName) continue;
517
+ if (existsSync(join(skillsDir, name))) {
518
+ verifyFailures.push(`skills/${name}: Cursor-managed builtin still present (should be absent)`);
519
+ }
520
+ }
521
+ }
522
+
523
+ function verifyTarget(base, opts, label) {
524
+ for (const it of listInstallItems(opts)) {
525
+ recordVerify(it.itemSrc, join(base, it.dest, it.outName), `${label} ${it.dest}/${it.outName}`);
526
+ }
527
+ if (opts.skipCursorBuiltins) assertNoCursorBuiltins(join(base, 'skills'));
528
+ }
529
+
530
+ function failIfUnverified(context) {
531
+ if (!verifyFailures.length) return;
532
+ console.error(`✗ ${context} failed:`);
533
+ for (const f of verifyFailures) console.error(` - ${f}`);
534
+ process.exit(1);
535
+ }
536
+
537
+ // ---- placement helper (copy or symlink) ------------------------------------
538
+ let linkFallbacks = 0;
539
+ function place(src, dest, isDir) {
540
+ if (isDryRun) { console.log(` [dry-run] ${useLink ? 'link' : 'copy'} ${src} → ${dest}`); return; }
541
+ if (useLink) {
542
+ rmSync(dest, { recursive: true, force: true });
543
+ try {
544
+ const type = isDir ? (platform() === 'win32' ? 'junction' : 'dir') : 'file';
545
+ symlinkSync(src, dest, type);
546
+ } catch {
547
+ cpSync(src, dest, { recursive: true }); // file symlinks may need privileges on Windows
548
+ linkFallbacks++;
549
+ }
550
+ } else {
551
+ cpSync(src, dest, { recursive: true });
552
+ }
553
+ recordVerify(src, dest, dest);
554
+ }
555
+
556
+ // ---- mirror mode: back up, then wipe managed dirs under a target base ------
557
+ function backupAndWipe(base) {
558
+ let wiped = 0;
559
+ let backupRoot = null;
560
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
561
+ backupRoot = join(base, BACKUPS_DIR, stamp);
562
+ for (const dest of managedDests) {
563
+ const p = join(base, dest);
564
+ if (!existsSync(p)) continue;
565
+ if (isDryRun) {
566
+ console.log(` [dry-run] backup ${p} → ${join(backupRoot, dest)}`);
567
+ console.log(` [dry-run] wipe ${p}`);
568
+ } else {
569
+ if (!noBackup) { mkdirSync(backupRoot, { recursive: true }); cpSync(p, join(backupRoot, dest), { recursive: true }); }
570
+ rmSync(p, { recursive: true, force: true });
571
+ }
572
+ wiped++;
573
+ }
574
+ return { wiped, backupRoot };
575
+ }
576
+
577
+ // ---- copy / link one target base -------------------------------------------
578
+ // renameMdc: Claude Code reads .md rules; .mdc sources are installed as .md.
579
+ function installDirs(base, opts = {}) {
580
+ let copiedDirs = 0;
581
+ let copiedFiles = 0;
582
+ const ensured = new Set();
583
+ for (const it of listInstallItems(opts)) {
584
+ const destPath = join(base, it.dest);
585
+ if (!isDryRun && !ensured.has(destPath)) {
586
+ mkdirSync(destPath, { recursive: true });
587
+ ensured.add(destPath);
588
+ }
589
+ place(it.itemSrc, join(destPath, it.outName), it.isDir);
590
+ if (it.isDir) copiedDirs++; else copiedFiles++;
591
+ }
592
+ return { copiedDirs, copiedFiles };
593
+ }
594
+
595
+ // ============================================================================
596
+ // Codex CLI + Gemini CLI — tools with NO skills system.
597
+ // They read a single global context/instructions file, so we map:
598
+ // rules/ (minus the skill-routing index) → one merged Markdown file
599
+ // commands-portable/ → per-tool custom prompt/command files
600
+ // No SKILL.md dirs are written — nothing in these tools would load them.
601
+ // ============================================================================
602
+
603
+ // Detect installed tools by their home-dir config footprint.
604
+ function detectTools() {
605
+ const found = new Set();
606
+ if (existsSync(cursorBase)) found.add('cursor');
607
+ if (existsSync(claudeBase)) found.add('claude');
608
+ if (existsSync(codexBase)) found.add('codex');
609
+ if (existsSync(geminiBase)) found.add('gemini');
610
+ return found;
611
+ }
612
+
613
+ // Rules merged into the global context file. The skill-routing index is
614
+ // excluded — it points at skills these tools cannot load (would be dead text).
615
+ const RULES_EXCLUDE = new Set(['skill-workflows.mdc']);
616
+ const RULES_ORDER = [
617
+ 'senior-engineer.mdc',
618
+ 'full-stack-ship-discipline.mdc',
619
+ 'approved-plan-execution.mdc', // renamed from composer-2.5-execution.mdc
620
+ 'shell-first-search.mdc', // was .md; now agent-requested in Cursor too
621
+ ];
622
+
623
+ // Strip a leading `--- ... ---` YAML frontmatter block, returning the body.
624
+ function stripFrontmatter(text) {
625
+ if (!text.startsWith('---')) return text;
626
+ const end = text.indexOf('\n---', 3);
627
+ if (end === -1) return text;
628
+ const after = text.indexOf('\n', end + 1);
629
+ return text.slice(after + 1).replace(/^\s+/, '');
630
+ }
631
+
632
+ // Parse `description:` out of frontmatter and return { description, body }.
633
+ function parseFrontmatter(text) {
634
+ let description = '';
635
+ if (text.startsWith('---')) {
636
+ const end = text.indexOf('\n---', 3);
637
+ if (end !== -1) {
638
+ const m = text.slice(3, end).match(/description:\s*"?(.*?)"?\s*$/m);
639
+ if (m) description = m[1];
640
+ }
641
+ }
642
+ return { description, body: stripFrontmatter(text) };
643
+ }
644
+
645
+ // Concatenate top-level rule files (non-recursive — skips project-starter/ etc.)
646
+ // into one deterministic Markdown document. No timestamp → regeneration is
647
+ // idempotent, so re-running the installer produces byte-identical output.
648
+ function buildMergedRules(toolLabel, loadPath) {
649
+ const rulesDir = resolve(__dir, 'rules');
650
+ for (const name of RULES_ORDER) {
651
+ if (!existsSync(join(rulesDir, name))) {
652
+ throw new Error(`RULES_ORDER names a missing rule: rules/${name} — update the list after a rename`);
653
+ }
654
+ }
655
+ const files = readdirSync(rulesDir)
656
+ .filter((f) => (f.endsWith('.md') || f.endsWith('.mdc'))
657
+ && !RULES_EXCLUDE.has(f)
658
+ && statSync(join(rulesDir, f)).isFile())
659
+ .sort((a, b) => {
660
+ const ia = RULES_ORDER.indexOf(a); const ib = RULES_ORDER.indexOf(b);
661
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);
662
+ });
663
+ const header = `<!-- Generated by kenji — https://github.com/kensaurus/skills
664
+ Merged from rules/. The skill-routing index is omitted (this tool has no skills loader).
665
+ Applies to: ${toolLabel} — loaded automatically from ${loadPath}.
666
+ Regenerate: npx @kensaurus/skills --auto -->\n\n`;
667
+ const body = files
668
+ .map((f) => stripFrontmatter(readFileSync(join(rulesDir, f), 'utf8')).trim())
669
+ .join('\n\n---\n\n');
670
+ return header + body + '\n';
671
+ }
672
+
673
+ // Load the tool-agnostic portable commands (single source of truth).
674
+ function portableCommands() {
675
+ const dir = resolve(__dir, 'commands-portable');
676
+ if (!existsSync(dir)) return [];
677
+ return readdirSync(dir)
678
+ .filter((f) => f.endsWith('.md'))
679
+ .sort()
680
+ .map((f) => {
681
+ const { description, body } = parseFrontmatter(readFileSync(join(dir, f), 'utf8'));
682
+ return { name: f.replace(/\.md$/, ''), description, body: body.trim() };
683
+ });
684
+ }
685
+
686
+ // Wrap a portable command as a Gemini TOML command. Uses a literal ''' string
687
+ // so shell backslashes/quotes pass through verbatim; fails loud if the body
688
+ // would collide with the delimiter rather than silently corrupting output.
689
+ function toGeminiToml({ name, description, body }) {
690
+ if (body.includes("'''")) {
691
+ throw new Error(`portable command '${name}' contains ''' — cannot wrap as a TOML literal string`);
692
+ }
693
+ const desc = String(description).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
694
+ return `description = "${desc}"\n\nprompt = '''\n${body}\n'''\n`;
695
+ }
696
+
697
+ // Write a single managed file. Idempotent (skips identical content), backs up
698
+ // on overwrite unless --no-backup, and honours --dry-run.
699
+ function writeManagedFile(destPath, content) {
700
+ if (isDryRun) { console.log(` [dry-run] write ${destPath}`); return 'dry-run'; }
701
+ mkdirSync(dirname(destPath), { recursive: true });
702
+ if (existsSync(destPath)) {
703
+ if (readFileSync(destPath, 'utf8') === content) return 'unchanged';
704
+ if (!noBackup) {
705
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
706
+ cpSync(destPath, `${destPath}.bak-${stamp}`);
707
+ }
708
+ writeFileSync(destPath, content);
709
+ return 'updated';
710
+ }
711
+ writeFileSync(destPath, content);
712
+ return 'created';
713
+ }
714
+
715
+ // Install the opt-in completion gate as a user hook. The hook itself is inert
716
+ // unless the active workspace has an unfinished durable closure-state file.
717
+ // Existing user hooks are preserved and this managed entry is replaced
718
+ // idempotently on upgrades.
719
+ // The managed entry now points at HOOK_DIR, so a pre-2.0.0 LEGACY_HOOK_DIR copy
720
+ // is orphaned. Remove it only when it holds nothing but the gate script.
721
+ function removeLegacyHookDir(base) {
722
+ const legacyDir = join(base, LEGACY_HOOK_DIR);
723
+ if (!existsSync(legacyDir)) return;
724
+ const entries = readdirSync(legacyDir);
725
+ if (entries.some((name) => name !== 'completion-gate.mjs')) return;
726
+ if (isDryRun) {
727
+ console.log(` [dry-run] remove legacy ${legacyDir}`);
728
+ return;
729
+ }
730
+ rmSync(legacyDir, { recursive: true, force: true });
731
+ }
732
+
733
+ function installCursorCompletionHook() {
734
+ const sourceScript = resolve(__dir, 'hooks', 'completion-gate.mjs');
735
+ const hookDir = join(cursorBase, HOOK_DIR);
736
+ const destScript = join(hookDir, 'completion-gate.mjs');
737
+ const configPath = join(cursorBase, 'hooks.json');
738
+ if (!existsSync(sourceScript)) return 'source-missing';
739
+
740
+ if (isDryRun) {
741
+ console.log(` [dry-run] ${sourceScript} → ${destScript}`);
742
+ } else {
743
+ mkdirSync(hookDir, { recursive: true });
744
+ place(sourceScript, destScript, false);
745
+ }
746
+
747
+ let config = { version: 1, hooks: {} };
748
+ if (existsSync(configPath)) {
749
+ try {
750
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
751
+ } catch {
752
+ console.warn(` [!] Skipped completion hook: ${configPath} is not valid JSON.`);
753
+ return 'skipped-invalid-config';
754
+ }
755
+ }
756
+
757
+ if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
758
+ if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
759
+ config.hooks = {};
760
+ }
761
+ if (!('version' in config)) config.version = 1;
762
+
763
+ const existing = Array.isArray(config.hooks.stop) ? config.hooks.stop : [];
764
+ const preserved = existing.filter(
765
+ (entry) => !MANAGED_GATE.test(String(entry?.command ?? '')),
766
+ );
767
+ config.hooks.stop = [
768
+ ...preserved,
769
+ {
770
+ command: `node ${HOOK_DIR}/completion-gate.mjs --host=cursor`,
771
+ timeout: 5,
772
+ loop_limit: 12,
773
+ failClosed: false,
774
+ },
775
+ ];
776
+
777
+ const status = writeManagedFile(configPath, JSON.stringify(config, null, 2) + '\n');
778
+ removeLegacyHookDir(cursorBase);
779
+ return status;
780
+ }
781
+
782
+ // Claude Code reads Stop hooks from ~/.claude/settings.json. Same script,
783
+ // Claude schema; the managed entry is replaced idempotently and user hooks
784
+ // in other groups are preserved. Cursor loads this file too (Third-Party
785
+ // Imports), so `--host=claude` lets the script stand aside there when the
786
+ // native Cursor entry exists.
787
+ function installClaudeCompletionHook() {
788
+ const sourceScript = resolve(__dir, 'hooks', 'completion-gate.mjs');
789
+ const hookDir = join(claudeBase, HOOK_DIR);
790
+ const destScript = join(hookDir, 'completion-gate.mjs');
791
+ const settingsPath = join(claudeBase, 'settings.json');
792
+ if (!existsSync(sourceScript)) return 'source-missing';
793
+
794
+ if (isDryRun) {
795
+ console.log(` [dry-run] ${sourceScript} → ${destScript}`);
796
+ } else {
797
+ mkdirSync(hookDir, { recursive: true });
798
+ place(sourceScript, destScript, false);
799
+ }
800
+
801
+ let settings = {};
802
+ if (existsSync(settingsPath)) {
803
+ try {
804
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
805
+ } catch {
806
+ console.warn(` [!] Skipped completion hook: ${settingsPath} is not valid JSON.`);
807
+ return 'skipped-invalid-config';
808
+ }
809
+ }
810
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) settings = {};
811
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
812
+
813
+ const existing = Array.isArray(settings.hooks.Stop) ? settings.hooks.Stop : [];
814
+ const preserved = existing
815
+ .map((group) => ({
816
+ ...group,
817
+ hooks: (Array.isArray(group?.hooks) ? group.hooks : []).filter(
818
+ (h) => !MANAGED_GATE.test(String(h?.command ?? '')),
819
+ ),
820
+ }))
821
+ .filter((group) => group.hooks.length > 0);
822
+ settings.hooks.Stop = [
823
+ ...preserved,
824
+ { hooks: [{ type: 'command', command: `node "${destScript.replace(/\\/g, '/')}" --host=claude`, timeout: 5 }] },
825
+ ];
826
+ const status = writeManagedFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
827
+ removeLegacyHookDir(claudeBase);
828
+ return status;
829
+ }
830
+
831
+ // Install a context-file tool (Codex / Gemini). `spec` describes its layout.
832
+ function installContextTool(spec) {
833
+ const { label, base, rulesFile, rulesLoadPath, commandsDir, commandExt, render } = spec;
834
+ console.log(`\n${isDryRun ? '[dry-run] ' : ''}${label} → ${base}`);
835
+
836
+ // --skill / --only skills has no meaning here (no skills loader).
837
+ const doRules = !onlyGroups || onlyGroups.has('rules');
838
+ const doCommands = (!onlyGroups || onlyGroups.has('commands')) && !skillName;
839
+ if (!doRules && !doCommands) {
840
+ console.log(' (no skills loader — nothing to install for the selected groups)');
841
+ return;
842
+ }
843
+
844
+ let rulesStatus = 'skipped';
845
+ if (doRules) {
846
+ rulesStatus = writeManagedFile(join(base, rulesFile), buildMergedRules(label, rulesLoadPath));
847
+ }
848
+ let cmdCount = 0;
849
+ if (doCommands) {
850
+ for (const cmd of portableCommands()) {
851
+ writeManagedFile(join(base, commandsDir, `${cmd.name}${commandExt}`), render(cmd));
852
+ cmdCount++;
853
+ }
854
+ }
855
+ if (!isDryRun) {
856
+ const parts = [];
857
+ if (doRules) parts.push(`${rulesFile} ${rulesStatus}`);
858
+ if (doCommands) parts.push(`${cmdCount} command(s) → ${commandsDir}/`);
859
+ console.log(` ✓ ${parts.join(', ')}`);
860
+ }
861
+ }
862
+
863
+ function verifyContextTool(spec) {
864
+ const { label, base, rulesFile, rulesLoadPath, commandsDir, commandExt, render } = spec;
865
+ const doRules = !onlyGroups || onlyGroups.has('rules');
866
+ const doCommands = (!onlyGroups || onlyGroups.has('commands')) && !skillName;
867
+ if (doRules) {
868
+ const dest = join(base, rulesFile);
869
+ const expected = buildMergedRules(label, rulesLoadPath);
870
+ if (!existsSync(dest)) verifyFailures.push(`${label} ${rulesFile}: missing destination`);
871
+ else if (readFileSync(dest, 'utf8') !== expected) verifyFailures.push(`${label} ${rulesFile}: content hash mismatch`);
872
+ }
873
+ if (doCommands) {
874
+ for (const cmd of portableCommands()) {
875
+ const dest = join(base, commandsDir, `${cmd.name}${commandExt}`);
876
+ const expected = render(cmd);
877
+ if (!existsSync(dest)) verifyFailures.push(`${label} ${cmd.name}: missing destination`);
878
+ else if (readFileSync(dest, 'utf8') !== expected) verifyFailures.push(`${label} ${cmd.name}: content hash mismatch`);
879
+ }
880
+ }
881
+ }
882
+
883
+ if (isVerifyOnly) {
884
+ if (wantCursor) {
885
+ verifyTarget(cursorBase, { skipCursorBuiltins: true }, 'Cursor');
886
+ if (!noAgentsMirror && (!onlyGroups || onlyGroups.has('skills'))) {
887
+ const agentsSkillsDest = join(agentsBase, 'skills');
888
+ for (const it of listInstallItems({ skipCursorBuiltins: true })) {
889
+ if (it.dest !== 'skills') continue;
890
+ recordVerify(it.itemSrc, join(agentsSkillsDest, it.outName), `Agents ${it.outName}`);
891
+ }
892
+ assertNoCursorBuiltins(agentsSkillsDest);
893
+ }
894
+ if (!onlyGroups || onlyGroups.has('hooks')) {
895
+ const hookSrc = resolve(__dir, 'hooks', 'completion-gate.mjs');
896
+ const hookDest = join(cursorBase, HOOK_DIR, 'completion-gate.mjs');
897
+ if (existsSync(hookSrc)) recordVerify(hookSrc, hookDest, 'Cursor completion-gate.mjs');
898
+ }
899
+ }
900
+ if (wantClaude) {
901
+ verifyTarget(claudeBase, { renameMdc: true }, 'Claude');
902
+ if (!onlyGroups || onlyGroups.has('hooks')) {
903
+ const hookSrc = resolve(__dir, 'hooks', 'completion-gate.mjs');
904
+ const hookDest = join(claudeBase, HOOK_DIR, 'completion-gate.mjs');
905
+ if (existsSync(hookSrc)) recordVerify(hookSrc, hookDest, 'Claude completion-gate.mjs');
906
+ }
907
+ }
908
+ if (wantCodex) {
909
+ verifyContextTool({
910
+ label: 'Codex CLI (OpenAI)',
911
+ base: codexBase,
912
+ rulesFile: 'AGENTS.md',
913
+ rulesLoadPath: '~/.codex/AGENTS.md',
914
+ commandsDir: 'prompts',
915
+ commandExt: '.md',
916
+ render: (cmd) => (cmd.body.endsWith('\n') ? cmd.body : cmd.body + '\n'),
917
+ });
918
+ }
919
+ if (wantGemini) {
920
+ verifyContextTool({
921
+ label: 'Gemini CLI',
922
+ base: geminiBase,
923
+ rulesFile: 'GEMINI.md',
924
+ rulesLoadPath: '~/.gemini/GEMINI.md',
925
+ commandsDir: 'commands',
926
+ commandExt: '.toml',
927
+ render: toGeminiToml,
928
+ });
929
+ }
930
+ failIfUnverified('install verify');
931
+ console.log('✓ install verify passed — every packaged file matches the destination.');
932
+ process.exit(0);
933
+ }
934
+
935
+ const results = [];
936
+
937
+ // ---- Cursor ----------------------------------------------------------------
938
+ if (wantCursor) {
939
+ const clean = isClean ? backupAndWipe(cursorBase) : null;
940
+ const counts = installDirs(cursorBase, { skipCursorBuiltins: true });
941
+ if (!skillName && !isDryRun) {
942
+ const cursorSkills = join(cursorBase, 'skills');
943
+ for (const name of CURSOR_MANAGED_BUILTIN_SKILLS) {
944
+ const stale = join(cursorSkills, name);
945
+ if (existsSync(stale)) rmSync(stale, { recursive: true, force: true });
946
+ }
947
+ assertNoCursorBuiltins(cursorSkills);
948
+ }
949
+ pruneRenamedSkills(join(cursorBase, 'skills'), 'Cursor');
950
+ pruneRenamedRules(join(cursorBase, 'rules'), 'Cursor');
951
+ pruneStaleCommands(join(cursorBase, 'commands'), 'Cursor');
952
+ const hookStatus =
953
+ !onlyGroups || onlyGroups.has('hooks')
954
+ ? installCursorCompletionHook()
955
+ : 'skipped';
956
+
957
+ if (skillName && counts.copiedDirs + counts.copiedFiles === 0) {
958
+ console.error(`✗ Skill '${skillName}' not found in skills/ or skills-cursor/.`);
959
+ process.exit(1);
960
+ }
961
+
962
+ // Also write skills to ~/.agents/skills/ (Cursor Skills UI historically reads here).
963
+ if (!noAgentsMirror && (!onlyGroups || onlyGroups.has('skills'))) {
964
+ const agentsSkillsDest = join(agentsBase, 'skills');
965
+ const cursorSkillsSrc = join(cursorBase, 'skills');
966
+ if (isDryRun) {
967
+ console.log(` [dry-run] sync ${cursorSkillsSrc} → ${agentsSkillsDest}`);
968
+ } else if (existsSync(cursorSkillsSrc)) {
969
+ if (isClean) rmSync(agentsSkillsDest, { recursive: true, force: true });
970
+ mkdirSync(agentsSkillsDest, { recursive: true });
971
+ for (const item of readdirSync(cursorSkillsSrc)) {
972
+ if (skillName && item !== skillName) continue;
973
+ const from = join(cursorSkillsSrc, item);
974
+ const to = join(agentsSkillsDest, item);
975
+ cpSync(from, to, { recursive: true });
976
+ recordVerify(from, to, `Agents ${item}`);
977
+ }
978
+ if (!skillName) {
979
+ for (const name of CURSOR_MANAGED_BUILTIN_SKILLS) {
980
+ const stale = join(agentsSkillsDest, name);
981
+ if (existsSync(stale)) rmSync(stale, { recursive: true, force: true });
982
+ }
983
+ assertNoCursorBuiltins(agentsSkillsDest);
984
+ }
985
+ pruneRenamedSkills(agentsSkillsDest, 'Agents');
986
+ }
987
+ }
988
+
989
+ // MCP config template (only if missing; never overwritten).
990
+ const mcpDest = join(cursorBase, 'mcp.json');
991
+ const mcpTemplate = resolve(__dir, 'mcp', 'mcp.json.template');
992
+ let mcpInstalled = false;
993
+ if (!onlyGroups && existsSync(mcpTemplate) && !existsSync(mcpDest)) {
994
+ if (isDryRun) console.log(` [dry-run] ${mcpTemplate} → ${mcpDest}`);
995
+ else { mkdirSync(cursorBase, { recursive: true }); cpSync(mcpTemplate, mcpDest); }
996
+ mcpInstalled = true;
997
+ }
998
+
999
+ results.push({ target: 'Cursor', base: cursorBase, ...counts, clean, mcpInstalled, hookStatus });
1000
+ }
1001
+
1002
+ // ---- Claude Code -----------------------------------------------------------
1003
+ if (wantClaude) {
1004
+ const clean = isClean ? backupAndWipe(claudeBase) : null;
1005
+ const counts = installDirs(claudeBase, { renameMdc: true });
1006
+ pruneRenamedSkills(join(claudeBase, 'skills'), 'Claude');
1007
+ pruneRenamedRules(join(claudeBase, 'rules'), 'Claude', { renameMdc: true });
1008
+ pruneStaleCommands(join(claudeBase, 'commands'), 'Claude');
1009
+ const hookStatus = !onlyGroups || onlyGroups.has('hooks') ? installClaudeCompletionHook() : 'skipped';
1010
+
1011
+ if (skillName && counts.copiedDirs + counts.copiedFiles === 0) {
1012
+ console.error(`✗ Skill '${skillName}' not found in skills/ or skills-cursor/.`);
1013
+ process.exit(1);
1014
+ }
1015
+
1016
+ if (!isDryRun) {
1017
+ // Claude Code lists every model-invocable skill and command as
1018
+ // "- name: description" against 1% of the context window at 3 chars/token
1019
+ // for current models (30,000 chars on a 1M Opus 5.5 session), shared with
1020
+ // built-in skills; over budget the least-used entries drop to name only
1021
+ // (client 2.1.280, ADR-0010).
1022
+ let listingChars = 0;
1023
+ let entries = 0;
1024
+ const addEntry = (name, file) => {
1025
+ const text = readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
1026
+ const end = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
1027
+ if (end === -1) return;
1028
+ const front = text.slice(3, end);
1029
+ if (/^disable-model-invocation:\s*true\s*$/m.test(front)) return;
1030
+ const lines = front.split('\n');
1031
+ const s = lines.findIndex((l) => /^description:/.test(l));
1032
+ if (s === -1) return;
1033
+ const buf = [];
1034
+ const head = lines[s].slice('description:'.length).trim();
1035
+ if (head && !/^[>|][-+0-9]*$/.test(head)) buf.push(head);
1036
+ for (let j = s + 1; j < lines.length && !/^[A-Za-z0-9_-]+:/.test(lines[j]); j++) buf.push(lines[j].trim());
1037
+ const desc = buf.join(' ').replace(/\s+/g, ' ').replace(/^["']|["']$/g, '').trim();
1038
+ listingChars += name.length + 4 + Math.min(desc.length, 1536) + 1;
1039
+ entries++;
1040
+ };
1041
+ const skillsDir = join(claudeBase, 'skills');
1042
+ for (const d of existsSync(skillsDir) ? readdirSync(skillsDir) : []) {
1043
+ const f = join(skillsDir, d, 'SKILL.md');
1044
+ if (existsSync(f)) addEntry(d, f);
1045
+ }
1046
+ const cmdDir = join(claudeBase, 'commands');
1047
+ for (const f of existsSync(cmdDir) ? readdirSync(cmdDir) : []) {
1048
+ const p = join(cmdDir, f);
1049
+ if (f.endsWith('.md') && statSync(p).isFile()) addEntry(f.slice(0, -3), p);
1050
+ }
1051
+ console.log(` Claude Code skill listing from ~/.claude (all packs): ${entries} entries, ${listingChars} chars.`);
1052
+ console.log(' The default budget is 1% of the context window (30,000 chars on a 1M Opus 5.5 session), shared with built-in skills;');
1053
+ console.log(' over budget the least-used skills show by name only. To keep every description, set "skillListingBudgetFraction": 0.02 in ~/.claude/settings.json.');
1054
+ }
1055
+
1056
+ results.push({ target: 'Claude Code', base: claudeBase, ...counts, clean, mcpInstalled: false, hookStatus });
1057
+ }
1058
+
1059
+ // ---- Codex CLI (context file + prompt ports) -------------------------------
1060
+ if (wantCodex) {
1061
+ installContextTool({
1062
+ label: 'Codex CLI (OpenAI)',
1063
+ base: codexBase,
1064
+ rulesFile: 'AGENTS.md',
1065
+ rulesLoadPath: '~/.codex/AGENTS.md',
1066
+ commandsDir: 'prompts',
1067
+ commandExt: '.md',
1068
+ render: (cmd) => (cmd.body.endsWith('\n') ? cmd.body : cmd.body + '\n'),
1069
+ });
1070
+ }
1071
+
1072
+ // ---- Gemini CLI (context file + TOML command ports) ------------------------
1073
+ if (wantGemini) {
1074
+ installContextTool({
1075
+ label: 'Gemini CLI',
1076
+ base: geminiBase,
1077
+ rulesFile: 'GEMINI.md',
1078
+ rulesLoadPath: '~/.gemini/GEMINI.md',
1079
+ commandsDir: 'commands',
1080
+ commandExt: '.toml',
1081
+ render: toGeminiToml,
1082
+ });
1083
+ }
1084
+
1085
+ // ---- summary ---------------------------------------------------------------
1086
+ const mode = `${isClean ? 'mirror' : 'merge'}${useLink ? '+link' : ''}${skillName ? `:skill=${skillName}` : ''}`;
1087
+ const verb = useLink ? 'linked' : 'copied';
1088
+
1089
+ if (!isDryRun) failIfUnverified('post-install verify');
1090
+
1091
+ if (isDryRun) {
1092
+ for (const r of results) {
1093
+ console.log(
1094
+ `\n[dry-run] ${r.target} (${mode})` +
1095
+ (r.clean ? ` — would back up + wipe ${r.clean.wiped} managed dir(s)` : '') +
1096
+ `\n[dry-run] Would ${useLink ? 'link' : 'copy'} ${r.copiedDirs} directories and ${r.copiedFiles} files to ${r.base}` +
1097
+ (r.mcpInstalled ? ' (plus mcp.json template)' : '')
1098
+ );
1099
+ }
1100
+ console.log('Run without --dry-run to apply.');
1101
+ } else {
1102
+ for (const r of results) {
1103
+ if (r.clean && r.clean.backupRoot && !noBackup && r.clean.wiped > 0) {
1104
+ console.log(`✓ Backed up previous ${r.base}/{${managedDests.join(',')}} → ${r.clean.backupRoot}`);
1105
+ }
1106
+ console.log(`\n✓ kenji installed for ${r.target} (${mode}) — ${r.copiedDirs} directories and ${r.copiedFiles} files ${verb} to ${r.base}`);
1107
+ if (r.target === 'Cursor' && (!onlyGroups || onlyGroups.has('skills'))) {
1108
+ const agentsCount = existsSync(join(agentsBase, 'skills')) ? readdirSync(join(agentsBase, 'skills')).length : 0;
1109
+ console.log(`✓ Skills synced to ${join(agentsBase, 'skills')} (${agentsCount} skills — Cursor UI path)`);
1110
+ }
1111
+ if (r.mcpInstalled) console.log(`✓ MCP template written to ${join(r.base, 'mcp.json')} — edit it to add your API keys.`);
1112
+ if (r.hookStatus && !r.hookStatus.startsWith('skip')) {
1113
+ const where = r.target === 'Cursor' ? 'hooks.json' : 'settings.json';
1114
+ console.log(`✓ Opt-in completion gate ${r.hookStatus} in ${join(r.base, where)}.`);
1115
+ }
1116
+ }
1117
+ if (linkFallbacks) console.log(` (note: ${linkFallbacks} file(s) copied instead of linked — symlinks need elevated rights on this OS)`);
1118
+ const dual = dualSkillCommandNames();
1119
+ if (dual.length) {
1120
+ console.log(`Note: these names exist as both a skill and a /command (the picker may show two /entries): ${dual.join(', ')}`);
1121
+ }
1122
+ console.log('Re-check anytime with: npx @kensaurus/skills --verify');
1123
+ if (wantCursor) console.log('Restart Cursor to activate skills, commands, and agents.');
1124
+ if (wantClaude) console.log('Restart any active claude sessions — skills appear as /slash-commands.');
1125
+ if (wantCodex) console.log('Codex CLI loads ~/.codex/AGENTS.md automatically; prompts appear via the / picker.');
1126
+ if (wantGemini) console.log('Gemini CLI loads ~/.gemini/GEMINI.md automatically; run /memory refresh in an active session.');
1127
+ }