@haposoft/cafekit 0.16.0-rc.7 → 0.16.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 +116 -27
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +295 -0
- package/bin/__tests__/codex-native.test.js +2289 -34
- package/bin/__tests__/develop-contract.test.js +400 -23
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +2131 -16
- package/bin/__tests__/runtime-schema.test.js +111 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
- package/bin/install.js +10 -0
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +14 -0
- package/bin/lib/i18n.js +12 -6
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +8 -2
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +24 -13
- package/src/claude/agents/strategist.md +52 -0
- package/src/claude/agents/test-runner.md +117 -132
- package/src/claude/agents/ui-ux-designer.md +1 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
- package/src/claude/hooks/__tests__/precompact.test.js +158 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +87 -4
- package/src/claude/hooks/__tests__/state.test.js +254 -3
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +1 -1
- package/src/claude/hooks/docs-sync.cjs +12 -10
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +3 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +105 -0
- package/src/claude/hooks/rules.cjs +1 -1
- package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
- package/src/claude/hooks/session.cjs +27 -1
- package/src/claude/hooks/spec-gate.cjs +3 -3
- package/src/claude/hooks/spec-state.cjs +32 -5
- package/src/claude/hooks/state.cjs +1 -1
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/hooks/usage.cjs +1 -1
- package/src/claude/migration-manifest.json +29 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/workflow.md +3 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +18 -5
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/scripts/workflow-policy.cjs +1 -1
- package/src/claude/settings/settings.json +16 -0
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/brainstorm/SKILL.md +193 -172
- package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
- package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +43 -25
- package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
- package/src/claude/skills/code-review/references/verification-gate.md +1 -1
- package/src/claude/skills/debug/SKILL.md +48 -19
- package/src/claude/skills/delegate/SKILL.md +1 -1
- package/src/claude/skills/develop/SKILL.md +92 -65
- package/src/claude/skills/develop/references/parallel-waves.md +36 -63
- package/src/claude/skills/develop/references/quality-gate.md +31 -4
- package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
- package/src/claude/skills/docs/SKILL.md +42 -23
- package/src/claude/skills/docs/references/init-workflow.md +4 -4
- package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
- package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
- package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +9 -7
- package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/commit-protocols.md +1 -1
- package/src/claude/skills/git/references/finish-branch.md +1 -1
- package/src/claude/skills/hotfix/SKILL.md +159 -170
- package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
- package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
- package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
- package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
- package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
- package/src/claude/skills/inspect/SKILL.md +55 -46
- package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
- package/src/claude/skills/loop/SKILL.md +114 -0
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/question/SKILL.md +16 -13
- package/src/claude/skills/research/SKILL.md +70 -43
- package/src/claude/skills/route/SKILL.md +94 -0
- package/src/claude/skills/route/references/agent-timing.md +46 -0
- package/src/claude/skills/route/references/chaining-patterns.md +53 -0
- package/src/claude/skills/route/references/task-taxonomy.md +48 -0
- package/src/claude/skills/specs/SKILL.md +48 -57
- package/src/claude/skills/specs/references/review.md +26 -8
- package/src/claude/skills/specs/references/templates.md +76 -31
- package/src/claude/skills/specs/templates/requirements-init.md +1 -1
- package/src/claude/skills/specs/templates/research.md +2 -2
- package/src/claude/skills/sync/SKILL.md +6 -6
- package/src/claude/skills/sync/references/sync-protocols.md +1 -1
- package/src/claude/skills/test/SKILL.md +120 -120
- package/src/claude/skills/test/references/execution-strategy.md +208 -361
- package/src/claude/skills/test/references/failure-triage.md +55 -121
- package/src/claude/skills/test/references/test-memory.md +20 -34
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/skills/web-testing/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/claude/status.cjs +135 -33
- package/src/codex/AGENTS.md +14 -4
- package/src/codex/hooks/docs-sync.cjs +8 -3
- package/src/codex/hooks/lib/hook-context.cjs +18 -4
- package/src/codex/hooks/rules.cjs +29 -1
- package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
- package/src/codex/hooks/spec-gate.cjs +3 -8
- package/src/codex/hooks/spec-state.cjs +34 -5
- package/src/codex/hooks.json +5 -0
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// runtime.json is the only configuration surface a CafeKit user edits by hand, so the
|
|
4
|
+
// schema beside it has to stay true: every shipped key described, every documented key
|
|
5
|
+
// still real, and no key silently drifting out of sync with what the runtime honours.
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { test } = require('node:test');
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
13
|
+
const SCHEMA_PATH = path.join(PACKAGE_ROOT, 'src/claude/runtime.schema.json');
|
|
14
|
+
const SCHEMA = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
15
|
+
const RUNTIMES = ['src/claude/runtime.json', 'src/codex/runtime.json'];
|
|
16
|
+
const MANIFEST = require(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'));
|
|
17
|
+
|
|
18
|
+
function readRuntime(relative) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, relative), 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Walk a config object against a schema node, collecting undescribed key paths. */
|
|
23
|
+
function undescribedKeys(value, schemaNode, trail = []) {
|
|
24
|
+
if (!schemaNode || typeof value !== 'object' || value === null || Array.isArray(value)) return [];
|
|
25
|
+
const properties = schemaNode.properties || {};
|
|
26
|
+
const found = [];
|
|
27
|
+
for (const [key, child] of Object.entries(value)) {
|
|
28
|
+
const childSchema = properties[key];
|
|
29
|
+
if (!childSchema) {
|
|
30
|
+
if (schemaNode.additionalProperties === true || typeof schemaNode.additionalProperties === 'object') continue;
|
|
31
|
+
found.push([...trail, key].join('.'));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
found.push(...undescribedKeys(child, childSchema, [...trail, key]));
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Every property node in the schema, keyed by dotted path. */
|
|
40
|
+
function schemaProperties(node, trail = [], out = new Map()) {
|
|
41
|
+
for (const [key, child] of Object.entries(node.properties || {})) {
|
|
42
|
+
const dotted = [...trail, key].join('.');
|
|
43
|
+
out.set(dotted, child);
|
|
44
|
+
schemaProperties(child, [...trail, key], out);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('the schema is a valid, self-describing JSON Schema document', () => {
|
|
50
|
+
assert.equal(SCHEMA.$schema, 'https://json-schema.org/draft/2020-12/schema');
|
|
51
|
+
assert.equal(SCHEMA.type, 'object');
|
|
52
|
+
assert.ok(SCHEMA.title, 'the schema needs a title');
|
|
53
|
+
assert.ok(SCHEMA.description, 'the schema needs a description');
|
|
54
|
+
assert.equal(SCHEMA.additionalProperties, false, 'an unknown top-level key must be reported, not accepted silently');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('every key in both shipped runtime.json files is described by the schema', () => {
|
|
58
|
+
for (const relative of RUNTIMES) {
|
|
59
|
+
const missing = undescribedKeys(readRuntime(relative), SCHEMA);
|
|
60
|
+
assert.deepEqual(missing, [], `${relative} ships keys the schema does not describe: ${missing.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('both runtimes point at the schema that installs beside them', () => {
|
|
65
|
+
for (const relative of RUNTIMES) {
|
|
66
|
+
assert.equal(readRuntime(relative).$schema, './runtime.schema.json', `${relative} must reference the schema`);
|
|
67
|
+
}
|
|
68
|
+
assert.ok(
|
|
69
|
+
MANIFEST.runtime.files.includes('runtime.schema.json'),
|
|
70
|
+
'the schema must be installed for Claude, or its $schema reference dangles'
|
|
71
|
+
);
|
|
72
|
+
const codexPhase = fs.readFileSync(path.join(PACKAGE_ROOT, 'bin/phases/codex-runtime.js'), 'utf8');
|
|
73
|
+
assert.match(codexPhase, /runtime\.schema\.json/, 'the schema must also be installed for Codex');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('every documented property carries a description a reader can act on', () => {
|
|
77
|
+
for (const [dotted, node] of schemaProperties(SCHEMA)) {
|
|
78
|
+
if (dotted === '$schema') continue;
|
|
79
|
+
assert.ok(node.description, `${dotted} has no description`);
|
|
80
|
+
assert.ok(node.description.length > 20, `${dotted} has a description too short to help: ${node.description}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('keys the runtime does not honour are marked deprecated, not documented as working', () => {
|
|
85
|
+
// develop.parallel and the hooks toggle map are still shipped for compatibility, but
|
|
86
|
+
// nothing reads them. The schema must say so rather than imply they work.
|
|
87
|
+
for (const dead of ['hooks', 'develop']) {
|
|
88
|
+
const node = SCHEMA.properties[dead];
|
|
89
|
+
assert.ok(node, `${dead} is shipped in runtime.json and must appear in the schema`);
|
|
90
|
+
assert.equal(node.deprecated, true, `${dead} is not honoured and must be marked deprecated`);
|
|
91
|
+
assert.match(node.description, /[Nn]ot honoured/, `${dead} must say plainly that it does nothing`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('the completion gate flag is documented as a block, never as a switch', () => {
|
|
96
|
+
const node = SCHEMA.properties.spec.properties.completion_gate;
|
|
97
|
+
assert.match(node.description, /[Nn]ot a switch/);
|
|
98
|
+
assert.match(node.description, /blocks/, 'the description must say the gate blocks while the flag is present');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('statusline values in the schema match the modes the renderer implements', () => {
|
|
102
|
+
const status = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/status.cjs'), 'utf8');
|
|
103
|
+
const implemented = new Set((status.match(/case '([a-z]+)':/g) || []).map((line) => line.slice(6, -2)));
|
|
104
|
+
for (const mode of SCHEMA.properties.statusline.enum) {
|
|
105
|
+
assert.ok(implemented.has(mode), `schema offers statusline "${mode}" but the renderer has no branch for it`);
|
|
106
|
+
}
|
|
107
|
+
const sections = SCHEMA.properties.statuslineLayout.properties.lines.items.items.enum;
|
|
108
|
+
for (const section of sections) {
|
|
109
|
+
assert.ok(status.includes(`'${section}'`), `schema offers layout section "${section}" but status.cjs never names it`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The privacy hook gates whether a sensitive file may be READ. Nothing gated what
|
|
4
|
+
// happens after that approval, so an approved read could still put a raw key into the
|
|
5
|
+
// transcript — which is written to disk, survives compaction, and is replayed to the
|
|
6
|
+
// model every later turn. This hook closes that gap with a reminder, on both runtimes.
|
|
7
|
+
|
|
8
|
+
const assert = require('node:assert/strict');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { spawnSync } = require('node:child_process');
|
|
13
|
+
const { test } = require('node:test');
|
|
14
|
+
|
|
15
|
+
const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
16
|
+
const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
|
|
17
|
+
const KEYWORDS = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'));
|
|
18
|
+
|
|
19
|
+
function inTempProject(run) {
|
|
20
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-secret-guardrail-'));
|
|
21
|
+
try {
|
|
22
|
+
spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
23
|
+
return run(root);
|
|
24
|
+
} finally {
|
|
25
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function install(root, platform) {
|
|
30
|
+
const result = spawnSync(process.execPath, [INSTALLER, '--platform', platform, '--yes'],
|
|
31
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
32
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
33
|
+
return path.join(root, `.${platform}`, 'hooks', 'secret-output-guardrail.cjs');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function run(hookPath, root, prompt) {
|
|
37
|
+
const result = spawnSync(process.execPath, [hookPath], {
|
|
38
|
+
cwd: root,
|
|
39
|
+
input: JSON.stringify({ session_id: 'guardrail-session', prompt, cwd: root }),
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
});
|
|
42
|
+
assert.equal(result.status, 0, `hook must fail open: ${result.stderr}`);
|
|
43
|
+
return result.stdout;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('the matcher fires on credential topics and stays quiet otherwise', () => {
|
|
47
|
+
for (const prompt of [
|
|
48
|
+
'read the .env file', 'rotate our credentials', 'where is the secret stored',
|
|
49
|
+
'print the API key', 'the access key expired', 'this token is stale',
|
|
50
|
+
'send a bearer header', 'decode the JWT', 'load the private key',
|
|
51
|
+
'the oauth client secret', 'client secret rotation', 'our cloud keys', 'check the env file',
|
|
52
|
+
]) {
|
|
53
|
+
assert.equal(KEYWORDS.containsSecretKeyword(prompt), true, `should match: ${prompt}`);
|
|
54
|
+
}
|
|
55
|
+
for (const prompt of [
|
|
56
|
+
'add two numbers', 'refactor the tokenizer', 'update the environment setup docs',
|
|
57
|
+
'fix the keyboard shortcut', '', ' ',
|
|
58
|
+
]) {
|
|
59
|
+
assert.equal(KEYWORDS.containsSecretKeyword(prompt), false, `should not match: ${prompt}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the reminder separates permission to read from permission to print', () => {
|
|
64
|
+
assert.match(KEYWORDS.REMINDER, /does not grant permission to print raw values/);
|
|
65
|
+
assert.match(KEYWORDS.REMINDER, /\[redacted\]/);
|
|
66
|
+
assert.match(KEYWORDS.REMINDER, /report only success or failure/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
for (const platform of ['claude', 'codex']) {
|
|
70
|
+
test(`${platform}: a credential prompt gets the reminder, an ordinary one gets nothing`, () => {
|
|
71
|
+
inTempProject((root) => {
|
|
72
|
+
const hook = install(root, platform);
|
|
73
|
+
assert.equal(fs.existsSync(path.join(root, `.${platform}`, 'hooks', 'lib', 'secret-keywords.cjs')), true,
|
|
74
|
+
'the shared matcher must install beside the hook');
|
|
75
|
+
|
|
76
|
+
const flagged = run(hook, root, 'please read .env and tell me what is set');
|
|
77
|
+
assert.match(flagged, /## Secret handling/);
|
|
78
|
+
assert.match(flagged, /does not grant permission to print raw values/);
|
|
79
|
+
|
|
80
|
+
assert.equal(run(hook, root, 'rename this variable'), '', 'an unrelated prompt must inject nothing');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test(`${platform}: the guardrail never echoes the prompt or a matched value`, () => {
|
|
85
|
+
inTempProject((root) => {
|
|
86
|
+
const hook = install(root, platform);
|
|
87
|
+
// A realistic-looking secret in the prompt must not come back in the output —
|
|
88
|
+
// a guardrail that quotes its input would itself write the value to the transcript.
|
|
89
|
+
const out = run(hook, root, 'my api key is sk-live-abc123SECRETVALUE, is it valid?');
|
|
90
|
+
assert.match(out, /## Secret handling/);
|
|
91
|
+
assert.doesNotMatch(out, /sk-live-abc123SECRETVALUE/, 'the hook echoed the secret it was warning about');
|
|
92
|
+
assert.doesNotMatch(out, /is it valid/, 'the hook echoed the prompt');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test(`${platform}: the hook fails open on malformed input`, () => {
|
|
97
|
+
inTempProject((root) => {
|
|
98
|
+
const hook = install(root, platform);
|
|
99
|
+
for (const input of ['not json', '', '{"prompt":null}']) {
|
|
100
|
+
const result = spawnSync(process.execPath, [hook], { cwd: root, input, encoding: 'utf8' });
|
|
101
|
+
assert.equal(result.status, 0, `malformed input must not fail the turn: ${JSON.stringify(input)}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test(`${platform}: the hook is registered for UserPromptSubmit`, () => {
|
|
107
|
+
inTempProject((root) => {
|
|
108
|
+
install(root, platform);
|
|
109
|
+
const configPath = platform === 'claude'
|
|
110
|
+
? path.join(root, '.claude', 'settings.json')
|
|
111
|
+
: path.join(root, '.codex', 'hooks.json');
|
|
112
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
113
|
+
const commands = (config.hooks.UserPromptSubmit || [])
|
|
114
|
+
.flatMap((group) => group.hooks || [])
|
|
115
|
+
.map((handler) => handler.command || '');
|
|
116
|
+
assert.ok(
|
|
117
|
+
commands.some((command) => command.includes('secret-output-guardrail.cjs')),
|
|
118
|
+
`${platform} never runs the guardrail`
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const { test } = require('node:test');
|
|
9
|
+
const { normalizeCodexBody } = require('../lib/codex-install');
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
12
|
+
const SCANNER = path.join(PACKAGE_ROOT, 'src/claude/scripts/generate-skill-catalog.cjs');
|
|
13
|
+
const MANIFEST = JSON.parse(fs.readFileSync(
|
|
14
|
+
path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8'
|
|
15
|
+
));
|
|
16
|
+
|
|
17
|
+
function withTempRoot(run) {
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-routing-'));
|
|
19
|
+
try {
|
|
20
|
+
return run(root);
|
|
21
|
+
} finally {
|
|
22
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeSkill(root, directory, fields = {}) {
|
|
27
|
+
const target = path.join(root, directory);
|
|
28
|
+
fs.mkdirSync(target, { recursive: true });
|
|
29
|
+
if (fields.raw) {
|
|
30
|
+
fs.writeFileSync(path.join(target, 'SKILL.md'), fields.raw);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const lines = ['---'];
|
|
34
|
+
if (fields.name !== undefined) lines.push(`name: ${fields.name}`);
|
|
35
|
+
if (fields.description !== undefined) lines.push(`description: "${fields.description}"`);
|
|
36
|
+
if (fields.when !== undefined) lines.push(`when_to_use: "${fields.when}"`);
|
|
37
|
+
if (fields.category !== undefined) lines.push(`category: ${fields.category}`);
|
|
38
|
+
if (fields.keywords !== undefined) lines.push(`keywords: [${fields.keywords.join(', ')}]`);
|
|
39
|
+
lines.push('user-invocable: true', '---', `# ${directory}`, '');
|
|
40
|
+
fs.writeFileSync(path.join(target, 'SKILL.md'), `${lines.join('\n')}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runCatalog(script, cwd, args = ['--json']) {
|
|
44
|
+
const result = spawnSync(process.execPath, [script, ...args], { cwd, encoding: 'utf8' });
|
|
45
|
+
assert.equal(result.status, 0, result.stderr);
|
|
46
|
+
return JSON.parse(result.stdout);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function installScanner(source, destination, transform = (content) => content) {
|
|
50
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
51
|
+
fs.writeFileSync(destination, transform(fs.readFileSync(source, 'utf8')));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('skill routing consumes live catalog without fixed optional commands', () => {
|
|
55
|
+
withTempRoot((root) => {
|
|
56
|
+
const claudeScript = path.join(root, '.claude/scripts/generate-skill-catalog.cjs');
|
|
57
|
+
const codexScript = path.join(root, '.codex/scripts/generate-skill-catalog.cjs');
|
|
58
|
+
installScanner(SCANNER, claudeScript);
|
|
59
|
+
installScanner(SCANNER, codexScript, (content) => normalizeCodexBody(content, SCANNER));
|
|
60
|
+
|
|
61
|
+
writeSkill(path.join(root, '.claude/skills'), 'question', {
|
|
62
|
+
name: 'cf:ask', description: 'Answer with evidence.', when: 'Use for factual questions.',
|
|
63
|
+
category: 'utilities', keywords: ['answer', 'evidence'],
|
|
64
|
+
});
|
|
65
|
+
writeSkill(path.join(root, '.claude/skills'), 'docs', {
|
|
66
|
+
name: 'cf:docs', description: 'Work with docs.', when: 'Use for documentation.',
|
|
67
|
+
category: 'documents', keywords: ['docs'],
|
|
68
|
+
});
|
|
69
|
+
writeSkill(path.join(root, '.agents/skills'), 'question', {
|
|
70
|
+
name: 'cf-ask', description: 'Answer with evidence.', when: 'Use for factual questions.',
|
|
71
|
+
category: 'utilities', keywords: ['answer', 'evidence'],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const claude = runCatalog(claudeScript, root);
|
|
75
|
+
const codex = runCatalog(codexScript, root);
|
|
76
|
+
assert.equal(claude.root, fs.realpathSync(path.join(root, '.claude/skills')));
|
|
77
|
+
assert.equal(codex.root, fs.realpathSync(path.join(root, '.agents/skills')));
|
|
78
|
+
assert.deepEqual(claude.skills.map((skill) => skill.public_id), ['cf:ask', 'cf:docs']);
|
|
79
|
+
assert.deepEqual(codex.skills.map((skill) => skill.public_id), ['cf:ask']);
|
|
80
|
+
assert.equal(codex.skills.some((skill) => skill.public_id === 'cf:docs'), false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const workflow = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/skill-workflow-routing.md'), 'utf8');
|
|
84
|
+
const domain = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/skill-domain-routing.md'), 'utf8');
|
|
85
|
+
const codexEntry = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/codex/AGENTS.md'), 'utf8');
|
|
86
|
+
assert.match(workflow, /user names a valid installed skill[\s\S]*one obvious low-risk[\s\S]*direct factual conversation/);
|
|
87
|
+
assert.match(workflow, /do not invoke Route or agents for ceremony/);
|
|
88
|
+
assert.match(workflow.replace(/\s+/g, ' '), /numeric optimization capability remains explicit-only/);
|
|
89
|
+
assert.match(domain, /examples below are intent hints, not a copied installed inventory/);
|
|
90
|
+
assert.match(domain, /document\/artifact work \| use only a matching installed optional capability/);
|
|
91
|
+
assert.doesNotMatch(domain, /\/cf:(?:docs|docx|pdf|pptx|xlsx|ai-multimodal)/);
|
|
92
|
+
assert.match(codexEntry, /node \.codex\/scripts\/generate-skill-catalog\.cjs --skills/);
|
|
93
|
+
assert.match(codexEntry, /Codex-bound[\s\S]*\.agents\/skills/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('skill catalog exposes discriminating routing metadata', () => {
|
|
97
|
+
withTempRoot((root) => {
|
|
98
|
+
const skills = path.join(root, 'skills');
|
|
99
|
+
writeSkill(skills, 'route', {
|
|
100
|
+
name: 'cf:route', description: 'Choose a bounded chain.',
|
|
101
|
+
when: 'Use for ambiguous multi-step work.', category: 'utilities',
|
|
102
|
+
keywords: ['routing', 'risk'],
|
|
103
|
+
});
|
|
104
|
+
writeSkill(skills, 'question', {
|
|
105
|
+
name: 'cf:ask', description: 'First duplicate.', when: 'Use for questions.',
|
|
106
|
+
category: 'utilities', keywords: ['answer'],
|
|
107
|
+
});
|
|
108
|
+
writeSkill(skills, 'ask', {
|
|
109
|
+
name: 'cf:ask', description: 'Second duplicate.', when: 'Use for questions.',
|
|
110
|
+
category: 'utilities', keywords: ['answer'],
|
|
111
|
+
});
|
|
112
|
+
writeSkill(skills, 'missing-description', { name: 'cf:missing-description' });
|
|
113
|
+
writeSkill(skills, 'mismatch', { name: 'cf:wrong', description: 'Wrong folder.' });
|
|
114
|
+
writeSkill(skills, 'malformed', { raw: '# no frontmatter\n' });
|
|
115
|
+
writeSkill(skills, 'bad-line', {
|
|
116
|
+
raw: '---\nname: cf:bad-line\ndescription: Valid-looking description.\nthis is not yaml\n---\n',
|
|
117
|
+
});
|
|
118
|
+
writeSkill(skills, 'unterminated', {
|
|
119
|
+
raw: '---\nname: cf:unterminated\ndescription: "unterminated\n---\n',
|
|
120
|
+
});
|
|
121
|
+
writeSkill(skills, 'blank-description', {
|
|
122
|
+
raw: '---\nname: cf:blank-description\ndescription: " "\n---\n',
|
|
123
|
+
});
|
|
124
|
+
writeSkill(skills, 'invalid-flow', {
|
|
125
|
+
raw: '---\nname: cf:invalid-flow\ndescription: [unterminated\n---\n',
|
|
126
|
+
});
|
|
127
|
+
writeSkill(skills, 'duplicate-key', {
|
|
128
|
+
raw: '---\nname: cf:wrong\nname: cf:duplicate-key\ndescription: Duplicate name.\n---\n',
|
|
129
|
+
});
|
|
130
|
+
writeSkill(skills, 'backend-development', {
|
|
131
|
+
name: 'cf:backend-development', description: 'User-modified retired skill.',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const catalog = runCatalog(SCANNER, root, ['--json', '--root', skills]);
|
|
135
|
+
assert.deepEqual(catalog.skills.map((skill) => skill.public_id), ['cf:route']);
|
|
136
|
+
assert.deepEqual(catalog.skills[0], {
|
|
137
|
+
name: 'cf:route', public_id: 'cf:route', directory: 'route',
|
|
138
|
+
description: 'Choose a bounded chain.', when_to_use: 'Use for ambiguous multi-step work.',
|
|
139
|
+
category: 'utilities', keywords: ['routing', 'risk'], user_invocable: true,
|
|
140
|
+
has_references: false, has_scripts: false,
|
|
141
|
+
});
|
|
142
|
+
const codes = catalog.diagnostics.map((item) => item.code);
|
|
143
|
+
assert.equal(codes.filter((code) => code === 'duplicate_public_name').length, 2);
|
|
144
|
+
for (const required of [
|
|
145
|
+
'folder_name_mismatch', 'malformed_frontmatter', 'missing_routing_metadata', 'retired_skill',
|
|
146
|
+
]) assert.ok(codes.includes(required), required);
|
|
147
|
+
for (const directory of ['bad-line', 'unterminated', 'duplicate-key', 'invalid-flow']) {
|
|
148
|
+
assert.ok(catalog.diagnostics.some((item) =>
|
|
149
|
+
item.directory === directory && item.code === 'malformed_frontmatter'), directory);
|
|
150
|
+
}
|
|
151
|
+
assert.ok(catalog.diagnostics.some((item) =>
|
|
152
|
+
item.directory === 'blank-description' && item.code === 'missing_routing_metadata'));
|
|
153
|
+
assert.ok(catalog.diagnostics.every((item) => item.code !== 'duplicate_public_name'
|
|
154
|
+
|| item.message.includes('explicit user disambiguation')));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const sourceCatalog = runCatalog(SCANNER, PACKAGE_ROOT);
|
|
158
|
+
const retiredDiagnostics = sourceCatalog.diagnostics
|
|
159
|
+
.filter((item) => item.code === 'retired_skill').map((item) => item.directory).sort();
|
|
160
|
+
assert.deepEqual(retiredDiagnostics, [...MANIFEST.obsolete.skills].sort());
|
|
161
|
+
assert.equal(sourceCatalog.skills.some((skill) => MANIFEST.obsolete.skills.includes(skill.directory)), false);
|
|
162
|
+
const route = sourceCatalog.skills.find((skill) => skill.public_id === 'cf:route');
|
|
163
|
+
assert.ok(route?.description && route.when_to_use && route.category && route.keywords.length > 0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('skill catalog rejects a missing root value before path resolution', () => {
|
|
167
|
+
const result = spawnSync(process.execPath, [SCANNER, '--root', '--json'], {
|
|
168
|
+
cwd: PACKAGE_ROOT, encoding: 'utf8',
|
|
169
|
+
});
|
|
170
|
+
assert.equal(result.status, 1);
|
|
171
|
+
assert.match(result.stderr, /--root requires a skills directory/);
|
|
172
|
+
});
|
|
@@ -263,7 +263,7 @@ function installGate(root, kind) {
|
|
|
263
263
|
'scripts/workflow-policy.cjs', 'scripts/provenance.cjs', 'scripts/spec-resolver.cjs',
|
|
264
264
|
'scripts/spec-receipt.cjs', 'scripts/validate-spec-output.cjs', 'scripts/spec-ground.cjs',
|
|
265
265
|
'scripts/spec-semantic-model.cjs', 'scripts/spec-final-state.cjs',
|
|
266
|
-
'hooks/lib/runtime-path-safety.cjs',
|
|
266
|
+
'hooks/lib/runtime-path-safety.cjs', 'hooks/lib/hook-state-dir.cjs',
|
|
267
267
|
]);
|
|
268
268
|
if (kind === 'claude') {
|
|
269
269
|
for (const name of [
|
package/bin/install.js
CHANGED
|
@@ -22,6 +22,7 @@ const backup = require('./lib/backup');
|
|
|
22
22
|
const manifestLib = require('./lib/manifest');
|
|
23
23
|
|
|
24
24
|
const { selectLanguage, resolvePlatforms } = require('./phases/select-platform');
|
|
25
|
+
const { selectDocumentSkills } = require('./phases/select-skill-bundles');
|
|
25
26
|
const { copyPlatformFiles } = require('./phases/copy-payload');
|
|
26
27
|
const {
|
|
27
28
|
copyRoutingFile,
|
|
@@ -30,8 +31,10 @@ const {
|
|
|
30
31
|
copyClaudeMdFile,
|
|
31
32
|
ensureSharedAgentsMdCore,
|
|
32
33
|
copyRulesDirectory,
|
|
34
|
+
copyOutputStylesDirectory,
|
|
33
35
|
removeObsoleteAgents
|
|
34
36
|
} = require('./phases/claude-runtime');
|
|
37
|
+
const { reconcileSkillInventory } = require('./phases/skill-inventory');
|
|
35
38
|
const { mergeClaudeSettings } = require('./phases/claude-settings');
|
|
36
39
|
const { installCodexRuntime } = require('./phases/codex-runtime');
|
|
37
40
|
const { writePlatformVersionMetadata } = require('./phases/write-metadata');
|
|
@@ -58,6 +61,7 @@ function installPlatform(ctx, platformKey) {
|
|
|
58
61
|
ctx.ui.startSpinner(ctx.t('installingPlatform', { name: platform.name }));
|
|
59
62
|
|
|
60
63
|
copyPlatformFiles(ctx, platformKey);
|
|
64
|
+
reconcileSkillInventory(ctx, platformKey);
|
|
61
65
|
removeObsoleteAgents(ctx, platformKey);
|
|
62
66
|
|
|
63
67
|
if (platformKey === 'claude') {
|
|
@@ -67,6 +71,7 @@ function installPlatform(ctx, platformKey) {
|
|
|
67
71
|
mergeClaudeSettings(ctx, platformKey);
|
|
68
72
|
copyClaudeMdFile(ctx, platformKey);
|
|
69
73
|
copyRulesDirectory(ctx, platformKey);
|
|
74
|
+
copyOutputStylesDirectory(ctx, platformKey);
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
if (platformKey === 'codex') {
|
|
@@ -102,6 +107,8 @@ Options:
|
|
|
102
107
|
Chromium/Playwright). Otherwise prompted interactively.
|
|
103
108
|
--with-rtk Install the rtk token-saver (binary + Claude Code hook).
|
|
104
109
|
Otherwise prompted interactively.
|
|
110
|
+
--with-document-skills Install docs/DOCX/PDF/PPTX/XLSX/multimodal skills
|
|
111
|
+
--without-document-skills Skip or remove CafeKit-owned document skills
|
|
105
112
|
-y, --yes Non-interactive: skip prompts, use defaults (CI)
|
|
106
113
|
-h, --help Show this help
|
|
107
114
|
-v, --version Print version
|
|
@@ -162,6 +169,9 @@ async function main() {
|
|
|
162
169
|
process.exit(result.status ?? 1);
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
await selectDocumentSkills(ctx);
|
|
173
|
+
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
174
|
+
|
|
165
175
|
// ── Pre-run snapshot for rollback ─────────────────────
|
|
166
176
|
// Capture platform folders AND the root files the pipeline mutates
|
|
167
177
|
// (CLAUDE.md, .gitignore) so a mid-run failure rolls back cleanly.
|
package/bin/lib/codex-install.js
CHANGED
|
@@ -25,7 +25,6 @@ const AGENT_NAMES = [
|
|
|
25
25
|
'ui-ux-designer'
|
|
26
26
|
];
|
|
27
27
|
const INSTRUCTION_REPLACEMENTS = [
|
|
28
|
-
[/`?\bAskUserQuestion\b`?/g, 'a structured user-input request'],
|
|
29
28
|
[/`?\bTodoWrite\b`?/g, 'task-state tracking'],
|
|
30
29
|
[/`?\bTaskCreate\b`?/g, 'task creation'],
|
|
31
30
|
[/`?\bTaskUpdate\b`?/g, 'task-state updates'],
|
|
@@ -64,6 +63,172 @@ function applyReplacements(content, replacements) {
|
|
|
64
63
|
return next;
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
function isMarkdownLinkDestination(content, offset) {
|
|
67
|
+
const opener = content.lastIndexOf('](', offset);
|
|
68
|
+
if (opener < 0 || content.lastIndexOf('[', opener) < 0) return false;
|
|
69
|
+
|
|
70
|
+
let depth = 1;
|
|
71
|
+
for (let index = opener + 2; index < offset; index += 1) {
|
|
72
|
+
if (content[index] === '\\') {
|
|
73
|
+
index += 1;
|
|
74
|
+
} else if (content[index] === '(') {
|
|
75
|
+
depth += 1;
|
|
76
|
+
} else if (content[index] === ')') {
|
|
77
|
+
depth -= 1;
|
|
78
|
+
if (depth === 0) return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return depth > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isUrlLikeToken(content, offset) {
|
|
85
|
+
const prefix = content.slice(0, offset).split(/[\s]/).at(-1) || '';
|
|
86
|
+
return /^<?(?:[a-z][a-z0-9+.-]*:\/\/|mailto:|www\.)\S*$/i.test(prefix);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function maskMarkdownCodeSpans(content) {
|
|
90
|
+
const spans = [];
|
|
91
|
+
let markerNonce = 0;
|
|
92
|
+
while (content.includes(`\uE000CAFEKIT_${markerNonce}_`)) markerNonce += 1;
|
|
93
|
+
const markerPrefix = `\uE000CAFEKIT_${markerNonce}_`;
|
|
94
|
+
let masked = '';
|
|
95
|
+
let index = 0;
|
|
96
|
+
|
|
97
|
+
const addMarker = (kind, original) => {
|
|
98
|
+
const marker = `${markerPrefix}${kind}_${spans.length}\uE001`;
|
|
99
|
+
spans.push({ marker, original });
|
|
100
|
+
return marker;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const fencedBlockEnd = (openerLineEnd, delimiter, delimiterLength) => {
|
|
104
|
+
let lineStart = openerLineEnd < 0 ? content.length : openerLineEnd + 1;
|
|
105
|
+
while (lineStart < content.length) {
|
|
106
|
+
const newline = content.indexOf('\n', lineStart);
|
|
107
|
+
const lineEnd = newline < 0 ? content.length : newline;
|
|
108
|
+
const line = content.slice(lineStart, lineEnd);
|
|
109
|
+
const match = line.match(/^[ \t]{0,3}([`~]+)[ \t]*\r?$/);
|
|
110
|
+
if (match
|
|
111
|
+
&& [...match[1]].every((character) => character === delimiter)
|
|
112
|
+
&& match[1].length >= delimiterLength) {
|
|
113
|
+
return lineEnd;
|
|
114
|
+
}
|
|
115
|
+
lineStart = newline < 0 ? content.length : newline + 1;
|
|
116
|
+
}
|
|
117
|
+
return content.length;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
while (index < content.length) {
|
|
121
|
+
if (content[index] !== '`' && content[index] !== '~') {
|
|
122
|
+
masked += content[index];
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const opener = index;
|
|
128
|
+
const delimiter = content[index];
|
|
129
|
+
while (index < content.length && content[index] === delimiter) index += 1;
|
|
130
|
+
const delimiterLength = index - opener;
|
|
131
|
+
const lineStart = content.lastIndexOf('\n', opener - 1) + 1;
|
|
132
|
+
const linePrefix = content.slice(lineStart, opener);
|
|
133
|
+
const openerLineEnd = content.indexOf('\n', index);
|
|
134
|
+
const infoEnd = openerLineEnd < 0 ? content.length : openerLineEnd;
|
|
135
|
+
const info = content.slice(index, infoEnd);
|
|
136
|
+
const isFence = delimiterLength >= 3
|
|
137
|
+
&& /^[ \t]{0,3}$/.test(linePrefix)
|
|
138
|
+
&& (delimiter === '~' || !info.includes('`'));
|
|
139
|
+
|
|
140
|
+
if (isFence) {
|
|
141
|
+
const end = fencedBlockEnd(openerLineEnd, delimiter, delimiterLength);
|
|
142
|
+
masked += addMarker('CODE', content.slice(opener, end));
|
|
143
|
+
index = end;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (delimiter !== '`') {
|
|
148
|
+
masked += content.slice(opener, index);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let cursor = index;
|
|
153
|
+
let closer = -1;
|
|
154
|
+
while (cursor < content.length) {
|
|
155
|
+
const runStart = content.indexOf('`', cursor);
|
|
156
|
+
if (runStart < 0) break;
|
|
157
|
+
let runEnd = runStart;
|
|
158
|
+
while (runEnd < content.length && content[runEnd] === '`') runEnd += 1;
|
|
159
|
+
if (runEnd - runStart === delimiterLength) {
|
|
160
|
+
closer = runStart;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
cursor = runEnd;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (closer < 0) {
|
|
167
|
+
const newline = content.indexOf('\n', opener);
|
|
168
|
+
const end = newline < 0 ? content.length : newline;
|
|
169
|
+
masked += addMarker('CODE', content.slice(opener, end));
|
|
170
|
+
index = end;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const end = closer + delimiterLength;
|
|
175
|
+
const original = content.slice(opener, end);
|
|
176
|
+
const inner = content.slice(opener + delimiterLength, closer);
|
|
177
|
+
const kind = inner.trim() === 'AskUserQuestion' ? 'ASK' : 'CODE';
|
|
178
|
+
masked += addMarker(kind, original);
|
|
179
|
+
index = end;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: masked,
|
|
184
|
+
askMarkerPattern: `${markerPrefix}ASK_[0-9]+\uE001`,
|
|
185
|
+
askMarkerPrefix: `${markerPrefix}ASK_`,
|
|
186
|
+
restore(value) {
|
|
187
|
+
return spans.reduce(
|
|
188
|
+
(next, { marker, original }) => next.split(marker).join(original),
|
|
189
|
+
value
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeAskUserQuestion(content) {
|
|
196
|
+
const masked = maskMarkdownCodeSpans(content);
|
|
197
|
+
const determinerPattern = [
|
|
198
|
+
'one', 'each', 'every', 'another', 'this', 'that', 'a', 'an', 'the'
|
|
199
|
+
].map((word) => [...word].map((letter) => `[${letter}${letter.toUpperCase()}]`).join('')).join('|');
|
|
200
|
+
const oneSeparator = '(?:[^\\S\\r\\n]+|[^\\S\\r\\n]*\\r?\\n[^\\S\\r\\n]*)';
|
|
201
|
+
const pattern = new RegExp(
|
|
202
|
+
`(?:(\\b(?:${determinerPattern}|[0-9]+)\\b)(${oneSeparator}))?`
|
|
203
|
+
+ `(\\bAskUserQuestion\\b|${masked.askMarkerPattern})`,
|
|
204
|
+
'g'
|
|
205
|
+
);
|
|
206
|
+
const normalized = masked.content.replace(pattern, (match, determiner, spacing, _token, offset, source) => {
|
|
207
|
+
const literalIndex = match.indexOf('AskUserQuestion');
|
|
208
|
+
const tokenOffset = offset + (literalIndex >= 0
|
|
209
|
+
? literalIndex
|
|
210
|
+
: match.indexOf(masked.askMarkerPrefix));
|
|
211
|
+
const previous = source[tokenOffset - 1] || '';
|
|
212
|
+
if (isMarkdownLinkDestination(source, tokenOffset)
|
|
213
|
+
|| isUrlLikeToken(source, tokenOffset)
|
|
214
|
+
|| /[/=?#&%@]/.test(previous)) {
|
|
215
|
+
return match;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (determiner) {
|
|
219
|
+
const normalizedDeterminer = /^an$/i.test(determiner)
|
|
220
|
+
? (determiner[0] === 'A' ? 'A' : 'a')
|
|
221
|
+
: determiner;
|
|
222
|
+
return `${normalizedDeterminer}${spacing}structured user-input request`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const trailing = source.slice(offset + match.length);
|
|
226
|
+
const pluralContext = new RegExp(`^${oneSeparator}(?:calls|batches)\\b`, 'i').test(trailing);
|
|
227
|
+
return `${pluralContext ? '' : 'a '}structured user-input request`;
|
|
228
|
+
});
|
|
229
|
+
return masked.restore(normalized);
|
|
230
|
+
}
|
|
231
|
+
|
|
67
232
|
function normalizeRuntimePaths(content) {
|
|
68
233
|
return normalizeSourcePaths(content, {
|
|
69
234
|
runtimeRoot: '.codex',
|
|
@@ -78,11 +243,11 @@ function normalizeRuntimePaths(content) {
|
|
|
78
243
|
|
|
79
244
|
function normalizeSkillNames(content) {
|
|
80
245
|
return content
|
|
81
|
-
.replace(/\/
|
|
82
|
-
.replace(/\
|
|
246
|
+
.replace(/\/cf:([a-z0-9-]+)/gi, (_match, name) => `$cf-${name}`)
|
|
247
|
+
.replace(/\bcf:([a-z0-9-]+)/gi, (_match, name) => `cf-${name}`)
|
|
83
248
|
.replace(
|
|
84
249
|
/(^|[\s("'`→])\/(brainstorm|code-review|debug|develop|docs|frontend-design|git|hotfix|inspect|question|research|specs|test)(?=$|[\s<`),.:])/gim,
|
|
85
|
-
(_match, prefix, name) => `${prefix}$
|
|
250
|
+
(_match, prefix, name) => `${prefix}$cf-${name}`
|
|
86
251
|
);
|
|
87
252
|
}
|
|
88
253
|
|
|
@@ -129,7 +294,7 @@ function normalizeCodexBody(content, sourcePath = '') {
|
|
|
129
294
|
if (!isInstructionAsset(sourcePath)) return next;
|
|
130
295
|
|
|
131
296
|
next = normalizeAgentInvocations(normalizeAgentNames(normalizeSkillNames(next)));
|
|
132
|
-
return applyReplacements(next, INSTRUCTION_REPLACEMENTS);
|
|
297
|
+
return applyReplacements(normalizeAskUserQuestion(next), INSTRUCTION_REPLACEMENTS);
|
|
133
298
|
}
|
|
134
299
|
|
|
135
300
|
function getCodexCopyOptions(baseOptions = {}) {
|