@haposoft/cafekit 0.16.0-rc.8 → 0.16.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 +117 -28
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +175 -0
- package/bin/__tests__/codex-native.test.js +1882 -70
- package/bin/__tests__/develop-contract.test.js +442 -23
- package/bin/__tests__/grok-envelope.test.js +271 -0
- package/bin/__tests__/grok-platform.test.js +133 -0
- package/bin/__tests__/hook-payload.test.js +194 -0
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/omp-bridge.test.js +168 -0
- package/bin/__tests__/omp-hooks.test.js +178 -0
- package/bin/__tests__/omp-runtime.test.js +150 -0
- 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 +1735 -98
- package/bin/__tests__/runtime-dir.test.js +158 -0
- package/bin/__tests__/runtime-schema.test.js +113 -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 +28 -1
- package/bin/install.js +15 -5
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +64 -3
- package/bin/lib/i18n.js +15 -36
- 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 +15 -2
- package/bin/phases/omp-runtime.js +140 -0
- package/bin/phases/root-config.js +3 -0
- package/bin/phases/select-platform.js +22 -1
- 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/summary.js +0 -4
- 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 +12 -7
- 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__/semantic-review-authority.test.js +2 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +348 -3
- package/src/claude/hooks/__tests__/state.test.js +7 -0
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +13 -8
- package/src/claude/hooks/completion-authority.cjs +10 -4
- package/src/claude/hooks/docs-sync.cjs +14 -11
- package/src/claude/hooks/inspect-block.cjs +28 -8
- package/src/claude/hooks/lib/config.cjs +8 -2
- package/src/claude/hooks/lib/counter.cjs +7 -5
- package/src/claude/hooks/lib/hook-payload.cjs +160 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +106 -0
- package/src/claude/hooks/privacy-block.cjs +4 -2
- package/src/claude/hooks/rules.cjs +5 -3
- package/src/claude/hooks/secret-output-guardrail.cjs +44 -0
- package/src/claude/hooks/semantic-review-authority.cjs +6 -4
- package/src/claude/hooks/session.cjs +37 -9
- package/src/claude/hooks/spec-gate.cjs +21 -11
- package/src/claude/hooks/spec-state.cjs +11 -7
- package/src/claude/hooks/state.cjs +7 -5
- package/src/claude/hooks/task-scaffold-guard.cjs +30 -9
- package/src/claude/hooks/usage.cjs +6 -5
- package/src/claude/migration-manifest.json +31 -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/state-sync.md +7 -1
- package/src/claude/rules/workflow.md +5 -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 +29 -9
- package/src/claude/scripts/spec-receipt.cjs +65 -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 +51 -71
- package/src/claude/skills/specs/references/review.md +11 -8
- package/src/claude/skills/specs/references/templates.md +34 -18
- 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 +3 -2
- package/src/codex/hooks.json +5 -0
- package/src/codex/rules/state-sync.md +7 -1
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/omp/extensions/cafekit-bridge.mjs +233 -0
- package/src/omp/hooks/privacy-block.cjs +250 -0
- package/src/omp/runtime.json +33 -0
- package/bin/lib/rtk-run.js +0 -28
- package/bin/phases/setup-rtk.js +0 -159
- 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,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// omp has no hooks.json. The bridge is the only thing between omp's lifecycle events and
|
|
4
|
+
// CafeKit's gate scripts, so a mistranslation disables every gate while looking installed.
|
|
5
|
+
// These tests drive the bridge's exported functions against real child hook processes from
|
|
6
|
+
// the tree an omp install produces: the Claude hook set with the omp overlay written over
|
|
7
|
+
// it. They do not launch omp, which needs provider credentials; the contract rows they rely
|
|
8
|
+
// on were read from the installed omp binary and are recorded in plan.md.
|
|
9
|
+
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { test } = require('node:test');
|
|
15
|
+
|
|
16
|
+
const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
17
|
+
const BRIDGE = path.join(PACKAGE_ROOT, 'src/omp/extensions/cafekit-bridge.mjs');
|
|
18
|
+
const SETTINGS = require(path.join(PACKAGE_ROOT, 'src/claude/settings/settings.json'));
|
|
19
|
+
const MANIFEST = require(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'));
|
|
20
|
+
const { PREFIX } = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/completion-authority-state.cjs'));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The hooks an omp install runs: the Claude set the manifest ships, then `src/omp/hooks/`
|
|
24
|
+
* written over it. Composed once under a dotted folder so runtime-dir.cjs derives `.omp`.
|
|
25
|
+
*/
|
|
26
|
+
const COMPOSED_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-composed-'));
|
|
27
|
+
const HOOKS = path.join(COMPOSED_ROOT, '.omp', 'hooks');
|
|
28
|
+
for (const rel of MANIFEST.runtime.files.filter((f) => f.startsWith('hooks/'))) {
|
|
29
|
+
const target = path.join(COMPOSED_ROOT, '.omp', rel);
|
|
30
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
31
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude', rel), target);
|
|
32
|
+
}
|
|
33
|
+
fs.cpSync(path.join(PACKAGE_ROOT, 'src/omp/hooks'), HOOKS, { recursive: true });
|
|
34
|
+
fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/scripts'), path.join(COMPOSED_ROOT, '.omp', 'scripts'), { recursive: true });
|
|
35
|
+
|
|
36
|
+
let bridge;
|
|
37
|
+
test.before(async () => { bridge = await import(BRIDGE); });
|
|
38
|
+
test.after(() => { fs.rmSync(COMPOSED_ROOT, { recursive: true, force: true }); });
|
|
39
|
+
|
|
40
|
+
function tempProject(withSecret = true) {
|
|
41
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-bridge-'));
|
|
42
|
+
if (withSecret) fs.writeFileSync(path.join(root, '.env'), 'TOKEN=redacted\n');
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function registered(eventName) {
|
|
47
|
+
return (SETTINGS.hooks[eventName] || [])
|
|
48
|
+
.flatMap((g) => g.hooks || [])
|
|
49
|
+
.map((h) => (h.command.match(/hooks\/([a-z-]+\.cjs)/) || [])[1])
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('the dispatch table mirrors settings.json for every event omp can deliver', () => {
|
|
54
|
+
const { DISPATCH } = bridge;
|
|
55
|
+
const pairs = [
|
|
56
|
+
['session_start', 'SessionStart'], ['session_before_compact', 'PreCompact'],
|
|
57
|
+
['input', 'UserPromptSubmit'], ['tool_call', 'PreToolUse'],
|
|
58
|
+
['tool_result', 'PostToolUse'], ['session_stop', 'Stop'],
|
|
59
|
+
];
|
|
60
|
+
for (const [omp, claude] of pairs) {
|
|
61
|
+
assert.deepEqual([...DISPATCH[omp].hooks].sort(), [...new Set(registered(claude))].sort(),
|
|
62
|
+
`${omp} must dispatch exactly the hooks settings.json registers for ${claude}`);
|
|
63
|
+
}
|
|
64
|
+
// omp has no subagent events; the gap is deliberate and documented, not forgotten.
|
|
65
|
+
assert.ok(!Object.values(DISPATCH).some((e) => e.event.startsWith('Subagent')));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('every dispatch carries a stable session id and the project cwd', () => {
|
|
69
|
+
const { shapePayload } = bridge;
|
|
70
|
+
const ctx = { cwd: '/tmp/project' };
|
|
71
|
+
const a = shapePayload('input', { text: 'hi', source: 'user' }, ctx);
|
|
72
|
+
const b = shapePayload('tool_call', { toolName: 'bash', input: { command: 'ls' } }, ctx);
|
|
73
|
+
assert.ok(a.session_id && a.session_id === b.session_id,
|
|
74
|
+
'omp input carries no session id; rules.cjs exits without one, so the bridge must mint and reuse it');
|
|
75
|
+
assert.equal(a.cwd, '/tmp/project', 'spec-gate.cjs falls back to payload.cwd to find the project');
|
|
76
|
+
assert.equal(shapePayload('session_stop', { session_id: 'omp-real' }, ctx).session_id, 'omp-real',
|
|
77
|
+
'a session id omp does provide must win over the minted one');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('each denial mechanism becomes an omp block carrying the hook reason', () => {
|
|
81
|
+
const { interpretVerdict } = bridge;
|
|
82
|
+
const stop = interpretVerdict({ code: 0, stdout: '{"decision":"block","reason":"R1"}\n' }, 'spec-gate.cjs');
|
|
83
|
+
assert.deepEqual([stop.block, stop.reason], [true, 'R1']);
|
|
84
|
+
const deny = interpretVerdict({ code: 0, stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'R2' } }) }, 'privacy-block.cjs');
|
|
85
|
+
assert.deepEqual([deny.block, deny.reason], [true, 'R2']);
|
|
86
|
+
// inspect-block / task-scaffold-guard exit 2 and write the reason on STDOUT, not stderr.
|
|
87
|
+
const exit2 = interpretVerdict({ code: 2, stdout: 'SCOPE LIMIT EXCEEDED\nRestricted zones: node_modules', stderr: '' }, 'inspect-block.cjs');
|
|
88
|
+
assert.equal(exit2.block, true);
|
|
89
|
+
assert.match(exit2.reason, /SCOPE LIMIT EXCEEDED/, 'a stderr-only reader would drop this reason');
|
|
90
|
+
// An ask under Claude has no omp equivalent; it must not fall through to allow.
|
|
91
|
+
const ask = interpretVerdict({ code: 0, stdout: JSON.stringify({ hookSpecificOutput: { permissionDecision: 'ask', permissionDecisionReason: 'R3' } }) }, 'x');
|
|
92
|
+
assert.equal(ask.block, true);
|
|
93
|
+
// Plain text that is not a verdict is context to inject, not a block.
|
|
94
|
+
const inject = interpretVerdict({ code: 0, stdout: '## Rules\n- keep docs under 800 lines' }, 'rules.cjs');
|
|
95
|
+
assert.deepEqual([inject.block, inject.inject.startsWith('## Rules')], [false, true]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a lowercase bash command on a secret file is blocked end to end through the real fork', async () => {
|
|
99
|
+
const root = tempProject();
|
|
100
|
+
try {
|
|
101
|
+
const r = await bridge.runEvent('tool_call',
|
|
102
|
+
{ toolName: 'bash', toolCallId: 'c1', input: { command: `cat ${path.join(root, '.env')}` } },
|
|
103
|
+
{ cwd: root }, { dir: HOOKS });
|
|
104
|
+
assert.equal(r.block, true);
|
|
105
|
+
assert.equal(r.hook, 'privacy-block.cjs');
|
|
106
|
+
assert.match(r.reason, /\.env/, 'the block must name what it protected');
|
|
107
|
+
const ok = await bridge.runEvent('tool_call',
|
|
108
|
+
{ toolName: 'read', toolCallId: 'c2', input: { file_path: path.join(root, 'README.md') } },
|
|
109
|
+
{ cwd: root }, { dir: HOOKS });
|
|
110
|
+
assert.equal(ok.block, false, 'an ordinary read must not be blocked');
|
|
111
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a slow hook blocks with its own name before omp cuts it off', async () => {
|
|
115
|
+
const slow = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-slow-'));
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(path.join(slow, 'privacy-block.cjs'), 'setTimeout(() => {}, 60000);');
|
|
118
|
+
assert.ok(bridge.BRIDGE_TIMEOUT_MS < 30000, 'omp substitutes a reasonless block at 30000 ms');
|
|
119
|
+
const t0 = Date.now();
|
|
120
|
+
const r = await bridge.runEvent('tool_call', { toolName: 'bash', input: { command: 'ls' } }, { cwd: slow }, { dir: slow, timeoutMs: 300 });
|
|
121
|
+
assert.equal(r.block, true);
|
|
122
|
+
assert.match(r.reason, /privacy-block\.cjs exceeded 300 ms/);
|
|
123
|
+
assert.ok(Date.now() - t0 < 5000, 'the budget must actually fire');
|
|
124
|
+
} finally { fs.rmSync(slow, { recursive: true, force: true }); }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('a crashing or missing gate blocks on gating events instead of silently allowing', async () => {
|
|
128
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-crash-'));
|
|
129
|
+
try {
|
|
130
|
+
// Only the crashing gate is under test; the other two PreToolUse gates get allow stubs,
|
|
131
|
+
// otherwise their absence is what blocks (see the missing-gate case below).
|
|
132
|
+
fs.writeFileSync(path.join(dir, 'privacy-block.cjs'), 'throw new Error("boom");');
|
|
133
|
+
for (const stub of ['inspect-block.cjs', 'task-scaffold-guard.cjs']) fs.writeFileSync(path.join(dir, stub), 'process.exit(0);');
|
|
134
|
+
const crash = await bridge.runEvent('tool_call', { toolName: 'bash', input: { command: 'ls' } }, { cwd: dir }, { dir });
|
|
135
|
+
// A bare exit 1 with no verdict is an allow under the hooks' own contract. Real gates
|
|
136
|
+
// never surface a crash this way: the fork's privacy-block catch-all converts its own
|
|
137
|
+
// errors into a deny, which the fork tests pin separately.
|
|
138
|
+
assert.equal(crash.block, false, 'exit 1 with no verdict is an allow per the hooks\' own contract');
|
|
139
|
+
const missing = await bridge.runEvent('tool_call', { toolName: 'bash', input: { command: 'ls' } }, { cwd: dir }, { dir: path.join(dir, 'nowhere') });
|
|
140
|
+
assert.equal(missing.block, true, 'a gate that is not installed must be loud, not a silent allow');
|
|
141
|
+
assert.match(missing.reason, /not installed/);
|
|
142
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('stop_hook_active short-circuits the gate so a blocked turn cannot loop forever', async () => {
|
|
146
|
+
const r = await bridge.runEvent('session_stop', { stop_hook_active: true, session_id: 's1' }, { cwd: os.tmpdir() }, { dir: HOOKS });
|
|
147
|
+
assert.deepEqual([r.block, r.skipped], [false, 'stop_hook_active']);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('the approval phrase reaches completion-authority through the input event', async () => {
|
|
151
|
+
const { DISPATCH, shapePayload } = bridge;
|
|
152
|
+
assert.ok(DISPATCH.input.hooks.includes('completion-authority.cjs'),
|
|
153
|
+
'the --approve path only runs on UserPromptSubmit; without this routing a blocked turn has no exit');
|
|
154
|
+
const phrase = `${PREFIX}${'a'.repeat(24)}`;
|
|
155
|
+
const p = shapePayload('input', { text: phrase, source: 'user' }, { cwd: '/tmp/p' });
|
|
156
|
+
assert.equal(p.hook_event_name, 'UserPromptSubmit', 'completion-authority selects --approve from this field');
|
|
157
|
+
assert.equal(p.prompt, phrase, 'the phrase must arrive byte-exact; the hook compares it exactly');
|
|
158
|
+
assert.ok(p.session_id, 'approve() returns early without a session id');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('input injections flow back as a transform that keeps the user text intact', async () => {
|
|
162
|
+
const root = tempProject(false);
|
|
163
|
+
try {
|
|
164
|
+
const r = await bridge.runEvent('input', { text: 'please read .env for me', source: 'user' }, { cwd: root }, { dir: HOOKS });
|
|
165
|
+
assert.equal(r.block, false);
|
|
166
|
+
assert.match(r.inject, /Secret handling/, 'secret-output-guardrail must inject on a credential prompt');
|
|
167
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
168
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The gate scripts were authored against Claude Code's vocabulary. omp speaks a
|
|
4
|
+
// different one: its tool registry uses lowercase names, and its tool_call result has
|
|
5
|
+
// only { block, reason } — there is no ask state. `src/omp/hooks/` is an overlay holding
|
|
6
|
+
// only the files that carry those contract differences; the installer writes it over the
|
|
7
|
+
// portable Claude set. Each test states the failure it prevents, because an overlay that
|
|
8
|
+
// silently drifts back to the Claude contract disables a gate while looking installed.
|
|
9
|
+
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { spawnSync } = require('node:child_process');
|
|
15
|
+
const { test } = require('node:test');
|
|
16
|
+
|
|
17
|
+
const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
18
|
+
const OMP_HOOKS = path.join(PACKAGE_ROOT, 'src/omp/hooks');
|
|
19
|
+
const CLAUDE_HOOKS = path.join(PACKAGE_ROOT, 'src/claude/hooks');
|
|
20
|
+
const { normalizeToolName } = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/hook-payload.cjs'));
|
|
21
|
+
const MANIFEST = require(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The overlay is not runnable on its own: it requires the shared libraries the Claude set
|
|
25
|
+
* carries. Behaviour is therefore exercised on what an install produces — the manifest's
|
|
26
|
+
* hook set with the overlay written over it, under a dotted folder so runtime-dir derives
|
|
27
|
+
* `.omp` — while the source-shape checks below read `src/omp/hooks/` directly.
|
|
28
|
+
*/
|
|
29
|
+
const INSTALLED_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-installed-'));
|
|
30
|
+
const INSTALLED = path.join(INSTALLED_ROOT, '.omp', 'hooks');
|
|
31
|
+
for (const rel of MANIFEST.runtime.files.filter((f) => f.startsWith('hooks/'))) {
|
|
32
|
+
const target = path.join(INSTALLED_ROOT, '.omp', rel);
|
|
33
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
34
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude', rel), target);
|
|
35
|
+
}
|
|
36
|
+
fs.cpSync(OMP_HOOKS, INSTALLED, { recursive: true });
|
|
37
|
+
test.after(() => { fs.rmSync(INSTALLED_ROOT, { recursive: true, force: true }); });
|
|
38
|
+
|
|
39
|
+
function withSecretProject(run) {
|
|
40
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-hooks-'));
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync(path.join(root, '.env'), 'API_TOKEN=redacted\n');
|
|
43
|
+
return run(root);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Run a hook as a real child process and return its verdict, if any. */
|
|
50
|
+
function verdict(hookPath, payload) {
|
|
51
|
+
const result = spawnSync(process.execPath, [hookPath], {
|
|
52
|
+
input: typeof payload === 'string' ? payload : JSON.stringify(payload),
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
});
|
|
55
|
+
const out = (result.stdout || '').trim();
|
|
56
|
+
let decision = null;
|
|
57
|
+
try {
|
|
58
|
+
decision = out ? JSON.parse(out)?.hookSpecificOutput?.permissionDecision ?? null : null;
|
|
59
|
+
} catch { decision = null; }
|
|
60
|
+
return { code: result.status, out, decision };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('omp tool names map to the Claude names the rules were written against', () => {
|
|
64
|
+
for (const [omp, claude] of [['bash', 'Bash'], ['read', 'Read'], ['edit', 'Edit'], ['write', 'Write'], ['grep', 'Grep']]) {
|
|
65
|
+
assert.equal(normalizeToolName(omp), claude, `${omp} must reach the ${claude} rule`);
|
|
66
|
+
}
|
|
67
|
+
// An unknown tool is left alone rather than renamed into a rule it does not belong to.
|
|
68
|
+
assert.equal(normalizeToolName('some_future_tool'), 'some_future_tool');
|
|
69
|
+
assert.equal(normalizeToolName('Bash'), 'Bash', 'a Claude name must survive unchanged');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('a lowercase bash command reading a secret file is denied, where Claude asks', () => {
|
|
73
|
+
withSecretProject((root) => {
|
|
74
|
+
const payload = { tool_name: 'bash', tool_input: { command: `cat ${path.join(root, '.env')}` }, cwd: root };
|
|
75
|
+
|
|
76
|
+
// Both hooks now see the lowercase name, because the shared payload reader aliases it.
|
|
77
|
+
// What still differs is the answer: omp's tool_call result carries only block and
|
|
78
|
+
// reason, so an ask there would be dropped and the access allowed.
|
|
79
|
+
const claude = verdict(path.join(CLAUDE_HOOKS, 'privacy-block.cjs'), payload);
|
|
80
|
+
assert.equal(claude.decision, 'ask', 'the Claude hook now recognises the lowercase name and asks');
|
|
81
|
+
|
|
82
|
+
const omp = verdict(path.join(INSTALLED, 'privacy-block.cjs'), payload);
|
|
83
|
+
assert.equal(omp.decision, 'deny');
|
|
84
|
+
assert.match(omp.out, /\.env/, 'the denial must name what it protected');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a capitalised tool name keeps working, so the fork adds a case rather than swapping one', () => {
|
|
89
|
+
withSecretProject((root) => {
|
|
90
|
+
const omp = verdict(path.join(INSTALLED, 'privacy-block.cjs'),
|
|
91
|
+
{ tool_name: 'Bash', tool_input: { command: `cat ${path.join(root, '.env')}` }, cwd: root });
|
|
92
|
+
assert.equal(omp.decision, 'deny');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('the privacy hook denies where Claude asks, since omp tool_call has no ask state', () => {
|
|
97
|
+
const source = fs.readFileSync(path.join(OMP_HOOKS, 'privacy-block.cjs'), 'utf8');
|
|
98
|
+
assert.ok(!source.includes("permissionDecision: 'ask'"),
|
|
99
|
+
"omp's tool_call result carries only block and reason; an ask would be dropped and the access allowed");
|
|
100
|
+
assert.ok(source.includes("permissionDecision: 'deny'"));
|
|
101
|
+
|
|
102
|
+
withSecretProject((root) => {
|
|
103
|
+
const omp = verdict(path.join(INSTALLED, 'privacy-block.cjs'),
|
|
104
|
+
{ tool_name: 'read', tool_input: { file_path: path.join(root, '.env') }, cwd: root });
|
|
105
|
+
assert.equal(omp.decision, 'deny');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('an unevaluable access denies rather than allows', () => {
|
|
110
|
+
for (const malformed of ['not json', '', '{"tool_name":']) {
|
|
111
|
+
const omp = verdict(path.join(INSTALLED, 'privacy-block.cjs'), malformed);
|
|
112
|
+
assert.equal(omp.code, 0, 'the hook must not crash the turn');
|
|
113
|
+
if (malformed === 'not json' || malformed === '{"tool_name":') {
|
|
114
|
+
assert.equal(omp.decision, 'deny',
|
|
115
|
+
'a hook that cannot evaluate an access must not hand the model a secret-bearing path');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a write to a scaffolded task path is guarded under omp lowercase names too', () => {
|
|
121
|
+
// The guard is no longer an overlay file, so this is a behaviour case on the tree an
|
|
122
|
+
// install produces: omp sends `write`, and the shared reader has to make it reach the
|
|
123
|
+
// rule that was authored against Claude's `Write`.
|
|
124
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-guard-'));
|
|
125
|
+
try {
|
|
126
|
+
const target = path.join(root, 'specs', 'demo', 'tasks', 'task-R0-01-x.md');
|
|
127
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
128
|
+
fs.cpSync(path.join(PACKAGE_ROOT, 'src/claude/scripts'), path.join(INSTALLED_ROOT, '.omp', 'scripts'),
|
|
129
|
+
{ recursive: true, force: true });
|
|
130
|
+
const r = verdict(path.join(INSTALLED, 'task-scaffold-guard.cjs'),
|
|
131
|
+
{ tool_name: 'write', tool_input: { file_path: target }, cwd: root });
|
|
132
|
+
assert.equal(r.code, 2, "omp sends `write`; an exact 'Write' comparison would skip the guard entirely");
|
|
133
|
+
assert.equal(r.decision, 'deny');
|
|
134
|
+
} finally {
|
|
135
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/** Every file under a directory, relative and sorted. */
|
|
140
|
+
function walk(dir, prefix = '') {
|
|
141
|
+
return fs.readdirSync(dir).flatMap((name) => {
|
|
142
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
143
|
+
return fs.statSync(path.join(dir, name)).isDirectory() ? walk(path.join(dir, name), rel) : [rel];
|
|
144
|
+
}).sort();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The contract edits, applied to the portable Claude bytes. This is the whole overlay. */
|
|
148
|
+
function expectedOverlay(name) {
|
|
149
|
+
let source = fs.readFileSync(path.join(CLAUDE_HOOKS, name), 'utf8');
|
|
150
|
+
const edit = (from, to) => {
|
|
151
|
+
assert.equal(source.split(from).length - 1, 1, `${name} must contain exactly one "${from.trim()}"`);
|
|
152
|
+
source = source.replace(from, to);
|
|
153
|
+
};
|
|
154
|
+
if (name === 'privacy-block.cjs') {
|
|
155
|
+
edit(" permissionDecision: 'ask',\n permissionDecisionReason: `Sensitive file access requires approval: ${basename}`\n",
|
|
156
|
+
" // omp has no ask state on tool_call, so an ask under Claude becomes a denial here.\n"
|
|
157
|
+
+ " permissionDecision: 'deny',\n permissionDecisionReason: `Sensitive file access requires approval: ${basename}`\n");
|
|
158
|
+
edit(" permissionDecision: 'ask',\n permissionDecisionReason: 'Sensitive access could not be evaluated safely.'\n",
|
|
159
|
+
" permissionDecision: 'deny',\n permissionDecisionReason: 'Sensitive access could not be evaluated safely.'\n");
|
|
160
|
+
}
|
|
161
|
+
return source;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
test('the fork differs from Claude only by its overlay', () => {
|
|
165
|
+
// A second file here would be a hook the installer stops taking from Claude, so a
|
|
166
|
+
// Claude fix would silently miss omp. A shared file that differs beyond its contract
|
|
167
|
+
// edits is the same drift in the other direction. Every lowercase tool name omp sends
|
|
168
|
+
// is handled by the shared payload reader, so only the ask-versus-deny difference —
|
|
169
|
+
// which is a contract, not a spelling — still needs a file of its own.
|
|
170
|
+
assert.deepEqual(walk(OMP_HOOKS), ['privacy-block.cjs']);
|
|
171
|
+
for (const name of ['privacy-block.cjs']) {
|
|
172
|
+
assert.equal(fs.readFileSync(path.join(OMP_HOOKS, name), 'utf8'), expectedOverlay(name),
|
|
173
|
+
`${name} must be the portable Claude file plus its omp contract edits, nothing else`);
|
|
174
|
+
}
|
|
175
|
+
const phase = fs.readFileSync(path.join(PACKAGE_ROOT, 'bin/phases/omp-runtime.js'), 'utf8');
|
|
176
|
+
assert.match(phase, /claudeHookFiles/, 'the phase must install the Claude set first');
|
|
177
|
+
assert.match(phase, /overlayFiles/, 'the phase must write the overlay over it');
|
|
178
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// omp discovers .claude/skills and .agents/skills on its own, so CafeKit ships it no
|
|
4
|
+
// skill payload. What omp lacks is the enforcement chain, so an omp install has to
|
|
5
|
+
// provision the hooks and the extension directory that carries it. These tests pin the
|
|
6
|
+
// install path only; gate behaviour belongs to the tasks that author the fork and bridge.
|
|
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 { PLATFORMS } = require(path.join(PACKAGE_ROOT, 'bin/lib/context'));
|
|
18
|
+
|
|
19
|
+
function inTempProject(run) {
|
|
20
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-omp-runtime-'));
|
|
21
|
+
try {
|
|
22
|
+
const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
23
|
+
assert.equal(init.status, 0, init.stderr);
|
|
24
|
+
return run(root);
|
|
25
|
+
} finally {
|
|
26
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function install(root, platform, extra = []) {
|
|
31
|
+
return spawnSync(process.execPath, [INSTALLER, '--platform', platform, '--yes', ...extra],
|
|
32
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('omp is a registered platform that ships no skill payload of its own', () => {
|
|
36
|
+
const omp = PLATFORMS.omp;
|
|
37
|
+
assert.ok(omp, 'omp must exist in the platform registry');
|
|
38
|
+
assert.equal(omp.folder, '.omp');
|
|
39
|
+
assert.equal(omp.capabilities.skills, false, 'omp reads .claude/skills itself; copying would duplicate');
|
|
40
|
+
assert.equal(omp.capabilities.agents, false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('an omp install creates hooks, an extensions directory, and platform metadata', () => {
|
|
44
|
+
inTempProject((root) => {
|
|
45
|
+
const result = install(root, 'omp');
|
|
46
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
47
|
+
|
|
48
|
+
const hooks = fs.readdirSync(path.join(root, '.omp', 'hooks')).filter((f) => f.endsWith('.cjs'));
|
|
49
|
+
assert.ok(hooks.length > 0, 'the gate scripts must be installed');
|
|
50
|
+
for (const required of ['spec-gate.cjs', 'privacy-block.cjs', 'rules.cjs']) {
|
|
51
|
+
assert.ok(hooks.includes(required), `missing ${required}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
assert.equal(fs.existsSync(path.join(root, '.omp', 'extensions')), true,
|
|
55
|
+
'omp auto-loads this directory; a half-provisioned install must be visibly empty, not absent');
|
|
56
|
+
|
|
57
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(root, '.omp', 'cafekit.json'), 'utf8'));
|
|
58
|
+
assert.equal(metadata.platform, 'omp');
|
|
59
|
+
assert.ok(metadata.version, 'the install must record a version');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a non-omp install creates no .omp directory', () => {
|
|
64
|
+
inTempProject((root) => {
|
|
65
|
+
const result = install(root, 'claude');
|
|
66
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
67
|
+
assert.equal(fs.existsSync(path.join(root, '.omp')), false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('an omp install ignores its own auto-executed extension directory', () => {
|
|
72
|
+
inTempProject((root) => {
|
|
73
|
+
assert.equal(install(root, 'omp').status, 0);
|
|
74
|
+
const ignore = fs.readFileSync(path.join(root, '.gitignore'), 'utf8');
|
|
75
|
+
// A committed .omp/extensions/ would run arbitrary code on clone, before any gate
|
|
76
|
+
// can act, so the ignore rule is part of the install rather than user hygiene.
|
|
77
|
+
assert.match(ignore, /^\.omp\/$/m, '.omp/ must be ignored like .claude/ and .codex/');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('reinstalling omp is idempotent and preserves a user-added extension', () => {
|
|
82
|
+
inTempProject((root) => {
|
|
83
|
+
assert.equal(install(root, 'omp').status, 0);
|
|
84
|
+
const mine = path.join(root, '.omp', 'extensions', 'my-own.mjs');
|
|
85
|
+
fs.writeFileSync(mine, 'export default function () {}\n');
|
|
86
|
+
|
|
87
|
+
assert.equal(install(root, 'omp').status, 0);
|
|
88
|
+
assert.equal(fs.existsSync(mine), true, 'a reinstall erased an extension the user added');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('an omp install ships the scripts the Stop gate needs', () => {
|
|
93
|
+
inTempProject((root) => {
|
|
94
|
+
fs.mkdirSync(path.join(root, 'specs'), { recursive: true });
|
|
95
|
+
const result = install(root, 'omp');
|
|
96
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
97
|
+
|
|
98
|
+
// Every Stop-time gate requires ../scripts/*.cjs. Without these files the
|
|
99
|
+
// installed gate answers "Completion gate unavailable" and the bridge turns that
|
|
100
|
+
// into a block on every session_stop — the defect this case exists to pin.
|
|
101
|
+
for (const script of ['workflow-policy.cjs', 'spec-receipt.cjs', 'spec-resolver.cjs', 'provenance.cjs']) {
|
|
102
|
+
assert.equal(fs.existsSync(path.join(root, '.omp', 'scripts', script)), true, `missing .omp/scripts/${script}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const gate = spawnSync(process.execPath, [path.join(root, '.omp', 'hooks', 'spec-gate.cjs')], {
|
|
106
|
+
cwd: root,
|
|
107
|
+
input: JSON.stringify({ session_id: 'gate-probe', cwd: root, hook_event_name: 'Stop', stop_hook_active: false }),
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
});
|
|
110
|
+
assert.equal(gate.status, 0, gate.stderr);
|
|
111
|
+
assert.doesNotMatch(gate.stdout, /Completion gate unavailable/, 'the installed Stop gate must load its scripts');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('an omp-only install injects rules', () => {
|
|
116
|
+
inTempProject((root) => {
|
|
117
|
+
const result = install(root, 'omp');
|
|
118
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
119
|
+
assert.equal(fs.existsSync(path.join(root, '.claude')), false, 'this case is about a project with no .claude at all');
|
|
120
|
+
|
|
121
|
+
// The hooks read their configuration from the folder they live in, so omp needs its
|
|
122
|
+
// own runtime.json: no statusline (omp has none), usage off (that hook reads Claude
|
|
123
|
+
// Code's credential file), coding level 1 like Codex.
|
|
124
|
+
const runtime = JSON.parse(fs.readFileSync(path.join(root, '.omp', 'runtime.json'), 'utf8'));
|
|
125
|
+
assert.equal(runtime.codingLevel, 1);
|
|
126
|
+
assert.equal(runtime.usage.enabled, false);
|
|
127
|
+
assert.deepEqual(Object.keys(runtime).filter((k) => k.startsWith('statusline')), []);
|
|
128
|
+
assert.equal(runtime.$schema, './runtime.schema.json');
|
|
129
|
+
assert.equal(fs.existsSync(path.join(root, '.omp', 'runtime.schema.json')), true, 'the schema installs beside the file that references it');
|
|
130
|
+
|
|
131
|
+
// The Claude set arrives whole, the overlay on top, and nothing from the test tree.
|
|
132
|
+
const hooks = fs.readdirSync(path.join(root, '.omp', 'hooks'));
|
|
133
|
+
assert.equal(hooks.includes('__tests__'), false, 'the source tree holds tests that must never install');
|
|
134
|
+
for (const required of ['rules.cjs', 'spec-gate.cjs', 'session.cjs', 'lib/runtime-dir.cjs', 'lib/hook-payload.cjs']) {
|
|
135
|
+
assert.equal(fs.existsSync(path.join(root, '.omp', 'hooks', required)), true, `missing .omp/hooks/${required}`);
|
|
136
|
+
}
|
|
137
|
+
const privacy = fs.readFileSync(path.join(root, '.omp', 'hooks', 'privacy-block.cjs'), 'utf8');
|
|
138
|
+
assert.ok(privacy.includes("permissionDecision: 'deny'") && !privacy.includes("permissionDecision: 'ask'"),
|
|
139
|
+
'the installed privacy hook must be the omp overlay, not the Claude file');
|
|
140
|
+
|
|
141
|
+
const rules = spawnSync(process.execPath, [path.join(root, '.omp', 'hooks', 'rules.cjs')], {
|
|
142
|
+
cwd: root,
|
|
143
|
+
input: JSON.stringify({ session_id: `omp-rules-${Date.now()}`, cwd: root, prompt: 'hello' }),
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: '', PROJECT_ROOT: '' },
|
|
146
|
+
});
|
|
147
|
+
assert.equal(rules.status, 0, rules.stderr);
|
|
148
|
+
assert.match(rules.stdout, /## Rules/, 'without .omp/runtime.json the hook exits silently and omp never sees the rules');
|
|
149
|
+
});
|
|
150
|
+
});
|