@haposoft/cafekit 0.16.7 → 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 +74 -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 +17 -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 +24 -7
- package/src/claude/scripts/spec-resolver.cjs +1 -2
- package/src/claude/scripts/validate-spec-output.cjs +5 -32
- 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 +34 -41
- 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 +2 -2
- 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 +7 -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
|
@@ -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
|
-
|
|
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'), '
|
|
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'), '
|
|
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
|
|
88
|
-
assert.
|
|
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, '
|
|
100
|
-
name: 'cf:
|
|
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: '
|
|
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: '
|
|
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:
|
|
136
|
-
assert.deepEqual(catalog.skills
|
|
137
|
-
name: 'cf:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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), /
|
|
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
|
|
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, /
|
|
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.
|
|
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
|
|
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
|
|
1585
|
+
completeSemanticReview(criticalDir);
|
|
1606
1586
|
const criticalResult = run(VALIDATOR, [criticalDir], ROOT);
|
|
1607
|
-
assert.
|
|
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);
|