@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.4

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.
Files changed (91) hide show
  1. package/bin/__tests__/codex-hooks.test.js +444 -14
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +615 -185
  4. package/bin/__tests__/installer-safety.test.js +39 -0
  5. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  6. package/bin/__tests__/package-inventory.test.js +687 -13
  7. package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
  8. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
  9. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
  10. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  11. package/bin/__tests__/validator-grounding.test.js +1914 -221
  12. package/bin/lib/managed-writer.js +12 -3
  13. package/bin/phases/codex-runtime.js +17 -1
  14. package/package.json +1 -1
  15. package/src/claude/CLAUDE.md +6 -0
  16. package/src/claude/agents/code-auditor.md +20 -0
  17. package/src/claude/agents/spec-maker.md +143 -99
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
  20. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  21. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  22. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  23. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  24. package/src/claude/hooks/completion-authority.cjs +13 -7
  25. package/src/claude/hooks/docs-sync.cjs +5 -2
  26. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  27. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  28. package/src/claude/hooks/privacy-block.cjs +77 -30
  29. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  30. package/src/claude/hooks/spec-gate.cjs +47 -117
  31. package/src/claude/hooks/spec-state.cjs +8 -2
  32. package/src/claude/migration-manifest.json +9 -1
  33. package/src/claude/rules/state-sync.md +52 -21
  34. package/src/claude/rules/workflow.md +17 -9
  35. package/src/claude/scripts/spec-final-state.cjs +244 -0
  36. package/src/claude/scripts/spec-ground.cjs +535 -173
  37. package/src/claude/scripts/spec-readiness.cjs +118 -0
  38. package/src/claude/scripts/spec-receipt.cjs +162 -0
  39. package/src/claude/scripts/spec-resolver.cjs +67 -16
  40. package/src/claude/scripts/spec-scaffold.cjs +1354 -139
  41. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  42. package/src/claude/scripts/validate-spec-output.cjs +2782 -142
  43. package/src/claude/scripts/workflow-policy.cjs +501 -183
  44. package/src/claude/settings/settings.json +4 -0
  45. package/src/claude/skills/code-review/SKILL.md +12 -3
  46. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  47. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  48. package/src/claude/skills/develop/SKILL.md +47 -60
  49. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  50. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  51. package/src/claude/skills/specs/SKILL.md +183 -177
  52. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  53. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  54. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  55. package/src/claude/skills/specs/references/review.md +200 -260
  56. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  57. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  58. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  59. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  60. package/src/claude/skills/specs/rules/design-review.md +42 -97
  61. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  62. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  63. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  64. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  65. package/src/claude/skills/specs/templates/design.md +49 -263
  66. package/src/claude/skills/specs/templates/requirements.md +26 -36
  67. package/src/claude/skills/specs/templates/research.md +9 -98
  68. package/src/claude/skills/specs/templates/spec-state.json +23 -85
  69. package/src/claude/skills/specs/templates/task.md +30 -102
  70. package/src/claude/skills/sync/SKILL.md +33 -10
  71. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  72. package/src/claude/skills/test/SKILL.md +17 -7
  73. package/src/codex/AGENTS.md +6 -0
  74. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  75. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  76. package/src/codex/hooks/completion-authority.cjs +13 -7
  77. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  78. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  79. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  80. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  81. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  82. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  83. package/src/codex/hooks/privacy-approval.cjs +1 -1
  84. package/src/codex/hooks/privacy-block.cjs +67 -11
  85. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  86. package/src/codex/hooks/spec-gate.cjs +49 -34
  87. package/src/codex/hooks/spec-state.cjs +10 -3
  88. package/src/codex/hooks/state.cjs +2 -0
  89. package/src/codex/hooks.json +6 -1
  90. package/src/codex/rules/state-sync.md +47 -27
  91. package/src/common/AGENTS.md +1 -1
@@ -60,9 +60,9 @@ function allFiles(root, predicate) {
60
60
  return found;
61
61
  }
62
62
 
63
- function allHookHandlers(config) {
64
- return Object.values(config.hooks).flatMap((groups) => (
65
- groups.flatMap((group) => group.hooks)
63
+ function allHookLaunchers(config) {
64
+ return Object.entries(config.hooks).flatMap(([event, groups]) => (
65
+ groups.flatMap((group) => group.hooks.map((handler) => ({ event, handler })))
66
66
  ));
67
67
  }
68
68
 
@@ -72,6 +72,156 @@ function parseGeneratedTomlString(content, key) {
72
72
  return JSON.parse(match[1]);
73
73
  }
74
74
 
75
+ function renderCanonicalVerificationExample(template) {
76
+ const values = new Map([
77
+ ['SUBJECT_REQ', '1'],
78
+ ['X', '1'],
79
+ ['PROOF_REQ', '1'],
80
+ ['Y', '2'],
81
+ ['exact command', 'node --test test/installed.test.js'],
82
+ ['exact anchored target', 'src/installed.js#entry'],
83
+ ['exact/repository/entrypoint', 'src/installed.js'],
84
+ ['observable result', 'the subject behavior and verifier proof both pass'],
85
+ ['concrete observable result and proof', 'the subject behavior and verifier proof both pass'],
86
+ ['concrete rejected or recovery case', 'invalid input remains rejected and observable'],
87
+ ['real entrypoint/caller and grounded anchor expectation', 'the installed entrypoint reaches A-D-01'],
88
+ ]);
89
+ return template.replace(/\{\{([^}]+)\}\}/g, (placeholder, name) => (
90
+ values.has(name) ? values.get(name) : placeholder
91
+ ));
92
+ }
93
+
94
+ function assertInstalledVerificationModel(grounderPath, designTemplate) {
95
+ const { parseVerificationDefinitions } = require(grounderPath);
96
+ assert.equal(typeof parseVerificationDefinitions, 'function');
97
+ const concreteDesign = renderCanonicalVerificationExample(designTemplate);
98
+ const errors = [];
99
+ const definitions = parseVerificationDefinitions(concreteDesign, errors);
100
+ assert.deepEqual(errors, []);
101
+ assert.equal(definitions.size, 1);
102
+ const definition = definitions.get('V1');
103
+ assert.ok(definition);
104
+ assert.deepEqual(definition.subject_criteria, ['R1.1']);
105
+ assert.deepEqual(definition.proof_criteria, []);
106
+ assert.equal(definition.proof_owner, null);
107
+ assert.equal(definition.evidence_anchor, null);
108
+ assert.deepEqual(definition.decision_refs, ['D1', 'I1', 'C1']);
109
+ for (const field of [
110
+ 'subject_criteria', 'subject_owner', 'decision_refs', 'method', 'expected',
111
+ 'negative', 'reachability'
112
+ ]) {
113
+ const value = definition[field];
114
+ assert.ok(
115
+ Array.isArray(value)
116
+ ? value.length > 0
117
+ : (typeof value === 'string' ? value.trim() !== '' : value && Object.keys(value).length > 0)
118
+ );
119
+ }
120
+ for (const mutation of [
121
+ concreteDesign.replace('- **V1**:', '### V1 —'),
122
+ concreteDesign.replace('- **V1**:', '| V1 |'),
123
+ concreteDesign.replace('; Expected ', '\nExpected '),
124
+ concreteDesign.replace('Decision refs ', 'Decisions '),
125
+ ]) {
126
+ const mutationErrors = [];
127
+ const mutated = parseVerificationDefinitions(mutation, mutationErrors);
128
+ assert.ok(mutationErrors.length > 0);
129
+ assert.equal(mutated.has('V1'), false);
130
+ }
131
+ }
132
+
133
+ function artifactRouterRecords(skill) {
134
+ const lines = skill.split('\n');
135
+ const heading = lines.findIndex((line) => line.trim() === '## Artifact router');
136
+ const start = lines.findIndex((line, index) => index > heading && line.trim().startsWith('|'));
137
+ const rows = [];
138
+ for (let index = start; index >= 0 && index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
139
+ const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
140
+ if (!cells.every((cell) => /^:?-+:?$/.test(cell))) rows.push(cells);
141
+ }
142
+ const header = rows.shift() || [];
143
+ return new Map(rows.map((cells) => [
144
+ (cells[0] || '').replaceAll('`', ''),
145
+ Object.fromEntries(header.map((name, index) => [name, cells[index] || '']))
146
+ ]));
147
+ }
148
+
149
+ function installedAuthoringProjectionIssues(files) {
150
+ const issues = [];
151
+ const routes = artifactRouterRecords(files.skill);
152
+ const none = routes.get('None');
153
+ if (!none || none['Durable core'] !== 'none'
154
+ || none['Research route'] !== 'absent' || none['Task route'] !== 'absent') {
155
+ issues.push('none-route');
156
+ }
157
+ for (const depth of ['Compact', 'Full']) {
158
+ const route = routes.get(depth);
159
+ if (!route || !['spec.json', 'requirements.md', 'design.md'].every((name) => (
160
+ route['Durable core'].includes(`\`${name}\``)
161
+ ))) issues.push(`${depth.toLowerCase()}-core`);
162
+ if (!route?.['Research route'].includes('research trigger')) issues.push(`${depth.toLowerCase()}-research`);
163
+ if (!route?.['Task route'].includes('typed topology trigger')) issues.push(`${depth.toLowerCase()}-tasks`);
164
+ }
165
+ if (/\balways search\b|\bcomprehensive research\b|\bcapture all findings\b/i.test(files.discovery)) {
166
+ issues.push('default-research');
167
+ }
168
+ if (/(?:limit|maximum|at most|top|≤|<=)\s*(?:to\s*)?3\b/i.test(files.review)) {
169
+ issues.push('review-cap');
170
+ }
171
+ if (!/For every `RN\.M`/.test(files.review)) issues.push('criterion-coverage');
172
+ if (/\bcurrent_phase\b/.test(files.codex)) issues.push('phase-alias');
173
+ for (const state of ['in_progress', 'paused', 'blocked', 'done']) {
174
+ if (!files.codex.includes(`\`${state}\``)) issues.push(`lifecycle-${state}`);
175
+ }
176
+ if (/^##\s+Evidence\s*$/m.test(files.tasks)) issues.push('task-evidence');
177
+ if (/\(P\)/.test(files.tasks)) issues.push('task-priority-marker');
178
+ for (const claudeOnly of [
179
+ 'AskUserQuestion', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList',
180
+ 'WebSearch', 'WebFetch', 'SendMessage'
181
+ ]) {
182
+ if (files.codex.includes(claudeOnly)) issues.push(`claude-tool-${claudeOnly}`);
183
+ }
184
+ if (!files.codex.includes('semantic_model')
185
+ || !files.codex.includes('explicit installed machine semantic-sync step')
186
+ || !files.codex.includes('round-trip')
187
+ || !files.codex.includes('node .codex/scripts/spec-scaffold.cjs <feature> --sync-semantic-model')) {
188
+ issues.push('semantic-model-promotion');
189
+ }
190
+ return issues;
191
+ }
192
+
193
+ function assertInstalledAuthoringProjection(root) {
194
+ const read = (relative) => fs.readFileSync(path.join(root, relative), 'utf8');
195
+ const files = {
196
+ skill: read('.agents/skills/specs/SKILL.md'),
197
+ discovery: [
198
+ read('.agents/skills/specs/rules/design-discovery-light.md'),
199
+ read('.agents/skills/specs/rules/design-discovery-full.md')
200
+ ].join('\n'),
201
+ review: [
202
+ read('.agents/skills/specs/rules/design-review.md'),
203
+ read('.agents/skills/specs/references/review.md')
204
+ ].join('\n'),
205
+ tasks: [
206
+ read('.agents/skills/specs/rules/tasks-generation.md'),
207
+ read('.agents/skills/specs/rules/task-scoring-rubric.md'),
208
+ read('.agents/skills/specs/rules/phase-decision-matrix.md')
209
+ ].join('\n'),
210
+ codex: [read('AGENTS.md'), read('.codex/rules/state-sync.md')].join('\n')
211
+ };
212
+ assert.deepEqual(installedAuthoringProjectionIssues(files), []);
213
+ const mutations = [
214
+ ['discovery', (value) => `${value}\nAlways search before design.`],
215
+ ['review', (value) => `${value}\nLimit to 3 blockers.`],
216
+ ['codex', (value) => `${value}\nSet current_phase then call TaskUpdate.`],
217
+ ['tasks', (value) => `${value}\n## Evidence\nUse (P).`]
218
+ ];
219
+ for (const [key, mutate] of mutations) {
220
+ const changed = { ...files, [key]: mutate(files[key]) };
221
+ assert.notDeepEqual(installedAuthoringProjectionIssues(changed), []);
222
+ }
223
+ }
224
+
75
225
  test('Codex payload transform emits native skill and subagent syntax', () => {
76
226
  const transformed = normalizeCodexBody(
77
227
  'Agent(subagent_type="implementer", prompt="Implement it", description="Code Feature")\n' +
@@ -441,9 +591,10 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
441
591
  const config = JSON.parse(
442
592
  fs.readFileSync(path.join(projectRoot, '.codex', 'hooks.json'), 'utf8')
443
593
  );
444
- const handlers = allHookHandlers(config);
445
- assert.equal(handlers.length, 16);
446
- for (const handler of handlers) {
594
+ const launchers = allHookLaunchers(config);
595
+ assert.ok(launchers.length > 0, 'installed Codex config must register hook launchers');
596
+ const semanticReviewEvents = [];
597
+ for (const { event, handler } of launchers) {
447
598
  assert.doesNotMatch(handler.commandWindows, /\$\(/);
448
599
  assert.doesNotMatch(handler.commandWindows, /\bgit\b/i);
449
600
  assert.doesNotMatch(handler.commandWindows, /process\.cwd\(\)|existsSync/);
@@ -456,7 +607,11 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
456
607
  true,
457
608
  `missing installed hook: ${path.basename(target)}`
458
609
  );
610
+ if (path.basename(target) === 'semantic-review-authority.cjs') {
611
+ semanticReviewEvents.push(event);
612
+ }
459
613
  }
614
+ assert.deepEqual(semanticReviewEvents, ['SubagentStop']);
460
615
 
461
616
  const nested = path.join(projectRoot, 'nested', 'workspace');
462
617
  fs.mkdirSync(nested, { recursive: true });
@@ -486,6 +641,7 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
486
641
  assert.match(launched.stdout, /Session startup\./);
487
642
  assert.match(launched.stdout, /CafeKit project root:/);
488
643
  assert.equal(fs.existsSync(shadowMarker), false);
644
+ assert.match(session.commandWindows, /require\('module'\)\.runMain\(\)/, 'Windows launcher must execute hook main');
489
645
  });
490
646
  });
491
647
 
@@ -506,13 +662,44 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
506
662
  '.codex/hooks/privacy-block.cjs',
507
663
  '.codex/rules/workflow.md',
508
664
  '.codex/rules/hook-protocols.md',
665
+ '.codex/rules/state-sync.md',
666
+ '.codex/scripts/spec-ground.cjs',
509
667
  '.codex/scripts/validate-spec-output.cjs',
510
668
  '.agents/.gitignore',
511
- '.agents/skills/specs/SKILL.md'
669
+ '.agents/skills/specs/SKILL.md',
670
+ '.agents/skills/specs/templates/design.md',
671
+ '.agents/skills/specs/templates/task.md',
672
+ '.agents/skills/specs/templates/spec-state.json'
512
673
  ]) {
513
674
  assert.equal(fs.existsSync(path.join(root, relative)), true, `missing ${relative}`);
514
675
  }
515
676
 
677
+ const installedDesign = fs.readFileSync(
678
+ path.join(root, '.agents/skills/specs/templates/design.md'), 'utf8'
679
+ );
680
+ assert.equal((installedDesign.match(/^## Verification Definitions$/gm) || []).length, 1);
681
+ assertInstalledVerificationModel(
682
+ path.join(root, '.codex/scripts/spec-ground.cjs'),
683
+ installedDesign
684
+ );
685
+ assertInstalledAuthoringProjection(root);
686
+
687
+ const installedTask = fs.readFileSync(
688
+ path.join(root, '.agents/skills/specs/templates/task.md'), 'utf8'
689
+ );
690
+ assert.deepEqual(
691
+ [...installedTask.matchAll(/^## (.+)$/gm)].map((match) => match[1]),
692
+ ['Outcome', 'Scope', 'Anchors and Ownership', 'Changes', 'Acceptance', 'Dependencies', 'Verification Plan']
693
+ );
694
+ assert.match(installedTask, /^- \*\*Task role:\*\*/m);
695
+
696
+ const installedState = JSON.parse(fs.readFileSync(
697
+ path.join(root, '.agents/skills/specs/templates/spec-state.json'), 'utf8'
698
+ ));
699
+ assert.deepEqual(Object.keys(installedState.workflow_policy).sort(), [
700
+ 'assurance_level', 'classified_minimum', 'planning_depth', 'risks', 'version'
701
+ ]);
702
+
516
703
  const agentsGitignore = fs.readFileSync(
517
704
  path.join(root, '.agents', '.gitignore'),
518
705
  'utf8'
@@ -663,6 +850,7 @@ test('Codex install on top of existing Claude installation preserves content and
663
850
  assert.equal(fs.existsSync(path.join(root, '.codex', 'runtime.json')), true, '.codex/runtime.json should exist');
664
851
  assert.equal(fs.existsSync(path.join(root, '.codex', 'hooks', 'privacy-block.cjs')), true, '.codex/hooks/privacy-block.cjs should exist');
665
852
  assert.equal(fs.existsSync(path.join(root, '.codex', 'rules', 'workflow.md')), true, '.codex/rules/workflow.md should exist');
853
+ assert.equal(fs.existsSync(path.join(root, '.codex', 'scripts', 'spec-ground.cjs')), true, '.codex/scripts/spec-ground.cjs should exist');
666
854
  assert.equal(fs.existsSync(path.join(root, '.codex', 'scripts', 'validate-spec-output.cjs')), true, '.codex/scripts/validate-spec-output.cjs should exist');
667
855
  assert.equal(fs.existsSync(path.join(root, '.agents', '.gitignore')), true, '.agents/.gitignore should exist');
668
856
  assert.equal(fs.existsSync(path.join(root, '.agents', 'skills', 'specs', 'SKILL.md')), true, '.agents/skills/specs/SKILL.md should exist');
@@ -684,3 +872,36 @@ test('Codex install on top of existing Claude installation preserves content and
684
872
  assert.equal(claudeMd, claudeMdContent, 'CLAUDE.md should be unchanged');
685
873
  });
686
874
  });
875
+
876
+ test('Codex installed code_auditor contains Strict conditional marker', () => {
877
+ inTempProject((root) => {
878
+ const result = install(root);
879
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
880
+ const toml = fs.readFileSync(path.join(root, '.codex', 'agents', 'code_auditor.toml'), 'utf8');
881
+ assert.match(toml, /Strict Semantic Review Attestation/);
882
+ assert.match(toml, /CAFEKIT_SEMANTIC_REVIEW_ATTESTATION/);
883
+ assert.match(toml, /MAC-protected host-hook observation/);
884
+ assert.doesNotMatch(toml, /host-signed/);
885
+ assert.match(toml, /specs\/<feature>\/spec\.json/);
886
+ });
887
+ });
888
+
889
+ test('Codex scaffold resolver rejects symlink template', () => {
890
+ inTempProject((root) => {
891
+ const result = install(root);
892
+ assert.equal(result.status, 0);
893
+ const template = path.join(root, '.agents', 'skills', 'specs', 'templates', 'task.md');
894
+ const target = path.join(root, 'outside.md');
895
+ fs.writeFileSync(target, 'evil');
896
+ const original = fs.readFileSync(template);
897
+ fs.unlinkSync(template);
898
+ fs.symlinkSync(target, template);
899
+ const scaffold = path.join(root, '.codex', 'scripts', 'spec-scaffold.cjs');
900
+ const out = spawnSync(process.execPath, [scaffold, 'symlink-test', '--tasks', 'R1-01-foo,R1-02-bar', '--boundaries', '[{"id":"B-OWN","type":"ownership","tasks":["R1-01","R1-02"],"write_sets":{"R1-01":["src/a.js"],"R1-02":["src/b.js"]}}]'], { cwd: root, encoding: 'utf8' });
901
+ // Should fail because template is symlink and resolver rejects it
902
+ assert.notEqual(out.status, 0, 'scaffold should reject symlink template');
903
+ assert.match(`${out.stdout}\n${out.stderr}`, /template not found|symlink/i);
904
+ fs.unlinkSync(template);
905
+ fs.writeFileSync(template, original);
906
+ });
907
+ });