@haposoft/cafekit 0.16.0-rc.8 → 0.16.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 (297) hide show
  1. package/README.md +116 -27
  2. package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
  3. package/bin/__tests__/codex-hooks.test.js +50 -0
  4. package/bin/__tests__/codex-native.test.js +1882 -70
  5. package/bin/__tests__/develop-contract.test.js +400 -23
  6. package/bin/__tests__/installer-safety.test.js +3 -3
  7. package/bin/__tests__/optional-skill-inventory.test.js +249 -0
  8. package/bin/__tests__/output-styles.test.js +214 -0
  9. package/bin/__tests__/package-inventory.test.js +1735 -98
  10. package/bin/__tests__/runtime-schema.test.js +111 -0
  11. package/bin/__tests__/secret-output-guardrail.test.js +122 -0
  12. package/bin/__tests__/skill-routing-source.test.js +172 -0
  13. package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
  14. package/bin/install.js +10 -0
  15. package/bin/lib/codex-install.js +170 -5
  16. package/bin/lib/context.js +14 -0
  17. package/bin/lib/i18n.js +12 -6
  18. package/bin/lib/skill-deps.js +5 -5
  19. package/bin/phases/claude-runtime.js +23 -0
  20. package/bin/phases/claude-settings.js +8 -0
  21. package/bin/phases/codex-hooks.js +156 -0
  22. package/bin/phases/codex-runtime.js +29 -38
  23. package/bin/phases/copy-payload.js +8 -2
  24. package/bin/phases/select-skill-bundles.js +96 -0
  25. package/bin/phases/skill-inventory.js +71 -0
  26. package/bin/phases/skills-setup.js +9 -2
  27. package/bin/phases/write-metadata.js +8 -5
  28. package/package.json +8 -2
  29. package/src/claude/CLAUDE.md +3 -3
  30. package/src/claude/agents/brainstormer.md +52 -69
  31. package/src/claude/agents/code-auditor.md +32 -14
  32. package/src/claude/agents/debugger.md +35 -20
  33. package/src/claude/agents/deployer.md +12 -5
  34. package/src/claude/agents/docs-keeper.md +11 -1
  35. package/src/claude/agents/implementer.md +25 -13
  36. package/src/claude/agents/inspector.md +6 -3
  37. package/src/claude/agents/project-manager.md +1 -1
  38. package/src/claude/agents/researcher.md +26 -55
  39. package/src/claude/agents/spec-maker.md +12 -7
  40. package/src/claude/agents/strategist.md +52 -0
  41. package/src/claude/agents/test-runner.md +117 -132
  42. package/src/claude/agents/ui-ux-designer.md +1 -1
  43. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
  44. package/src/claude/hooks/__tests__/precompact.test.js +158 -0
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +10 -3
  46. package/src/claude/hooks/__tests__/state.test.js +5 -0
  47. package/src/claude/hooks/__tests__/statusline.test.js +196 -0
  48. package/src/claude/hooks/agent.cjs +1 -1
  49. package/src/claude/hooks/docs-sync.cjs +12 -10
  50. package/src/claude/hooks/inspect-block.cjs +1 -1
  51. package/src/claude/hooks/lib/config.cjs +3 -0
  52. package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
  53. package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
  54. package/src/claude/hooks/precompact.cjs +105 -0
  55. package/src/claude/hooks/rules.cjs +1 -1
  56. package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
  57. package/src/claude/hooks/session.cjs +27 -1
  58. package/src/claude/hooks/spec-gate.cjs +3 -3
  59. package/src/claude/hooks/spec-state.cjs +2 -2
  60. package/src/claude/hooks/state.cjs +1 -1
  61. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  62. package/src/claude/hooks/usage.cjs +1 -1
  63. package/src/claude/migration-manifest.json +29 -15
  64. package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
  65. package/src/claude/output-styles/coding-level-1-junior.md +124 -0
  66. package/src/claude/output-styles/coding-level-2-mid.md +146 -0
  67. package/src/claude/output-styles/coding-level-3-senior.md +148 -0
  68. package/src/claude/output-styles/coding-level-4-lead.md +159 -0
  69. package/src/claude/output-styles/coding-level-5-god.md +91 -0
  70. package/src/claude/references/debugger/core-philosophy.md +5 -3
  71. package/src/claude/references/debugger/frontend-verification.md +2 -2
  72. package/src/claude/references/debugger/log-ci-analysis.md +13 -1
  73. package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
  74. package/src/claude/references/debugger/performance-diagnostics.md +2 -2
  75. package/src/claude/references/debugger/root-cause-tracing.md +37 -6
  76. package/src/claude/references/debugger/side-effect-gate.md +3 -1
  77. package/src/claude/references/debugger/verification-protocol.md +13 -12
  78. package/src/claude/rules/orchestrator.md +7 -1
  79. package/src/claude/rules/process-management.md +39 -0
  80. package/src/claude/rules/review-audit-self-decision.md +48 -0
  81. package/src/claude/rules/skill-domain-routing.md +26 -24
  82. package/src/claude/rules/skill-workflow-routing.md +22 -14
  83. package/src/claude/rules/workflow.md +3 -3
  84. package/src/claude/runtime.json +1 -0
  85. package/src/claude/runtime.schema.json +205 -0
  86. package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
  87. package/src/claude/scripts/provenance.cjs +18 -5
  88. package/src/claude/scripts/workflow-policy.cjs +1 -1
  89. package/src/claude/settings/settings.json +16 -0
  90. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  91. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  92. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  93. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  94. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  95. package/src/claude/skills/code-review/SKILL.md +43 -25
  96. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  97. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  98. package/src/claude/skills/debug/SKILL.md +48 -19
  99. package/src/claude/skills/delegate/SKILL.md +1 -1
  100. package/src/claude/skills/develop/SKILL.md +92 -65
  101. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  102. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  103. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  104. package/src/claude/skills/docs/SKILL.md +42 -23
  105. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  106. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  107. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  108. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  109. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  110. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  111. package/src/claude/skills/docx/SKILL.md +1 -1
  112. package/src/claude/skills/git/SKILL.md +1 -1
  113. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  114. package/src/claude/skills/git/references/finish-branch.md +1 -1
  115. package/src/claude/skills/hotfix/SKILL.md +159 -170
  116. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  117. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  118. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  119. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  120. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  121. package/src/claude/skills/inspect/SKILL.md +55 -46
  122. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  123. package/src/claude/skills/loop/SKILL.md +114 -0
  124. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  125. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  126. package/src/claude/skills/pdf/SKILL.md +1 -1
  127. package/src/claude/skills/pptx/SKILL.md +1 -1
  128. package/src/claude/skills/question/SKILL.md +16 -13
  129. package/src/claude/skills/research/SKILL.md +70 -43
  130. package/src/claude/skills/route/SKILL.md +94 -0
  131. package/src/claude/skills/route/references/agent-timing.md +46 -0
  132. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  133. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  134. package/src/claude/skills/specs/SKILL.md +41 -60
  135. package/src/claude/skills/specs/references/review.md +11 -8
  136. package/src/claude/skills/specs/references/templates.md +34 -18
  137. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  138. package/src/claude/skills/specs/templates/research.md +2 -2
  139. package/src/claude/skills/sync/SKILL.md +6 -6
  140. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  141. package/src/claude/skills/test/SKILL.md +120 -120
  142. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  143. package/src/claude/skills/test/references/failure-triage.md +55 -121
  144. package/src/claude/skills/test/references/test-memory.md +20 -34
  145. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  146. package/src/claude/skills/web-testing/SKILL.md +1 -1
  147. package/src/claude/skills/xlsx/SKILL.md +1 -1
  148. package/src/claude/status.cjs +135 -33
  149. package/src/codex/AGENTS.md +14 -4
  150. package/src/codex/hooks/docs-sync.cjs +8 -3
  151. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  152. package/src/codex/hooks/rules.cjs +29 -1
  153. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  154. package/src/codex/hooks/spec-gate.cjs +3 -8
  155. package/src/codex/hooks/spec-state.cjs +2 -1
  156. package/src/codex/hooks.json +5 -0
  157. package/src/codex/runtime.json +2 -0
  158. package/src/common/.env.example +1 -1
  159. package/src/claude/skills/backend-development/SKILL.md +0 -101
  160. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  161. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  162. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  163. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  164. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  165. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  166. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  167. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  168. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  169. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  170. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  171. package/src/claude/skills/devops/.env.example +0 -76
  172. package/src/claude/skills/devops/SKILL.md +0 -102
  173. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  174. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  175. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  176. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  177. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  178. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  179. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  180. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  181. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  182. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  183. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  184. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  185. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  186. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  187. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  188. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  189. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  190. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  191. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  192. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  193. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  194. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  195. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  196. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  197. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  198. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  199. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  200. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  201. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  202. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  203. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  204. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  205. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  206. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  207. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  208. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  209. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  210. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  211. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  212. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  213. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  214. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  215. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  216. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  217. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  218. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  219. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  220. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  221. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  222. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  223. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  224. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  225. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  226. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  227. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  228. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  229. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  230. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  231. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  232. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  233. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  234. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  235. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  236. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  237. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  238. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  239. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  240. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  241. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  242. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  243. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  244. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  245. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  246. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  247. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  248. package/src/claude/skills/react-best-practices/README.md +0 -123
  249. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  250. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  251. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  252. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  253. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  254. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  255. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  256. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  257. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  258. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  259. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  260. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  261. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  262. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  263. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  264. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  265. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  266. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  267. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  268. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  269. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  270. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  271. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  272. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  273. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  274. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  275. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  276. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  277. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  278. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  279. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  280. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  281. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  282. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  283. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  284. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  285. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  286. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  287. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  288. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  289. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  290. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  291. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  292. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  293. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  294. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  295. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  296. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  297. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ // runtime.json is the only configuration surface a CafeKit user edits by hand, so the
4
+ // schema beside it has to stay true: every shipped key described, every documented key
5
+ // still real, and no key silently drifting out of sync with what the runtime honours.
6
+
7
+ const assert = require('node:assert/strict');
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const { test } = require('node:test');
11
+
12
+ const PACKAGE_ROOT = path.join(__dirname, '../..');
13
+ const SCHEMA_PATH = path.join(PACKAGE_ROOT, 'src/claude/runtime.schema.json');
14
+ const SCHEMA = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
15
+ const RUNTIMES = ['src/claude/runtime.json', 'src/codex/runtime.json'];
16
+ const MANIFEST = require(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'));
17
+
18
+ function readRuntime(relative) {
19
+ return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, relative), 'utf8'));
20
+ }
21
+
22
+ /** Walk a config object against a schema node, collecting undescribed key paths. */
23
+ function undescribedKeys(value, schemaNode, trail = []) {
24
+ if (!schemaNode || typeof value !== 'object' || value === null || Array.isArray(value)) return [];
25
+ const properties = schemaNode.properties || {};
26
+ const found = [];
27
+ for (const [key, child] of Object.entries(value)) {
28
+ const childSchema = properties[key];
29
+ if (!childSchema) {
30
+ if (schemaNode.additionalProperties === true || typeof schemaNode.additionalProperties === 'object') continue;
31
+ found.push([...trail, key].join('.'));
32
+ continue;
33
+ }
34
+ found.push(...undescribedKeys(child, childSchema, [...trail, key]));
35
+ }
36
+ return found;
37
+ }
38
+
39
+ /** Every property node in the schema, keyed by dotted path. */
40
+ function schemaProperties(node, trail = [], out = new Map()) {
41
+ for (const [key, child] of Object.entries(node.properties || {})) {
42
+ const dotted = [...trail, key].join('.');
43
+ out.set(dotted, child);
44
+ schemaProperties(child, [...trail, key], out);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ test('the schema is a valid, self-describing JSON Schema document', () => {
50
+ assert.equal(SCHEMA.$schema, 'https://json-schema.org/draft/2020-12/schema');
51
+ assert.equal(SCHEMA.type, 'object');
52
+ assert.ok(SCHEMA.title, 'the schema needs a title');
53
+ assert.ok(SCHEMA.description, 'the schema needs a description');
54
+ assert.equal(SCHEMA.additionalProperties, false, 'an unknown top-level key must be reported, not accepted silently');
55
+ });
56
+
57
+ test('every key in both shipped runtime.json files is described by the schema', () => {
58
+ for (const relative of RUNTIMES) {
59
+ const missing = undescribedKeys(readRuntime(relative), SCHEMA);
60
+ assert.deepEqual(missing, [], `${relative} ships keys the schema does not describe: ${missing.join(', ')}`);
61
+ }
62
+ });
63
+
64
+ test('both runtimes point at the schema that installs beside them', () => {
65
+ for (const relative of RUNTIMES) {
66
+ assert.equal(readRuntime(relative).$schema, './runtime.schema.json', `${relative} must reference the schema`);
67
+ }
68
+ assert.ok(
69
+ MANIFEST.runtime.files.includes('runtime.schema.json'),
70
+ 'the schema must be installed for Claude, or its $schema reference dangles'
71
+ );
72
+ const codexPhase = fs.readFileSync(path.join(PACKAGE_ROOT, 'bin/phases/codex-runtime.js'), 'utf8');
73
+ assert.match(codexPhase, /runtime\.schema\.json/, 'the schema must also be installed for Codex');
74
+ });
75
+
76
+ test('every documented property carries a description a reader can act on', () => {
77
+ for (const [dotted, node] of schemaProperties(SCHEMA)) {
78
+ if (dotted === '$schema') continue;
79
+ assert.ok(node.description, `${dotted} has no description`);
80
+ assert.ok(node.description.length > 20, `${dotted} has a description too short to help: ${node.description}`);
81
+ }
82
+ });
83
+
84
+ test('keys the runtime does not honour are marked deprecated, not documented as working', () => {
85
+ // develop.parallel and the hooks toggle map are still shipped for compatibility, but
86
+ // nothing reads them. The schema must say so rather than imply they work.
87
+ for (const dead of ['hooks', 'develop']) {
88
+ const node = SCHEMA.properties[dead];
89
+ assert.ok(node, `${dead} is shipped in runtime.json and must appear in the schema`);
90
+ assert.equal(node.deprecated, true, `${dead} is not honoured and must be marked deprecated`);
91
+ assert.match(node.description, /[Nn]ot honoured/, `${dead} must say plainly that it does nothing`);
92
+ }
93
+ });
94
+
95
+ test('the completion gate flag is documented as a block, never as a switch', () => {
96
+ const node = SCHEMA.properties.spec.properties.completion_gate;
97
+ assert.match(node.description, /[Nn]ot a switch/);
98
+ assert.match(node.description, /blocks/, 'the description must say the gate blocks while the flag is present');
99
+ });
100
+
101
+ test('statusline values in the schema match the modes the renderer implements', () => {
102
+ const status = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/status.cjs'), 'utf8');
103
+ const implemented = new Set((status.match(/case '([a-z]+)':/g) || []).map((line) => line.slice(6, -2)));
104
+ for (const mode of SCHEMA.properties.statusline.enum) {
105
+ assert.ok(implemented.has(mode), `schema offers statusline "${mode}" but the renderer has no branch for it`);
106
+ }
107
+ const sections = SCHEMA.properties.statuslineLayout.properties.lines.items.items.enum;
108
+ for (const section of sections) {
109
+ assert.ok(status.includes(`'${section}'`), `schema offers layout section "${section}" but status.cjs never names it`);
110
+ }
111
+ });
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ // The privacy hook gates whether a sensitive file may be READ. Nothing gated what
4
+ // happens after that approval, so an approved read could still put a raw key into the
5
+ // transcript — which is written to disk, survives compaction, and is replayed to the
6
+ // model every later turn. This hook closes that gap with a reminder, on both runtimes.
7
+
8
+ const assert = require('node:assert/strict');
9
+ const fs = require('node:fs');
10
+ const os = require('node:os');
11
+ const path = require('node:path');
12
+ const { spawnSync } = require('node:child_process');
13
+ const { test } = require('node:test');
14
+
15
+ const PACKAGE_ROOT = path.join(__dirname, '../..');
16
+ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
17
+ const KEYWORDS = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'));
18
+
19
+ function inTempProject(run) {
20
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-secret-guardrail-'));
21
+ try {
22
+ spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
23
+ return run(root);
24
+ } finally {
25
+ fs.rmSync(root, { recursive: true, force: true });
26
+ }
27
+ }
28
+
29
+ function install(root, platform) {
30
+ const result = spawnSync(process.execPath, [INSTALLER, '--platform', platform, '--yes'],
31
+ { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
32
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
33
+ return path.join(root, `.${platform}`, 'hooks', 'secret-output-guardrail.cjs');
34
+ }
35
+
36
+ function run(hookPath, root, prompt) {
37
+ const result = spawnSync(process.execPath, [hookPath], {
38
+ cwd: root,
39
+ input: JSON.stringify({ session_id: 'guardrail-session', prompt, cwd: root }),
40
+ encoding: 'utf8',
41
+ });
42
+ assert.equal(result.status, 0, `hook must fail open: ${result.stderr}`);
43
+ return result.stdout;
44
+ }
45
+
46
+ test('the matcher fires on credential topics and stays quiet otherwise', () => {
47
+ for (const prompt of [
48
+ 'read the .env file', 'rotate our credentials', 'where is the secret stored',
49
+ 'print the API key', 'the access key expired', 'this token is stale',
50
+ 'send a bearer header', 'decode the JWT', 'load the private key',
51
+ 'the oauth client secret', 'client secret rotation', 'our cloud keys', 'check the env file',
52
+ ]) {
53
+ assert.equal(KEYWORDS.containsSecretKeyword(prompt), true, `should match: ${prompt}`);
54
+ }
55
+ for (const prompt of [
56
+ 'add two numbers', 'refactor the tokenizer', 'update the environment setup docs',
57
+ 'fix the keyboard shortcut', '', ' ',
58
+ ]) {
59
+ assert.equal(KEYWORDS.containsSecretKeyword(prompt), false, `should not match: ${prompt}`);
60
+ }
61
+ });
62
+
63
+ test('the reminder separates permission to read from permission to print', () => {
64
+ assert.match(KEYWORDS.REMINDER, /does not grant permission to print raw values/);
65
+ assert.match(KEYWORDS.REMINDER, /\[redacted\]/);
66
+ assert.match(KEYWORDS.REMINDER, /report only success or failure/);
67
+ });
68
+
69
+ for (const platform of ['claude', 'codex']) {
70
+ test(`${platform}: a credential prompt gets the reminder, an ordinary one gets nothing`, () => {
71
+ inTempProject((root) => {
72
+ const hook = install(root, platform);
73
+ assert.equal(fs.existsSync(path.join(root, `.${platform}`, 'hooks', 'lib', 'secret-keywords.cjs')), true,
74
+ 'the shared matcher must install beside the hook');
75
+
76
+ const flagged = run(hook, root, 'please read .env and tell me what is set');
77
+ assert.match(flagged, /## Secret handling/);
78
+ assert.match(flagged, /does not grant permission to print raw values/);
79
+
80
+ assert.equal(run(hook, root, 'rename this variable'), '', 'an unrelated prompt must inject nothing');
81
+ });
82
+ });
83
+
84
+ test(`${platform}: the guardrail never echoes the prompt or a matched value`, () => {
85
+ inTempProject((root) => {
86
+ const hook = install(root, platform);
87
+ // A realistic-looking secret in the prompt must not come back in the output —
88
+ // a guardrail that quotes its input would itself write the value to the transcript.
89
+ const out = run(hook, root, 'my api key is sk-live-abc123SECRETVALUE, is it valid?');
90
+ assert.match(out, /## Secret handling/);
91
+ assert.doesNotMatch(out, /sk-live-abc123SECRETVALUE/, 'the hook echoed the secret it was warning about');
92
+ assert.doesNotMatch(out, /is it valid/, 'the hook echoed the prompt');
93
+ });
94
+ });
95
+
96
+ test(`${platform}: the hook fails open on malformed input`, () => {
97
+ inTempProject((root) => {
98
+ const hook = install(root, platform);
99
+ for (const input of ['not json', '', '{"prompt":null}']) {
100
+ const result = spawnSync(process.execPath, [hook], { cwd: root, input, encoding: 'utf8' });
101
+ assert.equal(result.status, 0, `malformed input must not fail the turn: ${JSON.stringify(input)}`);
102
+ }
103
+ });
104
+ });
105
+
106
+ test(`${platform}: the hook is registered for UserPromptSubmit`, () => {
107
+ inTempProject((root) => {
108
+ install(root, platform);
109
+ const configPath = platform === 'claude'
110
+ ? path.join(root, '.claude', 'settings.json')
111
+ : path.join(root, '.codex', 'hooks.json');
112
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
113
+ const commands = (config.hooks.UserPromptSubmit || [])
114
+ .flatMap((group) => group.hooks || [])
115
+ .map((handler) => handler.command || '');
116
+ assert.ok(
117
+ commands.some((command) => command.includes('secret-output-guardrail.cjs')),
118
+ `${platform} never runs the guardrail`
119
+ );
120
+ });
121
+ });
122
+ }
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+ const { test } = require('node:test');
9
+ const { normalizeCodexBody } = require('../lib/codex-install');
10
+
11
+ const PACKAGE_ROOT = path.resolve(__dirname, '../..');
12
+ const SCANNER = path.join(PACKAGE_ROOT, 'src/claude/scripts/generate-skill-catalog.cjs');
13
+ const MANIFEST = JSON.parse(fs.readFileSync(
14
+ path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8'
15
+ ));
16
+
17
+ function withTempRoot(run) {
18
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-routing-'));
19
+ try {
20
+ return run(root);
21
+ } finally {
22
+ fs.rmSync(root, { recursive: true, force: true });
23
+ }
24
+ }
25
+
26
+ function writeSkill(root, directory, fields = {}) {
27
+ const target = path.join(root, directory);
28
+ fs.mkdirSync(target, { recursive: true });
29
+ if (fields.raw) {
30
+ fs.writeFileSync(path.join(target, 'SKILL.md'), fields.raw);
31
+ return;
32
+ }
33
+ const lines = ['---'];
34
+ if (fields.name !== undefined) lines.push(`name: ${fields.name}`);
35
+ if (fields.description !== undefined) lines.push(`description: "${fields.description}"`);
36
+ if (fields.when !== undefined) lines.push(`when_to_use: "${fields.when}"`);
37
+ if (fields.category !== undefined) lines.push(`category: ${fields.category}`);
38
+ if (fields.keywords !== undefined) lines.push(`keywords: [${fields.keywords.join(', ')}]`);
39
+ lines.push('user-invocable: true', '---', `# ${directory}`, '');
40
+ fs.writeFileSync(path.join(target, 'SKILL.md'), `${lines.join('\n')}\n`);
41
+ }
42
+
43
+ function runCatalog(script, cwd, args = ['--json']) {
44
+ const result = spawnSync(process.execPath, [script, ...args], { cwd, encoding: 'utf8' });
45
+ assert.equal(result.status, 0, result.stderr);
46
+ return JSON.parse(result.stdout);
47
+ }
48
+
49
+ function installScanner(source, destination, transform = (content) => content) {
50
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
51
+ fs.writeFileSync(destination, transform(fs.readFileSync(source, 'utf8')));
52
+ }
53
+
54
+ test('skill routing consumes live catalog without fixed optional commands', () => {
55
+ withTempRoot((root) => {
56
+ const claudeScript = path.join(root, '.claude/scripts/generate-skill-catalog.cjs');
57
+ const codexScript = path.join(root, '.codex/scripts/generate-skill-catalog.cjs');
58
+ installScanner(SCANNER, claudeScript);
59
+ installScanner(SCANNER, codexScript, (content) => normalizeCodexBody(content, SCANNER));
60
+
61
+ writeSkill(path.join(root, '.claude/skills'), 'question', {
62
+ name: 'cf:ask', description: 'Answer with evidence.', when: 'Use for factual questions.',
63
+ category: 'utilities', keywords: ['answer', 'evidence'],
64
+ });
65
+ writeSkill(path.join(root, '.claude/skills'), 'docs', {
66
+ name: 'cf:docs', description: 'Work with docs.', when: 'Use for documentation.',
67
+ category: 'documents', keywords: ['docs'],
68
+ });
69
+ writeSkill(path.join(root, '.agents/skills'), 'question', {
70
+ name: 'cf-ask', description: 'Answer with evidence.', when: 'Use for factual questions.',
71
+ category: 'utilities', keywords: ['answer', 'evidence'],
72
+ });
73
+
74
+ const claude = runCatalog(claudeScript, root);
75
+ const codex = runCatalog(codexScript, root);
76
+ assert.equal(claude.root, fs.realpathSync(path.join(root, '.claude/skills')));
77
+ assert.equal(codex.root, fs.realpathSync(path.join(root, '.agents/skills')));
78
+ assert.deepEqual(claude.skills.map((skill) => skill.public_id), ['cf:ask', 'cf:docs']);
79
+ assert.deepEqual(codex.skills.map((skill) => skill.public_id), ['cf:ask']);
80
+ assert.equal(codex.skills.some((skill) => skill.public_id === 'cf:docs'), false);
81
+ });
82
+
83
+ const workflow = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/skill-workflow-routing.md'), 'utf8');
84
+ const domain = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/skill-domain-routing.md'), 'utf8');
85
+ const codexEntry = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/codex/AGENTS.md'), 'utf8');
86
+ assert.match(workflow, /user names a valid installed skill[\s\S]*one obvious low-risk[\s\S]*direct factual conversation/);
87
+ assert.match(workflow, /do not invoke Route or agents for ceremony/);
88
+ assert.match(workflow.replace(/\s+/g, ' '), /numeric optimization capability remains explicit-only/);
89
+ assert.match(domain, /examples below are intent hints, not a copied installed inventory/);
90
+ assert.match(domain, /document\/artifact work \| use only a matching installed optional capability/);
91
+ assert.doesNotMatch(domain, /\/cf:(?:docs|docx|pdf|pptx|xlsx|ai-multimodal)/);
92
+ assert.match(codexEntry, /node \.codex\/scripts\/generate-skill-catalog\.cjs --skills/);
93
+ assert.match(codexEntry, /Codex-bound[\s\S]*\.agents\/skills/);
94
+ });
95
+
96
+ test('skill catalog exposes discriminating routing metadata', () => {
97
+ withTempRoot((root) => {
98
+ const skills = path.join(root, 'skills');
99
+ writeSkill(skills, 'route', {
100
+ name: 'cf:route', description: 'Choose a bounded chain.',
101
+ when: 'Use for ambiguous multi-step work.', category: 'utilities',
102
+ keywords: ['routing', 'risk'],
103
+ });
104
+ writeSkill(skills, 'question', {
105
+ name: 'cf:ask', description: 'First duplicate.', when: 'Use for questions.',
106
+ category: 'utilities', keywords: ['answer'],
107
+ });
108
+ writeSkill(skills, 'ask', {
109
+ name: 'cf:ask', description: 'Second duplicate.', when: 'Use for questions.',
110
+ category: 'utilities', keywords: ['answer'],
111
+ });
112
+ writeSkill(skills, 'missing-description', { name: 'cf:missing-description' });
113
+ writeSkill(skills, 'mismatch', { name: 'cf:wrong', description: 'Wrong folder.' });
114
+ writeSkill(skills, 'malformed', { raw: '# no frontmatter\n' });
115
+ writeSkill(skills, 'bad-line', {
116
+ raw: '---\nname: cf:bad-line\ndescription: Valid-looking description.\nthis is not yaml\n---\n',
117
+ });
118
+ writeSkill(skills, 'unterminated', {
119
+ raw: '---\nname: cf:unterminated\ndescription: "unterminated\n---\n',
120
+ });
121
+ writeSkill(skills, 'blank-description', {
122
+ raw: '---\nname: cf:blank-description\ndescription: " "\n---\n',
123
+ });
124
+ writeSkill(skills, 'invalid-flow', {
125
+ raw: '---\nname: cf:invalid-flow\ndescription: [unterminated\n---\n',
126
+ });
127
+ writeSkill(skills, 'duplicate-key', {
128
+ raw: '---\nname: cf:wrong\nname: cf:duplicate-key\ndescription: Duplicate name.\n---\n',
129
+ });
130
+ writeSkill(skills, 'backend-development', {
131
+ name: 'cf:backend-development', description: 'User-modified retired skill.',
132
+ });
133
+
134
+ const catalog = runCatalog(SCANNER, root, ['--json', '--root', skills]);
135
+ assert.deepEqual(catalog.skills.map((skill) => skill.public_id), ['cf:route']);
136
+ assert.deepEqual(catalog.skills[0], {
137
+ name: 'cf:route', public_id: 'cf:route', directory: 'route',
138
+ description: 'Choose a bounded chain.', when_to_use: 'Use for ambiguous multi-step work.',
139
+ category: 'utilities', keywords: ['routing', 'risk'], user_invocable: true,
140
+ has_references: false, has_scripts: false,
141
+ });
142
+ const codes = catalog.diagnostics.map((item) => item.code);
143
+ assert.equal(codes.filter((code) => code === 'duplicate_public_name').length, 2);
144
+ for (const required of [
145
+ 'folder_name_mismatch', 'malformed_frontmatter', 'missing_routing_metadata', 'retired_skill',
146
+ ]) assert.ok(codes.includes(required), required);
147
+ for (const directory of ['bad-line', 'unterminated', 'duplicate-key', 'invalid-flow']) {
148
+ assert.ok(catalog.diagnostics.some((item) =>
149
+ item.directory === directory && item.code === 'malformed_frontmatter'), directory);
150
+ }
151
+ assert.ok(catalog.diagnostics.some((item) =>
152
+ item.directory === 'blank-description' && item.code === 'missing_routing_metadata'));
153
+ assert.ok(catalog.diagnostics.every((item) => item.code !== 'duplicate_public_name'
154
+ || item.message.includes('explicit user disambiguation')));
155
+ });
156
+
157
+ const sourceCatalog = runCatalog(SCANNER, PACKAGE_ROOT);
158
+ const retiredDiagnostics = sourceCatalog.diagnostics
159
+ .filter((item) => item.code === 'retired_skill').map((item) => item.directory).sort();
160
+ assert.deepEqual(retiredDiagnostics, [...MANIFEST.obsolete.skills].sort());
161
+ assert.equal(sourceCatalog.skills.some((skill) => MANIFEST.obsolete.skills.includes(skill.directory)), false);
162
+ const route = sourceCatalog.skills.find((skill) => skill.public_id === 'cf:route');
163
+ assert.ok(route?.description && route.when_to_use && route.category && route.keywords.length > 0);
164
+ });
165
+
166
+ test('skill catalog rejects a missing root value before path resolution', () => {
167
+ const result = spawnSync(process.execPath, [SCANNER, '--root', '--json'], {
168
+ cwd: PACKAGE_ROOT, encoding: 'utf8',
169
+ });
170
+ assert.equal(result.status, 1);
171
+ assert.match(result.stderr, /--root requires a skills directory/);
172
+ });
@@ -263,7 +263,7 @@ function installGate(root, kind) {
263
263
  'scripts/workflow-policy.cjs', 'scripts/provenance.cjs', 'scripts/spec-resolver.cjs',
264
264
  'scripts/spec-receipt.cjs', 'scripts/validate-spec-output.cjs', 'scripts/spec-ground.cjs',
265
265
  'scripts/spec-semantic-model.cjs', 'scripts/spec-final-state.cjs',
266
- 'hooks/lib/runtime-path-safety.cjs',
266
+ 'hooks/lib/runtime-path-safety.cjs', 'hooks/lib/hook-state-dir.cjs',
267
267
  ]);
268
268
  if (kind === 'claude') {
269
269
  for (const name of [
package/bin/install.js CHANGED
@@ -22,6 +22,7 @@ const backup = require('./lib/backup');
22
22
  const manifestLib = require('./lib/manifest');
23
23
 
24
24
  const { selectLanguage, resolvePlatforms } = require('./phases/select-platform');
25
+ const { selectDocumentSkills } = require('./phases/select-skill-bundles');
25
26
  const { copyPlatformFiles } = require('./phases/copy-payload');
26
27
  const {
27
28
  copyRoutingFile,
@@ -30,8 +31,10 @@ const {
30
31
  copyClaudeMdFile,
31
32
  ensureSharedAgentsMdCore,
32
33
  copyRulesDirectory,
34
+ copyOutputStylesDirectory,
33
35
  removeObsoleteAgents
34
36
  } = require('./phases/claude-runtime');
37
+ const { reconcileSkillInventory } = require('./phases/skill-inventory');
35
38
  const { mergeClaudeSettings } = require('./phases/claude-settings');
36
39
  const { installCodexRuntime } = require('./phases/codex-runtime');
37
40
  const { writePlatformVersionMetadata } = require('./phases/write-metadata');
@@ -58,6 +61,7 @@ function installPlatform(ctx, platformKey) {
58
61
  ctx.ui.startSpinner(ctx.t('installingPlatform', { name: platform.name }));
59
62
 
60
63
  copyPlatformFiles(ctx, platformKey);
64
+ reconcileSkillInventory(ctx, platformKey);
61
65
  removeObsoleteAgents(ctx, platformKey);
62
66
 
63
67
  if (platformKey === 'claude') {
@@ -67,6 +71,7 @@ function installPlatform(ctx, platformKey) {
67
71
  mergeClaudeSettings(ctx, platformKey);
68
72
  copyClaudeMdFile(ctx, platformKey);
69
73
  copyRulesDirectory(ctx, platformKey);
74
+ copyOutputStylesDirectory(ctx, platformKey);
70
75
  }
71
76
 
72
77
  if (platformKey === 'codex') {
@@ -102,6 +107,8 @@ Options:
102
107
  Chromium/Playwright). Otherwise prompted interactively.
103
108
  --with-rtk Install the rtk token-saver (binary + Claude Code hook).
104
109
  Otherwise prompted interactively.
110
+ --with-document-skills Install docs/DOCX/PDF/PPTX/XLSX/multimodal skills
111
+ --without-document-skills Skip or remove CafeKit-owned document skills
105
112
  -y, --yes Non-interactive: skip prompts, use defaults (CI)
106
113
  -h, --help Show this help
107
114
  -v, --version Print version
@@ -162,6 +169,9 @@ async function main() {
162
169
  process.exit(result.status ?? 1);
163
170
  }
164
171
 
172
+ await selectDocumentSkills(ctx);
173
+ if (ctx.cancelled) { lock.release(); process.exit(0); }
174
+
165
175
  // ── Pre-run snapshot for rollback ─────────────────────
166
176
  // Capture platform folders AND the root files the pipeline mutates
167
177
  // (CLAUDE.md, .gitignore) so a mid-run failure rolls back cleanly.
@@ -25,7 +25,6 @@ const AGENT_NAMES = [
25
25
  'ui-ux-designer'
26
26
  ];
27
27
  const INSTRUCTION_REPLACEMENTS = [
28
- [/`?\bAskUserQuestion\b`?/g, 'a structured user-input request'],
29
28
  [/`?\bTodoWrite\b`?/g, 'task-state tracking'],
30
29
  [/`?\bTaskCreate\b`?/g, 'task creation'],
31
30
  [/`?\bTaskUpdate\b`?/g, 'task-state updates'],
@@ -64,6 +63,172 @@ function applyReplacements(content, replacements) {
64
63
  return next;
65
64
  }
66
65
 
66
+ function isMarkdownLinkDestination(content, offset) {
67
+ const opener = content.lastIndexOf('](', offset);
68
+ if (opener < 0 || content.lastIndexOf('[', opener) < 0) return false;
69
+
70
+ let depth = 1;
71
+ for (let index = opener + 2; index < offset; index += 1) {
72
+ if (content[index] === '\\') {
73
+ index += 1;
74
+ } else if (content[index] === '(') {
75
+ depth += 1;
76
+ } else if (content[index] === ')') {
77
+ depth -= 1;
78
+ if (depth === 0) return false;
79
+ }
80
+ }
81
+ return depth > 0;
82
+ }
83
+
84
+ function isUrlLikeToken(content, offset) {
85
+ const prefix = content.slice(0, offset).split(/[\s]/).at(-1) || '';
86
+ return /^<?(?:[a-z][a-z0-9+.-]*:\/\/|mailto:|www\.)\S*$/i.test(prefix);
87
+ }
88
+
89
+ function maskMarkdownCodeSpans(content) {
90
+ const spans = [];
91
+ let markerNonce = 0;
92
+ while (content.includes(`\uE000CAFEKIT_${markerNonce}_`)) markerNonce += 1;
93
+ const markerPrefix = `\uE000CAFEKIT_${markerNonce}_`;
94
+ let masked = '';
95
+ let index = 0;
96
+
97
+ const addMarker = (kind, original) => {
98
+ const marker = `${markerPrefix}${kind}_${spans.length}\uE001`;
99
+ spans.push({ marker, original });
100
+ return marker;
101
+ };
102
+
103
+ const fencedBlockEnd = (openerLineEnd, delimiter, delimiterLength) => {
104
+ let lineStart = openerLineEnd < 0 ? content.length : openerLineEnd + 1;
105
+ while (lineStart < content.length) {
106
+ const newline = content.indexOf('\n', lineStart);
107
+ const lineEnd = newline < 0 ? content.length : newline;
108
+ const line = content.slice(lineStart, lineEnd);
109
+ const match = line.match(/^[ \t]{0,3}([`~]+)[ \t]*\r?$/);
110
+ if (match
111
+ && [...match[1]].every((character) => character === delimiter)
112
+ && match[1].length >= delimiterLength) {
113
+ return lineEnd;
114
+ }
115
+ lineStart = newline < 0 ? content.length : newline + 1;
116
+ }
117
+ return content.length;
118
+ };
119
+
120
+ while (index < content.length) {
121
+ if (content[index] !== '`' && content[index] !== '~') {
122
+ masked += content[index];
123
+ index += 1;
124
+ continue;
125
+ }
126
+
127
+ const opener = index;
128
+ const delimiter = content[index];
129
+ while (index < content.length && content[index] === delimiter) index += 1;
130
+ const delimiterLength = index - opener;
131
+ const lineStart = content.lastIndexOf('\n', opener - 1) + 1;
132
+ const linePrefix = content.slice(lineStart, opener);
133
+ const openerLineEnd = content.indexOf('\n', index);
134
+ const infoEnd = openerLineEnd < 0 ? content.length : openerLineEnd;
135
+ const info = content.slice(index, infoEnd);
136
+ const isFence = delimiterLength >= 3
137
+ && /^[ \t]{0,3}$/.test(linePrefix)
138
+ && (delimiter === '~' || !info.includes('`'));
139
+
140
+ if (isFence) {
141
+ const end = fencedBlockEnd(openerLineEnd, delimiter, delimiterLength);
142
+ masked += addMarker('CODE', content.slice(opener, end));
143
+ index = end;
144
+ continue;
145
+ }
146
+
147
+ if (delimiter !== '`') {
148
+ masked += content.slice(opener, index);
149
+ continue;
150
+ }
151
+
152
+ let cursor = index;
153
+ let closer = -1;
154
+ while (cursor < content.length) {
155
+ const runStart = content.indexOf('`', cursor);
156
+ if (runStart < 0) break;
157
+ let runEnd = runStart;
158
+ while (runEnd < content.length && content[runEnd] === '`') runEnd += 1;
159
+ if (runEnd - runStart === delimiterLength) {
160
+ closer = runStart;
161
+ break;
162
+ }
163
+ cursor = runEnd;
164
+ }
165
+
166
+ if (closer < 0) {
167
+ const newline = content.indexOf('\n', opener);
168
+ const end = newline < 0 ? content.length : newline;
169
+ masked += addMarker('CODE', content.slice(opener, end));
170
+ index = end;
171
+ continue;
172
+ }
173
+
174
+ const end = closer + delimiterLength;
175
+ const original = content.slice(opener, end);
176
+ const inner = content.slice(opener + delimiterLength, closer);
177
+ const kind = inner.trim() === 'AskUserQuestion' ? 'ASK' : 'CODE';
178
+ masked += addMarker(kind, original);
179
+ index = end;
180
+ }
181
+
182
+ return {
183
+ content: masked,
184
+ askMarkerPattern: `${markerPrefix}ASK_[0-9]+\uE001`,
185
+ askMarkerPrefix: `${markerPrefix}ASK_`,
186
+ restore(value) {
187
+ return spans.reduce(
188
+ (next, { marker, original }) => next.split(marker).join(original),
189
+ value
190
+ );
191
+ }
192
+ };
193
+ }
194
+
195
+ function normalizeAskUserQuestion(content) {
196
+ const masked = maskMarkdownCodeSpans(content);
197
+ const determinerPattern = [
198
+ 'one', 'each', 'every', 'another', 'this', 'that', 'a', 'an', 'the'
199
+ ].map((word) => [...word].map((letter) => `[${letter}${letter.toUpperCase()}]`).join('')).join('|');
200
+ const oneSeparator = '(?:[^\\S\\r\\n]+|[^\\S\\r\\n]*\\r?\\n[^\\S\\r\\n]*)';
201
+ const pattern = new RegExp(
202
+ `(?:(\\b(?:${determinerPattern}|[0-9]+)\\b)(${oneSeparator}))?`
203
+ + `(\\bAskUserQuestion\\b|${masked.askMarkerPattern})`,
204
+ 'g'
205
+ );
206
+ const normalized = masked.content.replace(pattern, (match, determiner, spacing, _token, offset, source) => {
207
+ const literalIndex = match.indexOf('AskUserQuestion');
208
+ const tokenOffset = offset + (literalIndex >= 0
209
+ ? literalIndex
210
+ : match.indexOf(masked.askMarkerPrefix));
211
+ const previous = source[tokenOffset - 1] || '';
212
+ if (isMarkdownLinkDestination(source, tokenOffset)
213
+ || isUrlLikeToken(source, tokenOffset)
214
+ || /[/=?#&%@]/.test(previous)) {
215
+ return match;
216
+ }
217
+
218
+ if (determiner) {
219
+ const normalizedDeterminer = /^an$/i.test(determiner)
220
+ ? (determiner[0] === 'A' ? 'A' : 'a')
221
+ : determiner;
222
+ return `${normalizedDeterminer}${spacing}structured user-input request`;
223
+ }
224
+
225
+ const trailing = source.slice(offset + match.length);
226
+ const pluralContext = new RegExp(`^${oneSeparator}(?:calls|batches)\\b`, 'i').test(trailing);
227
+ return `${pluralContext ? '' : 'a '}structured user-input request`;
228
+ });
229
+ return masked.restore(normalized);
230
+ }
231
+
67
232
  function normalizeRuntimePaths(content) {
68
233
  return normalizeSourcePaths(content, {
69
234
  runtimeRoot: '.codex',
@@ -78,11 +243,11 @@ function normalizeRuntimePaths(content) {
78
243
 
79
244
  function normalizeSkillNames(content) {
80
245
  return content
81
- .replace(/\/hapo:([a-z0-9-]+)/gi, (_match, name) => `$hapo-${name}`)
82
- .replace(/\bhapo:([a-z0-9-]+)/gi, (_match, name) => `hapo-${name}`)
246
+ .replace(/\/cf:([a-z0-9-]+)/gi, (_match, name) => `$cf-${name}`)
247
+ .replace(/\bcf:([a-z0-9-]+)/gi, (_match, name) => `cf-${name}`)
83
248
  .replace(
84
249
  /(^|[\s("'`→])\/(brainstorm|code-review|debug|develop|docs|frontend-design|git|hotfix|inspect|question|research|specs|test)(?=$|[\s<`),.:])/gim,
85
- (_match, prefix, name) => `${prefix}$hapo-${name}`
250
+ (_match, prefix, name) => `${prefix}$cf-${name}`
86
251
  );
87
252
  }
88
253
 
@@ -129,7 +294,7 @@ function normalizeCodexBody(content, sourcePath = '') {
129
294
  if (!isInstructionAsset(sourcePath)) return next;
130
295
 
131
296
  next = normalizeAgentInvocations(normalizeAgentNames(normalizeSkillNames(next)));
132
- return applyReplacements(next, INSTRUCTION_REPLACEMENTS);
297
+ return applyReplacements(normalizeAskUserQuestion(next), INSTRUCTION_REPLACEMENTS);
133
298
  }
134
299
 
135
300
  function getCodexCopyOptions(baseOptions = {}) {