@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.
- package/README.md +14 -22
- package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
- package/bin/__tests__/codex-hooks.test.js +0 -36
- package/bin/__tests__/codex-native.test.js +46 -88
- package/bin/__tests__/develop-contract.test.js +26 -57
- package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
- package/bin/__tests__/grok-envelope.test.js +0 -56
- package/bin/__tests__/hook-payload.test.js +4 -6
- package/bin/__tests__/legacy-authority-decoupled.test.js +159 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
- package/bin/__tests__/omp-bridge.test.js +20 -17
- package/bin/__tests__/omp-hooks.test.js +0 -19
- package/bin/__tests__/optional-skill-inventory.test.js +112 -4
- package/bin/__tests__/orca-session.test.js +0 -2
- package/bin/__tests__/package-inventory.test.js +100 -541
- package/bin/__tests__/runtime-dir.test.js +0 -3
- package/bin/__tests__/runtime-schema.test.js +3 -2
- package/bin/__tests__/skill-routing-source.test.js +27 -15
- package/bin/__tests__/spec-narrowing.test.js +0 -27
- package/bin/__tests__/specs-v2-execution-closeout.test.js +4 -69
- package/bin/__tests__/ui-ux-skill.test.js +101 -0
- package/bin/__tests__/usage-hook-retired.test.js +114 -0
- package/bin/__tests__/validator-grounding.test.js +9 -28
- package/bin/phases/codex-runtime.js +24 -0
- package/bin/phases/omp-runtime.js +22 -0
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +3 -10
- package/src/claude/agents/brainstormer.md +7 -3
- package/src/claude/agents/code-auditor.md +35 -50
- package/src/claude/agents/debugger.md +5 -79
- package/src/claude/agents/deployer.md +3 -5
- package/src/claude/agents/docs-keeper.md +2 -7
- package/src/claude/agents/implementer.md +6 -9
- package/src/claude/agents/inspector.md +2 -3
- package/src/claude/agents/project-manager.md +7 -15
- package/src/claude/agents/researcher.md +9 -3
- package/src/claude/agents/spec-maker.md +11 -11
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +112 -3
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +134 -0
- package/src/claude/hooks/__tests__/state.test.js +68 -0
- package/src/claude/hooks/__tests__/statusline.test.js +140 -11
- package/src/claude/hooks/agent.cjs +2 -1
- package/src/claude/hooks/lib/context.cjs +0 -89
- package/src/claude/hooks/lib/hook-payload.cjs +2 -2
- package/src/claude/hooks/rules.cjs +2 -1
- package/src/claude/hooks/session.cjs +0 -1
- package/src/claude/hooks/spec-gate.cjs +18 -28
- package/src/claude/hooks/spec-state.cjs +95 -1
- package/src/claude/hooks/state.cjs +21 -2
- package/src/claude/migration-manifest.json +30 -15
- package/src/claude/rules/ai-dev-rules.md +1 -1
- package/src/claude/rules/hook-protocols.md +8 -15
- package/src/claude/rules/manage-docs.md +9 -23
- package/src/claude/rules/orchestrator.md +3 -4
- package/src/claude/rules/review-audit-self-decision.md +3 -3
- package/src/claude/rules/skill-domain-routing.md +1 -3
- package/src/claude/rules/skill-workflow-routing.md +4 -5
- package/src/claude/rules/state-sync.md +3 -12
- package/src/claude/rules/workflow.md +10 -18
- package/src/claude/runtime.schema.json +5 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
- package/src/claude/scripts/provenance.cjs +23 -5
- package/src/claude/scripts/spec-receipt.cjs +52 -6
- package/src/claude/scripts/spec-resolver.cjs +1 -2
- package/src/claude/scripts/validate-spec-output.cjs +5 -32
- package/src/claude/scripts/workflow-policy.cjs +22 -10
- package/src/claude/settings/settings.json +1 -32
- package/src/claude/skills/{question → ask}/SKILL.md +9 -75
- package/src/claude/skills/brainstorm/SKILL.md +43 -131
- package/src/claude/skills/code-review/SKILL.md +48 -17
- package/src/claude/skills/code-review/references/spec-compliance-review.md +2 -3
- package/src/claude/skills/code-review/references/verification-gate.md +22 -27
- package/src/claude/skills/debug/SKILL.md +29 -149
- package/src/claude/skills/develop/SKILL.md +27 -36
- package/src/claude/skills/develop/references/parallel-waves.md +0 -8
- package/src/claude/skills/develop/references/quality-gate.md +15 -13
- package/src/claude/skills/docs/SKILL.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +1 -1
- package/src/claude/skills/fix/SKILL.md +153 -0
- package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
- package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
- package/src/claude/skills/git/SKILL.md +48 -9
- package/src/claude/skills/git/references/commit-protocols.md +35 -16
- package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
- package/src/claude/skills/orca/SKILL.md +9 -8
- package/src/claude/skills/research/SKILL.md +2 -0
- package/src/claude/skills/scout/SKILL.md +61 -0
- package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
- package/src/claude/skills/specs/SKILL.md +22 -29
- package/src/claude/skills/specs/references/review.md +8 -6
- package/src/claude/skills/specs/references/templates.md +38 -44
- package/src/claude/skills/sync/SKILL.md +8 -15
- package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
- package/src/claude/skills/sync/references/sync-protocols.md +4 -13
- package/src/claude/skills/test/SKILL.md +22 -3
- package/src/claude/skills/test/references/execution-strategy.md +83 -0
- package/src/claude/skills/test/references/failure-triage.md +1 -1
- package/src/claude/skills/ui-ux/LICENSE +21 -0
- package/src/claude/skills/ui-ux/SKILL.md +497 -0
- package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
- package/src/claude/skills/ui-ux/references/budgets.md +113 -0
- package/src/claude/skills/ui-ux/references/checklist.md +271 -0
- package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
- package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
- package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
- package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
- package/src/claude/skills/ui-ux/references/components/button.md +142 -0
- package/src/claude/skills/ui-ux/references/components/card.md +113 -0
- package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
- package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
- package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
- package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
- package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
- package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
- package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
- package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
- package/src/claude/skills/ui-ux/references/components/input.md +161 -0
- package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
- package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
- package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
- package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
- package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
- package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
- package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
- package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
- package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
- package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
- package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
- package/src/claude/skills/ui-ux/references/design-process.md +650 -0
- package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
- package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
- package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
- package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
- package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
- package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
- package/src/claude/skills/ui-ux/references/principles.md +408 -0
- package/src/claude/skills/ui-ux/references/refactor.md +245 -0
- package/src/claude/skills/ui-ux/references/responsive.md +195 -0
- package/src/claude/skills/ui-ux/references/review.md +528 -0
- package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
- package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
- package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
- package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
- package/src/claude/skills/ui-ux/references/styles.md +311 -0
- package/src/claude/skills/ui-ux/references/system.md +219 -0
- package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
- package/src/claude/skills/ui-ux/references/tokens.css +362 -0
- package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
- package/src/claude/skills/ui-ux/scripts/package.json +9 -0
- package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/status.cjs +149 -144
- package/src/codex/AGENTS.md +8 -13
- package/src/codex/hooks/agent.cjs +2 -1
- package/src/codex/hooks/lib/spec-receipt.cjs +5 -0
- package/src/codex/hooks/lib/spec-utils.cjs +0 -7
- package/src/codex/hooks/rules.cjs +2 -1
- package/src/codex/hooks/session.cjs +0 -2
- package/src/codex/hooks/spec-gate.cjs +8 -18
- package/src/codex/hooks.json +0 -20
- package/src/codex/rules/state-sync.md +3 -12
- package/src/common/AGENTS.md +2 -2
- package/src/omp/extensions/cafekit-bridge.mjs +6 -6
- package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
- package/src/claude/hooks/completion-authority-check.cjs +0 -56
- package/src/claude/hooks/completion-authority-state.cjs +0 -527
- package/src/claude/hooks/completion-authority.cjs +0 -163
- package/src/claude/hooks/semantic-review-authority.cjs +0 -154
- package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
- package/src/claude/hooks/usage.cjs +0 -197
- package/src/claude/skills/delegate/SKILL.md +0 -124
- package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
- package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
- package/src/claude/skills/hotfix/SKILL.md +0 -289
- package/src/claude/skills/inspect/SKILL.md +0 -142
- package/src/claude/skills/loop/SKILL.md +0 -114
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
- package/src/claude/skills/route/SKILL.md +0 -94
- package/src/claude/skills/route/references/agent-timing.md +0 -46
- package/src/claude/skills/route/references/chaining-patterns.md +0 -53
- package/src/claude/skills/route/references/task-taxonomy.md +0 -48
- package/src/codex/hooks/completion-authority-check.cjs +0 -67
- package/src/codex/hooks/completion-authority-state.cjs +0 -527
- package/src/codex/hooks/completion-authority.cjs +0 -109
- package/src/codex/hooks/semantic-review-authority.cjs +0 -130
- package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
- /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
- /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
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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'),
|
|
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')),
|
|
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'),
|
|
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'),
|