@haposoft/cafekit 0.16.7 → 0.18.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 +14 -22
- package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
- package/bin/__tests__/codex-hooks.test.js +0 -36
- package/bin/__tests__/codex-native.test.js +46 -88
- package/bin/__tests__/develop-contract.test.js +26 -57
- package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
- package/bin/__tests__/grok-envelope.test.js +0 -56
- package/bin/__tests__/hook-payload.test.js +4 -6
- package/bin/__tests__/legacy-authority-decoupled.test.js +159 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
- package/bin/__tests__/omp-bridge.test.js +20 -17
- package/bin/__tests__/omp-hooks.test.js +0 -19
- package/bin/__tests__/optional-skill-inventory.test.js +112 -4
- package/bin/__tests__/orca-session.test.js +0 -2
- package/bin/__tests__/package-inventory.test.js +100 -541
- package/bin/__tests__/runtime-dir.test.js +0 -3
- package/bin/__tests__/runtime-schema.test.js +3 -2
- package/bin/__tests__/skill-routing-source.test.js +27 -15
- package/bin/__tests__/spec-narrowing.test.js +0 -27
- package/bin/__tests__/specs-v2-execution-closeout.test.js +4 -69
- package/bin/__tests__/ui-ux-skill.test.js +101 -0
- package/bin/__tests__/usage-hook-retired.test.js +114 -0
- package/bin/__tests__/validator-grounding.test.js +9 -28
- package/bin/phases/codex-runtime.js +24 -0
- package/bin/phases/omp-runtime.js +22 -0
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +3 -10
- package/src/claude/agents/brainstormer.md +7 -3
- package/src/claude/agents/code-auditor.md +35 -50
- package/src/claude/agents/debugger.md +5 -79
- package/src/claude/agents/deployer.md +3 -5
- package/src/claude/agents/docs-keeper.md +2 -7
- package/src/claude/agents/implementer.md +6 -9
- package/src/claude/agents/inspector.md +2 -3
- package/src/claude/agents/project-manager.md +7 -15
- package/src/claude/agents/researcher.md +9 -3
- package/src/claude/agents/spec-maker.md +11 -11
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +74 -3
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +134 -0
- package/src/claude/hooks/__tests__/state.test.js +68 -0
- package/src/claude/hooks/__tests__/statusline.test.js +140 -11
- package/src/claude/hooks/agent.cjs +2 -1
- package/src/claude/hooks/lib/context.cjs +0 -89
- package/src/claude/hooks/lib/hook-payload.cjs +2 -2
- package/src/claude/hooks/rules.cjs +2 -1
- package/src/claude/hooks/session.cjs +0 -1
- package/src/claude/hooks/spec-gate.cjs +17 -28
- package/src/claude/hooks/spec-state.cjs +95 -1
- package/src/claude/hooks/state.cjs +21 -2
- package/src/claude/migration-manifest.json +30 -15
- package/src/claude/rules/ai-dev-rules.md +1 -1
- package/src/claude/rules/hook-protocols.md +8 -15
- package/src/claude/rules/manage-docs.md +9 -23
- package/src/claude/rules/orchestrator.md +3 -4
- package/src/claude/rules/review-audit-self-decision.md +3 -3
- package/src/claude/rules/skill-domain-routing.md +1 -3
- package/src/claude/rules/skill-workflow-routing.md +4 -5
- package/src/claude/rules/state-sync.md +3 -12
- package/src/claude/rules/workflow.md +10 -18
- package/src/claude/runtime.schema.json +5 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
- package/src/claude/scripts/provenance.cjs +23 -5
- package/src/claude/scripts/spec-receipt.cjs +24 -7
- package/src/claude/scripts/spec-resolver.cjs +1 -2
- package/src/claude/scripts/validate-spec-output.cjs +5 -32
- package/src/claude/settings/settings.json +1 -32
- package/src/claude/skills/{question → ask}/SKILL.md +9 -75
- package/src/claude/skills/brainstorm/SKILL.md +43 -131
- package/src/claude/skills/code-review/SKILL.md +48 -17
- package/src/claude/skills/code-review/references/spec-compliance-review.md +2 -3
- package/src/claude/skills/code-review/references/verification-gate.md +22 -27
- package/src/claude/skills/debug/SKILL.md +29 -149
- package/src/claude/skills/develop/SKILL.md +27 -36
- package/src/claude/skills/develop/references/parallel-waves.md +0 -8
- package/src/claude/skills/develop/references/quality-gate.md +15 -13
- package/src/claude/skills/docs/SKILL.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +1 -1
- package/src/claude/skills/fix/SKILL.md +153 -0
- package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
- package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
- package/src/claude/skills/git/SKILL.md +48 -9
- package/src/claude/skills/git/references/commit-protocols.md +35 -16
- package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
- package/src/claude/skills/orca/SKILL.md +9 -8
- package/src/claude/skills/research/SKILL.md +2 -0
- package/src/claude/skills/scout/SKILL.md +61 -0
- package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
- package/src/claude/skills/specs/SKILL.md +22 -29
- package/src/claude/skills/specs/references/review.md +8 -6
- package/src/claude/skills/specs/references/templates.md +34 -41
- package/src/claude/skills/sync/SKILL.md +8 -15
- package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
- package/src/claude/skills/sync/references/sync-protocols.md +4 -13
- package/src/claude/skills/test/SKILL.md +22 -3
- package/src/claude/skills/test/references/execution-strategy.md +83 -0
- package/src/claude/skills/test/references/failure-triage.md +1 -1
- package/src/claude/skills/ui-ux/LICENSE +21 -0
- package/src/claude/skills/ui-ux/SKILL.md +497 -0
- package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
- package/src/claude/skills/ui-ux/references/budgets.md +113 -0
- package/src/claude/skills/ui-ux/references/checklist.md +271 -0
- package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
- package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
- package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
- package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
- package/src/claude/skills/ui-ux/references/components/button.md +142 -0
- package/src/claude/skills/ui-ux/references/components/card.md +113 -0
- package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
- package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
- package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
- package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
- package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
- package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
- package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
- package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
- package/src/claude/skills/ui-ux/references/components/input.md +161 -0
- package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
- package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
- package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
- package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
- package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
- package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
- package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
- package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
- package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
- package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
- package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
- package/src/claude/skills/ui-ux/references/design-process.md +650 -0
- package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
- package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
- package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
- package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
- package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
- package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
- package/src/claude/skills/ui-ux/references/principles.md +408 -0
- package/src/claude/skills/ui-ux/references/refactor.md +245 -0
- package/src/claude/skills/ui-ux/references/responsive.md +195 -0
- package/src/claude/skills/ui-ux/references/review.md +528 -0
- package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
- package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
- package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
- package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
- package/src/claude/skills/ui-ux/references/styles.md +311 -0
- package/src/claude/skills/ui-ux/references/system.md +219 -0
- package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
- package/src/claude/skills/ui-ux/references/tokens.css +362 -0
- package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
- package/src/claude/skills/ui-ux/scripts/package.json +9 -0
- package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/status.cjs +149 -144
- package/src/codex/AGENTS.md +8 -13
- package/src/codex/hooks/agent.cjs +2 -1
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -2
- package/src/codex/hooks/lib/spec-utils.cjs +0 -7
- package/src/codex/hooks/rules.cjs +2 -1
- package/src/codex/hooks/session.cjs +0 -2
- package/src/codex/hooks/spec-gate.cjs +7 -18
- package/src/codex/hooks.json +0 -20
- package/src/codex/rules/state-sync.md +3 -12
- package/src/common/AGENTS.md +2 -2
- package/src/omp/extensions/cafekit-bridge.mjs +6 -6
- package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
- package/src/claude/hooks/completion-authority-check.cjs +0 -56
- package/src/claude/hooks/completion-authority-state.cjs +0 -527
- package/src/claude/hooks/completion-authority.cjs +0 -163
- package/src/claude/hooks/semantic-review-authority.cjs +0 -154
- package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
- package/src/claude/hooks/usage.cjs +0 -197
- package/src/claude/skills/delegate/SKILL.md +0 -124
- package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
- package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
- package/src/claude/skills/hotfix/SKILL.md +0 -289
- package/src/claude/skills/inspect/SKILL.md +0 -142
- package/src/claude/skills/loop/SKILL.md +0 -114
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
- package/src/claude/skills/route/SKILL.md +0 -94
- package/src/claude/skills/route/references/agent-timing.md +0 -46
- package/src/claude/skills/route/references/chaining-patterns.md +0 -53
- package/src/claude/skills/route/references/task-taxonomy.md +0 -48
- package/src/codex/hooks/completion-authority-check.cjs +0 -67
- package/src/codex/hooks/completion-authority-state.cjs +0 -527
- package/src/codex/hooks/completion-authority.cjs +0 -109
- package/src/codex/hooks/semantic-review-authority.cjs +0 -130
- package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
- /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
|
@@ -17,7 +17,6 @@ const RECEIPTS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-receip
|
|
|
17
17
|
const FINAL_STATE = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-final-state.cjs'));
|
|
18
18
|
const VALIDATOR = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs'));
|
|
19
19
|
const GROUNDER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs'));
|
|
20
|
-
const SEMANTIC_AUTHORITY = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs'));
|
|
21
20
|
const READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
|
|
22
21
|
const AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
|
|
23
22
|
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
@@ -80,9 +79,6 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
|
|
|
80
79
|
const hooksRoot = path.join(PACKAGE_ROOT, 'src', 'claude', 'hooks');
|
|
81
80
|
for (const hook of [
|
|
82
81
|
'spec-gate.cjs',
|
|
83
|
-
'completion-authority-check.cjs',
|
|
84
|
-
'completion-authority-state.cjs',
|
|
85
|
-
'semantic-review-authority.cjs',
|
|
86
82
|
]) {
|
|
87
83
|
const target = path.join(destinationRoot, 'hooks', hook);
|
|
88
84
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -759,10 +755,7 @@ test('CLI exposes a derived lane without making it primary Develop authority', (
|
|
|
759
755
|
const regions = markdownLegacyRegions(develop);
|
|
760
756
|
assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
|
|
761
757
|
assert.doesNotMatch(regions.primary, /planning_depth|assurance_level|execution_tier|\blane\b/i);
|
|
762
|
-
assert.
|
|
763
|
-
assert.match(regions.legacy, /assurance_level/);
|
|
764
|
-
assert.match(regions.legacy, /execution_tier/);
|
|
765
|
-
assert.match(regions.legacy, /derived lane/i);
|
|
758
|
+
assert.doesNotMatch(develop, /## Legacy workflow compatibility|Also supports existing legacy Specs packets/);
|
|
766
759
|
assert.match(regions.primary, /specs\/<feature>\/plan\.md[\s\S]*task-NN-\*\.md/i);
|
|
767
760
|
assert.match(regions.primary, /one unblocked task at a time/i);
|
|
768
761
|
assert.match(regions.primary, /Verification Plan/);
|
|
@@ -789,66 +782,63 @@ test('Specs primary output is a flat process-first packet with isolated legacy c
|
|
|
789
782
|
const specs = read(SPECS);
|
|
790
783
|
const regions = markdownLegacyRegions(specs);
|
|
791
784
|
const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
|
|
792
|
-
assert.equal(legacyHeadings.length,
|
|
785
|
+
assert.equal(legacyHeadings.length, 0);
|
|
793
786
|
assert.match(regions.primary, /specs\/<feature>\/[\s\S]*plan\.md[\s\S]*task-01-<slug>\.md[\s\S]*task-02-<slug>\.md/i);
|
|
794
787
|
assert.match(regions.primary, /Task files are flat beside `plan\.md`/i);
|
|
795
788
|
assert.match(regions.primary, /one task at a time/i);
|
|
796
789
|
assert.match(regions.primary, /inline `## Receipt`/i);
|
|
797
|
-
assert.match(regions.primary, /
|
|
798
|
-
assert.
|
|
799
|
-
assert.match(regions.legacy, /never requires the legacy kernel/i);
|
|
790
|
+
assert.match(regions.primary, /GATE-SCOPE[\s\S]*GATE-REVIEW[\s\S]*GATE-DONE/i);
|
|
791
|
+
assert.doesNotMatch(specs, /installed legacy adapters?/i);
|
|
800
792
|
assertVocabularyIsLegacyOnly(SPECS, regions);
|
|
801
793
|
});
|
|
802
794
|
|
|
803
|
-
test('core execution agents default to process-first state and
|
|
795
|
+
test('core execution agents default to process-first state and carry no legacy adapter', () => {
|
|
804
796
|
for (const filePath of PROCESS_FIRST_AGENTS) {
|
|
805
797
|
const content = read(filePath);
|
|
806
798
|
const name = path.basename(filePath);
|
|
807
799
|
assert.match(content, /plan\.md/i, `${name} must read the process-first plan`);
|
|
808
800
|
assert.match(content, /task-NN-\*\.md|task-NN-<slug>\.md/i, `${name} must understand flat tasks`);
|
|
809
|
-
assert.
|
|
801
|
+
assert.doesNotMatch(content, /legacy|spec\.json|scope_lock/i, `${name} must not teach a legacy adapter`);
|
|
810
802
|
}
|
|
811
803
|
|
|
812
804
|
const implementer = read(PROCESS_FIRST_AGENTS[0]);
|
|
813
805
|
assert.match(implementer, /Under every dispatch mode[\s\S]{0,160}do NOT edit[\s\S]{0,80}`plan\.md`[\s\S]{0,80}`Status:`[\s\S]{0,80}`## Receipt`/i);
|
|
814
806
|
assert.match(implementer, /controller/i);
|
|
815
|
-
assert.match(implementer, /process-first work[\s\S]{0,100}Ownership
|
|
807
|
+
assert.match(implementer, /process-first work[\s\S]{0,100}Ownership\s+boundary\./i);
|
|
816
808
|
|
|
817
809
|
const auditor = read(PROCESS_FIRST_AGENTS[2]);
|
|
818
|
-
assert.match(auditor, /accepted
|
|
819
|
-
assert.
|
|
820
|
-
assert.match(auditor, /Never require those legacy artifacts from a[\s\S]*process-first packet/i);
|
|
821
|
-
assert.match(auditor, /attestation belongs only to the valid legacy/i);
|
|
810
|
+
assert.match(auditor, /accepted GATE-SCOPE\/GATE-REVIEW decisions/i);
|
|
811
|
+
assert.doesNotMatch(auditor, /CAFEKIT_SEMANTIC_REVIEW_ATTESTATION/);
|
|
822
812
|
|
|
823
813
|
const docsKeeper = read(PROCESS_FIRST_AGENTS[3]);
|
|
824
814
|
assert.match(docsKeeper, /never[\s\S]{0,80}invent or write Status, Receipt, approval, or execution proof/i);
|
|
825
815
|
|
|
826
816
|
const deployer = read(PROCESS_FIRST_AGENTS[4]);
|
|
827
817
|
assert.match(deployer, /current final inline Receipt/i);
|
|
828
|
-
assert.match(deployer, /
|
|
818
|
+
assert.match(deployer, /GATE-DONE\/release authorization/i);
|
|
829
819
|
assert.match(deployer, /does not write process-first Status\/Receipt/i);
|
|
830
820
|
});
|
|
831
821
|
|
|
832
822
|
test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Legacy sections', () => {
|
|
833
823
|
const surfaces = [
|
|
834
|
-
['develop', DEVELOP],
|
|
835
|
-
['parallel waves', PARALLEL_WAVES],
|
|
836
|
-
['sync', SYNC_SKILL],
|
|
837
|
-
['sync protocols', SYNC_PROTOCOLS],
|
|
824
|
+
['develop', DEVELOP, 0],
|
|
825
|
+
['parallel waves', PARALLEL_WAVES, 0],
|
|
826
|
+
['sync', SYNC_SKILL, 0],
|
|
827
|
+
['sync protocols', SYNC_PROTOCOLS, 0],
|
|
838
828
|
];
|
|
839
829
|
const regionsByName = new Map();
|
|
840
|
-
let legacyCorpus = '';
|
|
841
830
|
|
|
842
|
-
for (const [name, filePath] of surfaces) {
|
|
831
|
+
for (const [name, filePath, expectedLegacy] of surfaces) {
|
|
843
832
|
const regions = markdownLegacyRegions(read(filePath));
|
|
844
833
|
const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
|
|
845
|
-
assert.equal(legacyHeadings.length,
|
|
834
|
+
assert.equal(legacyHeadings.length, expectedLegacy, `${name} must have exactly ${expectedLegacy} Legacy heading(s)`);
|
|
846
835
|
assert.match(regions.primary, /inline (?:`## )?Receipts?/i, `${name} must teach inline Receipts`);
|
|
847
|
-
|
|
848
|
-
|
|
836
|
+
if (expectedLegacy === 1) {
|
|
837
|
+
assert.match(regions.legacy, /spec\.json/i, `${name} must retain the spec.json adapter`);
|
|
838
|
+
assert.match(regions.legacy, /task_registry/i, `${name} must retain task_registry compatibility`);
|
|
839
|
+
}
|
|
849
840
|
assertVocabularyIsLegacyOnly(filePath, regions);
|
|
850
841
|
regionsByName.set(name, regions);
|
|
851
|
-
legacyCorpus += `\n${regions.legacy}`;
|
|
852
842
|
}
|
|
853
843
|
|
|
854
844
|
const develop = regionsByName.get('develop').primary;
|
|
@@ -874,7 +864,7 @@ test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Leg
|
|
|
874
864
|
assert.match(protocols, /`plan\.md` and flat `task-\*\.md` files/i);
|
|
875
865
|
assert.match(protocols, /acceptance IDs/i);
|
|
876
866
|
assert.match(protocols, /current inline Receipt/i);
|
|
877
|
-
assert.match(
|
|
867
|
+
assert.match(regionsByName.get('sync').primary, /sync-finalize/i);
|
|
878
868
|
});
|
|
879
869
|
|
|
880
870
|
test('Develop process-first source contract preserves selection, recovery, final-Head, parallel, and Flash boundaries', () => {
|
|
@@ -1293,7 +1283,7 @@ test('auto authoring reaches readiness only after promotion, validation, groundi
|
|
|
1293
1283
|
assert.ok(executableBlocks.every((block) => !/cf:?develop/i.test(block)), 'Specs must never execute Develop');
|
|
1294
1284
|
assert.match(specs, /An implementation workflow owns execution/i);
|
|
1295
1285
|
assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
|
|
1296
|
-
assert.match(specs, /The user decides\s+at
|
|
1286
|
+
assert.match(specs, /The user decides\s+at GATE-DONE whether the feature is done/i);
|
|
1297
1287
|
} finally {
|
|
1298
1288
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
1299
1289
|
}
|
|
@@ -1369,41 +1359,20 @@ test('atomic readiness preserves exact bytes on review, grounding, decision, and
|
|
|
1369
1359
|
blocked.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
1370
1360
|
fs.writeFileSync(fixture.specPath, `${JSON.stringify(blocked, null, 2)}\n`);
|
|
1371
1361
|
const strictBytes = fs.readFileSync(fixture.specPath);
|
|
1372
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict
|
|
1362
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict assurance is no longer supported/);
|
|
1373
1363
|
assert.equal(fs.readFileSync(fixture.specPath).equals(strictBytes), true);
|
|
1374
1364
|
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1375
1365
|
});
|
|
1376
1366
|
|
|
1377
|
-
test('Strict
|
|
1367
|
+
test('Strict readiness is refused as no longer supported', () => {
|
|
1378
1368
|
const fixture = createOrdinaryAuthoringFixture();
|
|
1379
1369
|
try {
|
|
1380
1370
|
const spec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
|
|
1381
1371
|
spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Strict' });
|
|
1382
1372
|
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1383
1373
|
const before = fs.readFileSync(fixture.specPath);
|
|
1384
|
-
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict
|
|
1374
|
+
assert.throws(() => READINESS.finalizeReadiness({ specDir: fixture.specDir, projectRoot: fixture.root, reviewResult: ordinaryReview() }), /Strict assurance is no longer supported/);
|
|
1385
1375
|
assert.equal(fs.readFileSync(fixture.specPath).equals(before), true);
|
|
1386
|
-
|
|
1387
|
-
const digest = VALIDATOR.computeSemanticDigest21(fixture.specDir, spec);
|
|
1388
|
-
assert.equal(digest.errors.length, 0, digest.errors.join('\n'));
|
|
1389
|
-
const authorityHome = path.join(fixture.root, 'authority-home');
|
|
1390
|
-
const claim = `CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ${JSON.stringify({ feature_name: 'ordinary', spec_file: 'specs/ordinary/spec.json', semantic_digest: digest.digest, verdict: 'PASS' })}`;
|
|
1391
|
-
const observed = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs')], {
|
|
1392
|
-
cwd: fixture.root,
|
|
1393
|
-
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
1394
|
-
input: JSON.stringify({ hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'reviewer-1', agent_type: 'code_auditor', last_assistant_message: claim, cwd: fixture.root }),
|
|
1395
|
-
encoding: 'utf8',
|
|
1396
|
-
});
|
|
1397
|
-
assert.equal(observed.status, 0, observed.stderr);
|
|
1398
|
-
const reviewFile = path.join(fixture.root, 'review.json');
|
|
1399
|
-
fs.writeFileSync(reviewFile, `${JSON.stringify(ordinaryReview())}\n`);
|
|
1400
|
-
const finalized = spawnSync(process.execPath, [path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'), fixture.specDir, '--review-result', reviewFile], {
|
|
1401
|
-
cwd: fixture.root,
|
|
1402
|
-
env: { ...process.env, HOME: authorityHome, USERPROFILE: authorityHome, PROJECT_ROOT: fixture.root },
|
|
1403
|
-
encoding: 'utf8',
|
|
1404
|
-
});
|
|
1405
|
-
assert.equal(finalized.status, 0, `${finalized.stdout}\n${finalized.stderr}`);
|
|
1406
|
-
assert.equal(JSON.parse(fs.readFileSync(fixture.specPath, 'utf8')).ready_for_implementation, true);
|
|
1407
1376
|
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
|
|
1408
1377
|
});
|
|
1409
1378
|
|
|
@@ -1423,7 +1392,7 @@ test('done is a final-state request bound to current semantic model and digest',
|
|
|
1423
1392
|
const dependencies = () => ({
|
|
1424
1393
|
validator: VALIDATOR,
|
|
1425
1394
|
grounder: GROUNDER,
|
|
1426
|
-
semanticAuthority:
|
|
1395
|
+
semanticAuthority: {},
|
|
1427
1396
|
});
|
|
1428
1397
|
const current = FINAL_STATE.validateCanonicalFinalState({
|
|
1429
1398
|
policy: POLICY, projectRoot: fixture.root, candidate, dependencies,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// docs-sync printed "Docs sync needed" at every Claude SessionStart, and nothing ever
|
|
4
|
+
// cleared it. It now runs only when the docs skill asks for it. The skill text is shared
|
|
5
|
+
// with Codex, whose copy crashes silently on an empty stdin, so the command feeds `{}`.
|
|
6
|
+
// Each case names the failure it prevents.
|
|
7
|
+
|
|
8
|
+
const assert = require('node:assert/strict');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { spawnSync, execSync } = require('node:child_process');
|
|
13
|
+
const { test } = require('node:test');
|
|
14
|
+
|
|
15
|
+
const PACKAGE_ROOT = path.join(__dirname, '..', '..');
|
|
16
|
+
const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
|
|
17
|
+
const DOCS_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/docs');
|
|
18
|
+
const ON_DEMAND = "echo '{}' | node .claude/hooks/docs-sync.cjs";
|
|
19
|
+
|
|
20
|
+
function inTempProject(run) {
|
|
21
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-docs-sync-'));
|
|
22
|
+
try {
|
|
23
|
+
const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
24
|
+
assert.equal(init.status, 0, init.stderr);
|
|
25
|
+
return run(root);
|
|
26
|
+
} finally {
|
|
27
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function install(root, platforms) {
|
|
32
|
+
const result = spawnSync(process.execPath, [INSTALLER, '--platform', platforms.join(','), '--yes'],
|
|
33
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
34
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commands(settings, event) {
|
|
38
|
+
return (settings.hooks[event] || []).flatMap((group) => (group.hooks || []).map((hook) => hook.command));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A git project with code, docs and a `.sync_hash` that no longer matches the source. */
|
|
42
|
+
function makeStale(root) {
|
|
43
|
+
fs.writeFileSync(path.join(root, 'package.json'), '{"name":"fixture"}\n');
|
|
44
|
+
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
|
|
45
|
+
fs.writeFileSync(path.join(root, 'docs', 'README.md'), '# Docs\n');
|
|
46
|
+
fs.writeFileSync(path.join(root, 'docs', '.sync_hash'), '0000000000000000000000000000000000000000\n');
|
|
47
|
+
execSync('git -c user.email=t@t -c user.name=t add -A && git -c user.email=t@t -c user.name=t commit -qm fixture', { cwd: root });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('fresh claude install runs no docs-sync at SessionStart', () => {
|
|
51
|
+
inTempProject((root) => {
|
|
52
|
+
install(root, ['claude']);
|
|
53
|
+
const settings = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'settings.json'), 'utf8'));
|
|
54
|
+
assert.equal(commands(settings, 'SessionStart').some((c) => c.includes('docs-sync.cjs')), false);
|
|
55
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'hooks', 'docs-sync.cjs')), true, 'the hook stays installed for on-demand use');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('upgrade drops the SessionStart docs-sync entry', () => {
|
|
60
|
+
inTempProject((root) => {
|
|
61
|
+
install(root, ['claude']);
|
|
62
|
+
const settingsPath = path.join(root, '.claude', 'settings.json');
|
|
63
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
64
|
+
const kept = commands(settings, 'SessionStart');
|
|
65
|
+
settings.hooks.SessionStart[0].hooks.push({ type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/docs-sync.cjs"' });
|
|
66
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
67
|
+
install(root, ['claude']);
|
|
68
|
+
const after = commands(JSON.parse(fs.readFileSync(settingsPath, 'utf8')), 'SessionStart');
|
|
69
|
+
assert.equal(after.some((c) => c.includes('docs-sync.cjs')), false);
|
|
70
|
+
for (const command of kept) assert.ok(after.includes(command), `kept: ${command}`);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the docs skill runs docs-sync on demand', () => {
|
|
75
|
+
for (const rel of ['SKILL.md', 'references/update-workflow.md']) {
|
|
76
|
+
assert.ok(fs.readFileSync(path.join(DOCS_SKILL, rel), 'utf8').includes(ON_DEMAND), `${rel} names the on-demand command`);
|
|
77
|
+
}
|
|
78
|
+
const files = fs.readdirSync(DOCS_SKILL, { recursive: true }).filter((f) => f.endsWith('.md'));
|
|
79
|
+
for (const rel of files) {
|
|
80
|
+
assert.doesNotMatch(fs.readFileSync(path.join(DOCS_SKILL, rel), 'utf8'), /SessionStart docs-sync/, rel);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
for (const [runtime, folder] of [['claude', '.claude'], ['codex', '.codex']]) {
|
|
85
|
+
test(`the ${runtime === 'claude' ? 'Claude' : 'Codex'} copy prints its signal on demand`, () => {
|
|
86
|
+
inTempProject((root) => {
|
|
87
|
+
install(root, [runtime]);
|
|
88
|
+
makeStale(root);
|
|
89
|
+
const out = execSync(`echo '{}' | node ${folder}/hooks/docs-sync.cjs`, { cwd: root, encoding: 'utf8' });
|
|
90
|
+
assert.match(out, /Docs sync needed/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -134,36 +134,6 @@ test('a broad glob is denied with its full reason in JSON', () => {
|
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
test('a nested task write is denied under the grok write tool', () => {
|
|
138
|
-
withInstall((root) => {
|
|
139
|
-
const target = path.join(root, 'specs', 'demo', 'tasks', 'task-R0-01-x.md');
|
|
140
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
141
|
-
// grok creates files with `write`; `search_replace` edits an existing one. The guard
|
|
142
|
-
// fires on Write only, so the alias has to keep those two apart.
|
|
143
|
-
const r = runHook(root, 'task-scaffold-guard.cjs', {
|
|
144
|
-
hookEventName: 'pre_tool_use',
|
|
145
|
-
cwd: root,
|
|
146
|
-
toolName: 'write',
|
|
147
|
-
toolInput: { path: target, content: '# x' },
|
|
148
|
-
});
|
|
149
|
-
const { decision, reason } = decisionOf(r.stdout);
|
|
150
|
-
assert.equal(decision, 'deny');
|
|
151
|
-
assert.match(reason, /TASK SCAFFOLD REQUIRED/);
|
|
152
|
-
assert.match(reason, /spec-scaffold\.cjs/, 'the command the model must run has to reach it');
|
|
153
|
-
assert.equal(r.code, 2);
|
|
154
|
-
|
|
155
|
-
// The escape route the message names must stay open: an edit is not a create.
|
|
156
|
-
const edit = runHook(root, 'task-scaffold-guard.cjs', {
|
|
157
|
-
hookEventName: 'pre_tool_use',
|
|
158
|
-
cwd: root,
|
|
159
|
-
toolName: 'search_replace',
|
|
160
|
-
toolInput: { path: target, old_string: 'a', new_string: 'b' },
|
|
161
|
-
});
|
|
162
|
-
assert.equal(decisionOf(edit.stdout).decision, null, 'search_replace must not be treated as a create');
|
|
163
|
-
assert.equal(edit.code, 0);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
137
|
/** A packet the Stop gate will block: one done task with no receipt. */
|
|
168
138
|
function unprovenPacket(root) {
|
|
169
139
|
const feature = path.join(root, 'specs', 'demo');
|
|
@@ -206,32 +176,6 @@ test('the Stop gate still blocks an unproven done task', () => {
|
|
|
206
176
|
}, { git: true });
|
|
207
177
|
});
|
|
208
178
|
|
|
209
|
-
test('the approval hook takes its approve path on a grok prompt', () => {
|
|
210
|
-
withInstall((root) => {
|
|
211
|
-
// Registered without an argv mode, this hook picks its mode from the event name, and
|
|
212
|
-
// approve() returns immediately unless that name is exactly 'UserPromptSubmit'. An
|
|
213
|
-
// approval phrase with no pending request is therefore the oracle: only the approve
|
|
214
|
-
// path can print a rejection line, so the line proves the event name was translated.
|
|
215
|
-
// Under an unreadable envelope the mode fell back to the stop path on every prompt.
|
|
216
|
-
const nonce = 'a'.repeat(24);
|
|
217
|
-
const r = runHook(root, 'completion-authority.cjs', {
|
|
218
|
-
hookEventName: 'user_prompt_submit',
|
|
219
|
-
sessionId: 'grok-1',
|
|
220
|
-
cwd: root,
|
|
221
|
-
prompt: `APPROVE CAFEKIT COMPLETION ${nonce}`,
|
|
222
|
-
});
|
|
223
|
-
assert.match(r.stdout, /CafeKit completion approval rejected/,
|
|
224
|
-
'the approve path must run and refuse an approval that matches no pending request');
|
|
225
|
-
assert.equal(decisionOf(r.stdout).decision, null, 'and it must not block the prompt');
|
|
226
|
-
|
|
227
|
-
// An ordinary prompt stays silent on both counts.
|
|
228
|
-
const plain = runHook(root, 'completion-authority.cjs', {
|
|
229
|
-
hookEventName: 'user_prompt_submit', sessionId: 'grok-1', cwd: root, prompt: 'please continue',
|
|
230
|
-
});
|
|
231
|
-
assert.equal(plain.stdout.trim(), '', 'an ordinary prompt produces nothing');
|
|
232
|
-
}, { git: true });
|
|
233
|
-
});
|
|
234
|
-
|
|
235
179
|
test('the secret guardrail sees a grok prompt', () => {
|
|
236
180
|
withInstall((root) => {
|
|
237
181
|
// grok's spelling for the prompt field is unverified, so the reader accepts the Claude
|
|
@@ -49,8 +49,8 @@ test('read_file path becomes file_path', () => {
|
|
|
49
49
|
toolInput: { path: '/p/.env' },
|
|
50
50
|
});
|
|
51
51
|
assert.equal(out.tool_name, 'Read');
|
|
52
|
-
// privacy-block
|
|
53
|
-
//
|
|
52
|
+
// privacy-block and inspect-block both read file_path first, so the normalized key
|
|
53
|
+
// must be file_path.
|
|
54
54
|
assert.equal(out.tool_input.file_path, '/p/.env');
|
|
55
55
|
assert.equal(out.tool_input.path, '/p/.env', 'the original key stays for hooks that read it');
|
|
56
56
|
});
|
|
@@ -83,7 +83,6 @@ test('Stop and subagent fields map', () => {
|
|
|
83
83
|
subagentId: 'agent-7',
|
|
84
84
|
lastAssistantMessage: 'REVIEW: PASS',
|
|
85
85
|
});
|
|
86
|
-
// semantic-review-authority refuses to record an attestation without agent_id.
|
|
87
86
|
assert.equal(sub.hook_event_name, 'SubagentStop');
|
|
88
87
|
assert.equal(sub.agent_type, 'code-auditor');
|
|
89
88
|
assert.equal(sub.agent_id, 'agent-7');
|
|
@@ -102,9 +101,8 @@ test('every name reaching a Bash matcher becomes Bash', () => {
|
|
|
102
101
|
});
|
|
103
102
|
|
|
104
103
|
test('write and search_replace map to different Claude tools', () => {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// reject the Edit-fill step the guard's own message tells the model to run.
|
|
104
|
+
// Grok creates files with `write` and edits them with `search_replace`; they must stay
|
|
105
|
+
// distinct Claude tools so a hook matcher on Write or on Edit sees the event it names.
|
|
108
106
|
assert.equal(normalizeToolName('write'), 'Write');
|
|
109
107
|
assert.equal(normalizeToolName('delete_file'), 'Write');
|
|
110
108
|
assert.equal(normalizeToolName('search_replace'), 'Edit');
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The Stop gates and the session hooks must not depend on the completion-authority hook files,
|
|
4
|
+
// so those files can be deleted without blocking every Stop or crashing SessionStart. Each case
|
|
5
|
+
// runs against a real install (bin/install.js), with and without the five legacy files.
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { spawnSync } = require('node:child_process');
|
|
12
|
+
const { test } = require('node:test');
|
|
13
|
+
|
|
14
|
+
const INSTALLER = path.join(__dirname, '..', 'install.js');
|
|
15
|
+
const LEGACY_FILES = [
|
|
16
|
+
'completion-authority.cjs',
|
|
17
|
+
'completion-authority-check.cjs',
|
|
18
|
+
'completion-authority-state.cjs',
|
|
19
|
+
'semantic-review-authority.cjs',
|
|
20
|
+
'task-scaffold-guard.cjs',
|
|
21
|
+
];
|
|
22
|
+
const MIGRATION = /Legacy spec\.json closeout is no longer supported/;
|
|
23
|
+
|
|
24
|
+
function inTempProject(run) {
|
|
25
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-decoupled-')));
|
|
26
|
+
try {
|
|
27
|
+
for (const args of [['init', '-q'], ['config', 'user.email', 'test@example.invalid'], ['config', 'user.name', 'Test'], ['commit', '--allow-empty', '-qm', 'base']]) {
|
|
28
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
29
|
+
assert.equal(result.status, 0, result.stderr);
|
|
30
|
+
}
|
|
31
|
+
const installed = spawnSync(process.execPath, [INSTALLER, '--platform', 'claude,codex', '--yes'],
|
|
32
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
33
|
+
assert.equal(installed.status, 0, `${installed.stdout}\n${installed.stderr}`);
|
|
34
|
+
return run(root);
|
|
35
|
+
} finally {
|
|
36
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeLegacyFiles(root) {
|
|
41
|
+
for (const runtime of ['.claude', '.codex']) {
|
|
42
|
+
for (const file of LEGACY_FILES) {
|
|
43
|
+
const target = path.join(root, runtime, 'hooks', file);
|
|
44
|
+
fs.rmSync(target, { force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runHook(root, kind, script, event, extra = {}) {
|
|
50
|
+
const hook = path.join(root, kind === 'claude' ? '.claude' : '.codex', 'hooks', script);
|
|
51
|
+
const result = spawnSync(process.execPath, [hook], {
|
|
52
|
+
cwd: root,
|
|
53
|
+
input: JSON.stringify({ hook_event_name: event, session_id: 's', stop_hook_active: false, cwd: root, ...extra }),
|
|
54
|
+
env: { ...process.env, PATH: '/usr/bin:/bin', ...(kind === 'claude' ? { CLAUDE_PROJECT_DIR: root } : {}) },
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
});
|
|
57
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
58
|
+
return result.stdout.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stopGate(root, kind) {
|
|
62
|
+
return runHook(root, kind, 'spec-gate.cjs', 'Stop');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isBlocked(stdout) {
|
|
66
|
+
return stdout !== '' && JSON.parse(stdout.split('\n').pop()).decision === 'block';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function blockReason(stdout) {
|
|
70
|
+
assert.equal(isBlocked(stdout), true, `expected a block, got: ${stdout}`);
|
|
71
|
+
return JSON.parse(stdout.split('\n').pop()).reason;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function provenance(root, taskFile) {
|
|
75
|
+
const result = spawnSync(process.execPath, [
|
|
76
|
+
path.join(root, '.claude', 'scripts', 'provenance.cjs'),
|
|
77
|
+
'--project-root', root, '--specs-root', 'specs', '--spec-file', taskFile,
|
|
78
|
+
'--feature-name', 'demo', '--session', 's', '--json',
|
|
79
|
+
], { cwd: root, encoding: 'utf8' });
|
|
80
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
81
|
+
const parsed = JSON.parse(result.stdout);
|
|
82
|
+
return { base: parsed.Base, head: parsed.Head };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeProcessFirst(root, { receipt }) {
|
|
86
|
+
const featureDir = path.join(root, 'specs', 'demo');
|
|
87
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
88
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
89
|
+
const taskFile = path.join(featureDir, 'task-01-demo.md');
|
|
90
|
+
const body = ['# Task 01: demo', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', ''];
|
|
91
|
+
fs.writeFileSync(taskFile, body.join('\n'));
|
|
92
|
+
if (!receipt) return;
|
|
93
|
+
const { base, head } = provenance(root, taskFile);
|
|
94
|
+
fs.writeFileSync(taskFile, [...body, '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
95
|
+
`Base: ${base}`, `Head: ${head}`, '```text', '$ node --test', 'pass: 1', '```', ''].join('\n'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A 2.1 packet keeps its machine state in spec.json and its tasks in a nested tasks/ directory.
|
|
99
|
+
function writeLegacy(root, { status, doneTask = false }) {
|
|
100
|
+
const featureDir = path.join(root, 'specs', 'legacy');
|
|
101
|
+
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
102
|
+
const task = 'tasks/task-R1-01-legacy.md';
|
|
103
|
+
const spec = { schema_version: '2.1', feature_name: 'legacy', status, current_phase: 'implementation', task_registry: {} };
|
|
104
|
+
if (doneTask) spec.task_registry[task] = { status: 'done', completed_at: '2026-08-13T00:00:00.000Z', dependencies: [] };
|
|
105
|
+
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
|
|
106
|
+
if (doneTask) {
|
|
107
|
+
fs.writeFileSync(path.join(featureDir, task), ['# Task R1-01: legacy', '**Status:** done', '## Outcome', 'Legacy work.',
|
|
108
|
+
'## Verification Plan', '- **Command:** `node --test`', '- **Expected:** passes', ''].join('\n'));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function checkProcessFirst(kind) {
|
|
113
|
+
inTempProject((root) => {
|
|
114
|
+
writeProcessFirst(root, { receipt: true });
|
|
115
|
+
assert.equal(stopGate(root, kind), '', 'valid receipt passes with the legacy files');
|
|
116
|
+
writeProcessFirst(root, { receipt: false });
|
|
117
|
+
assert.match(blockReason(stopGate(root, kind)), /missing_receipt/, 'no receipt blocks with the legacy files');
|
|
118
|
+
removeLegacyFiles(root);
|
|
119
|
+
assert.match(blockReason(stopGate(root, kind)), /missing_receipt/, 'no receipt blocks without the legacy files');
|
|
120
|
+
writeProcessFirst(root, { receipt: true });
|
|
121
|
+
assert.equal(stopGate(root, kind), '', 'valid receipt passes without the legacy files');
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function checkLegacyCloseout(kind) {
|
|
126
|
+
for (const status of ['done', 'completed']) {
|
|
127
|
+
inTempProject((root) => {
|
|
128
|
+
writeLegacy(root, { status });
|
|
129
|
+
removeLegacyFiles(root);
|
|
130
|
+
assert.match(blockReason(stopGate(root, kind)), MIGRATION, `${kind} ${status}`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkLegacyMidExecution(kind) {
|
|
136
|
+
inTempProject((root) => {
|
|
137
|
+
writeLegacy(root, { status: 'in_progress', doneTask: true });
|
|
138
|
+
removeLegacyFiles(root);
|
|
139
|
+
const reason = blockReason(stopGate(root, kind));
|
|
140
|
+
assert.doesNotMatch(reason, MIGRATION);
|
|
141
|
+
assert.match(reason, /task-R1-01-legacy\.md/);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkSession(kind) {
|
|
146
|
+
inTempProject((root) => {
|
|
147
|
+
removeLegacyFiles(root);
|
|
148
|
+
runHook(root, kind, 'session.cjs', 'SessionStart', { source: 'startup' });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test('claude gate passes and blocks process-first with and without the legacy files', () => checkProcessFirst('claude'));
|
|
153
|
+
test('codex gate passes and blocks process-first with and without the legacy files', () => checkProcessFirst('codex'));
|
|
154
|
+
test('claude gate blocks legacy 2.1 closeout with the migration message', () => checkLegacyCloseout('claude'));
|
|
155
|
+
test('codex gate blocks legacy 2.1 closeout with the migration message', () => checkLegacyCloseout('codex'));
|
|
156
|
+
test('claude gate still receipt-checks a legacy 2.1 packet mid-execution', () => checkLegacyMidExecution('claude'));
|
|
157
|
+
test('codex gate still receipt-checks a legacy 2.1 packet mid-execution', () => checkLegacyMidExecution('codex'));
|
|
158
|
+
test('claude session starts without the legacy files', () => checkSession('claude'));
|
|
159
|
+
test('codex session starts without the legacy files', () => checkSession('codex'));
|