@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.8
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 +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +382 -0
- package/bin/__tests__/codex-native.test.js +579 -80
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +494 -26
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +2 -5
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +82 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +51 -13
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +196 -1
- package/src/claude/scripts/spec-resolver.cjs +428 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +152 -204
- package/src/claude/skills/specs/references/review.md +124 -243
- package/src/claude/skills/specs/references/templates.md +202 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +54 -16
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- package/src/opencode/plugins/usage.ts +0 -178
|
@@ -27,6 +27,7 @@ const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
|
|
|
27
27
|
const SYNC_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/SKILL.md');
|
|
28
28
|
const CODE_REVIEW_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/code-review/SKILL.md');
|
|
29
29
|
const PARALLEL_WAVES = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/parallel-waves.md');
|
|
30
|
+
const SYNC_PROTOCOLS = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/references/sync-protocols.md');
|
|
30
31
|
const PROVENANCE = path.join(PACKAGE_ROOT, '../../docs/provenance.md');
|
|
31
32
|
const RUNTIME_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-policy-runtime-'));
|
|
32
33
|
const RUNTIME_SPEC = path.join(RUNTIME_ROOT, 'specs', 'demo', 'spec.json');
|
|
@@ -75,28 +76,62 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
|
|
|
75
76
|
return path.join(destinationRoot, entry);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
function markdownLegacyRegions(content) {
|
|
80
|
+
const headings = [];
|
|
81
|
+
const primaryLines = [];
|
|
82
|
+
const legacyLines = [];
|
|
83
|
+
const hierarchy = [];
|
|
84
|
+
|
|
85
|
+
for (const [index, line] of content.split('\n').entries()) {
|
|
86
|
+
const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
87
|
+
if (heading) {
|
|
88
|
+
const level = heading[1].length;
|
|
89
|
+
while (hierarchy.at(-1)?.level >= level) hierarchy.pop();
|
|
90
|
+
const entry = { level, text: heading[2], line: index + 1 };
|
|
91
|
+
hierarchy.push(entry);
|
|
92
|
+
headings.push(entry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const target = hierarchy.some(({ text }) => /legacy/i.test(text))
|
|
96
|
+
? legacyLines
|
|
97
|
+
: primaryLines;
|
|
98
|
+
target.push({ line: index + 1, text: line });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
headings,
|
|
103
|
+
primary: primaryLines.map(({ text }) => text).join('\n'),
|
|
104
|
+
legacy: legacyLines.map(({ text }) => text).join('\n'),
|
|
105
|
+
primaryLines,
|
|
106
|
+
};
|
|
85
107
|
}
|
|
86
108
|
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
function assertVocabularyIsLegacyOnly(filePath, regions) {
|
|
110
|
+
const vocabulary = [
|
|
111
|
+
['v2.1', /\bv2\.1\b/i],
|
|
112
|
+
['spec.json', /\bspec\.json\b/i],
|
|
113
|
+
['semantic_model', /\bsemantic[-_]model\b/i],
|
|
114
|
+
['machine authority', /\bmachine authority\b/i],
|
|
115
|
+
['task_registry', /\btask_registry\b/i],
|
|
116
|
+
['workflow_policy', /\bworkflow_policy\b/i],
|
|
117
|
+
['planning_depth', /\bplanning_depth\b/i],
|
|
118
|
+
['assurance_level', /\bassurance_level\b/i],
|
|
119
|
+
['execution_tier', /\bexecution_tier\b/i],
|
|
120
|
+
['classified_minimum', /\bclassified_minimum\b/i],
|
|
121
|
+
['coordination.boundaries', /\bcoordination\.boundaries\b/i],
|
|
122
|
+
['lane', /\blane\b/i],
|
|
123
|
+
];
|
|
124
|
+
const violations = [];
|
|
125
|
+
for (const { line, text } of regions.primaryLines) {
|
|
126
|
+
for (const [term, pattern] of vocabulary) {
|
|
127
|
+
if (pattern.test(text)) violations.push(`${line}:${term}`);
|
|
128
|
+
}
|
|
98
129
|
}
|
|
99
|
-
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
violations,
|
|
132
|
+
[],
|
|
133
|
+
`${path.relative(PACKAGE_ROOT, filePath)} has v2.1 vocabulary outside a Legacy heading`,
|
|
134
|
+
);
|
|
100
135
|
}
|
|
101
136
|
|
|
102
137
|
function runNode(root, script, args) {
|
|
@@ -305,11 +340,16 @@ test('CLI rejects flash and parallel before any mutation', () => {
|
|
|
305
340
|
}
|
|
306
341
|
});
|
|
307
342
|
|
|
308
|
-
test('develop
|
|
343
|
+
test('develop flags retain their process-v3 contracts', () => {
|
|
309
344
|
const develop = read(DEVELOP);
|
|
310
345
|
assert.match(develop, /node \.claude\/scripts\/workflow-policy\.cjs --flash --parallel --json/);
|
|
311
|
-
assert.match(develop, /
|
|
312
|
-
assert.match(develop,
|
|
346
|
+
assert.match(develop, /exits `2` before a task edit, receipt, worktree, subagent, or commit/i);
|
|
347
|
+
assert.match(develop, /--notes` is opt-in/i);
|
|
348
|
+
assert.match(develop, /implementation-notes-template\.html/);
|
|
349
|
+
assert.match(develop, /Never create it by default/i);
|
|
350
|
+
assert.match(develop, /--parallel \[N\][\s\S]*parallel-waves\.md[\s\S]*isolated worktrees[\s\S]*one controller writer/i);
|
|
351
|
+
assert.match(develop, /--flash[\s\S]*Status: in_progress[\s\S]*FLASH_UNVERIFIED[\s\S]*awaiting \/hapo:test <feature>/i);
|
|
352
|
+
assert.match(develop, /FLASH_UNVERIFIED[\s\S]*do not unblock dependents[\s\S]*sync-finalize path/i);
|
|
313
353
|
});
|
|
314
354
|
|
|
315
355
|
test('delegation plan consumes legacy tiers as obligations without an agent chain', () => {
|
|
@@ -674,7 +714,7 @@ test('approval schema fails closed for explicit null, array, empty, and malforme
|
|
|
674
714
|
assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
|
|
675
715
|
});
|
|
676
716
|
|
|
677
|
-
test('CLI exposes a derived lane
|
|
717
|
+
test('CLI exposes a derived lane without making it primary Develop authority', () => {
|
|
678
718
|
const cli = spawnSync(process.execPath, [
|
|
679
719
|
POLICY_PATH,
|
|
680
720
|
'--classify-lane',
|
|
@@ -687,18 +727,17 @@ test('CLI exposes a derived lane and Develop consumes independent canonical poli
|
|
|
687
727
|
assert.equal(payload.classification.lane, 'Direct');
|
|
688
728
|
assert.equal(payload.policy.requiresSpec, false);
|
|
689
729
|
const develop = read(DEVELOP);
|
|
730
|
+
const regions = markdownLegacyRegions(develop);
|
|
690
731
|
assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
assert.
|
|
699
|
-
assert.
|
|
700
|
-
assert.ok(policySection.replace(/\s+/g, ' ').includes('zero authority'));
|
|
701
|
-
assert.match(develop, /Classify lane[\s\S]*test receipt[\s\S]*docs-impact sync/i);
|
|
732
|
+
assert.doesNotMatch(regions.primary, /planning_depth|assurance_level|execution_tier|\blane\b/i);
|
|
733
|
+
assert.match(regions.legacy, /planning_depth/);
|
|
734
|
+
assert.match(regions.legacy, /assurance_level/);
|
|
735
|
+
assert.match(regions.legacy, /execution_tier/);
|
|
736
|
+
assert.match(regions.legacy, /derived lane/i);
|
|
737
|
+
assert.match(regions.primary, /specs\/<feature>\/plan\.md[\s\S]*task-NN-\*\.md/i);
|
|
738
|
+
assert.match(regions.primary, /one unblocked task at a time/i);
|
|
739
|
+
assert.match(regions.primary, /Verification Plan/);
|
|
740
|
+
assert.match(regions.primary, /inline Receipt/i);
|
|
702
741
|
assert.doesNotMatch(develop, /D2\[Step 3/);
|
|
703
742
|
assert.match(develop, /--notes.*opt-in/i);
|
|
704
743
|
assert.doesNotMatch(develop, /--no-notes/);
|
|
@@ -717,30 +756,67 @@ test('CLI exposes a derived lane and Develop consumes independent canonical poli
|
|
|
717
756
|
assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
|
|
718
757
|
});
|
|
719
758
|
|
|
720
|
-
test('
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
assert.equal(
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
759
|
+
test('Specs primary output is a flat process-first packet with isolated legacy compatibility', () => {
|
|
760
|
+
const specs = read(SPECS);
|
|
761
|
+
const regions = markdownLegacyRegions(specs);
|
|
762
|
+
const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
|
|
763
|
+
assert.equal(legacyHeadings.length, 1);
|
|
764
|
+
assert.match(regions.primary, /specs\/<feature>\/[\s\S]*plan\.md[\s\S]*task-01-<slug>\.md[\s\S]*task-02-<slug>\.md/i);
|
|
765
|
+
assert.match(regions.primary, /Task files are flat beside `plan\.md`/i);
|
|
766
|
+
assert.match(regions.primary, /one task at a time/i);
|
|
767
|
+
assert.match(regions.primary, /inline `## Receipt`/i);
|
|
768
|
+
assert.match(regions.primary, /C1 — Scope[\s\S]*C2 — Findings[\s\S]*C3 — Done/i);
|
|
769
|
+
assert.match(regions.legacy, /spec\.json/i);
|
|
770
|
+
assert.match(regions.legacy, /never requires the legacy kernel/i);
|
|
771
|
+
assertVocabularyIsLegacyOnly(SPECS, regions);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Legacy sections', () => {
|
|
775
|
+
const surfaces = [
|
|
776
|
+
['develop', DEVELOP],
|
|
777
|
+
['parallel waves', PARALLEL_WAVES],
|
|
778
|
+
['sync', SYNC_SKILL],
|
|
779
|
+
['sync protocols', SYNC_PROTOCOLS],
|
|
780
|
+
];
|
|
781
|
+
const regionsByName = new Map();
|
|
782
|
+
let legacyCorpus = '';
|
|
783
|
+
|
|
784
|
+
for (const [name, filePath] of surfaces) {
|
|
785
|
+
const regions = markdownLegacyRegions(read(filePath));
|
|
786
|
+
const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
|
|
787
|
+
assert.equal(legacyHeadings.length, 1, `${name} must have exactly one Legacy heading`);
|
|
788
|
+
assert.match(regions.primary, /inline (?:`## )?Receipts?/i, `${name} must teach inline Receipts`);
|
|
789
|
+
assert.match(regions.legacy, /spec\.json/i, `${name} must retain the spec.json adapter`);
|
|
790
|
+
assert.match(regions.legacy, /task_registry/i, `${name} must retain task_registry compatibility`);
|
|
791
|
+
assertVocabularyIsLegacyOnly(filePath, regions);
|
|
792
|
+
regionsByName.set(name, regions);
|
|
793
|
+
legacyCorpus += `\n${regions.legacy}`;
|
|
740
794
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
assert.
|
|
795
|
+
|
|
796
|
+
const develop = regionsByName.get('develop').primary;
|
|
797
|
+
assert.match(develop, /`specs\/<feature>\/plan\.md` plus flat `task-NN-\*\.md`/i);
|
|
798
|
+
assert.match(develop, /selects one unblocked task at a time/i);
|
|
799
|
+
assert.match(develop, /Outcome and Acceptance/i);
|
|
800
|
+
assert.match(develop, /Verification Plan/);
|
|
801
|
+
assert.match(develop, /final `## Receipt`/i);
|
|
802
|
+
|
|
803
|
+
const sync = regionsByName.get('sync').primary;
|
|
804
|
+
assert.match(sync, /<task-NN-slug\.md>/i);
|
|
805
|
+
assert.match(sync, /specs\/<feature>\/task-\*\.md` beside `plan\.md`/i);
|
|
806
|
+
assert.match(sync, /Acceptance[\s\S]*Verification Plan/i);
|
|
807
|
+
assert.match(sync, /current inline Receipt/i);
|
|
808
|
+
|
|
809
|
+
const waves = regionsByName.get('parallel waves').primary;
|
|
810
|
+
assert.match(waves, /plan task table[\s\S]*flat task's[\s\S]*`## Dependencies`/i);
|
|
811
|
+
assert.match(waves, /acceptance, Verification Plan/i);
|
|
812
|
+
assert.match(waves, /one file has one writer per wave/i);
|
|
813
|
+
assert.match(waves, /writes inline Receipts and Status updates one task at a\s+time/i);
|
|
814
|
+
|
|
815
|
+
const protocols = regionsByName.get('sync protocols').primary;
|
|
816
|
+
assert.match(protocols, /`plan\.md` and flat `task-\*\.md` files/i);
|
|
817
|
+
assert.match(protocols, /acceptance IDs/i);
|
|
818
|
+
assert.match(protocols, /current inline Receipt/i);
|
|
819
|
+
assert.match(legacyCorpus, /sync-finalize/i);
|
|
744
820
|
});
|
|
745
821
|
|
|
746
822
|
test('canonical verdict adapter handles completion and unfinished decisions', () => {
|
|
@@ -896,11 +972,9 @@ test('auto authoring reaches readiness only after promotion, validation, groundi
|
|
|
896
972
|
const specs = read(SPECS);
|
|
897
973
|
const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
|
|
898
974
|
assert.ok(executableBlocks.every((block) => !/hapo:?develop/i.test(block)), 'Specs must never execute Develop');
|
|
899
|
-
|
|
900
|
-
assert.
|
|
901
|
-
assert.
|
|
902
|
-
assert.ok(handoff.includes('$hapo-develop <feature>'));
|
|
903
|
-
assert.ok(handoff.includes('must stop in Specs'));
|
|
975
|
+
assert.match(specs, /An implementation workflow owns execution/i);
|
|
976
|
+
assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
|
|
977
|
+
assert.match(specs, /The user decides\s+at C3 whether the feature is done/i);
|
|
904
978
|
} finally {
|
|
905
979
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
906
980
|
}
|
|
@@ -1519,18 +1593,29 @@ test('flash selective promotion - only specific task is promoted, not blanket',
|
|
|
1519
1593
|
});
|
|
1520
1594
|
|
|
1521
1595
|
test('parallel waves require immutable provenance receipts and safe recovery', () => {
|
|
1522
|
-
const waves = read(PARALLEL_WAVES);
|
|
1596
|
+
const waves = markdownLegacyRegions(read(PARALLEL_WAVES)).primary;
|
|
1597
|
+
assert.match(waves, /only for explicit parallel execution/i);
|
|
1598
|
+
assert.match(waves, /worktree isolation[\s\S]*fall back to the sequential loop/i);
|
|
1599
|
+
assert.match(waves, /Clamp `--parallel N` to 1\.\.5; default to 3/i);
|
|
1600
|
+
assert.match(waves, /clean destination[\s\S]*base_sha[\s\S]*base compatibility/i);
|
|
1601
|
+
assert.match(waves, /parallel consent[\s\S]*worker commits[\s\S]*controller cherry-picks/i);
|
|
1523
1602
|
for (const field of ['base_sha', 'head_sha', 'branch', 'worktree_path', 'commit_range']) {
|
|
1524
1603
|
assert.match(waves, new RegExp(`\\b${field}\\b`));
|
|
1525
1604
|
}
|
|
1526
1605
|
assert.match(waves, /git diff(?: --stat)? [^\\n]*(?:BASE_SHA|base_sha)[^\\n]*(?:HEAD_SHA|head_sha)/i);
|
|
1527
1606
|
assert.match(waves, /new commit|new commits|Every fix is a new commit/i);
|
|
1528
|
-
assert.match(waves, /
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
assert.match(waves, /
|
|
1533
|
-
assert.match(waves, /
|
|
1607
|
+
assert.match(waves, /plan task table[\s\S]*flat task's[\s\S]*Dependencies/i);
|
|
1608
|
+
for (const sharedSurface of ['registry', 'lockfile', 'manifest', 'generated output', 'migration', 'export barrel']) {
|
|
1609
|
+
assert.match(waves, new RegExp(sharedSurface, 'i'));
|
|
1610
|
+
}
|
|
1611
|
+
assert.match(waves, /One file has one writer per wave/i);
|
|
1612
|
+
assert.match(waves, /controller owns[\s\S]*state and evidence synchronization/i);
|
|
1613
|
+
assert.match(waves, /authorization covers worker commits/i);
|
|
1614
|
+
assert.match(waves, /blocked or failed worker keeps[\s\S]*worktree and branch/i);
|
|
1615
|
+
assert.match(waves, /Never[\s\S]*force-delete[\s\S]*merge success or explicit discard authorization/i);
|
|
1616
|
+
assert.match(waves, /git cherry-pick <worker-commit>/i);
|
|
1617
|
+
assert.match(waves, /On conflict, abort the cherry-pick[\s\S]*retain its[\s\S]*worktree\/branch/i);
|
|
1618
|
+
assert.match(waves, /cleanup_authorization: merged-release/i);
|
|
1534
1619
|
assert.match(waves, /affected integration|final.*integration/i);
|
|
1535
1620
|
for (const category of ['baseline', 'environment', 'spec', 'code']) {
|
|
1536
1621
|
assert.match(waves, new RegExp(`\\b${category}\\b`));
|
|
@@ -2635,17 +2720,11 @@ test('P0 regression: safeTaskFile fail-closed on realpath error and single autho
|
|
|
2635
2720
|
// Single authority: Claude hook should delegate to POLICY, not duplicate parser
|
|
2636
2721
|
assert.match(specGateSrc, /POLICY\.validateCanonicalReceipt/);
|
|
2637
2722
|
assert.doesNotMatch(specGateSrc, /const EXPLICIT_FAILURE/);
|
|
2638
|
-
// Codex
|
|
2723
|
+
// Codex should delegate to shared workflow-policy
|
|
2639
2724
|
const codexSrc = fs2.readFileSync(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'), 'utf8');
|
|
2640
2725
|
assert.match(codexSrc, /getSharedValidate/);
|
|
2641
2726
|
assert.match(codexSrc, /workflow-policy\.cjs/);
|
|
2642
2727
|
assert.doesNotMatch(codexSrc, /const PLACEHOLDER_TOKENS/);
|
|
2643
|
-
const opencodeSrc = fs2.readFileSync(path2.join(PACKAGE_ROOT, 'src/opencode/plugins/spec-gate.ts'), 'utf8');
|
|
2644
|
-
assert.match(opencodeSrc, /getSharedValidate/);
|
|
2645
|
-
assert.match(opencodeSrc, /workflow-policy\.cjs/);
|
|
2646
|
-
// The parser should only exist in workflow-policy, not in adapters (check that adapter does not define full validate logic)
|
|
2647
|
-
const opencodeValidateCount = (opencodeSrc.match(/function validateCanonicalReceipt/g) || []).length;
|
|
2648
|
-
assert.equal(opencodeValidateCount, 1, 'OpenCode should have only delegating validate, not duplicate parser');
|
|
2649
2728
|
// safeTaskFile fail-closed: simulate realpathSync throwing
|
|
2650
2729
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-realpath-'));
|
|
2651
2730
|
try {
|
|
@@ -8,10 +8,6 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const { test } = require('node:test');
|
|
9
9
|
|
|
10
10
|
const backup = require('../lib/backup');
|
|
11
|
-
const {
|
|
12
|
-
copyOpenCodeAgentsMdFile,
|
|
13
|
-
normalizeOpenCodeBody
|
|
14
|
-
} = require('../lib/opencode-install');
|
|
15
11
|
const {
|
|
16
12
|
commandFailureReason,
|
|
17
13
|
resolvePackageCommand
|
|
@@ -34,8 +30,6 @@ const SHARED_TEMPLATE = fs.readFileSync(path.join(__dirname, '../../src/common/A
|
|
|
34
30
|
const INSTALLER = path.join(__dirname, '../install.js');
|
|
35
31
|
const CODEX_START = '<!-- CAFEKIT CODEX START -->';
|
|
36
32
|
const CODEX_END = '<!-- CAFEKIT CODEX END -->';
|
|
37
|
-
const OPENCODE_START = '<!-- CAFEKIT OPENCODE START -->';
|
|
38
|
-
const OPENCODE_END = '<!-- CAFEKIT OPENCODE END -->';
|
|
39
33
|
|
|
40
34
|
function managedBody(content, start, end) {
|
|
41
35
|
const startIndex = content.indexOf(start);
|
|
@@ -47,7 +41,7 @@ function managedBody(content, start, end) {
|
|
|
47
41
|
function runCombinedInstaller(root, extraArgs = []) {
|
|
48
42
|
return spawnSync(
|
|
49
43
|
process.execPath,
|
|
50
|
-
[INSTALLER, '--platform', 'claude,codex
|
|
44
|
+
[INSTALLER, '--platform', 'claude,codex', '--yes', ...extraArgs],
|
|
51
45
|
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } }
|
|
52
46
|
);
|
|
53
47
|
}
|
|
@@ -118,7 +112,7 @@ test('legacy managed root gitignore migrates A13 plan patterns and preserves use
|
|
|
118
112
|
'.cafekit-backup/',
|
|
119
113
|
'.cafekit.lock',
|
|
120
114
|
'.claude/',
|
|
121
|
-
'.
|
|
115
|
+
'.legacy-runtime/',
|
|
122
116
|
'.codex/',
|
|
123
117
|
'.agents/',
|
|
124
118
|
'',
|
|
@@ -150,7 +144,7 @@ test('legacy managed root gitignore migrates A13 plan patterns and preserves use
|
|
|
150
144
|
'.cafekit-backup/',
|
|
151
145
|
'.cafekit.lock',
|
|
152
146
|
'.claude/',
|
|
153
|
-
'.
|
|
147
|
+
'.legacy-runtime/',
|
|
154
148
|
'.codex/',
|
|
155
149
|
'.agents/'
|
|
156
150
|
]);
|
|
@@ -188,8 +182,7 @@ test('legacy managed root gitignore migrates A13 plan patterns and preserves use
|
|
|
188
182
|
test('malformed managed marker topology fails transactionally and preserves exact bytes', () => {
|
|
189
183
|
const markers = {
|
|
190
184
|
claude: ['<!-- CAFEKIT CORE START -->', '<!-- CAFEKIT CORE END -->'],
|
|
191
|
-
codex: ['<!-- CAFEKIT CODEX START -->', '<!-- CAFEKIT CODEX END -->']
|
|
192
|
-
opencode: ['<!-- CAFEKIT OPENCODE START -->', '<!-- CAFEKIT OPENCODE END -->']
|
|
185
|
+
codex: ['<!-- CAFEKIT CODEX START -->', '<!-- CAFEKIT CODEX END -->']
|
|
193
186
|
};
|
|
194
187
|
const topologies = (start, end) => [
|
|
195
188
|
`user\n${end}\n`,
|
|
@@ -227,37 +220,31 @@ test('combined install keeps CORE neutral and records native shared-root trade-o
|
|
|
227
220
|
const claude = fs.readFileSync('CLAUDE.md', 'utf8');
|
|
228
221
|
const core = managedBody(agents, CORE_START, CORE_END);
|
|
229
222
|
const codex = managedBody(agents, CODEX_START, CODEX_END);
|
|
230
|
-
const opencode = managedBody(agents, OPENCODE_START, OPENCODE_END);
|
|
231
223
|
const claudeBlock = managedBody(claude, START, END);
|
|
232
224
|
|
|
233
225
|
for (const [content, marker] of [
|
|
234
226
|
[agents, CORE_START],
|
|
235
227
|
[claude, START],
|
|
236
|
-
[agents, CODEX_START]
|
|
237
|
-
[agents, OPENCODE_START]
|
|
228
|
+
[agents, CODEX_START]
|
|
238
229
|
]) assert.equal((content.match(new RegExp(marker, 'g')) || []).length, 1);
|
|
239
230
|
|
|
240
|
-
assert.doesNotMatch(core, /Claude|Codex
|
|
241
|
-
assert.doesNotMatch(claudeBlock, /Codex
|
|
231
|
+
assert.doesNotMatch(core, /Claude|Codex|\.claude|\.codex|\/hapo:|\$hapo-/i);
|
|
232
|
+
assert.doesNotMatch(claudeBlock, /Codex|\.codex|\$hapo-/i);
|
|
242
233
|
assert.match(codex, /native project instruction surface is root `AGENTS\.md`/);
|
|
243
|
-
assert.match(opencode, /native project instruction surface is root `AGENTS\.md`/);
|
|
244
234
|
assert.match(agents, /shared-root trade-off is intentional/);
|
|
245
235
|
// H5 remediation: ownership/ignore contract must be explicit — not just marker presence
|
|
246
236
|
assert.match(core, /runtime-neutral/i);
|
|
247
237
|
assert.match(core, /fail-safe/i);
|
|
248
238
|
assert.match(core, /Ignore managed blocks not owned by your runtime/i);
|
|
249
239
|
assert.match(codex, /owned by Codex/i);
|
|
250
|
-
assert.match(codex, /ignore this entire Codex block/i);
|
|
240
|
+
assert.match(codex, /If you are Claude Code or any other runtime, ignore this entire Codex block/i);
|
|
241
|
+
assert.doesNotMatch(codex, /If you are Codex CLI or any other runtime, ignore this entire Codex block/i);
|
|
251
242
|
assert.match(codex, /fail-safe/i);
|
|
252
|
-
assert.match(opencode, /owned by OpenCode/i);
|
|
253
|
-
assert.match(opencode, /ignore this entire OpenCode block/i);
|
|
254
|
-
assert.match(opencode, /fail-safe/i);
|
|
255
243
|
// CORE must not contain runtime-specific directives that would leak
|
|
256
244
|
assert.doesNotMatch(core, /\$hapo-|hapo:/i);
|
|
257
245
|
// No cross-runtime directive leakage: Claude block must not contain foreign runtime directives
|
|
258
|
-
assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT
|
|
259
|
-
assert.doesNotMatch(codex, /<!-- CAFEKIT
|
|
260
|
-
assert.doesNotMatch(opencode, /<!-- CAFEKIT (CORE|CODEX) /);
|
|
246
|
+
assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT CODEX /);
|
|
247
|
+
assert.doesNotMatch(codex, /<!-- CAFEKIT CORE /);
|
|
261
248
|
|
|
262
249
|
const userAgents = '\n## User-owned combined note\nKeep this exact.\n';
|
|
263
250
|
const userClaude = '\n## User-owned Claude note\nKeep this exact.\n';
|
|
@@ -271,17 +258,15 @@ test('combined install keeps CORE neutral and records native shared-root trade-o
|
|
|
271
258
|
assert.match(afterClaude, /User-owned Claude note\nKeep this exact\./);
|
|
272
259
|
assert.equal((afterAgents.match(new RegExp(CORE_START, 'g')) || []).length, 1);
|
|
273
260
|
assert.equal((afterAgents.match(new RegExp(CODEX_START, 'g')) || []).length, 1);
|
|
274
|
-
assert.equal((afterAgents.match(new RegExp(OPENCODE_START, 'g')) || []).length, 1);
|
|
275
261
|
assert.equal((afterClaude.match(new RegExp(START, 'g')) || []).length, 1);
|
|
276
262
|
assert.equal(afterAgents.slice(-userAgents.length), userAgents);
|
|
277
263
|
assert.equal(afterClaude.slice(-userClaude.length), userClaude);
|
|
278
264
|
// Ownership contract must survive rerun
|
|
279
265
|
const afterCore = managedBody(afterAgents, CORE_START, CORE_END);
|
|
280
266
|
const afterCodex = managedBody(afterAgents, CODEX_START, CODEX_END);
|
|
281
|
-
const afterOpenCode = managedBody(afterAgents, OPENCODE_START, OPENCODE_END);
|
|
282
267
|
assert.match(afterCore, /runtime-neutral/i);
|
|
283
|
-
assert.match(afterCodex, /ignore this entire Codex block/i);
|
|
284
|
-
assert.
|
|
268
|
+
assert.match(afterCodex, /If you are Claude Code or any other runtime, ignore this entire Codex block/i);
|
|
269
|
+
assert.doesNotMatch(afterCodex, /If you are Codex CLI or any other runtime, ignore this entire Codex block/i);
|
|
285
270
|
});
|
|
286
271
|
});
|
|
287
272
|
|
|
@@ -289,7 +274,7 @@ test('combined locale stays in CORE and addressing stays in each native managed
|
|
|
289
274
|
inTempProject((root) => {
|
|
290
275
|
const result = spawnSync(
|
|
291
276
|
process.execPath,
|
|
292
|
-
[INSTALLER, '--platform', 'claude,codex
|
|
277
|
+
[INSTALLER, '--platform', 'claude,codex', '--yes', '--lang', 'vi'],
|
|
293
278
|
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } }
|
|
294
279
|
);
|
|
295
280
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
@@ -301,18 +286,17 @@ test('combined locale stays in CORE and addressing stays in each native managed
|
|
|
301
286
|
assert.equal((claude.match(/## Language Consistency <!-- cafekit:lang -->/g) || []).length, 0);
|
|
302
287
|
|
|
303
288
|
configureAddressing({
|
|
304
|
-
platforms: ['claude', 'codex'
|
|
289
|
+
platforms: ['claude', 'codex'],
|
|
305
290
|
ui: { success() {}, warn() {} },
|
|
306
291
|
t: () => 'updated'
|
|
307
292
|
}, 'anh');
|
|
308
293
|
|
|
309
294
|
const addressedAgents = fs.readFileSync('AGENTS.md', 'utf8');
|
|
310
295
|
const addressedClaude = fs.readFileSync('CLAUDE.md', 'utf8');
|
|
311
|
-
assert.equal((addressedAgents.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length,
|
|
296
|
+
assert.equal((addressedAgents.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 1);
|
|
312
297
|
assert.equal((addressedClaude.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 1);
|
|
313
298
|
assert.equal((managedBody(addressedAgents, CORE_START, CORE_END).match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 0);
|
|
314
299
|
assert.match(managedBody(addressedAgents, CODEX_START, CODEX_END), /Codex CLI always addresses the user as "anh"/);
|
|
315
|
-
assert.match(managedBody(addressedAgents, OPENCODE_START, OPENCODE_END), /OpenCode always addresses the user as "anh"/);
|
|
316
300
|
assert.match(managedBody(addressedClaude, START, END), /Claude Code always addresses the user as "anh"/);
|
|
317
301
|
});
|
|
318
302
|
});
|
|
@@ -359,11 +343,10 @@ test('fresh no-lang install leaves locale unset and rules hook silent', () => {
|
|
|
359
343
|
});
|
|
360
344
|
});
|
|
361
345
|
|
|
362
|
-
test('obsolete agent pruning is ownership-aware across Claude
|
|
346
|
+
test('obsolete agent pruning is ownership-aware across Claude and Codex', () => {
|
|
363
347
|
const cases = {
|
|
364
348
|
claude: ['agents/god-developer.md', '.claude'],
|
|
365
|
-
codex: ['agents/god_developer.toml', '.codex']
|
|
366
|
-
opencode: ['agents/god-developer.md', '.opencode']
|
|
349
|
+
codex: ['agents/god_developer.toml', '.codex']
|
|
367
350
|
};
|
|
368
351
|
for (const [platformKey, [relative, folder]] of Object.entries(cases)) {
|
|
369
352
|
for (const state of ['pristine', 'modified', 'untracked']) {
|
|
@@ -523,39 +506,7 @@ test('addressing appends when template has no section without entering shared CO
|
|
|
523
506
|
});
|
|
524
507
|
});
|
|
525
508
|
|
|
526
|
-
test('OpenCode force refresh preserves user and Codex AGENTS blocks', () => {
|
|
527
|
-
inTempProject(() => {
|
|
528
|
-
const userContent = '# User instructions\n\nKeep this exact.\n';
|
|
529
|
-
const codexBlock = '<!-- CAFEKIT CODEX START -->\nCodex managed\n<!-- CAFEKIT CODEX END -->\n';
|
|
530
|
-
fs.writeFileSync('AGENTS.md', `${userContent}\n${codexBlock}`);
|
|
531
|
-
const results = { copied: 0, updated: 0, skipped: 0, missingDependencies: 0 };
|
|
532
|
-
|
|
533
|
-
copyOpenCodeAgentsMdFile('opencode', results, { upgrade: true });
|
|
534
|
-
|
|
535
|
-
const content = fs.readFileSync('AGENTS.md', 'utf8');
|
|
536
|
-
assert.ok(content.startsWith(userContent));
|
|
537
|
-
assert.ok(content.includes(codexBlock.trim()));
|
|
538
|
-
assert.equal((content.match(/<!-- CAFEKIT OPENCODE START -->/g) || []).length, 1);
|
|
539
|
-
assert.equal(results.updated, 1);
|
|
540
|
-
});
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
test('legacy unmarked OpenCode AGENTS content is wrapped without duplication', () => {
|
|
544
|
-
inTempProject(() => {
|
|
545
|
-
const source = path.join(__dirname, '../../src/opencode/AGENTS.md');
|
|
546
|
-
const legacy = normalizeOpenCodeBody(fs.readFileSync(source, 'utf8'));
|
|
547
|
-
const codexBlock = '<!-- CAFEKIT CODEX START -->\nCodex managed\n<!-- CAFEKIT CODEX END -->\n';
|
|
548
|
-
fs.writeFileSync('AGENTS.md', `${legacy}\n${codexBlock}`);
|
|
549
|
-
const results = { copied: 0, updated: 0, skipped: 0, missingDependencies: 0 };
|
|
550
|
-
|
|
551
|
-
copyOpenCodeAgentsMdFile('opencode', results, { upgrade: true });
|
|
552
509
|
|
|
553
|
-
const content = fs.readFileSync('AGENTS.md', 'utf8');
|
|
554
|
-
assert.equal((content.match(/<!-- CAFEKIT OPENCODE START -->/g) || []).length, 1);
|
|
555
|
-
assert.ok(content.includes('## OpenCode Runtime Mapping'));
|
|
556
|
-
assert.ok(content.includes(codexBlock.trim()));
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
510
|
|
|
560
511
|
test('locale patch does not claim ownership of a preserved user runtime', () => {
|
|
561
512
|
inTempProject(() => {
|
|
@@ -737,9 +688,7 @@ test('managed Claude Addressing survives force refresh when template drops it',
|
|
|
737
688
|
|
|
738
689
|
test('managed Codex block Addressing survives upsert with the current template', () => {
|
|
739
690
|
inTempProject(() => {
|
|
740
|
-
const codexTemplate =
|
|
741
|
-
fs.readFileSync(path.join(__dirname, '../../src/codex/AGENTS.md'), 'utf8')
|
|
742
|
-
);
|
|
691
|
+
const codexTemplate = fs.readFileSync(path.join(__dirname, '../../src/codex/AGENTS.md'), 'utf8');
|
|
743
692
|
const userTop = '# User codex notes\n\n';
|
|
744
693
|
const userBottom = '\n\n## User section\nKeep me.\n';
|
|
745
694
|
const configuredAddressing = '## Addressing (Context Overflow Indicator)\n\nThe AI always addresses the user as "bro" throughout the conversation.';
|
|
@@ -757,28 +706,6 @@ test('managed Codex block Addressing survives upsert with the current template',
|
|
|
757
706
|
});
|
|
758
707
|
});
|
|
759
708
|
|
|
760
|
-
test('managed OpenCode block Addressing survives force refresh with the current template', () => {
|
|
761
|
-
inTempProject(() => {
|
|
762
|
-
const userContent = '# User instructions\n\nKeep this exact.\n';
|
|
763
|
-
const codexBlock = '<!-- CAFEKIT CODEX START -->\nCodex managed\n<!-- CAFEKIT CODEX END -->\n';
|
|
764
|
-
const configuredAddressing = '## Addressing (Context Overflow Indicator)\n\nThe AI always addresses the user as "anh" throughout the conversation.';
|
|
765
|
-
fs.writeFileSync(
|
|
766
|
-
'AGENTS.md',
|
|
767
|
-
`${userContent}<!-- CAFEKIT OPENCODE START -->\nOpenCode runtime line.\n\n${configuredAddressing}\n<!-- CAFEKIT OPENCODE END -->\n${codexBlock}`
|
|
768
|
-
);
|
|
769
|
-
const results = { copied: 0, updated: 0, skipped: 0, missingDependencies: 0 };
|
|
770
|
-
|
|
771
|
-
copyOpenCodeAgentsMdFile('opencode', results, { upgrade: true });
|
|
772
|
-
|
|
773
|
-
const content = fs.readFileSync('AGENTS.md', 'utf8');
|
|
774
|
-
assert.ok(content.startsWith(userContent));
|
|
775
|
-
assert.ok(content.includes(codexBlock.trim()));
|
|
776
|
-
assert.equal((content.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 1);
|
|
777
|
-
assert.ok(content.includes('The AI always addresses the user as "anh"'));
|
|
778
|
-
assert.equal((content.match(/<!-- CAFEKIT OPENCODE START -->/g) || []).length, 1);
|
|
779
|
-
assert.equal(results.updated, 1);
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
709
|
|
|
783
710
|
test('copyRecursive skips generated artifacts while copying normal files', () => {
|
|
784
711
|
inTempProject(() => {
|
|
@@ -799,65 +726,3 @@ test('copyRecursive skips generated artifacts while copying normal files', () =>
|
|
|
799
726
|
assert.equal(fs.existsSync('dest/extra/bundle.pyo'), false);
|
|
800
727
|
});
|
|
801
728
|
});
|
|
802
|
-
|
|
803
|
-
test('OpenCode gate uses a real before-hook block and documents the host boundary', () => {
|
|
804
|
-
const opencodeAgents = fs.readFileSync(path.join(__dirname, '../../src/opencode/AGENTS.md'), 'utf8');
|
|
805
|
-
const assertOpenCodeBoundary = (content) => {
|
|
806
|
-
assert.match(content, /tool\.execute\.before/);
|
|
807
|
-
assert.match(content, /hard-block supported completion\/state tools/);
|
|
808
|
-
assert.match(content, /`task`, `taskupdate`, and `todowrite`/);
|
|
809
|
-
assert.match(content, /CAFEKIT_SPEC_GATE_BLOCKED/);
|
|
810
|
-
assert.match(content, /`session\.idle`\/Stop/);
|
|
811
|
-
assert.match(content, /final assistant-turn cancellation boundary/);
|
|
812
|
-
assert.match(content, /observational only/);
|
|
813
|
-
assert.match(content, /full-turn parity with Claude\/Codex/);
|
|
814
|
-
assert.doesNotMatch(content, /tool\.execute\.after/);
|
|
815
|
-
assert.doesNotMatch(content, /best-effort/);
|
|
816
|
-
assert.doesNotMatch(content, /tier-2/);
|
|
817
|
-
assert.doesNotMatch(content, /\badvisory\b/i);
|
|
818
|
-
};
|
|
819
|
-
assertOpenCodeBoundary(opencodeAgents);
|
|
820
|
-
|
|
821
|
-
const pluginPath = path.join(__dirname, '../../src/opencode/plugins/spec-gate.ts');
|
|
822
|
-
assert.equal(fs.existsSync(pluginPath), true, 'OpenCode spec-gate plugin must exist');
|
|
823
|
-
const plugin = fs.readFileSync(pluginPath, 'utf8');
|
|
824
|
-
assert.match(plugin, /completion_gate/);
|
|
825
|
-
assert.match(plugin, /tool\.execute\.before/);
|
|
826
|
-
assert.match(plugin, /CAFEKIT_SPEC_GATE_BLOCKED/);
|
|
827
|
-
assert.match(plugin, /throw createBlockError/);
|
|
828
|
-
assert.match(plugin, /session\.idle/);
|
|
829
|
-
assert.match(plugin, /final assistant message[\s\S]*outside this adapter's boundary/);
|
|
830
|
-
assert.doesNotMatch(plugin, /advisory/i);
|
|
831
|
-
assert.doesNotMatch(plugin, /fail-open/i);
|
|
832
|
-
// JSDoc-safe wording: must not contain block-comment-closing pattern specs/*/spec.json
|
|
833
|
-
assert.doesNotMatch(plugin, /specs\/\*\/spec\.json/);
|
|
834
|
-
// Traversal check must be sibling-prefix safe (resolve + relative, not simple startsWith).
|
|
835
|
-
assert.match(plugin, /relative\s*\(\s*resolvedFeatureDir/);
|
|
836
|
-
assert.match(plugin, /isAbsolute/);
|
|
837
|
-
assert.doesNotMatch(plugin, /if \(!abs\.startsWith\(featureDir\)\)/);
|
|
838
|
-
// Single executable authorities: OpenCode delegates both validation and resolution.
|
|
839
|
-
assert.match(plugin, /workflow-policy\.cjs/);
|
|
840
|
-
assert.match(plugin, /spec-resolver\.cjs/);
|
|
841
|
-
assert.match(plugin, /getSharedValidate|validateCanonicalReceipt/);
|
|
842
|
-
assert.doesNotMatch(plugin, /const hasBase\s*&&\s*hasHead/);
|
|
843
|
-
assert.doesNotMatch(plugin, /PLACEHOLDER_TOKENS/);
|
|
844
|
-
|
|
845
|
-
const installerDocs = fs.readFileSync(path.join(__dirname, '../../../../docs/installer-architecture.md'), 'utf8');
|
|
846
|
-
assert.doesNotMatch(installerDocs, /OpenCode.*hard.*completion gate/i);
|
|
847
|
-
|
|
848
|
-
// Combined install must copy the plugin and both shared authorities.
|
|
849
|
-
inTempProject((root) => {
|
|
850
|
-
const result = spawnSync(process.execPath, [INSTALLER, '--platform', 'opencode', '--yes'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
851
|
-
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
852
|
-
assert.equal(fs.existsSync(path.join(root, '.opencode/plugins/spec-gate.ts')), true);
|
|
853
|
-
assert.equal(fs.existsSync(path.join(root, '.opencode/scripts/workflow-policy.cjs')), true);
|
|
854
|
-
assert.equal(fs.existsSync(path.join(root, '.opencode/scripts/spec-resolver.cjs')), true);
|
|
855
|
-
const installedAgents = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
|
|
856
|
-
assertOpenCodeBoundary(managedBody(installedAgents, OPENCODE_START, OPENCODE_END));
|
|
857
|
-
const installed = fs.readFileSync(path.join(root, '.opencode/plugins/spec-gate.ts'), 'utf8');
|
|
858
|
-
assert.match(installed, /tool\.execute\.before/);
|
|
859
|
-
assert.match(installed, /CAFEKIT_SPEC_GATE_BLOCKED/);
|
|
860
|
-
assert.match(installed, /workflow-policy\.cjs/);
|
|
861
|
-
assert.match(installed, /spec-resolver\.cjs/);
|
|
862
|
-
});
|
|
863
|
-
});
|