@haposoft/cafekit 0.16.0-rc.8 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -27
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +50 -0
- package/bin/__tests__/codex-native.test.js +1882 -70
- package/bin/__tests__/develop-contract.test.js +400 -23
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +1735 -98
- package/bin/__tests__/runtime-schema.test.js +111 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
- package/bin/install.js +10 -0
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +14 -0
- package/bin/lib/i18n.js +12 -6
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +8 -2
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +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__/spec-gate.test.js +10 -3
- package/src/claude/hooks/__tests__/state.test.js +5 -0
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +1 -1
- package/src/claude/hooks/docs-sync.cjs +12 -10
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +3 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +105 -0
- package/src/claude/hooks/rules.cjs +1 -1
- package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
- package/src/claude/hooks/session.cjs +27 -1
- package/src/claude/hooks/spec-gate.cjs +3 -3
- package/src/claude/hooks/spec-state.cjs +2 -2
- package/src/claude/hooks/state.cjs +1 -1
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/hooks/usage.cjs +1 -1
- package/src/claude/migration-manifest.json +29 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/workflow.md +3 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +18 -5
- package/src/claude/scripts/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 +41 -60
- 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 +2 -1
- package/src/codex/hooks.json +5 -0
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Behavioral tests for precompact.cjs and the compact-restart readback in
|
|
4
|
+
// session.cjs. Both hooks run as real subprocesses against a temporary git
|
|
5
|
+
// fixture; the capture is asserted from the file it writes and from what the
|
|
6
|
+
// next session actually prints.
|
|
7
|
+
|
|
8
|
+
const { test } = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const { spawnSync } = require('node:child_process');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
const PRECOMPACT = path.join(__dirname, '..', 'precompact.cjs');
|
|
16
|
+
const SESSION = path.join(__dirname, '..', 'session.cjs');
|
|
17
|
+
const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
|
|
18
|
+
const RECOVERY = path.join(hookStateDir(), 'compact-recovery.json');
|
|
19
|
+
|
|
20
|
+
function fixture() {
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-'));
|
|
22
|
+
for (const args of [
|
|
23
|
+
['init', '-q', '-b', 'work'],
|
|
24
|
+
['config', 'user.email', 'cafekit@example.invalid'],
|
|
25
|
+
['config', 'user.name', 'CafeKit Test'],
|
|
26
|
+
]) {
|
|
27
|
+
const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
|
|
28
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(path.join(dir, 'tracked.txt'), 'one\n');
|
|
31
|
+
for (const args of [['add', '-A'], ['commit', '-qm', 'fixture']]) {
|
|
32
|
+
const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
|
|
33
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
34
|
+
}
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(hook, dir, payload) {
|
|
39
|
+
const result = spawnSync(process.execPath, [hook], {
|
|
40
|
+
cwd: dir,
|
|
41
|
+
input: JSON.stringify(payload),
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
|
|
44
|
+
});
|
|
45
|
+
return { code: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clearCapture() {
|
|
49
|
+
try { fs.unlinkSync(RECOVERY); } catch { /* absent */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readCapture() {
|
|
53
|
+
return JSON.parse(fs.readFileSync(RECOVERY, 'utf8'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('PreCompact records branch, HEAD, dirty count and the in-flight task', () => {
|
|
57
|
+
const dir = fixture();
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(path.join(dir, 'tracked.txt'), 'two\n');
|
|
60
|
+
fs.writeFileSync(path.join(dir, 'untracked.txt'), 'scratch\n');
|
|
61
|
+
const featureDir = path.join(dir, 'specs', 'demo');
|
|
62
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
64
|
+
for (const [name, status] of [['task-01-one.md', 'in_progress'], ['task-02-two.md', 'pending']]) {
|
|
65
|
+
fs.writeFileSync(path.join(featureDir, name), [
|
|
66
|
+
`# ${name}`, '', `Status: ${status}`, '',
|
|
67
|
+
'## Dependencies', '', '- none', '',
|
|
68
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
69
|
+
].join('\n'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
clearCapture();
|
|
73
|
+
const result = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'auto', session_id: 'compact-session', cwd: dir });
|
|
74
|
+
assert.strictEqual(result.code, 0, result.stderr);
|
|
75
|
+
|
|
76
|
+
const record = readCapture();
|
|
77
|
+
assert.strictEqual(record.branch, 'work');
|
|
78
|
+
assert.match(record.head, /^[0-9a-f]{7,}$/);
|
|
79
|
+
assert.strictEqual(record.dirty_count, 3, 'one modified file, one untracked file, one untracked specs directory');
|
|
80
|
+
assert.strictEqual(record.trigger, 'auto');
|
|
81
|
+
assert.strictEqual(record.session_id, 'compact-session');
|
|
82
|
+
assert.strictEqual(record.workflow.feature, 'demo');
|
|
83
|
+
assert.deepStrictEqual(record.workflow.in_progress, ['task-01-one.md']);
|
|
84
|
+
assert.strictEqual(record.workflow.counts.in_progress, 1);
|
|
85
|
+
assert.strictEqual(record.workflow.counts.pending, 1);
|
|
86
|
+
} finally {
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
clearCapture();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a compact restart prints the captured anchors alongside the authorization warning', () => {
|
|
93
|
+
const dir = fixture();
|
|
94
|
+
try {
|
|
95
|
+
clearCapture();
|
|
96
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', cwd: dir }).code, 0);
|
|
97
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
|
|
98
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
99
|
+
assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/, 'the existing warning must survive');
|
|
100
|
+
assert.match(restart.stdout, /State before compaction/);
|
|
101
|
+
assert.match(restart.stdout, /branch work/);
|
|
102
|
+
assert.match(restart.stdout, /Re-read these from disk before acting/);
|
|
103
|
+
} finally {
|
|
104
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
105
|
+
clearCapture();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('a compact restart without a capture still prints the warning and nothing invented', () => {
|
|
110
|
+
const dir = fixture();
|
|
111
|
+
try {
|
|
112
|
+
clearCapture();
|
|
113
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
|
|
114
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
115
|
+
assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/);
|
|
116
|
+
assert.doesNotMatch(restart.stdout, /State before compaction/, 'no capture means no claim about prior state');
|
|
117
|
+
} finally {
|
|
118
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a normal session start prints no compaction anchors', () => {
|
|
123
|
+
const dir = fixture();
|
|
124
|
+
try {
|
|
125
|
+
clearCapture();
|
|
126
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir }).code, 0);
|
|
127
|
+
const startup = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'startup', cwd: dir });
|
|
128
|
+
assert.strictEqual(startup.code, 0, startup.stderr);
|
|
129
|
+
assert.doesNotMatch(startup.stdout, /State before compaction/);
|
|
130
|
+
assert.doesNotMatch(startup.stdout, /VERIFY PENDING AUTHORIZATIONS/);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
clearCapture();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('PreCompact fails open outside a repository and on malformed input', () => {
|
|
138
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-bare-'));
|
|
139
|
+
try {
|
|
140
|
+
clearCapture();
|
|
141
|
+
const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir });
|
|
142
|
+
assert.strictEqual(noGit.code, 0, 'a directory outside git must not fail the hook');
|
|
143
|
+
const record = readCapture();
|
|
144
|
+
assert.strictEqual(record.branch, null, 'unavailable git facts are recorded as null, never guessed');
|
|
145
|
+
assert.strictEqual(record.head, null);
|
|
146
|
+
assert.strictEqual(record.workflow, null);
|
|
147
|
+
|
|
148
|
+
clearCapture();
|
|
149
|
+
const malformed = spawnSync(process.execPath, [PRECOMPACT], {
|
|
150
|
+
cwd: dir, input: 'not json', encoding: 'utf8', env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
|
|
151
|
+
});
|
|
152
|
+
assert.strictEqual(malformed.status, 0, 'malformed payload must fail open');
|
|
153
|
+
assert.strictEqual(fs.existsSync(RECOVERY), false, 'a malformed payload writes no capture');
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
clearCapture();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// Behavioral tests for spec-gate.cjs (Stop completion gate). The hook is run as
|
|
4
4
|
// a real subprocess: a Stop payload is piped to stdin; we assert exit code and
|
|
5
|
-
// whether stdout carries {"decision":"block",...}.
|
|
6
|
-
//
|
|
5
|
+
// whether stdout carries {"decision":"block",...}. The hook resolves its own
|
|
6
|
+
// state directory; running from source that is a temp directory rather than the
|
|
7
|
+
// source tree, and it is shared, so each test seeds/clears it deliberately.
|
|
7
8
|
|
|
8
9
|
const { test, beforeEach, after } = require('node:test');
|
|
9
10
|
const assert = require('node:assert');
|
|
@@ -14,7 +15,8 @@ const os = require('node:os');
|
|
|
14
15
|
const path = require('node:path');
|
|
15
16
|
|
|
16
17
|
const HOOK = path.join(__dirname, '..', 'spec-gate.cjs');
|
|
17
|
-
const
|
|
18
|
+
const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
|
|
19
|
+
const CACHE = path.join(hookStateDir(), 'spec-gate-last.json');
|
|
18
20
|
const PROVENANCE = require(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'));
|
|
19
21
|
const POLICY = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'));
|
|
20
22
|
const FEATURE = 'demo';
|
|
@@ -286,6 +288,11 @@ function installClaudeGate(root, policyMode = 'valid') {
|
|
|
286
288
|
fs.mkdirSync(hooks, { recursive: true });
|
|
287
289
|
fs.mkdirSync(scripts, { recursive: true });
|
|
288
290
|
fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
|
|
291
|
+
fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
|
|
292
|
+
fs.copyFileSync(
|
|
293
|
+
path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
|
|
294
|
+
path.join(hooks, 'lib', 'hook-state-dir.cjs'),
|
|
295
|
+
);
|
|
289
296
|
for (const name of [
|
|
290
297
|
'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
|
|
291
298
|
]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
|
|
@@ -43,6 +43,11 @@ function inInstalledProcessSpecStateFixture(run) {
|
|
|
43
43
|
fs.mkdirSync(hooks, { recursive: true });
|
|
44
44
|
fs.mkdirSync(scripts, { recursive: true });
|
|
45
45
|
fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'spec-state.cjs'), path.join(hooks, 'spec-state.cjs'));
|
|
46
|
+
fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
|
|
47
|
+
fs.copyFileSync(
|
|
48
|
+
path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-state-dir.cjs'),
|
|
49
|
+
path.join(hooks, 'lib', 'hook-state-dir.cjs'),
|
|
50
|
+
);
|
|
46
51
|
for (const file of [
|
|
47
52
|
'provenance.cjs',
|
|
48
53
|
'spec-receipt.cjs',
|
|
@@ -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
|
+
});
|
|
@@ -90,7 +90,7 @@ try {
|
|
|
90
90
|
} catch (e) {
|
|
91
91
|
try {
|
|
92
92
|
const fs = require('fs'), p = require('path');
|
|
93
|
-
const d =
|
|
93
|
+
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
94
94
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
95
95
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
96
96
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'agent', status: 'crash', error: e.message }) + '\n');
|
|
@@ -14,7 +14,7 @@
|
|
|
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
|
|
@@ -24,6 +24,9 @@ try {
|
|
|
24
24
|
const config = loadConfig({ cwd, includeProject: false, includeAssertions: false, includeLocale: false });
|
|
25
25
|
|
|
26
26
|
const docsDir = path.join(cwd, config.paths?.docs || 'docs');
|
|
27
|
+
const specsDir = path.join(cwd, config.paths?.specs || 'specs');
|
|
28
|
+
const docsRelative = path.relative(cwd, docsDir).replace(/\\/g, '/');
|
|
29
|
+
const specsRelative = path.relative(cwd, specsDir).replace(/\\/g, '/');
|
|
27
30
|
|
|
28
31
|
// Xác định dự án đã có cốt lõi code hay chưa?
|
|
29
32
|
const hasCode = fs.existsSync(path.join(cwd, 'src')) ||
|
|
@@ -46,16 +49,18 @@ try {
|
|
|
46
49
|
lines.push('> Source exists but `docs/` does not. Create baseline docs first:');
|
|
47
50
|
lines.push('> 1. `docs/system-architecture.md` — system architecture and flows');
|
|
48
51
|
lines.push('> 2. `docs/project-overview-pdr.md` — overview, goals, features');
|
|
49
|
-
lines.push('> 3. Write
|
|
52
|
+
lines.push('> 3. Write the latest non-docs/non-Specs source hash to `docs/.sync_hash`.');
|
|
50
53
|
lines.push('');
|
|
51
54
|
}
|
|
52
55
|
// Case 2: docs exist — check continuous sync vs source-only git hash
|
|
53
56
|
else {
|
|
54
57
|
try {
|
|
55
58
|
// Latest SOURCE-only hash (ignore docs-only commits)
|
|
56
|
-
const currentHash =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const currentHash = execFileSync('git', [
|
|
60
|
+
'log', '-1', '--format=%H', '--', '.',
|
|
61
|
+
`:(exclude,literal)${docsRelative}`,
|
|
62
|
+
`:(exclude,literal)${specsRelative}`,
|
|
63
|
+
], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
59
64
|
|
|
60
65
|
if (currentHash) {
|
|
61
66
|
const syncTrackingFile = path.join(docsDir, '.sync_hash');
|
|
@@ -87,11 +92,8 @@ try {
|
|
|
87
92
|
} catch (e) {
|
|
88
93
|
// Ghi log lỗi ẩn danh nếu sập hook
|
|
89
94
|
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');
|
|
95
|
+
const fs = require('fs'), p = require('path');
|
|
96
|
+
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
95
97
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
96
98
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
97
99
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'docs-sync', status: 'crash', error: e.message }) + '\n');
|
|
@@ -115,7 +115,7 @@ try {
|
|
|
115
115
|
} catch (e) {
|
|
116
116
|
try {
|
|
117
117
|
const fs = require('fs'), p = require('path');
|
|
118
|
-
const d =
|
|
118
|
+
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
119
119
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
120
120
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
121
121
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'inspect-block', status: 'crash', error: e.message }) + '\n');
|
|
@@ -520,6 +520,9 @@ function loadConfig(options = {}) {
|
|
|
520
520
|
// Statusline mode
|
|
521
521
|
result.statusline = merged.statusline || 'full';
|
|
522
522
|
result.statuslineColors = merged.statuslineColors !== false;
|
|
523
|
+
if (merged.statuslineLayout && typeof merged.statuslineLayout === 'object') {
|
|
524
|
+
result.statuslineLayout = merged.statuslineLayout;
|
|
525
|
+
}
|
|
523
526
|
|
|
524
527
|
return sanitizeConfig(result, projectRoot);
|
|
525
528
|
} catch (e) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where a Claude hook keeps its own generated state: crash logs and the
|
|
5
|
+
* gate/tollgate caches.
|
|
6
|
+
*
|
|
7
|
+
* Hooks run from two places. In an installed project they live in
|
|
8
|
+
* `.claude/hooks/`, whose `.logs/` is gitignored, so writing beside the hook is
|
|
9
|
+
* correct. During this package's tests they run straight from
|
|
10
|
+
* `packages/spec/src/claude/hooks/`, where the same write lands inside the
|
|
11
|
+
* source tree as an untracked directory. That changes the worktree digest the
|
|
12
|
+
* receipt provenance is bound to, and the completion gate then reports every
|
|
13
|
+
* done task as stale. It surfaced three times as a false alarm before the cause
|
|
14
|
+
* was found, so source-tree runs write to a temp directory instead.
|
|
15
|
+
*
|
|
16
|
+
* Keeping the rule here means a new hook cannot reintroduce the bug by copying
|
|
17
|
+
* the old `path.join(__dirname, '.logs')` line.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// This module ships in `hooks/lib/`, so the hook directory is always its parent.
|
|
24
|
+
const HOOKS_DIR = path.join(__dirname, '..');
|
|
25
|
+
const SOURCE_MARKER = `${path.sep}packages${path.sep}spec${path.sep}src`;
|
|
26
|
+
|
|
27
|
+
/** True when `dir` sits inside this package's source tree rather than an install. */
|
|
28
|
+
function isSourceTree(dir) {
|
|
29
|
+
return String(dir || '').includes(SOURCE_MARKER);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Absolute directory for hook-generated state; create it before writing. */
|
|
33
|
+
function hookStateDir() {
|
|
34
|
+
return isSourceTree(HOOKS_DIR)
|
|
35
|
+
? path.join(os.tmpdir(), 'cafekit-hook-logs', 'claude')
|
|
36
|
+
: path.join(HOOKS_DIR, '.logs');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { hookStateDir, isSourceTree };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Haposoft. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* Secret-output guardrail keyword matcher.
|
|
5
|
+
*
|
|
6
|
+
* These patterns detect prompts about credential handling. They are deliberately
|
|
7
|
+
* separate from the privacy hooks: those gate whether a sensitive file may be READ,
|
|
8
|
+
* while this list drives a soft reminder about whether a value may be PRINTED.
|
|
9
|
+
* Approval for the first is not approval for the second.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SECRET_KEYWORD_PATTERNS = [
|
|
13
|
+
/\.env(?:\b|$)/i,
|
|
14
|
+
/(?:^|[^A-Za-z0-9])credentials?(?:$|[^A-Za-z0-9])/i,
|
|
15
|
+
/(?:^|[^A-Za-z0-9])secrets?(?:$|[^A-Za-z0-9])/i,
|
|
16
|
+
/(?:^|[^A-Za-z0-9])api[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
|
|
17
|
+
/(?:^|[^A-Za-z0-9])access[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
|
|
18
|
+
/(?:^|[^A-Za-z0-9])tokens?(?:$|[^A-Za-z0-9])/i,
|
|
19
|
+
/(?:^|[^A-Za-z0-9])bearer(?:$|[^A-Za-z0-9])/i,
|
|
20
|
+
/(?:^|[^A-Za-z0-9])jwts?(?:$|[^A-Za-z0-9])/i,
|
|
21
|
+
/(?:^|[^A-Za-z0-9])private[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
|
|
22
|
+
/(?:^|[^A-Za-z0-9])oauth(?:[\s_-]+client)?[\s_-]*secrets?(?:$|[^A-Za-z0-9])/i,
|
|
23
|
+
/(?:^|[^A-Za-z0-9])client[\s_-]*secrets?(?:$|[^A-Za-z0-9])/i,
|
|
24
|
+
/(?:^|[^A-Za-z0-9])cloud[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
|
|
25
|
+
/(?:^|[^A-Za-z0-9])env[\s_-]+files?(?:$|[^A-Za-z0-9])/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** The reminder text. It names no value and quotes no prompt. */
|
|
29
|
+
const REMINDER = [
|
|
30
|
+
'## Secret handling',
|
|
31
|
+
'- Do not print raw credentials, API keys, tokens, JWTs, private keys, or secret values into the conversation.',
|
|
32
|
+
'- Use [redacted], variable names, counts, or high-level status instead.',
|
|
33
|
+
'- Approval to read a sensitive file or command output does not grant permission to print raw values.',
|
|
34
|
+
'- If a value is needed for a machine action, pass it through a non-echoing path and report only success or failure.',
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
function containsSecretKeyword(prompt) {
|
|
38
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') return false;
|
|
39
|
+
return SECRET_KEYWORD_PATTERNS.some((pattern) => pattern.test(prompt));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { SECRET_KEYWORD_PATTERNS, REMINDER, containsSecretKeyword };
|