@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
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ // The five spec.json-era hooks (completion authority, its check and state modules, the semantic
4
+ // review authority and the scaffold guard) are gone from every runtime. Each case pins one way
5
+ // they could survive: a fresh install, an upgrade on Claude (settings prune matches by substring,
6
+ // so a user's own hook with the same file name must stay), an upgrade on Codex (whose tracker
7
+ // keys are `.codex/…` and whose handlers come in three shapes), an upgrade on omp, an omp bridge
8
+ // that still lists the removed gates, and the source lists themselves.
9
+
10
+ const assert = require('node:assert/strict');
11
+ const crypto = require('node:crypto');
12
+ const fs = require('node:fs');
13
+ const os = require('node:os');
14
+ const path = require('node:path');
15
+ const { spawnSync } = require('node:child_process');
16
+ const { test } = require('node:test');
17
+
18
+ const PACKAGE_ROOT = path.join(__dirname, '..', '..');
19
+ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
20
+ const SRC = path.join(PACKAGE_ROOT, 'src');
21
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
22
+ const MANIFEST = readJson(path.join(SRC, 'claude/migration-manifest.json'));
23
+ const SOURCE_SETTINGS = readJson(path.join(SRC, 'claude/settings/settings.json'));
24
+ const SOURCE_BRIDGE_PATH = path.join(SRC, 'omp/extensions/cafekit-bridge.mjs');
25
+
26
+ const NAMES = [
27
+ 'completion-authority.cjs',
28
+ 'completion-authority-check.cjs',
29
+ 'completion-authority-state.cjs',
30
+ 'semantic-review-authority.cjs',
31
+ 'task-scaffold-guard.cjs',
32
+ ];
33
+ const REGISTERED = ['completion-authority.cjs', 'semantic-review-authority.cjs', 'task-scaffold-guard.cjs'];
34
+ const FOREIGN = REGISTERED.map((name) => `node ~/my-hooks/${name}`);
35
+
36
+ function inTempProject(run) {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-legacy-retired-'));
38
+ try {
39
+ const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
40
+ assert.equal(init.status, 0, init.stderr);
41
+ return run(root);
42
+ } finally {
43
+ fs.rmSync(root, { recursive: true, force: true });
44
+ }
45
+ }
46
+
47
+ function install(root, platforms) {
48
+ const result = spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes'],
49
+ { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
50
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
51
+ }
52
+
53
+ function commands(settings) {
54
+ return Object.values(settings.hooks || {})
55
+ .flat()
56
+ .flatMap((group) => (group.hooks || []).map((hook) => hook.command));
57
+ }
58
+
59
+ function mentionsName(text) {
60
+ return NAMES.filter((name) => text.includes(name));
61
+ }
62
+
63
+ test('fresh installs ship and register none of the five legacy hooks', () => {
64
+ inTempProject((root) => {
65
+ install(root, ['claude', 'codex', 'omp']);
66
+ for (const dir of ['.claude', '.codex', '.omp']) {
67
+ for (const name of NAMES) assert.equal(fs.existsSync(path.join(root, dir, 'hooks', name)), false, `${dir}/hooks/${name}`);
68
+ }
69
+ const claude = commands(readJson(path.join(root, '.claude', 'settings.json')));
70
+ const codex = commands(readJson(path.join(root, '.codex', 'hooks.json')));
71
+ for (const command of [...claude, ...codex]) assert.deepEqual(mentionsName(command), [], command);
72
+ const bridge = fs.readFileSync(path.join(root, '.omp', 'extensions', 'cafekit-bridge.mjs'), 'utf8');
73
+ assert.deepEqual(mentionsName(bridge), []);
74
+ });
75
+ });
76
+
77
+ test('claude upgrade removes the legacy hooks and their CafeKit entries but keeps foreign ones', () => {
78
+ inTempProject((root) => {
79
+ install(root, ['claude']);
80
+ const settingsPath = path.join(root, '.claude', 'settings.json');
81
+ const manifestPath = path.join(root, '.claude', 'cafekit-manifest.json');
82
+ const settings = readJson(settingsPath);
83
+ const kept = commands(settings);
84
+
85
+ const manifest = readJson(manifestPath);
86
+ for (const name of NAMES) {
87
+ fs.writeFileSync(path.join(root, '.claude', 'hooks', name), '// user edit\n');
88
+ manifest.files[`hooks/${name}`] = { sha256: '0'.repeat(64) };
89
+ }
90
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
91
+
92
+ const cmd = (name) => `node "$CLAUDE_PROJECT_DIR/.claude/hooks/${name}"`;
93
+ settings.hooks.UserPromptSubmit[0].hooks.push(
94
+ { type: 'command', command: cmd('completion-authority.cjs') },
95
+ ...FOREIGN.map((command) => ({ type: 'command', command })),
96
+ );
97
+ settings.hooks.Stop[0].hooks.push({ type: 'command', command: cmd('completion-authority.cjs') });
98
+ settings.hooks.PreToolUse.push({ matcher: 'Write', hooks: [{ type: 'command', command: cmd('task-scaffold-guard.cjs') }] });
99
+ settings.hooks.SubagentStop[0].hooks.push({ type: 'command', command: cmd('semantic-review-authority.cjs') });
100
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
101
+
102
+ install(root, ['claude']);
103
+ for (const name of NAMES) assert.equal(fs.existsSync(path.join(root, '.claude', 'hooks', name)), false, name);
104
+ const after = readJson(manifestPath);
105
+ for (const name of NAMES) assert.equal(Object.prototype.hasOwnProperty.call(after.files, `hooks/${name}`), false, `record ${name}`);
106
+ const afterCommands = commands(readJson(settingsPath));
107
+ for (const name of REGISTERED) {
108
+ assert.equal(afterCommands.some((command) => command.includes(`.claude/hooks/${name}`)), false, name);
109
+ }
110
+ for (const command of FOREIGN) assert.ok(afterCommands.includes(command), `foreign: ${command}`);
111
+ for (const command of kept) assert.ok(afterCommands.includes(command), `kept: ${command}`);
112
+ });
113
+ });
114
+
115
+ test('codex upgrade removes the legacy hooks, their records and their CafeKit entries but keeps foreign ones', () => {
116
+ inTempProject((root) => {
117
+ install(root, ['codex']);
118
+ const hooksPath = path.join(root, '.codex', 'hooks.json');
119
+ const manifestPath = path.join(root, '.codex', 'cafekit-manifest.json');
120
+ const config = readJson(hooksPath);
121
+ const kept = commands(config);
122
+
123
+ // A record whose file is already gone must leave the manifest too.
124
+ const manifest = readJson(manifestPath);
125
+ for (const name of NAMES) {
126
+ if (name !== 'completion-authority-state.cjs') fs.writeFileSync(path.join(root, '.codex', 'hooks', name), '// user edit\n');
127
+ manifest.files[`.codex/hooks/${name}`] = { sha256: '0'.repeat(64) };
128
+ }
129
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
130
+
131
+ const posix = kept.find((command) => command.includes('spec-gate.cjs'));
132
+ assert.ok(posix, 'the installed spec-gate launcher is the template for the POSIX form');
133
+ const add = (event, command) => config.hooks[event].push({ matcher: '*', hooks: [{ type: 'command', command }] });
134
+ add('Stop', posix.split('spec-gate.cjs').join('completion-authority.cjs'));
135
+ add('PreToolUse', 'node ".codex/hooks/task-scaffold-guard.cjs"');
136
+ add('SubagentStop', "node 'C:\\work\\proj\\.codex\\hooks\\semantic-review-authority.cjs'");
137
+ add('UserPromptSubmit', FOREIGN[0]);
138
+ fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2));
139
+
140
+ install(root, ['codex']);
141
+ for (const name of NAMES) assert.equal(fs.existsSync(path.join(root, '.codex', 'hooks', name)), false, name);
142
+ const after = readJson(manifestPath);
143
+ for (const name of NAMES) assert.equal(Object.prototype.hasOwnProperty.call(after.files, `.codex/hooks/${name}`), false, `record ${name}`);
144
+ const afterCommands = commands(readJson(hooksPath));
145
+ for (const name of REGISTERED) {
146
+ assert.equal(afterCommands.some((command) => command.includes(`.codex/hooks/${name}`) || command.includes(`.codex\\hooks\\${name}`)), false, name);
147
+ }
148
+ assert.ok(afterCommands.includes(FOREIGN[0]), 'a foreign hook with a legacy name survives');
149
+ for (const command of kept) assert.ok(afterCommands.includes(command), `kept: ${command}`);
150
+ });
151
+ });
152
+
153
+ test('omp upgrade removes the legacy hooks and their records', () => {
154
+ inTempProject((root) => {
155
+ install(root, ['omp']);
156
+ const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
157
+ const manifest = readJson(manifestPath);
158
+ // omp records keys relative to the project root (recordRoot '.').
159
+ for (const name of NAMES) {
160
+ fs.writeFileSync(path.join(root, '.omp', 'hooks', name), '// shipped by an older CafeKit\n');
161
+ manifest.files[`.omp/hooks/${name}`] = { sha256: '0'.repeat(64) };
162
+ }
163
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
164
+
165
+ install(root, ['omp']);
166
+ const after = readJson(manifestPath);
167
+ for (const name of NAMES) {
168
+ assert.equal(fs.existsSync(path.join(root, '.omp', 'hooks', name)), false, name);
169
+ assert.equal(Object.prototype.hasOwnProperty.call(after.files, `.omp/hooks/${name}`), false, `record ${name}`);
170
+ }
171
+ });
172
+ });
173
+
174
+ test('omp upgrade replaces a pristine old bridge that still lists the removed gates', () => {
175
+ inTempProject((root) => {
176
+ install(root, ['omp']);
177
+ const bridgePath = path.join(root, '.omp', 'extensions', 'cafekit-bridge.mjs');
178
+ const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
179
+ const old = fs.readFileSync(SOURCE_BRIDGE_PATH, 'utf8')
180
+ .replace("'inspect-block.cjs']", "'inspect-block.cjs', 'task-scaffold-guard.cjs']")
181
+ .replace("'rules.cjs', 'spec-state.cjs'", "'rules.cjs', 'completion-authority.cjs', 'spec-state.cjs'")
182
+ .replace("'spec-gate.cjs', 'state.cjs'", "'spec-gate.cjs', 'completion-authority.cjs', 'state.cjs'");
183
+ assert.deepEqual(mentionsName(old).sort(), ['completion-authority.cjs', 'task-scaffold-guard.cjs'], 'the fixture lists the removed gates');
184
+ fs.writeFileSync(bridgePath, old);
185
+ const manifest = readJson(manifestPath);
186
+ manifest.files['.omp/extensions/cafekit-bridge.mjs'] = { sha256: crypto.createHash('sha256').update(old).digest('hex') };
187
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
188
+
189
+ install(root, ['omp']);
190
+ const installed = fs.readFileSync(bridgePath, 'utf8');
191
+ assert.deepEqual(mentionsName(installed), []);
192
+ assert.equal(installed, fs.readFileSync(SOURCE_BRIDGE_PATH, 'utf8'));
193
+ });
194
+ });
195
+
196
+ test('no source ships, registers or bridges the five legacy hooks', () => {
197
+ for (const name of NAMES) {
198
+ assert.equal(fs.existsSync(path.join(SRC, 'claude/hooks', name)), false, `claude ${name}`);
199
+ assert.equal(fs.existsSync(path.join(SRC, 'codex/hooks', name)), false, `codex ${name}`);
200
+ }
201
+ for (const test of ['completion-authority.test.js', 'semantic-review-authority.test.js']) {
202
+ assert.equal(fs.existsSync(path.join(SRC, 'claude/hooks/__tests__', test)), false, test);
203
+ }
204
+ const runtimeFiles = MANIFEST.runtime.files.join('\n');
205
+ assert.deepEqual(mentionsName(runtimeFiles), []);
206
+ assert.deepEqual(mentionsName(JSON.stringify(SOURCE_SETTINGS)), []);
207
+ assert.deepEqual(mentionsName(fs.readFileSync(path.join(SRC, 'codex/hooks.json'), 'utf8')), []);
208
+ assert.deepEqual(mentionsName(fs.readFileSync(SOURCE_BRIDGE_PATH, 'utf8')), []);
209
+ for (const name of NAMES) assert.ok(MANIFEST.obsolete.runtimeFiles.includes(`hooks/${name}`), name);
210
+ const substrings = MANIFEST.obsolete.settingsHookCommandSubstrings;
211
+ assert.equal(substrings[0], 'hooks/skill-router.cjs');
212
+ for (const name of REGISTERED) {
213
+ for (const entry of [`.claude/hooks/${name}`, `.codex/hooks/${name}`, `.codex\\hooks\\${name}`]) {
214
+ assert.ok(substrings.includes(entry), entry);
215
+ }
216
+ }
217
+ // The installer's merge drops an empty group, so only the source can show one.
218
+ for (const [event, groups] of Object.entries(SOURCE_SETTINGS.hooks)) {
219
+ for (const group of groups) assert.ok(group.hooks.length > 0, `${event} ${group.matcher} has no hooks`);
220
+ }
221
+ });
@@ -17,7 +17,6 @@ const PACKAGE_ROOT = path.join(__dirname, '../..');
17
17
  const BRIDGE = path.join(PACKAGE_ROOT, 'src/omp/extensions/cafekit-bridge.mjs');
18
18
  const SETTINGS = require(path.join(PACKAGE_ROOT, 'src/claude/settings/settings.json'));
19
19
  const MANIFEST = require(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'));
20
- const { PREFIX } = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/completion-authority-state.cjs'));
21
20
 
22
21
  /**
23
22
  * The hooks an omp install runs: the Claude set the manifest ships, then `src/omp/hooks/`
@@ -50,6 +49,11 @@ function registered(eventName) {
50
49
  .filter(Boolean);
51
50
  }
52
51
 
52
+ // Deliberate, named divergences while omp is not yet updated. Each must still be a real
53
+ // difference, so the test fails (and the entry must go) once omp catches up.
54
+ const CLAUDE_ONLY = { PostToolUse: ['spec-state.cjs'] };
55
+ const OMP_ONLY = { SessionStart: ['docs-sync.cjs'] };
56
+
53
57
  test('the dispatch table mirrors settings.json for every event omp can deliver', () => {
54
58
  const { DISPATCH } = bridge;
55
59
  const pairs = [
@@ -58,8 +62,18 @@ test('the dispatch table mirrors settings.json for every event omp can deliver',
58
62
  ['tool_result', 'PostToolUse'], ['session_stop', 'Stop'],
59
63
  ];
60
64
  for (const [omp, claude] of pairs) {
61
- assert.deepEqual([...DISPATCH[omp].hooks].sort(), [...new Set(registered(claude))].sort(),
62
- `${omp} must dispatch exactly the hooks settings.json registers for ${claude}`);
65
+ const claudeOnly = CLAUDE_ONLY[claude] || [];
66
+ const ompOnly = OMP_ONLY[claude] || [];
67
+ const claudeHooks = [...new Set(registered(claude))];
68
+ for (const hook of claudeOnly) {
69
+ assert.ok(claudeHooks.includes(hook) && !DISPATCH[omp].hooks.includes(hook), `${hook} is no longer Claude-only for ${claude}; drop it from CLAUDE_ONLY`);
70
+ }
71
+ for (const hook of ompOnly) {
72
+ assert.ok(!claudeHooks.includes(hook) && DISPATCH[omp].hooks.includes(hook), `${hook} is no longer omp-only for ${claude}; drop it from OMP_ONLY`);
73
+ }
74
+ const expected = [...claudeHooks.filter((hook) => !claudeOnly.includes(hook)), ...ompOnly].sort();
75
+ assert.deepEqual([...DISPATCH[omp].hooks].sort(), expected,
76
+ `${omp} must dispatch the hooks settings.json registers for ${claude}, minus CLAUDE_ONLY, plus OMP_ONLY`);
63
77
  }
64
78
  // omp has no subagent events; the gap is deliberate and documented, not forgotten.
65
79
  assert.ok(!Object.values(DISPATCH).some((e) => e.event.startsWith('Subagent')));
@@ -83,7 +97,7 @@ test('each denial mechanism becomes an omp block carrying the hook reason', () =
83
97
  assert.deepEqual([stop.block, stop.reason], [true, 'R1']);
84
98
  const deny = interpretVerdict({ code: 0, stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'R2' } }) }, 'privacy-block.cjs');
85
99
  assert.deepEqual([deny.block, deny.reason], [true, 'R2']);
86
- // inspect-block / task-scaffold-guard exit 2 and write the reason on STDOUT, not stderr.
100
+ // inspect-block exits 2 and writes the reason on STDOUT, not stderr.
87
101
  const exit2 = interpretVerdict({ code: 2, stdout: 'SCOPE LIMIT EXCEEDED\nRestricted zones: node_modules', stderr: '' }, 'inspect-block.cjs');
88
102
  assert.equal(exit2.block, true);
89
103
  assert.match(exit2.reason, /SCOPE LIMIT EXCEEDED/, 'a stderr-only reader would drop this reason');
@@ -127,10 +141,10 @@ test('a slow hook blocks with its own name before omp cuts it off', async () =>
127
141
  test('a crashing or missing gate blocks on gating events instead of silently allowing', async () => {
128
142
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-crash-'));
129
143
  try {
130
- // Only the crashing gate is under test; the other two PreToolUse gates get allow stubs,
144
+ // Only the crashing gate is under test; the other PreToolUse gate gets an allow stub,
131
145
  // otherwise their absence is what blocks (see the missing-gate case below).
132
146
  fs.writeFileSync(path.join(dir, 'privacy-block.cjs'), 'throw new Error("boom");');
133
- for (const stub of ['inspect-block.cjs', 'task-scaffold-guard.cjs']) fs.writeFileSync(path.join(dir, stub), 'process.exit(0);');
147
+ fs.writeFileSync(path.join(dir, 'inspect-block.cjs'), 'process.exit(0);');
134
148
  const crash = await bridge.runEvent('tool_call', { toolName: 'bash', input: { command: 'ls' } }, { cwd: dir }, { dir });
135
149
  // A bare exit 1 with no verdict is an allow under the hooks' own contract. Real gates
136
150
  // never surface a crash this way: the fork's privacy-block catch-all converts its own
@@ -147,17 +161,6 @@ test('stop_hook_active short-circuits the gate so a blocked turn cannot loop for
147
161
  assert.deepEqual([r.block, r.skipped], [false, 'stop_hook_active']);
148
162
  });
149
163
 
150
- test('the approval phrase reaches completion-authority through the input event', async () => {
151
- const { DISPATCH, shapePayload } = bridge;
152
- assert.ok(DISPATCH.input.hooks.includes('completion-authority.cjs'),
153
- 'the --approve path only runs on UserPromptSubmit; without this routing a blocked turn has no exit');
154
- const phrase = `${PREFIX}${'a'.repeat(24)}`;
155
- const p = shapePayload('input', { text: phrase, source: 'user' }, { cwd: '/tmp/p' });
156
- assert.equal(p.hook_event_name, 'UserPromptSubmit', 'completion-authority selects --approve from this field');
157
- assert.equal(p.prompt, phrase, 'the phrase must arrive byte-exact; the hook compares it exactly');
158
- assert.ok(p.session_id, 'approve() returns early without a session id');
159
- });
160
-
161
164
  test('input injections flow back as a transform that keeps the user text intact', async () => {
162
165
  const root = tempProject(false);
163
166
  try {
@@ -117,25 +117,6 @@ test('an unevaluable access denies rather than allows', () => {
117
117
  }
118
118
  });
119
119
 
120
- test('a write to a scaffolded task path is guarded under omp lowercase names too', () => {
121
- // The guard is no longer an overlay file, so this is a behaviour case on the tree an
122
- // install produces: omp sends `write`, and the shared reader has to make it reach the
123
- // rule that was authored against Claude's `Write`.
124
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-guard-'));
125
- try {
126
- const target = path.join(root, 'specs', 'demo', 'tasks', 'task-R0-01-x.md');
127
- fs.mkdirSync(path.dirname(target), { recursive: true });
128
- fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/scripts'), path.join(INSTALLED_ROOT, '.omp', 'scripts'),
129
- { recursive: true, force: true });
130
- const r = verdict(path.join(INSTALLED, 'task-scaffold-guard.cjs'),
131
- { tool_name: 'write', tool_input: { file_path: target }, cwd: root });
132
- assert.equal(r.code, 2, "omp sends `write`; an exact 'Write' comparison would skip the guard entirely");
133
- assert.equal(r.decision, 'deny');
134
- } finally {
135
- fs.rmSync(root, { recursive: true, force: true });
136
- }
137
- });
138
-
139
120
  /** Every file under a directory, relative and sorted. */
140
121
  function walk(dir, prefix = '') {
141
122
  return fs.readdirSync(dir).flatMap((name) => {
@@ -50,7 +50,7 @@ function silentUi() {
50
50
 
51
51
  test('manifest separates core, optional documents, and retired skills', () => {
52
52
  const manifest = loadClaudeMigrationManifest();
53
- assert.equal(manifest.skills.required.includes('route'), true);
53
+ assert.equal(manifest.skills.required.includes('route'), false);
54
54
  assert.deepEqual(manifest.skills.bundles.documentSkills, [
55
55
  'ai-multimodal', 'docs', 'docx', 'pdf', 'pptx', 'xlsx'
56
56
  ]);
@@ -59,7 +59,8 @@ test('manifest separates core, optional documents, and retired skills', () => {
59
59
  }
60
60
  assert.deepEqual(manifest.obsolete.skills, [
61
61
  'backend-development', 'frontend-development', 'frontend-design',
62
- 'mobile-development', 'devops', 'react-best-practices'
62
+ 'mobile-development', 'devops', 'react-best-practices',
63
+ 'inspect', 'hotfix', 'question', 'loop', 'route', 'delegate'
63
64
  ]);
64
65
  });
65
66
 
@@ -167,7 +168,7 @@ test('fresh combined install defaults optional bundle OFF', () => {
167
168
  for (const platformKey of ['claude', 'codex']) {
168
169
  const platform = PLATFORMS[platformKey];
169
170
  assert.equal(fs.existsSync(path.join(platform.skillsDir, 'docs')), false);
170
- assert.equal(fs.existsSync(path.join(platform.skillsDir, 'route', 'SKILL.md')), true);
171
+ assert.equal(fs.existsSync(path.join(platform.skillsDir, 'route', 'SKILL.md')), false);
171
172
  const metadata = JSON.parse(fs.readFileSync(path.join(platform.folder, 'cafekit.json'), 'utf8'));
172
173
  assert.equal(metadata.schemaVersion, 2);
173
174
  assert.equal(metadata.documentSkills.enabled, false);
@@ -193,7 +194,7 @@ test('explicit opt-in installs all six optional skills on both runtimes', () =>
193
194
  ], { cwd: root, encoding: 'utf8' });
194
195
  assert.equal(catalog.status, 0, catalog.stderr);
195
196
  const catalogData = JSON.parse(catalog.stdout);
196
- assert.equal(catalogData.skills.some((skill) => skill.public_id === 'cf:route'), true);
197
+ assert.equal(catalogData.skills.some((skill) => skill.public_id === 'cf:route'), false);
197
198
  assert.equal(catalogData.skills.some((skill) => skill.public_id === 'cf:docs'), true);
198
199
  const metadata = JSON.parse(fs.readFileSync(path.join(platform.folder, 'cafekit.json'), 'utf8'));
199
200
  assert.deepEqual(metadata.documentSkills, {
@@ -247,3 +248,110 @@ test('modified retired skill is preserved but excluded from automatic routing',
247
248
  item.directory === 'backend-development' && item.code === 'retired_skill'));
248
249
  });
249
250
  });
251
+
252
+ for (const platformKey of ['claude', 'codex']) {
253
+ test(`${platformKey} upgrade retires the inspect, hotfix and question directories`, () => {
254
+ withTempProject((root) => {
255
+ const platform = PLATFORMS[platformKey];
256
+ const recordRoot = platform.ownership?.recordRoot || platform.folder;
257
+ const key = (file) => path.relative(recordRoot, file).replace(/\\/g, '/');
258
+ const ownership = { schemaVersion: 1, version: 'old', files: {} };
259
+ for (const old of ['inspect', 'hotfix', 'question']) {
260
+ const file = path.join(platform.skillsDir, old, 'SKILL.md');
261
+ fs.mkdirSync(path.dirname(file), { recursive: true });
262
+ fs.writeFileSync(file, `pristine ${old}\n`);
263
+ ownership.files[key(file)] = { sha256: manifestLib.hashFile(file), version: 'old' };
264
+ }
265
+ const ctx = {
266
+ manifest: loadClaudeMigrationManifest(),
267
+ documentSkills: { [platformKey]: { enabled: true, selectionSource: 'cli-opt-in' } },
268
+ ownership: { [platform.folder]: ownership },
269
+ trackers: { [platformKey]: manifestLib.createTracker(platform.folder, 'next', platform.ownership) },
270
+ dryRun: false,
271
+ ui: silentUi(),
272
+ results: { updated: 0, preserved: 0, preservedFiles: [] }
273
+ };
274
+ reconcileSkillInventory(ctx, platformKey);
275
+ for (const old of ['inspect', 'hotfix', 'question']) {
276
+ assert.equal(fs.existsSync(path.join(root, platform.skillsDir, old)), false, old);
277
+ }
278
+ assert.equal(ctx.results.updated, 3);
279
+
280
+ // A user-modified retired copy survives with a warning, and the catalog serves cf:fix from fix/
281
+ // while marking the old directory retired rather than reporting a folder mismatch.
282
+ const edited = path.join(root, platform.skillsDir, 'hotfix', 'SKILL.md');
283
+ fs.mkdirSync(path.dirname(edited), { recursive: true });
284
+ fs.writeFileSync(edited, [
285
+ '---', `name: ${platformKey === 'codex' ? 'cf-fix' : 'cf:fix'}`,
286
+ 'description: "User-modified retired copy."', '---', '# Preserved', '',
287
+ ].join('\n'));
288
+ const result = spawnSync(process.execPath, [
289
+ installerPath, '--platform', platformKey, '--yes'
290
+ ], { cwd: root, encoding: 'utf8' });
291
+ assert.equal(result.status, 0, result.stderr || result.stdout);
292
+ assert.equal(fs.existsSync(edited), true);
293
+ assert.match(`${result.stdout}\n${result.stderr}`, /Preserved user-owned skill: .*hotfix/);
294
+ for (const renamed of ['scout', 'fix', 'ask']) {
295
+ assert.equal(fs.existsSync(path.join(root, platform.skillsDir, renamed, 'SKILL.md')), true, renamed);
296
+ }
297
+ const scriptRoot = platformKey === 'codex' ? '.codex' : '.claude';
298
+ const catalog = spawnSync(process.execPath, [
299
+ path.join(root, scriptRoot, 'scripts', 'generate-skill-catalog.cjs'), '--json'
300
+ ], { cwd: root, encoding: 'utf8' });
301
+ assert.equal(catalog.status, 0, catalog.stderr);
302
+ const parsed = JSON.parse(catalog.stdout);
303
+ for (const id of ['cf:scout', 'cf:fix', 'cf:ask']) {
304
+ assert.ok(parsed.skills.some((skill) => skill.public_id === id), id);
305
+ }
306
+ assert.equal(parsed.skills.find((skill) => skill.public_id === 'cf:fix').directory, 'fix');
307
+ assert.ok(parsed.diagnostics.some((item) => item.directory === 'hotfix' && item.code === 'retired_skill'));
308
+ assert.equal(parsed.diagnostics.some((item) => item.code === 'duplicate_public_name'), false);
309
+ });
310
+ });
311
+ }
312
+
313
+ for (const platformKey of ['claude', 'codex']) {
314
+ test(`${platformKey} upgrade retires the removed loop, route and delegate directories`, () => {
315
+ withTempProject((root) => {
316
+ const platform = PLATFORMS[platformKey];
317
+ const recordRoot = platform.ownership?.recordRoot || platform.folder;
318
+ const key = (file) => path.relative(recordRoot, file).replace(/\\/g, '/');
319
+ const ownership = { schemaVersion: 1, version: 'old', files: {} };
320
+ const removed = ['loop', 'route', 'delegate'];
321
+ for (const old of removed) {
322
+ const file = path.join(platform.skillsDir, old, 'SKILL.md');
323
+ fs.mkdirSync(path.dirname(file), { recursive: true });
324
+ fs.writeFileSync(file, `pristine ${old}\n`);
325
+ ownership.files[key(file)] = { sha256: manifestLib.hashFile(file), version: 'old' };
326
+ }
327
+ // A user-edited copy has no matching ownership hash, so it must survive the upgrade.
328
+ const edited = path.join(platform.skillsDir, 'route', 'references', 'my-notes.md');
329
+ fs.mkdirSync(path.dirname(edited), { recursive: true });
330
+ fs.writeFileSync(edited, 'user notes\n');
331
+ const ctx = {
332
+ manifest: loadClaudeMigrationManifest(),
333
+ documentSkills: { [platformKey]: { enabled: true, selectionSource: 'cli-opt-in' } },
334
+ ownership: { [platform.folder]: ownership },
335
+ trackers: { [platformKey]: manifestLib.createTracker(platform.folder, 'next', platform.ownership) },
336
+ dryRun: false,
337
+ ui: silentUi(),
338
+ results: { updated: 0, preserved: 0, preservedFiles: [] }
339
+ };
340
+ reconcileSkillInventory(ctx, platformKey);
341
+ assert.equal(fs.existsSync(path.join(root, platform.skillsDir, 'loop')), false, 'loop');
342
+ assert.equal(fs.existsSync(path.join(root, platform.skillsDir, 'delegate')), false, 'delegate');
343
+ assert.equal(fs.existsSync(path.join(root, edited)), true, 'user-edited route copy is preserved');
344
+ assert.equal(ctx.results.updated, 2);
345
+ assert.equal(ctx.results.preserved, 1);
346
+ });
347
+ });
348
+ }
349
+
350
+ test('the catalog retires exactly the directories the manifest retires', () => {
351
+ const source = fs.readFileSync(
352
+ path.resolve(__dirname, '..', '..', 'src', 'claude', 'scripts', 'generate-skill-catalog.cjs'), 'utf8');
353
+ const block = source.match(/const RETIRED_DIRECTORIES = new Set\(\[([\s\S]*?)\]\);/);
354
+ assert.ok(block, 'RETIRED_DIRECTORIES not found');
355
+ const retired = [...block[1].matchAll(/'([^']+)'/g)].map((match) => match[1]);
356
+ assert.deepEqual([...retired].sort(), [...loadClaudeMigrationManifest().obsolete.skills].sort());
357
+ });
@@ -145,8 +145,6 @@ function composeCodexHookTree(root) {
145
145
  const hooksDir = path.join(root, '.codex', 'hooks');
146
146
  fs.mkdirSync(path.join(hooksDir, 'lib'), { recursive: true });
147
147
  fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks/session.cjs'), path.join(hooksDir, 'session.cjs'));
148
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks/completion-authority-state.cjs'),
149
- path.join(hooksDir, 'completion-authority-state.cjs'));
150
148
  fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/hook-context.cjs'),
151
149
  path.join(hooksDir, 'lib', 'hook-context.cjs'));
152
150
  fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks/lib/privacy-state.cjs'),