@haposoft/cafekit 0.16.6 → 0.18.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 (194) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +0 -36
  4. package/bin/__tests__/codex-native.test.js +46 -88
  5. package/bin/__tests__/develop-contract.test.js +26 -57
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +159 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +100 -541
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +0 -27
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +4 -69
  21. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  22. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  23. package/bin/__tests__/validator-grounding.test.js +9 -28
  24. package/bin/phases/codex-runtime.js +24 -0
  25. package/bin/phases/omp-runtime.js +22 -0
  26. package/package.json +1 -1
  27. package/src/claude/CLAUDE.md +3 -10
  28. package/src/claude/agents/brainstormer.md +7 -3
  29. package/src/claude/agents/code-auditor.md +35 -50
  30. package/src/claude/agents/debugger.md +5 -79
  31. package/src/claude/agents/deployer.md +3 -5
  32. package/src/claude/agents/docs-keeper.md +2 -7
  33. package/src/claude/agents/implementer.md +6 -9
  34. package/src/claude/agents/inspector.md +2 -3
  35. package/src/claude/agents/project-manager.md +7 -15
  36. package/src/claude/agents/researcher.md +9 -3
  37. package/src/claude/agents/spec-maker.md +11 -11
  38. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  39. package/src/claude/hooks/__tests__/spec-gate.test.js +112 -3
  40. package/src/claude/hooks/__tests__/spec-state-touched.test.js +134 -0
  41. package/src/claude/hooks/__tests__/state.test.js +68 -0
  42. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  43. package/src/claude/hooks/agent.cjs +2 -1
  44. package/src/claude/hooks/lib/context.cjs +0 -89
  45. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  46. package/src/claude/hooks/rules.cjs +2 -1
  47. package/src/claude/hooks/session.cjs +0 -1
  48. package/src/claude/hooks/spec-gate.cjs +18 -28
  49. package/src/claude/hooks/spec-state.cjs +95 -1
  50. package/src/claude/hooks/state.cjs +21 -2
  51. package/src/claude/migration-manifest.json +30 -15
  52. package/src/claude/rules/ai-dev-rules.md +1 -1
  53. package/src/claude/rules/hook-protocols.md +8 -15
  54. package/src/claude/rules/manage-docs.md +9 -23
  55. package/src/claude/rules/orchestrator.md +3 -4
  56. package/src/claude/rules/review-audit-self-decision.md +3 -3
  57. package/src/claude/rules/skill-domain-routing.md +1 -3
  58. package/src/claude/rules/skill-workflow-routing.md +4 -5
  59. package/src/claude/rules/state-sync.md +3 -12
  60. package/src/claude/rules/workflow.md +10 -18
  61. package/src/claude/runtime.schema.json +5 -3
  62. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  63. package/src/claude/scripts/provenance.cjs +23 -5
  64. package/src/claude/scripts/spec-receipt.cjs +52 -6
  65. package/src/claude/scripts/spec-resolver.cjs +1 -2
  66. package/src/claude/scripts/validate-spec-output.cjs +5 -32
  67. package/src/claude/scripts/workflow-policy.cjs +22 -10
  68. package/src/claude/settings/settings.json +1 -32
  69. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  70. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  71. package/src/claude/skills/code-review/SKILL.md +48 -17
  72. package/src/claude/skills/code-review/references/spec-compliance-review.md +2 -3
  73. package/src/claude/skills/code-review/references/verification-gate.md +22 -27
  74. package/src/claude/skills/debug/SKILL.md +29 -149
  75. package/src/claude/skills/develop/SKILL.md +27 -36
  76. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  77. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  78. package/src/claude/skills/docs/SKILL.md +2 -2
  79. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  80. package/src/claude/skills/fix/SKILL.md +153 -0
  81. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  82. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  83. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  84. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  85. package/src/claude/skills/git/SKILL.md +48 -9
  86. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  87. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  88. package/src/claude/skills/orca/SKILL.md +9 -8
  89. package/src/claude/skills/research/SKILL.md +2 -0
  90. package/src/claude/skills/scout/SKILL.md +61 -0
  91. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  92. package/src/claude/skills/specs/SKILL.md +22 -29
  93. package/src/claude/skills/specs/references/review.md +8 -6
  94. package/src/claude/skills/specs/references/templates.md +38 -44
  95. package/src/claude/skills/sync/SKILL.md +8 -15
  96. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  97. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  98. package/src/claude/skills/test/SKILL.md +22 -3
  99. package/src/claude/skills/test/references/execution-strategy.md +83 -0
  100. package/src/claude/skills/test/references/failure-triage.md +1 -1
  101. package/src/claude/skills/ui-ux/LICENSE +21 -0
  102. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  103. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  104. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  105. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  106. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  107. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  108. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  109. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  110. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  111. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  112. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  113. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  114. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  115. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  116. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  117. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  118. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  119. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  120. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  121. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  122. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  123. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  124. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  125. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  126. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  127. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  128. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  129. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  130. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  131. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  132. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  133. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  134. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  135. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  136. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  137. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  138. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  139. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  140. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  141. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  142. package/src/claude/skills/ui-ux/references/review.md +528 -0
  143. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  144. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  145. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  146. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  147. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  148. package/src/claude/skills/ui-ux/references/system.md +219 -0
  149. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  150. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  151. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  152. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  153. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  154. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  155. package/src/claude/status.cjs +149 -144
  156. package/src/codex/AGENTS.md +8 -13
  157. package/src/codex/hooks/agent.cjs +2 -1
  158. package/src/codex/hooks/lib/spec-receipt.cjs +5 -0
  159. package/src/codex/hooks/lib/spec-utils.cjs +0 -7
  160. package/src/codex/hooks/rules.cjs +2 -1
  161. package/src/codex/hooks/session.cjs +0 -2
  162. package/src/codex/hooks/spec-gate.cjs +8 -18
  163. package/src/codex/hooks.json +0 -20
  164. package/src/codex/rules/state-sync.md +3 -12
  165. package/src/common/AGENTS.md +2 -2
  166. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  167. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  168. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  169. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  170. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  171. package/src/claude/hooks/completion-authority.cjs +0 -163
  172. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  173. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  174. package/src/claude/hooks/usage.cjs +0 -197
  175. package/src/claude/skills/delegate/SKILL.md +0 -124
  176. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  177. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  178. package/src/claude/skills/hotfix/SKILL.md +0 -289
  179. package/src/claude/skills/inspect/SKILL.md +0 -142
  180. package/src/claude/skills/loop/SKILL.md +0 -114
  181. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  182. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  183. package/src/claude/skills/route/SKILL.md +0 -94
  184. package/src/claude/skills/route/references/agent-timing.md +0 -46
  185. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  186. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  187. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  188. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  189. package/src/codex/hooks/completion-authority.cjs +0 -109
  190. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  191. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  192. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  193. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  194. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -143,9 +143,6 @@ test('advice names the directory the hook lives in', () => {
143
143
  const rt = installTreeAs(project, folder);
144
144
  fs.mkdirSync(path.join(project, 'specs', 'demo'), { recursive: true });
145
145
  fs.writeFileSync(path.join(rt, 'runtime.json'), '{}');
146
- // task-scaffold-guard: a raw Write into a nested task path is refused with scaffold advice
147
- const guard = runHook(rt, 'task-scaffold-guard.cjs', { tool_name: 'Write', tool_input: { file_path: path.join(project, 'specs', 'demo', 'tasks', 'task-R0-01-x.md'), content: '# x' }, cwd: project });
148
- assert.match(guard.out, new RegExp(`node ${folder.replace('.', '\\.')}/scripts/spec-scaffold\\.cjs`), `scaffold advice must name ${folder}`);
149
146
  // inspect-block: header hint names the runtime file (static check on the installed copy)
150
147
  const src = fs.readFileSync(path.join(rt, 'hooks', 'inspect-block.cjs'), 'utf8');
151
148
  assert.match(src, /<runtime>\/runtime\.json/, 'the disable hint no longer hardcodes a platform');
@@ -86,8 +86,9 @@ test('every documented property carries a description a reader can act on', () =
86
86
  test('keys the runtime does not honour are marked deprecated, not documented as working', () => {
87
87
  // develop.parallel and the hooks toggle map are still shipped for compatibility, but
88
88
  // nothing reads them. The schema must say so rather than imply they work.
89
- for (const dead of ['hooks', 'develop']) {
90
- const node = SCHEMA.properties[dead];
89
+ for (const dead of ['hooks', 'develop', 'usage', 'spec.scaffold_guard']) {
90
+ // A dotted path walks nested `properties`: `spec.scaffold_guard` is spec.properties.scaffold_guard.
91
+ const node = dead.split('.').reduce((schema, key) => schema && schema.properties && schema.properties[key], SCHEMA);
91
92
  assert.ok(node, `${dead} is shipped in runtime.json and must appear in the schema`);
92
93
  assert.equal(node.deprecated, true, `${dead} is not honoured and must be marked deprecated`);
93
94
  assert.match(node.description, /[Nn]ot honoured/, `${dead} must say plainly that it does nothing`);
@@ -58,7 +58,7 @@ test('skill routing consumes live catalog without fixed optional commands', () =
58
58
  installScanner(SCANNER, claudeScript);
59
59
  installScanner(SCANNER, codexScript, (content) => normalizeCodexBody(content, SCANNER));
60
60
 
61
- writeSkill(path.join(root, '.claude/skills'), 'question', {
61
+ writeSkill(path.join(root, '.claude/skills'), 'ask', {
62
62
  name: 'cf:ask', description: 'Answer with evidence.', when: 'Use for factual questions.',
63
63
  category: 'utilities', keywords: ['answer', 'evidence'],
64
64
  });
@@ -66,7 +66,7 @@ test('skill routing consumes live catalog without fixed optional commands', () =
66
66
  name: 'cf:docs', description: 'Work with docs.', when: 'Use for documentation.',
67
67
  category: 'documents', keywords: ['docs'],
68
68
  });
69
- writeSkill(path.join(root, '.agents/skills'), 'question', {
69
+ writeSkill(path.join(root, '.agents/skills'), 'ask', {
70
70
  name: 'cf-ask', description: 'Answer with evidence.', when: 'Use for factual questions.',
71
71
  category: 'utilities', keywords: ['answer', 'evidence'],
72
72
  });
@@ -84,8 +84,8 @@ test('skill routing consumes live catalog without fixed optional commands', () =
84
84
  const domain = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/skill-domain-routing.md'), 'utf8');
85
85
  const codexEntry = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/codex/AGENTS.md'), 'utf8');
86
86
  assert.match(workflow, /user names a valid installed skill[\s\S]*one obvious low-risk[\s\S]*direct factual conversation/);
87
- assert.match(workflow, /do not invoke Route or agents for ceremony/);
88
- assert.match(workflow.replace(/\s+/g, ' '), /numeric optimization capability remains explicit-only/);
87
+ assert.match(workflow, /do not spawn agents for ceremony/);
88
+ assert.doesNotMatch(`${workflow}\n${domain}`, /numeric optimization capability/);
89
89
  assert.match(domain, /examples below are intent hints, not a copied installed inventory/);
90
90
  assert.match(domain, /document\/artifact work \| use only a matching installed optional capability/);
91
91
  assert.doesNotMatch(domain, /\/cf:(?:docs|docx|pdf|pptx|xlsx|ai-multimodal)/);
@@ -96,17 +96,19 @@ test('skill routing consumes live catalog without fixed optional commands', () =
96
96
  test('skill catalog exposes discriminating routing metadata', () => {
97
97
  withTempRoot((root) => {
98
98
  const skills = path.join(root, 'skills');
99
- writeSkill(skills, 'route', {
100
- name: 'cf:route', description: 'Choose a bounded chain.',
99
+ writeSkill(skills, 'triage', {
100
+ name: 'cf:triage', description: 'Choose a bounded chain.',
101
101
  when: 'Use for ambiguous multi-step work.', category: 'utilities',
102
102
  keywords: ['routing', 'risk'],
103
103
  });
104
+ // An upgrade can leave a user-modified retired directory beside its replacement; both carry the
105
+ // same public name, and only the retired one may drop out.
104
106
  writeSkill(skills, 'question', {
105
- name: 'cf:ask', description: 'First duplicate.', when: 'Use for questions.',
107
+ name: 'cf:ask', description: 'User-modified retired copy.', when: 'Use for questions.',
106
108
  category: 'utilities', keywords: ['answer'],
107
109
  });
108
110
  writeSkill(skills, 'ask', {
109
- name: 'cf:ask', description: 'Second duplicate.', when: 'Use for questions.',
111
+ name: 'cf:ask', description: 'Answer with evidence.', when: 'Use for questions.',
110
112
  category: 'utilities', keywords: ['answer'],
111
113
  });
112
114
  writeSkill(skills, 'missing-description', { name: 'cf:missing-description' });
@@ -132,15 +134,16 @@ test('skill catalog exposes discriminating routing metadata', () => {
132
134
  });
133
135
 
134
136
  const catalog = runCatalog(SCANNER, root, ['--json', '--root', skills]);
135
- assert.deepEqual(catalog.skills.map((skill) => skill.public_id), ['cf:route']);
136
- assert.deepEqual(catalog.skills[0], {
137
- name: 'cf:route', public_id: 'cf:route', directory: 'route',
137
+ assert.deepEqual(catalog.skills.map((skill) => skill.public_id), ['cf:ask', 'cf:triage']);
138
+ assert.deepEqual(catalog.skills.find((skill) => skill.public_id === 'cf:triage'), {
139
+ name: 'cf:triage', public_id: 'cf:triage', directory: 'triage',
138
140
  description: 'Choose a bounded chain.', when_to_use: 'Use for ambiguous multi-step work.',
139
141
  category: 'utilities', keywords: ['routing', 'risk'], user_invocable: true,
140
142
  has_references: false, has_scripts: false,
141
143
  });
142
144
  const codes = catalog.diagnostics.map((item) => item.code);
143
- assert.equal(codes.filter((code) => code === 'duplicate_public_name').length, 2);
145
+ assert.equal(codes.filter((code) => code === 'duplicate_public_name').length, 0);
146
+ assert.ok(catalog.diagnostics.some((item) => item.directory === 'question' && item.code === 'retired_skill'));
144
147
  for (const required of [
145
148
  'folder_name_mismatch', 'malformed_frontmatter', 'missing_routing_metadata', 'retired_skill',
146
149
  ]) assert.ok(codes.includes(required), required);
@@ -157,10 +160,19 @@ test('skill catalog exposes discriminating routing metadata', () => {
157
160
  const sourceCatalog = runCatalog(SCANNER, PACKAGE_ROOT);
158
161
  const retiredDiagnostics = sourceCatalog.diagnostics
159
162
  .filter((item) => item.code === 'retired_skill').map((item) => item.directory).sort();
160
- assert.deepEqual(retiredDiagnostics, [...MANIFEST.obsolete.skills].sort());
163
+ // Retired skills kept in the source tree must be flagged; renamed ones were moved, so they are
164
+ // absent from the source and only matter to an upgrade over an older install.
165
+ const sourceSkills = path.join(PACKAGE_ROOT, 'src/claude/skills');
166
+ const keptRetired = MANIFEST.obsolete.skills.filter((name) => fs.existsSync(path.join(sourceSkills, name)));
167
+ assert.deepEqual(retiredDiagnostics, [...keptRetired].sort());
168
+ for (const renamed of ['inspect', 'hotfix', 'question']) {
169
+ assert.ok(MANIFEST.obsolete.skills.includes(renamed), renamed);
170
+ assert.equal(fs.existsSync(path.join(sourceSkills, renamed)), false, renamed);
171
+ }
161
172
  assert.equal(sourceCatalog.skills.some((skill) => MANIFEST.obsolete.skills.includes(skill.directory)), false);
162
- const route = sourceCatalog.skills.find((skill) => skill.public_id === 'cf:route');
163
- assert.ok(route?.description && route.when_to_use && route.category && route.keywords.length > 0);
173
+ assert.equal(sourceCatalog.skills.some((skill) => skill.public_id === 'cf:route'), false);
174
+ const ask = sourceCatalog.skills.find((skill) => skill.public_id === 'cf:ask');
175
+ assert.ok(ask?.description && ask.when_to_use && ask.category && ask.keywords.length > 0);
164
176
  });
165
177
 
166
178
  test('skill catalog rejects a missing root value before path resolution', () => {
@@ -188,21 +188,6 @@ test('several packets claiming closeout stay ambiguous and name only those', ()
188
188
  });
189
189
  });
190
190
 
191
- test('codex resolveCandidate agrees with the shared resolver', () => {
192
- // Codex kept its own candidate count, reachable because spec-utils did not export the
193
- // shared function. A Claude-only change would leave every Codex turn blocked.
194
- const codexCheck = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/completion-authority-check.cjs'));
195
- const utils = require(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-utils.cjs'));
196
- assert.equal(typeof utils.resolvePersistedSpec, 'function', 'the wrapper must be exported');
197
- withProject((root) => {
198
- legacyPacket(root, 'shipped', { status: 'in_progress', phase: 'execution', taskStatus: 'done' });
199
- legacyPacket(root, 'closing', { status: 'done', phase: 'closeout' });
200
- const shared = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
201
- const viaCodex = codexCheck.resolveCandidate({ projectRoot: root, runtime: {}, payload: {} });
202
- assert.equal(viaCodex.featureName, shared.featureName);
203
- });
204
- });
205
-
206
191
  // ── The recorded active feature: the escape the block told users to take ────────────
207
192
 
208
193
  test('a recorded active feature resolves two packets claiming closeout', () => {
@@ -281,18 +266,6 @@ test('an absent file leaves every project exactly as it was', () => {
281
266
  });
282
267
  });
283
268
 
284
- test('the approval prompt names its feature', () => {
285
- // Without the name a user types a bare nonce and cannot tell which closeout they are
286
- // approving, while the recorded file can influence which packet resolves.
287
- for (const runtimeDir of ['claude', 'codex']) {
288
- const hook = fs.readFileSync(
289
- path.join(PACKAGE_ROOT, 'src', runtimeDir, 'hooks', 'completion-authority.cjs'), 'utf8'
290
- );
291
- assert.match(hook, /const feature = result\.candidate\?\.featureName;/, `${runtimeDir} must read the name`);
292
- assert.match(hook, /closeout approval is required for \$\{feature \?/, `${runtimeDir} must print it`);
293
- }
294
- });
295
-
296
269
  test('the shared workflow resolver ignores the recorded file', () => {
297
270
  // resolveWorkflowCandidate has five consumers, including spec-state.cjs and
298
271
  // precompact.cjs. Reading the file inside it would retarget hooks that never asked,
@@ -18,8 +18,6 @@ const VALIDATOR = path.join(SCRIPTS, 'validate-spec-output.cjs');
18
18
  const AUTHORING_VALIDATION = path.join(SCRIPTS, 'spec-authoring-validation.cjs');
19
19
  const RUNTIME_CLOSURE = require('./test-runtime-dependency-closure.cjs');
20
20
  const SEMANTIC_MODEL = require(path.join(SCRIPTS, 'spec-semantic-model.cjs'));
21
- const CLAUDE_CHECK = require(path.join(ROOT, 'src/claude/hooks/completion-authority-check.cjs'));
22
- const CODEX_CHECK = require(path.join(ROOT, 'src/codex/hooks/completion-authority-check.cjs'));
23
21
  const FEATURE = 'closeout-demo';
24
22
  const TASK = 'tasks/task-R1-01-closeout.md';
25
23
  const TEMP_RUN_ID = String(process.env.CAFEKIT_CLOSEOUT_TEMP_RUN_ID || `${process.pid}-${Date.now()}`)
@@ -267,10 +265,7 @@ function installGate(root, kind) {
267
265
  'hooks/lib/hook-payload.cjs',
268
266
  ]);
269
267
  if (kind === 'claude') {
270
- for (const name of [
271
- 'spec-gate.cjs', 'completion-authority-check.cjs', 'completion-authority-state.cjs',
272
- 'semantic-review-authority.cjs',
273
- ]) fs.copyFileSync(path.join(ROOT, 'src/claude/hooks', name), path.join(runtimeDir, 'hooks', name));
268
+ fs.copyFileSync(path.join(ROOT, 'src/claude/hooks', 'spec-gate.cjs'), path.join(runtimeDir, 'hooks', 'spec-gate.cjs'));
274
269
  } else {
275
270
  fs.cpSync(path.join(ROOT, 'src/codex/hooks'), path.join(runtimeDir, 'hooks'), { recursive: true });
276
271
  }
@@ -339,11 +334,11 @@ test('Claude and Codex gates require task proof at every Stop and feature proof
339
334
  const invalidAliasSpec = JSON.parse(fs.readFileSync(invalidAliasFile, 'utf8'));
340
335
  invalidAliasSpec.status = 'completed';
341
336
  fs.writeFileSync(invalidAliasFile, `${JSON.stringify(invalidAliasSpec, null, 2)}\n`);
342
- assert.match(gate(invalidAlias, kind), /spec status is invalid: completed/, `${kind} rejects legacy aliases in schema 2.1`);
337
+ assert.match(gate(invalidAlias, kind), /Legacy spec\.json closeout is no longer supported/, `${kind} blocks legacy 2.1 closeout with the migration message`);
343
338
  prepare(taskless, { taskless: true, specStatus: 'complete', featureReceipt: true });
344
- assert.equal(gate(taskless, kind), '', `${kind} taskless closeout`);
339
+ assert.match(gate(taskless, kind), /Legacy spec\.json closeout is no longer supported/, `${kind} taskless 2.1 closeout is retired`);
345
340
  fs.appendFileSync(path.join(taskless, 'specs', FEATURE, 'requirements.md'), '\nSemantic mutation after review.\n');
346
- assert.match(gate(taskless, kind), /validation failed|semantic digest|stale/i, `${kind} rejects stale semantics with unchanged execution provenance`);
341
+ assert.match(gate(taskless, kind), /Legacy spec\.json closeout is no longer supported/, `${kind} still blocks a changed 2.1 packet`);
347
342
  prepare(taskBearing, { taskReceipt: false, featureReceipt: false });
348
343
  assert.match(gate(taskBearing, kind), new RegExp(path.basename(TASK)), `${kind} unproven done task before closeout`);
349
344
  prepare(taskBearing, { taskReceipt: true, featureReceipt: false });
@@ -377,21 +372,6 @@ test('an unproven done dependency cannot advance a dependent task', () => {
377
372
  });
378
373
  });
379
374
 
380
- test('completion authority supports taskless v2 and binds task plan plus receipt bytes', () => {
381
- for (const [kind, checker] of [['claude', CLAUDE_CHECK], ['codex', CODEX_CHECK]]) {
382
- withTempResources((resources) => {
383
- const root = gitRoot(resources);
384
- resources.useHome(`${kind}-home`);
385
- prepare(root, { taskless: true, specStatus: 'complete', featureReceipt: true });
386
- const args = { policy: POLICY, projectRoot: root, runtime: {}, payload: { session_id: 'execution-session' } };
387
- if (kind === 'claude') args.resolver = require(path.join(SCRIPTS, 'spec-resolver.cjs'));
388
- const result = checker.evaluateCloseout(args);
389
- assert.equal(result.ok, true, result.reason);
390
- assert.match(result.binding.feature_receipt_digest, /^[a-f0-9]{64}$/);
391
- });
392
- }
393
- });
394
-
395
375
  test('shared taskless terminal predicate requires canonical 2.1 policy and physical tasklessness', () => {
396
376
  withTempResources((resources) => {
397
377
  const root = gitRoot(resources);
@@ -413,51 +393,6 @@ test('shared taskless terminal predicate requires canonical 2.1 policy and physi
413
393
  });
414
394
  });
415
395
 
416
- test('completion authority activates only at durable closeout and honors the Stop loop guard', () => {
417
- for (const [kind, checker] of [['claude', CLAUDE_CHECK], ['codex', CODEX_CHECK]]) {
418
- withTempResources((resources) => {
419
- const root = gitRoot(resources);
420
- resources.useHome(`${kind}-home`);
421
- prepare(root, { featureReceipt: false });
422
- const args = { policy: POLICY, projectRoot: root, runtime: {}, payload: { session_id: 'execution-session' } };
423
- if (kind === 'claude') args.resolver = require(path.join(SCRIPTS, 'spec-resolver.cjs'));
424
- const inactive = checker.evaluateCloseout(args);
425
- assert.equal(inactive.ok, true, inactive.reason);
426
- assert.equal(inactive.active, false);
427
-
428
- args.payload.stop_hook_active = true;
429
- const loop = checker.evaluateCloseout(args);
430
- assert.equal(loop.ok, true, loop.reason);
431
- assert.equal(loop.active, false);
432
-
433
- prepare(root, { specStatus: 'complete', featureReceipt: false });
434
- args.payload.stop_hook_active = false;
435
- const blocked = checker.evaluateCloseout(args);
436
- assert.equal(blocked.ok, false);
437
- assert.match(blocked.reason, /feature-receipt\.md/);
438
- });
439
- }
440
- });
441
-
442
- test('v2 floor merge keeps axes independent, planning controls artifact profile, and risks raise assurance floor', () => {
443
- const policy = {
444
- PLANNING_DEPTHS: ['None', 'Compact', 'Full'], ASSURANCE_LEVELS: ['Routine', 'Elevated', 'Strict'],
445
- compatibilityLane: POLICY.compatibilityLane, planningObligationsFor: POLICY.planningObligationsFor,
446
- obligationsForAssurance: POLICY.obligationsForAssurance, actorNeedsFor: POLICY.actorNeedsFor,
447
- classifyLane: POLICY.classifyLane,
448
- };
449
- const floor = { version: '2', planning_depth: 'Full', automatic_planning_depth: 'Full', assurance_level: 'Routine', automatic_assurance_level: 'Routine', lane: 'Standard', automatic_lane: 'Standard', risks: [], artifact_profile: 'strict', planning_obligations: [], proof_obligations: [], actor_needs: [] };
450
- const current = { ...floor, planning_depth: 'Compact', automatic_planning_depth: 'Compact', risks: ['privacy'], artifact_profile: 'bounded' };
451
- for (const checker of [CLAUDE_CHECK, CODEX_CHECK]) {
452
- const merged = checker.mergePolicyFloor(policy, floor, current);
453
- assert.equal(merged.planning_depth, 'Full');
454
- assert.equal(merged.artifact_profile, 'strict');
455
- assert.equal(merged.automatic_assurance_level, 'Elevated');
456
- assert.equal(merged.assurance_level, 'Elevated');
457
- assert.ok(merged.proof_obligations.includes('needsExecutionProof'));
458
- }
459
- });
460
-
461
396
  test('scaffold creates planning artifacts but no task or feature receipts', () => {
462
397
  withTempResources((resources) => {
463
398
  const root = resources.directory('scaffold-root');
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ // cf:ui-ux is evondevKit's ui-ux skill vendored byte-identical apart from its frontmatter
4
+ // and one body line. A resync copies the upstream tree again, so each case here pins what
5
+ // that copy must not lose, and states the failure it prevents.
6
+
7
+ const assert = require('node:assert/strict');
8
+ const fs = require('node:fs');
9
+ const os = require('node:os');
10
+ const path = require('node:path');
11
+ const { spawnSync } = require('node:child_process');
12
+ const { test } = require('node:test');
13
+
14
+ const { loadClaudeMigrationManifest } = require('../lib/context');
15
+
16
+ const PACKAGE_ROOT = path.join(__dirname, '..', '..');
17
+ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
18
+ const SKILL_DIR = path.join(PACKAGE_ROOT, 'src/claude/skills/ui-ux');
19
+ const SKILL_SOURCE = path.join(SKILL_DIR, 'SKILL.md');
20
+ const PRO_MAX_SOURCE = path.join(PACKAGE_ROOT, 'src/claude/skills/ui-ux-pro-max/SKILL.md');
21
+
22
+ function inTempProject(run) {
23
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-ui-ux-skill-'));
24
+ try {
25
+ const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
26
+ assert.equal(init.status, 0, init.stderr);
27
+ return run(root);
28
+ } finally {
29
+ fs.rmSync(root, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ function install(root, platforms, extra = []) {
34
+ return spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes', ...extra],
35
+ { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
36
+ }
37
+
38
+ function frontmatterLines(content) {
39
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
40
+ assert.ok(match, 'SKILL.md must open with a frontmatter block');
41
+ return match[1].split(/\r?\n/);
42
+ }
43
+
44
+ function field(lines, key) {
45
+ const line = lines.find((entry) => entry.startsWith(`${key}:`));
46
+ return line === undefined ? undefined : line.slice(key.length + 1).trim();
47
+ }
48
+
49
+ test('manifest requires ui-ux', () => {
50
+ // Without the entry the installer never copies the skill, and nothing else fails.
51
+ const required = loadClaudeMigrationManifest().skills.required;
52
+ assert.equal(required.includes('ui-ux'), true);
53
+ assert.equal(required.includes('ui-ux-pro-max'), true, 'the lookup skill stays installed beside it');
54
+ });
55
+
56
+ test('cf:ui-ux frontmatter carries routing metadata and the MIT license', () => {
57
+ // A resync that copies upstream SKILL.md whole brings back `name: ui-ux` (no catalog
58
+ // identity), the Vietnamese description, and `grep -rlE "$1"`, which Claude Code fills
59
+ // with the invocation arguments and so corrupts the style audit.
60
+ const content = fs.readFileSync(SKILL_SOURCE, 'utf8');
61
+ const lines = frontmatterLines(content);
62
+ assert.equal(field(lines, 'name'), 'cf:ui-ux');
63
+ assert.equal(field(lines, 'license'), 'MIT');
64
+ assert.equal(field(lines, 'category'), 'frontend');
65
+ assert.ok(field(lines, 'when_to_use'), 'when_to_use must be set');
66
+ assert.match(field(lines, 'keywords') || '', /^\[.+\]$/, 'keywords must be a non-empty list');
67
+
68
+ const description = field(lines, 'description') || '';
69
+ assert.ok(description.length > 2 && description.length <= 1024, `description length ${description.length}`);
70
+
71
+ assert.doesNotMatch(content, /\$[0-9]/, 'no positional argument placeholder in the skill body');
72
+ assert.doesNotMatch(content, /\$ARGUMENTS/);
73
+
74
+ // MIT requires the copyright and permission notice to travel with the copy.
75
+ assert.equal(fs.existsSync(path.join(SKILL_DIR, 'LICENSE')), true);
76
+ const pkg = JSON.parse(fs.readFileSync(path.join(SKILL_DIR, 'scripts', 'package.json'), 'utf8'));
77
+ assert.ok(pkg.dependencies && pkg.dependencies.playwright, 'the installer provisions Playwright only when declared');
78
+ });
79
+
80
+ test('ui-ux-pro-max when_to_use no longer claims layout or design systems', () => {
81
+ // Both skills advertising layout and design systems leaves the host to pick at random.
82
+ const lines = frontmatterLines(fs.readFileSync(PRO_MAX_SOURCE, 'utf8'));
83
+ const whenToUse = field(lines, 'when_to_use') || '';
84
+ assert.doesNotMatch(whenToUse, /layout/i);
85
+ assert.doesNotMatch(whenToUse, /design system/i);
86
+ assert.match(whenToUse, /cf:ui-ux/);
87
+ });
88
+
89
+ test('fresh claude install copies ui-ux with its playwright package manifest', () => {
90
+ // The package manifest is what makes --with-skills-deps install Playwright for probe.mjs.
91
+ inTempProject((root) => {
92
+ const result = install(root, ['claude']);
93
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
94
+
95
+ const installedSkill = path.join(root, '.claude', 'skills', 'ui-ux', 'SKILL.md');
96
+ const installedPackage = path.join(root, '.claude', 'skills', 'ui-ux', 'scripts', 'package.json');
97
+ assert.equal(fs.existsSync(installedSkill), true, 'the skill directory must be installed');
98
+ assert.equal(fs.existsSync(installedPackage), true, 'scripts/package.json must be installed');
99
+ assert.match(fs.readFileSync(installedSkill, 'utf8'), /^name: cf:ui-ux$/m);
100
+ });
101
+ });
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ // usage.cjs read the Claude Code OAuth token from the macOS Keychain to call an undocumented
4
+ // quota endpoint; the statusline now reads Claude Code's own rate_limits instead. Each case
5
+ // pins one way the hook could survive the removal: a fresh install, an upgrade on Claude
6
+ // (removeObsoleteClaudeRuntimeFiles deletes unconditionally; the settings prune matches by
7
+ // substring, so it must not take a user's own hook), an upgrade on omp (whose tracker keys
8
+ // are `.omp/…`), and the source lists themselves.
9
+
10
+ const assert = require('node:assert/strict');
11
+ const fs = require('node:fs');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+ const { spawnSync } = require('node:child_process');
15
+ const { test } = require('node:test');
16
+
17
+ const PACKAGE_ROOT = path.join(__dirname, '..', '..');
18
+ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
19
+ const MANIFEST = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8'));
20
+ const SOURCE_SETTINGS = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/settings/settings.json'), 'utf8'));
21
+ const BRIDGE = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/omp/extensions/cafekit-bridge.mjs'), 'utf8');
22
+
23
+ const CAFEKIT_USAGE_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/usage.cjs"';
24
+ const FOREIGN_USAGE_COMMAND = 'node ~/my-hooks/usage.cjs';
25
+
26
+ function inTempProject(run) {
27
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-usage-retired-'));
28
+ try {
29
+ const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
30
+ assert.equal(init.status, 0, init.stderr);
31
+ return run(root);
32
+ } finally {
33
+ fs.rmSync(root, { recursive: true, force: true });
34
+ }
35
+ }
36
+
37
+ function install(root, platforms) {
38
+ const result = spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes'],
39
+ { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
40
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
41
+ }
42
+
43
+ function commands(settings) {
44
+ return Object.values(settings.hooks || {})
45
+ .flat()
46
+ .flatMap((group) => (group.hooks || []).map((hook) => hook.command));
47
+ }
48
+
49
+ test('fresh claude install ships no usage hook', () => {
50
+ inTempProject((root) => {
51
+ install(root, ['claude']);
52
+ assert.equal(fs.existsSync(path.join(root, '.claude', 'hooks', 'usage.cjs')), false);
53
+ const settings = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'settings.json'), 'utf8'));
54
+ assert.equal(commands(settings).some((command) => command.includes('usage.cjs')), false);
55
+ });
56
+ });
57
+
58
+ test('upgrade removes an installed usage hook and its CafeKit entries but keeps a foreign one', () => {
59
+ inTempProject((root) => {
60
+ install(root, ['claude']);
61
+ const settingsPath = path.join(root, '.claude', 'settings.json');
62
+ const before = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
63
+ const kept = commands(before);
64
+
65
+ // An older install, edited by the user: the file differs from anything CafeKit shipped.
66
+ fs.writeFileSync(path.join(root, '.claude', 'hooks', 'usage.cjs'), '// user edit\n');
67
+ before.hooks.UserPromptSubmit[0].hooks.push(
68
+ { type: 'command', command: CAFEKIT_USAGE_COMMAND, timeout: 30 },
69
+ { type: 'command', command: FOREIGN_USAGE_COMMAND },
70
+ );
71
+ before.hooks.PostToolUse.push({
72
+ matcher: 'Edit|Write|MultiEdit',
73
+ hooks: [{ type: 'command', command: CAFEKIT_USAGE_COMMAND, timeout: 30 }],
74
+ });
75
+ fs.writeFileSync(settingsPath, JSON.stringify(before, null, 2));
76
+
77
+ install(root, ['claude']);
78
+ assert.equal(fs.existsSync(path.join(root, '.claude', 'hooks', 'usage.cjs')), false, 'an edited copy is removed too');
79
+ const after = commands(JSON.parse(fs.readFileSync(settingsPath, 'utf8')));
80
+ assert.equal(after.includes(CAFEKIT_USAGE_COMMAND), false, 'both CafeKit entries are pruned');
81
+ assert.equal(after.includes(FOREIGN_USAGE_COMMAND), true, 'a hook outside .claude/hooks/ survives');
82
+ for (const command of kept) assert.ok(after.includes(command), `kept: ${command}`);
83
+ });
84
+ });
85
+
86
+ test('omp upgrade removes an installed usage hook and its manifest record', () => {
87
+ inTempProject((root) => {
88
+ install(root, ['omp']);
89
+ const hookPath = path.join(root, '.omp', 'hooks', 'usage.cjs');
90
+ const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
91
+ fs.writeFileSync(hookPath, '// shipped by an older CafeKit\n');
92
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
93
+ // omp records keys relative to the project root (recordRoot '.').
94
+ manifest.files['.omp/hooks/usage.cjs'] = { sha256: '0'.repeat(64) };
95
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
96
+
97
+ install(root, ['omp']);
98
+ assert.equal(fs.existsSync(hookPath), false);
99
+ const afterManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
100
+ assert.equal(Object.prototype.hasOwnProperty.call(afterManifest.files, '.omp/hooks/usage.cjs'), false);
101
+ });
102
+ });
103
+
104
+ test('no runtime ships or bridges usage.cjs', () => {
105
+ assert.equal(MANIFEST.runtime.files.includes('hooks/usage.cjs'), false);
106
+ assert.equal(BRIDGE.includes("'usage.cjs'"), false);
107
+ assert.equal(fs.existsSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/usage.cjs')), false);
108
+ assert.ok(MANIFEST.obsolete.runtimeFiles.includes('hooks/usage.cjs'));
109
+ assert.ok(MANIFEST.obsolete.settingsHookCommandSubstrings.includes('.claude/hooks/usage.cjs'));
110
+ // The installer's merge drops an empty group, so only the source can show one.
111
+ for (const [event, groups] of Object.entries(SOURCE_SETTINGS.hooks)) {
112
+ for (const group of groups) assert.ok(group.hooks.length > 0, `${event} ${group.matcher} has no hooks`);
113
+ }
114
+ });
@@ -150,7 +150,7 @@ function semanticDigest(specDir) {
150
150
  return result.stdout.trim();
151
151
  }
152
152
 
153
- function completeSemanticReview(specDir, independence = 'same-session') {
153
+ function completeSemanticReview(specDir) {
154
154
  const statePath = path.join(specDir, 'spec.json');
155
155
  const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
156
156
  state.validation = isPlainObjectForTest(state.validation) ? state.validation : {};
@@ -166,24 +166,6 @@ function completeSemanticReview(specDir, independence = 'same-session') {
166
166
  }],
167
167
  };
168
168
  write(statePath, JSON.stringify(state, null, 2));
169
- if (independence === 'independent') {
170
- const root = path.dirname(path.dirname(specDir));
171
- const featureName = state.feature_name;
172
- const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({
173
- feature_name: featureName,
174
- spec_file: path.relative(root, statePath),
175
- semantic_digest: state.validation.semantic_review.reviewed_artifact_digest,
176
- verdict: 'PASS',
177
- })}`;
178
- const result = spawnSync(process.execPath, [path.join(ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
179
- cwd: root,
180
- env: { ...process.env, HOME: path.join(root, '.home'), USERPROFILE: path.join(root, '.home'), PROJECT_ROOT: root },
181
- input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'review-agent', agent_type: 'code_auditor', last_assistant_message: claim, cwd: root }),
182
- encoding: 'utf8',
183
- });
184
- assert.equal(result.status, 0, output(result));
185
- assert.equal(result.stderr, '');
186
- }
187
169
  return state;
188
170
  }
189
171
 
@@ -549,7 +531,7 @@ test('Codex installer transform preserves all owned spec assets with idempotent
549
531
  const review = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/references/review.md'), 'utf8');
550
532
  const templates = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/references/templates.md'), 'utf8');
551
533
  assert.match(skill, /specs\/<feature>\/[\s\S]{0,160}plan\.md[\s\S]{0,160}task-01-<slug>\.md/);
552
- assert.match(skill, /C1 — Scope[\s\S]*C2 — Findings[\s\S]*C3 — Done/);
534
+ assert.match(skill, /GATE-SCOPE[\s\S]*GATE-REVIEW[\s\S]*GATE-DONE/);
553
535
  assert.doesNotMatch(skill, /--(?:status|validate|archive)\b/);
554
536
  assert.match(review, /path:line/);
555
537
  assert.match(review, /Cap the presented[\s\S]{0,40}list at 15/);
@@ -1496,10 +1478,7 @@ test('all ready non-Direct specs require semantic design and coherent authoring
1496
1478
  state.timestamps = lifecycleTimestamps();
1497
1479
  mutate(state, specDir);
1498
1480
  write(specPath, JSON.stringify(state, null, 2));
1499
- completeSemanticReview(
1500
- specDir,
1501
- state.workflow_policy.assurance_level === 'Strict' ? 'independent' : 'same-session',
1502
- );
1481
+ completeSemanticReview(specDir);
1503
1482
  return specDir;
1504
1483
  }
1505
1484
 
@@ -1519,7 +1498,8 @@ test('all ready non-Direct specs require semantic design and coherent authoring
1519
1498
  write(path.join(specDir, 'design.md'), criticalNoObligationDesign());
1520
1499
  });
1521
1500
  result = run(VALIDATOR, [strictAuthoring], ROOT);
1522
- assert.equal(result.status, 0, output(result));
1501
+ assert.notEqual(result.status, 0);
1502
+ assert.match(output(result), /Strict assurance is no longer supported/);
1523
1503
 
1524
1504
  const invalidCases = [
1525
1505
  ['boolean-created-at', (state) => { state.created_at = true; }, /created_at: must be an ISO 8601 timestamp|created_at: ready handoff requires/],
@@ -1562,7 +1542,7 @@ test('all ready non-Direct specs require semantic design and coherent authoring
1562
1542
  }
1563
1543
  });
1564
1544
 
1565
- test('spec-ready excludes execution receipts while preserving Strict review state', () => {
1545
+ test('spec-ready excludes execution receipts and refuses Strict as no longer supported', () => {
1566
1546
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-spec-ready-'));
1567
1547
  try {
1568
1548
  const standardDir = createSpec(root, {
@@ -1602,9 +1582,10 @@ test('spec-ready excludes execution receipts while preserving Strict review stat
1602
1582
  timestamps: lifecycleTimestamps({ research: true, review: true }),
1603
1583
  },
1604
1584
  });
1605
- completeSemanticReview(criticalDir, 'independent');
1585
+ completeSemanticReview(criticalDir);
1606
1586
  const criticalResult = run(VALIDATOR, [criticalDir], ROOT);
1607
- assert.equal(criticalResult.status, 0, output(criticalResult));
1587
+ assert.notEqual(criticalResult.status, 0);
1588
+ assert.match(output(criticalResult), /Strict assurance is no longer supported/);
1608
1589
  } finally {
1609
1590
  fs.rmSync(root, { recursive: true, force: true });
1610
1591
  }
@@ -23,6 +23,29 @@ const CODEX_OWN_RUNTIME = [
23
23
  ['runtime.json', 'runtime.json']
24
24
  ];
25
25
 
26
+ /**
27
+ * Delete hooks the manifest marks obsolete. Codex tracker keys are project-relative
28
+ * (`.codex/hooks/…`), so the record is pruned through keyFor, and always: a record whose
29
+ * file is already gone must still leave the manifest.
30
+ */
31
+ function removeObsoleteCodexHooks(ctx, platformKey) {
32
+ const platform = PLATFORMS[platformKey];
33
+ const tracker = ctx.trackers && ctx.trackers[platformKey];
34
+ const obsoleteHooks = (ctx.manifest?.obsolete?.runtimeFiles || []).filter((rel) => rel.startsWith('hooks/'));
35
+ for (const rel of obsoleteHooks) {
36
+ const target = path.join(platform.folder, rel);
37
+ const exists = fs.existsSync(target);
38
+ if (!ctx.dryRun) {
39
+ if (exists) fs.rmSync(target, { force: true, recursive: fs.statSync(target).isDirectory() });
40
+ if (tracker) tracker.prune(tracker.keyFor(target));
41
+ }
42
+ if (exists) {
43
+ ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete Codex runtime: ${rel}`);
44
+ ctx.results.updated++;
45
+ }
46
+ }
47
+ }
48
+
26
49
  function writeSourceFile(ctx, platformKey, src, dest, label) {
27
50
  const platform = PLATFORMS[platformKey];
28
51
  const { action } = writeManagedFile({
@@ -64,6 +87,7 @@ function installRuntimeFiles(ctx, platformKey) {
64
87
  tracker: ctx.trackers[platformKey]
65
88
  });
66
89
  report(ctx, treeAction(agg), 'Codex native hooks');
90
+ removeObsoleteCodexHooks(ctx, platformKey);
67
91
  // Keep one canonical command analyzer in the Claude source tree, then
68
92
  // materialize the same bytes into the installed Codex runtime.
69
93
  // One canonical runtime schema lives in the Claude source tree; both installed
@@ -128,9 +128,31 @@ function installBridge(ctx, platformKey) {
128
128
  }
129
129
  }
130
130
 
131
+ /**
132
+ * Delete hooks the manifest marks obsolete. Mirrors removeObsoleteClaudeRuntimeFiles, but the
133
+ * omp tracker records keys relative to the project root (`.omp/hooks/…`), so the record is
134
+ * pruned through keyFor rather than by the bare `hooks/…` path.
135
+ */
136
+ function removeObsoleteOmpHooks(ctx, platformKey) {
137
+ const platform = PLATFORMS[platformKey];
138
+ const tracker = ctx.trackers && ctx.trackers[platformKey];
139
+ const obsoleteHooks = (ctx.manifest?.obsolete?.runtimeFiles || []).filter((rel) => rel.startsWith('hooks/'));
140
+ for (const rel of obsoleteHooks) {
141
+ const target = path.join(platform.folder, rel);
142
+ if (!fs.existsSync(target)) continue;
143
+ if (!ctx.dryRun) {
144
+ fs.rmSync(target, { force: true, recursive: fs.statSync(target).isDirectory() });
145
+ if (tracker) tracker.prune(tracker.keyFor(target));
146
+ }
147
+ ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete omp runtime: ${rel}`);
148
+ ctx.results.updated++;
149
+ }
150
+ }
151
+
131
152
  function installOmpRuntime(ctx, platformKey) {
132
153
  if (platformKey !== 'omp') return;
133
154
  installHooks(ctx, platformKey);
155
+ removeObsoleteOmpHooks(ctx, platformKey);
134
156
  installRuntimeFiles(ctx, platformKey);
135
157
  installBridge(ctx, platformKey);
136
158
  copyRulesDirectory(ctx, platformKey);