@haposoft/cafekit 0.16.0-rc.8 → 0.16.1

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 (323) hide show
  1. package/README.md +117 -28
  2. package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
  3. package/bin/__tests__/codex-hooks.test.js +175 -0
  4. package/bin/__tests__/codex-native.test.js +1882 -70
  5. package/bin/__tests__/develop-contract.test.js +442 -23
  6. package/bin/__tests__/grok-envelope.test.js +271 -0
  7. package/bin/__tests__/grok-platform.test.js +133 -0
  8. package/bin/__tests__/hook-payload.test.js +194 -0
  9. package/bin/__tests__/installer-safety.test.js +3 -3
  10. package/bin/__tests__/omp-bridge.test.js +168 -0
  11. package/bin/__tests__/omp-hooks.test.js +178 -0
  12. package/bin/__tests__/omp-runtime.test.js +150 -0
  13. package/bin/__tests__/optional-skill-inventory.test.js +249 -0
  14. package/bin/__tests__/output-styles.test.js +214 -0
  15. package/bin/__tests__/package-inventory.test.js +1735 -98
  16. package/bin/__tests__/runtime-dir.test.js +158 -0
  17. package/bin/__tests__/runtime-schema.test.js +113 -0
  18. package/bin/__tests__/secret-output-guardrail.test.js +122 -0
  19. package/bin/__tests__/skill-routing-source.test.js +172 -0
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +28 -1
  21. package/bin/install.js +15 -5
  22. package/bin/lib/codex-install.js +170 -5
  23. package/bin/lib/context.js +64 -3
  24. package/bin/lib/i18n.js +15 -36
  25. package/bin/lib/skill-deps.js +5 -5
  26. package/bin/phases/claude-runtime.js +23 -0
  27. package/bin/phases/claude-settings.js +8 -0
  28. package/bin/phases/codex-hooks.js +156 -0
  29. package/bin/phases/codex-runtime.js +29 -38
  30. package/bin/phases/copy-payload.js +15 -2
  31. package/bin/phases/omp-runtime.js +140 -0
  32. package/bin/phases/root-config.js +3 -0
  33. package/bin/phases/select-platform.js +22 -1
  34. package/bin/phases/select-skill-bundles.js +96 -0
  35. package/bin/phases/skill-inventory.js +71 -0
  36. package/bin/phases/skills-setup.js +9 -2
  37. package/bin/phases/summary.js +0 -4
  38. package/bin/phases/write-metadata.js +8 -5
  39. package/package.json +8 -2
  40. package/src/claude/CLAUDE.md +3 -3
  41. package/src/claude/agents/brainstormer.md +52 -69
  42. package/src/claude/agents/code-auditor.md +32 -14
  43. package/src/claude/agents/debugger.md +35 -20
  44. package/src/claude/agents/deployer.md +12 -5
  45. package/src/claude/agents/docs-keeper.md +11 -1
  46. package/src/claude/agents/implementer.md +25 -13
  47. package/src/claude/agents/inspector.md +6 -3
  48. package/src/claude/agents/project-manager.md +1 -1
  49. package/src/claude/agents/researcher.md +26 -55
  50. package/src/claude/agents/spec-maker.md +12 -7
  51. package/src/claude/agents/strategist.md +52 -0
  52. package/src/claude/agents/test-runner.md +117 -132
  53. package/src/claude/agents/ui-ux-designer.md +1 -1
  54. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
  55. package/src/claude/hooks/__tests__/precompact.test.js +158 -0
  56. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +2 -0
  57. package/src/claude/hooks/__tests__/spec-gate.test.js +348 -3
  58. package/src/claude/hooks/__tests__/state.test.js +7 -0
  59. package/src/claude/hooks/__tests__/statusline.test.js +196 -0
  60. package/src/claude/hooks/agent.cjs +13 -8
  61. package/src/claude/hooks/completion-authority.cjs +10 -4
  62. package/src/claude/hooks/docs-sync.cjs +14 -11
  63. package/src/claude/hooks/inspect-block.cjs +28 -8
  64. package/src/claude/hooks/lib/config.cjs +8 -2
  65. package/src/claude/hooks/lib/counter.cjs +7 -5
  66. package/src/claude/hooks/lib/hook-payload.cjs +160 -0
  67. package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
  68. package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
  69. package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
  70. package/src/claude/hooks/precompact.cjs +106 -0
  71. package/src/claude/hooks/privacy-block.cjs +4 -2
  72. package/src/claude/hooks/rules.cjs +5 -3
  73. package/src/claude/hooks/secret-output-guardrail.cjs +44 -0
  74. package/src/claude/hooks/semantic-review-authority.cjs +6 -4
  75. package/src/claude/hooks/session.cjs +37 -9
  76. package/src/claude/hooks/spec-gate.cjs +21 -11
  77. package/src/claude/hooks/spec-state.cjs +11 -7
  78. package/src/claude/hooks/state.cjs +7 -5
  79. package/src/claude/hooks/task-scaffold-guard.cjs +30 -9
  80. package/src/claude/hooks/usage.cjs +6 -5
  81. package/src/claude/migration-manifest.json +31 -15
  82. package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
  83. package/src/claude/output-styles/coding-level-1-junior.md +124 -0
  84. package/src/claude/output-styles/coding-level-2-mid.md +146 -0
  85. package/src/claude/output-styles/coding-level-3-senior.md +148 -0
  86. package/src/claude/output-styles/coding-level-4-lead.md +159 -0
  87. package/src/claude/output-styles/coding-level-5-god.md +91 -0
  88. package/src/claude/references/debugger/core-philosophy.md +5 -3
  89. package/src/claude/references/debugger/frontend-verification.md +2 -2
  90. package/src/claude/references/debugger/log-ci-analysis.md +13 -1
  91. package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
  92. package/src/claude/references/debugger/performance-diagnostics.md +2 -2
  93. package/src/claude/references/debugger/root-cause-tracing.md +37 -6
  94. package/src/claude/references/debugger/side-effect-gate.md +3 -1
  95. package/src/claude/references/debugger/verification-protocol.md +13 -12
  96. package/src/claude/rules/orchestrator.md +7 -1
  97. package/src/claude/rules/process-management.md +39 -0
  98. package/src/claude/rules/review-audit-self-decision.md +48 -0
  99. package/src/claude/rules/skill-domain-routing.md +26 -24
  100. package/src/claude/rules/skill-workflow-routing.md +22 -14
  101. package/src/claude/rules/state-sync.md +7 -1
  102. package/src/claude/rules/workflow.md +5 -3
  103. package/src/claude/runtime.json +1 -0
  104. package/src/claude/runtime.schema.json +205 -0
  105. package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
  106. package/src/claude/scripts/provenance.cjs +29 -9
  107. package/src/claude/scripts/spec-receipt.cjs +65 -4
  108. package/src/claude/scripts/workflow-policy.cjs +1 -1
  109. package/src/claude/settings/settings.json +16 -0
  110. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  111. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  112. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  113. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  114. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  115. package/src/claude/skills/code-review/SKILL.md +43 -25
  116. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  117. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  118. package/src/claude/skills/debug/SKILL.md +48 -19
  119. package/src/claude/skills/delegate/SKILL.md +1 -1
  120. package/src/claude/skills/develop/SKILL.md +92 -65
  121. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  122. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  123. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  124. package/src/claude/skills/docs/SKILL.md +42 -23
  125. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  126. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  127. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  128. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  129. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  130. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  131. package/src/claude/skills/docx/SKILL.md +1 -1
  132. package/src/claude/skills/git/SKILL.md +1 -1
  133. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  134. package/src/claude/skills/git/references/finish-branch.md +1 -1
  135. package/src/claude/skills/hotfix/SKILL.md +159 -170
  136. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  137. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  138. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  139. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  140. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  141. package/src/claude/skills/inspect/SKILL.md +55 -46
  142. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  143. package/src/claude/skills/loop/SKILL.md +114 -0
  144. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  145. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  146. package/src/claude/skills/pdf/SKILL.md +1 -1
  147. package/src/claude/skills/pptx/SKILL.md +1 -1
  148. package/src/claude/skills/question/SKILL.md +16 -13
  149. package/src/claude/skills/research/SKILL.md +70 -43
  150. package/src/claude/skills/route/SKILL.md +94 -0
  151. package/src/claude/skills/route/references/agent-timing.md +46 -0
  152. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  153. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  154. package/src/claude/skills/specs/SKILL.md +51 -71
  155. package/src/claude/skills/specs/references/review.md +11 -8
  156. package/src/claude/skills/specs/references/templates.md +34 -18
  157. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  158. package/src/claude/skills/specs/templates/research.md +2 -2
  159. package/src/claude/skills/sync/SKILL.md +6 -6
  160. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  161. package/src/claude/skills/test/SKILL.md +120 -120
  162. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  163. package/src/claude/skills/test/references/failure-triage.md +55 -121
  164. package/src/claude/skills/test/references/test-memory.md +20 -34
  165. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  166. package/src/claude/skills/web-testing/SKILL.md +1 -1
  167. package/src/claude/skills/xlsx/SKILL.md +1 -1
  168. package/src/claude/status.cjs +135 -33
  169. package/src/codex/AGENTS.md +14 -4
  170. package/src/codex/hooks/docs-sync.cjs +8 -3
  171. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  172. package/src/codex/hooks/rules.cjs +29 -1
  173. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  174. package/src/codex/hooks/spec-gate.cjs +3 -8
  175. package/src/codex/hooks/spec-state.cjs +3 -2
  176. package/src/codex/hooks.json +5 -0
  177. package/src/codex/rules/state-sync.md +7 -1
  178. package/src/codex/runtime.json +2 -0
  179. package/src/common/.env.example +1 -1
  180. package/src/omp/extensions/cafekit-bridge.mjs +233 -0
  181. package/src/omp/hooks/privacy-block.cjs +250 -0
  182. package/src/omp/runtime.json +33 -0
  183. package/bin/lib/rtk-run.js +0 -28
  184. package/bin/phases/setup-rtk.js +0 -159
  185. package/src/claude/skills/backend-development/SKILL.md +0 -101
  186. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  187. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  188. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  189. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  190. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  191. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  192. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  193. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  194. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  195. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  196. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  197. package/src/claude/skills/devops/.env.example +0 -76
  198. package/src/claude/skills/devops/SKILL.md +0 -102
  199. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  200. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  201. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  202. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  203. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  204. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  205. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  206. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  207. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  208. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  209. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  210. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  211. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  212. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  213. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  214. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  215. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  216. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  217. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  218. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  219. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  220. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  221. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  222. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  223. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  224. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  225. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  226. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  227. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  228. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  229. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  230. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  231. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  232. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  233. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  234. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  235. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  236. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  237. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  238. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  239. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  240. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  241. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  242. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  243. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  244. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  245. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  246. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  247. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  248. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  249. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  250. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  251. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  252. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  253. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  254. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  255. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  256. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  257. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  258. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  259. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  260. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  261. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  262. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  263. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  264. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  265. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  266. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  267. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  268. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  269. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  270. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  271. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  272. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  273. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  274. package/src/claude/skills/react-best-practices/README.md +0 -123
  275. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  276. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  277. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  278. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  279. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  280. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  281. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  282. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  283. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  284. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  285. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  286. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  287. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  288. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  289. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  290. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  291. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  292. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  293. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  294. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  295. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  296. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  297. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  298. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  299. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  300. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  301. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  302. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  303. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  304. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  305. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  306. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  307. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  308. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  309. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  310. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  311. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  312. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  313. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  314. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  315. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  316. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  317. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  318. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  319. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  320. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  321. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  322. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  323. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ // The gate hooks derive their runtime directory from their own location instead of a
4
+ // literal `.claude`. Each case copies the helper into a directory shaped like a real
5
+ // layout and requires it from there, so the derivation is exercised against the path the
6
+ // module actually sees — not a mocked one.
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 { test } = require('node:test');
13
+
14
+ const PACKAGE_ROOT = path.join(__dirname, '../..');
15
+ const HELPER = path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-dir.cjs');
16
+ const { spawnSync } = require('node:child_process');
17
+
18
+ /** Place the helper at <root>/<layout>/hooks/lib/ and return what it derives. */
19
+ function deriveAt(root, layout) {
20
+ const lib = path.join(root, layout, 'hooks', 'lib');
21
+ fs.mkdirSync(lib, { recursive: true });
22
+ const target = path.join(lib, 'runtime-dir.cjs');
23
+ fs.copyFileSync(HELPER, target);
24
+ // Each copy is a distinct module path, so require caches never collide.
25
+ return require(target);
26
+ }
27
+
28
+ function withTemp(run) {
29
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-runtime-dir-'));
30
+ try { return run(root); } finally { fs.rmSync(root, { recursive: true, force: true }); }
31
+ }
32
+
33
+ test('installed layouts derive their own folder', () => {
34
+ withTemp((root) => {
35
+ for (const folder of ['.claude', '.codex', '.omp']) {
36
+ const m = deriveAt(root, path.join('project-' + folder.slice(1), folder));
37
+ assert.equal(m.runtimeDirName(), folder);
38
+ assert.equal(m.runtimeDir('/p'), path.join('/p', folder));
39
+ assert.equal(m.runtimePath('/p', 'runtime.json'), path.join('/p', folder, 'runtime.json'));
40
+ }
41
+ });
42
+ });
43
+
44
+ test('the source tree maps platform to dotted name', () => {
45
+ withTemp((root) => {
46
+ for (const platform of ['claude', 'omp', 'codex']) {
47
+ const m = deriveAt(root, path.join('packages', 'spec', 'src', platform));
48
+ assert.equal(m.runtimeDirName(), `.${platform}`, `source tree for ${platform}`);
49
+ }
50
+ });
51
+ });
52
+
53
+ test('a dotted folder wins over the source marker', () => {
54
+ withTemp((root) => {
55
+ // An install whose absolute path happens to contain /packages/spec/src/ must still
56
+ // report its real folder, not a doubled-dot name derived from the marker.
57
+ const m = deriveAt(root, path.join('packages', 'spec', 'src', 'fixture', '.codex'));
58
+ assert.equal(m.runtimeDirName(), '.codex');
59
+ });
60
+ });
61
+
62
+ test('an unknown layout falls back to .claude', () => {
63
+ withTemp((root) => {
64
+ const m = deriveAt(root, path.join('somewhere', 'else'));
65
+ assert.equal(m.runtimeDirName(), '.claude', 'the pre-helper behaviour is the fallback');
66
+ });
67
+ });
68
+
69
+ test('the helper in the real source tree derives .claude', () => {
70
+ const m = require(HELPER);
71
+ assert.equal(m.runtimeDirName(), '.claude');
72
+ });
73
+
74
+ /** Copy the whole hook tree and the scripts it requires under <root>/<folder>/. */
75
+ function installTreeAs(root, folder) {
76
+ const rt = path.join(root, folder);
77
+ fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/hooks'), path.join(rt, 'hooks'), {
78
+ recursive: true, filter: (src) => !src.includes(`${path.sep}__tests__`),
79
+ });
80
+ fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/scripts'), path.join(rt, 'scripts'), { recursive: true });
81
+ return rt;
82
+ }
83
+
84
+ /** Provenance derives Base/Head with git, so the fixture must be a repository (one empty commit). */
85
+ function gitInit(root) {
86
+ for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['commit', '--allow-empty', '-qm', 'fixture']]) {
87
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
88
+ assert.equal(result.status, 0, `git ${args[0]} failed: ${result.stderr}`);
89
+ }
90
+ }
91
+
92
+ function runHook(rt, name, payload) {
93
+ const r = spawnSync(process.execPath, [path.join(rt, 'hooks', name)], {
94
+ cwd: path.dirname(rt), input: JSON.stringify(payload), encoding: 'utf8',
95
+ env: { ...process.env, CLAUDE_PROJECT_DIR: '', PROJECT_ROOT: '' },
96
+ });
97
+ return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
98
+ }
99
+
100
+ test('a hook tree copied under .omp reads .omp/runtime.json', () => {
101
+ withTemp((root) => {
102
+ const rt = installTreeAs(root, '.omp');
103
+ gitInit(root);
104
+ fs.mkdirSync(path.join(root, 'specs'), { recursive: true });
105
+ fs.writeFileSync(path.join(root, '.env'), 'TOKEN=redacted\n');
106
+ assert.equal(fs.existsSync(path.join(root, '.claude')), false, 'the fixture must hold no .claude path at all');
107
+
108
+ // rules.cjs: docs path from the sentinel config
109
+ fs.writeFileSync(path.join(rt, 'runtime.json'), JSON.stringify({ paths: { docs: 'SENTINEL-DOCS', plans: 'plans' } }));
110
+ const rules = runHook(rt, 'rules.cjs', { session_id: `s-${Date.now()}`, cwd: root });
111
+ assert.match(rules.out, /SENTINEL-DOCS/, 'rules.cjs must read .omp/runtime.json');
112
+
113
+ // inspect-block.cjs: disabled through the sentinel allows a broad search
114
+ fs.writeFileSync(path.join(rt, 'runtime.json'), JSON.stringify({ inspect: { enabled: false } }));
115
+ const inspect = runHook(rt, 'inspect-block.cjs', { tool_name: 'Bash', tool_input: { command: 'grep -rn TODO .' }, cwd: root });
116
+ assert.equal(inspect.code, 0, 'inspect.enabled:false in .omp/runtime.json must allow');
117
+
118
+ // privacy-block.cjs: kill switch through the sentinel allows a .env read
119
+ fs.writeFileSync(path.join(rt, 'runtime.json'), JSON.stringify({ privacyBlock: false }));
120
+ const privacy = runHook(rt, 'privacy-block.cjs', { tool_name: 'Read', tool_input: { file_path: path.join(root, '.env') }, cwd: root });
121
+ assert.doesNotMatch(privacy.out, /permissionDecision/, 'privacyBlock:false in .omp/runtime.json must allow');
122
+
123
+ // spec-gate.cjs exits silently unless a packet resolves, and only then reads the
124
+ // completion_gate flag. Resolving a done task derives provenance through git, so the
125
+ // fixture is a repository (gitInit above) and holds one done task to look at.
126
+ const feature = path.join(root, 'specs', 'demo'); fs.mkdirSync(feature, { recursive: true });
127
+ fs.writeFileSync(path.join(feature, 'plan.md'), '# Demo plan\n');
128
+ fs.writeFileSync(path.join(feature, 'task-01-demo.md'), [
129
+ '# Task 01: demo', '', 'Status: done', '', '## Dependencies', '', '- none', '',
130
+ '## Verification Plan', '', '- Command: node --test', '',
131
+ ].join('\n'));
132
+ // the completion_gate flag is read from the sentinel and answered as a block
133
+ fs.writeFileSync(path.join(rt, 'runtime.json'), JSON.stringify({ spec: { completion_gate: false } }));
134
+ const gate = runHook(rt, 'spec-gate.cjs', { session_id: 's', cwd: root, hook_event_name: 'Stop', stop_hook_active: false });
135
+ assert.match(gate.out, /worker-writable flag/, 'spec-gate.cjs must read .omp/runtime.json');
136
+ });
137
+ });
138
+
139
+ test('advice names the directory the hook lives in', () => {
140
+ withTemp((root) => {
141
+ for (const [folder, skills] of [['.omp', '.agents/skills'], ['.claude', '.claude/skills']]) {
142
+ const project = path.join(root, `p${folder}`); fs.mkdirSync(project, { recursive: true });
143
+ const rt = installTreeAs(project, folder);
144
+ fs.mkdirSync(path.join(project, 'specs', 'demo'), { recursive: true });
145
+ fs.writeFileSync(path.join(rt, 'runtime.json'), '{}');
146
+ // task-scaffold-guard: a raw Write into a nested task path is refused with scaffold advice
147
+ const guard = runHook(rt, 'task-scaffold-guard.cjs', { tool_name: 'Write', tool_input: { file_path: path.join(project, 'specs', 'demo', 'tasks', 'task-R0-01-x.md'), content: '# x' }, cwd: project });
148
+ assert.match(guard.out, new RegExp(`node ${folder.replace('.', '\\.')}/scripts/spec-scaffold\\.cjs`), `scaffold advice must name ${folder}`);
149
+ // inspect-block: header hint names the runtime file (static check on the installed copy)
150
+ const src = fs.readFileSync(path.join(rt, 'hooks', 'inspect-block.cjs'), 'utf8');
151
+ assert.match(src, /<runtime>\/runtime\.json/, 'the disable hint no longer hardcodes a platform');
152
+ // agent.cjs: skills advice follows the platform registry, not the runtime dir
153
+ const agentSrc = fs.readFileSync(path.join(rt, 'hooks', 'agent.cjs'), 'utf8');
154
+ assert.match(agentSrc, /'\.agents\/skills'/, 'non-Claude platforms read .agents/skills');
155
+ void skills;
156
+ }
157
+ });
158
+ });
@@ -0,0 +1,113 @@
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', 'src/omp/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 each shipped runtime.json 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('every runtime points at the schema that installs beside it', () => {
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
+ const ompPhase = fs.readFileSync(path.join(PACKAGE_ROOT, 'bin/phases/omp-runtime.js'), 'utf8');
75
+ assert.match(ompPhase, /runtime\.schema\.json/, 'the schema must also be installed for omp');
76
+ });
77
+
78
+ test('every documented property carries a description a reader can act on', () => {
79
+ for (const [dotted, node] of schemaProperties(SCHEMA)) {
80
+ if (dotted === '$schema') continue;
81
+ assert.ok(node.description, `${dotted} has no description`);
82
+ assert.ok(node.description.length > 20, `${dotted} has a description too short to help: ${node.description}`);
83
+ }
84
+ });
85
+
86
+ test('keys the runtime does not honour are marked deprecated, not documented as working', () => {
87
+ // develop.parallel and the hooks toggle map are still shipped for compatibility, but
88
+ // nothing reads them. The schema must say so rather than imply they work.
89
+ for (const dead of ['hooks', 'develop']) {
90
+ const node = SCHEMA.properties[dead];
91
+ assert.ok(node, `${dead} is shipped in runtime.json and must appear in the schema`);
92
+ assert.equal(node.deprecated, true, `${dead} is not honoured and must be marked deprecated`);
93
+ assert.match(node.description, /[Nn]ot honoured/, `${dead} must say plainly that it does nothing`);
94
+ }
95
+ });
96
+
97
+ test('the completion gate flag is documented as a block, never as a switch', () => {
98
+ const node = SCHEMA.properties.spec.properties.completion_gate;
99
+ assert.match(node.description, /[Nn]ot a switch/);
100
+ assert.match(node.description, /blocks/, 'the description must say the gate blocks while the flag is present');
101
+ });
102
+
103
+ test('statusline values in the schema match the modes the renderer implements', () => {
104
+ const status = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/status.cjs'), 'utf8');
105
+ const implemented = new Set((status.match(/case '([a-z]+)':/g) || []).map((line) => line.slice(6, -2)));
106
+ for (const mode of SCHEMA.properties.statusline.enum) {
107
+ assert.ok(implemented.has(mode), `schema offers statusline "${mode}" but the renderer has no branch for it`);
108
+ }
109
+ const sections = SCHEMA.properties.statuslineLayout.properties.lines.items.items.enum;
110
+ for (const section of sections) {
111
+ assert.ok(status.includes(`'${section}'`), `schema offers layout section "${section}" but status.cjs never names it`);
112
+ }
113
+ });
@@ -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,8 @@ 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', 'hooks/lib/runtime-dir.cjs',
267
+ 'hooks/lib/hook-payload.cjs',
267
268
  ]);
268
269
  if (kind === 'claude') {
269
270
  for (const name of [
@@ -466,3 +467,29 @@ test('scaffold creates planning artifacts but no task or feature receipts', () =
466
467
  assert.equal(files.some((file) => file.includes('receipt')), false);
467
468
  });
468
469
  });
470
+
471
+ test('legacy receipt still requires provenance under a clean committed tree', () => {
472
+ // The workflow task path may validate a committed, unchanged receipt on
473
+ // structure alone; the legacy task and feature receipt paths must not.
474
+ withTempResources((resources) => {
475
+ const root = gitRoot(resources);
476
+ const { featureDir } = prepare(root);
477
+ const stale = { base: 'f'.repeat(40), head: 'e'.repeat(40) };
478
+ fs.writeFileSync(
479
+ path.join(featureDir, 'receipts', path.basename(TASK)),
480
+ receiptBody(stale, `Task: ${path.basename(TASK)}\nTask path: ${TASK}`),
481
+ );
482
+ fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), receiptBody(stale, `Feature: ${FEATURE}`));
483
+ for (const args of [['add', '-A'], ['commit', '-qm', 'committed legacy receipts']]) {
484
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
485
+ assert.equal(result.status, 0, result.stderr);
486
+ }
487
+ const clean = spawnSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' });
488
+ assert.equal(clean.stdout.trim(), '', 'fixture tree must be clean before the check');
489
+ const ctx = runtime(root);
490
+ const taskFailures = RECEIPT.checkTaskReceipt(featureDir, TASK, { completed_at: '2026-08-13T00:00:00.000Z' }, ctx, POLICY).failures;
491
+ assert.ok(taskFailures.includes('provenance'), `legacy task receipts keep provenance binding when committed and clean; got ${JSON.stringify(taskFailures)}`);
492
+ const featureFailures = RECEIPT.checkFeatureReceipt(featureDir, ctx, POLICY).failures;
493
+ assert.ok(featureFailures.includes('provenance'), `feature receipts keep provenance binding when committed and clean; got ${JSON.stringify(featureFailures)}`);
494
+ });
495
+ });