@haposoft/cafekit 0.16.0-rc.7 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -27
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +295 -0
- package/bin/__tests__/codex-native.test.js +2289 -34
- package/bin/__tests__/develop-contract.test.js +400 -23
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +2131 -16
- package/bin/__tests__/runtime-schema.test.js +111 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
- package/bin/install.js +10 -0
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +14 -0
- package/bin/lib/i18n.js +12 -6
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +8 -2
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +24 -13
- package/src/claude/agents/strategist.md +52 -0
- package/src/claude/agents/test-runner.md +117 -132
- package/src/claude/agents/ui-ux-designer.md +1 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
- package/src/claude/hooks/__tests__/precompact.test.js +158 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +87 -4
- package/src/claude/hooks/__tests__/state.test.js +254 -3
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +1 -1
- package/src/claude/hooks/docs-sync.cjs +12 -10
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +3 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +105 -0
- package/src/claude/hooks/rules.cjs +1 -1
- package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
- package/src/claude/hooks/session.cjs +27 -1
- package/src/claude/hooks/spec-gate.cjs +3 -3
- package/src/claude/hooks/spec-state.cjs +32 -5
- package/src/claude/hooks/state.cjs +1 -1
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/hooks/usage.cjs +1 -1
- package/src/claude/migration-manifest.json +29 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/workflow.md +3 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +18 -5
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/scripts/workflow-policy.cjs +1 -1
- package/src/claude/settings/settings.json +16 -0
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/brainstorm/SKILL.md +193 -172
- package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
- package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +43 -25
- package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
- package/src/claude/skills/code-review/references/verification-gate.md +1 -1
- package/src/claude/skills/debug/SKILL.md +48 -19
- package/src/claude/skills/delegate/SKILL.md +1 -1
- package/src/claude/skills/develop/SKILL.md +92 -65
- package/src/claude/skills/develop/references/parallel-waves.md +36 -63
- package/src/claude/skills/develop/references/quality-gate.md +31 -4
- package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
- package/src/claude/skills/docs/SKILL.md +42 -23
- package/src/claude/skills/docs/references/init-workflow.md +4 -4
- package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
- package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
- package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +9 -7
- package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/commit-protocols.md +1 -1
- package/src/claude/skills/git/references/finish-branch.md +1 -1
- package/src/claude/skills/hotfix/SKILL.md +159 -170
- package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
- package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
- package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
- package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
- package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
- package/src/claude/skills/inspect/SKILL.md +55 -46
- package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
- package/src/claude/skills/loop/SKILL.md +114 -0
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/question/SKILL.md +16 -13
- package/src/claude/skills/research/SKILL.md +70 -43
- package/src/claude/skills/route/SKILL.md +94 -0
- package/src/claude/skills/route/references/agent-timing.md +46 -0
- package/src/claude/skills/route/references/chaining-patterns.md +53 -0
- package/src/claude/skills/route/references/task-taxonomy.md +48 -0
- package/src/claude/skills/specs/SKILL.md +48 -57
- package/src/claude/skills/specs/references/review.md +26 -8
- package/src/claude/skills/specs/references/templates.md +76 -31
- package/src/claude/skills/specs/templates/requirements-init.md +1 -1
- package/src/claude/skills/specs/templates/research.md +2 -2
- package/src/claude/skills/sync/SKILL.md +6 -6
- package/src/claude/skills/sync/references/sync-protocols.md +1 -1
- package/src/claude/skills/test/SKILL.md +120 -120
- package/src/claude/skills/test/references/execution-strategy.md +208 -361
- package/src/claude/skills/test/references/failure-triage.md +55 -121
- package/src/claude/skills/test/references/test-memory.md +20 -34
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/skills/web-testing/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/claude/status.cjs +135 -33
- package/src/codex/AGENTS.md +14 -4
- package/src/codex/hooks/docs-sync.cjs +8 -3
- package/src/codex/hooks/lib/hook-context.cjs +18 -4
- package/src/codex/hooks/rules.cjs +29 -1
- package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
- package/src/codex/hooks/spec-gate.cjs +3 -8
- package/src/codex/hooks/spec-state.cjs +34 -5
- package/src/codex/hooks.json +5 -0
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
|
@@ -0,0 +1,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';
|
|
@@ -99,6 +101,10 @@ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test')
|
|
|
99
101
|
'',
|
|
100
102
|
'Status: done',
|
|
101
103
|
'',
|
|
104
|
+
'## Dependencies',
|
|
105
|
+
'',
|
|
106
|
+
'- none',
|
|
107
|
+
'',
|
|
102
108
|
'## Verification Plan',
|
|
103
109
|
'',
|
|
104
110
|
`- Command: ${plannedCommand}`,
|
|
@@ -282,6 +288,11 @@ function installClaudeGate(root, policyMode = 'valid') {
|
|
|
282
288
|
fs.mkdirSync(hooks, { recursive: true });
|
|
283
289
|
fs.mkdirSync(scripts, { recursive: true });
|
|
284
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
|
+
);
|
|
285
296
|
for (const name of [
|
|
286
297
|
'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
|
|
287
298
|
]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
|
|
@@ -1318,10 +1329,80 @@ test('32. process-v3 requires Receipt heading and runtime-bound provenance', ()
|
|
|
1318
1329
|
}
|
|
1319
1330
|
});
|
|
1320
1331
|
|
|
1332
|
+
test('32b. process-v3 ignores a Receipt heading and proof contained only in a fence', () => {
|
|
1333
|
+
const dir = makeWorkflowFixture([
|
|
1334
|
+
'~~~~markdown', '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1335
|
+
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', 'pass: 1', '```', '~~~~',
|
|
1336
|
+
]);
|
|
1337
|
+
try {
|
|
1338
|
+
clearCache();
|
|
1339
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1340
|
+
assert.ok(body, 'fenced-only process-v3 Receipt must block');
|
|
1341
|
+
assert.strictEqual(body.decision, 'block');
|
|
1342
|
+
assert.match(body.reason, /missing_receipt/);
|
|
1343
|
+
} finally {
|
|
1344
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test('32c. process-v3 ignores canonical Receipt fields contained only in its output fence', () => {
|
|
1349
|
+
const dir = makeWorkflowFixture([
|
|
1350
|
+
'## Receipt', '', '```text', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1351
|
+
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, 'pass: 1', '```',
|
|
1352
|
+
]);
|
|
1353
|
+
try {
|
|
1354
|
+
clearCache();
|
|
1355
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1356
|
+
assert.ok(body, 'fenced-only canonical fields must block');
|
|
1357
|
+
assert.strictEqual(body.decision, 'block');
|
|
1358
|
+
assert.match(body.reason, /verification_state/);
|
|
1359
|
+
assert.match(body.reason, /command/);
|
|
1360
|
+
assert.match(body.reason, /provenance/);
|
|
1361
|
+
} finally {
|
|
1362
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
test('32d. process-v3 preserves explicit failures from the Receipt output fence', () => {
|
|
1367
|
+
const dir = makeWorkflowFixture([
|
|
1368
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1369
|
+
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
|
|
1370
|
+
'exit code: 1', 'FAIL', '```',
|
|
1371
|
+
]);
|
|
1372
|
+
try {
|
|
1373
|
+
clearCache();
|
|
1374
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1375
|
+
assert.ok(body, 'explicit fenced output failure must block');
|
|
1376
|
+
assert.strictEqual(body.decision, 'block');
|
|
1377
|
+
assert.match(body.reason, /verification_state/);
|
|
1378
|
+
assert.match(body.reason, /exit_result/);
|
|
1379
|
+
} finally {
|
|
1380
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test('32e. process-v3 rejects output whose closing fence is shorter than its opener', () => {
|
|
1385
|
+
const dir = makeWorkflowFixture([
|
|
1386
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1387
|
+
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '````text', '$ node --test',
|
|
1388
|
+
'pass: 1', '```',
|
|
1389
|
+
]);
|
|
1390
|
+
try {
|
|
1391
|
+
clearCache();
|
|
1392
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1393
|
+
assert.ok(body, 'malformed output fence must block');
|
|
1394
|
+
assert.strictEqual(body.decision, 'block');
|
|
1395
|
+
assert.match(body.reason, /command_output/);
|
|
1396
|
+
} finally {
|
|
1397
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1321
1401
|
test('33. process-v3 done task with canonical inline Receipt passes', () => {
|
|
1322
1402
|
const dir = makeWorkflowFixture([
|
|
1323
1403
|
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
1324
|
-
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
|
|
1404
|
+
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
|
|
1405
|
+
'Status: pending', '## Receipt', 'pass: 1', '```',
|
|
1325
1406
|
]);
|
|
1326
1407
|
try {
|
|
1327
1408
|
clearCache();
|
|
@@ -1360,6 +1441,7 @@ test('35. process-v3 Stop ignores a completed packet when one packet remains act
|
|
|
1360
1441
|
fs.writeFileSync(path.join(activeDir, 'plan.md'), '# Active plan\n');
|
|
1361
1442
|
fs.writeFileSync(path.join(activeDir, 'task-01-active.md'), [
|
|
1362
1443
|
'# Task 01: active', '', 'Status: pending', '',
|
|
1444
|
+
'## Dependencies', '', '- none', '',
|
|
1363
1445
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
1364
1446
|
].join('\n'));
|
|
1365
1447
|
clearCache();
|
|
@@ -1389,6 +1471,7 @@ test('36. process-v3 Stop accepts two completed packets with valid Receipts', ()
|
|
|
1389
1471
|
});
|
|
1390
1472
|
fs.writeFileSync(path.join(otherDir, 'task-01-other.md'), [
|
|
1391
1473
|
'# Task 01: other', '', 'Status: done', '',
|
|
1474
|
+
'## Dependencies', '', '- none', '',
|
|
1392
1475
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
1393
1476
|
'## Receipt', '',
|
|
1394
1477
|
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
@@ -7,7 +7,258 @@ const fs = require('node:fs');
|
|
|
7
7
|
const os = require('node:os');
|
|
8
8
|
const path = require('node:path');
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const CLAUDE_ROOT = path.join(__dirname, '..', '..');
|
|
11
|
+
const STATE_HOOK = path.join(CLAUDE_ROOT, 'hooks', 'state.cjs');
|
|
12
|
+
const POLICY = require(path.join(CLAUDE_ROOT, 'scripts', 'workflow-policy.cjs'));
|
|
13
|
+
|
|
14
|
+
function processTaskContent(title, status, dependency = 'none') {
|
|
15
|
+
return [
|
|
16
|
+
`# ${title}`, '', `Status: ${status}`, '',
|
|
17
|
+
'## Dependencies', '', `- ${dependency}`, '',
|
|
18
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
19
|
+
].join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canonicalProcessTask(root, title, dependency = 'none') {
|
|
23
|
+
const context = POLICY.deriveRuntimeContext({
|
|
24
|
+
projectRoot: root,
|
|
25
|
+
specsRoot: path.join(root, 'specs'),
|
|
26
|
+
specFile: path.join(root, 'specs', 'auth', 'plan.md'),
|
|
27
|
+
featureName: 'auth',
|
|
28
|
+
runtimeSession: 'session-auth-state',
|
|
29
|
+
});
|
|
30
|
+
return [
|
|
31
|
+
processTaskContent(title, 'done', dependency),
|
|
32
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
33
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
34
|
+
'```text', 'node --test', '1 test passed', '```', '',
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function inInstalledProcessSpecStateFixture(run) {
|
|
39
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-state-'));
|
|
40
|
+
try {
|
|
41
|
+
const hooks = path.join(dir, '.claude', 'hooks');
|
|
42
|
+
const scripts = path.join(dir, '.claude', 'scripts');
|
|
43
|
+
fs.mkdirSync(hooks, { recursive: true });
|
|
44
|
+
fs.mkdirSync(scripts, { recursive: true });
|
|
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
|
+
);
|
|
51
|
+
for (const file of [
|
|
52
|
+
'provenance.cjs',
|
|
53
|
+
'spec-receipt.cjs',
|
|
54
|
+
'spec-resolver.cjs',
|
|
55
|
+
'workflow-policy.cjs',
|
|
56
|
+
]) {
|
|
57
|
+
fs.copyFileSync(path.join(CLAUDE_ROOT, 'scripts', file), path.join(scripts, file));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const feature = path.join(dir, 'specs', 'auth');
|
|
61
|
+
fs.mkdirSync(feature, { recursive: true });
|
|
62
|
+
fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
63
|
+
|
|
64
|
+
for (const args of [
|
|
65
|
+
['init', '-q'],
|
|
66
|
+
['config', 'user.email', 'cafekit@example.invalid'],
|
|
67
|
+
['config', 'user.name', 'CafeKit Test'],
|
|
68
|
+
['commit', '--allow-empty', '-qm', 'fixture'],
|
|
69
|
+
]) {
|
|
70
|
+
const git = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
|
|
71
|
+
assert.strictEqual(git.status, 0, git.stderr);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const writeTask = (basename, title, status, dependency = 'none') => {
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(feature, basename),
|
|
77
|
+
processTaskContent(title, status, dependency),
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
const runState = () => spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
|
|
81
|
+
cwd: dir,
|
|
82
|
+
env: { ...process.env, PROJECT_ROOT: dir },
|
|
83
|
+
input: JSON.stringify({
|
|
84
|
+
cwd: dir,
|
|
85
|
+
featureName: 'auth',
|
|
86
|
+
session_id: 'session-auth-state',
|
|
87
|
+
hook_event_name: 'UserPromptSubmit',
|
|
88
|
+
prompt: 'Continue',
|
|
89
|
+
}),
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
});
|
|
92
|
+
return run({
|
|
93
|
+
cacheFile: path.join(hooks, '.logs', 'tollgate-last.txt'),
|
|
94
|
+
dir,
|
|
95
|
+
feature,
|
|
96
|
+
hooks,
|
|
97
|
+
runState,
|
|
98
|
+
writePlan: (content) => fs.writeFileSync(path.join(feature, 'plan.md'), content),
|
|
99
|
+
writeTask,
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test('Claude installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
|
|
107
|
+
inInstalledProcessSpecStateFixture(({ cacheFile, hooks, runState, writeTask }) => {
|
|
108
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
|
|
109
|
+
|
|
110
|
+
const blocked = runState();
|
|
111
|
+
assert.strictEqual(blocked.status, 0, blocked.stderr);
|
|
112
|
+
assert.match(blocked.stdout, /0 pending, 1 blocked/);
|
|
113
|
+
assert.doesNotMatch(blocked.stdout, /Next unblocked/);
|
|
114
|
+
assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
|
|
115
|
+
assert.strictEqual(fs.existsSync(cacheFile), true, 'installed cache must be created');
|
|
116
|
+
const blockedCache = fs.readFileSync(cacheFile, 'utf8');
|
|
117
|
+
|
|
118
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
|
|
119
|
+
const pending = runState();
|
|
120
|
+
assert.strictEqual(pending.status, 0, pending.stderr);
|
|
121
|
+
assert.match(pending.stdout, /Spec state changed: `auth`/);
|
|
122
|
+
assert.match(pending.stdout, /1 pending, 0 blocked/);
|
|
123
|
+
assert.match(pending.stdout, /Next unblocked: `task-01-auth\.md`/);
|
|
124
|
+
assert.notStrictEqual(fs.readFileSync(cacheFile, 'utf8'), blockedCache, 'state cache must change');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('Claude installed spec-state requires current canonical dependency proof and keys receipt mutations', () => {
|
|
129
|
+
inInstalledProcessSpecStateFixture(({ cacheFile, dir, feature, hooks, runState, writeTask }) => {
|
|
130
|
+
const predecessor = 'task-01-semantic-review.md';
|
|
131
|
+
const dependent = 'task-02-implementation.md';
|
|
132
|
+
writeTask(predecessor, 'Task 01: semantic review', 'blocked');
|
|
133
|
+
writeTask(dependent, 'Task 02: implementation', 'pending', predecessor);
|
|
134
|
+
const dependentPath = path.join(feature, dependent);
|
|
135
|
+
const unchangedDependent = fs.readFileSync(dependentPath, 'utf8');
|
|
136
|
+
|
|
137
|
+
const blocked = runState();
|
|
138
|
+
assert.strictEqual(blocked.status, 0, blocked.stderr);
|
|
139
|
+
assert.match(blocked.stdout, /0 done \/ 2 total \(0 in_progress, 1 pending, 1 blocked\)/);
|
|
140
|
+
assert.doesNotMatch(blocked.stdout, /Next unblocked/);
|
|
141
|
+
assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
|
|
142
|
+
assert.strictEqual(fs.existsSync(cacheFile), true, 'installed cache must be created');
|
|
143
|
+
const blockedCache = fs.readFileSync(cacheFile, 'utf8');
|
|
144
|
+
|
|
145
|
+
writeTask(predecessor, 'Task 01: semantic review', 'done');
|
|
146
|
+
const missing = runState();
|
|
147
|
+
assert.strictEqual(missing.status, 0, missing.stderr);
|
|
148
|
+
assert.doesNotMatch(missing.stdout, /Next unblocked/);
|
|
149
|
+
const missingCache = fs.readFileSync(cacheFile, 'utf8');
|
|
150
|
+
assert.notStrictEqual(missingCache, blockedCache, 'done without proof must change cached proof state');
|
|
151
|
+
|
|
152
|
+
fs.appendFileSync(path.join(feature, predecessor), '\n## Receipt\n\nVerification: PASS\n');
|
|
153
|
+
const malformed = runState();
|
|
154
|
+
assert.strictEqual(malformed.status, 0, malformed.stderr);
|
|
155
|
+
assert.doesNotMatch(malformed.stdout, /Next unblocked/);
|
|
156
|
+
|
|
157
|
+
fs.writeFileSync(path.join(feature, predecessor), canonicalProcessTask(dir, 'Task 01: semantic review'));
|
|
158
|
+
const ready = runState();
|
|
159
|
+
assert.strictEqual(ready.status, 0, ready.stderr);
|
|
160
|
+
assert.match(ready.stdout, /Next unblocked: `task-02-implementation\.md`/);
|
|
161
|
+
const readyCache = fs.readFileSync(cacheFile, 'utf8');
|
|
162
|
+
assert.notStrictEqual(readyCache, missingCache, 'canonical proof must change cached proof state');
|
|
163
|
+
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
path.join(feature, predecessor),
|
|
166
|
+
fs.readFileSync(path.join(feature, predecessor), 'utf8').replace('Verification: PASS', 'Verification: FAIL'),
|
|
167
|
+
);
|
|
168
|
+
const mutated = runState();
|
|
169
|
+
assert.strictEqual(mutated.status, 0, mutated.stderr);
|
|
170
|
+
assert.doesNotMatch(mutated.stdout, /Next unblocked/);
|
|
171
|
+
assert.notStrictEqual(fs.readFileSync(cacheFile, 'utf8'), readyCache, 'receipt-only mutation must change cache');
|
|
172
|
+
assert.strictEqual(fs.readFileSync(dependentPath, 'utf8'), unchangedDependent, 'dependent must remain pending');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('Claude installed spec-state keeps unversioned pending packets out of Next with migration guidance', () => {
|
|
177
|
+
inInstalledProcessSpecStateFixture(({ feature, runState, writePlan, writeTask }) => {
|
|
178
|
+
const fencedMarkerPlan = '# Auth plan\n\n```markdown\nSpecs-Contract: process-first-ready-v1\n```\n';
|
|
179
|
+
writePlan(fencedMarkerPlan);
|
|
180
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
|
|
181
|
+
const taskBytes = fs.readFileSync(path.join(feature, 'task-01-auth.md'));
|
|
182
|
+
const result = runState();
|
|
183
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
184
|
+
assert.doesNotMatch(result.stdout, /Next unblocked/);
|
|
185
|
+
assert.match(result.stdout, /Migration required: add `Specs-Contract: process-first-ready-v1`/);
|
|
186
|
+
const cached = runState();
|
|
187
|
+
assert.strictEqual(cached.status, 0, cached.stderr);
|
|
188
|
+
assert.doesNotMatch(cached.stdout, /Next unblocked/);
|
|
189
|
+
assert.match(cached.stdout, /Add `Specs-Contract: process-first-ready-v1`/);
|
|
190
|
+
|
|
191
|
+
writePlan(fencedMarkerPlan.replace('# Auth plan\n', '# Auth plan\nSpecs-Contract: process-first-ready-v1\n'));
|
|
192
|
+
const marked = runState();
|
|
193
|
+
assert.strictEqual(marked.status, 0, marked.stderr);
|
|
194
|
+
assert.match(marked.stdout, /Spec state changed: `auth`/);
|
|
195
|
+
assert.match(marked.stdout, /Next unblocked: `task-01-auth\.md`/);
|
|
196
|
+
assert.deepStrictEqual(fs.readFileSync(path.join(feature, 'task-01-auth.md')), taskBytes, 'marker-only transition must not change task bytes');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('Claude resolver ignores fenced workflow annotations and rejects fenced-only Status', () => {
|
|
201
|
+
inInstalledProcessSpecStateFixture(({ dir, feature, runState, writePlan, writeTask }) => {
|
|
202
|
+
const resolver = require(path.join(dir, '.claude', 'scripts', 'spec-resolver.cjs'));
|
|
203
|
+
const trailingFence = resolver.annotatedMarkdownLines([
|
|
204
|
+
'```markdown', '``` trailing', 'Specs-Contract: process-first-ready-v1',
|
|
205
|
+
'Status: done', '## Receipt', '```',
|
|
206
|
+
].join('\n'));
|
|
207
|
+
assert.deepStrictEqual(trailingFence.slice(2, 5).map(({ outsideFence }) => outsideFence), [false, false, false]);
|
|
208
|
+
const fourSpaceFence = resolver.annotatedMarkdownLines(' ```markdown\nStatus: pending');
|
|
209
|
+
assert.strictEqual(fourSpaceFence[1].outsideFence, true);
|
|
210
|
+
writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
211
|
+
fs.writeFileSync(path.join(feature, 'task-01-fenced-only.md'), [
|
|
212
|
+
'# Task 01', '', '```markdown', 'Status: done', '```', '',
|
|
213
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '',
|
|
214
|
+
].join('\n'));
|
|
215
|
+
const malformed = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
|
|
216
|
+
assert.strictEqual(malformed.error, 'explicit_malformed');
|
|
217
|
+
assert.match(malformed.reason, /must contain exactly one supported Status field/);
|
|
218
|
+
|
|
219
|
+
fs.rmSync(path.join(feature, 'task-01-fenced-only.md'));
|
|
220
|
+
writeTask('task-01-dependent.md', 'Task 01: dependent', 'pending', 'task-02-predecessor.md');
|
|
221
|
+
fs.writeFileSync(path.join(feature, 'task-02-predecessor.md'), [
|
|
222
|
+
'# Task 02', '', 'Status: pending', '', '## Dependencies', '', '- none', '',
|
|
223
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
224
|
+
'~~~~markdown', 'Status: done', '## Dependencies', '- task-01-dependent.md', '## Receipt',
|
|
225
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0', '```text', 'fake pass', '```', '~~~~', '',
|
|
226
|
+
].join('\n'));
|
|
227
|
+
const result = runState();
|
|
228
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
229
|
+
assert.match(result.stdout, /Next unblocked: `task-02-predecessor\.md`/);
|
|
230
|
+
assert.doesNotMatch(result.stdout, /Next unblocked: `task-01-dependent\.md`/);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('Claude resolver accepts unique legacy task numbers, rejects ambiguity, and rejects exact cycles', () => {
|
|
235
|
+
inInstalledProcessSpecStateFixture(({ dir, feature, writePlan, writeTask }) => {
|
|
236
|
+
const resolver = require(path.join(dir, '.claude', 'scripts', 'spec-resolver.cjs'));
|
|
237
|
+
writePlan('# Auth plan\n');
|
|
238
|
+
writeTask('task-01-a.md', 'Task 01: a', 'pending');
|
|
239
|
+
writeTask('task-02-b.md', 'Task 02: b', 'pending', 'Task 01');
|
|
240
|
+
fs.writeFileSync(path.join(feature, 'task-03-c.md'), '# Task 03: c\n\nStatus: pending\n');
|
|
241
|
+
const unique = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
|
|
242
|
+
assert.deepStrictEqual(unique.taskRegistry['task-02-b.md'].dependencies, ['task-01-a.md']);
|
|
243
|
+
assert.deepStrictEqual(unique.taskRegistry['task-03-c.md'].dependencies, []);
|
|
244
|
+
assert.strictEqual(unique.workflowContract, null);
|
|
245
|
+
assert.strictEqual(unique.queueReady, false);
|
|
246
|
+
|
|
247
|
+
writeTask('task-01-second.md', 'Task 01: second', 'pending');
|
|
248
|
+
const ambiguous = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
|
|
249
|
+
assert.strictEqual(ambiguous.error, 'explicit_malformed');
|
|
250
|
+
assert.match(ambiguous.reason, /legacy dependency Task 01 maps to 2 task basenames/);
|
|
251
|
+
|
|
252
|
+
fs.rmSync(path.join(feature, 'task-01-second.md'));
|
|
253
|
+
fs.rmSync(path.join(feature, 'task-03-c.md'));
|
|
254
|
+
writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
255
|
+
writeTask('task-01-a.md', 'Task 01: a', 'pending', 'task-02-b.md');
|
|
256
|
+
writeTask('task-02-b.md', 'Task 02: b', 'pending', 'task-01-a.md');
|
|
257
|
+
const cycle = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
|
|
258
|
+
assert.strictEqual(cycle.error, 'explicit_malformed');
|
|
259
|
+
assert.match(cycle.reason, /dependency cycle detected: task-01-a\.md -> task-02-b\.md -> task-01-a\.md/);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
11
262
|
|
|
12
263
|
test('Stop snapshot includes tracked modifications and untracked files', () => {
|
|
13
264
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-state-'));
|
|
@@ -22,7 +273,7 @@ test('Stop snapshot includes tracked modifications and untracked files', () => {
|
|
|
22
273
|
|
|
23
274
|
fs.writeFileSync(path.join(dir, 'tracked.txt'), 'after\n');
|
|
24
275
|
fs.writeFileSync(path.join(dir, 'new-untracked.json'), '{}\n');
|
|
25
|
-
const result = spawnSync(process.execPath, [
|
|
276
|
+
const result = spawnSync(process.execPath, [STATE_HOOK], {
|
|
26
277
|
cwd: dir,
|
|
27
278
|
input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
|
|
28
279
|
encoding: 'utf8',
|
|
@@ -47,7 +298,7 @@ test('Stop snapshot includes untracked files before the first commit', () => {
|
|
|
47
298
|
spawnSync('git', ['init', '-q'], { cwd: dir });
|
|
48
299
|
fs.writeFileSync(path.join(dir, 'first-untracked.txt'), 'new\n');
|
|
49
300
|
|
|
50
|
-
const result = spawnSync(process.execPath, [
|
|
301
|
+
const result = spawnSync(process.execPath, [STATE_HOOK], {
|
|
51
302
|
cwd: dir,
|
|
52
303
|
input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
|
|
53
304
|
encoding: 'utf8',
|