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

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 (49) hide show
  1. package/bin/__tests__/codex-hooks.test.js +355 -12
  2. package/bin/__tests__/codex-native.test.js +1 -1
  3. package/bin/__tests__/develop-contract.test.js +1374 -78
  4. package/bin/__tests__/installer-safety.test.js +43 -29
  5. package/bin/__tests__/opencode-spec-gate.test.js +791 -0
  6. package/bin/__tests__/package-inventory.test.js +157 -0
  7. package/bin/__tests__/validator-grounding.test.js +707 -2
  8. package/package.json +1 -1
  9. package/src/claude/agents/spec-maker.md +107 -200
  10. package/src/claude/hooks/__tests__/completion-authority.test.js +720 -0
  11. package/src/claude/hooks/__tests__/spec-gate.test.js +511 -38
  12. package/src/claude/hooks/completion-authority-check.cjs +307 -0
  13. package/src/claude/hooks/completion-authority-state.cjs +525 -0
  14. package/src/claude/hooks/completion-authority.cjs +148 -0
  15. package/src/claude/hooks/session.cjs +33 -19
  16. package/src/claude/hooks/spec-gate.cjs +173 -61
  17. package/src/claude/hooks/spec-state.cjs +104 -45
  18. package/src/claude/migration-manifest.json +6 -1
  19. package/src/claude/scripts/provenance.cjs +315 -0
  20. package/src/claude/scripts/spec-resolver.cjs +450 -0
  21. package/src/claude/scripts/spec-scaffold.cjs +716 -41
  22. package/src/claude/scripts/validate-spec-output.cjs +355 -53
  23. package/src/claude/scripts/workflow-policy.cjs +1529 -162
  24. package/src/claude/settings/settings.json +8 -0
  25. package/src/claude/skills/code-review/SKILL.md +85 -66
  26. package/src/claude/skills/develop/SKILL.md +153 -285
  27. package/src/claude/skills/develop/references/quality-gate.md +116 -148
  28. package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
  29. package/src/claude/skills/specs/SKILL.md +170 -381
  30. package/src/claude/skills/specs/rules/task-scoring-rubric.md +45 -46
  31. package/src/claude/skills/specs/rules/tasks-generation.md +49 -20
  32. package/src/claude/skills/specs/templates/spec-state.json +23 -2
  33. package/src/claude/skills/specs/templates/task.md +20 -0
  34. package/src/claude/skills/sync/SKILL.md +15 -8
  35. package/src/claude/skills/sync/references/sync-protocols.md +4 -4
  36. package/src/claude/skills/test/SKILL.md +141 -167
  37. package/src/codex/hooks/completion-authority-check.cjs +260 -0
  38. package/src/codex/hooks/completion-authority-state.cjs +525 -0
  39. package/src/codex/hooks/completion-authority.cjs +100 -0
  40. package/src/codex/hooks/lib/hook-context.cjs +42 -2
  41. package/src/codex/hooks/lib/spec-receipt.cjs +76 -43
  42. package/src/codex/hooks/lib/spec-utils.cjs +53 -27
  43. package/src/codex/hooks/session.cjs +2 -0
  44. package/src/codex/hooks/spec-gate.cjs +132 -20
  45. package/src/codex/hooks/spec-state.cjs +56 -10
  46. package/src/codex/hooks.json +10 -0
  47. package/src/opencode/AGENTS.md +4 -2
  48. package/src/opencode/plugins/spec-gate.ts +296 -122
  49. package/src/opencode/plugins/spec-state.ts +183 -116
@@ -0,0 +1,307 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const STATE = require('./completion-authority-state.cjs');
6
+
7
+ function plain(value) {
8
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
9
+ }
10
+
11
+ function inside(root, target) {
12
+ const relative = path.relative(path.resolve(root), path.resolve(target));
13
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
14
+ }
15
+
16
+ function readBoundFile(root, target) {
17
+ const requested = path.isAbsolute(target) ? target : path.resolve(root, target);
18
+ if (!inside(root, requested)) return null;
19
+ try {
20
+ const canonicalRoot = fs.realpathSync(root);
21
+ const canonical = fs.realpathSync(requested);
22
+ if (!inside(canonicalRoot, canonical) || !fs.statSync(canonical).isFile()) return null;
23
+ return fs.readFileSync(canonical);
24
+ } catch { return null; }
25
+ }
26
+
27
+ function readTask(featureDir, taskPath) {
28
+ const target = path.resolve(featureDir, taskPath);
29
+ if (!inside(featureDir, target)) return null;
30
+ const bytes = readBoundFile(featureDir, target);
31
+ if (!bytes) return null;
32
+ return { bytes, text: bytes.toString('utf8'), path: fs.realpathSync(target) };
33
+ }
34
+
35
+ function evidenceBody(text, policy) {
36
+ const lines = text.split('\n');
37
+ let start = -1;
38
+ let level = 0;
39
+ for (let index = 0; index < lines.length; index += 1) {
40
+ const match = lines[index].match(/^#{2,3}\s+(Evidence|Task Test Plan & Verification Evidence|Verification & Evidence)\b/);
41
+ if (match) {
42
+ start = index + 1;
43
+ level = lines[index].match(/^#+/)[0].length;
44
+ break;
45
+ }
46
+ }
47
+ if (start < 0) return null;
48
+ let end = lines.length;
49
+ for (let index = start; index < lines.length; index += 1) {
50
+ const heading = lines[index].match(/^(#{1,6})\s+/);
51
+ if (heading && heading[1].length <= level && !(policy.isTapMetadataHeading?.(lines[index]))) {
52
+ end = index;
53
+ break;
54
+ }
55
+ }
56
+ return lines.slice(start, end).join('\n');
57
+ }
58
+
59
+ function receiptFailures(featureDir, taskPath, task, runtimeContext, policy) {
60
+ const file = readTask(featureDir, taskPath);
61
+ if (!file) return { failures: ['a'], body: null, file: null };
62
+ const failures = [];
63
+ const status = file.text.split('\n').find((line) => /^\s*(?:\*\*)?Status(?:\*\*)?\s*:/i.test(line));
64
+ if (!status || !/\bdone\b/i.test(status)) failures.push('a');
65
+ const body = evidenceBody(file.text, policy);
66
+ if (!body || !/^\s*Verification:\s*PASS\s*$/m.test(body) || /\{\{[^}]+\}\}/.test(body)) {
67
+ failures.push('c');
68
+ } else {
69
+ const options = policy.receiptValidatorOptions(task, { runtimeContext, requireProvenanceBinding: true });
70
+ const map = { verification_state: 'c', command: 'e', exit_result: 'f', provenance: 'g', artifact_hash: 'h', artifact_declaration: 'h', placeholder: 'c', shared_validator: 'c', validator_unavailable: 'c' };
71
+ for (const failure of policy.validateCanonicalReceipt(body, options)) {
72
+ const letter = map[failure];
73
+ if (letter && !failures.includes(letter)) failures.push(letter);
74
+ }
75
+ }
76
+ const completedAt = task?.completed_at;
77
+ if (typeof completedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}T/.test(completedAt) || Number.isNaN(Date.parse(completedAt))) {
78
+ failures.push('d');
79
+ }
80
+ return { failures, body, file };
81
+ }
82
+
83
+ function artifactReferences(body) {
84
+ if (typeof body !== 'string') return [];
85
+ const references = [];
86
+ for (const line of body.split('\n')) {
87
+ if (!/^\s*Artifacts?\s*:/i.test(line) && !/^\s*Artifact\s+produced\b/i.test(line)) continue;
88
+ const hashes = [...line.matchAll(/\b(?:artifact[_-])?sha-?256\s*:\s*([^\s),;`]+)/gi)].map((match) => match[1]);
89
+ const raw = line.replace(/^\s*Artifacts?\s*:\s*/i, '').replace(/^\s*Artifact\s+produced\s*:?[ \t]*/i, '').replace(/\b(?:artifact[_-])?sha-?256\s*:\s*[^\s),;`]+/gi, '').replace(/[()[\]{}]/g, ' ').trim();
90
+ const paths = raw.split(/\s*(?:\+|,|;|\band\b)\s*/i).map((value) => value.trim()).filter(Boolean);
91
+ paths.forEach((artifactPath, index) => references.push({ path: artifactPath, hash: hashes[index] || null }));
92
+ }
93
+ return references;
94
+ }
95
+
96
+ function resolveCandidate({ resolver, projectRoot, runtime, payload }) {
97
+ const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
98
+ const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
99
+ const candidates = resolver.findAllSpecCandidates(projectRoot, runtime);
100
+ if (candidates.length > 1) return { error: 'multiple_persisted', candidates: candidates.map((candidate) => candidate.featureName) };
101
+ if (candidates.length === 0) {
102
+ if (explicitFeature || explicitPath) return { error: 'explicit_not_found', explicitFeature, explicitPath, reason: 'no globally persisted spec matches the explicit target' };
103
+ return null;
104
+ }
105
+ if (!explicitFeature && !explicitPath) return candidates[0];
106
+ const targeted = resolver.resolveActiveSpec({ projectRoot, runtime, explicitFeature, explicitPath });
107
+ if (!targeted || targeted.error) {
108
+ return {
109
+ error: 'explicit_mismatch',
110
+ explicitFeature,
111
+ explicitPath,
112
+ reason: targeted?.reason || 'explicit target does not resolve to the persisted candidate',
113
+ };
114
+ }
115
+ if (targeted.featureName !== candidates[0].featureName || path.resolve(targeted.specFile) !== path.resolve(candidates[0].specFile)) {
116
+ return {
117
+ error: 'explicit_mismatch',
118
+ explicitFeature,
119
+ explicitPath,
120
+ reason: 'explicit target does not match the sole globally persisted candidate',
121
+ };
122
+ }
123
+ return candidates[0];
124
+ }
125
+
126
+ function policyRank(policy, lane) {
127
+ const lanes = Array.isArray(policy.LANES) ? policy.LANES : ['Direct', 'Standard', 'Critical'];
128
+ return lanes.indexOf(lane);
129
+ }
130
+
131
+ function policyMonotonicFailures(policy, baseline, current) {
132
+ const failures = [];
133
+ if (baseline.version !== current.version) failures.push('workflow_policy.version changed');
134
+ if (policyRank(policy, current.lane) < policyRank(policy, baseline.lane)) failures.push(`workflow_policy.lane downgraded from ${baseline.lane} to ${current.lane}`);
135
+ if (policyRank(policy, current.automatic_lane) < policyRank(policy, baseline.automatic_lane)) failures.push(`workflow_policy.automatic_lane downgraded from ${baseline.automatic_lane} to ${current.automatic_lane}`);
136
+ const currentRisks = new Set(current.risks);
137
+ for (const risk of baseline.risks) if (!currentRisks.has(risk)) failures.push(`workflow_policy risk ${risk} was removed`);
138
+ const currentObligations = new Set(current.proof_obligations);
139
+ for (const obligation of baseline.proof_obligations) if (!currentObligations.has(obligation)) failures.push(`workflow_policy obligation ${obligation} was removed`);
140
+ return failures;
141
+ }
142
+
143
+ function mergePolicyFloor(policy, floor, current) {
144
+ const merged = { ...current };
145
+ if (policyRank(policy, floor.lane) > policyRank(policy, current.lane)) merged.lane = floor.lane;
146
+ if (policyRank(policy, floor.automatic_lane) > policyRank(policy, current.automatic_lane)) merged.automatic_lane = floor.automatic_lane;
147
+ merged.risks = [...new Set([...current.risks, ...floor.risks])];
148
+ const proofObligations = [];
149
+ const actorNeeds = [];
150
+ for (const source of [current, floor]) {
151
+ for (let index = 0; index < source.proof_obligations.length; index += 1) {
152
+ const obligation = source.proof_obligations[index];
153
+ if (proofObligations.includes(obligation)) continue;
154
+ proofObligations.push(obligation);
155
+ actorNeeds.push(source.actor_needs[index]);
156
+ }
157
+ }
158
+ merged.proof_obligations = proofObligations;
159
+ merged.actor_needs = actorNeeds;
160
+ return merged;
161
+ }
162
+
163
+ function observePolicyBaseline({ STATE: state, policy, projectRoot, candidate }) {
164
+ if (!Object.prototype.hasOwnProperty.call(candidate.spec, 'workflow_policy')) {
165
+ return { ok: false, reason: 'workflow_policy is missing for persisted spec' };
166
+ }
167
+ const current = candidate.spec.workflow_policy;
168
+ const validation = policy.validateWorkflowPolicySnapshot(current);
169
+ if (!validation.valid) return { ok: false, reason: `workflow_policy is malformed: ${validation.errors.join('; ')}` };
170
+ let identity;
171
+ try {
172
+ identity = {
173
+ project_root: fs.realpathSync(projectRoot),
174
+ spec_file: fs.realpathSync(candidate.specFile),
175
+ feature_name: candidate.featureName,
176
+ };
177
+ state.ensureKey();
178
+ let authorityState = state.readState(projectRoot);
179
+ if (authorityState.malformed || authorityState.keyError || authorityState.keyMissing) {
180
+ return { ok: false, reason: authorityState.reason || 'completion authority state is unavailable' };
181
+ }
182
+ if (!authorityState.floor) {
183
+ if (authorityState.baselines.length > 0 || authorityState.pending || authorityState.grant) {
184
+ return { ok: false, reason: 'project policy floor is missing for persisted authority state' };
185
+ }
186
+ state.writePolicyFloor(projectRoot, current);
187
+ authorityState = state.readState(projectRoot);
188
+ if (authorityState.malformed || authorityState.keyError || authorityState.keyMissing || !authorityState.floor) {
189
+ return { ok: false, reason: authorityState.reason || 'project policy floor could not be persisted' };
190
+ }
191
+ } else {
192
+ const floorValidation = policy.validateWorkflowPolicySnapshot(authorityState.floor.policy);
193
+ if (!floorValidation.valid) return { ok: false, reason: `persisted project policy floor is malformed: ${floorValidation.errors.join('; ')}` };
194
+ const floorFailures = policyMonotonicFailures(policy, authorityState.floor.policy, current);
195
+ if (floorFailures.length) return { ok: false, reason: `project policy floor is monotonic and cannot be downgraded (${floorFailures.join('; ')})` };
196
+ const mergedFloor = mergePolicyFloor(policy, authorityState.floor.policy, current);
197
+ if (state.digest(authorityState.floor.policy) !== state.digest(mergedFloor)) state.writePolicyFloor(projectRoot, mergedFloor);
198
+ }
199
+ const baseline = state.readBaseline(projectRoot, identity);
200
+ if (!baseline) {
201
+ state.writeBaseline(projectRoot, identity, current);
202
+ return { ok: true };
203
+ }
204
+ const baselineValidation = policy.validateWorkflowPolicySnapshot(baseline.policy);
205
+ if (!baselineValidation.valid) return { ok: false, reason: `persisted workflow policy baseline is malformed: ${baselineValidation.errors.join('; ')}` };
206
+ const failures = policyMonotonicFailures(policy, baseline.policy, current);
207
+ if (failures.length) return { ok: false, reason: `workflow_policy baseline is monotonic and cannot be downgraded (${failures.join('; ')})` };
208
+ if (state.digest(baseline.policy) !== state.digest(current)) state.writeBaseline(projectRoot, identity, current);
209
+ return { ok: true };
210
+ } catch (error) {
211
+ return { ok: false, reason: `workflow_policy baseline unavailable: ${error.message}` };
212
+ }
213
+ }
214
+
215
+ function evaluateCloseout({ resolver, policy, projectRoot, runtime, payload }) {
216
+ const candidate = resolveCandidate({ resolver, projectRoot, runtime, payload });
217
+ if (!candidate) return { ok: true, candidate: null };
218
+ if (candidate.error) {
219
+ const detail = Array.isArray(candidate.candidates) ? candidate.candidates.join(', ') : (candidate.reason || candidate.error);
220
+ return { ok: false, candidate, reason: `${candidate.error}: ${detail}` };
221
+ }
222
+ const sessionId = payload.session_id;
223
+ if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: false, candidate, reason: 'trusted host session ID is unavailable' };
224
+ const baseline = observePolicyBaseline({ STATE, policy, projectRoot, candidate });
225
+ if (!baseline.ok) return { ok: false, candidate, reason: baseline.reason };
226
+ if (runtime.spec?.completion_gate !== undefined && runtime.spec.completion_gate !== true) return { ok: false, candidate, reason: 'runtime.spec.completion_gate is worker-writable and cannot authorize completion' };
227
+ const status = candidate.spec.status;
228
+ if (!['in_progress', 'in-progress', 'completed', 'complete'].includes(status)) return { ok: false, candidate, reason: `spec status is invalid: ${String(status)}` };
229
+ const registry = candidate.spec.task_registry;
230
+ if (!plain(registry) || Object.keys(registry).length === 0) return { ok: false, candidate, reason: 'task_registry is missing or empty' };
231
+
232
+ let runtimeContext;
233
+ try {
234
+ runtimeContext = policy.deriveRuntimeContext({
235
+ projectRoot,
236
+ specsRoot: candidate.specsDir,
237
+ specFile: candidate.specFile,
238
+ featureName: candidate.featureName,
239
+ runtimeSession: sessionId,
240
+ });
241
+ } catch (error) {
242
+ return { ok: false, candidate, reason: `runtime provenance unavailable: ${error.message}` };
243
+ }
244
+ if (!policy.isTrustedRuntimeContext(runtimeContext)) return { ok: false, candidate, reason: 'fresh trusted runtime Base/Head/context is unavailable' };
245
+
246
+ const featureDir = path.join(candidate.specsDir, candidate.featureName);
247
+ const receiptBodies = new Map();
248
+ const taskFiles = [];
249
+ const failures = [];
250
+ for (const [taskPath, task] of Object.entries(registry)) {
251
+ if (!plain(task) || task.status !== 'done') {
252
+ failures.push(`${taskPath}: status is not done`);
253
+ continue;
254
+ }
255
+ if (policy.isStaleFlashDone(task)) failures.push(`${taskPath}: FLASH_UNVERIFIED`);
256
+ const receipt = receiptFailures(featureDir, taskPath, task, runtimeContext, policy);
257
+ if (receipt.failures.length) failures.push(`${taskPath}: checks ${receipt.failures.join(',')}`);
258
+ if (receipt.body !== null) receiptBodies.set(taskPath, receipt.body);
259
+ if (receipt.file) taskFiles.push({ path: taskPath, digest: cryptoDigest(receipt.file.bytes) });
260
+ }
261
+ if (failures.length) return { ok: false, candidate, reason: `technical completion proof is incomplete (${failures.slice(0, 4).join('; ')})` };
262
+
263
+ const firstTask = Object.keys(registry).find((taskPath) => receiptBodies.has(taskPath));
264
+ let completion;
265
+ try {
266
+ completion = policy.completionDecisionForSpec(candidate.spec, {
267
+ runtimeContext,
268
+ executionReceipt: firstTask ? receiptBodies.get(firstTask) : null,
269
+ taskContext: firstTask ? registry[firstTask] : null,
270
+ });
271
+ } catch (error) {
272
+ return { ok: false, candidate, reason: `workflow completion proof could not be evaluated: ${error.message}` };
273
+ }
274
+ if (completion.completion !== 'complete' && completion.completion !== 'not_applicable') {
275
+ return { ok: false, candidate, reason: completion.blocker || 'workflow completion proof is incomplete' };
276
+ }
277
+
278
+ const artifactRefs = [...receiptBodies.values()].flatMap(artifactReferences).sort((left, right) => `${left.path}:${left.hash}`.localeCompare(`${right.path}:${right.hash}`));
279
+ const artifactFiles = artifactRefs.map((reference) => ({
280
+ ...reference,
281
+ digest: cryptoDigest(readBoundFile(projectRoot, reference.path) || Buffer.from('<missing>')),
282
+ }));
283
+ const specBytes = readBoundFile(projectRoot, candidate.specFile);
284
+ const binding = {
285
+ project_root: runtimeContext.project_root,
286
+ platform: process.platform,
287
+ session_id: sessionId,
288
+ feature_name: candidate.featureName,
289
+ spec_file: candidate.specFile,
290
+ runtime: { base: runtimeContext.base, head: runtimeContext.head, context_id: runtimeContext.context_id },
291
+ spec_digest: STATE.digest(candidate.spec),
292
+ spec_bytes_digest: cryptoDigest(specBytes || Buffer.from('<missing>')),
293
+ policy_digest: STATE.digest(candidate.spec.workflow_policy ?? null),
294
+ task_registry_digest: STATE.digest(registry),
295
+ task_digest: STATE.digest(taskFiles),
296
+ artifact_digest: STATE.digest(artifactFiles),
297
+ provenance_digest: STATE.digest({ runtime: runtimeContext, receipts: [...receiptBodies.values()].map((body) => body.match(/^\s*(?:Base|Head|base_sha|head_sha)\s*:.*$/gim) || []) }),
298
+ };
299
+ binding.binding_digest = STATE.digest(binding);
300
+ return { ok: true, candidate, runtimeContext, binding };
301
+ }
302
+
303
+ function cryptoDigest(bytes) {
304
+ return require('crypto').createHash('sha256').update(bytes).digest('hex');
305
+ }
306
+
307
+ module.exports = { evaluateCloseout, resolveCandidate, observePolicyBaseline };