@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,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
|
+
});
|
|
@@ -30,6 +30,8 @@ function installSemanticRuntime(target, runtime) {
|
|
|
30
30
|
for (const name of ['semantic-review-authority.cjs', 'completion-authority-state.cjs']) {
|
|
31
31
|
fs.copyFileSync(path.join(PACKAGE_ROOT, `src/${runtime}/hooks`, name), path.join(hooks, name));
|
|
32
32
|
}
|
|
33
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-dir.cjs'), path.join(hooks, 'lib/runtime-dir.cjs'));
|
|
34
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/hook-payload.cjs'), path.join(hooks, 'lib/hook-payload.cjs'));
|
|
33
35
|
fs.copyFileSync(
|
|
34
36
|
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
35
37
|
path.join(hooks, 'lib/runtime-path-safety.cjs'),
|
|
@@ -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,13 @@ 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(path.join(__dirname, '..', 'lib', 'runtime-dir.cjs'), path.join(hooks, 'lib', 'runtime-dir.cjs'));
|
|
293
|
+
fs.copyFileSync(path.join(__dirname, '..', 'lib', 'hook-payload.cjs'), path.join(hooks, 'lib', 'hook-payload.cjs'));
|
|
294
|
+
fs.copyFileSync(
|
|
295
|
+
path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
|
|
296
|
+
path.join(hooks, 'lib', 'hook-state-dir.cjs'),
|
|
297
|
+
);
|
|
289
298
|
for (const name of [
|
|
290
299
|
'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
|
|
291
300
|
]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
|
|
@@ -1479,3 +1488,339 @@ test('36. process-v3 Stop accepts two completed packets with valid Receipts', ()
|
|
|
1479
1488
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1480
1489
|
}
|
|
1481
1490
|
});
|
|
1491
|
+
|
|
1492
|
+
// ── Receipt freshness: bound while written or altered, structure-only once committed on a clean tree ──
|
|
1493
|
+
|
|
1494
|
+
const STALE_BASE = 'f'.repeat(40);
|
|
1495
|
+
const STALE_HEAD = 'e'.repeat(40);
|
|
1496
|
+
|
|
1497
|
+
function commitAll(dir, message) {
|
|
1498
|
+
for (const args of [['add', '-A'], ['commit', '-qm', message, '--allow-empty']]) {
|
|
1499
|
+
const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
|
|
1500
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function boundReceipt(base = VALID_BASE, head = VALID_HEAD) {
|
|
1505
|
+
return [
|
|
1506
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1507
|
+
`Base: ${base}`, `Head: ${head}`, '```text', '$ node --test', 'pass: 1', '```',
|
|
1508
|
+
];
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function runHookFrom(dir, cwd) {
|
|
1512
|
+
const res = spawnSync(process.execPath, [HOOK], {
|
|
1513
|
+
cwd,
|
|
1514
|
+
input: JSON.stringify({ hook_event_name: 'Stop', session_id: 'test', transcript_path: '/tmp/t', stop_hook_active: false, cwd: dir }),
|
|
1515
|
+
encoding: 'utf8',
|
|
1516
|
+
env: { ...process.env, PROJECT_ROOT: dir },
|
|
1517
|
+
});
|
|
1518
|
+
return { code: res.status, stdout: (res.stdout || '').trim(), stderr: res.stderr || '' };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
test('37. committed receipt on a clean tree survives later commits', () => {
|
|
1522
|
+
const dir = makeWorkflowFixture(boundReceipt());
|
|
1523
|
+
try {
|
|
1524
|
+
commitAll(dir, 'receipt');
|
|
1525
|
+
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
|
1526
|
+
fs.writeFileSync(path.join(dir, 'src', 'unrelated.txt'), 'later change\n');
|
|
1527
|
+
commitAll(dir, 'unrelated source change');
|
|
1528
|
+
clearCache();
|
|
1529
|
+
const result = runHook({}, dir);
|
|
1530
|
+
assert.strictEqual(result.code, 0, result.stderr);
|
|
1531
|
+
assert.strictEqual(result.stdout, '', 'a committed, unchanged receipt on a clean tree must stay accepted after unrelated commits');
|
|
1532
|
+
} finally {
|
|
1533
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
test('38. untracked task file requires binding', () => {
|
|
1538
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1539
|
+
try {
|
|
1540
|
+
clearCache();
|
|
1541
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1542
|
+
assert.ok(body && body.decision === 'block', 'an untracked task file must keep full binding');
|
|
1543
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
1544
|
+
} finally {
|
|
1545
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
test('38b. the same stale receipt is accepted once committed on a clean tree', () => {
|
|
1550
|
+
// Documents the accepted trade-off: once committed and clean, Base and Head
|
|
1551
|
+
// must be concrete but are no longer compared against the runtime.
|
|
1552
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1553
|
+
try {
|
|
1554
|
+
commitAll(dir, 'receipt');
|
|
1555
|
+
clearCache();
|
|
1556
|
+
const result = runHook({}, dir);
|
|
1557
|
+
assert.strictEqual(result.stdout, '', result.stderr);
|
|
1558
|
+
} finally {
|
|
1559
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
test('39. staged but uncommitted task file requires binding', () => {
|
|
1564
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1565
|
+
try {
|
|
1566
|
+
const add = spawnSync('git', ['-C', dir, 'add', '-A'], { encoding: 'utf8' });
|
|
1567
|
+
assert.strictEqual(add.status, 0, add.stderr);
|
|
1568
|
+
clearCache();
|
|
1569
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1570
|
+
assert.ok(body && body.decision === 'block', 'git add alone must not grant structure-only');
|
|
1571
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
1572
|
+
} finally {
|
|
1573
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
test('40. modified receipt requires binding', () => {
|
|
1578
|
+
const dir = makeWorkflowFixture(boundReceipt());
|
|
1579
|
+
try {
|
|
1580
|
+
commitAll(dir, 'receipt');
|
|
1581
|
+
const taskFile = path.join(dir, 'specs', FEATURE, 'task-01-demo.md');
|
|
1582
|
+
const context = workflowRuntimeContext(dir);
|
|
1583
|
+
const before = fs.readFileSync(taskFile, 'utf8');
|
|
1584
|
+
const after = before.replace(`Head: ${context.head}`, `Head: ${STALE_HEAD}`);
|
|
1585
|
+
assert.notStrictEqual(after, before, 'fixture must contain the bound Head');
|
|
1586
|
+
fs.writeFileSync(taskFile, after);
|
|
1587
|
+
clearCache();
|
|
1588
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1589
|
+
assert.ok(body && body.decision === 'block', 'a receipt edited after commit must be rebound');
|
|
1590
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
1591
|
+
} finally {
|
|
1592
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
test('41. unborn HEAD fails closed before any receipt check', () => {
|
|
1597
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-gate-'));
|
|
1598
|
+
try {
|
|
1599
|
+
for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test']]) {
|
|
1600
|
+
const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
|
|
1601
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
1602
|
+
}
|
|
1603
|
+
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1604
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
1605
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1606
|
+
fs.writeFileSync(path.join(featureDir, 'task-01-demo.md'), [
|
|
1607
|
+
'# Task 01: demo', '', 'Status: done', '', '## Dependencies', '', '- none', '',
|
|
1608
|
+
'## Verification Plan', '', '- Command: node --test', '', ...boundReceipt(STALE_BASE, STALE_HEAD), '',
|
|
1609
|
+
].join('\n'));
|
|
1610
|
+
clearCache();
|
|
1611
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1612
|
+
assert.ok(body && body.decision === 'block', 'an unborn HEAD must block');
|
|
1613
|
+
// The block arrives from provenance derivation, which cannot resolve a base
|
|
1614
|
+
// commit here, so the receipt check is never reached. Asserting the reason
|
|
1615
|
+
// keeps this case honest: it guards fail-closed behavior on an unborn HEAD,
|
|
1616
|
+
// not the binding-mode selector. The selector's own HEAD-unreadable branch
|
|
1617
|
+
// is guarded by cases 38 and 39.
|
|
1618
|
+
assert.match(body.reason, /controlled failure/);
|
|
1619
|
+
} finally {
|
|
1620
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
test('42. dirty tree outside specs requires binding, regardless of working directory', () => {
|
|
1625
|
+
const dir = makeWorkflowFixture(boundReceipt());
|
|
1626
|
+
try {
|
|
1627
|
+
commitAll(dir, 'receipt');
|
|
1628
|
+
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
|
1629
|
+
fs.writeFileSync(path.join(dir, 'src', 'dirty.txt'), 'uncommitted\n');
|
|
1630
|
+
clearCache();
|
|
1631
|
+
const fromRoot = parseBlock(runHook({}, dir).stdout);
|
|
1632
|
+
assert.ok(fromRoot && fromRoot.decision === 'block', 'an uncommitted change outside specs must rebind');
|
|
1633
|
+
assert.match(fromRoot.reason, /\bprovenance\b/);
|
|
1634
|
+
clearCache();
|
|
1635
|
+
const fromSpecs = parseBlock(runHookFrom(dir, path.join(dir, 'specs', FEATURE)).stdout);
|
|
1636
|
+
assert.ok(fromSpecs && fromSpecs.decision === 'block', 'the status check must pin the project root, not the working directory');
|
|
1637
|
+
assert.match(fromSpecs.reason, /\bprovenance\b/);
|
|
1638
|
+
} finally {
|
|
1639
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
test('42b. a change hidden by skip-worktree or assume-unchanged requires binding', () => {
|
|
1644
|
+
// `git status` honors both index flags, so a tracked file outside the specs
|
|
1645
|
+
// root can differ from HEAD while the tree reports clean. Structure mode must
|
|
1646
|
+
// not be granted from a report that git itself is suppressing.
|
|
1647
|
+
for (const flag of ['--skip-worktree', '--assume-unchanged']) {
|
|
1648
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1649
|
+
try {
|
|
1650
|
+
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
|
1651
|
+
fs.writeFileSync(path.join(dir, 'src', 'hidden.txt'), 'committed\n');
|
|
1652
|
+
commitAll(dir, 'receipt and source');
|
|
1653
|
+
const marked = spawnSync('git', ['-C', dir, 'update-index', flag, 'src/hidden.txt'], { encoding: 'utf8' });
|
|
1654
|
+
assert.strictEqual(marked.status, 0, marked.stderr);
|
|
1655
|
+
fs.writeFileSync(path.join(dir, 'src', 'hidden.txt'), 'tampered\n');
|
|
1656
|
+
const status = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' });
|
|
1657
|
+
assert.strictEqual(status.stdout.trim(), '', `${flag} must make the tree look clean for this case to mean anything`);
|
|
1658
|
+
clearCache();
|
|
1659
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1660
|
+
assert.ok(body && body.decision === 'block', `${flag} must not grant structure-only`);
|
|
1661
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
1662
|
+
} finally {
|
|
1663
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
test('43. structure checks still block in committed mode', () => {
|
|
1669
|
+
const dir = makeWorkflowFixture([
|
|
1670
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 1',
|
|
1671
|
+
`Base: ${STALE_BASE}`, `Head: ${STALE_HEAD}`, '```text', '$ node --test', 'fail: 1', '```',
|
|
1672
|
+
]);
|
|
1673
|
+
try {
|
|
1674
|
+
commitAll(dir, 'receipt');
|
|
1675
|
+
clearCache();
|
|
1676
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1677
|
+
assert.ok(body && body.decision === 'block');
|
|
1678
|
+
assert.match(body.reason, /exit_result/);
|
|
1679
|
+
assert.doesNotMatch(body.reason, /\bprovenance\b/, 'committed mode must not report a provenance failure for a stale but concrete pair');
|
|
1680
|
+
} finally {
|
|
1681
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
test('43b. committed mode still requires concrete Base and Head fields', () => {
|
|
1686
|
+
const dir = makeWorkflowFixture([
|
|
1687
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1688
|
+
`Base: ${STALE_BASE}`, '```text', '$ node --test', 'pass: 1', '```',
|
|
1689
|
+
]);
|
|
1690
|
+
try {
|
|
1691
|
+
commitAll(dir, 'receipt without Head');
|
|
1692
|
+
clearCache();
|
|
1693
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1694
|
+
assert.ok(body && body.decision === 'block', 'a missing Head field must still block');
|
|
1695
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
1696
|
+
} finally {
|
|
1697
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
test('44. edited artifact bytes still block in committed mode', () => {
|
|
1702
|
+
const artifactPath = 'output/bundle.js';
|
|
1703
|
+
const dir = makeWorkflowFixture([
|
|
1704
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1705
|
+
`Base: ${STALE_BASE}`, `Head: ${STALE_HEAD}`,
|
|
1706
|
+
`Artifact: ${artifactPath} (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
|
|
1707
|
+
'```text', '$ node --test', 'pass: 1', '```',
|
|
1708
|
+
]);
|
|
1709
|
+
try {
|
|
1710
|
+
fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
|
|
1711
|
+
fs.writeFileSync(path.join(dir, artifactPath), FIXTURE_ARTIFACT);
|
|
1712
|
+
commitAll(dir, 'receipt and artifact');
|
|
1713
|
+
clearCache();
|
|
1714
|
+
const accepted = runHook({}, dir);
|
|
1715
|
+
assert.strictEqual(accepted.stdout, '', accepted.stderr);
|
|
1716
|
+
fs.writeFileSync(path.join(dir, artifactPath), 'tampered\n');
|
|
1717
|
+
commitAll(dir, 'tamper artifact');
|
|
1718
|
+
clearCache();
|
|
1719
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1720
|
+
assert.ok(body && body.decision === 'block', 'artifact bytes must still be verified in committed mode');
|
|
1721
|
+
assert.match(body.reason, /artifact_hash/);
|
|
1722
|
+
} finally {
|
|
1723
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
test('45. the cache grants no authority across a state change', () => {
|
|
1728
|
+
// A cached PASS must not survive a later mutation. The first run accepts and
|
|
1729
|
+
// writes the cache; the second run sees tampered artifact bytes committed on a
|
|
1730
|
+
// still-clean tree and must block without the cache being cleared first.
|
|
1731
|
+
const artifactPath = 'output/cached.js';
|
|
1732
|
+
const dir = makeWorkflowFixture([
|
|
1733
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1734
|
+
`Base: ${STALE_BASE}`, `Head: ${STALE_HEAD}`,
|
|
1735
|
+
`Artifact: ${artifactPath} (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
|
|
1736
|
+
'```text', '$ node --test', 'pass: 1', '```',
|
|
1737
|
+
]);
|
|
1738
|
+
try {
|
|
1739
|
+
fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
|
|
1740
|
+
fs.writeFileSync(path.join(dir, artifactPath), FIXTURE_ARTIFACT);
|
|
1741
|
+
commitAll(dir, 'receipt and artifact');
|
|
1742
|
+
clearCache();
|
|
1743
|
+
const accepted = runHook({}, dir);
|
|
1744
|
+
assert.strictEqual(accepted.stdout, '', 'first run must accept and populate the cache');
|
|
1745
|
+
fs.writeFileSync(path.join(dir, artifactPath), 'tampered after the cached pass\n');
|
|
1746
|
+
commitAll(dir, 'tamper artifact, tree stays clean');
|
|
1747
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1748
|
+
assert.ok(body && body.decision === 'block', 'a cached pass must not hide a later mutation');
|
|
1749
|
+
assert.match(body.reason, /artifact_hash/);
|
|
1750
|
+
} finally {
|
|
1751
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
test('46. a dirty file inside the specs root does not force binding', () => {
|
|
1756
|
+
// The status and index queries exclude the specs root on purpose: a receipt is
|
|
1757
|
+
// written into that root, so counting it would make every receipt invalidate
|
|
1758
|
+
// itself. Dropping the exclusion from either query must fail this case.
|
|
1759
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1760
|
+
try {
|
|
1761
|
+
commitAll(dir, 'receipt');
|
|
1762
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'plan.md'), '# Demo plan\n\nEdited after the receipt was committed.\n');
|
|
1763
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'notes-untracked.md'), 'scratch\n');
|
|
1764
|
+
const dirty = spawnSync('git', ['-C', dir, 'status', '--porcelain', '--untracked-files=all'], { encoding: 'utf8' });
|
|
1765
|
+
assert.match(dirty.stdout, /specs\//, 'the specs root must actually be dirty for this case to mean anything');
|
|
1766
|
+
clearCache();
|
|
1767
|
+
const result = runHook({}, dir);
|
|
1768
|
+
assert.strictEqual(result.stdout, '', 'work inside the specs root must not reopen a committed receipt');
|
|
1769
|
+
} finally {
|
|
1770
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
test('47. completed-set branch accepts committed receipts and blocks a tampered one', () => {
|
|
1775
|
+
// Two finished packets resolve to the completed-set branch, which is the path
|
|
1776
|
+
// that revalidated every done receipt on every Stop. Prove the new mode works
|
|
1777
|
+
// there, then prove it still blocks.
|
|
1778
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1779
|
+
try {
|
|
1780
|
+
const secondDir = path.join(dir, 'specs', 'second');
|
|
1781
|
+
fs.mkdirSync(secondDir, { recursive: true });
|
|
1782
|
+
fs.writeFileSync(path.join(secondDir, 'plan.md'), '# Second plan\n');
|
|
1783
|
+
fs.writeFileSync(path.join(secondDir, 'task-01-second.md'), [
|
|
1784
|
+
'# Task 01: second', '', 'Status: done', '', '## Dependencies', '', '- none', '',
|
|
1785
|
+
'## Verification Plan', '', '- Command: node --test', '', ...boundReceipt(STALE_BASE, STALE_HEAD), '',
|
|
1786
|
+
].join('\n'));
|
|
1787
|
+
commitAll(dir, 'two completed packets');
|
|
1788
|
+
clearCache();
|
|
1789
|
+
const accepted = runHook({}, dir);
|
|
1790
|
+
assert.strictEqual(accepted.stdout, '', 'the completed-set branch must accept committed receipts on a clean tree');
|
|
1791
|
+
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
|
1792
|
+
fs.writeFileSync(path.join(dir, 'src', 'dirty.txt'), 'uncommitted\n');
|
|
1793
|
+
clearCache();
|
|
1794
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1795
|
+
assert.ok(body && body.decision === 'block', 'the completed-set branch must rebind on a dirty tree');
|
|
1796
|
+
assert.match(body.reason, /\bprovenance\b/);
|
|
1797
|
+
assert.match(body.reason, /second|demo/);
|
|
1798
|
+
} finally {
|
|
1799
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
test('48. the gate\'s own runtime state does not defeat structure mode', () => {
|
|
1804
|
+
// The gate writes its cache under .claude/hooks/.logs on every Stop. If that
|
|
1805
|
+
// write counted as a dirty tree, a project tracking its runtime directory could
|
|
1806
|
+
// never reach structure mode and the gate would defeat itself. The status query
|
|
1807
|
+
// excludes the same runtime-state roots the provenance manifest already ignores.
|
|
1808
|
+
const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
|
|
1809
|
+
try {
|
|
1810
|
+
const stateDir = path.join(dir, '.claude', 'hooks', '.logs');
|
|
1811
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1812
|
+
fs.writeFileSync(path.join(stateDir, 'spec-gate-last.json'), '{}\n');
|
|
1813
|
+
fs.writeFileSync(path.join(dir, '.claude', 'runtime.json'), '{}\n');
|
|
1814
|
+
commitAll(dir, 'receipt plus tracked runtime state');
|
|
1815
|
+
// Mutate the tracked runtime state exactly as a Stop would.
|
|
1816
|
+
fs.writeFileSync(path.join(stateDir, 'spec-gate-last.json'), '{"demo":{"task-01-demo.md":"done"}}\n');
|
|
1817
|
+
fs.writeFileSync(path.join(dir, '.claude', 'runtime.json'), '{"spec":{}}\n');
|
|
1818
|
+
const dirty = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' });
|
|
1819
|
+
assert.match(dirty.stdout, /\.claude\//, 'the runtime state must actually be dirty for this case to mean anything');
|
|
1820
|
+
clearCache();
|
|
1821
|
+
const result = runHook({}, dir);
|
|
1822
|
+
assert.strictEqual(result.stdout, '', 'generated runtime state must not be read as a dirty worktree');
|
|
1823
|
+
} finally {
|
|
1824
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
@@ -43,6 +43,13 @@ 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(path.join(CLAUDE_ROOT, 'hooks', 'lib', 'runtime-dir.cjs'), path.join(hooks, 'lib', 'runtime-dir.cjs'));
|
|
48
|
+
fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-payload.cjs'), path.join(hooks, 'lib', 'hook-payload.cjs'));
|
|
49
|
+
fs.copyFileSync(
|
|
50
|
+
path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-state-dir.cjs'),
|
|
51
|
+
path.join(hooks, 'lib', 'hook-state-dir.cjs'),
|
|
52
|
+
);
|
|
46
53
|
for (const file of [
|
|
47
54
|
'provenance.cjs',
|
|
48
55
|
'spec-receipt.cjs',
|