@haposoft/cafekit 0.16.7 → 0.18.1
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 +169 -272
- package/bin/__tests__/codex-native.test.js +100 -202
- package/bin/__tests__/develop-contract.test.js +122 -1413
- 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 +154 -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 +90 -925
- 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 +30 -161
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- 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 +18 -2949
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +27 -0
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +25 -0
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +3 -10
- package/src/claude/agents/brainstormer.md +7 -3
- package/src/claude/agents/code-auditor.md +44 -56
- package/src/claude/agents/debugger.md +8 -88
- package/src/claude/agents/deployer.md +3 -5
- package/src/claude/agents/docs-keeper.md +6 -14
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +12 -16
- package/src/claude/agents/inspector.md +4 -3
- package/src/claude/agents/project-manager.md +9 -17
- package/src/claude/agents/researcher.md +9 -3
- package/src/claude/agents/spec-maker.md +15 -15
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -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/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 -83
- package/src/claude/hooks/spec-state.cjs +142 -39
- package/src/claude/hooks/state.cjs +21 -2
- package/src/claude/migration-manifest.json +42 -22
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +2 -3
- 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 +11 -19
- 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 -121
- package/src/claude/scripts/spec-resolver.cjs +41 -458
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -32
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/{question → ask}/SKILL.md +9 -75
- package/src/claude/skills/brainstorm/SKILL.md +43 -131
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +29 -81
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
- package/src/claude/skills/debug/SKILL.md +29 -149
- package/src/claude/skills/develop/SKILL.md +26 -83
- 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/develop/references/subagent-patterns.md +0 -6
- 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 +17 -46
- 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 +16 -45
- 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 +39 -104
- package/src/claude/skills/test/references/execution-strategy.md +93 -21
- 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 +25 -80
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- 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 +4 -32
- package/src/codex/hooks/lib/spec-utils.cjs +6 -52
- package/src/codex/hooks/rules.cjs +2 -1
- package/src/codex/hooks/session.cjs +0 -2
- package/src/codex/hooks/spec-gate.cjs +13 -81
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/codex/hooks.json +0 -20
- package/src/codex/rules/state-sync.md +3 -12
- package/src/common/AGENTS.md +6 -12
- package/src/omp/extensions/cafekit-bridge.mjs +6 -6
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- 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/lib/context.cjs +0 -616
- package/src/claude/hooks/lib/detect.cjs +0 -474
- 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/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3941
- package/src/claude/skills/code-review/references/verification-gate.md +0 -46
- 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/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- 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', () => {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// ambiguity is what left a repository of accumulated features permanently blocked, with
|
|
8
|
-
// the documented escape — an explicit feature target — unavailable.
|
|
3
|
+
// The Stop gate narrows an ambiguous scan by asking which packet still has unfinished
|
|
4
|
+
// work, so it can validate that packet's receipts. Counting finished history as ambiguity
|
|
5
|
+
// is what left a repository of accumulated features permanently blocked, with the
|
|
6
|
+
// documented escape — an explicit feature target — unavailable.
|
|
9
7
|
|
|
10
8
|
const assert = require('node:assert/strict');
|
|
11
9
|
const { spawnSync } = require('node:child_process');
|
|
@@ -38,7 +36,7 @@ function legacyPacket(root, name, { status, phase, taskStatus = 'done' } = {}) {
|
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
/** A process-first packet: plan.md plus one flat task file. */
|
|
41
|
-
function workflowPacket(root, name, taskStatus) {
|
|
39
|
+
function workflowPacket(root, name, taskStatus, nextStatus = null) {
|
|
42
40
|
const dir = path.join(root, 'specs', name);
|
|
43
41
|
fs.mkdirSync(dir, { recursive: true });
|
|
44
42
|
fs.writeFileSync(path.join(dir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
@@ -46,6 +44,12 @@ function workflowPacket(root, name, taskStatus) {
|
|
|
46
44
|
path.join(dir, 'task-01-x.md'),
|
|
47
45
|
`# Task 01\n\nStatus: ${taskStatus}\n\n## Dependencies\n- none\n`
|
|
48
46
|
);
|
|
47
|
+
if (nextStatus) {
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
path.join(dir, 'task-02-y.md'),
|
|
50
|
+
`# Task 02\n\nStatus: ${nextStatus}\n\n## Dependencies\n- none\n`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
function recordActiveFeature(root, contents) {
|
|
@@ -63,34 +67,19 @@ function gateResolution(root) {
|
|
|
63
67
|
|
|
64
68
|
// ── The gate: which packet still has work ───────────────────────────────────────────
|
|
65
69
|
|
|
66
|
-
test('a legacy
|
|
70
|
+
test('a leftover legacy packet creates no ambiguity for the gate', () => {
|
|
67
71
|
withProject((root) => {
|
|
68
72
|
for (let index = 1; index <= 3; index += 1) {
|
|
69
|
-
legacyPacket(root, `
|
|
73
|
+
legacyPacket(root, `history-${index}`, { status: 'in_progress', phase: 'execution', taskStatus: 'pending' });
|
|
70
74
|
}
|
|
71
|
-
|
|
75
|
+
workflowPacket(root, 'active-now', 'pending');
|
|
72
76
|
const resolved = gateResolution(root);
|
|
73
77
|
assert.equal(resolved.error, undefined, `expected a resolution, got ${resolved.error}`);
|
|
74
78
|
assert.equal(resolved.featureName, 'active-now');
|
|
79
|
+
assert.deepEqual(RESOLVER.findLegacyPackets(root, {}), ['history-1', 'history-2', 'history-3']);
|
|
75
80
|
});
|
|
76
81
|
});
|
|
77
82
|
|
|
78
|
-
test('a legacy registry written with the older vocabulary counts as finished', () => {
|
|
79
|
-
// Issue #79's repository was upgraded from an older release whose task_registry used
|
|
80
|
-
// `completed`. Today's validator allows only pending/in_progress/blocked/done, so
|
|
81
|
-
// narrowing read those packets as unfinished and the block survived the fix that was
|
|
82
|
-
// supposed to clear it. Measured on the reported shape before this: 18 candidates.
|
|
83
|
-
for (const status of ['done', 'completed', 'complete']) {
|
|
84
|
-
withProject((root) => {
|
|
85
|
-
for (let index = 1; index <= 3; index += 1) {
|
|
86
|
-
legacyPacket(root, `history-${index}`, { taskStatus: status });
|
|
87
|
-
}
|
|
88
|
-
legacyPacket(root, 'active-now', { status: 'in_progress', phase: 'execution', taskStatus: 'pending' });
|
|
89
|
-
assert.equal(gateResolution(root).featureName, 'active-now', `task status ${status} must read as finished`);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
83
|
test('a process-first packet keeps the strict task vocabulary', () => {
|
|
95
84
|
// The widened set is legacy-only. A process-first task saying `completed` is malformed,
|
|
96
85
|
// not finished, and must not be silently read as done.
|
|
@@ -102,20 +91,6 @@ test('a process-first packet keeps the strict task vocabulary', () => {
|
|
|
102
91
|
});
|
|
103
92
|
});
|
|
104
93
|
|
|
105
|
-
test('a legacy candidate never reaches the bulk branch', () => {
|
|
106
|
-
// The bulk branch exits before the semantic-digest, FLASH_UNVERIFIED, feature-receipt
|
|
107
|
-
// and completion-policy layers, so a legacy packet reaching it would turn a block into
|
|
108
|
-
// a silent pass. With every legacy packet finished there is nothing to narrow to, and
|
|
109
|
-
// the existing ambiguity must stand rather than the set being audited.
|
|
110
|
-
withProject((root) => {
|
|
111
|
-
legacyPacket(root, 'done-1', { status: 'done', phase: 'closeout' });
|
|
112
|
-
legacyPacket(root, 'done-2', { status: 'done', phase: 'closeout' });
|
|
113
|
-
const resolved = gateResolution(root);
|
|
114
|
-
assert.notEqual(resolved.layoutKind, 'process-v3-completed-set');
|
|
115
|
-
assert.ok(resolved.error, 'an all-legacy finished set must stay ambiguous');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
94
|
test('a process-first set still reaches the bulk branch', () => {
|
|
120
95
|
withProject((root) => {
|
|
121
96
|
workflowPacket(root, 'done-1', 'done');
|
|
@@ -132,116 +107,21 @@ test('one unfinished process-first packet still narrows', () => {
|
|
|
132
107
|
});
|
|
133
108
|
});
|
|
134
109
|
|
|
135
|
-
// ── Closeout approval: which packet is claiming closeout ────────────────────────────
|
|
136
|
-
|
|
137
|
-
test('one packet claiming closeout among finished ones resolves', () => {
|
|
138
|
-
withProject((root) => {
|
|
139
|
-
legacyPacket(root, 'shipped', { status: 'in_progress', phase: 'execution', taskStatus: 'done' });
|
|
140
|
-
legacyPacket(root, 'closing', { status: 'done', phase: 'closeout' });
|
|
141
|
-
const resolved = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
|
|
142
|
-
assert.equal(resolved.error, undefined);
|
|
143
|
-
assert.equal(resolved.featureName, 'closing');
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('a lone finished packet still resolves', () => {
|
|
148
|
-
// Four existing cases depend on this: closeout approval is claimed against a finished
|
|
149
|
-
// spec, so a single packet must resolve whatever its status.
|
|
150
|
-
for (const status of ['done', 'completed']) {
|
|
151
|
-
withProject((root) => {
|
|
152
|
-
legacyPacket(root, 'only', { status });
|
|
153
|
-
const resolved = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
|
|
154
|
-
assert.equal(resolved.featureName, 'only', `status ${status} must still resolve`);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('a lone packet mid-execution still resolves', () => {
|
|
160
|
-
// The closeout filter would drop this one, so the single-candidate return above it is
|
|
161
|
-
// what keeps a repository holding exactly one in-flight packet resolvable. Without it
|
|
162
|
-
// the hook goes silent for that repository instead of checking the packet.
|
|
163
|
-
withProject((root) => {
|
|
164
|
-
legacyPacket(root, 'solo', { status: 'in_progress', phase: 'implementation', taskStatus: 'pending' });
|
|
165
|
-
const resolved = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
|
|
166
|
-
assert.equal(resolved.featureName, 'solo');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('no packet claiming closeout resolves to null', () => {
|
|
171
|
-
// null means nothing is in flight and the hook stays silent; an error would keep
|
|
172
|
-
// blocking a repository where no closeout is being claimed at all.
|
|
173
|
-
withProject((root) => {
|
|
174
|
-
legacyPacket(root, 'a', { status: 'in_progress', phase: 'execution' });
|
|
175
|
-
legacyPacket(root, 'b', { status: 'in_progress', phase: 'execution' });
|
|
176
|
-
assert.equal(RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} }), null);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('several packets claiming closeout stay ambiguous and name only those', () => {
|
|
181
|
-
withProject((root) => {
|
|
182
|
-
legacyPacket(root, 'closing-a', { status: 'done', phase: 'closeout' });
|
|
183
|
-
legacyPacket(root, 'closing-b', { status: 'done', phase: 'closeout' });
|
|
184
|
-
legacyPacket(root, 'working', { status: 'in_progress', phase: 'execution' });
|
|
185
|
-
const resolved = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
|
|
186
|
-
assert.equal(resolved.error, 'multiple_persisted');
|
|
187
|
-
assert.deepEqual(resolved.candidates, ['closing-a', 'closing-b']);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
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
110
|
// ── The recorded active feature: the escape the block told users to take ────────────
|
|
207
111
|
|
|
208
|
-
test('a recorded active feature resolves two packets claiming closeout', () => {
|
|
209
|
-
withProject((root) => {
|
|
210
|
-
legacyPacket(root, 'closing-a', { status: 'done', phase: 'closeout' });
|
|
211
|
-
legacyPacket(root, 'closing-b', { status: 'done', phase: 'closeout' });
|
|
212
|
-
assert.equal(RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} }).error, 'multiple_persisted');
|
|
213
|
-
recordActiveFeature(root, '{"featureName": "closing-b"}\n');
|
|
214
|
-
const resolved = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
|
|
215
|
-
assert.equal(resolved.error, undefined, `expected a resolution, got ${resolved.error}`);
|
|
216
|
-
assert.equal(resolved.featureName, 'closing-b');
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('a payload target beats the recorded file', () => {
|
|
221
|
-
withProject((root) => {
|
|
222
|
-
legacyPacket(root, 'alpha', { status: 'done', phase: 'closeout' });
|
|
223
|
-
legacyPacket(root, 'beta', { status: 'done', phase: 'closeout' });
|
|
224
|
-
recordActiveFeature(root, '{"featureName": "alpha"}\n');
|
|
225
|
-
const resolved = RESOLVER.resolvePersistedSpec({
|
|
226
|
-
projectRoot: root, runtime: {}, target: { featureName: 'beta' },
|
|
227
|
-
});
|
|
228
|
-
assert.equal(resolved.featureName, 'beta', 'the caller must win');
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
112
|
test('a blank recorded feature is ignored rather than malformed', () => {
|
|
233
113
|
// explicitTargetValue turns a blank into null, and a null target becomes an
|
|
234
114
|
// explicit_malformed error that blocks. The reader must return null instead, or every
|
|
235
115
|
// project holding a stray file would be blocked by the very escape hatch.
|
|
236
116
|
for (const contents of ['{"featureName": " "}', '{"featureName": 42}', '{}', 'not json', '[]']) {
|
|
237
117
|
withProject((root) => {
|
|
238
|
-
|
|
118
|
+
workflowPacket(root, 'only', 'pending');
|
|
239
119
|
recordActiveFeature(root, contents);
|
|
240
120
|
assert.equal(
|
|
241
121
|
RESOLVER.readActiveFeatureTarget({ projectRoot: root, runtime: {} }), null,
|
|
242
122
|
`${contents} must be invisible`
|
|
243
123
|
);
|
|
244
|
-
assert.equal(RESOLVER.
|
|
124
|
+
assert.equal(RESOLVER.resolveWorkflowCandidate({ projectRoot: root, runtime: {} }).featureName, 'only');
|
|
245
125
|
});
|
|
246
126
|
}
|
|
247
127
|
});
|
|
@@ -249,11 +129,12 @@ test('a blank recorded feature is ignored rather than malformed', () => {
|
|
|
249
129
|
test('a recorded feature cannot escape the specs root', () => {
|
|
250
130
|
// The reader adds no trust: the value goes through the resolver's existing checks.
|
|
251
131
|
withProject((root) => {
|
|
252
|
-
|
|
253
|
-
|
|
132
|
+
workflowPacket(root, 'a', 'pending');
|
|
133
|
+
workflowPacket(root, 'b', 'pending');
|
|
254
134
|
recordActiveFeature(root, '{"featureName": "../../etc"}');
|
|
255
|
-
const
|
|
256
|
-
|
|
135
|
+
const target = RESOLVER.readActiveFeatureTarget({ projectRoot: root, runtime: {} });
|
|
136
|
+
const resolved = RESOLVER.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, target });
|
|
137
|
+
assert.equal(resolved.error, 'explicit_malformed', 'an escaping name must produce the existing error, not a resolution');
|
|
257
138
|
assert.notEqual(resolved.featureName, '../../etc');
|
|
258
139
|
});
|
|
259
140
|
});
|
|
@@ -263,7 +144,7 @@ test('a symlinked recorded feature is refused', () => {
|
|
|
263
144
|
// project does not contain. The repository already refuses symlinked runtime
|
|
264
145
|
// dependencies elsewhere; this reader follows the same rule.
|
|
265
146
|
withProject((root) => {
|
|
266
|
-
|
|
147
|
+
workflowPacket(root, 'only', 'pending');
|
|
267
148
|
const outside = path.join(root, 'outside.json');
|
|
268
149
|
fs.writeFileSync(outside, '{"featureName": "only"}');
|
|
269
150
|
const dir = path.join(root, 'specs', '_shared');
|
|
@@ -275,32 +156,20 @@ test('a symlinked recorded feature is refused', () => {
|
|
|
275
156
|
|
|
276
157
|
test('an absent file leaves every project exactly as it was', () => {
|
|
277
158
|
withProject((root) => {
|
|
278
|
-
|
|
159
|
+
workflowPacket(root, 'only', 'pending');
|
|
279
160
|
assert.equal(RESOLVER.readActiveFeatureTarget({ projectRoot: root, runtime: {} }), null);
|
|
280
|
-
assert.equal(RESOLVER.
|
|
161
|
+
assert.equal(RESOLVER.resolveWorkflowCandidate({ projectRoot: root, runtime: {} }).featureName, 'only');
|
|
281
162
|
});
|
|
282
163
|
});
|
|
283
164
|
|
|
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
165
|
test('the shared workflow resolver ignores the recorded file', () => {
|
|
297
166
|
// resolveWorkflowCandidate has five consumers, including spec-state.cjs and
|
|
298
167
|
// precompact.cjs. Reading the file inside it would retarget hooks that never asked,
|
|
299
168
|
// so the two gates apply it at their own call sites instead. Neither of those suites
|
|
300
169
|
// writes the file, so nothing else pins this containment.
|
|
301
170
|
withProject((root) => {
|
|
302
|
-
|
|
303
|
-
|
|
171
|
+
workflowPacket(root, 'alpha', 'done');
|
|
172
|
+
workflowPacket(root, 'beta', 'done');
|
|
304
173
|
recordActiveFeature(root, '{"featureName": "beta"}');
|
|
305
174
|
const resolved = RESOLVER.resolveWorkflowCandidate({
|
|
306
175
|
projectRoot: root, runtime: {}, target: null, includeCompleted: true,
|
|
@@ -315,7 +184,7 @@ test('the codex gate consults the file only after its payload', () => {
|
|
|
315
184
|
assert.equal(typeof utils.readActiveFeatureTarget, 'function', 'the wrapper must be exported');
|
|
316
185
|
assert.equal(typeof utils.extractExplicitTarget, 'function', 'the gate needs the payload extractor');
|
|
317
186
|
withProject((root) => {
|
|
318
|
-
|
|
187
|
+
workflowPacket(root, 'alpha', 'done');
|
|
319
188
|
recordActiveFeature(root, '{"featureName": "alpha"}');
|
|
320
189
|
assert.deepEqual(
|
|
321
190
|
utils.extractExplicitTarget({ featureName: 'beta' }) || utils.readActiveFeatureTarget({ projectRoot: root, runtime: {} }),
|
|
@@ -365,8 +234,8 @@ for (const [runtimeName, hook] of [['claude', 'src/claude/hooks/spec-gate.cjs']]
|
|
|
365
234
|
// identity complaint whose stated remedy nothing could supply; naming the feature
|
|
366
235
|
// makes the gate resolve it and report the violation that was there all along.
|
|
367
236
|
gitProject((dir) => {
|
|
368
|
-
|
|
369
|
-
|
|
237
|
+
workflowPacket(dir, 'alpha', 'done', 'pending');
|
|
238
|
+
workflowPacket(dir, 'beta', 'done', 'pending');
|
|
370
239
|
|
|
371
240
|
const blocked = runGate(hook, dir);
|
|
372
241
|
assert.match(blocked, /multiple active specs detected/, 'the reported block must reproduce');
|