@haposoft/cafekit 0.16.0-rc.2 → 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 (98) hide show
  1. package/bin/__tests__/codex-hooks.test.js +794 -21
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +1838 -112
  4. package/bin/__tests__/installer-safety.test.js +82 -29
  5. package/bin/__tests__/opencode-spec-gate.test.js +799 -0
  6. package/bin/__tests__/package-inventory.test.js +841 -10
  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 +2469 -71
  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 +153 -202
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
  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 +604 -56
  22. package/src/claude/hooks/completion-authority-check.cjs +56 -0
  23. package/src/claude/hooks/completion-authority-state.cjs +527 -0
  24. package/src/claude/hooks/completion-authority.cjs +154 -0
  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/session.cjs +33 -19
  31. package/src/claude/hooks/spec-gate.cjs +181 -139
  32. package/src/claude/hooks/spec-state.cjs +112 -47
  33. package/src/claude/migration-manifest.json +15 -2
  34. package/src/claude/rules/state-sync.md +52 -21
  35. package/src/claude/rules/workflow.md +17 -9
  36. package/src/claude/scripts/provenance.cjs +315 -0
  37. package/src/claude/scripts/spec-final-state.cjs +244 -0
  38. package/src/claude/scripts/spec-ground.cjs +535 -173
  39. package/src/claude/scripts/spec-readiness.cjs +118 -0
  40. package/src/claude/scripts/spec-receipt.cjs +162 -0
  41. package/src/claude/scripts/spec-resolver.cjs +501 -0
  42. package/src/claude/scripts/spec-scaffold.cjs +1945 -55
  43. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  44. package/src/claude/scripts/validate-spec-output.cjs +3204 -262
  45. package/src/claude/scripts/workflow-policy.cjs +1888 -203
  46. package/src/claude/settings/settings.json +12 -0
  47. package/src/claude/skills/code-review/SKILL.md +94 -66
  48. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  49. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  50. package/src/claude/skills/develop/SKILL.md +138 -283
  51. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  52. package/src/claude/skills/develop/references/quality-gate.md +123 -148
  53. package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
  54. package/src/claude/skills/specs/SKILL.md +192 -397
  55. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  56. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  57. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  58. package/src/claude/skills/specs/references/review.md +200 -260
  59. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  60. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  61. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  62. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  63. package/src/claude/skills/specs/rules/design-review.md +42 -97
  64. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  65. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  66. package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
  67. package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
  68. package/src/claude/skills/specs/templates/design.md +49 -263
  69. package/src/claude/skills/specs/templates/requirements.md +26 -36
  70. package/src/claude/skills/specs/templates/research.md +9 -98
  71. package/src/claude/skills/specs/templates/spec-state.json +26 -67
  72. package/src/claude/skills/specs/templates/task.md +30 -82
  73. package/src/claude/skills/sync/SKILL.md +45 -15
  74. package/src/claude/skills/sync/references/sync-protocols.md +34 -16
  75. package/src/claude/skills/test/SKILL.md +151 -167
  76. package/src/codex/AGENTS.md +6 -0
  77. package/src/codex/hooks/completion-authority-check.cjs +63 -0
  78. package/src/codex/hooks/completion-authority-state.cjs +527 -0
  79. package/src/codex/hooks/completion-authority.cjs +106 -0
  80. package/src/codex/hooks/lib/hook-context.cjs +58 -4
  81. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  82. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  83. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  84. package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
  85. package/src/codex/hooks/lib/spec-utils.cjs +104 -27
  86. package/src/codex/hooks/privacy-approval.cjs +1 -1
  87. package/src/codex/hooks/privacy-block.cjs +67 -11
  88. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  89. package/src/codex/hooks/session.cjs +2 -0
  90. package/src/codex/hooks/spec-gate.cjs +151 -24
  91. package/src/codex/hooks/spec-state.cjs +65 -12
  92. package/src/codex/hooks/state.cjs +2 -0
  93. package/src/codex/hooks.json +16 -1
  94. package/src/codex/rules/state-sync.md +47 -27
  95. package/src/common/AGENTS.md +1 -1
  96. package/src/opencode/AGENTS.md +4 -2
  97. package/src/opencode/plugins/spec-gate.ts +296 -122
  98. package/src/opencode/plugins/spec-state.ts +183 -116
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const SEMANTIC = require('./spec-semantic-model.cjs');
7
+ const VALIDATOR = require('./validate-spec-output.cjs');
8
+ const { canonicalProjectRoot, groundSpec } = require('./spec-ground.cjs');
9
+
10
+ function plain(value) {
11
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
12
+ }
13
+
14
+ function exactKeys(value, fields) {
15
+ return plain(value)
16
+ && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...fields].sort());
17
+ }
18
+
19
+ function readReviewResult(input) {
20
+ if (!exactKeys(input, ['reviewed_criteria', 'counterexamples'])) {
21
+ throw new Error('semantic review result fields must be exactly reviewed_criteria and counterexamples');
22
+ }
23
+ return JSON.parse(JSON.stringify(input));
24
+ }
25
+
26
+ function atomicReplace(file, bytes, io = fs) {
27
+ const stat = io.lstatSync(file);
28
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('spec.json must be a regular non-symlink file');
29
+ const original = io.readFileSync(file);
30
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.finalize-${process.pid}-${Date.now()}`);
31
+ let descriptor;
32
+ try {
33
+ descriptor = io.openSync(temporary, 'wx', stat.mode);
34
+ io.writeFileSync(descriptor, bytes);
35
+ io.fsyncSync(descriptor);
36
+ io.closeSync(descriptor);
37
+ descriptor = undefined;
38
+ if (process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME === '1') throw new Error('injected finalization failure before rename');
39
+ io.renameSync(temporary, file);
40
+ } catch (error) {
41
+ if (descriptor !== undefined) try { io.closeSync(descriptor); } catch {}
42
+ try { io.unlinkSync(temporary); } catch {}
43
+ if (!io.readFileSync(file).equals(original)) {
44
+ const rollback = `${temporary}-rollback`;
45
+ try {
46
+ io.writeFileSync(rollback, original, { flag: 'wx', mode: stat.mode });
47
+ io.renameSync(rollback, file);
48
+ } finally { try { io.unlinkSync(rollback); } catch {} }
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ function finalizeReadiness({ specDir, reviewResult, projectRoot, io = fs }) {
55
+ const directory = io.realpathSync(path.resolve(specDir));
56
+ const root = projectRoot ? io.realpathSync(path.resolve(projectRoot)) : canonicalProjectRoot(directory);
57
+ const relative = path.relative(root, directory);
58
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
59
+ throw new Error('spec directory escapes the canonical project root');
60
+ }
61
+ const specFile = path.join(directory, 'spec.json');
62
+ const original = io.readFileSync(specFile);
63
+ let spec;
64
+ try { spec = JSON.parse(original); } catch (error) { throw new Error(`spec.json is invalid JSON (${error.message})`); }
65
+ if (!plain(spec) || spec.schema_version !== '2.1') throw new Error('readiness finalization accepts canonical schema 2.1 only');
66
+ const review = readReviewResult(reviewResult);
67
+ const projection = SEMANTIC.modelFromMarkdown(directory, spec);
68
+ if (projection.errors.length) throw new Error(`semantic projection failed (${projection.errors.join('; ')})`);
69
+ const candidate = JSON.parse(JSON.stringify(spec));
70
+ candidate.semantic_model = projection.model;
71
+ candidate.validation = {
72
+ status: 'completed',
73
+ semantic_review: {
74
+ status: 'not-run', semantic_digest: null, reviewed_criteria: [], counterexamples: [],
75
+ },
76
+ };
77
+ candidate.ready_for_implementation = false;
78
+ const digestResult = VALIDATOR.computeSemanticDigest21(directory, candidate);
79
+ if (digestResult.errors.length) throw new Error(`semantic digest failed (${digestResult.errors.join('; ')})`);
80
+ candidate.validation.semantic_review = {
81
+ status: 'completed', semantic_digest: digestResult.digest,
82
+ reviewed_criteria: review.reviewed_criteria, counterexamples: review.counterexamples,
83
+ };
84
+ candidate.ready_for_implementation = true;
85
+ candidate.updated_at = new Date().toISOString();
86
+ const validation = VALIDATOR.validateSpec(directory, candidate);
87
+ if (validation.errors.length) throw new Error(`readiness validation failed (${validation.errors.join('; ')})`);
88
+ const grounding = groundSpec({ specDir: directory, root, spec: candidate });
89
+ if (grounding.errors.length) throw new Error(`readiness grounding failed (${grounding.errors.join('; ')})`);
90
+ const bytes = Buffer.from(`${JSON.stringify(candidate, null, 2)}\n`);
91
+ atomicReplace(specFile, bytes, io);
92
+ return { spec: candidate, semantic_digest: digestResult.digest };
93
+ }
94
+
95
+ function usage() {
96
+ process.stderr.write('Usage: node .claude/scripts/spec-readiness.cjs specs/<feature> --review-result <review.json>\n');
97
+ }
98
+
99
+ function main() {
100
+ const args = process.argv.slice(2);
101
+ const marker = args.indexOf('--review-result');
102
+ if (args.length !== 3 || marker !== 1) { usage(); process.exitCode = 2; return; }
103
+ try {
104
+ const reviewFile = path.resolve(args[2]);
105
+ const reviewStat = fs.lstatSync(reviewFile);
106
+ if (!reviewStat.isFile() || reviewStat.isSymbolicLink()) throw new Error('review result must be a regular non-symlink JSON file');
107
+ const reviewResult = JSON.parse(fs.readFileSync(reviewFile, 'utf8'));
108
+ const result = finalizeReadiness({ specDir: args[0], reviewResult });
109
+ process.stdout.write(`SPEC_READY ${args[0]} ${result.semantic_digest}\n`);
110
+ } catch (error) {
111
+ process.stderr.write(`SPEC_NOT_READY ${error.message}\n`);
112
+ process.exitCode = 1;
113
+ }
114
+ }
115
+
116
+ module.exports = { atomicReplace, finalizeReadiness, readReviewResult };
117
+
118
+ if (require.main === module) main();
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const EVIDENCE_HEADINGS = new Set([
8
+ 'Evidence',
9
+ 'Task Test Plan & Verification Evidence',
10
+ 'Verification & Evidence',
11
+ ]);
12
+
13
+ function inside(root, target) {
14
+ const relative = path.relative(path.resolve(root), path.resolve(target));
15
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
16
+ }
17
+
18
+ function safeRead(root, relativePath) {
19
+ if (typeof relativePath !== 'string' || path.isAbsolute(relativePath)) return { status: 'unsafe' };
20
+ const segments = relativePath.split(/[\\/]+/);
21
+ if (segments.length === 0 || segments.some((segment) => !segment || segment === '.' || segment === '..')) return { status: 'unsafe' };
22
+ const resolvedRoot = path.resolve(root);
23
+ const target = path.resolve(resolvedRoot, ...segments);
24
+ if (!inside(resolvedRoot, target)) return { status: 'unsafe' };
25
+ try {
26
+ const canonicalRoot = fs.realpathSync(resolvedRoot);
27
+ let current = resolvedRoot;
28
+ for (const segment of segments) {
29
+ current = path.join(current, segment);
30
+ const stat = fs.lstatSync(current);
31
+ if (stat.isSymbolicLink()) return { status: 'unsafe' };
32
+ }
33
+ const canonicalTarget = fs.realpathSync(target);
34
+ if (!inside(canonicalRoot, canonicalTarget) || !fs.statSync(target).isFile()) return { status: 'unsafe' };
35
+ return { status: 'ok', path: canonicalTarget, bytes: fs.readFileSync(target) };
36
+ } catch (error) {
37
+ return { status: error && error.code === 'ENOENT' ? 'missing' : 'unsafe' };
38
+ }
39
+ }
40
+
41
+ function canonicalTaskReceiptPath(taskPath) {
42
+ if (typeof taskPath !== 'string') return null;
43
+ const match = taskPath.match(/^tasks\/(task-[A-Za-z0-9][A-Za-z0-9._-]*\.md)$/);
44
+ return match ? `receipts/${match[1]}` : null;
45
+ }
46
+
47
+ function evidenceBody(text, policy = {}) {
48
+ const lines = String(text || '').split('\n');
49
+ let start = -1;
50
+ let level = 0;
51
+ for (let index = 0; index < lines.length; index += 1) {
52
+ const match = lines[index].match(/^(#{2,3})\s+(.+?)\s*$/);
53
+ if (match && EVIDENCE_HEADINGS.has(match[2])) {
54
+ start = index + 1;
55
+ level = match[1].length;
56
+ break;
57
+ }
58
+ }
59
+ if (start < 0) return null;
60
+ let end = lines.length;
61
+ for (let index = start; index < lines.length; index += 1) {
62
+ const heading = lines[index].match(/^(#{1,6})\s+/);
63
+ const tap = typeof policy.isTapMetadataHeading === 'function' && policy.isTapMetadataHeading(lines[index]);
64
+ if (heading && heading[1].length <= level && !tap) { end = index; break; }
65
+ }
66
+ return lines.slice(start, end).join('\n');
67
+ }
68
+
69
+ function fieldValues(body, name) {
70
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
71
+ const matcher = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*?)\\s*$`, 'i');
72
+ return String(body || '').split('\n').map((line) => line.match(matcher)).filter(Boolean).map((match) => match[1]);
73
+ }
74
+
75
+ function concrete(value) {
76
+ return typeof value === 'string' && value.trim() !== '' && !/^(?:N\/A|NONE|PENDING|TBD|UNKNOWN|\{\{.*\}\})$/i.test(value.trim());
77
+ }
78
+
79
+ function plannedField(taskText, name) {
80
+ const plan = String(taskText || '').split(/^##\s+Verification Plan\s*$/m)[1];
81
+ if (!plan) return null;
82
+ const value = fieldValues(plan.split(/^##\s+/m)[0], `- **${name}**`)[0];
83
+ return value ? value.replace(/^`|`$/g, '').trim() : null;
84
+ }
85
+
86
+ function identity(body) {
87
+ const names = ['Verification', 'Command', 'Commands', 'Exit', 'Result', 'Base', 'Head', 'base_sha', 'head_sha'];
88
+ const fields = names.flatMap((name) => fieldValues(body, name).map((value) => [name.toLowerCase(), value.trim()]));
89
+ const artifacts = String(body || '').split('\n').filter((line) => /^\s*Artifacts?\s*:|^\s*Artifact\s+produced\b/i.test(line)).map((line) => line.trim());
90
+ return crypto.createHash('sha256').update(JSON.stringify({ fields, artifacts })).digest('hex');
91
+ }
92
+
93
+ function canonicalFailures(body, task, runtimeContext, policy) {
94
+ if (!policy || typeof policy.validateCanonicalReceipt !== 'function') return ['validator_unavailable'];
95
+ const options = typeof policy.receiptValidatorOptions === 'function'
96
+ ? policy.receiptValidatorOptions(task || {}, { runtimeContext, requireProvenanceBinding: true })
97
+ : { requireProvenanceBinding: true };
98
+ return policy.validateCanonicalReceipt(body, options);
99
+ }
100
+
101
+ function separateMetadataFailures(body, taskPath, taskText) {
102
+ const failures = [];
103
+ const basename = path.posix.basename(taskPath);
104
+ if (fieldValues(body, 'Task').length !== 1 || fieldValues(body, 'Task')[0] !== basename) failures.push('task_identity');
105
+ if (fieldValues(body, 'Task path').length !== 1 || fieldValues(body, 'Task path')[0] !== taskPath) failures.push('task_identity');
106
+ if (!concrete(fieldValues(body, 'Expected')[0]) || !concrete(fieldValues(body, 'Observed')[0])) failures.push('expected_observed');
107
+ const plannedCommand = plannedField(taskText, 'Command');
108
+ const actualCommand = fieldValues(body, 'Command')[0] || fieldValues(body, 'Commands')[0];
109
+ if (plannedCommand && !/^N\/A\b/i.test(plannedCommand) && actualCommand !== plannedCommand) failures.push('command_identity');
110
+ for (const name of ['Negative path', 'Reachability']) {
111
+ const planned = plannedField(taskText, name);
112
+ if (planned && !/^(?:N\/A|none|not relevant)\b/i.test(planned) && !concrete(fieldValues(body, name)[0])) failures.push(name.toLowerCase().replace(' ', '_'));
113
+ }
114
+ return [...new Set(failures)];
115
+ }
116
+
117
+ function readTaskProof(featureDir, taskPath, policy = {}) {
118
+ const receiptPath = canonicalTaskReceiptPath(taskPath);
119
+ if (typeof taskPath !== 'string' || !/^tasks\/[^/]+\.md$/.test(taskPath)) return { status: 'unsafe', taskPath, receiptPath: null };
120
+ const taskFile = safeRead(featureDir, taskPath);
121
+ if (taskFile.status !== 'ok') return { status: taskFile.status, taskPath, receiptPath };
122
+ const taskText = taskFile.bytes.toString('utf8');
123
+ const separate = receiptPath ? safeRead(featureDir, receiptPath) : { status: 'missing' };
124
+ const legacyBody = evidenceBody(taskText, policy);
125
+ if (separate.status === 'unsafe') return { status: 'unsafe', taskPath, receiptPath, taskFile };
126
+ if (separate.status === 'ok') {
127
+ const body = separate.bytes.toString('utf8');
128
+ return { status: 'ok', source: 'separate', body, receiptBytes: separate.bytes, receiptFile: separate, taskFile, taskText, taskPath, receiptPath, legacyBody };
129
+ }
130
+ if (legacyBody !== null) return { status: 'ok', source: 'legacy', body: legacyBody, receiptBytes: Buffer.from(legacyBody), taskFile, taskText, taskPath, receiptPath, legacyBody };
131
+ return { status: 'missing', taskPath, receiptPath, taskFile, taskText };
132
+ }
133
+
134
+ function checkTaskReceipt(featureDir, taskPath, task, runtimeContext, policy) {
135
+ const proof = readTaskProof(featureDir, taskPath, policy);
136
+ const failures = [];
137
+ if (proof.status !== 'ok') return { ...proof, failures: [proof.status === 'unsafe' ? 'unsafe_path' : 'missing_receipt'] };
138
+ const status = proof.taskText.split('\n').find((line) => /^\s*(?:\*\*)?Status(?:\*\*)?\s*:/i.test(line));
139
+ if (!status || !/\bdone\b/i.test(status)) failures.push('task_status');
140
+ failures.push(...canonicalFailures(proof.body, task, runtimeContext, policy));
141
+ if (proof.source === 'separate') {
142
+ failures.push(...separateMetadataFailures(proof.body, taskPath, proof.taskText));
143
+ if (proof.legacyBody !== null
144
+ && /^\s*Verification:\s*PASS\s*$/m.test(proof.legacyBody)
145
+ && identity(proof.legacyBody) !== identity(proof.body)) failures.push('receipt_conflict');
146
+ }
147
+ const completedAt = task && task.completed_at;
148
+ if (typeof completedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}T/.test(completedAt) || Number.isNaN(Date.parse(completedAt))) failures.push('completed_at');
149
+ return { ...proof, failures: [...new Set(failures)] };
150
+ }
151
+
152
+ function checkFeatureReceipt(featureDir, runtimeContext, policy) {
153
+ const receipt = safeRead(featureDir, 'feature-receipt.md');
154
+ if (receipt.status !== 'ok') return { status: receipt.status, failures: [receipt.status === 'unsafe' ? 'unsafe_path' : 'missing_receipt'] };
155
+ const body = receipt.bytes.toString('utf8');
156
+ const failures = canonicalFailures(body, {}, runtimeContext, policy);
157
+ if (fieldValues(body, 'Feature').length !== 1 || fieldValues(body, 'Feature')[0] !== path.basename(featureDir)) failures.push('feature_identity');
158
+ if (!concrete(fieldValues(body, 'Expected')[0]) || !concrete(fieldValues(body, 'Observed')[0])) failures.push('expected_observed');
159
+ return { status: 'ok', source: 'feature', body, receiptBytes: receipt.bytes, receiptFile: receipt, failures: [...new Set(failures)] };
160
+ }
161
+
162
+ module.exports = { canonicalTaskReceiptPath, checkFeatureReceipt, checkTaskReceipt, evidenceBody, readTaskProof, safeRead };