@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
@@ -3,305 +3,54 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const STATE = require('./completion-authority-state.cjs');
6
+ const RECEIPT = require('../scripts/spec-receipt.cjs');
7
+ const RESOLVER = require('../scripts/spec-resolver.cjs');
6
8
 
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
- }
9
+ const scripts = path.join(__dirname, '..', 'scripts');
15
10
 
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; }
11
+ function runtimeModule(root, name) {
12
+ const target = path.join(root, name);
13
+ const stat = fs.lstatSync(target);
14
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`runtime dependency must be a regular non-symlink file: ${name}`);
15
+ const canonicalRoot = fs.realpathSync(root);
16
+ const canonical = fs.realpathSync(target);
17
+ const relative = path.relative(canonicalRoot, canonical);
18
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`runtime dependency escaped scripts root: ${name}`);
19
+ return require(canonical);
25
20
  }
26
21
 
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
- }
22
+ const FINAL_STATE = runtimeModule(scripts, 'spec-final-state.cjs');
58
23
 
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 };
24
+ function explicitTarget(payload) {
25
+ return typeof RESOLVER.extractExplicitTarget === 'function' ? RESOLVER.extractExplicitTarget(payload) : null;
81
26
  }
82
27
 
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;
28
+ function resolveCandidate({ resolver = RESOLVER, projectRoot, runtime, payload }) {
29
+ const target = typeof resolver.extractExplicitTarget === 'function' ? resolver.extractExplicitTarget(payload) : explicitTarget(payload);
30
+ if (typeof resolver.resolvePersistedSpec === 'function') return resolver.resolvePersistedSpec({ projectRoot, runtime, target });
31
+ if (target) return resolver.resolveActiveSpec({ projectRoot, runtime, explicitFeature: target.explicitFeature, explicitPath: target.explicitPath });
99
32
  const candidates = resolver.findAllSpecCandidates(projectRoot, runtime);
100
33
  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;
34
+ return candidates[0] || null;
161
35
  }
162
36
 
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) || []) }),
37
+ function dependencies(overrides = {}) {
38
+ if (!overrides.grounder) runtimeModule(scripts, 'spec-semantic-model.cjs');
39
+ const grounder = overrides.grounder || runtimeModule(scripts, 'spec-ground.cjs');
40
+ const validator = overrides.validator || runtimeModule(scripts, 'validate-spec-output.cjs');
41
+ return {
42
+ validator,
43
+ grounder,
44
+ semanticAuthority: overrides.semanticAuthority || require('./semantic-review-authority.cjs'),
298
45
  };
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
46
  }
306
47
 
307
- module.exports = { evaluateCloseout, resolveCandidate, observePolicyBaseline };
48
+ module.exports = FINAL_STATE.createAuthority({
49
+ state: STATE,
50
+ resolveCandidate,
51
+ dependencies,
52
+ receipts: {
53
+ task: (featureDir, taskPath, task, context, policy) => RECEIPT.checkTaskReceipt(featureDir, taskPath, task, context, policy),
54
+ feature: (featureDir, context, policy) => RECEIPT.checkFeatureReceipt(featureDir, context, policy),
55
+ },
56
+ });
@@ -10,7 +10,8 @@ const NONCE_RE = /^[a-f0-9]{24}$/;
10
10
  const HASH_RE = /^[a-f0-9]{64}$/;
11
11
  const TTL_MS = 5 * 60 * 1000;
12
12
  const KEY_BYTES = 32;
13
- const SCHEMA_VERSION = 2;
13
+ const SCHEMA_VERSION = 3;
14
+ const LEGACY_SCHEMA_VERSION = 2;
14
15
  const EPHEMERAL_FILE_RE = /^(pending|grant)-([a-f0-9]{24})\.json$/;
15
16
  const BASELINE_FILE_RE = /^baseline-([a-f0-9]{64})\.json$/;
16
17
  const FLOOR_FILE = 'policy-floor.json';
@@ -222,7 +223,7 @@ function validateEphemeral(record, kind, nonce, now, key) {
222
223
  : ['schema_version', 'kind', 'nonce', 'binding', 'issued_at', 'expires_at', 'approved_at', 'mac'];
223
224
  if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed authority record' };
224
225
  if (!macMatches(record, key)) return { ok: false, reason: 'authority record MAC is invalid' };
225
- if (record.schema_version !== SCHEMA_VERSION || record.kind !== kind || record.nonce !== nonce || !NONCE_RE.test(nonce)) return { ok: false, reason: 'authority record identity is invalid' };
226
+ if (![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== kind || record.nonce !== nonce || !NONCE_RE.test(nonce)) return { ok: false, reason: 'authority record identity is invalid' };
226
227
  if (!plain(record.binding) || typeof record.binding.session_id !== 'string' || !record.binding.session_id.trim()) return { ok: false, reason: 'authority record binding is invalid' };
227
228
  if (!validTimestamp(record.issued_at) || !Number.isFinite(record.expires_at) || record.expires_at <= Date.parse(record.issued_at)) return { ok: false, reason: 'authority record timestamps are invalid' };
228
229
  if (kind === 'grant' && !validTimestamp(record.approved_at)) return { ok: false, reason: 'authority grant approval timestamp is invalid' };
@@ -234,7 +235,7 @@ function validateBaseline(record, filename, projectRoot, key) {
234
235
  const expected = ['schema_version', 'kind', 'identity', 'policy', 'issued_at', 'updated_at', 'mac'];
235
236
  if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed policy baseline' };
236
237
  if (!macMatches(record, key)) return { ok: false, reason: 'policy baseline MAC is invalid' };
237
- if (record.schema_version !== SCHEMA_VERSION || record.kind !== 'baseline' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) return { ok: false, reason: 'policy baseline identity or timestamps are invalid' };
238
+ if (![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== 'baseline' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) return { ok: false, reason: 'policy baseline identity or timestamps are invalid' };
238
239
  let identity;
239
240
  try { identity = normalizeIdentity(record.identity, { allowMissingSpec: true }); } catch (error) { return { ok: false, reason: error.message }; }
240
241
  if (filename !== `baseline-${identityDigest(identity)}.json` || identity.project_root !== canonicalProjectRoot(projectRoot)) return { ok: false, reason: 'policy baseline namespace is invalid' };
@@ -246,7 +247,7 @@ function validatePolicyFloor(record, filename, key) {
246
247
  const expected = ['schema_version', 'kind', 'policy', 'issued_at', 'updated_at', 'mac'];
247
248
  if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed project policy floor' };
248
249
  if (!macMatches(record, key)) return { ok: false, reason: 'project policy floor MAC is invalid' };
249
- if (filename !== FLOOR_FILE || record.schema_version !== SCHEMA_VERSION || record.kind !== 'policy-floor' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) {
250
+ if (filename !== FLOOR_FILE || ![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== 'policy-floor' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) {
250
251
  return { ok: false, reason: 'project policy floor identity or timestamps are invalid' };
251
252
  }
252
253
  if (!plain(record.policy)) return { ok: false, reason: 'project policy floor policy is malformed' };
@@ -344,10 +345,6 @@ function readState(projectRoot, now = Date.now()) {
344
345
  }
345
346
  if (!state.floor) {
346
347
  state.floorMissing = true;
347
- if (state.pending || state.grant || state.baselines.length > 0) {
348
- state.malformed = true;
349
- state.reason ||= 'project policy floor is missing';
350
- }
351
348
  }
352
349
  return state;
353
350
  }
@@ -509,6 +506,11 @@ module.exports = {
509
506
  FLOOR_FILE,
510
507
  stableStringify,
511
508
  digest,
509
+ canonicalProjectRoot,
510
+ atomicWrite,
511
+ readKey,
512
+ recordMac,
513
+ macMatches,
512
514
  userStateRoot,
513
515
  stateDir,
514
516
  clearState,
@@ -88,6 +88,7 @@ function approve(payload) {
88
88
  }
89
89
 
90
90
  function stop(payload) {
91
+ if (payload.stop_hook_active === true) return;
91
92
  const root = projectRoot(payload);
92
93
  const runtime = readRuntime(root);
93
94
  const resolverPath = path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs');
@@ -120,6 +121,7 @@ function stop(payload) {
120
121
  }
121
122
  return;
122
123
  }
124
+ if (result.active === false) return;
123
125
  if (!result.ok) {
124
126
  STATE.clearState(root);
125
127
  emitBlock(`Completion authority: ${result.reason}`);
@@ -135,13 +137,17 @@ let mode = process.argv[2] || null;
135
137
  try {
136
138
  const payload = readPayload();
137
139
  mode = mode || (payload.hook_event_name === 'UserPromptSubmit' ? '--approve' : '--stop');
138
- const dependencyError = loadAuthority();
139
- if (dependencyError) {
140
- if (mode === '--approve') rejectApproval(`authority dependency unavailable (${dependencyError.message})`);
141
- else emitBlock(`Completion authority unavailable: ${dependencyError.message}. Repair the installed authority before completing tasks`);
142
- } else if (mode === '--approve') approve(payload);
143
- else if (mode === '--stop') stop(payload);
144
- else throw new Error('completion authority mode is required');
140
+ if (mode === '--stop' && payload.stop_hook_active === true) {
141
+ // Host loop prevention is always silent, including when dependencies are unavailable.
142
+ } else {
143
+ const dependencyError = loadAuthority();
144
+ if (dependencyError) {
145
+ if (mode === '--approve') rejectApproval(`authority dependency unavailable (${dependencyError.message})`);
146
+ else emitBlock(`Completion authority unavailable: ${dependencyError.message}. Repair the installed authority before completing tasks`);
147
+ } else if (mode === '--approve') approve(payload);
148
+ else if (mode === '--stop') stop(payload);
149
+ else throw new Error('completion authority mode is required');
150
+ }
145
151
  } catch (error) {
146
152
  if (mode === '--approve') rejectApproval(`authority failed safely (${error.message})`);
147
153
  else emitBlock(`Completion authority controlled failure: ${error.message}. Completion is blocked until the authority is repaired`);
@@ -87,8 +87,11 @@ try {
87
87
  } catch (e) {
88
88
  // Ghi log lỗi ẩn danh nếu sập hook
89
89
  try {
90
- const fs = require('fs'), p = require('path');
91
- const d = p.join(__dirname, '.logs');
90
+ const fs = require('fs'), p = require('path'), os = require('os');
91
+ const isSourceTree = __dirname.includes(`${p.sep}packages${p.sep}spec${p.sep}src`);
92
+ const d = isSourceTree
93
+ ? p.join(os.tmpdir(), 'cafekit-hook-logs', 'claude-docs-sync')
94
+ : p.join(__dirname, '.logs');
92
95
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
93
96
  fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
94
97
  JSON.stringify({ ts: new Date().toISOString(), hook: 'docs-sync', status: 'crash', error: e.message }) + '\n');