@haposoft/cafekit 0.16.0-rc.7 → 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 (299) 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 +295 -0
  4. package/bin/__tests__/codex-native.test.js +2289 -34
  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 +2131 -16
  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 +24 -13
  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 +87 -4
  46. package/src/claude/hooks/__tests__/state.test.js +254 -3
  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 +32 -5
  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/spec-receipt.cjs +76 -21
  89. package/src/claude/scripts/spec-resolver.cjs +144 -4
  90. package/src/claude/scripts/workflow-policy.cjs +1 -1
  91. package/src/claude/settings/settings.json +16 -0
  92. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  93. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  94. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  95. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  96. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  97. package/src/claude/skills/code-review/SKILL.md +43 -25
  98. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  99. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  100. package/src/claude/skills/debug/SKILL.md +48 -19
  101. package/src/claude/skills/delegate/SKILL.md +1 -1
  102. package/src/claude/skills/develop/SKILL.md +92 -65
  103. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  104. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  105. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  106. package/src/claude/skills/docs/SKILL.md +42 -23
  107. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  108. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  109. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  110. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  111. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  112. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  113. package/src/claude/skills/docx/SKILL.md +1 -1
  114. package/src/claude/skills/git/SKILL.md +1 -1
  115. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  116. package/src/claude/skills/git/references/finish-branch.md +1 -1
  117. package/src/claude/skills/hotfix/SKILL.md +159 -170
  118. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  119. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  120. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  121. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  122. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  123. package/src/claude/skills/inspect/SKILL.md +55 -46
  124. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  125. package/src/claude/skills/loop/SKILL.md +114 -0
  126. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  127. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  128. package/src/claude/skills/pdf/SKILL.md +1 -1
  129. package/src/claude/skills/pptx/SKILL.md +1 -1
  130. package/src/claude/skills/question/SKILL.md +16 -13
  131. package/src/claude/skills/research/SKILL.md +70 -43
  132. package/src/claude/skills/route/SKILL.md +94 -0
  133. package/src/claude/skills/route/references/agent-timing.md +46 -0
  134. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  135. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  136. package/src/claude/skills/specs/SKILL.md +48 -57
  137. package/src/claude/skills/specs/references/review.md +26 -8
  138. package/src/claude/skills/specs/references/templates.md +76 -31
  139. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  140. package/src/claude/skills/specs/templates/research.md +2 -2
  141. package/src/claude/skills/sync/SKILL.md +6 -6
  142. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  143. package/src/claude/skills/test/SKILL.md +120 -120
  144. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  145. package/src/claude/skills/test/references/failure-triage.md +55 -121
  146. package/src/claude/skills/test/references/test-memory.md +20 -34
  147. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  148. package/src/claude/skills/web-testing/SKILL.md +1 -1
  149. package/src/claude/skills/xlsx/SKILL.md +1 -1
  150. package/src/claude/status.cjs +135 -33
  151. package/src/codex/AGENTS.md +14 -4
  152. package/src/codex/hooks/docs-sync.cjs +8 -3
  153. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  154. package/src/codex/hooks/rules.cjs +29 -1
  155. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  156. package/src/codex/hooks/spec-gate.cjs +3 -8
  157. package/src/codex/hooks/spec-state.cjs +34 -5
  158. package/src/codex/hooks.json +5 -0
  159. package/src/codex/runtime.json +2 -0
  160. package/src/common/.env.example +1 -1
  161. package/src/claude/skills/backend-development/SKILL.md +0 -101
  162. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  163. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  164. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  165. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  166. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  167. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  168. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  169. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  170. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  171. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  172. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  173. package/src/claude/skills/devops/.env.example +0 -76
  174. package/src/claude/skills/devops/SKILL.md +0 -102
  175. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  176. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  177. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  178. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  179. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  180. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  181. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  182. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  183. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  184. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  185. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  186. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  187. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  188. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  189. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  190. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  191. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  192. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  193. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  194. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  195. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  196. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  197. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  198. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  199. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  200. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  201. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  202. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  203. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  204. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  205. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  206. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  207. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  208. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  209. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  210. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  211. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  212. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  213. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  214. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  215. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  216. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  217. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  218. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  219. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  220. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  221. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  222. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  223. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  224. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  225. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  226. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  227. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  228. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  229. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  230. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  231. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  232. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  233. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  234. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  235. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  236. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  237. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  238. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  239. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  240. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  241. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  242. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  243. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  244. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  245. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  246. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  247. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  248. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  249. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  250. package/src/claude/skills/react-best-practices/README.md +0 -123
  251. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  252. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  253. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  254. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  255. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  256. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  257. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  258. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  259. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  260. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  261. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  262. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  263. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  264. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  265. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  266. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  267. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  268. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  269. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  270. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  271. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  272. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  273. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  274. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  275. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  276. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  277. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  278. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  279. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  280. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  281. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  282. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  283. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  284. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  285. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  286. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  287. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  288. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  289. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  290. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  291. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  292. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  293. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  294. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  295. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  296. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  297. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  298. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  299. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const assert = require('node:assert/strict');
4
+ const { createHash } = require('node:crypto');
4
5
  const fs = require('node:fs');
5
6
  const os = require('node:os');
6
7
  const path = require('node:path');
@@ -8,6 +9,7 @@ const { spawnSync } = require('node:child_process');
8
9
  const { test } = require('node:test');
9
10
 
10
11
  const {
12
+ convertCodexAgentContent,
11
13
  normalizeCodexBody,
12
14
  transformManagedCodexContent,
13
15
  upsertManagedCodexBlock
@@ -29,6 +31,153 @@ const MANIFEST = JSON.parse(
29
31
  fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8')
30
32
  );
31
33
  const SPECS_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/specs');
34
+ const SPEC_MAKER_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/spec-maker.md');
35
+ const BRAINSTORM_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/brainstorm');
36
+ const BRAINSTORM_SKILL_SOURCE_PATH = path.join(BRAINSTORM_SOURCE_ROOT, 'SKILL.md');
37
+ const BRAINSTORM_REFERENCE_SOURCE_PATH = path.join(
38
+ BRAINSTORM_SOURCE_ROOT,
39
+ 'references/question-framework.md'
40
+ );
41
+ const BRAINSTORM_AGENT_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/brainstormer.md');
42
+ const BRAINSTORM_SOURCE_PATHS = [
43
+ BRAINSTORM_SKILL_SOURCE_PATH,
44
+ BRAINSTORM_REFERENCE_SOURCE_PATH,
45
+ BRAINSTORM_AGENT_SOURCE_PATH
46
+ ];
47
+ const RESEARCH_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/research');
48
+ const RESEARCH_SKILL_SOURCE_PATH = path.join(RESEARCH_SOURCE_ROOT, 'SKILL.md');
49
+ const RESEARCH_AGENT_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/researcher.md');
50
+ const DEVELOP_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/develop');
51
+ const DEVELOP_BUNDLE = [
52
+ 'SKILL.md',
53
+ 'references/quality-gate.md',
54
+ 'references/parallel-waves.md',
55
+ 'references/subagent-patterns.md'
56
+ ];
57
+ const TEST_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/test');
58
+ const TEST_BUNDLE = [
59
+ 'SKILL.md',
60
+ 'references/execution-strategy.md',
61
+ 'references/failure-triage.md',
62
+ 'references/test-memory.md'
63
+ ];
64
+ const TEST_RUNNER_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/test-runner.md');
65
+ const DOCS_ADAPTIVE_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/docs');
66
+ const DOCS_ADAPTIVE_BUNDLE = [
67
+ 'SKILL.md',
68
+ 'references/init-workflow.md',
69
+ 'references/update-workflow.md',
70
+ 'references/standard-docs-workflow.md'
71
+ ];
72
+ const HOTFIX_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/hotfix');
73
+ const HOTFIX_BUNDLE = [
74
+ 'SKILL.md',
75
+ 'references/diagnosis-protocol.md',
76
+ 'references/review-cycle.md',
77
+ 'references/parallel-patterns.md',
78
+ 'references/prevention-gate.md',
79
+ 'references/workflow-specialized.md'
80
+ ];
81
+ const DEBUG_SKILL_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/skills/debug/SKILL.md');
82
+ const DEBUG_AGENT_SOURCE_PATH = path.join(PACKAGE_ROOT, 'src/claude/agents/debugger.md');
83
+ const DEBUG_REFERENCE_FILES = [
84
+ 'core-philosophy.md',
85
+ 'root-cause-tracing.md',
86
+ 'verification-protocol.md',
87
+ 'log-ci-analysis.md',
88
+ 'parallel-agent-hydration.md',
89
+ 'side-effect-gate.md'
90
+ ];
91
+ const CODE_REVIEW_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/code-review');
92
+ const IMPLEMENTATION_READINESS_BOUNDARY_ROWS = [
93
+ ['Interaction/UI', 'entry journey; visible/loading/empty/error states; input/focus/keyboard; accessibility; responsive/native/device behavior'],
94
+ ['API/CLI', 'entrypoint/route or command grammar; identity/auth; input/default/normalization; success output; error/status/exit; duplicate/retry/idempotency; compatibility'],
95
+ ['Data/schema', 'authority/storage/transaction; version; exact keys/nesting/types; required/optional; enum/format/bounds/cardinality; unknown-field behavior; compatibility/migration'],
96
+ ['Async/state', 'initial/terminal states; event + guard + effect + next + error; ordering/concurrency; duplicate/retry; writer/lock acquire/contention/release; cancellation; rollback/recovery'],
97
+ ['Filesystem/security', 'authoritative root; trusted/untrusted segment grammar; lexical + canonical containment and symlink policy; flags/mode; temp/rename/fsync; lock/stale reclaim; crash cleanup'],
98
+ ['Runtime/deploy', 'config/env/flags; registration/packaging; OS/arch; rollout/rollback; health/logging; operator recovery'],
99
+ ['Time/retention', 'clock source; unit/precision/timezone; endpoints and inclusion/comparator; anomaly behavior; expiry/purge/recovery'],
100
+ ['AI/model', 'provider/model/prompt/tool schema; nondeterminism/bounds; safety/privacy; fallback; cost/token limit; eval oracle'],
101
+ ['Integration/proof', 'caller; export/registration; packaging/config; native path/consumer; proof level (`source`/`installed`/`live`); observable failure oracle']
102
+ ];
103
+
104
+ function boundaryTableHasRequiredRows(table) {
105
+ const [header, ...rows] = table;
106
+ const labels = rows.map((row) => row[0]);
107
+ return JSON.stringify(header || []) === JSON.stringify(['Boundary', 'Required contract when material'])
108
+ && new Set(labels).size === labels.length
109
+ && IMPLEMENTATION_READINESS_BOUNDARY_ROWS.every((expected) =>
110
+ rows.some((row) => JSON.stringify(row) === JSON.stringify(expected)));
111
+ }
112
+
113
+ const IMPLEMENTATION_READINESS_CLAUSES = {
114
+ noInvention: 'Before implementation handoff, apply the **no-invention gate**: if two implementations conform to the packet text yet can produce different externally observable output, state, error, security, or compatibility behavior, surface the missing choice as an explicit C1 or C2 question and block handoff.',
115
+ materialDefinition: 'A boundary is material when the task creates, changes, or depends on it and a different choice changes an external observation, security, durable data, compatibility, or proof reachability. Require only the matching material row; omit nonmaterial categories.',
116
+ exactBoundaryChoices: 'For every required row, name each listed choice exactly; labels such as “JSON”, “local path”, “locked”, or “timestamped” alone remain unresolved.',
117
+ proofPlanLines: [
118
+ '- Command: `<exact runnable command>`',
119
+ '- Named probe: <existing concrete probe/test/hook ID; never only a suite label>',
120
+ '- Reachability: <known command/caller/environment per required level; `UNKNOWN` only when the path cannot yet be established>',
121
+ '- Oracle: <externally observable success or failure>',
122
+ '- Counterexample: <material alternative behavior that must make this proof fail>',
123
+ '- Artifacts: <required artifact path plus digest algorithm/comparison rule, or explicitly ephemeral with cleanup rule>'
124
+ ],
125
+ proofTrace: 'Trace `Command → Named probe → Reachability → Oracle`.',
126
+ namedProbeOwnership: 'Aggregate suites name the\nowning concrete probe.',
127
+ proofLevelSeparation: 'Levels stay separate and never promote one another.',
128
+ disposableTemplateControls: 'Run mutation or destructive\nnegative controls only on disposable copies under a verified temporary root,\nnever tracked worktree or canonical source bytes.',
129
+ proofLevelMapping: 'For every required level in each referenced CP row, map its named probe and\nreachability here; one command may own several explicitly named level probes.',
130
+ disposableReviewControls: 'Run mutation or destructive negative controls only on disposable copies below a verified temporary root, never tracked worktree or canonical source bytes.',
131
+ failureSemantics: '`Crash` means abrupt unhandled termination before the claimed catch point; a catchable failure returns/raises an error or exits nonzero. Never use them interchangeably.',
132
+ privacyIdentifiers: 'Any privacy/security claim names the exact identifier surface at risk, such as an env var, header, path, token class, or field name; generic “sensitive data” is insufficient.',
133
+ freshReplay: 'After applying an accepted C2 finding, a fresh-context closure pass records and freshly replays its original counterexample after the repair under this exact review-log header:',
134
+ distinctRepairProof: '`Repaired at` cites the repair edit; `Proved at` must cite distinct evidence from the fresh replay, never the repair-edit citation.',
135
+ closureTransition: 'An accepted finding transitions `accepted → repaired → PASS|FAIL|UNKNOWN`.',
136
+ unknownBlocks: 'Only `PASS` closes it; `FAIL` remains open for the remaining paper-review round; `UNKNOWN` blocks implementation handoff.',
137
+ scopeReturnsToC1: 'A repair that adds user semantics or scope returns to C1.'
138
+ };
139
+ const ADAPTIVE_COVERAGE_PROFILE_HEADER = [
140
+ 'ID', 'Outcome', 'Change kinds', 'Material surfaces', 'Ambiguity/action',
141
+ 'Risk/evidence', 'Required proof'
142
+ ];
143
+ const ADAPTIVE_COVERAGE_PROFILE_ROW = [
144
+ 'CP-01', '<externally observable outcome>', '<all kinds>', '<all material surfaces>',
145
+ '<state + action>', '<level + evidence>', '<source/installed/live set>'
146
+ ];
147
+ const ADAPTIVE_COVERAGE_AMBIGUITY_ROWS = [
148
+ ['State', 'Required action'],
149
+ ['`none`', 'proceed'],
150
+ ['`examples-needed`', 'add two or three examples only for an already decided rule; promote to `decision-needed` if an example changes observable behavior'],
151
+ ['`decision-needed`', 'ask the user at C1/C2 and keep affected tasks blocked'],
152
+ ['`design-needed`', 'after user-owned decisions settle, route material competing technical designs through Brainstorm']
153
+ ];
154
+ const ADAPTIVE_REVIEWER_ROWS = [
155
+ ['Groups', 'Reviewers', 'Roles', 'Claim budget'],
156
+ ['1-2', '2', 'Fact Checker plus all matching material lenses', 'about 5 per group'],
157
+ ['3-5', '3', 'Fact Checker plus all matching material lenses', 'about 10 per group'],
158
+ ['6+', '4', 'Fact Checker plus all matching material lenses', 'at least 15 total']
159
+ ];
160
+ const ADAPTIVE_COVERAGE_CLAUSES = {
161
+ riskFirst: 'Classify material risk before choosing a workflow; user wording never lowers an observed floor.',
162
+ criticalFloor: '`critical`: auth/secrets/privacy; destructive/irreversible work or possible data loss/corruption; money/privilege/safety; production-state mutation.',
163
+ elevatedFloor: '`elevated`: cross-component contracts, compatibility, concurrency, external integration, or installed/runtime behavior.',
164
+ frontmatterGate: 'skip only when a change is clear, isolated, reversible, routine, and likely limited to one or two files.',
165
+ directGate: 'Work directly only when the cause and change are clear, isolated, reversible,\n`routine`, and likely limited to one or two files.',
166
+ splitRoute: 'Split three or more independent\nsubsystems; otherwise use one Specs packet for any material work that does not qualify for direct work or Brainstorm-only exploration.',
167
+ independentSubsystem: 'A subsystem is independent only when its outcome, boundary, and verification/deployment path can move through the lifecycle separately.',
168
+ profileAuthority: 'For a Specs route, `plan.md` owns one `## Coverage profile` row per externally observable outcome; direct and Brainstorm-only routes do not persist it.',
169
+ openKinds: 'Change kinds are multi-valued (`add`, `modify`, `fix`, `refactor`, `remove`, `migrate`, `integrate`), and unfamiliar kinds or surfaces use `other:<verbatim>` rather than disappearing.',
170
+ scopedUnion: 'Each task references its CP IDs; authoring, review, edge, and proof obligations union only inside affected rows/tasks.',
171
+ profileRederivation: 'Rederive affected CP rows after any accepted scope, outcome, criteria, ownership, dependency, risk, or proof delta before task status.',
172
+ plannedProof: '`Required proof` is a planned level set, not execution\nevidence: known but unrun proof may be `pending`; `UNKNOWN` reachability blocks\n`pending`; missing, failed, or unavailable required evidence blocks `done`/C3.',
173
+ proofSeparation: 'Levels stay separate and never promote one another.',
174
+ liveLimit: 'Source/static checks prove the written contract, not live-model adherence.',
175
+ specMakerAuthority: 'they are the canonical risk and coverage authority. Do not duplicate\ntheir taxonomy here.',
176
+ specMakerAmbiguity: 'Apply the canonical ambiguity action; examples never decide observable behavior.',
177
+ specMakerRoute: 'Apply their risk-first route before C1 and stop when the\nrequest qualifies for direct work; hand off when it requires Brainstorm-only exploration.',
178
+ reviewRisk: 'Keep Fact Checker as the baseline. Assign every remaining material CP risk to a\nnamed reviewer lens; a critical row includes both relevant security-adversary and\nfailure-mode coverage, and nonmaterial lenses are not added.',
179
+ reviewCapacity: 'Reviewer count is fixed by the table, not lens count. Give each reviewer a distinct primary lens; when material lenses exceed reviewers, combine related named lenses on one reviewer and keep every material lens assigned.'
180
+ };
32
181
  const V3_SPECS_BUNDLE = [
33
182
  'SKILL.md',
34
183
  'references/review.md',
@@ -79,6 +228,21 @@ function install(root, extraArgs = []) {
79
228
  );
80
229
  }
81
230
 
231
+ function installPlatforms(root, platforms, extraArgs = []) {
232
+ return spawnSync(
233
+ process.execPath,
234
+ [INSTALLER, '--platform', platforms.join(','), '--yes', ...extraArgs],
235
+ { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } }
236
+ );
237
+ }
238
+
239
+ function installedCatalog(root, runtime) {
240
+ const script = path.join(root, runtime === 'codex' ? '.codex' : '.claude', 'scripts', 'generate-skill-catalog.cjs');
241
+ const result = spawnSync(process.execPath, [script, '--json'], { cwd: root, encoding: 'utf8' });
242
+ assert.equal(result.status, 0, result.stderr);
243
+ return JSON.parse(result.stdout);
244
+ }
245
+
82
246
  function allFiles(root, predicate) {
83
247
  const found = [];
84
248
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
@@ -101,6 +265,13 @@ function parseGeneratedTomlString(content, key) {
101
265
  return JSON.parse(match[1]);
102
266
  }
103
267
 
268
+ function replaceGeneratedTomlString(content, key, value) {
269
+ const pattern = new RegExp(`^${key} = .+$`, 'gm');
270
+ const matches = [...content.matchAll(pattern)];
271
+ assert.equal(matches.length, 1, `expected exactly one TOML key: ${key}`);
272
+ return content.replace(pattern, `${key} = ${JSON.stringify(value)}`);
273
+ }
274
+
104
275
  function renderCanonicalVerificationExample(template) {
105
276
  const values = new Map([
106
277
  ['SUBJECT_REQ', '1'],
@@ -159,8 +330,225 @@ function assertInstalledVerificationModel(grounderPath, designTemplate) {
159
330
  }
160
331
  }
161
332
 
162
- function installedAuthoringProjectionIssues(files) {
333
+ function markdownSection(content, heading) {
334
+ const lines = String(content).split('\n');
335
+ const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
336
+ if (start < 0) return '';
337
+ const end = lines.findIndex((line, index) => index > start && /^##\s+/.test(line));
338
+ return lines.slice(start + 1, end < 0 ? undefined : end).join('\n');
339
+ }
340
+
341
+ function markdownTableUnderHeading(content, heading) {
342
+ const lines = markdownSection(content, heading).split('\n');
343
+ const tableStart = lines.findIndex((line) => line.trim().startsWith('|'));
344
+ if (tableStart < 0) return [];
345
+ const rows = [];
346
+ for (let index = tableStart; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
347
+ const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
348
+ if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;
349
+ rows.push(cells);
350
+ }
351
+ return rows;
352
+ }
353
+
354
+ function normalizeMarkdownWhitespace(content) {
355
+ return String(content).replace(/\s+/g, ' ').trim();
356
+ }
357
+
358
+ function markdownBetweenHeadings(content, startHeading, endHeading) {
359
+ const value = String(content);
360
+ const startMarker = `## ${startHeading}`;
361
+ const endMarker = `## ${endHeading}`;
362
+ const startIndex = value.indexOf(startMarker);
363
+ const endIndex = value.indexOf(endMarker, startIndex + startMarker.length);
364
+ if (startIndex < 0 || endIndex < 0) return '';
365
+ return value.slice(startIndex + startMarker.length, endIndex);
366
+ }
367
+
368
+ function implementationReadinessIssues(input) {
369
+ const keys = input && typeof input === 'object' && !Array.isArray(input)
370
+ ? Object.keys(input).sort()
371
+ : [];
372
+ if (keys.join(',') !== 'review,templates'
373
+ || typeof input.templates !== 'string' || typeof input.review !== 'string') {
374
+ throw new TypeError('implementation-readiness checker expects templates and review UTF-8 strings');
375
+ }
376
+
377
+ const issues = new Set();
378
+ const { templates, review } = input;
379
+ const authoring = markdownSection(templates, 'No-invention and conditional boundary contracts');
380
+ if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.noInvention)) {
381
+ issues.add('no-invention');
382
+ }
383
+ const contradictoryNoInvention = /\b(?:exception|however)\b.{0,160}\bimplementation handoff\b.{0,80}\b(?:may|can)\s+(?:proceed|continue)\b.{0,160}\bunresolved\b/i;
384
+ if (contradictoryNoInvention.test(normalizeMarkdownWhitespace(authoring))) {
385
+ issues.add('no-invention');
386
+ }
387
+
388
+ const boundaryTable = markdownTableUnderHeading(
389
+ templates,
390
+ 'No-invention and conditional boundary contracts'
391
+ );
392
+ if (!authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.materialDefinition)
393
+ || !authoring.includes(IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices)
394
+ || IMPLEMENTATION_READINESS_BOUNDARY_ROWS.length !== 9
395
+ || !boundaryTableHasRequiredRows(boundaryTable)) {
396
+ issues.add('boundary-contract');
397
+ }
398
+
399
+ const proof = markdownSection(templates, 'Verification Plan');
400
+ const proofPlanLines = proof.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '));
401
+ const normalizedProofContract = normalizeMarkdownWhitespace(markdownBetweenHeadings(
402
+ templates,
403
+ 'Verification Plan',
404
+ 'Canonical inline Receipt'
405
+ ));
406
+ const normalizedProofClauses = [
407
+ IMPLEMENTATION_READINESS_CLAUSES.proofTrace,
408
+ IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
409
+ IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
410
+ IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
411
+ IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping
412
+ ].map(normalizeMarkdownWhitespace);
413
+ const reviewNegativeControls = normalizeMarkdownWhitespace(
414
+ markdownSection(review, 'B2 — fresh-context red team')
415
+ );
416
+ if (JSON.stringify(proofPlanLines) !== JSON.stringify(IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines)
417
+ || normalizedProofClauses.some((clause) => !normalizedProofContract.includes(clause))
418
+ || !reviewNegativeControls.includes(normalizeMarkdownWhitespace(
419
+ IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls
420
+ ))) {
421
+ issues.add('proof-chain');
422
+ }
423
+
424
+ const failureGuidance = markdownSection(templates, 'Twelve edge-case dimensions');
425
+ if (!failureGuidance.includes(IMPLEMENTATION_READINESS_CLAUSES.failureSemantics)) {
426
+ issues.add('failure-semantics');
427
+ }
428
+
429
+ const evidenceRules = markdownSection(review, 'B1 — evidence rule');
430
+ if (!evidenceRules.includes(IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers)) {
431
+ issues.add('privacy-identifiers');
432
+ }
433
+
434
+ const closure = markdownSection(review, 'Accepted-repair closure');
435
+ const normalizedClosure = normalizeMarkdownWhitespace(closure);
436
+ const closureHeader = markdownTableUnderHeading(review, 'Accepted-repair closure')[0] || [];
437
+ if (JSON.stringify(closureHeader) !== JSON.stringify([
438
+ 'ID', 'Decision', 'Original counterexample', 'Repaired at', 'Proved at', 'Replay', 'Closure'
439
+ ])
440
+ || !normalizedClosure.includes(normalizeMarkdownWhitespace(IMPLEMENTATION_READINESS_CLAUSES.freshReplay))
441
+ || !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof)
442
+ || !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.closureTransition)
443
+ || !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks)
444
+ || !closure.includes(IMPLEMENTATION_READINESS_CLAUSES.scopeReturnsToC1)) {
445
+ issues.add('repair-closure');
446
+ }
447
+
448
+ return [...issues].sort();
449
+ }
450
+
451
+ function adaptiveCoverageIssues(input) {
452
+ const expectedKeys = ['review', 'skill', 'specMaker', 'templates'];
453
+ const keys = input && typeof input === 'object' && !Array.isArray(input)
454
+ ? Object.keys(input).sort()
455
+ : [];
456
+ if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)
457
+ || expectedKeys.some((key) => typeof input[key] !== 'string')) {
458
+ throw new TypeError('adaptive-coverage checker expects skill, specMaker, templates, and review UTF-8 strings');
459
+ }
460
+
461
+ const issues = new Set();
462
+ const skill = normalizeMarkdownWhitespace(input.skill);
463
+ const templates = normalizeMarkdownWhitespace(input.templates);
464
+ const review = normalizeMarkdownWhitespace(input.review);
465
+ const specMaker = normalizeMarkdownWhitespace(input.specMaker);
466
+ const has = (source, clause) => source.includes(normalizeMarkdownWhitespace(clause));
467
+
468
+ const riskClauses = [
469
+ ADAPTIVE_COVERAGE_CLAUSES.riskFirst,
470
+ ADAPTIVE_COVERAGE_CLAUSES.criticalFloor,
471
+ ADAPTIVE_COVERAGE_CLAUSES.elevatedFloor,
472
+ ADAPTIVE_COVERAGE_CLAUSES.frontmatterGate,
473
+ ADAPTIVE_COVERAGE_CLAUSES.directGate,
474
+ ADAPTIVE_COVERAGE_CLAUSES.splitRoute,
475
+ ADAPTIVE_COVERAGE_CLAUSES.independentSubsystem
476
+ ];
477
+ const riskDowngrade = /\buser\b.{0,80}\b(?:may|can)\b.{0,80}\blower\b.{0,40}\brisk\b/i;
478
+ const riskyDirectOverride = /\b(?:exception|even if|regardless)\b.{0,160}\b(?:auth|secret|privacy|destructive|irreversible|data loss|corruption|production[- ]state|critical)\b.{0,160}\b(?:direct|work directly|go direct)\b/i;
479
+ if (riskClauses.some((clause) => !has(skill, clause))
480
+ || riskDowngrade.test(skill) || riskyDirectOverride.test(skill)) {
481
+ issues.add('risk-first-routing');
482
+ }
483
+
484
+ const profileTable = markdownTableUnderHeading(input.templates, 'Coverage profile');
485
+ const profileHeadingCount = (input.templates.match(/^## Coverage profile\s*$/gm) || []).length;
486
+ const taskReferencesCoverage = /## Coverage\s*\n- <exact `CP-NN` IDs owned by this task>/.test(input.templates);
487
+ if (JSON.stringify(profileTable[0] || []) !== JSON.stringify(ADAPTIVE_COVERAGE_PROFILE_HEADER)
488
+ || profileHeadingCount !== 1 || profileTable.length !== 2
489
+ || JSON.stringify(profileTable[1]) !== JSON.stringify(ADAPTIVE_COVERAGE_PROFILE_ROW)
490
+ || !taskReferencesCoverage
491
+ || !has(templates, ADAPTIVE_COVERAGE_CLAUSES.profileAuthority)
492
+ || !has(templates, ADAPTIVE_COVERAGE_CLAUSES.openKinds)) {
493
+ issues.add('coverage-profile-shape');
494
+ }
495
+
496
+ const ambiguityTable = markdownTableUnderHeading(input.templates, 'Example Mapping rule');
497
+ if (JSON.stringify(ambiguityTable) !== JSON.stringify(ADAPTIVE_COVERAGE_AMBIGUITY_ROWS)
498
+ || !templates.includes('retention of 30 versus 90 days is `decision-needed`')) {
499
+ issues.add('ambiguity-actions');
500
+ }
501
+
502
+ const globalCeremony = /\b(?:critical|security|failure|proof|review|edge|obligations?|lenses?)\b[^.!?\n]{0,120}\b(?:union|apply|spread|require)\w*\b[^.!?\n]{0,120}\b(?:all|every)\s+(?:cp\s+)?(?:rows?|outcomes?|tasks?)\b|\b(?:union|apply|spread)\w*\b[^.!?\n]{0,120}\b(?:all|every)\s+(?:cp\s+)?(?:rows?|outcomes?|tasks?)\b/i;
503
+ if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.scopedUnion)
504
+ || globalCeremony.test(templates)
505
+ || !review.includes('nonmaterial lenses are not added')) {
506
+ issues.add('scoped-coverage');
507
+ }
508
+
509
+ const rederiveSources = [templates, skill, specMaker, review];
510
+ if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.profileRederivation)
511
+ || rederiveSources.some((source) => !/rederive affected cp rows?/.test(source.toLowerCase()))) {
512
+ issues.add('profile-lifecycle');
513
+ }
514
+
515
+ const statusMatrix = markdownTableUnderHeading(input.templates, 'Status matrix');
516
+ if (!has(templates, ADAPTIVE_COVERAGE_CLAUSES.plannedProof)
517
+ || !has(templates, ADAPTIVE_COVERAGE_CLAUSES.proofSeparation)
518
+ || !has(templates, IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping)
519
+ || !has(skill, ADAPTIVE_COVERAGE_CLAUSES.liveLimit)
520
+ || !statusMatrix.some((row) => row[0] === 'accepted finding open or `UNKNOWN` reachability' && row[1] === '`blocked`')) {
521
+ issues.add('proof-lifecycle');
522
+ }
523
+
524
+ const reviewerRowsPresent = ADAPTIVE_REVIEWER_ROWS.every((row) =>
525
+ input.review.includes(`| ${row.join(' | ')} |`));
526
+ if (!has(review, ADAPTIVE_COVERAGE_CLAUSES.reviewRisk)
527
+ || !has(review, ADAPTIVE_COVERAGE_CLAUSES.reviewCapacity)
528
+ || !reviewerRowsPresent) {
529
+ issues.add('reviewer-routing');
530
+ }
531
+ if (!specMaker.includes('skills/specs/SKILL.md')
532
+ || !specMaker.includes('skills/specs/references/templates.md')
533
+ || !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerAuthority)
534
+ || !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerAmbiguity)
535
+ || !has(specMaker, ADAPTIVE_COVERAGE_CLAUSES.specMakerRoute)) {
536
+ issues.add('spec-maker-authority');
537
+ }
538
+
539
+ const boundaryTable = markdownTableUnderHeading(
540
+ input.templates,
541
+ 'No-invention and conditional boundary contracts'
542
+ );
543
+ if (!boundaryTableHasRequiredRows(boundaryTable)) {
544
+ issues.add('adaptive-boundary-lenses');
545
+ }
546
+ return [...issues].sort();
547
+ }
548
+
549
+ function authoringProjectionIssues(files) {
163
550
  const issues = [];
551
+ issues.push(...implementationReadinessIssues({ templates: files.templates, review: files.review }));
164
552
  const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
165
553
  if (!files.skill.includes('Task files are flat beside `plan.md`')
166
554
  || !files.skill.includes('Do not create a nested task directory.')) {
@@ -187,7 +575,7 @@ function installedAuthoringProjectionIssues(files) {
187
575
  ]) {
188
576
  if (!files.templates.includes(field)) issues.push(`receipt-${field.toLowerCase()}`);
189
577
  }
190
- if (!/^name: hapo-specs$/m.test(files.skill)) issues.push('codex-skill-name');
578
+ if (!/^name: cf-specs$/m.test(files.skill)) issues.push('codex-skill-name');
191
579
  const bundle = [files.skill, files.review, files.templates, files.legacyTemplates].join('\n');
192
580
  const runtimeProjection = `${bundle}\n${files.codex.replace(foreignRuntimeBoundary, '')}`;
193
581
  if (!files.codex.includes(foreignRuntimeBoundary)
@@ -196,11 +584,11 @@ function installedAuthoringProjectionIssues(files) {
196
584
  }
197
585
  for (const claudeOnly of [
198
586
  'AskUserQuestion', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList',
199
- 'WebSearch', 'WebFetch', 'SendMessage', 'Claude Code', '.claude', '/hapo:', 'hapo:'
587
+ 'WebSearch', 'WebFetch', 'SendMessage', 'Claude Code', '.claude', '/cf:', 'cf:'
200
588
  ]) {
201
589
  if (runtimeProjection.includes(claudeOnly)) issues.push(`claude-vocabulary-${claudeOnly}`);
202
590
  }
203
- for (const command of ['$hapo-specs', '$hapo-develop', '$hapo-sync']) {
591
+ for (const command of ['$cf-specs', '$cf-develop', '$cf-sync']) {
204
592
  if (!files.codex.includes(command)) issues.push(`codex-command-${command}`);
205
593
  }
206
594
  for (const state of ['pending', 'in_progress', 'paused', 'blocked', 'done']) {
@@ -211,11 +599,346 @@ function installedAuthoringProjectionIssues(files) {
211
599
  || !files.codex.includes('C1') || !files.codex.includes('C2') || !files.codex.includes('C3')) {
212
600
  issues.push('codex-process-v3');
213
601
  }
214
- return issues;
602
+ return [...new Set(issues)].sort();
603
+ }
604
+
605
+ function canonicalInstalledSpecsRoot(projectRoot, installedRoot) {
606
+ assert.equal(path.isAbsolute(projectRoot), true, 'project root must be absolute');
607
+ assert.equal(path.isAbsolute(installedRoot), true, 'installed Specs root must be absolute');
608
+ const canonicalProject = fs.realpathSync(projectRoot);
609
+ const canonicalInstalled = fs.realpathSync(installedRoot);
610
+ const relative = path.relative(canonicalProject, canonicalInstalled);
611
+ assert.ok(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'installed Specs root must stay inside project');
612
+ assert.equal(
613
+ canonicalInstalled,
614
+ fs.realpathSync(path.join(canonicalProject, '.agents/skills/specs')),
615
+ 'checker must read the native installed Codex Specs root'
616
+ );
617
+ return canonicalInstalled;
618
+ }
619
+
620
+ function readInstalledAuthoringProjection(projectRoot, installedRoot) {
621
+ const canonicalRoot = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
622
+ const read = (relative) => fs.readFileSync(path.join(canonicalRoot, relative), 'utf8');
623
+ const specMakerToml = fs.readFileSync(
624
+ path.join(projectRoot, '.codex/agents/spec_maker.toml'),
625
+ 'utf8'
626
+ );
627
+ return {
628
+ skill: read('SKILL.md'),
629
+ review: read('references/review.md'),
630
+ templates: read('references/templates.md'),
631
+ specMaker: parseGeneratedTomlString(specMakerToml, 'developer_instructions'),
632
+ legacyTemplates: V3_SPECS_BUNDLE.filter((relative) => relative.startsWith('templates/')).map(read).join('\n'),
633
+ codex: [
634
+ fs.readFileSync(path.join(projectRoot, 'AGENTS.md'), 'utf8'),
635
+ fs.readFileSync(path.join(projectRoot, '.codex/rules/state-sync.md'), 'utf8')
636
+ ].join('\n')
637
+ };
638
+ }
639
+
640
+ function installedAuthoringProjectionIssues(projectRoot, installedRoot) {
641
+ return authoringProjectionIssues(readInstalledAuthoringProjection(projectRoot, installedRoot));
642
+ }
643
+
644
+ function installedAdaptiveCoverageIssues(projectRoot, installedRoot) {
645
+ const { skill, specMaker, templates, review } = readInstalledAuthoringProjection(
646
+ projectRoot,
647
+ installedRoot
648
+ );
649
+ return adaptiveCoverageIssues({ skill, specMaker, templates, review });
650
+ }
651
+
652
+ function assertDisposableInstalledMutationTarget({
653
+ projectRoot,
654
+ installedScope,
655
+ targetPath,
656
+ expectedTargetPath,
657
+ canonicalSourcePaths,
658
+ scopeLabel
659
+ }) {
660
+ const canonicalProject = fs.realpathSync(projectRoot);
661
+ const canonicalScope = fs.realpathSync(installedScope);
662
+ const targetLstat = fs.lstatSync(targetPath);
663
+ assert.equal(targetLstat.isSymbolicLink(), false, 'installed mutation target must not be a symlink');
664
+ assert.equal(targetLstat.isFile(), true, 'installed mutation target must be a regular file');
665
+ const canonicalTarget = fs.realpathSync(targetPath);
666
+ assert.equal(
667
+ canonicalTarget,
668
+ fs.realpathSync(expectedTargetPath),
669
+ `installed mutation target must be the exact ${scopeLabel} projection`
670
+ );
671
+ const scopeRelative = path.relative(canonicalScope, canonicalTarget);
672
+ const projectRelative = path.relative(canonicalProject, canonicalTarget);
673
+ assert.ok(
674
+ scopeRelative && scopeRelative !== '..'
675
+ && !scopeRelative.startsWith(`..${path.sep}`)
676
+ && !path.isAbsolute(scopeRelative),
677
+ `installed mutation target must stay inside ${scopeLabel}`
678
+ );
679
+ assert.ok(
680
+ projectRelative && projectRelative !== '..'
681
+ && !projectRelative.startsWith(`..${path.sep}`)
682
+ && !path.isAbsolute(projectRelative),
683
+ 'installed mutation target must stay inside the disposable project'
684
+ );
685
+ assert.ok(
686
+ Array.isArray(canonicalSourcePaths) && canonicalSourcePaths.length > 0,
687
+ 'installed mutation guard requires the full canonical source set'
688
+ );
689
+ const targetStat = fs.statSync(canonicalTarget);
690
+ for (const sourcePath of canonicalSourcePaths) {
691
+ const canonicalSource = fs.realpathSync(sourcePath);
692
+ assert.notEqual(
693
+ canonicalTarget,
694
+ canonicalSource,
695
+ 'installed mutation target must not resolve to any canonical source'
696
+ );
697
+ const sourceStat = fs.statSync(canonicalSource);
698
+ assert.notDeepEqual(
699
+ [targetStat.dev, targetStat.ino],
700
+ [sourceStat.dev, sourceStat.ino],
701
+ `installed mutation target must not share an inode with canonical source set: ${sourcePath}`
702
+ );
703
+ }
704
+ assert.equal(targetStat.nlink, 1, 'installed mutation target must have exactly one hard link');
705
+ return canonicalTarget;
706
+ }
707
+
708
+ function assertInstalledMutationTarget(projectRoot, installedRoot, relative) {
709
+ const canonicalInstalled = canonicalInstalledSpecsRoot(projectRoot, installedRoot);
710
+ const targetPath = path.join(canonicalInstalled, relative);
711
+ return assertDisposableInstalledMutationTarget({
712
+ projectRoot,
713
+ installedScope: canonicalInstalled,
714
+ targetPath,
715
+ expectedTargetPath: path.join(projectRoot, '.agents/skills/specs', relative),
716
+ canonicalSourcePaths: canonicalProjectionSourcePaths(),
717
+ scopeLabel: '.agents/skills/specs'
718
+ });
719
+ }
720
+
721
+ function assertInstalledSpecMakerMutationTarget(projectRoot) {
722
+ const installedScope = path.join(projectRoot, '.codex/agents');
723
+ const targetPath = path.join(installedScope, 'spec_maker.toml');
724
+ return assertDisposableInstalledMutationTarget({
725
+ projectRoot,
726
+ installedScope,
727
+ targetPath,
728
+ expectedTargetPath: targetPath,
729
+ canonicalSourcePaths: canonicalProjectionSourcePaths(),
730
+ scopeLabel: '.codex/agents'
731
+ });
732
+ }
733
+
734
+ function canonicalProjectionSourcePaths() {
735
+ return [
736
+ ...V3_SPECS_BUNDLE.map((relative) => path.join(SPECS_SOURCE_ROOT, relative)),
737
+ SPEC_MAKER_SOURCE_PATH
738
+ ];
739
+ }
740
+
741
+ function canonicalProjectionSourceBytes() {
742
+ const sourcePaths = canonicalProjectionSourcePaths();
743
+ return new Map(sourcePaths.map((sourcePath) => [
744
+ sourcePath,
745
+ fs.readFileSync(sourcePath)
746
+ ]));
747
+ }
748
+
749
+ function sha256(content) {
750
+ return createHash('sha256').update(content).digest('hex');
751
+ }
752
+
753
+ function canonicalBrainstormSourceBytes() {
754
+ return new Map(BRAINSTORM_SOURCE_PATHS.map((sourcePath) => [
755
+ sourcePath,
756
+ fs.readFileSync(sourcePath)
757
+ ]));
758
+ }
759
+
760
+ function assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes) {
761
+ for (const [sourcePath, expected] of sourceBytes) {
762
+ const actual = fs.readFileSync(sourcePath);
763
+ assert.deepEqual(
764
+ actual,
765
+ expected,
766
+ `canonical Brainstorm source changed: ${path.relative(PACKAGE_ROOT, sourcePath)}`
767
+ );
768
+ assert.equal(sha256(actual), sha256(expected));
769
+ }
770
+ }
771
+
772
+ function readInstalledBrainstormProjection(projectRoot) {
773
+ const skillRoot = path.join(projectRoot, '.agents/skills/brainstorm');
774
+ const agentPath = path.join(projectRoot, '.codex/agents/brainstormer.toml');
775
+ return {
776
+ skill: fs.readFileSync(path.join(skillRoot, 'SKILL.md'), 'utf8'),
777
+ framework: fs.readFileSync(path.join(skillRoot, 'references/question-framework.md'), 'utf8'),
778
+ agent: parseGeneratedTomlString(fs.readFileSync(agentPath, 'utf8'), 'developer_instructions')
779
+ };
780
+ }
781
+
782
+ function brainstormProjectionClausePrefix(content, index) {
783
+ const starts = ['.', '!', '?', ';', '\n'].map((marker) => content.lastIndexOf(marker, index - 1));
784
+ return content.slice(Math.max(...starts) + 1, index).trim();
785
+ }
786
+
787
+ function brainstormProjectionIsLocallyNegated(content, index) {
788
+ const clausePrefix = brainstormProjectionClausePrefix(content, index);
789
+ const localBoundary = Math.max(
790
+ clausePrefix.lastIndexOf(','),
791
+ clausePrefix.lastIndexOf(':'),
792
+ clausePrefix.lastIndexOf('—')
793
+ );
794
+ const localPrefix = clausePrefix.slice(localBoundary + 1).trim().toLowerCase();
795
+ const directNegation = /\b(?:never|do not|does not|must not|may not|should not|will not|cannot|can't|is forbidden to|are forbidden to|is not (?:permitted|allowed) to|are not (?:permitted|allowed) to|not (?:permitted|allowed) to)(?:\s+(?:ever|directly|automatically|implicitly|explicitly|immediately|intentionally|silently))*\s*$/;
796
+ if (directNegation.test(localPrefix)) return true;
797
+ const fullPrefix = clausePrefix.toLowerCase();
798
+ if (/\b(?:but|yet|however|except|instead)\b[^.!?;\n]*$/.test(fullPrefix)) return false;
799
+ return /\b(?:never|do not|does not|must not|may not|should not|will not|cannot|can't|is forbidden to|are forbidden to|is not (?:permitted|allowed) to|are not (?:permitted|allowed) to|not (?:permitted|allowed) to)\b(?:(?!\b(?:but|yet|however|except|instead)\b).){0,160}\b(?:and|or|nor)(?:\s+then)?(?:\s+(?:ever|directly|automatically|implicitly|explicitly|immediately|intentionally|silently))*\s*$/.test(fullPrefix);
800
+ }
801
+
802
+ function brainstormProjectionHasUnauthorizedAuthority(content) {
803
+ const actionPattern = /\b(?:ask|question|contact|invoke|start|dispatch|launch|route|forward|run|execute|hand(?:\s+|-)?off|write|edit|update|mutate|delegate|claim)\b/gi;
804
+ const actions = [...content.matchAll(actionPattern)];
805
+ for (let index = 0; index < actions.length; index += 1) {
806
+ const match = actions[index];
807
+ const normalizedAction = match[0].toLowerCase().replace(/[-\s]+/g, ' ');
808
+ const action = normalizedAction === 'handoff' ? 'hand off' : normalizedAction;
809
+ const start = match.index + match[0].length;
810
+ const nextAction = actions[index + 1]?.index ?? content.length;
811
+ const punctuation = content.slice(start).search(/[.!?;\n]/);
812
+ const punctuationEnd = punctuation < 0 ? content.length : start + punctuation;
813
+ const tail = content.slice(start, Math.min(start + 100, nextAction, punctuationEnd));
814
+ const hasTarget = ['ask', 'question', 'contact'].includes(action)
815
+ ? /\b(?:the )?user\b/i.test(tail)
816
+ : ['invoke', 'start', 'dispatch', 'launch', 'route', 'forward', 'run', 'execute', 'hand off'].includes(action)
817
+ ? /\b(?:Specs|Fix|Develop)\b/i.test(tail)
818
+ : ['write', 'edit', 'update', 'mutate'].includes(action)
819
+ ? /\b(?:files?|task state|shared state)\b/i.test(tail)
820
+ : action === 'delegate'
821
+ ? /\b(?:work|tasks?)\b/i.test(tail)
822
+ : /\bapproval\b/i.test(tail);
823
+ if (hasTarget && !brainstormProjectionIsLocallyNegated(content, match.index)) return true;
824
+ }
825
+ return false;
826
+ }
827
+
828
+ function brainstormProjectionIssues(files) {
829
+ const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
830
+ const skill = compact(files.skill);
831
+ const framework = compact(files.framework);
832
+ const agent = compact(files.agent);
833
+ const issues = new Set();
834
+ const specialistBoundary = 'Do not ask the user directly, write files, mutate shared task state, delegate work, invoke Specs/Fix/Develop, or claim approval.';
835
+ const specialistHandoff = 'Non-bug exploration may end in chat; feature/docs delivery may only prepare a future explicit Specs invocation; bug handoff requires evidenced root cause and the user\'s explicit fix request.';
836
+ const agentAuthorityRemainder = agent
837
+ .replace(specialistBoundary, '')
838
+ .replace(specialistHandoff, '');
839
+ const adaptiveClauses = {
840
+ 'adaptive-direct-precedence': 'Route Direct first, then apply controls only to requests that remain in Brainstorm.',
841
+ 'adaptive-ordered-depth': 'With no Deep signal, use Standard. `--deep` raises Standard to Deep.',
842
+ 'adaptive-leading-flags': 'Parse controls only from the leading consecutive token segment.',
843
+ 'adaptive-lens-trigger-skip': 'failure isolation for partial or cascading failure across boundaries',
844
+ 'adaptive-evidence-semantics': 'Missing evidence forces feasibility `unknown` and confidence `low`.',
845
+ 'adaptive-numeric-estimates': 'A numeric estimate requires range, unit, basis, evidence, and assumptions; otherwise report `unknown`.',
846
+ 'adaptive-pre-tool-redaction': 'Before an external visual tool or adviser handoff, minimize context and redact secrets, credentials, private keys, access tokens, and unnecessary PII.',
847
+ 'adaptive-adviser-gate': '`--advice` invokes `brainstormer` only after the material-choice gate;',
848
+ 'adaptive-decision-freshness': 'The first section records target identity, current source revision and worktree state or `[UNVERIFIED]`, an evidence-as-of value, and what change invalidates the brief.',
849
+ 'adaptive-non-authority': 'Neither overlay writes, approves, persists, dispatches, or completes work.'
850
+ };
851
+
852
+ if (!skill.includes('leave Brainstorm before scout, questions, approval, or persistence.')
853
+ || !skill.includes('Hydration is not a terminal route; continue to exactly one intent route below.')) {
854
+ issues.add('front-door-routing');
855
+ }
856
+ if (!skill.includes('Then use `cf-debug` until root cause is evidenced.')
857
+ || !skill.includes('Hand off to `cf-fix` only when the user explicitly requested a fix')) {
858
+ issues.add('bug-routing');
859
+ }
860
+ if (!skill.includes('Do not request design approval, persist a report, or invoke another workflow without a new explicit request.')) {
861
+ issues.add('exploration-stop');
862
+ }
863
+ if (!skill.includes('For a material choice, compare 2–3 mechanically distinct viable approaches')
864
+ || !skill.includes('Never create strawman options.')
865
+ || !agent.includes('never invent strawmen to fill a quota.')) {
866
+ issues.add('option-cardinality');
867
+ }
868
+ if (!agent.includes('You advise `cf-brainstorm`; you do not replace its routing, question, approval, persistence, or handoff ownership.')
869
+ || !agent.includes(specialistBoundary)
870
+ || brainstormProjectionHasUnauthorizedAuthority(agentAuthorityRemainder)) {
871
+ issues.add('specialist-boundary');
872
+ }
873
+ if (!agent.includes('If the request is a symptom without an evidenced root cause, return it to `cf-debug`.')
874
+ || !agent.includes('If the controller has not identified whether the work is feature delivery, an explicitly authorized fix, or non-bug exploration, request that routing context instead of guessing.')) {
875
+ issues.add('specialist-routing');
876
+ }
877
+ if (!agent.includes(specialistHandoff)) {
878
+ issues.add('specialist-handoff');
879
+ }
880
+ if (!skill.includes('Persist only approved decisions and semantics, only with user authority')
881
+ || !skill.includes('redact live secrets, credentials, private keys, access tokens, and unnecessary PII;')
882
+ || !framework.includes('Do not write "user selected" unless direct user text or the native input tool confirms it.')) {
883
+ issues.add('approval-persistence');
884
+ }
885
+ for (const [issue, clause] of Object.entries(adaptiveClauses)) {
886
+ if (!skill.includes(clause)) issues.add(issue);
887
+ }
888
+ const visible = `${skill}\n${framework}\n${agent}`;
889
+ if (/\bAskUserQuestion\b/.test(visible)
890
+ || /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a structured user-input request\b/i.test(visible)
891
+ || /\ban structured user-input request\b/i.test(visible)
892
+ || /\ba structured user-input request\s+(?:calls|batches)\b/i.test(visible)) {
893
+ issues.add('projection-grammar');
894
+ }
895
+ return [...issues].sort();
896
+ }
897
+
898
+ function assertInstalledBrainstormMutationTarget(projectRoot, source) {
899
+ const skillRoot = path.join(projectRoot, '.agents/skills/brainstorm');
900
+ if (source === 'agent') {
901
+ const agentRoot = path.join(projectRoot, '.codex/agents');
902
+ const targetPath = path.join(agentRoot, 'brainstormer.toml');
903
+ return assertDisposableInstalledMutationTarget({
904
+ projectRoot,
905
+ installedScope: agentRoot,
906
+ targetPath,
907
+ expectedTargetPath: targetPath,
908
+ canonicalSourcePaths: BRAINSTORM_SOURCE_PATHS,
909
+ scopeLabel: '.codex/agents'
910
+ });
911
+ }
912
+ const relative = source === 'skill' ? 'SKILL.md' : 'references/question-framework.md';
913
+ const targetPath = path.join(skillRoot, relative);
914
+ return assertDisposableInstalledMutationTarget({
915
+ projectRoot,
916
+ installedScope: skillRoot,
917
+ targetPath,
918
+ expectedTargetPath: targetPath,
919
+ canonicalSourcePaths: BRAINSTORM_SOURCE_PATHS,
920
+ scopeLabel: '.agents/skills/brainstorm'
921
+ });
922
+ }
923
+
924
+ function assertCanonicalSpecsSourceBytesUnchanged(sourceBytes) {
925
+ for (const [sourcePath, expected] of sourceBytes) {
926
+ const actual = fs.readFileSync(sourcePath);
927
+ assert.deepEqual(
928
+ actual,
929
+ expected,
930
+ `canonical source bytes changed during installed mutation: ${path.relative(PACKAGE_ROOT, sourcePath)}`
931
+ );
932
+ assert.equal(
933
+ sha256(actual),
934
+ sha256(expected),
935
+ `canonical source SHA changed during installed mutation: ${path.relative(PACKAGE_ROOT, sourcePath)}`
936
+ );
937
+ }
215
938
  }
216
939
 
217
- function assertInstalledAuthoringProjection(root) {
218
- const installedRoot = path.join(root, '.agents/skills/specs');
940
+ function assertInstalledAuthoringProjection(root, installedRoot) {
941
+ canonicalInstalledSpecsRoot(root, installedRoot);
219
942
  const relativeFiles = (directory) => allFiles(directory, () => true)
220
943
  .map((file) => path.relative(directory, file).split(path.sep).join('/'))
221
944
  .sort();
@@ -228,25 +951,460 @@ function assertInstalledAuthoringProjection(root) {
228
951
  const actual = fs.readFileSync(path.join(installedRoot, relative), 'utf8');
229
952
  assert.equal(actual, expected, `Codex Specs projection drifted: ${relative}`);
230
953
  }
954
+ const sourceSpecMaker = fs.readFileSync(SPEC_MAKER_SOURCE_PATH, 'utf8');
955
+ const installedSpecMakerPath = path.join(root, '.codex/agents/spec_maker.toml');
956
+ assert.equal(
957
+ fs.readFileSync(installedSpecMakerPath, 'utf8'),
958
+ convertCodexAgentContent(sourceSpecMaker, path.basename(SPEC_MAKER_SOURCE_PATH)),
959
+ 'Codex spec_maker projection drifted'
960
+ );
231
961
  for (const relative of OBSOLETE_SPECS_FILES) {
232
962
  assert.equal(fs.existsSync(path.join(installedRoot, relative)), false, `obsolete Specs file installed: ${relative}`);
233
963
  }
234
964
 
235
- const read = (relative) => fs.readFileSync(path.join(installedRoot, relative), 'utf8');
236
- const files = {
237
- skill: read('SKILL.md'),
238
- review: read('references/review.md'),
239
- templates: read('references/templates.md'),
240
- legacyTemplates: V3_SPECS_BUNDLE
241
- .filter((relative) => relative.startsWith('templates/'))
242
- .map(read)
243
- .join('\n'),
244
- codex: [
245
- fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'),
246
- fs.readFileSync(path.join(root, '.codex/rules/state-sync.md'), 'utf8')
247
- ].join('\n')
965
+ const files = readInstalledAuthoringProjection(root, installedRoot);
966
+ const sourceBytes = canonicalProjectionSourceBytes();
967
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), []);
968
+ assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), []);
969
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
970
+
971
+ if (process.platform !== 'win32') {
972
+ for (const [installedPath, sourcePath, assertTarget] of [
973
+ [
974
+ path.join(installedRoot, 'references/templates.md'),
975
+ path.join(SPECS_SOURCE_ROOT, 'references/templates.md'),
976
+ () => assertInstalledMutationTarget(root, installedRoot, 'references/templates.md')
977
+ ],
978
+ [
979
+ installedSpecMakerPath,
980
+ SPEC_MAKER_SOURCE_PATH,
981
+ () => assertInstalledSpecMakerMutationTarget(root)
982
+ ]
983
+ ]) {
984
+ const backupPath = `${installedPath}.cafekit-backup`;
985
+ fs.renameSync(installedPath, backupPath);
986
+ try {
987
+ fs.linkSync(sourcePath, installedPath);
988
+ assert.throws(assertTarget, /must not share an inode with canonical source/);
989
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
990
+ } finally {
991
+ try { fs.unlinkSync(installedPath); } catch { /* best-effort disposable cleanup */ }
992
+ fs.renameSync(backupPath, installedPath);
993
+ }
994
+ }
995
+
996
+ const crossSourceRoot = path.join(root, 'cross-source-native-control');
997
+ const crossSourceProject = path.join(crossSourceRoot, 'project');
998
+ const crossSourceScope = path.join(crossSourceProject, '.agents/skills/specs');
999
+ const crossSourceTarget = path.join(crossSourceScope, 'target-a.md');
1000
+ const crossSourcePaths = [
1001
+ path.join(crossSourceRoot, 'sources/source-a.md'),
1002
+ path.join(crossSourceRoot, 'sources/source-b.md')
1003
+ ];
1004
+ fs.mkdirSync(crossSourceScope, { recursive: true });
1005
+ fs.mkdirSync(path.dirname(crossSourcePaths[0]), { recursive: true });
1006
+ fs.writeFileSync(crossSourceTarget, 'native installed target A\n');
1007
+ fs.writeFileSync(crossSourcePaths[0], 'native canonical source A\n');
1008
+ fs.writeFileSync(crossSourcePaths[1], 'native canonical source B\n');
1009
+ const targetBytes = fs.readFileSync(crossSourceTarget);
1010
+ const sourceState = new Map(crossSourcePaths.map((sourcePath) => {
1011
+ const bytes = fs.readFileSync(sourcePath);
1012
+ return [sourcePath, { bytes, sha: sha256(bytes) }];
1013
+ }));
1014
+ assert.notEqual(crossSourcePaths[0], crossSourcePaths[1], 'cross-source control requires A != B');
1015
+ fs.unlinkSync(crossSourceTarget);
1016
+ fs.linkSync(crossSourcePaths[1], crossSourceTarget);
1017
+ try {
1018
+ assert.throws(
1019
+ () => fs.writeFileSync(
1020
+ assertDisposableInstalledMutationTarget({
1021
+ projectRoot: crossSourceProject,
1022
+ installedScope: crossSourceScope,
1023
+ targetPath: crossSourceTarget,
1024
+ expectedTargetPath: crossSourceTarget,
1025
+ canonicalSourcePaths: crossSourcePaths,
1026
+ scopeLabel: '.agents/skills/specs'
1027
+ }),
1028
+ 'forbidden native cross-source mutation\n'
1029
+ ),
1030
+ /must not share an inode with canonical source set/
1031
+ );
1032
+ } finally {
1033
+ fs.unlinkSync(crossSourceTarget);
1034
+ fs.writeFileSync(crossSourceTarget, targetBytes);
1035
+ fs.writeFileSync(crossSourcePaths[1], sourceState.get(crossSourcePaths[1]).bytes);
1036
+ }
1037
+ assert.deepEqual(fs.readFileSync(crossSourceTarget), targetBytes, 'cross-source control must restore exact target bytes');
1038
+ for (const [sourcePath, expected] of sourceState) {
1039
+ const actual = fs.readFileSync(sourcePath);
1040
+ assert.deepEqual(actual, expected.bytes, 'cross-source rejection must preserve every source byte');
1041
+ assert.equal(sha256(actual), expected.sha, 'cross-source rejection must preserve every source SHA');
1042
+ }
1043
+ }
1044
+
1045
+ const boundaryMutation = (name, boundary, replacements) => {
1046
+ const row = IMPLEMENTATION_READINESS_BOUNDARY_ROWS.find(([label]) => label === boundary);
1047
+ assert.ok(row, `${name} references unknown boundary ${boundary}`);
1048
+ let weakened = row[1];
1049
+ for (const [from, to] of replacements) {
1050
+ const next = weakened.replace(from, to);
1051
+ assert.notEqual(next, weakened, `${name} weakening anchor must exist in ${boundary}`);
1052
+ weakened = next;
1053
+ }
1054
+ return {
1055
+ name,
1056
+ issue: 'boundary-contract',
1057
+ relative: 'references/templates.md',
1058
+ from: `| ${row[0]} | ${row[1]} |`,
1059
+ to: `| ${row[0]} | ${weakened} |`
1060
+ };
248
1061
  };
249
- assert.deepEqual(installedAuthoringProjectionIssues(files), []);
1062
+
1063
+ const readinessMutations = [
1064
+ {
1065
+ name: 'no-invention-blocking',
1066
+ issue: 'no-invention',
1067
+ relative: 'references/templates.md',
1068
+ from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
1069
+ to: 'Before implementation handoff, note ambiguous choices without blocking handoff.'
1070
+ },
1071
+ {
1072
+ name: 'no-invention-contradictory-override',
1073
+ issue: 'no-invention',
1074
+ relative: 'references/templates.md',
1075
+ from: IMPLEMENTATION_READINESS_CLAUSES.noInvention,
1076
+ to: `${IMPLEMENTATION_READINESS_CLAUSES.noInvention}\n\nException: implementation handoff may proceed with an unresolved material choice.`
1077
+ },
1078
+ {
1079
+ name: 'material-boundary-definition',
1080
+ issue: 'boundary-contract',
1081
+ relative: 'references/templates.md',
1082
+ from: IMPLEMENTATION_READINESS_CLAUSES.materialDefinition,
1083
+ to: 'A boundary is material when it seems relevant to the task.'
1084
+ },
1085
+ {
1086
+ name: 'exact-boundary-choices',
1087
+ issue: 'boundary-contract',
1088
+ relative: 'references/templates.md',
1089
+ from: IMPLEMENTATION_READINESS_CLAUSES.exactBoundaryChoices,
1090
+ to: 'For every required row, describe the listed choices generally.'
1091
+ },
1092
+ boundaryMutation('interaction-accessibility', 'Interaction/UI', [
1093
+ ['input/focus/keyboard; ', ''],
1094
+ ['accessibility; ', '']
1095
+ ]),
1096
+ boundaryMutation('api-success-and-error-semantics', 'API/CLI', [
1097
+ ['success output; ', ''],
1098
+ ['error/status/exit; ', '']
1099
+ ]),
1100
+ boundaryMutation('schema-shape-and-unknown-fields', 'Data/schema', [
1101
+ ['exact keys/nesting/types; ', ''],
1102
+ ['unknown-field behavior; ', '']
1103
+ ]),
1104
+ boundaryMutation('schema-enum-and-format', 'Data/schema', [
1105
+ ['enum/format/', '']
1106
+ ]),
1107
+ boundaryMutation('state-lock-lifecycle', 'Async/state', [
1108
+ ['writer/lock acquire/contention/release; ', 'writer/lock; ']
1109
+ ]),
1110
+ boundaryMutation('filesystem-segment-grammar', 'Filesystem/security', [
1111
+ ['trusted/untrusted segment grammar; ', '']
1112
+ ]),
1113
+ boundaryMutation('filesystem-stale-lock-reclaim', 'Filesystem/security', [
1114
+ ['lock/stale reclaim; ', '']
1115
+ ]),
1116
+ boundaryMutation('runtime-rollout-and-recovery', 'Runtime/deploy', [
1117
+ ['rollout/rollback; ', ''],
1118
+ ['operator recovery', '']
1119
+ ]),
1120
+ boundaryMutation('retention-clock-and-endpoints', 'Time/retention', [
1121
+ ['clock source; ', ''],
1122
+ ['unit/precision/timezone; ', ''],
1123
+ ['endpoints and inclusion/comparator; ', '']
1124
+ ]),
1125
+ boundaryMutation('ai-model-safety-and-eval', 'AI/model', [
1126
+ ['safety/privacy; ', ''],
1127
+ ['eval oracle', '']
1128
+ ]),
1129
+ boundaryMutation('proof-level-partition', 'Integration/proof', [
1130
+ ['proof level (`source`/`installed`/`live`)', 'proof level']
1131
+ ]),
1132
+ {
1133
+ name: 'concrete-named-probe',
1134
+ issue: 'proof-chain',
1135
+ relative: 'references/templates.md',
1136
+ from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[1],
1137
+ to: '- Named probe: <suite label>'
1138
+ },
1139
+ {
1140
+ name: 'aggregate-suite-probe-owner',
1141
+ issue: 'proof-chain',
1142
+ relative: 'references/templates.md',
1143
+ from: IMPLEMENTATION_READINESS_CLAUSES.namedProbeOwnership,
1144
+ to: 'Aggregate suites may cite only the suite label.'
1145
+ },
1146
+ {
1147
+ name: 'reachability-levels',
1148
+ issue: 'proof-chain',
1149
+ relative: 'references/templates.md',
1150
+ from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[2],
1151
+ to: '- Reachability: <entrypoint or consumer>'
1152
+ },
1153
+ {
1154
+ name: 'proof-level-separation',
1155
+ issue: 'proof-chain',
1156
+ relative: 'references/templates.md',
1157
+ from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelSeparation,
1158
+ to: 'Proof may be promoted between source, installed, and live levels.'
1159
+ },
1160
+ {
1161
+ name: 'disposable-template-negative-controls',
1162
+ issue: 'proof-chain',
1163
+ relative: 'references/templates.md',
1164
+ from: IMPLEMENTATION_READINESS_CLAUSES.disposableTemplateControls,
1165
+ to: 'Run mutation or destructive negative controls against the available project copy.'
1166
+ },
1167
+ {
1168
+ name: 'disposable-review-negative-controls',
1169
+ issue: 'proof-chain',
1170
+ relative: 'references/review.md',
1171
+ from: IMPLEMENTATION_READINESS_CLAUSES.disposableReviewControls,
1172
+ to: 'Run mutation or destructive negative controls against the available project copy.'
1173
+ },
1174
+ {
1175
+ name: 'required-proof-level-mapping',
1176
+ issue: 'proof-chain',
1177
+ relative: 'references/templates.md',
1178
+ from: IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
1179
+ to: 'Map one required proof level to one probe; other levels may remain implicit.'
1180
+ },
1181
+ {
1182
+ name: 'artifact-path-and-digest-or-ephemeral',
1183
+ issue: 'proof-chain',
1184
+ relative: 'references/templates.md',
1185
+ from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[5],
1186
+ to: '- Artifacts: <required artifact path, or none>'
1187
+ },
1188
+ {
1189
+ name: 'proof-counterexample',
1190
+ issue: 'proof-chain',
1191
+ relative: 'references/templates.md',
1192
+ from: IMPLEMENTATION_READINESS_CLAUSES.proofPlanLines[4],
1193
+ to: '- Counterexample: <example>'
1194
+ },
1195
+ {
1196
+ name: 'repair-and-proof-columns',
1197
+ issue: 'repair-closure',
1198
+ relative: 'references/review.md',
1199
+ from: '| ID | Decision | Original counterexample | Repaired at | Proved at | Replay | Closure |',
1200
+ to: '| ID | Decision | Original counterexample | Repaired at | Evidence | Replay | Closure |'
1201
+ },
1202
+ {
1203
+ name: 'fresh-original-counterexample-replay',
1204
+ issue: 'repair-closure',
1205
+ relative: 'references/review.md',
1206
+ from: 'After applying an accepted C2 finding, a fresh-context closure pass records and\nfreshly replays its original counterexample after the repair under this exact review-log header:',
1207
+ to: 'After applying an accepted C2 finding, record the repair under this review-log header:'
1208
+ },
1209
+ {
1210
+ name: 'distinct-repair-and-proof-evidence',
1211
+ issue: 'repair-closure',
1212
+ relative: 'references/review.md',
1213
+ from: IMPLEMENTATION_READINESS_CLAUSES.distinctRepairProof,
1214
+ to: '`Repaired at` and `Proved at` may cite the same repair edit.'
1215
+ },
1216
+ {
1217
+ name: 'unknown-blocks-handoff',
1218
+ issue: 'repair-closure',
1219
+ relative: 'references/review.md',
1220
+ from: IMPLEMENTATION_READINESS_CLAUSES.unknownBlocks,
1221
+ to: '`PASS` closes it; `FAIL` and `UNKNOWN` may continue to implementation handoff.'
1222
+ },
1223
+ {
1224
+ name: 'crash-versus-catchable-failure',
1225
+ issue: 'failure-semantics',
1226
+ relative: 'references/templates.md',
1227
+ from: IMPLEMENTATION_READINESS_CLAUSES.failureSemantics,
1228
+ to: 'Crash and catchable failure both mean an error occurred.'
1229
+ },
1230
+ {
1231
+ name: 'privacy-identifier-surface',
1232
+ issue: 'privacy-identifiers',
1233
+ relative: 'references/review.md',
1234
+ from: IMPLEMENTATION_READINESS_CLAUSES.privacyIdentifiers,
1235
+ to: 'Any privacy/security claim names the sensitive data at risk.'
1236
+ }
1237
+ ];
1238
+ assert.equal(readinessMutations.length, 30, 'installed readiness mutation set must match canonical source');
1239
+
1240
+ for (const { name, issue, relative, from, to } of readinessMutations) {
1241
+ const target = assertInstalledMutationTarget(root, installedRoot, relative);
1242
+ const installedBytes = fs.readFileSync(target, 'utf8');
1243
+ const anchorIndex = installedBytes.indexOf(from);
1244
+ assert.ok(anchorIndex >= 0, `${name} mutation anchor must exist in installed bytes`);
1245
+ assert.equal(
1246
+ installedBytes.indexOf(from, anchorIndex + from.length),
1247
+ -1,
1248
+ `${name} mutation anchor must be unique in installed bytes`
1249
+ );
1250
+ const weakened = `${installedBytes.slice(0, anchorIndex)}${to}${installedBytes.slice(anchorIndex + from.length)}`;
1251
+ try {
1252
+ fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), weakened);
1253
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1254
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [issue], name);
1255
+ } finally {
1256
+ fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, relative), installedBytes);
1257
+ }
1258
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [], `${name} restore`);
1259
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1260
+ }
1261
+
1262
+ const integrationRow = IMPLEMENTATION_READINESS_BOUNDARY_ROWS
1263
+ .find(([label]) => label === 'Integration/proof');
1264
+ const integrationLine = `| ${integrationRow.join(' | ')} |`;
1265
+ const templatesTarget = assertInstalledMutationTarget(
1266
+ root,
1267
+ installedRoot,
1268
+ 'references/templates.md'
1269
+ );
1270
+ const templatesBytes = fs.readFileSync(templatesTarget, 'utf8');
1271
+ const extendedTemplates = templatesBytes.replace(
1272
+ integrationLine,
1273
+ `${integrationLine}\n| other:domain-specific | task-specific material choices and observable oracle |`
1274
+ );
1275
+ assert.notEqual(extendedTemplates, templatesBytes, 'open-boundary positive control must change installed templates');
1276
+ try {
1277
+ fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, 'references/templates.md'), extendedTemplates);
1278
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), []);
1279
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1280
+ } finally {
1281
+ fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, 'references/templates.md'), templatesBytes);
1282
+ }
1283
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedRoot), [], 'open-boundary restore');
1284
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1285
+
1286
+ const adaptiveMutations = [
1287
+ ['frontmatter-risk-bypass', 'skill', ADAPTIVE_COVERAGE_CLAUSES.frontmatterGate,
1288
+ 'skip for any clear one-file or two-file change.', ['risk-first-routing']],
1289
+ ['destructive-routine-direct', 'skill', ADAPTIVE_COVERAGE_CLAUSES.directGate,
1290
+ 'Work directly when the change is routine and likely limited to one or two files.', ['risk-first-routing']],
1291
+ ['destructive-direct-exception', 'skill', ADAPTIVE_COVERAGE_CLAUSES.directGate,
1292
+ `${ADAPTIVE_COVERAGE_CLAUSES.directGate} Exception: destructive one-file work labeled routine may go direct.`, ['risk-first-routing']],
1293
+ ['user-risk-downgrade', 'skill', ADAPTIVE_COVERAGE_CLAUSES.riskFirst,
1294
+ `${ADAPTIVE_COVERAGE_CLAUSES.riskFirst} A user may lower critical risk to routine.`, ['risk-first-routing']],
1295
+ ['four-subsystem-split', 'skill', ADAPTIVE_COVERAGE_CLAUSES.splitRoute,
1296
+ 'Split four or more independent subsystems; otherwise use one Specs packet for substantial work.', ['risk-first-routing']],
1297
+ ['forced-single-kind', 'templates', ADAPTIVE_COVERAGE_CLAUSES.openKinds,
1298
+ 'Choose one primary change kind and ignore unfamiliar kinds or surfaces.', ['coverage-profile-shape']],
1299
+ ['missing-profile-column', 'templates', '| ID | Outcome | Change kinds | Material surfaces | Ambiguity/action | Risk/evidence | Required proof |',
1300
+ '| ID | Outcome | Change kinds | Material surfaces | Risk/evidence | Required proof |', ['coverage-profile-shape']],
1301
+ ['duplicate-profile-heading', 'templates', '## Coverage profile\n',
1302
+ '## Coverage profile\n\n## Coverage profile\n', ['coverage-profile-shape']],
1303
+ ['truncated-profile-row', 'templates', `| ${ADAPTIVE_COVERAGE_PROFILE_ROW.join(' | ')} |`,
1304
+ '| CP-01 | <externally observable outcome> |', ['coverage-profile-shape']],
1305
+ ['examples-promote-to-design', 'templates', `| ${ADAPTIVE_COVERAGE_AMBIGUITY_ROWS[2].join(' | ')} |`,
1306
+ '| `examples-needed` | add examples and promote to `design-needed` if behavior changes |', ['ambiguity-actions']],
1307
+ ['examples-select-retention', 'templates', 'retention of 30 versus 90 days is\n`decision-needed`',
1308
+ 'retention of 30 versus 90 days may remain\n`examples-needed`', ['ambiguity-actions']],
1309
+ ['global-critical-ceremony', 'templates', ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
1310
+ 'Each task copies its CP values; authoring, review, edge, and proof obligations union across every task.', ['scoped-coverage']],
1311
+ ['scoped-union-contradiction', 'templates', ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
1312
+ `${ADAPTIVE_COVERAGE_CLAUSES.scopedUnion} Exception: critical proof obligations apply across every CP row.`, ['scoped-coverage']],
1313
+ ['stale-profile-after-c2', 'templates', ADAPTIVE_COVERAGE_CLAUSES.profileRederivation,
1314
+ 'Keep existing coverage rows after accepted plan changes.', ['profile-lifecycle']],
1315
+ ['planned-proof-blocks-start', 'templates', ADAPTIVE_COVERAGE_CLAUSES.plannedProof,
1316
+ '`Required proof` is execution evidence: known but unrun proof blocks `pending`; `UNKNOWN` may proceed; missing evidence may still reach `done`/C3.', ['proof-lifecycle']],
1317
+ ['source-promotes-live', 'templates', ADAPTIVE_COVERAGE_CLAUSES.proofSeparation,
1318
+ 'Source proof may promote installed and live proof.', ['proof-lifecycle']],
1319
+ ['unmapped-proof-level', 'templates', IMPLEMENTATION_READINESS_CLAUSES.proofLevelMapping,
1320
+ 'A task may map only one required proof level to its probe.', ['proof-lifecycle']],
1321
+ ['critical-reviewer-omitted', 'review', ADAPTIVE_COVERAGE_CLAUSES.reviewRisk,
1322
+ 'Keep Fact Checker as the baseline and choose any remaining reviewer; critical rows need no matching risk role.', ['reviewer-routing', 'scoped-coverage']],
1323
+ ['reviewer-lens-overflow', 'review', ADAPTIVE_COVERAGE_CLAUSES.reviewCapacity,
1324
+ 'Each reviewer owns exactly one lens; skip excess material lenses when the fixed reviewer count is full.', ['reviewer-routing']],
1325
+ ['spec-maker-local-taxonomy', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerAuthority,
1326
+ 'this agent owns a separate risk and coverage taxonomy.', ['spec-maker-authority']],
1327
+ ['spec-maker-examples-decide', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerAmbiguity,
1328
+ 'Use examples to settle every ambiguous observable behavior.', ['spec-maker-authority']],
1329
+ ['spec-maker-skips-brainstorm', 'specMaker', ADAPTIVE_COVERAGE_CLAUSES.specMakerRoute,
1330
+ 'Apply the risk-first route before C1 and stop only for direct work.', ['spec-maker-authority']],
1331
+ ['static-proves-live', 'skill', ADAPTIVE_COVERAGE_CLAUSES.liveLimit,
1332
+ 'Source/static checks prove live-model adherence.', ['proof-lifecycle']]
1333
+ ];
1334
+ assert.equal(adaptiveMutations.length, 23, 'installed adaptive mutation set must match canonical source');
1335
+
1336
+ const adaptiveTarget = (source) => {
1337
+ if (source === 'specMaker') {
1338
+ const target = assertInstalledSpecMakerMutationTarget(root);
1339
+ const bytes = fs.readFileSync(target);
1340
+ const content = bytes.toString('utf8');
1341
+ return {
1342
+ target,
1343
+ bytes,
1344
+ content: parseGeneratedTomlString(content, 'developer_instructions'),
1345
+ render: (value) => replaceGeneratedTomlString(content, 'developer_instructions', value)
1346
+ };
1347
+ }
1348
+ const relative = {
1349
+ skill: 'SKILL.md',
1350
+ templates: 'references/templates.md',
1351
+ review: 'references/review.md'
1352
+ }[source];
1353
+ assert.ok(relative, `unknown adaptive mutation source: ${source}`);
1354
+ const target = assertInstalledMutationTarget(root, installedRoot, relative);
1355
+ const bytes = fs.readFileSync(target);
1356
+ return { target, bytes, content: bytes.toString('utf8'), render: (value) => value };
1357
+ };
1358
+
1359
+ for (const [name, source, from, to, expected] of adaptiveMutations) {
1360
+ const targetState = adaptiveTarget(source);
1361
+ const anchorIndex = targetState.content.indexOf(from);
1362
+ assert.ok(anchorIndex >= 0, `${name} mutation anchor must exist in installed bytes`);
1363
+ assert.equal(
1364
+ targetState.content.indexOf(from, anchorIndex + from.length),
1365
+ -1,
1366
+ `${name} mutation anchor must be unique in installed bytes`
1367
+ );
1368
+ const weakened = `${targetState.content.slice(0, anchorIndex)}${to}${targetState.content.slice(anchorIndex + from.length)}`;
1369
+ try {
1370
+ fs.writeFileSync(adaptiveTarget(source).target, targetState.render(weakened));
1371
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1372
+ assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [...expected].sort(), name);
1373
+ } finally {
1374
+ fs.writeFileSync(adaptiveTarget(source).target, targetState.bytes);
1375
+ }
1376
+ assert.deepEqual(fs.readFileSync(adaptiveTarget(source).target), targetState.bytes, `${name} byte restore`);
1377
+ assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [], `${name} restore`);
1378
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1379
+ }
1380
+
1381
+ const intactAdaptive = readInstalledAuthoringProjection(root, installedRoot);
1382
+ const adaptivePositiveControls = [
1383
+ ['open material surface', intactAdaptive.templates.replace(
1384
+ integrationLine,
1385
+ `${integrationLine}\n| other:domain-specific | task-specific material choices and observable oracle |`
1386
+ )],
1387
+ ['task CP reference requirement', intactAdaptive.templates.replace(
1388
+ ADAPTIVE_COVERAGE_CLAUSES.scopedUnion,
1389
+ `${ADAPTIVE_COVERAGE_CLAUSES.scopedUnion} Require every task to reference a CP row.`
1390
+ )]
1391
+ ];
1392
+ for (const [name, templates] of adaptivePositiveControls) {
1393
+ assert.notEqual(templates, intactAdaptive.templates, `${name} positive control must change installed templates`);
1394
+ const target = assertInstalledMutationTarget(root, installedRoot, 'references/templates.md');
1395
+ const original = fs.readFileSync(target);
1396
+ try {
1397
+ fs.writeFileSync(target, templates);
1398
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1399
+ assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [], name);
1400
+ } finally {
1401
+ fs.writeFileSync(assertInstalledMutationTarget(root, installedRoot, 'references/templates.md'), original);
1402
+ }
1403
+ assert.deepEqual(fs.readFileSync(target), original, `${name} byte restore`);
1404
+ assert.deepEqual(installedAdaptiveCoverageIssues(root, installedRoot), [], `${name} restore`);
1405
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
1406
+ }
1407
+
250
1408
  const mutations = [
251
1409
  ['skill', (value) => value.replace('Task files are flat beside `plan.md`', 'Task files may be nested')],
252
1410
  ['review', (value) => value.replace('list at 15', 'list without a cap')],
@@ -255,8 +1413,14 @@ function assertInstalledAuthoringProjection(root) {
255
1413
  ];
256
1414
  for (const [key, mutate] of mutations) {
257
1415
  const changed = { ...files, [key]: mutate(files[key]) };
258
- assert.notDeepEqual(installedAuthoringProjectionIssues(changed), []);
1416
+ assert.notDeepEqual(authoringProjectionIssues(changed), []);
259
1417
  }
1418
+ assert.equal(
1419
+ fs.readFileSync(installedSpecMakerPath, 'utf8'),
1420
+ convertCodexAgentContent(sourceSpecMaker, path.basename(SPEC_MAKER_SOURCE_PATH)),
1421
+ 'Codex spec_maker projection must be byte-exact after mutation restores'
1422
+ );
1423
+ assertCanonicalSpecsSourceBytesUnchanged(sourceBytes);
260
1424
  }
261
1425
 
262
1426
  test('Codex payload transform emits native skill and subagent syntax', () => {
@@ -269,13 +1433,165 @@ test('Codex payload transform emits native skill and subagent syntax', () => {
269
1433
  transformed,
270
1434
  /spawn_agent\(agent_type="implementer", fork_turns="none", message="Implement it", task_name="code_feature"\)/
271
1435
  );
272
- assert.match(transformed, /\$hapo-specs auth/);
1436
+ assert.match(transformed, /\$cf-specs auth/);
273
1437
  assert.match(transformed, /`send_message`/);
274
1438
  assert.match(transformed, /`exec_command`/);
275
1439
  assert.match(transformed, /`apply_patch`/);
276
1440
  assert.doesNotMatch(transformed, /Agent\(|subagent_type|\/specs\b|Claude Code/);
277
1441
  });
278
1442
 
1443
+ test('Codex payload transform keeps structured user-input grammar across determiners', () => {
1444
+ const cases = [
1445
+ ['standalone bare token', 'AskUserQuestion', 'a structured user-input request'],
1446
+ ['standalone code token', '`AskUserQuestion`', 'a structured user-input request'],
1447
+ ['standalone multi-backtick token', '``AskUserQuestion``', 'a structured user-input request'],
1448
+ ['standalone padded multi-backtick token', '`` AskUserQuestion ``', 'a structured user-input request'],
1449
+ ['one', 'one AskUserQuestion call', 'one structured user-input request call'],
1450
+ ['one multi-backtick', 'one ``AskUserQuestion`` call', 'one structured user-input request call'],
1451
+ ['one padded multi-backtick', 'one `` AskUserQuestion `` call', 'one structured user-input request call'],
1452
+ ['each', 'each AskUserQuestion call', 'each structured user-input request call'],
1453
+ ['every', 'every AskUserQuestion batch', 'every structured user-input request batch'],
1454
+ ['another', 'another AskUserQuestion call', 'another structured user-input request call'],
1455
+ ['this', 'this AskUserQuestion call', 'this structured user-input request call'],
1456
+ ['that', 'that AskUserQuestion call', 'that structured user-input request call'],
1457
+ ['a', 'a AskUserQuestion call', 'a structured user-input request call'],
1458
+ ['an', 'an AskUserQuestion call', 'a structured user-input request call'],
1459
+ ['capitalized each', 'Each AskUserQuestion call', 'Each structured user-input request call'],
1460
+ ['capitalized an', 'An AskUserQuestion call', 'A structured user-input request call'],
1461
+ ['the', 'the AskUserQuestion call', 'the structured user-input request call'],
1462
+ ['numeric', '27 AskUserQuestion calls', '27 structured user-input request calls'],
1463
+ ['plural calls', 'AskUserQuestion calls', 'structured user-input request calls'],
1464
+ ['plural batches', 'AskUserQuestion batches', 'structured user-input request batches']
1465
+ ];
1466
+
1467
+ for (const [name, input, expected] of cases) {
1468
+ const actual = normalizeCodexBody(input, '/fixture/instructions.md');
1469
+ assert.equal(actual, expected, name);
1470
+ assert.equal(normalizeCodexBody(actual, '/fixture/instructions.md'), expected, `${name} idempotence`);
1471
+ assert.doesNotMatch(actual, /\bAskUserQuestion\b/, `${name} Claude-only token`);
1472
+ assert.doesNotMatch(actual, /\ban structured user-input request\b/i, `${name} incompatible article`);
1473
+ assert.doesNotMatch(
1474
+ actual,
1475
+ /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a structured user-input request\b/i,
1476
+ `${name} doubled article`
1477
+ );
1478
+ }
1479
+
1480
+ assert.equal(
1481
+ normalizeCodexBody('one\nAskUserQuestion call', '/fixture/instructions.md'),
1482
+ 'one\nstructured user-input request call',
1483
+ 'a single Markdown soft break must preserve its determiner relationship'
1484
+ );
1485
+ assert.equal(
1486
+ normalizeCodexBody('AskUserQuestion\nCalls', '/fixture/instructions.md'),
1487
+ 'structured user-input request\nCalls',
1488
+ 'a single Markdown soft break must preserve a plural relationship'
1489
+ );
1490
+ assert.equal(
1491
+ normalizeCodexBody('one\n\nAskUserQuestion call', '/fixture/instructions.md'),
1492
+ 'one\n\na structured user-input request call',
1493
+ 'a paragraph break must not bind a prior determiner'
1494
+ );
1495
+ assert.equal(
1496
+ normalizeCodexBody('MyAskUserQuestionHelper AskUserQuestionFactory', '/fixture/instructions.md'),
1497
+ 'MyAskUserQuestionHelper AskUserQuestionFactory',
1498
+ 'balanced token matching must preserve identifiers'
1499
+ );
1500
+ assert.equal(
1501
+ normalizeCodexBody('[tool](https://example.test/AskUserQuestion)', '/fixture/instructions.md'),
1502
+ '[tool](https://example.test/AskUserQuestion)',
1503
+ 'Markdown link destinations must stay byte-exact'
1504
+ );
1505
+ assert.equal(
1506
+ normalizeCodexBody('https://example.test/AskUserQuestion', '/fixture/instructions.md'),
1507
+ 'https://example.test/AskUserQuestion',
1508
+ 'raw URL tokens must stay byte-exact'
1509
+ );
1510
+ assert.equal(
1511
+ normalizeCodexBody('one ``AskUserQuestion``` call', '/fixture/instructions.md'),
1512
+ 'one ``AskUserQuestion``` call',
1513
+ 'mismatched backtick runs must stay byte-exact'
1514
+ );
1515
+ assert.equal(
1516
+ normalizeCodexBody('``Use AskUserQuestion here``', '/fixture/instructions.md'),
1517
+ '``Use AskUserQuestion here``',
1518
+ 'non-token code spans must stay byte-exact'
1519
+ );
1520
+ assert.equal(
1521
+ normalizeCodexBody('```js\nAskUserQuestion\n````', '/fixture/instructions.md'),
1522
+ '```js\nAskUserQuestion\n````',
1523
+ 'a longer closing backtick fence must protect its body'
1524
+ );
1525
+ assert.equal(
1526
+ normalizeCodexBody('~~~text\nAskUserQuestion\n~~~~', '/fixture/instructions.md'),
1527
+ '~~~text\nAskUserQuestion\n~~~~',
1528
+ 'a longer closing tilde fence must protect its body'
1529
+ );
1530
+ const markerCollision = '\uE000CAFEKIT_0_ASK_999\uE001';
1531
+ assert.equal(
1532
+ normalizeCodexBody(markerCollision, '/fixture/instructions.md'),
1533
+ markerCollision,
1534
+ 'literal internal-marker-shaped input must stay byte-exact'
1535
+ );
1536
+ const codeMarkerCollision = '\uE000CAFEKIT_0_CODE_0\uE001 and ``not token``';
1537
+ assert.equal(
1538
+ normalizeCodexBody(codeMarkerCollision, '/fixture/instructions.md'),
1539
+ codeMarkerCollision,
1540
+ 'mask restoration must not replace a literal marker-shaped prefix'
1541
+ );
1542
+ assert.equal(
1543
+ normalizeCodexBody('one AskUserQuestion call', '/fixture/session.cjs'),
1544
+ 'one AskUserQuestion call',
1545
+ 'non-instruction assets must preserve executable text'
1546
+ );
1547
+ assert.equal(
1548
+ normalizeCodexBody('No structured input token here.', '/fixture/instructions.md'),
1549
+ 'No structured input token here.',
1550
+ 'token-free instruction text must stay byte-exact'
1551
+ );
1552
+ });
1553
+
1554
+ test('Codex structured-input corpus oracle stays differential and production-aware', () => {
1555
+ const sourceRoot = path.join(PACKAGE_ROOT, 'src/claude');
1556
+ const occurrenceFiles = allFiles(sourceRoot, (file) => /\.(?:md|mdx|txt|cjs|js)$/i.test(file))
1557
+ .filter((file) => fs.readFileSync(file, 'utf8').includes('AskUserQuestion'));
1558
+ const relative = (file) => path.relative(PACKAGE_ROOT, file).split(path.sep).join('/');
1559
+ const actualPaths = occurrenceFiles.map(relative).sort();
1560
+ const expectedPaths = [
1561
+ 'src/claude/agents/spec-maker.md',
1562
+ 'src/claude/hooks/session.cjs',
1563
+ 'src/claude/skills/git/SKILL.md',
1564
+ 'src/claude/skills/inspect/SKILL.md'
1565
+ ];
1566
+ assert.deepEqual(actualPaths, expectedPaths, 'every source occurrence needs an explicit projection oracle');
1567
+
1568
+ const instructionExpectedSnippets = new Map([
1569
+ ['src/claude/skills/git/SKILL.md', 'Present options via a structured user-input request — header'],
1570
+ ['src/claude/skills/inspect/SKILL.md', '**Fallback to a structured user-input request:**']
1571
+ ]);
1572
+ for (const [relativePath, expectedSnippet] of instructionExpectedSnippets) {
1573
+ const sourcePath = path.join(PACKAGE_ROOT, relativePath);
1574
+ const source = fs.readFileSync(sourcePath, 'utf8');
1575
+ const sentinel = '__CAFEKIT_STRUCTURED_INPUT_SENTINEL__';
1576
+ const withSentinel = source.replace(/`?\bAskUserQuestion\b`?/g, sentinel);
1577
+ assert.notEqual(withSentinel, source, `${relativePath} differential control must replace a token`);
1578
+ const expected = normalizeCodexBody(withSentinel, sourcePath)
1579
+ .replaceAll(sentinel, 'a structured user-input request');
1580
+ const actual = normalizeCodexBody(source, sourcePath);
1581
+ assert.equal(actual, expected, `${relativePath} independent differential oracle`);
1582
+ assert.match(actual, new RegExp(expectedSnippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
1583
+ }
1584
+
1585
+ const specMaker = fs.readFileSync(SPEC_MAKER_SOURCE_PATH, 'utf8');
1586
+ const convertedAgent = convertCodexAgentContent(specMaker, path.basename(SPEC_MAKER_SOURCE_PATH));
1587
+ assert.doesNotMatch(convertedAgent, /\bAskUserQuestion\b/);
1588
+ assert.doesNotMatch(convertedAgent, /structured user-input request/);
1589
+
1590
+ const sessionPath = path.join(PACKAGE_ROOT, 'src/claude/hooks/session.cjs');
1591
+ const session = normalizeCodexBody(fs.readFileSync(sessionPath, 'utf8'), sessionPath);
1592
+ assert.equal((session.match(/\bAskUserQuestion\b/g) || []).length, 2);
1593
+ });
1594
+
279
1595
  test('Codex payload transform preserves executable keyword arguments', () => {
280
1596
  const transformed = normalizeCodexBody(
281
1597
  'parser = ArgumentParser(description="Analyze it")\n' +
@@ -618,6 +1934,43 @@ test('Codex dry-run leaves both managed roots untouched', () => {
618
1934
  });
619
1935
  });
620
1936
 
1937
+ test('Codex install preserves the adaptive diagnostic-only Debug contract', () => {
1938
+ inTempProject((root) => {
1939
+ const result = install(root);
1940
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
1941
+
1942
+ const installedSkill = fs.readFileSync(
1943
+ path.join(root, '.agents/skills/debug/SKILL.md'), 'utf8'
1944
+ );
1945
+ assert.match(installedSkill, /^name: cf-debug$/m);
1946
+ assert.match(installedSkill, /## Proportional depth/);
1947
+ assert.match(installedSkill, /Evidence Timeline/);
1948
+ assert.match(installedSkill, /### Elimination Path/);
1949
+ assert.match(installedSkill, /### Recurrence-Prevention Handoff/);
1950
+ assert.match(installedSkill, /`cf-debug` is read-only for product code/);
1951
+
1952
+ const installedAgent = fs.readFileSync(
1953
+ path.join(root, '.codex/agents/debugger.toml'), 'utf8'
1954
+ );
1955
+ assert.equal(
1956
+ installedAgent,
1957
+ convertCodexAgentContent(fs.readFileSync(DEBUG_AGENT_SOURCE_PATH, 'utf8'), 'debugger.md')
1958
+ );
1959
+ assert.match(installedAgent, /Never implement the repair/);
1960
+
1961
+ for (const relative of DEBUG_REFERENCE_FILES) {
1962
+ const source = fs.readFileSync(
1963
+ path.join(PACKAGE_ROOT, 'src/claude/references/debugger', relative), 'utf8'
1964
+ );
1965
+ const installed = fs.readFileSync(
1966
+ path.join(root, '.codex/references/debugger', relative), 'utf8'
1967
+ );
1968
+ assert.equal(installed, normalizeCodexBody(source, relative), relative);
1969
+ }
1970
+ assert.equal(fs.existsSync(DEBUG_SKILL_SOURCE_PATH), true);
1971
+ });
1972
+ });
1973
+
621
1974
  test('Codex Windows hook launchers stay project-bound without Git from nested cwd', () => {
622
1975
  inTempProject((root) => {
623
1976
  const projectRoot = path.join(root, 'project with spaces');
@@ -682,12 +2035,12 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
682
2035
  });
683
2036
  });
684
2037
 
685
- test('Codex fresh install is exact while refresh and upgrade preserve existing files', () => {
2038
+ test('Codex installed Specs and spec-maker reject adaptive coverage mutations', () => {
686
2039
  inTempProject((root) => {
687
2040
  const userInstructions = '# User rules\n\nKeep this exact.\n';
688
2041
  fs.writeFileSync(path.join(root, 'AGENTS.md'), userInstructions);
689
2042
 
690
- const first = install(root);
2043
+ const first = install(root, ['--with-document-skills']);
691
2044
  assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
692
2045
  assert.equal(fs.existsSync(path.join(root, '.claude')), false);
693
2046
  assert.equal(fs.existsSync(path.join(root, '.codex', 'config.toml')), false);
@@ -699,6 +2052,8 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
699
2052
  '.codex/rules/workflow.md',
700
2053
  '.codex/rules/hook-protocols.md',
701
2054
  '.codex/rules/state-sync.md',
2055
+ '.codex/rules/review-audit-self-decision.md',
2056
+ '.codex/rules/process-management.md',
702
2057
  '.codex/scripts/spec-ground.cjs',
703
2058
  '.codex/scripts/validate-spec-output.cjs',
704
2059
  '.agents/.gitignore',
@@ -715,7 +2070,20 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
715
2070
  path.join(root, '.codex/scripts/spec-ground.cjs'),
716
2071
  installedDesign
717
2072
  );
718
- assertInstalledAuthoringProjection(root);
2073
+ const installedSpecsRoot = path.join(root, '.agents/skills/specs');
2074
+ assert.throws(
2075
+ () => installedAuthoringProjectionIssues(root, '.agents/skills/specs'),
2076
+ /must be absolute/
2077
+ );
2078
+ assert.throws(
2079
+ () => installedAuthoringProjectionIssues(root, SPECS_SOURCE_ROOT),
2080
+ /must stay inside project/
2081
+ );
2082
+ assert.throws(
2083
+ () => installedAuthoringProjectionIssues(root, path.dirname(root)),
2084
+ /must stay inside project/
2085
+ );
2086
+ assertInstalledAuthoringProjection(root, installedSpecsRoot);
719
2087
 
720
2088
  const installedTask = fs.readFileSync(
721
2089
  path.join(root, '.agents/skills/specs/templates/task.md'), 'utf8'
@@ -758,6 +2126,12 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
758
2126
  const name = path.basename(fileName, '.md').replace(/-/g, '_');
759
2127
  const agentPath = path.join(root, '.codex', 'agents', `${name}.toml`);
760
2128
  const content = fs.readFileSync(agentPath, 'utf8');
2129
+ const sourcePath = path.join(PACKAGE_ROOT, 'src', 'claude', 'agents', fileName);
2130
+ assert.equal(
2131
+ content,
2132
+ convertCodexAgentContent(fs.readFileSync(sourcePath, 'utf8'), fileName),
2133
+ `Codex agent projection drifted: ${fileName}`
2134
+ );
761
2135
  assert.equal(parseGeneratedTomlString(content, 'name'), name);
762
2136
  assert.ok(parseGeneratedTomlString(content, 'description').length > 8);
763
2137
  assert.ok(parseGeneratedTomlString(content, 'developer_instructions').length > 20);
@@ -776,6 +2150,27 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
776
2150
  assert.equal(new Set(skillNames).size, skillNames.length);
777
2151
  assert.ok(skillNames.every((name) => /^[a-z0-9-]+$/.test(name)));
778
2152
 
2153
+ const scoutSkill = fs.readFileSync(
2154
+ path.join(root, '.agents', 'skills', 'inspect', 'SKILL.md'),
2155
+ 'utf8'
2156
+ );
2157
+ assert.match(scoutSkill, /^name:\s*cf-scout$/m);
2158
+ assert.equal(
2159
+ fs.existsSync(path.join(root, '.agents', 'skills', 'scout')),
2160
+ false,
2161
+ 'public Scout rename must keep the manifest-owned inspect directory'
2162
+ );
2163
+ const askSkill = fs.readFileSync(
2164
+ path.join(root, '.agents', 'skills', 'question', 'SKILL.md'),
2165
+ 'utf8'
2166
+ );
2167
+ assert.match(askSkill, /^name:\s*cf-ask$/m);
2168
+ assert.equal(
2169
+ fs.existsSync(path.join(root, '.agents', 'skills', 'ask')),
2170
+ false,
2171
+ 'public Ask rename must keep the manifest-owned question directory'
2172
+ );
2173
+
779
2174
  const catalog = spawnSync(
780
2175
  process.execPath,
781
2176
  [path.join(root, '.codex', 'scripts', 'generate-skill-catalog.cjs'), '--json'],
@@ -789,12 +2184,13 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
789
2184
  fs.realpathSync(path.join(root, '.agents', 'skills'))
790
2185
  );
791
2186
 
792
- const dockerScript = fs.readFileSync(
793
- path.join(root, '.agents', 'skills', 'devops', 'scripts', 'docker_optimize.py'),
794
- 'utf8'
795
- );
796
- assert.match(dockerScript, /description="Analyze Dockerfile for optimization opportunities"/);
797
- assert.doesNotMatch(dockerScript, /task_name="analyze_dockerfile/);
2187
+ for (const retired of MANIFEST.obsolete.skills) {
2188
+ assert.equal(
2189
+ fs.existsSync(path.join(root, '.agents', 'skills', retired)),
2190
+ false,
2191
+ `retired skill should not be installed: ${retired}`
2192
+ );
2193
+ }
798
2194
 
799
2195
  const multimodalScript = fs.readFileSync(
800
2196
  path.join(root, '.agents', 'skills', 'ai-multimodal', 'scripts', 'gemini_batch_process.py'),
@@ -815,10 +2211,10 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
815
2211
  assert.doesNotMatch(visible, /If you are Codex CLI or any other runtime, ignore this entire Codex block/);
816
2212
  assert.doesNotMatch(
817
2213
  visible.replaceAll(foreignRuntimeBoundary, ''),
818
- /\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/hapo:|\bhapo:|Claude Code/,
2214
+ /\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/cf:|\bcf:|Claude Code/,
819
2215
  );
820
2216
  assert.doesNotMatch(visible, /@@PRIVACY_PROMPT_START@@|Claude Tasks/);
821
- assert.match(visible, /\$hapo-specs/);
2217
+ assert.match(visible, /\$cf-specs/);
822
2218
 
823
2219
  const agentsMd = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
824
2220
  assert.ok(agentsMd.startsWith(userInstructions));
@@ -845,6 +2241,7 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
845
2241
 
846
2242
  const sameVersion = install(root);
847
2243
  assert.equal(sameVersion.status, 0, `${sameVersion.stdout}\n${sameVersion.stderr}`);
2244
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
848
2245
  assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
849
2246
  assert.equal(
850
2247
  fs.existsSync(obsoleteSpecsRule),
@@ -859,6 +2256,7 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
859
2256
 
860
2257
  const upgrade = install(root);
861
2258
  assert.equal(upgrade.status, 0, `${upgrade.stdout}\n${upgrade.stderr}`);
2259
+ assert.deepEqual(installedAuthoringProjectionIssues(root, installedSpecsRoot), []);
862
2260
  assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
863
2261
  assert.equal(
864
2262
  fs.existsSync(obsoleteSpecsRule),
@@ -873,6 +2271,732 @@ test('Codex fresh install is exact while refresh and upgrade preserve existing f
873
2271
  });
874
2272
  });
875
2273
 
2274
+ test('Codex installed Brainstorm skill reference and agent preserve proportional routing parity', () => {
2275
+ inTempProject((root) => {
2276
+ const sourceBytes = canonicalBrainstormSourceBytes();
2277
+ const result = install(root);
2278
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
2279
+
2280
+ const installedSkillPath = path.join(root, '.agents/skills/brainstorm/SKILL.md');
2281
+ const installedReferencePath = path.join(
2282
+ root,
2283
+ '.agents/skills/brainstorm/references/question-framework.md'
2284
+ );
2285
+ const installedAgentPath = path.join(root, '.codex/agents/brainstormer.toml');
2286
+ const sourceSkill = fs.readFileSync(BRAINSTORM_SKILL_SOURCE_PATH, 'utf8');
2287
+ const sourceReference = fs.readFileSync(BRAINSTORM_REFERENCE_SOURCE_PATH, 'utf8');
2288
+ const sourceAgent = fs.readFileSync(BRAINSTORM_AGENT_SOURCE_PATH, 'utf8');
2289
+
2290
+ assert.equal(
2291
+ fs.readFileSync(installedSkillPath, 'utf8'),
2292
+ normalizeCodexBody(sourceSkill, BRAINSTORM_SKILL_SOURCE_PATH),
2293
+ 'installed Brainstorm skill must equal the generic Codex projection'
2294
+ );
2295
+ assert.equal(
2296
+ fs.readFileSync(installedReferencePath, 'utf8'),
2297
+ normalizeCodexBody(sourceReference, BRAINSTORM_REFERENCE_SOURCE_PATH),
2298
+ 'installed Brainstorm reference must equal the generic Codex projection'
2299
+ );
2300
+ assert.equal(
2301
+ fs.readFileSync(installedAgentPath, 'utf8'),
2302
+ convertCodexAgentContent(sourceAgent, path.basename(BRAINSTORM_AGENT_SOURCE_PATH)),
2303
+ 'installed brainstormer agent must equal the Codex TOML projection'
2304
+ );
2305
+
2306
+ assert.deepEqual(brainstormProjectionIssues(readInstalledBrainstormProjection(root)), []);
2307
+ assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes);
2308
+
2309
+ const safelyStrengthened = readInstalledBrainstormProjection(root);
2310
+ safelyStrengthened.agent += '\nThe specialist may never ask the user directly or contact the user. Do not write files or mutate shared task state. Do not delegate work or delegate tasks. The specialist is not permitted to launch Specs or run Develop.';
2311
+ assert.deepEqual(
2312
+ brainstormProjectionIssues(safelyStrengthened),
2313
+ [],
2314
+ 'safe specialist prohibitions must not produce authority issues'
2315
+ );
2316
+
2317
+ const mutations = [
2318
+ {
2319
+ name: 'adaptive-direct-precedence-removed', source: 'skill',
2320
+ from: 'Route Direct first, then\napply controls only to requests that remain in Brainstorm.',
2321
+ to: 'Apply controls before Direct classification.', expected: ['adaptive-direct-precedence']
2322
+ },
2323
+ {
2324
+ name: 'adaptive-ordered-depth-removed', source: 'skill',
2325
+ from: 'With no Deep signal, use Standard.', to: 'Deep is always the default.',
2326
+ expected: ['adaptive-ordered-depth']
2327
+ },
2328
+ {
2329
+ name: 'adaptive-leading-flags-removed', source: 'skill',
2330
+ from: 'Parse controls only from the leading consecutive token segment.',
2331
+ to: 'Parse flag-like tokens anywhere.', expected: ['adaptive-leading-flags']
2332
+ },
2333
+ {
2334
+ name: 'adaptive-failure-isolation-removed', source: 'skill',
2335
+ from: 'failure isolation for partial or cascading failure\nacross boundaries',
2336
+ to: 'generic failure notes', expected: ['adaptive-lens-trigger-skip']
2337
+ },
2338
+ {
2339
+ name: 'adaptive-evidence-fallback-removed', source: 'skill',
2340
+ from: 'Missing evidence\nforces feasibility `unknown` and confidence `low`.',
2341
+ to: 'Missing evidence permits a confident guess.', expected: ['adaptive-evidence-semantics']
2342
+ },
2343
+ {
2344
+ name: 'adaptive-numeric-evidence-removed', source: 'skill',
2345
+ from: 'A numeric estimate requires\nrange, unit, basis, evidence, and assumptions; otherwise report `unknown`.',
2346
+ to: 'A numeric estimate may be a best-effort number.', expected: ['adaptive-numeric-estimates']
2347
+ },
2348
+ {
2349
+ name: 'adaptive-pre-tool-redaction-removed', source: 'skill',
2350
+ from: 'Before an\nexternal visual tool or adviser handoff, minimize context and redact secrets,\ncredentials, private keys, access tokens, and unnecessary PII.',
2351
+ to: 'Forward full context to every external tool and adviser.', expected: ['adaptive-pre-tool-redaction']
2352
+ },
2353
+ {
2354
+ name: 'adaptive-adviser-gate-removed', source: 'skill',
2355
+ from: '`--advice` invokes\n`brainstormer` only after the material-choice gate;',
2356
+ to: '`--advice` invokes `brainstormer` before routing;', expected: ['adaptive-adviser-gate']
2357
+ },
2358
+ {
2359
+ name: 'adaptive-decision-freshness-removed', source: 'skill',
2360
+ from: 'The first section records target\nidentity, current source revision and worktree state or `[UNVERIFIED]`, an\nevidence-as-of value, and what change invalidates the brief.',
2361
+ to: 'The handoff has no revision or freshness binding.', expected: ['adaptive-decision-freshness']
2362
+ },
2363
+ {
2364
+ name: 'adaptive-overlay-authority-removed', source: 'skill',
2365
+ from: 'Neither overlay\nwrites, approves, persists, dispatches, or completes work.',
2366
+ to: 'Overlays may persist, approve, dispatch, and complete work.', expected: ['adaptive-non-authority']
2367
+ },
2368
+ {
2369
+ name: 'debug-route-removed',
2370
+ source: 'skill',
2371
+ from: 'Then use `cf-debug` until',
2372
+ to: 'Skip diagnosis and choose a remedy before',
2373
+ expected: ['bug-routing']
2374
+ },
2375
+ {
2376
+ name: 'fix-authority-removed',
2377
+ source: 'skill',
2378
+ from: 'only when the user explicitly requested a fix',
2379
+ to: 'automatically whenever root cause is known',
2380
+ expected: ['bug-routing']
2381
+ },
2382
+ {
2383
+ name: 'exploration-stop-removed',
2384
+ source: 'skill',
2385
+ from: 'Do not request design approval, persist a report, or\n invoke another workflow without a new explicit request.',
2386
+ to: 'Request approval, persist a report, and invoke another workflow.',
2387
+ expected: ['exploration-stop']
2388
+ },
2389
+ {
2390
+ name: 'conditional-options-removed',
2391
+ source: 'skill',
2392
+ from: 'For a material choice, compare 2–3 mechanically distinct viable approaches',
2393
+ to: 'For every request, present one predetermined approach',
2394
+ expected: ['option-cardinality']
2395
+ },
2396
+ {
2397
+ name: 'specialist-strawman-boundary-removed',
2398
+ source: 'agent',
2399
+ from: 'never invent strawmen to fill a quota.',
2400
+ to: 'invent strawmen to fill the quota.',
2401
+ expected: ['option-cardinality']
2402
+ },
2403
+ {
2404
+ name: 'specialist-controller-ownership-removed',
2405
+ source: 'agent',
2406
+ from: 'you do not replace its routing, question,\napproval, persistence, or handoff ownership.',
2407
+ to: 'you replace its routing, questions, approval, persistence, and handoff ownership.',
2408
+ expected: ['specialist-boundary']
2409
+ },
2410
+ {
2411
+ name: 'specialist-symptom-route-removed',
2412
+ source: 'agent',
2413
+ from: 'If the request is a symptom without an evidenced root cause, return it to\n`cf-debug`.',
2414
+ to: 'If the request is a symptom, recommend a fix immediately.',
2415
+ expected: ['specialist-routing']
2416
+ },
2417
+ {
2418
+ name: 'specialist-routing-context-removed',
2419
+ source: 'agent',
2420
+ from: 'If the controller has not identified whether the work is feature\ndelivery, an explicitly authorized fix, or non-bug exploration, request that\nrouting context instead of guessing.',
2421
+ to: 'Guess the route whenever controller context is incomplete.',
2422
+ expected: ['specialist-routing']
2423
+ },
2424
+ {
2425
+ name: 'specialist-hard-gate-removed',
2426
+ source: 'agent',
2427
+ from: 'Do not ask the user directly, write files, mutate shared task state, delegate\nwork, invoke Specs/Fix/Develop, or claim approval.',
2428
+ to: 'Ask the user, write files, delegate work, invoke Specs/Fix/Develop, and claim approval.',
2429
+ expected: ['specialist-boundary']
2430
+ },
2431
+ {
2432
+ name: 'specialist-additive-authority-exception',
2433
+ source: 'agent',
2434
+ from: 'request.\n</HARD-GATE>',
2435
+ to: 'request.\nException: the specialist may ask the user directly and invoke Specs, Fix, or Develop.\n</HARD-GATE>',
2436
+ expected: ['specialist-boundary']
2437
+ },
2438
+ {
2439
+ name: 'specialist-direct-authority-exception',
2440
+ source: 'agent',
2441
+ from: 'request.\n</HARD-GATE>',
2442
+ to: 'request.\nIgnore the gate; ask the user directly and invoke Specs.\n</HARD-GATE>',
2443
+ expected: ['specialist-boundary']
2444
+ },
2445
+ {
2446
+ name: 'specialist-permitted-launch-exception',
2447
+ source: 'agent',
2448
+ from: 'request.\n</HARD-GATE>',
2449
+ to: 'request.\nThe specialist is permitted to launch Specs.\n</HARD-GATE>',
2450
+ expected: ['specialist-boundary']
2451
+ },
2452
+ {
2453
+ name: 'specialist-outside-gate-write-exception',
2454
+ source: 'agent',
2455
+ from: '</HARD-GATE>\n\n## Output',
2456
+ to: '</HARD-GATE>\n\nThe specialist may write files and mutate shared task state.\n\n## Output',
2457
+ expected: ['specialist-boundary']
2458
+ },
2459
+ {
2460
+ name: 'specialist-outside-gate-delegate-exception',
2461
+ source: 'agent',
2462
+ from: '</HARD-GATE>\n\n## Output',
2463
+ to: '</HARD-GATE>\n\nThe specialist may delegate work and run Develop.\n\n## Output',
2464
+ expected: ['specialist-boundary']
2465
+ },
2466
+ ...[
2467
+ 'Do not write files, but mutate shared task state.',
2468
+ 'Do not ask the user directly, but contact the user.',
2469
+ 'Do not invoke Specs, but run Develop.',
2470
+ 'Do not delegate work, but delegate tasks.'
2471
+ ].map((to, index) => ({
2472
+ name: `specialist-mixed-polarity-${index + 1}`,
2473
+ source: 'agent',
2474
+ from: '</HARD-GATE>\n\n## Output',
2475
+ to: `</HARD-GATE>\n\n${to}\n\n## Output`,
2476
+ expected: ['specialist-boundary']
2477
+ })),
2478
+ {
2479
+ name: 'specialist-handoff-spelling-exception',
2480
+ source: 'agent',
2481
+ from: '</HARD-GATE>\n\n## Output',
2482
+ to: '</HARD-GATE>\n\nThe specialist may handoff to Specs.\n\n## Output',
2483
+ expected: ['specialist-boundary']
2484
+ },
2485
+ {
2486
+ name: 'specialist-handoff-authority-removed',
2487
+ source: 'agent',
2488
+ from: 'Non-bug exploration may end\nin chat; feature/docs delivery may only prepare a future explicit Specs\ninvocation; bug handoff requires evidenced root cause and the user\'s explicit fix\nrequest.',
2489
+ to: 'Every route may invoke delivery workflows automatically.',
2490
+ expected: ['specialist-handoff']
2491
+ },
2492
+ {
2493
+ name: 'decision-provenance-removed',
2494
+ source: 'framework',
2495
+ from: 'Do not write "user selected" unless direct user text or the native input tool\n confirms it.',
2496
+ to: 'Write "user selected" for inferred defaults.',
2497
+ expected: ['approval-persistence']
2498
+ },
2499
+ {
2500
+ name: 'installed-grammar-corruption',
2501
+ source: 'skill',
2502
+ from: '## Completion bar',
2503
+ to: 'one a structured user-input request call\n\n## Completion bar',
2504
+ expected: ['projection-grammar']
2505
+ }
2506
+ ];
2507
+
2508
+ for (const mutation of mutations) {
2509
+ const target = assertInstalledBrainstormMutationTarget(root, mutation.source);
2510
+ const original = fs.readFileSync(target);
2511
+ const fullContent = original.toString('utf8');
2512
+ const projectedContent = mutation.source === 'agent'
2513
+ ? parseGeneratedTomlString(fullContent, 'developer_instructions')
2514
+ : fullContent;
2515
+ const anchor = projectedContent.indexOf(mutation.from);
2516
+ assert.ok(anchor >= 0, `${mutation.name} mutation anchor must exist`);
2517
+ assert.equal(
2518
+ projectedContent.indexOf(mutation.from, anchor + mutation.from.length),
2519
+ -1,
2520
+ `${mutation.name} mutation anchor must be unique`
2521
+ );
2522
+ const weakened = `${projectedContent.slice(0, anchor)}${mutation.to}${projectedContent.slice(anchor + mutation.from.length)}`;
2523
+ const rendered = mutation.source === 'agent'
2524
+ ? replaceGeneratedTomlString(fullContent, 'developer_instructions', weakened)
2525
+ : weakened;
2526
+ try {
2527
+ fs.writeFileSync(assertInstalledBrainstormMutationTarget(root, mutation.source), rendered);
2528
+ assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes);
2529
+ assert.deepEqual(
2530
+ brainstormProjectionIssues(readInstalledBrainstormProjection(root)),
2531
+ mutation.expected,
2532
+ mutation.name
2533
+ );
2534
+ } finally {
2535
+ fs.writeFileSync(assertInstalledBrainstormMutationTarget(root, mutation.source), original);
2536
+ }
2537
+ assert.deepEqual(fs.readFileSync(target), original, `${mutation.name} byte restore`);
2538
+ assert.deepEqual(brainstormProjectionIssues(readInstalledBrainstormProjection(root)), []);
2539
+ assertCanonicalBrainstormSourceBytesUnchanged(sourceBytes);
2540
+ }
2541
+ });
2542
+ });
2543
+
2544
+ test('Codex installed Develop preserves plan-native execution and references', () => {
2545
+ inTempProject((root) => {
2546
+ const sourceBytes = new Map(DEVELOP_BUNDLE.map((relative) => [
2547
+ path.join(DEVELOP_SOURCE_ROOT, relative),
2548
+ fs.readFileSync(path.join(DEVELOP_SOURCE_ROOT, relative))
2549
+ ]));
2550
+ const result = install(root);
2551
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
2552
+
2553
+ const installedRoot = path.join(root, '.agents/skills/develop');
2554
+ for (const relative of DEVELOP_BUNDLE) {
2555
+ const sourcePath = path.join(DEVELOP_SOURCE_ROOT, relative);
2556
+ assert.equal(
2557
+ fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
2558
+ normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
2559
+ `Codex Develop projection drifted: ${relative}`
2560
+ );
2561
+ }
2562
+ const installedSkill = fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8');
2563
+ assert.match(installedSkill, /\$cf-develop <feature>/);
2564
+ assert.match(installedSkill, /\.codex\/scripts\/workflow-policy\.cjs/);
2565
+ assert.doesNotMatch(installedSkill, /\/cf:develop|\.claude\/scripts/);
2566
+ assert.equal(fs.existsSync(path.join(root, '.claude/skills/develop')), false);
2567
+ for (const [sourcePath, expected] of sourceBytes) {
2568
+ assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
2569
+ }
2570
+ });
2571
+ });
2572
+
2573
+ test('Codex installed Research preserves adaptive evidence semantics', () => {
2574
+ inTempProject((root) => {
2575
+ const sourceBytes = new Map([
2576
+ [RESEARCH_SKILL_SOURCE_PATH, fs.readFileSync(RESEARCH_SKILL_SOURCE_PATH)],
2577
+ [RESEARCH_AGENT_SOURCE_PATH, fs.readFileSync(RESEARCH_AGENT_SOURCE_PATH)],
2578
+ ]);
2579
+ const result = install(root);
2580
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
2581
+
2582
+ const installedSkillPath = path.join(root, '.agents/skills/research/SKILL.md');
2583
+ const installedAgentPath = path.join(root, '.codex/agents/researcher.toml');
2584
+ assert.equal(
2585
+ fs.readFileSync(installedSkillPath, 'utf8'),
2586
+ normalizeCodexBody(fs.readFileSync(RESEARCH_SKILL_SOURCE_PATH, 'utf8'), RESEARCH_SKILL_SOURCE_PATH)
2587
+ );
2588
+ assert.equal(
2589
+ fs.readFileSync(installedAgentPath, 'utf8'),
2590
+ convertCodexAgentContent(fs.readFileSync(RESEARCH_AGENT_SOURCE_PATH, 'utf8'), 'researcher.md')
2591
+ );
2592
+ const installedSkill = fs.readFileSync(installedSkillPath, 'utf8');
2593
+ const installedAgent = parseGeneratedTomlString(
2594
+ fs.readFileSync(installedAgentPath, 'utf8'), 'developer_instructions'
2595
+ );
2596
+ assert.match(installedSkill, /^name: cf-research$/m);
2597
+ assert.match(installedSkill, /Quick \| One low-risk, reversible fact or known option/);
2598
+ assert.match(installedSkill, /research sequentially with the same evidence bar/);
2599
+ assert.match(installedSkill, /Default to a concise answer in chat/);
2600
+ assert.match(installedSkill, /confirmed \| inferred \| unresolved/);
2601
+ assert.match(installedAgent, /do not implement code,\s+write files, mutate task state/);
2602
+ assert.match(installedAgent, /owns any\s+authorized persistence/);
2603
+ assert.doesNotMatch(installedSkill, /must (?:instantly |always )?delegate/i);
2604
+
2605
+ const manifest = JSON.parse(
2606
+ fs.readFileSync(path.join(root, '.codex/cafekit-manifest.json'), 'utf8')
2607
+ );
2608
+ assert.ok(manifest.files['.agents/skills/research/SKILL.md']);
2609
+ assert.ok(manifest.files['.codex/agents/researcher.toml']);
2610
+ assert.equal(fs.existsSync(path.join(root, '.claude/skills/research')), false);
2611
+ for (const [sourcePath, expected] of sourceBytes) {
2612
+ assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
2613
+ }
2614
+ });
2615
+ });
2616
+
2617
+ test('Codex installed Test preserves plan-native proof and references', () => {
2618
+ inTempProject((root) => {
2619
+ const sourcePaths = [
2620
+ ...TEST_BUNDLE.map((relative) => path.join(TEST_SOURCE_ROOT, relative)),
2621
+ TEST_RUNNER_SOURCE_PATH,
2622
+ path.join(CODE_REVIEW_SOURCE_ROOT, 'SKILL.md')
2623
+ ];
2624
+ const sourceBytes = new Map(sourcePaths.map((sourcePath) => [
2625
+ sourcePath, fs.readFileSync(sourcePath)
2626
+ ]));
2627
+ const result = install(root);
2628
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
2629
+
2630
+ const installedRoot = path.join(root, '.agents/skills/test');
2631
+ for (const relative of TEST_BUNDLE) {
2632
+ const sourcePath = path.join(TEST_SOURCE_ROOT, relative);
2633
+ assert.equal(
2634
+ fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
2635
+ normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
2636
+ `Codex Test projection drifted: ${relative}`
2637
+ );
2638
+ }
2639
+ const installedSkill = fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8');
2640
+ assert.match(installedSkill, /\$cf-test/);
2641
+ assert.match(installedSkill, /test-proof-v1/);
2642
+ assert.doesNotMatch(installedSkill, /\/cf:test/);
2643
+
2644
+ const installedRunnerPath = path.join(root, '.codex/agents/test_runner.toml');
2645
+ assert.equal(
2646
+ fs.readFileSync(installedRunnerPath, 'utf8'),
2647
+ convertCodexAgentContent(
2648
+ fs.readFileSync(TEST_RUNNER_SOURCE_PATH, 'utf8'),
2649
+ path.basename(TEST_RUNNER_SOURCE_PATH)
2650
+ ),
2651
+ 'Codex test_runner projection drifted'
2652
+ );
2653
+ const installedRunner = parseGeneratedTomlString(
2654
+ fs.readFileSync(installedRunnerPath, 'utf8'), 'developer_instructions'
2655
+ );
2656
+ assert.match(installedRunner, /test-proof-v1/);
2657
+ assert.match(installedRunner, /sole writer of[\s\S]*Status and inline Receipt/i);
2658
+
2659
+ const reviewSourcePath = path.join(CODE_REVIEW_SOURCE_ROOT, 'SKILL.md');
2660
+ const installedReview = fs.readFileSync(path.join(root, '.agents/skills/code-review/SKILL.md'), 'utf8');
2661
+ assert.equal(installedReview, normalizeCodexBody(
2662
+ fs.readFileSync(reviewSourcePath, 'utf8'), reviewSourcePath
2663
+ ));
2664
+ assert.match(installedReview, /controller-validated `test-proof-v1`/);
2665
+ assert.match(installedReview, /Never create or search for a separate[\s\S]*process-first receipt/i);
2666
+ assert.equal(fs.existsSync(path.join(root, '.claude/skills/test')), false);
2667
+ for (const [sourcePath, expected] of sourceBytes) {
2668
+ assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
2669
+ }
2670
+ });
2671
+ });
2672
+
2673
+ function hotfixProjectionIssues(files) {
2674
+ const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
2675
+ const skill = compact(files.skill);
2676
+ const review = compact(files.review);
2677
+ const parallel = compact(files.parallel);
2678
+ const specialized = compact(files.specialized);
2679
+ const issues = new Set();
2680
+ if (!skill.includes('name: cf-fix')
2681
+ || !skill.includes('# Fix — root-cause repair workflow')
2682
+ || skill.includes('name: cf-hotfix')) {
2683
+ issues.add('public-rename');
2684
+ }
2685
+ if (!skill.includes('## Proportional depth')
2686
+ || !skill.includes('Quick mode only reduces depth; it never skips scout, pre-fix evidence, diagnosis, or before/after verification.')) {
2687
+ issues.add('adaptive-depth');
2688
+ }
2689
+ if (!skill.includes('`Timeline: skipped - <reason>` or `- skipped: <reason>`')
2690
+ || !skill.includes('`Recurrence-Prevention Handoff`, when present, carries evidence-backed candidates only.')
2691
+ || !skill.includes('routes back to diagnosis (`cf-debug`)')) {
2692
+ issues.add('debug-handoff');
2693
+ }
2694
+ if (!skill.includes('report `PASS | PASS_WITH_WARNINGS | FAIL | BLOCKED`')
2695
+ || !skill.includes('The definition of `PASS` defers to `cf-code-review`.')
2696
+ || skill.includes('Confidence score')
2697
+ || !review.includes('Only `FAIL` and `PASS_WITH_WARNINGS` enter remediation retry')
2698
+ || !review.includes('only a fresh literal `PASS` enters finalization')
2699
+ || review.includes('"Approve anyway"')
2700
+ || review.includes('"Approve with known issues"')
2701
+ || review.includes('at most one Medium')) {
2702
+ issues.add('verdict-surface');
2703
+ }
2704
+ if (!skill.includes('The user explicitly requested or permitted delegation or parallel agents.')
2705
+ || !skill.includes('The active runtime exposes an Explore/delegation capability.')
2706
+ || !skill.includes('at least two distinct, non-overlapping scopes')
2707
+ || !parallel.includes('The user explicitly requested or permitted delegation or parallel agents.')) {
2708
+ issues.add('delegation-gate');
2709
+ }
2710
+ if (!skill.includes('The original symptom no longer reproduces with the exact pre-fix command/user flow.')
2711
+ || !skill.includes('Do not silently patch around the regression.')) {
2712
+ issues.add('side-effect-gate');
2713
+ }
2714
+ if (!skill.includes('## Bounded repair frame')
2715
+ || !skill.includes('Quick/local does not add a separate framing ceremony')
2716
+ || !skill.includes('**Outcome:**')
2717
+ || !skill.includes('**Constraints:**')
2718
+ || !skill.includes('**Non-goals:**')
2719
+ || !skill.includes('**Acceptance:**')) {
2720
+ issues.add('bounded-repair-frame');
2721
+ }
2722
+ if (!skill.includes('after diagnosis, research only unresolved external facts')
2723
+ || !skill.includes('`cf-brainstorm` to compare 2-3 options')
2724
+ || !skill.includes('When diagnosis leaves one safe direct repair, skip research and')) {
2725
+ issues.add('deep-decision-route');
2726
+ }
2727
+ if (!skill.includes('Load only the matching')
2728
+ || !specialized.includes('Load only the matching section')
2729
+ || (specialized.match(/\*\*Baseline:\*\*/g) || []).length < 5
2730
+ || (specialized.match(/\*\*Proof:\*\*/g) || []).length < 5) {
2731
+ issues.add('specialized-proof-overlays');
2732
+ }
2733
+ if (!parallel.includes('Diagnosis still starts only')
2734
+ || !parallel.includes('after the required scout outputs are synthesized')
2735
+ || !parallel.includes('Research begins only after Step 2 diagnosis')
2736
+ || parallel.includes('scout + diagnose + research together')
2737
+ || parallel.includes("You don't need to wait for scouting")) {
2738
+ issues.add('scout-before-diagnosis');
2739
+ }
2740
+ return [...issues].sort();
2741
+ }
2742
+
2743
+ test('Codex installed Fix preserves the adaptive repair contract', () => {
2744
+ inTempProject((root) => {
2745
+ const sourceBytes = new Map(HOTFIX_BUNDLE.map((relative) => [
2746
+ path.join(HOTFIX_SOURCE_ROOT, relative),
2747
+ fs.readFileSync(path.join(HOTFIX_SOURCE_ROOT, relative))
2748
+ ]));
2749
+ const result = install(root);
2750
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
2751
+
2752
+ const installedRoot = path.join(root, '.agents/skills/hotfix');
2753
+ assert.equal(fs.existsSync(path.join(root, '.agents/skills/fix')), false,
2754
+ 'public Fix rename must keep the manifest-owned hotfix directory');
2755
+ for (const relative of HOTFIX_BUNDLE) {
2756
+ const sourcePath = path.join(HOTFIX_SOURCE_ROOT, relative);
2757
+ assert.equal(
2758
+ fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
2759
+ normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
2760
+ `Codex Fix projection drifted: ${relative}`
2761
+ );
2762
+ }
2763
+ const readProjection = () => ({
2764
+ skill: fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8'),
2765
+ review: fs.readFileSync(path.join(installedRoot, 'references/review-cycle.md'), 'utf8'),
2766
+ parallel: fs.readFileSync(path.join(installedRoot, 'references/parallel-patterns.md'), 'utf8'),
2767
+ specialized: fs.readFileSync(path.join(installedRoot, 'references/workflow-specialized.md'), 'utf8'),
2768
+ });
2769
+ const installedSkill = readProjection().skill;
2770
+ assert.match(installedSkill, /name: cf-fix/);
2771
+ assert.doesNotMatch(installedSkill, /name: cf-hotfix|\/cf:fix|cf:debug|cf:code-review/);
2772
+ assert.deepEqual(hotfixProjectionIssues(readProjection()), []);
2773
+
2774
+ const mutations = [
2775
+ {
2776
+ name: 'public-name-reverts', file: 'SKILL.md',
2777
+ from: 'name: cf-fix', to: 'name: cf-hotfix',
2778
+ expected: ['public-rename']
2779
+ },
2780
+ {
2781
+ name: 'quick-skips-diagnosis', file: 'SKILL.md',
2782
+ from: 'Quick mode only reduces depth', to: 'Quick mode may shorten scope',
2783
+ expected: ['adaptive-depth']
2784
+ },
2785
+ {
2786
+ name: 'handoff-drops-skip-forms', file: 'SKILL.md',
2787
+ from: '`Timeline: skipped - <reason>` or `- skipped: <reason>`', to: '`Timeline: omitted`',
2788
+ expected: ['debug-handoff']
2789
+ },
2790
+ {
2791
+ name: 'warnings-auto-accept', file: 'references/review-cycle.md',
2792
+ from: 'Only `FAIL` and `PASS_WITH_WARNINGS` enter remediation retry',
2793
+ to: 'Only `FAIL` enters remediation retry; `PASS_WITH_WARNINGS` auto-approves',
2794
+ expected: ['verdict-surface']
2795
+ },
2796
+ {
2797
+ name: 'warnings-user-approve', file: 'references/review-cycle.md',
2798
+ from: 'only a fresh literal `PASS` enters finalization', to: '"Approve anyway" enters finalization',
2799
+ expected: ['verdict-surface']
2800
+ },
2801
+ {
2802
+ name: 'pass-redefined-locally', file: 'references/review-cycle.md',
2803
+ from: 'The definition of `PASS` defers to `cf-code-review`; Fix never redefines it with local severity thresholds.',
2804
+ to: 'The definition of `PASS` is local: no Critical, no High, at most one Medium.',
2805
+ expected: ['verdict-surface']
2806
+ },
2807
+ {
2808
+ name: 'confidence-score-returns', file: 'SKILL.md',
2809
+ from: '**Report:** root cause, changes made',
2810
+ to: '**Report:** Confidence score, root cause, changes made',
2811
+ expected: ['verdict-surface']
2812
+ },
2813
+ {
2814
+ name: 'delegation-loses-user-clause', file: 'SKILL.md',
2815
+ from: 'The user explicitly requested or permitted delegation or parallel agents.',
2816
+ to: 'Delegation is at the agent\'s discretion.',
2817
+ expected: ['delegation-gate']
2818
+ },
2819
+ {
2820
+ name: 'sweep-permits-silent-patch', file: 'SKILL.md',
2821
+ from: 'Do not silently patch around the regression.', to: 'Patch around regressions quietly.',
2822
+ expected: ['side-effect-gate']
2823
+ },
2824
+ {
2825
+ name: 'quick-gains-mandatory-frame', file: 'SKILL.md',
2826
+ from: 'Quick/local does not add a separate framing ceremony', to: 'Quick/local always requires a separate framing ceremony',
2827
+ expected: ['bounded-repair-frame']
2828
+ },
2829
+ {
2830
+ name: 'deep-researches-before-diagnosis', file: 'SKILL.md',
2831
+ from: 'after diagnosis, research only unresolved external facts', to: 'research broadly before diagnosis',
2832
+ expected: ['deep-decision-route']
2833
+ },
2834
+ {
2835
+ name: 'overlays-load-everything', file: 'references/workflow-specialized.md',
2836
+ from: 'Load only the matching section', to: 'Load every section',
2837
+ expected: ['specialized-proof-overlays']
2838
+ },
2839
+ {
2840
+ name: 'diagnosis-starts-before-scout', file: 'references/parallel-patterns.md',
2841
+ from: 'Diagnosis still starts only', to: 'Diagnosis may start',
2842
+ expected: ['scout-before-diagnosis']
2843
+ },
2844
+ {
2845
+ name: 'research-starts-before-diagnosis', file: 'references/parallel-patterns.md',
2846
+ from: 'Research begins only after Step 2 diagnosis', to: 'Research may begin before Step 2 diagnosis',
2847
+ expected: ['scout-before-diagnosis']
2848
+ }
2849
+ ];
2850
+ for (const mutation of mutations) {
2851
+ const target = path.join(installedRoot, mutation.file);
2852
+ const original = fs.readFileSync(target);
2853
+ const content = original.toString('utf8');
2854
+ const anchor = content.indexOf(mutation.from);
2855
+ assert.ok(anchor >= 0, `${mutation.name} mutation anchor must exist`);
2856
+ assert.equal(
2857
+ content.indexOf(mutation.from, anchor + mutation.from.length),
2858
+ -1,
2859
+ `${mutation.name} mutation anchor must be unique`
2860
+ );
2861
+ const weakened = `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`;
2862
+ try {
2863
+ fs.writeFileSync(target, weakened);
2864
+ assert.deepEqual(hotfixProjectionIssues(readProjection()), mutation.expected, mutation.name);
2865
+ } finally {
2866
+ fs.writeFileSync(target, original);
2867
+ }
2868
+ assert.deepEqual(fs.readFileSync(target), original, `${mutation.name} byte restore`);
2869
+ assert.deepEqual(hotfixProjectionIssues(readProjection()), []);
2870
+ }
2871
+ for (const [sourcePath, expected] of sourceBytes) {
2872
+ assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
2873
+ }
2874
+ });
2875
+ });
2876
+
2877
+ function docsProjectionIssues(files) {
2878
+ const compact = (value) => String(value).replace(/\s+/g, ' ').trim();
2879
+ const skill = compact(files.skill);
2880
+ const init = compact(files.init);
2881
+ const update = compact(files.update);
2882
+ const standard = compact(files.standard);
2883
+ const issues = new Set();
2884
+ if (!skill.includes('The user explicitly requested or permitted delegation or parallel agents.')
2885
+ || !skill.includes('The active runtime exposes an Explore/delegation capability.')
2886
+ || !skill.includes('at least two distinct, non-overlapping scopes')
2887
+ || !skill.includes('Otherwise continue sequentially in the main agent')
2888
+ || !skill.includes('only through the Delegation Gate above')
2889
+ || !init.includes('only through the Delegation Gate in `../SKILL.md`')
2890
+ || !update.includes('only through the Delegation Gate in `../SKILL.md`')
2891
+ || skill.includes('when delegation is available')
2892
+ || init.includes('when delegation is available')
2893
+ || update.includes('when delegation is available')) {
2894
+ issues.add('delegation-gate');
2895
+ }
2896
+ if (!skill.includes('## Post-Task Docs Checkpoint')
2897
+ || !skill.includes('`none`: report that no docs change is needed and edit nothing.')
2898
+ || !skill.includes('A checkpoint never invents a new document')
2899
+ || !skill.includes('never auto-selects `init` and overrides')
2900
+ || !skill.includes('report the gap and recommend an explicit')
2901
+ || !standard.includes('checkpoint contract in `../SKILL.md`')
2902
+ || skill.includes('checkpoint may create')) {
2903
+ issues.add('docs-checkpoint');
2904
+ }
2905
+ if (!skill.includes('Type: Observed | Inferred | Unknown')
2906
+ || !skill.includes('Confidence: High | Medium | Low')
2907
+ || skill.includes('evidence is optional')) {
2908
+ issues.add('evidence-taxonomy');
2909
+ }
2910
+ if (!skill.includes('## Reconstruction Is Not Specs')
2911
+ || skill.includes('are advisory')) {
2912
+ issues.add('reconstruction-boundary');
2913
+ }
2914
+ return [...issues].sort();
2915
+ }
2916
+
2917
+ test('Codex installed Docs preserves the adaptive contract', () => {
2918
+ inTempProject((root) => {
2919
+ const sourceBytes = new Map(DOCS_ADAPTIVE_BUNDLE.map((relative) => [
2920
+ path.join(DOCS_ADAPTIVE_SOURCE_ROOT, relative),
2921
+ fs.readFileSync(path.join(DOCS_ADAPTIVE_SOURCE_ROOT, relative))
2922
+ ]));
2923
+ const result = install(root, ['--with-document-skills']);
2924
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
2925
+
2926
+ const installedRoot = path.join(root, '.agents/skills/docs');
2927
+ for (const relative of DOCS_ADAPTIVE_BUNDLE) {
2928
+ const sourcePath = path.join(DOCS_ADAPTIVE_SOURCE_ROOT, relative);
2929
+ assert.equal(
2930
+ fs.readFileSync(path.join(installedRoot, relative), 'utf8'),
2931
+ normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath),
2932
+ `Codex Docs projection drifted: ${relative}`
2933
+ );
2934
+ }
2935
+ const readProjection = () => ({
2936
+ skill: fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8'),
2937
+ init: fs.readFileSync(path.join(installedRoot, 'references/init-workflow.md'), 'utf8'),
2938
+ update: fs.readFileSync(path.join(installedRoot, 'references/update-workflow.md'), 'utf8'),
2939
+ standard: fs.readFileSync(path.join(installedRoot, 'references/standard-docs-workflow.md'), 'utf8'),
2940
+ });
2941
+ assert.match(readProjection().skill, /name: cf-docs/);
2942
+ assert.deepEqual(docsProjectionIssues(readProjection()), []);
2943
+
2944
+ const mutations = [
2945
+ {
2946
+ name: 'gate-loses-user-clause', file: 'SKILL.md',
2947
+ from: 'The user explicitly requested or permitted delegation or parallel agents.',
2948
+ to: 'Delegation is at the agent\'s discretion.',
2949
+ expected: ['delegation-gate']
2950
+ },
2951
+ {
2952
+ name: 'keeper-ungated-returns', file: 'SKILL.md',
2953
+ from: 'only through the Delegation Gate above', to: 'when delegation is available',
2954
+ expected: ['delegation-gate']
2955
+ },
2956
+ {
2957
+ name: 'checkpoint-invents-doc', file: 'SKILL.md',
2958
+ from: 'A checkpoint never invents a new document', to: 'A checkpoint may create missing documents',
2959
+ expected: ['docs-checkpoint']
2960
+ },
2961
+ {
2962
+ name: 'taxonomy-drops-inferred', file: 'SKILL.md',
2963
+ from: 'Type: Observed | Inferred | Unknown', to: 'Type: Observed | Unknown',
2964
+ expected: ['evidence-taxonomy']
2965
+ },
2966
+ {
2967
+ name: 'reconstruction-becomes-advisory', file: 'SKILL.md',
2968
+ from: '## Reconstruction Is Not Specs',
2969
+ to: '## Reconstruction Is Not Specs\n\nThe prohibitions below are advisory.',
2970
+ expected: ['reconstruction-boundary']
2971
+ }
2972
+ ];
2973
+ for (const mutation of mutations) {
2974
+ const target = path.join(installedRoot, mutation.file);
2975
+ const original = fs.readFileSync(target);
2976
+ const content = original.toString('utf8');
2977
+ const anchor = content.indexOf(mutation.from);
2978
+ assert.ok(anchor >= 0, `${mutation.name} mutation anchor must exist`);
2979
+ assert.equal(
2980
+ content.indexOf(mutation.from, anchor + mutation.from.length),
2981
+ -1,
2982
+ `${mutation.name} mutation anchor must be unique`
2983
+ );
2984
+ const weakened = `${content.slice(0, anchor)}${mutation.to}${content.slice(anchor + mutation.from.length)}`;
2985
+ try {
2986
+ fs.writeFileSync(target, weakened);
2987
+ assert.deepEqual(docsProjectionIssues(readProjection()), mutation.expected, mutation.name);
2988
+ } finally {
2989
+ fs.writeFileSync(target, original);
2990
+ }
2991
+ assert.deepEqual(fs.readFileSync(target), original, `${mutation.name} byte restore`);
2992
+ assert.deepEqual(docsProjectionIssues(readProjection()), []);
2993
+ }
2994
+ for (const [sourcePath, expected] of sourceBytes) {
2995
+ assert.deepEqual(fs.readFileSync(sourcePath), expected, `canonical source changed: ${sourcePath}`);
2996
+ }
2997
+ });
2998
+ });
2999
+
876
3000
  test('Codex install on top of existing Claude installation preserves content and adds Codex', () => {
877
3001
  inTempProject((root) => {
878
3002
  // Setup existing Claude installation with content
@@ -961,3 +3085,134 @@ test('Codex scaffold resolver rejects symlink template', () => {
961
3085
  fs.writeFileSync(template, original);
962
3086
  });
963
3087
  });
3088
+
3089
+ test('Claude and Codex installed Route preserve proportional live-catalog semantics', () => {
3090
+ inTempProject((root) => {
3091
+ const result = installPlatforms(root, ['claude', 'codex']);
3092
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
3093
+ const sourceRoot = path.join(PACKAGE_ROOT, 'src/claude/skills/route');
3094
+ const expectedFiles = [
3095
+ 'SKILL.md', 'references/task-taxonomy.md',
3096
+ 'references/chaining-patterns.md', 'references/agent-timing.md'
3097
+ ];
3098
+ const expectedRules = ['skill-workflow-routing.md', 'skill-domain-routing.md'];
3099
+ for (const runtime of ['claude', 'codex']) {
3100
+ const installedRoot = path.join(root, runtime === 'codex' ? '.agents/skills/route' : '.claude/skills/route');
3101
+ for (const relative of expectedFiles) {
3102
+ const source = fs.readFileSync(path.join(sourceRoot, relative), 'utf8');
3103
+ const installed = fs.readFileSync(path.join(installedRoot, relative), 'utf8');
3104
+ const expected = runtime === 'codex'
3105
+ ? normalizeCodexBody(source, path.join(sourceRoot, relative))
3106
+ : source;
3107
+ assert.equal(installed, expected, `${runtime}:${relative}`);
3108
+ }
3109
+ for (const relative of expectedRules) {
3110
+ const sourcePath = path.join(PACKAGE_ROOT, 'src/claude/rules', relative);
3111
+ const source = fs.readFileSync(sourcePath, 'utf8');
3112
+ const installed = fs.readFileSync(path.join(
3113
+ root, runtime === 'codex' ? '.codex/rules' : '.claude/rules', relative
3114
+ ), 'utf8');
3115
+ const expected = runtime === 'codex' ? normalizeCodexBody(source, sourcePath) : source;
3116
+ assert.equal(installed, expected, `${runtime}:rules/${relative}`);
3117
+ }
3118
+ const route = fs.readFileSync(path.join(installedRoot, 'SKILL.md'), 'utf8');
3119
+ assert.match(route, /names a valid installed skill[\s\S]*one installed skill clearly covers[\s\S]*Direct factual conversation/);
3120
+ assert.match(route, /highest-link[\s\S]*risk[\s\S]*number of material domains/);
3121
+ assert.match(route, /never expand it/);
3122
+ const catalog = installedCatalog(root, runtime);
3123
+ assert.ok(catalog.skills.some((skill) => skill.public_id === 'cf:route'));
3124
+ assert.equal(catalog.skills.some((skill) => skill.public_id === 'cf:docs'), false);
3125
+ }
3126
+ });
3127
+ });
3128
+
3129
+ test('combined installs bind each catalog to its native runtime inventory', () => {
3130
+ inTempProject((root) => {
3131
+ const result = installPlatforms(root, ['claude', 'codex']);
3132
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
3133
+ fs.cpSync(
3134
+ path.join(PACKAGE_ROOT, 'src/claude/skills/docs'),
3135
+ path.join(root, '.claude/skills/docs'),
3136
+ { recursive: true }
3137
+ );
3138
+ const claude = installedCatalog(root, 'claude');
3139
+ const codex = installedCatalog(root, 'codex');
3140
+ assert.equal(claude.root, fs.realpathSync(path.join(root, '.claude/skills')));
3141
+ assert.equal(codex.root, fs.realpathSync(path.join(root, '.agents/skills')));
3142
+ assert.equal(claude.skills.some((skill) => skill.public_id === 'cf:docs'), true);
3143
+ assert.equal(codex.skills.some((skill) => skill.public_id === 'cf:docs'), false);
3144
+ });
3145
+ });
3146
+
3147
+ test('Claude and Codex installed rules preserve the ported review and process guidance', () => {
3148
+ inTempProject((root) => {
3149
+ const result = installPlatforms(root, ['claude', 'codex']);
3150
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
3151
+ const ported = ['review-audit-self-decision.md', 'process-management.md'];
3152
+ const renameOnly = (content) => content
3153
+ .replace(/\/cf:([a-z0-9-]+)/gi, (_match, name) => `$cf-${name}`)
3154
+ .replace(/\bcf:([a-z0-9-]+)/gi, (_match, name) => `cf-${name}`);
3155
+ for (const relative of ported) {
3156
+ const sourcePath = path.join(PACKAGE_ROOT, 'src/claude/rules', relative);
3157
+ const source = fs.readFileSync(sourcePath, 'utf8');
3158
+ assert.equal(
3159
+ fs.existsSync(path.join(PACKAGE_ROOT, 'src/codex/rules', relative)),
3160
+ false,
3161
+ `${relative} must not be shadowed by a Codex native override`
3162
+ );
3163
+ assert.equal(
3164
+ fs.readFileSync(path.join(root, '.claude/rules', relative), 'utf8'),
3165
+ source,
3166
+ `claude:rules/${relative}`
3167
+ );
3168
+ const installedCodex = fs.readFileSync(path.join(root, '.codex/rules', relative), 'utf8');
3169
+ assert.equal(installedCodex, normalizeCodexBody(source, sourcePath), `codex:rules/${relative}`);
3170
+ assert.equal(
3171
+ installedCodex,
3172
+ renameOnly(source),
3173
+ `codex:rules/${relative} must differ from source only by skill-name renames`
3174
+ );
3175
+ }
3176
+ const agentsMd = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
3177
+ const pointerLines = agentsMd
3178
+ .split('\n')
3179
+ .filter((line) => ported.some((relative) => line.includes(relative)));
3180
+ assert.equal(pointerLines.length, ported.length, 'AGENTS.md must point at both ported rules');
3181
+ for (const relative of ported) {
3182
+ assert.ok(
3183
+ pointerLines.some((line) => line.includes(`.codex/rules/${relative}`)),
3184
+ `AGENTS.md must name .codex/rules/${relative}`
3185
+ );
3186
+ }
3187
+ const pointerText = pointerLines.join('\n');
3188
+ for (const forbidden of ['cf:', 'Claude Code', 'Claude Tasks', 'SendMessage', '.claude/']) {
3189
+ assert.equal(
3190
+ pointerText.includes(forbidden),
3191
+ false,
3192
+ `AGENTS.md pointer must stay Codex-native: ${forbidden}`
3193
+ );
3194
+ }
3195
+ });
3196
+ });
3197
+
3198
+ test('installed Route degrades safely when an agent is absent', () => {
3199
+ inTempProject((root) => {
3200
+ const result = installPlatforms(root, ['claude', 'codex']);
3201
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
3202
+ const cases = [
3203
+ ['claude', '.claude/agents/researcher.md', '.claude/skills/route'],
3204
+ ['codex', '.codex/agents/researcher.toml', '.agents/skills/route'],
3205
+ ];
3206
+ for (const [runtime, agentRelative, routeRelative] of cases) {
3207
+ fs.rmSync(path.join(root, agentRelative), { force: true });
3208
+ assert.equal(fs.existsSync(path.join(root, agentRelative)), false);
3209
+ const timing = fs.readFileSync(path.join(root, routeRelative, 'references/agent-timing.md'), 'utf8');
3210
+ const route = fs.readFileSync(path.join(root, routeRelative, 'SKILL.md'), 'utf8');
3211
+ const normalizedRoute = route.replace(/\s+/g, ' ');
3212
+ assert.match(timing, /If the preferred agent is absent[\s\S]*never synthesize a role/);
3213
+ assert.match(normalizedRoute, /continue inline when safe or return the named gap/);
3214
+ assert.match(normalizedRoute, /Never synthesize an unavailable agent/);
3215
+ assert.doesNotMatch(`${timing}\n${route}`, /researcher(?:\.md|\.toml)/i, runtime);
3216
+ }
3217
+ });
3218
+ });