@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,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
const STATUS_PATH = path.resolve(__dirname, '..', '..', 'status.cjs');
|
|
11
|
+
const GOLDEN_PATH = path.join(__dirname, 'fixtures', 'statusline-default.golden');
|
|
12
|
+
const NBSP = / /g;
|
|
13
|
+
|
|
14
|
+
const cleanupDirs = [];
|
|
15
|
+
process.on('exit', () => {
|
|
16
|
+
for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function basePayload(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
model: { display_name: 'TestModel' },
|
|
22
|
+
workspace: { current_dir: '/fixture/dir' },
|
|
23
|
+
cwd: '/fixture/dir',
|
|
24
|
+
context_window: {
|
|
25
|
+
context_window_size: 200000,
|
|
26
|
+
current_usage: {
|
|
27
|
+
input_tokens: 40000,
|
|
28
|
+
cache_creation_input_tokens: 0,
|
|
29
|
+
cache_read_input_tokens: 20000,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Spawn status.cjs as a child process under a fully pinned environment:
|
|
38
|
+
* - cwd: a fixture root carrying its own .claude/runtime.json
|
|
39
|
+
* - TMPDIR/TEMP/TMP: an isolated per-case temp root (git + usage + context caches)
|
|
40
|
+
* - NO_COLOR/FORCE_COLOR scrubbed unless the case provides them
|
|
41
|
+
* - COLUMNS pinned so responsive wrapping is deterministic
|
|
42
|
+
*/
|
|
43
|
+
function runStatus({ runtime = { statusline: 'full', statuslineColors: true }, payload = basePayload(), envExtra = {}, tmpFiles = {} } = {}) {
|
|
44
|
+
const caseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-case-'));
|
|
45
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-tmp-'));
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(path.join(caseDir, '.claude'));
|
|
48
|
+
fs.writeFileSync(path.join(caseDir, '.claude', 'runtime.json'), JSON.stringify(runtime));
|
|
49
|
+
for (const [name, content] of Object.entries(tmpFiles)) {
|
|
50
|
+
fs.writeFileSync(path.join(tmpDir, name), JSON.stringify(content));
|
|
51
|
+
}
|
|
52
|
+
const env = {
|
|
53
|
+
PATH: process.env.PATH,
|
|
54
|
+
HOME: process.env.HOME,
|
|
55
|
+
TMPDIR: tmpDir,
|
|
56
|
+
TEMP: tmpDir,
|
|
57
|
+
TMP: tmpDir,
|
|
58
|
+
COLUMNS: '200',
|
|
59
|
+
...envExtra,
|
|
60
|
+
};
|
|
61
|
+
const result = spawnSync(process.execPath, [STATUS_PATH], {
|
|
62
|
+
cwd: caseDir,
|
|
63
|
+
env,
|
|
64
|
+
input: Buffer.from(JSON.stringify(payload)),
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
status: result.status,
|
|
68
|
+
stdoutBuffer: result.stdout,
|
|
69
|
+
stdout: result.stdout.toString('utf8'),
|
|
70
|
+
stderr: result.stderr.toString('utf8'),
|
|
71
|
+
readTmpFile(name) {
|
|
72
|
+
const p = path.join(tmpDir, name);
|
|
73
|
+
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
} finally {
|
|
77
|
+
// Defer cleanup so readTmpFile still works after the spawn returns.
|
|
78
|
+
cleanupDirs.push(caseDir, tmpDir);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function freshUsageCache(now = Date.now()) {
|
|
83
|
+
return {
|
|
84
|
+
status: 'available',
|
|
85
|
+
timestamp: now,
|
|
86
|
+
data: {
|
|
87
|
+
five_hour: { utilization: 20, resets_at: new Date(now + 2 * 3600 * 1000 + 29 * 60 * 1000).toISOString() },
|
|
88
|
+
seven_day: { utilization: 45, resets_at: new Date(now + (2 * 86400 + 5 * 3600) * 1000).toISOString() },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
test('statusline default output is byte-identical to the golden fixture when no layout is configured', () => {
|
|
94
|
+
const golden = fs.readFileSync(GOLDEN_PATH);
|
|
95
|
+
const run = runStatus();
|
|
96
|
+
assert.equal(run.status, 0, run.stderr);
|
|
97
|
+
assert.ok(run.stdoutBuffer.equals(golden),
|
|
98
|
+
`default output drifted from golden\nexpected: ${JSON.stringify(golden.toString('utf8'))}\nactual: ${JSON.stringify(run.stdout)}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('statusline custom layout renders named sections in order per line', () => {
|
|
102
|
+
const run = runStatus({
|
|
103
|
+
runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['directory', 'model']] } },
|
|
104
|
+
});
|
|
105
|
+
assert.equal(run.status, 0, run.stderr);
|
|
106
|
+
const dirIndex = run.stdout.indexOf('📁');
|
|
107
|
+
const modelIndex = run.stdout.indexOf('🤖');
|
|
108
|
+
assert.ok(dirIndex >= 0 && modelIndex >= 0, run.stdout);
|
|
109
|
+
assert.ok(dirIndex < modelIndex, `directory must precede model: ${JSON.stringify(run.stdout)}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('statusline ignores unknown section ids without crashing or rendering placeholders', () => {
|
|
113
|
+
const run = runStatus({
|
|
114
|
+
runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['model', 'bogus']] } },
|
|
115
|
+
});
|
|
116
|
+
assert.equal(run.status, 0, run.stderr);
|
|
117
|
+
assert.match(run.stdout, /🤖/);
|
|
118
|
+
assert.doesNotMatch(run.stdout, /bogus/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('statusline modes slice the layout line count (minimal renders only the first line)', () => {
|
|
122
|
+
const run = runStatus({
|
|
123
|
+
runtime: { statusline: 'minimal', statuslineColors: true, statuslineLayout: { lines: [['model'], ['directory']] } },
|
|
124
|
+
});
|
|
125
|
+
assert.equal(run.status, 0, run.stderr);
|
|
126
|
+
assert.match(run.stdout, /🤖/);
|
|
127
|
+
assert.doesNotMatch(run.stdout, /📁/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('statusline empty or invalid layout falls back to the default renderers', () => {
|
|
131
|
+
const golden = fs.readFileSync(GOLDEN_PATH);
|
|
132
|
+
for (const layout of [{}, { lines: [] }, { lines: [['bogus']] }, 'nonsense']) {
|
|
133
|
+
const run = runStatus({
|
|
134
|
+
runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: layout },
|
|
135
|
+
});
|
|
136
|
+
assert.equal(run.status, 0, run.stderr);
|
|
137
|
+
assert.ok(run.stdoutBuffer.equals(golden), `fallback drifted for layout ${JSON.stringify(layout)}: ${JSON.stringify(run.stdout)}`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('statusline shows five-hour and weekly windows with countdowns when the cache is fresh', () => {
|
|
142
|
+
const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': freshUsageCache() } });
|
|
143
|
+
assert.equal(run.status, 0, run.stderr);
|
|
144
|
+
const plain = run.stdout.replace(NBSP, ' ');
|
|
145
|
+
assert.match(plain, /⌛ \d+h \d+m left \(20% used\)/, plain);
|
|
146
|
+
assert.match(plain, /wk 45% \(\d+d \d+h\)/, plain);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('statusline hides the quota area when the cache is stale', () => {
|
|
150
|
+
const stale = freshUsageCache(Date.now() - 10 * 60 * 1000);
|
|
151
|
+
stale.timestamp = Date.now() - 10 * 60 * 1000;
|
|
152
|
+
const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': stale } });
|
|
153
|
+
assert.equal(run.status, 0, run.stderr);
|
|
154
|
+
assert.doesNotMatch(run.stdout, /⌛/);
|
|
155
|
+
assert.doesNotMatch(run.stdout, /wk /);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('statusline cost renders only when the layout enables it, billing is api, and data exists', () => {
|
|
159
|
+
const costLayout = { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['model', 'cost']] } };
|
|
160
|
+
const withCost = basePayload({ cost: { total_cost_usd: '1.2345' } });
|
|
161
|
+
|
|
162
|
+
const enabled = runStatus({ runtime: costLayout, payload: withCost, envExtra: { CLAUDE_BILLING_MODE: 'api' } });
|
|
163
|
+
assert.equal(enabled.status, 0, enabled.stderr);
|
|
164
|
+
const plain = enabled.stdout.replace(NBSP, ' ');
|
|
165
|
+
assert.match(plain, /💵 \$1\.23(?!\d)/, plain);
|
|
166
|
+
|
|
167
|
+
const wrongBilling = runStatus({ runtime: costLayout, payload: withCost, envExtra: { CLAUDE_BILLING_MODE: 'subscription' } });
|
|
168
|
+
assert.equal(wrongBilling.status, 0, wrongBilling.stderr);
|
|
169
|
+
assert.doesNotMatch(wrongBilling.stdout, /💵/);
|
|
170
|
+
|
|
171
|
+
const noData = runStatus({ runtime: costLayout, envExtra: { CLAUDE_BILLING_MODE: 'api' } });
|
|
172
|
+
assert.equal(noData.status, 0, noData.stderr);
|
|
173
|
+
assert.doesNotMatch(noData.stdout, /💵/);
|
|
174
|
+
|
|
175
|
+
const noLayout = runStatus({ payload: withCost, envExtra: { CLAUDE_BILLING_MODE: 'api' } });
|
|
176
|
+
assert.equal(noLayout.status, 0, noLayout.stderr);
|
|
177
|
+
assert.doesNotMatch(noLayout.stdout, /💵/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('statusline writes the ck-context session file regardless of layout', () => {
|
|
181
|
+
const run = runStatus({
|
|
182
|
+
runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['model']] } },
|
|
183
|
+
payload: basePayload({ session_id: 'ctxcase' }),
|
|
184
|
+
});
|
|
185
|
+
assert.equal(run.status, 0, run.stderr);
|
|
186
|
+
const contextFile = run.readTmpFile('ck-context-ctxcase.json');
|
|
187
|
+
assert.ok(contextFile, 'ck-context-ctxcase.json must be written even under a custom layout');
|
|
188
|
+
assert.equal(typeof contextFile.percent, 'number');
|
|
189
|
+
assert.equal(typeof contextFile.tokens, 'number');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('statusline honors NO_COLOR with no ANSI escapes in output', () => {
|
|
193
|
+
const run = runStatus({ envExtra: { NO_COLOR: '1' } });
|
|
194
|
+
assert.equal(run.status, 0, run.stderr);
|
|
195
|
+
assert.doesNotMatch(run.stdout, /\[/);
|
|
196
|
+
});
|
|
@@ -15,19 +15,23 @@ try {
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
|
-
/** Read
|
|
18
|
+
/** Read <runtime>/runtime.json */
|
|
19
|
+
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
19
20
|
function readRuntime(cwd) {
|
|
20
21
|
try {
|
|
21
|
-
const p =
|
|
22
|
+
const p = runtimePath(cwd, 'runtime.json');
|
|
22
23
|
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
|
|
23
24
|
} catch { return null; }
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
// Skills live beside the runtime only on Claude; Codex and omp read `.agents/skills`.
|
|
28
|
+
function skillsDir() { return runtimeDirName() === '.claude' ? '.claude/skills' : '.agents/skills'; }
|
|
29
|
+
|
|
26
30
|
/** Resolve Python venv executable path if it exists */
|
|
27
31
|
function resolveVenv(cwd) {
|
|
28
32
|
const candidates = [
|
|
29
|
-
path.join(cwd,
|
|
30
|
-
path.join(cwd,
|
|
33
|
+
path.join(cwd, skillsDir(), '.venv', 'bin', 'python3'),
|
|
34
|
+
path.join(cwd, skillsDir(), '.venv', 'Scripts', 'python.exe'),
|
|
31
35
|
];
|
|
32
36
|
return candidates.find(p => fs.existsSync(p)) || null;
|
|
33
37
|
}
|
|
@@ -37,7 +41,8 @@ try {
|
|
|
37
41
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
38
42
|
if (!stdin) process.exit(0);
|
|
39
43
|
|
|
40
|
-
const
|
|
44
|
+
const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
|
|
45
|
+
const payload = normalizeHookPayload(JSON.parse(stdin));
|
|
41
46
|
// Use payload.cwd for monorepo support — subagent may run in a different dir
|
|
42
47
|
const payloadCwd = typeof payload.cwd === 'string' ? payload.cwd.trim() : '';
|
|
43
48
|
const agentCwd = payloadCwd || process.env.PROJECT_ROOT || process.cwd();
|
|
@@ -67,13 +72,13 @@ try {
|
|
|
67
72
|
lines.push('');
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
// Python venv (optional — if
|
|
75
|
+
// Python venv (optional — if <skills>/.venv exists)
|
|
71
76
|
const venv = resolveVenv(agentCwd);
|
|
72
77
|
|
|
73
78
|
lines.push('## Rules');
|
|
74
79
|
lines.push(`- Plans → ${plansPath}/ | Docs → ${docsPath}/`);
|
|
75
80
|
if (venv) {
|
|
76
|
-
lines.push(`- Python in
|
|
81
|
+
lines.push(`- Python in ${skillsDir()}/: use \`${venv}\``);
|
|
77
82
|
lines.push('- Never use global pip install');
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -90,7 +95,7 @@ try {
|
|
|
90
95
|
} catch (e) {
|
|
91
96
|
try {
|
|
92
97
|
const fs = require('fs'), p = require('path');
|
|
93
|
-
const d =
|
|
98
|
+
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
94
99
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
95
100
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
96
101
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'agent', status: 'crash', error: e.message }) + '\n');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
6
7
|
let STATE;
|
|
7
8
|
let evaluateCloseout;
|
|
8
9
|
|
|
@@ -27,7 +28,7 @@ function projectRoot(payload = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
30
|
-
const installedHook =
|
|
31
|
+
const installedHook = runtimePath(installedRoot, 'hooks', path.basename(__filename));
|
|
31
32
|
if (fs.existsSync(installedHook)) return installedRoot;
|
|
32
33
|
|
|
33
34
|
const sourceFixture = typeof process.env.PROJECT_ROOT === 'string'
|
|
@@ -50,7 +51,7 @@ function sessionId(payload) {
|
|
|
50
51
|
|
|
51
52
|
function readRuntime(root) {
|
|
52
53
|
try {
|
|
53
|
-
const file =
|
|
54
|
+
const file = runtimePath(root, 'runtime.json');
|
|
54
55
|
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
|
|
55
56
|
} catch { return {}; }
|
|
56
57
|
}
|
|
@@ -135,9 +136,14 @@ function stop(payload) {
|
|
|
135
136
|
|
|
136
137
|
let mode = process.argv[2] || null;
|
|
137
138
|
try {
|
|
138
|
-
const
|
|
139
|
+
const raw = readPayload();
|
|
140
|
+
// Read the loop guard before normalization: the catch below answers a block, so a
|
|
141
|
+
// reader failure must not be able to reach it. Both spellings are accepted here.
|
|
142
|
+
const hostLoop = raw.stop_hook_active === true || raw.stopHookActive === true;
|
|
143
|
+
const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
|
|
144
|
+
const payload = normalizeHookPayload(raw);
|
|
139
145
|
mode = mode || (payload.hook_event_name === 'UserPromptSubmit' ? '--approve' : '--stop');
|
|
140
|
-
if (mode === '--stop' &&
|
|
146
|
+
if (mode === '--stop' && hostLoop) {
|
|
141
147
|
// Host loop prevention is always silent, including when dependencies are unavailable.
|
|
142
148
|
} else {
|
|
143
149
|
const dependencyError = loadAuthority();
|
|
@@ -14,16 +14,20 @@
|
|
|
14
14
|
try {
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
-
const {
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
18
|
const { loadConfig } = require('./lib/config.cjs');
|
|
19
19
|
|
|
20
20
|
// Đọc stdin theo chuẩn hook
|
|
21
21
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
22
|
-
const
|
|
22
|
+
const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
|
|
23
|
+
const payload = stdin ? normalizeHookPayload(JSON.parse(stdin)) : {};
|
|
23
24
|
const cwd = payload.cwd || process.cwd();
|
|
24
25
|
const config = loadConfig({ cwd, includeProject: false, includeAssertions: false, includeLocale: false });
|
|
25
26
|
|
|
26
27
|
const docsDir = path.join(cwd, config.paths?.docs || 'docs');
|
|
28
|
+
const specsDir = path.join(cwd, config.paths?.specs || 'specs');
|
|
29
|
+
const docsRelative = path.relative(cwd, docsDir).replace(/\\/g, '/');
|
|
30
|
+
const specsRelative = path.relative(cwd, specsDir).replace(/\\/g, '/');
|
|
27
31
|
|
|
28
32
|
// Xác định dự án đã có cốt lõi code hay chưa?
|
|
29
33
|
const hasCode = fs.existsSync(path.join(cwd, 'src')) ||
|
|
@@ -46,16 +50,18 @@ try {
|
|
|
46
50
|
lines.push('> Source exists but `docs/` does not. Create baseline docs first:');
|
|
47
51
|
lines.push('> 1. `docs/system-architecture.md` — system architecture and flows');
|
|
48
52
|
lines.push('> 2. `docs/project-overview-pdr.md` — overview, goals, features');
|
|
49
|
-
lines.push('> 3. Write
|
|
53
|
+
lines.push('> 3. Write the latest non-docs/non-Specs source hash to `docs/.sync_hash`.');
|
|
50
54
|
lines.push('');
|
|
51
55
|
}
|
|
52
56
|
// Case 2: docs exist — check continuous sync vs source-only git hash
|
|
53
57
|
else {
|
|
54
58
|
try {
|
|
55
59
|
// Latest SOURCE-only hash (ignore docs-only commits)
|
|
56
|
-
const currentHash =
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
const currentHash = execFileSync('git', [
|
|
61
|
+
'log', '-1', '--format=%H', '--', '.',
|
|
62
|
+
`:(exclude,literal)${docsRelative}`,
|
|
63
|
+
`:(exclude,literal)${specsRelative}`,
|
|
64
|
+
], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
59
65
|
|
|
60
66
|
if (currentHash) {
|
|
61
67
|
const syncTrackingFile = path.join(docsDir, '.sync_hash');
|
|
@@ -87,11 +93,8 @@ try {
|
|
|
87
93
|
} catch (e) {
|
|
88
94
|
// Ghi log lỗi ẩn danh nếu sập hook
|
|
89
95
|
try {
|
|
90
|
-
const fs = require('fs'), p = require('path')
|
|
91
|
-
const
|
|
92
|
-
const d = isSourceTree
|
|
93
|
-
? p.join(os.tmpdir(), 'cafekit-hook-logs', 'claude-docs-sync')
|
|
94
|
-
: p.join(__dirname, '.logs');
|
|
96
|
+
const fs = require('fs'), p = require('path');
|
|
97
|
+
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
95
98
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
96
99
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
97
100
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'docs-sync', status: 'crash', error: e.message }) + '\n');
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Blocks access to heavy directories that would flood the LLM context window.
|
|
9
9
|
* Also warns on overly-broad glob patterns.
|
|
10
10
|
*
|
|
11
|
-
* Disable: set "inspect": { "enabled": false } in
|
|
11
|
+
* Disable: set "inspect": { "enabled": false } in <runtime>/runtime.json
|
|
12
12
|
*
|
|
13
13
|
* Exit: 0 = allow, 2 = block
|
|
14
14
|
*/
|
|
@@ -60,19 +60,41 @@ try {
|
|
|
60
60
|
return out.filter(Boolean);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
63
64
|
function readRuntime(cwd) {
|
|
64
65
|
try {
|
|
65
|
-
const p =
|
|
66
|
+
const p = runtimePath(cwd, 'runtime.json');
|
|
66
67
|
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {};
|
|
67
68
|
} catch { return {}; }
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Deny the call with a reason every host can read.
|
|
75
|
+
*
|
|
76
|
+
* The reason is the JSON `permissionDecisionReason`, which Claude Code reads natively and
|
|
77
|
+
* grok honours "regardless of exit code"; the same text also goes to stderr, because on
|
|
78
|
+
* an exit-2 denial that is the channel Claude Code feeds back, and grok reads only the
|
|
79
|
+
* first stderr line. Exit 2 stays so a host that reads neither still blocks.
|
|
80
|
+
*/
|
|
81
|
+
function denyWithReason(reason) {
|
|
82
|
+
process.stdout.write(JSON.stringify({
|
|
83
|
+
hookSpecificOutput: {
|
|
84
|
+
hookEventName: 'PreToolUse',
|
|
85
|
+
permissionDecision: 'deny',
|
|
86
|
+
permissionDecisionReason: reason
|
|
87
|
+
}
|
|
88
|
+
}) + '\n');
|
|
89
|
+
process.stderr.write(reason + '\n');
|
|
90
|
+
process.exit(2);
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
73
94
|
if (!stdin) process.exit(0);
|
|
74
95
|
|
|
75
|
-
const
|
|
96
|
+
const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
|
|
97
|
+
const data = normalizeHookPayload(JSON.parse(stdin));
|
|
76
98
|
const toolName = data.tool_name || '';
|
|
77
99
|
const toolInput = data.tool_input || {};
|
|
78
100
|
const cwd = data.cwd || process.cwd();
|
|
@@ -88,12 +110,11 @@ try {
|
|
|
88
110
|
|
|
89
111
|
// Broad glob check
|
|
90
112
|
if (toolInput.pattern && isBroadGlob(toolInput.pattern)) {
|
|
91
|
-
|
|
113
|
+
denyWithReason(
|
|
92
114
|
`SCOPE LIMIT EXCEEDED: Glob pattern is excessively broad\n` +
|
|
93
115
|
`Requested Pattern: ${toolInput.pattern}\n\n` +
|
|
94
116
|
`Please narrow your scope (e.g., src/**/*.ts rather than **/*.ts).`
|
|
95
117
|
);
|
|
96
|
-
process.exit(2);
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
// Blocked directory check
|
|
@@ -101,12 +122,11 @@ try {
|
|
|
101
122
|
for (const p of paths) {
|
|
102
123
|
if (isBlockedPath(p)) {
|
|
103
124
|
const blocked = p.replace(/\\/g, '/').split('/').find(s => BLOCKED_DIRS.includes(s));
|
|
104
|
-
|
|
125
|
+
denyWithReason(
|
|
105
126
|
`SCOPE LIMIT EXCEEDED: Directory "${blocked}/" is explicitly forbidden\n` +
|
|
106
127
|
`Requested Path: ${p}\n` +
|
|
107
128
|
`Restricted zones: ${BLOCKED_DIRS.join(', ')}`
|
|
108
129
|
);
|
|
109
|
-
process.exit(2);
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
|
|
@@ -115,7 +135,7 @@ try {
|
|
|
115
135
|
} catch (e) {
|
|
116
136
|
try {
|
|
117
137
|
const fs = require('fs'), p = require('path');
|
|
118
|
-
const d =
|
|
138
|
+
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
119
139
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
120
140
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
121
141
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'inspect-block', status: 'crash', error: e.message }) + '\n');
|
|
@@ -9,7 +9,10 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const { runtimeDirName } = require('./runtime-dir.cjs');
|
|
13
|
+
|
|
14
|
+
// Derived from this library's own location so an install under .omp/ or .codex/ reads its own file.
|
|
15
|
+
const RUNTIME_CONFIG_PATH = `${runtimeDirName()}/runtime.json`;
|
|
13
16
|
|
|
14
17
|
const CONFIG_PATH = RUNTIME_CONFIG_PATH;
|
|
15
18
|
|
|
@@ -461,7 +464,7 @@ function sanitizeConfig(config, projectRoot) {
|
|
|
461
464
|
*
|
|
462
465
|
* Resolution order (each layer overrides the previous):
|
|
463
466
|
* 1. DEFAULT_CONFIG (hardcoded defaults)
|
|
464
|
-
* 2. Runtime config (
|
|
467
|
+
* 2. Runtime config (<runtime-dir>/runtime.json, derived by lib/runtime-dir.cjs) - installed CafeKit runtime config
|
|
465
468
|
*
|
|
466
469
|
* @param {Object} options - Options for config loading
|
|
467
470
|
* @param {boolean} options.includeProject - Include project section (default: true)
|
|
@@ -520,6 +523,9 @@ function loadConfig(options = {}) {
|
|
|
520
523
|
// Statusline mode
|
|
521
524
|
result.statusline = merged.statusline || 'full';
|
|
522
525
|
result.statuslineColors = merged.statuslineColors !== false;
|
|
526
|
+
if (merged.statuslineLayout && typeof merged.statuslineLayout === 'object') {
|
|
527
|
+
result.statuslineLayout = merged.statuslineLayout;
|
|
528
|
+
}
|
|
523
529
|
|
|
524
530
|
return sanitizeConfig(result, projectRoot);
|
|
525
531
|
} catch (e) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
const { runtimeDir } = require('./runtime-dir.cjs');
|
|
9
10
|
|
|
10
11
|
function getMcpServerNames(filePath) {
|
|
11
12
|
if (!fs.existsSync(filePath)) return new Set();
|
|
@@ -83,16 +84,17 @@ function countConfigs(cwd) {
|
|
|
83
84
|
|
|
84
85
|
// Project scope
|
|
85
86
|
if (cwd) {
|
|
87
|
+
const projectRuntime = runtimeDir(cwd);
|
|
86
88
|
if (fs.existsSync(path.join(cwd, 'CLAUDE.md'))) claudeMdCount++;
|
|
87
89
|
if (fs.existsSync(path.join(cwd, 'CLAUDE.local.md'))) claudeMdCount++;
|
|
88
|
-
if (fs.existsSync(path.join(
|
|
89
|
-
if (fs.existsSync(path.join(
|
|
90
|
-
rulesCount += countRulesInDir(path.join(
|
|
90
|
+
if (fs.existsSync(path.join(projectRuntime, 'CLAUDE.md'))) claudeMdCount++;
|
|
91
|
+
if (fs.existsSync(path.join(projectRuntime, 'CLAUDE.local.md'))) claudeMdCount++;
|
|
92
|
+
rulesCount += countRulesInDir(path.join(projectRuntime, 'rules'));
|
|
91
93
|
mcpCount += countMcpServersInFile(path.join(cwd, '.mcp.json'));
|
|
92
|
-
const projectSettings = path.join(
|
|
94
|
+
const projectSettings = path.join(projectRuntime, 'settings.json');
|
|
93
95
|
mcpCount += countMcpServersInFile(projectSettings);
|
|
94
96
|
hooksCount += countHooksInFile(projectSettings);
|
|
95
|
-
const localSettings = path.join(
|
|
97
|
+
const localSettings = path.join(projectRuntime, 'settings.local.json');
|
|
96
98
|
mcpCount += countMcpServersInFile(localSettings);
|
|
97
99
|
hooksCount += countHooksInFile(localSettings);
|
|
98
100
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Haposoft. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* Hook payload normalization.
|
|
5
|
+
*
|
|
6
|
+
* The gate hooks were written against Claude Code's stdin envelope: snake_case keys and
|
|
7
|
+
* capitalised tool names. Two other hosts run the very same scripts with a different
|
|
8
|
+
* spelling. Grok CLI loads `.claude/settings.json` through its Claude-compatibility layer
|
|
9
|
+
* but sends camelCase keys (`toolName`, `stopHookActive`) and its own tool names
|
|
10
|
+
* (`run_terminal_command`, `read_file`, `search_replace`). Oh My Pi's bridge sends
|
|
11
|
+
* Claude-shaped keys with lowercase tool names (`bash`, `read`).
|
|
12
|
+
*
|
|
13
|
+
* A hook that reads `data.tool_name` on a grok payload gets `undefined`, extracts no
|
|
14
|
+
* paths, and allows the call: the gate looks installed and is open. This module is the
|
|
15
|
+
* one place that reconciles those spellings, so every hook keeps a single code path.
|
|
16
|
+
*
|
|
17
|
+
* Two rules earn their keep:
|
|
18
|
+
*
|
|
19
|
+
* 1. The tool name is aliased by VALUE, under any key. omp already sends `tool_name`,
|
|
20
|
+
* so a key-only translation would silently skip its lowercase names.
|
|
21
|
+
* 2. Every name that can reach a `Bash` matcher must map to `Bash`. Grok expands
|
|
22
|
+
* matchers by alias, so a shell call can arrive under a name the hook does not
|
|
23
|
+
* recognise; an unmapped name skips the command scan and a secret-bearing command
|
|
24
|
+
* is never examined.
|
|
25
|
+
*
|
|
26
|
+
* The reader is total: it never throws. Callers include two fail-closed hooks whose final
|
|
27
|
+
* catch emits a block, so an exception here would block the Stop gate on Claude too.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
/** Grok's event values are snake_case; the hooks compare against Claude's PascalCase. */
|
|
33
|
+
const EVENT_NAMES = Object.freeze({
|
|
34
|
+
pre_tool_use: 'PreToolUse',
|
|
35
|
+
post_tool_use: 'PostToolUse',
|
|
36
|
+
post_tool_use_failure: 'PostToolUseFailure',
|
|
37
|
+
permission_denied: 'PermissionDenied',
|
|
38
|
+
user_prompt_submit: 'UserPromptSubmit',
|
|
39
|
+
session_start: 'SessionStart',
|
|
40
|
+
session_end: 'SessionEnd',
|
|
41
|
+
stop: 'Stop',
|
|
42
|
+
stop_failure: 'StopFailure',
|
|
43
|
+
stop_cancelled: 'StopCancelled',
|
|
44
|
+
subagent_start: 'SubagentStart',
|
|
45
|
+
subagent_stop: 'SubagentStop',
|
|
46
|
+
pre_compact: 'PreCompact',
|
|
47
|
+
post_compact: 'PostCompact',
|
|
48
|
+
notification: 'Notification',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Foreign tool names to the Claude names the rules were authored against.
|
|
53
|
+
*
|
|
54
|
+
* Grok's registry and matcher aliases come first, then omp's lowercase registry. Where
|
|
55
|
+
* both hosts use a spelling (`write`, `grep`) they agree on the destination. `write` is
|
|
56
|
+
* grok's file-creating tool and maps to `Write`, while `search_replace` edits an existing
|
|
57
|
+
* file and maps to `Edit`; conflating them would make the scaffold guard reject ordinary
|
|
58
|
+
* edits. `delete_file` maps to `Write` so a delete is gated like any other file write.
|
|
59
|
+
*/
|
|
60
|
+
const TOOL_ALIASES = Object.freeze({
|
|
61
|
+
// grok — every spelling that reaches a Bash matcher
|
|
62
|
+
run_terminal_command: 'Bash',
|
|
63
|
+
run_terminal_cmd: 'Bash',
|
|
64
|
+
PowerShell: 'Bash',
|
|
65
|
+
// grok — file and search tools
|
|
66
|
+
read_file: 'Read',
|
|
67
|
+
hashline_read: 'Read',
|
|
68
|
+
search_replace: 'Edit',
|
|
69
|
+
hashline_edit: 'Edit',
|
|
70
|
+
write: 'Write',
|
|
71
|
+
delete_file: 'Write',
|
|
72
|
+
grep: 'Grep',
|
|
73
|
+
grep_search: 'Grep',
|
|
74
|
+
hashline_grep: 'Grep',
|
|
75
|
+
list_dir: 'Glob',
|
|
76
|
+
web_search: 'WebSearch',
|
|
77
|
+
web_fetch: 'WebFetch',
|
|
78
|
+
spawn_subagent: 'Task',
|
|
79
|
+
todo_write: 'TodoWrite',
|
|
80
|
+
// omp — lowercase registry
|
|
81
|
+
bash: 'Bash',
|
|
82
|
+
read: 'Read',
|
|
83
|
+
edit: 'Edit',
|
|
84
|
+
glob: 'Glob',
|
|
85
|
+
find: 'Glob',
|
|
86
|
+
ls: 'Glob',
|
|
87
|
+
webfetch: 'WebFetch',
|
|
88
|
+
websearch: 'WebSearch',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Camel-cased foreign key to the snake_case key the hooks read.
|
|
93
|
+
*
|
|
94
|
+
* `transcriptPath` is deliberately absent: grok stores sessions in its own format, and
|
|
95
|
+
* mapping it would make `state.cjs` stream a file it cannot parse. The last three pairs
|
|
96
|
+
* cover fields whose grok spelling is unverified, so both spellings are accepted.
|
|
97
|
+
*/
|
|
98
|
+
const KEY_ALIASES = Object.freeze({
|
|
99
|
+
hookEventName: 'hook_event_name',
|
|
100
|
+
sessionId: 'session_id',
|
|
101
|
+
stopHookActive: 'stop_hook_active',
|
|
102
|
+
lastAssistantMessage: 'last_assistant_message',
|
|
103
|
+
toolName: 'tool_name',
|
|
104
|
+
toolInput: 'tool_input',
|
|
105
|
+
toolUseId: 'tool_use_id',
|
|
106
|
+
toolResult: 'tool_response',
|
|
107
|
+
subagentType: 'agent_type',
|
|
108
|
+
subagentId: 'agent_id',
|
|
109
|
+
permissionMode: 'permission_mode',
|
|
110
|
+
workspaceRoot: 'workspace_root',
|
|
111
|
+
userPrompt: 'prompt',
|
|
112
|
+
startSource: 'source',
|
|
113
|
+
compactTrigger: 'trigger',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const owns = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
117
|
+
|
|
118
|
+
/** The Claude name for a foreign tool, or the input unchanged when unknown. */
|
|
119
|
+
function normalizeToolName(name) {
|
|
120
|
+
if (typeof name !== 'string' || name === '') return name;
|
|
121
|
+
return owns(TOOL_ALIASES, name) ? TOOL_ALIASES[name] : name;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The Claude name for an event value, or the input unchanged (already PascalCase). */
|
|
125
|
+
function normalizeEventName(value) {
|
|
126
|
+
if (typeof value !== 'string' || value === '') return value;
|
|
127
|
+
return owns(EVENT_NAMES, value) ? EVENT_NAMES[value] : value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A hook payload in the shape the gate scripts read, whatever host sent it.
|
|
132
|
+
*
|
|
133
|
+
* Anything that is not a plain object is returned as-is rather than coerced, so a caller
|
|
134
|
+
* that already guards on shape keeps its own behaviour. The input is never mutated.
|
|
135
|
+
*/
|
|
136
|
+
function normalizeHookPayload(payload) {
|
|
137
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
|
138
|
+
|
|
139
|
+
const out = { ...payload };
|
|
140
|
+
|
|
141
|
+
// A snake_case key already present wins: the host that sent it meant it.
|
|
142
|
+
for (const [foreign, native] of Object.entries(KEY_ALIASES)) {
|
|
143
|
+
if (owns(out, foreign) && !owns(out, native)) out[native] = out[foreign];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (owns(out, 'hook_event_name')) out.hook_event_name = normalizeEventName(out.hook_event_name);
|
|
147
|
+
// By value, under whichever key carried it — omp sends `tool_name` with a lowercase value.
|
|
148
|
+
if (owns(out, 'tool_name')) out.tool_name = normalizeToolName(out.tool_name);
|
|
149
|
+
|
|
150
|
+
// grok's `read_file` carries its argument as `path`; the hooks read `file_path`.
|
|
151
|
+
const input = out.tool_input;
|
|
152
|
+
if (input && typeof input === 'object' && !Array.isArray(input)
|
|
153
|
+
&& owns(input, 'path') && !owns(input, 'file_path')) {
|
|
154
|
+
out.tool_input = { ...input, file_path: input.path };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { normalizeHookPayload, normalizeToolName, TOOL_ALIASES, KEY_ALIASES, EVENT_NAMES };
|