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

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__/change-firewall.semantic-firewall.test.js +2134 -0
  2. package/bin/__tests__/codex-hooks.test.js +531 -29
  3. package/bin/__tests__/codex-native.test.js +228 -7
  4. package/bin/__tests__/develop-contract.test.js +653 -185
  5. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
  6. package/bin/__tests__/installer-safety.test.js +39 -0
  7. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  8. package/bin/__tests__/package-inventory.test.js +705 -13
  9. package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
  10. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
  11. package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
  12. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
  13. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
  14. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  15. package/bin/__tests__/validator-grounding.test.js +2013 -221
  16. package/bin/lib/managed-writer.js +12 -3
  17. package/bin/phases/codex-runtime.js +17 -1
  18. package/package.json +5 -2
  19. package/src/claude/CLAUDE.md +6 -0
  20. package/src/claude/agents/code-auditor.md +20 -0
  21. package/src/claude/agents/spec-maker.md +143 -99
  22. package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
  23. package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
  24. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  25. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  26. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  27. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  28. package/src/claude/hooks/completion-authority.cjs +13 -7
  29. package/src/claude/hooks/docs-sync.cjs +5 -2
  30. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  31. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  32. package/src/claude/hooks/privacy-block.cjs +99 -28
  33. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  34. package/src/claude/hooks/spec-gate.cjs +47 -117
  35. package/src/claude/hooks/spec-state.cjs +8 -2
  36. package/src/claude/migration-manifest.json +9 -1
  37. package/src/claude/rules/state-sync.md +52 -21
  38. package/src/claude/rules/workflow.md +17 -9
  39. package/src/claude/scripts/change-firewall.cjs +1748 -0
  40. package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
  41. package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
  42. package/src/claude/scripts/spec-final-state.cjs +290 -0
  43. package/src/claude/scripts/spec-ground.cjs +571 -173
  44. package/src/claude/scripts/spec-readiness.cjs +425 -0
  45. package/src/claude/scripts/spec-receipt.cjs +162 -0
  46. package/src/claude/scripts/spec-resolver.cjs +67 -16
  47. package/src/claude/scripts/spec-scaffold.cjs +1594 -139
  48. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  49. package/src/claude/scripts/validate-spec-output.cjs +3110 -218
  50. package/src/claude/scripts/workflow-policy.cjs +501 -183
  51. package/src/claude/settings/settings.json +4 -0
  52. package/src/claude/skills/code-review/SKILL.md +12 -3
  53. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  54. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  55. package/src/claude/skills/develop/SKILL.md +47 -60
  56. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  57. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  58. package/src/claude/skills/specs/SKILL.md +183 -177
  59. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  60. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  61. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  62. package/src/claude/skills/specs/references/review.md +214 -262
  63. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  64. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  65. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  66. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  67. package/src/claude/skills/specs/rules/design-review.md +42 -97
  68. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  69. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  70. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  71. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  72. package/src/claude/skills/specs/templates/design.md +49 -263
  73. package/src/claude/skills/specs/templates/requirements.md +26 -36
  74. package/src/claude/skills/specs/templates/research.md +9 -98
  75. package/src/claude/skills/specs/templates/spec-state.json +35 -85
  76. package/src/claude/skills/specs/templates/task.md +30 -102
  77. package/src/claude/skills/sync/SKILL.md +33 -10
  78. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  79. package/src/claude/skills/test/SKILL.md +17 -7
  80. package/src/codex/AGENTS.md +6 -0
  81. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  82. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  83. package/src/codex/hooks/completion-authority.cjs +13 -7
  84. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  85. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  86. package/src/codex/hooks/lib/privacy-state.cjs +61 -38
  87. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  88. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  89. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  90. package/src/codex/hooks/privacy-approval.cjs +6 -2
  91. package/src/codex/hooks/privacy-block.cjs +94 -15
  92. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  93. package/src/codex/hooks/spec-gate.cjs +49 -34
  94. package/src/codex/hooks/spec-state.cjs +10 -3
  95. package/src/codex/hooks/state.cjs +2 -0
  96. package/src/codex/hooks.json +6 -1
  97. package/src/codex/rules/state-sync.md +47 -27
  98. package/src/common/AGENTS.md +1 -1
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const STATE = require('./completion-authority-state.cjs');
8
+ const PATH_SAFETY = require('./lib/runtime-path-safety.cjs');
9
+
10
+ const SCHEMA_VERSION = 1;
11
+ const MARKER = 'CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ';
12
+ const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
13
+ const REVIEWER_ROLE_ALLOWLIST = new Set(['code-auditor', 'code_auditor']);
14
+ const RECORD_FIELDS = ['schema_version', 'kind', 'identity', 'semantic_digest', 'event', 'verdict', 'host_session_id', 'reviewer_agent_id', 'reviewer_agent_type', 'issued_at', 'mac'];
15
+
16
+ function plain(value) {
17
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
18
+ }
19
+
20
+ function exactKeys(value, fields) {
21
+ return plain(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...fields].sort());
22
+ }
23
+
24
+ function inside(root, target) {
25
+ const relative = path.relative(root, target);
26
+ return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
27
+ }
28
+
29
+ function projectRoot(payload = {}) {
30
+ const installedRoot = path.resolve(__dirname, '..', '..');
31
+ if (fs.existsSync(path.join(installedRoot, '.claude', 'hooks', path.basename(__filename)))) {
32
+ try { return STATE.canonicalProjectRoot(installedRoot); } catch {}
33
+ }
34
+ for (const candidate of [process.env.CLAUDE_PROJECT_DIR, process.env.PROJECT_ROOT]) {
35
+ if (typeof candidate === 'string' && candidate.trim()) {
36
+ try { return STATE.canonicalProjectRoot(candidate); } catch { /* continue */ }
37
+ }
38
+ }
39
+ const cwd = typeof payload.cwd === 'string' && payload.cwd.trim() ? payload.cwd : process.cwd();
40
+ return STATE.canonicalProjectRoot(cwd);
41
+ }
42
+
43
+ function identityFor(root, specFile, featureName) {
44
+ if (typeof featureName !== 'string' || !featureName.trim() || featureName.includes('/') || featureName.includes('\\') || featureName === '.' || featureName === '..' || featureName.includes('..')) throw new Error('feature identity is malformed');
45
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(featureName)) throw new Error('feature identity is malformed');
46
+ const canonicalRoot = STATE.canonicalProjectRoot(root);
47
+ const canonicalSpec = fs.realpathSync(path.resolve(specFile));
48
+ if (!inside(canonicalRoot, canonicalSpec) || path.basename(canonicalSpec) !== 'spec.json') throw new Error('spec identity escapes the project');
49
+ const relative = path.relative(canonicalRoot, canonicalSpec);
50
+ const expected = path.join('specs', featureName, 'spec.json');
51
+ if (relative !== expected) throw new Error(`spec identity must be ${expected}, not ${relative}`);
52
+ const spec = JSON.parse(fs.readFileSync(canonicalSpec, 'utf8'));
53
+ if (spec.feature_name !== featureName) throw new Error('feature identity does not match spec.json');
54
+ return { project_root: canonicalRoot, spec_file: canonicalSpec, feature_name: featureName };
55
+ }
56
+
57
+ function recordPath(root, identity) {
58
+ const name = STATE.digest({ namespace: 'cafekit-semantic-review-observation-v1', identity });
59
+ return path.join(STATE.stateDir(root), 'semantic-reviews', `observation-${name}.json`);
60
+ }
61
+
62
+ function parseClaim(message) {
63
+ const lines = String(message || '').split(/\r?\n/).filter((line) => line.startsWith(MARKER));
64
+ if (lines.length === 0) return null;
65
+ if (lines.length !== 1) throw new Error('exactly one semantic review attestation marker is required');
66
+ const claim = JSON.parse(lines[0].slice(MARKER.length));
67
+ if (!exactKeys(claim, ['feature_name', 'spec_file', 'semantic_digest', 'verdict'])) throw new Error('semantic review claim fields are malformed');
68
+ if (claim.verdict !== 'PASS' || !SHA256_RE.test(claim.semantic_digest || '')) throw new Error('semantic review claim must contain literal PASS and a sha256 digest');
69
+ return claim;
70
+ }
71
+
72
+ function validatorFor(root) {
73
+ const installedRoot = path.join(root, '.claude');
74
+ const installedHook = path.join(installedRoot, 'hooks', path.basename(__filename));
75
+ if (fs.existsSync(installedHook)) {
76
+ const canonicalHook = PATH_SAFETY.canonicalRegularFile(root, installedHook, 'installed semantic hook');
77
+ if (canonicalHook !== fs.realpathSync(__filename)) throw new Error('installed semantic hook identity is invalid');
78
+ const validator = path.join(installedRoot, 'scripts', 'validate-spec-output.cjs');
79
+ PATH_SAFETY.verifyCommonJsClosure(installedRoot, validator);
80
+ return PATH_SAFETY.canonicalRegularFile(installedRoot, validator, 'semantic validator');
81
+ }
82
+
83
+ const sourceRuntime = path.resolve(__dirname, '..');
84
+ const sourceRoot = path.resolve(sourceRuntime, '..');
85
+ if (path.basename(sourceRuntime) !== 'claude' || path.basename(sourceRoot) !== 'src') {
86
+ throw new Error('semantic validator runtime root is not recognized');
87
+ }
88
+ const validator = path.join(sourceRuntime, 'scripts', 'validate-spec-output.cjs');
89
+ PATH_SAFETY.verifyCommonJsClosure(sourceRoot, validator);
90
+ return PATH_SAFETY.canonicalRegularFile(sourceRoot, validator, 'semantic validator');
91
+ }
92
+
93
+ function currentDigest(root, specFile) {
94
+ const validator = validatorFor(root);
95
+ const result = spawnSync(process.execPath, [validator, path.dirname(specFile), '--semantic-digest'], { cwd: root, encoding: 'utf8' });
96
+ const digest = result.stdout.trim();
97
+ if (result.status !== 0 || !SHA256_RE.test(digest)) throw new Error(`semantic digest could not be recomputed (${result.stderr.trim() || `exit ${result.status}`})`);
98
+ return digest;
99
+ }
100
+
101
+ function recordFromSubagentStop(payload) {
102
+ // Honest-agent guardrail: the host hook observes this SubagentStop payload.
103
+ // The worker-authored claim is not authenticated input or a security boundary.
104
+ if (!plain(payload) || payload.hook_event_name !== 'SubagentStop') return { recorded: false, reason: 'event' };
105
+ for (const field of ['session_id', 'agent_id', 'agent_type', 'last_assistant_message']) {
106
+ if (typeof payload[field] !== 'string' || !payload[field].trim()) return { recorded: false, reason: `missing-${field}` };
107
+ }
108
+ if (!REVIEWER_ROLE_ALLOWLIST.has(payload.agent_type)) return { recorded: false, reason: 'reviewer-role' };
109
+ const root = projectRoot(payload);
110
+ const claim = parseClaim(payload.last_assistant_message);
111
+ if (claim === null) return { recorded: false, reason: 'no-claim' };
112
+ const requestedSpec = path.resolve(root, claim.spec_file);
113
+ const identity = identityFor(root, requestedSpec, claim.feature_name);
114
+ const digest = currentDigest(root, identity.spec_file);
115
+ if (digest !== claim.semantic_digest) throw new Error('claimed semantic digest does not match current artifacts');
116
+ const key = STATE.ensureKey();
117
+ const record = {
118
+ schema_version: SCHEMA_VERSION, kind: 'semantic-review-observation', identity,
119
+ semantic_digest: digest, event: 'SubagentStop', verdict: 'PASS',
120
+ host_session_id: payload.session_id, reviewer_agent_id: payload.agent_id,
121
+ reviewer_agent_type: payload.agent_type, issued_at: new Date().toISOString(),
122
+ };
123
+ record.mac = STATE.recordMac(record, key);
124
+ STATE.atomicWrite(recordPath(root, identity), record);
125
+ return { recorded: true, record };
126
+ }
127
+
128
+ function verifyAttestation(root, specFile, featureName, semanticDigest) {
129
+ let identity;
130
+ try { identity = identityFor(root, specFile, featureName); } catch (error) { return { ok: false, reason: error.message }; }
131
+ const file = recordPath(root, identity);
132
+ let record;
133
+ try { record = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) { return { ok: false, reason: error.code === 'ENOENT' ? 'matching host-hook observation is missing' : 'host-hook observation is unreadable' }; }
134
+ const key = STATE.readKey();
135
+ if (key.missing) return { ok: false, reason: 'completion authority key is missing' };
136
+ if (!exactKeys(record, RECORD_FIELDS) || !STATE.macMatches(record, key.key)) return { ok: false, reason: 'host-hook observation is malformed or its MAC is invalid' };
137
+ if (record.schema_version !== SCHEMA_VERSION || record.kind !== 'semantic-review-observation' || record.event !== 'SubagentStop' || record.verdict !== 'PASS') return { ok: false, reason: 'host-hook observation event or verdict is invalid' };
138
+ if (STATE.stableStringify(record.identity) !== STATE.stableStringify(identity)) return { ok: false, reason: 'host-hook observation project, feature, or spec identity is invalid' };
139
+ if (record.semantic_digest !== semanticDigest) return { ok: false, reason: 'host-hook observation semantic digest is stale' };
140
+ return { ok: true, record };
141
+ }
142
+
143
+ if (require.main === module) {
144
+ try {
145
+ const raw = fs.readFileSync(0, 'utf8').trim();
146
+ if (raw) recordFromSubagentStop(JSON.parse(raw));
147
+ } catch (error) {
148
+ process.stderr.write(`CafeKit semantic review SubagentStop claim rejected: ${error.message}\n`);
149
+ }
150
+ }
151
+
152
+ module.exports = { MARKER, REVIEWER_ROLE_ALLOWLIST, recordFromSubagentStop, verifyAttestation, identityFor, recordPath };
@@ -5,7 +5,7 @@
5
5
  * Stop Hook — spec-gate.cjs
6
6
  * Implements: https://docs.anthropic.com/en/docs/claude-code/hooks
7
7
  *
8
- * Blocks turn end when *newly-done* tasks lack a verification receipt.
8
+ * Blocks turn end when any done task lacks a verification receipt.
9
9
  * Block contract (exit 0 + stdout): {"decision":"block","reason":"..."}.
10
10
  *
11
11
  * Safety: stop_hook_active loop guard; no worker-writable completion-gate
@@ -83,11 +83,17 @@ try {
83
83
 
84
84
  let POLICY;
85
85
  let RESOLVER;
86
+ let RECEIPT;
87
+ let FINAL_STATE;
86
88
  try {
87
89
  POLICY = require(policyPath);
88
90
  RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
91
+ RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
92
+ FINAL_STATE = require('./completion-authority-check.cjs');
89
93
  if (typeof POLICY.validateCanonicalReceipt !== 'function'
90
- || typeof POLICY.completionDecisionForSpec !== 'function') {
94
+ || typeof POLICY.completionDecisionForSpec !== 'function'
95
+ || typeof RECEIPT.checkTaskReceipt !== 'function'
96
+ || typeof FINAL_STATE.evaluateCloseout !== 'function') {
91
97
  throw new Error('shared workflow policy lacks completion authority functions');
92
98
  }
93
99
  } catch (error) {
@@ -106,11 +112,9 @@ try {
106
112
  } catch { /* gate stays on */ }
107
113
  // Active-spec discovery via shared resolver — explicit target if present, else fail on ambiguity.
108
114
  const baseDir = cwd;
109
- const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
110
- const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
111
- const resolved = RESOLVER.resolveActiveSpec({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
115
+ const resolved = FINAL_STATE.resolveCandidate({ resolver: RESOLVER, projectRoot: baseDir, runtime, payload });
112
116
  if (!resolved) process.exit(0);
113
- if (resolved.error === 'multiple_active') {
117
+ if (resolved.error === 'multiple_active' || resolved.error === 'multiple_persisted') {
114
118
  process.stdout.write(JSON.stringify({
115
119
  decision: 'block',
116
120
  reason: `Completion gate: multiple active specs detected (${resolved.candidates.join(', ')}). Provide explicit feature target or resolve ambiguity before completing tasks. Candidates: ${resolved.candidates.join(', ')}`
@@ -136,6 +140,23 @@ try {
136
140
  const activeSpec = resolved.spec;
137
141
  const featureName = resolved.featureName;
138
142
  const specsPath = resolved.specsDir;
143
+ const lifecyclePhase = activeSpec.current_phase || activeSpec.phase;
144
+ const explicitCloseout = ['done', 'completed', 'complete'].includes(activeSpec.status)
145
+ || ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
146
+ if (activeSpec.schema_version === '2.1') {
147
+ const finalState = FINAL_STATE.evaluateCloseout({
148
+ resolver: RESOLVER,
149
+ policy: POLICY,
150
+ projectRoot: baseDir,
151
+ runtime,
152
+ payload: { ...payload, session_id: sessionIdentity(payload) },
153
+ });
154
+ if (!finalState.ok) {
155
+ emitBlock(`Completion gate: ${finalState.reason}`);
156
+ process.exit(0);
157
+ }
158
+ if (finalState.active) process.exit(0);
159
+ }
139
160
  const runtimeContext = POLICY.deriveRuntimeContext({
140
161
  projectRoot: baseDir,
141
162
  specsRoot: specsPath,
@@ -181,117 +202,27 @@ try {
181
202
  const allDoneTasks = Object.keys(taskRegistry).filter((tp) =>
182
203
  (taskRegistry[tp]?.status || 'pending') === 'done'
183
204
  );
184
- const receiptBodies = new Map();
185
- // /m so ^ matches line starts (Evidence is never at byte 0 of the file).
186
- // legacy heading aliases: read-compat only, no longer advertised
187
- const EVID_RE = /^#{2,3}\s+(Evidence|Task Test Plan & Verification Evidence|Verification & Evidence)\b/m;
188
- const PASS_MARKER_RE = /^\s*Verification:\s*PASS\s*$/m;
189
- const LEGACY_SUCCESS_RE = /^\s*(?:PASS(?:ED)?|✓)(?:\s*:|$)|exit\s+code\s*[:=]?\s*0\b/im;
190
-
191
- /** Body of first Evidence heading until next same-or-higher heading. */
192
- function evidenceBody(text) {
193
- const lines = text.split('\n');
194
- let start = -1, level = 0;
195
- for (let i = 0; i < lines.length; i++) {
196
- const m = lines[i].match(/^(#{2,3})\s+(Evidence|Task Test Plan & Verification Evidence|Verification & Evidence)\b/);
197
- if (m) { start = i + 1; level = m[1].length; break; }
198
- }
199
- if (start < 0) return null;
200
- let end = lines.length;
201
- for (let i = start; i < lines.length; i++) {
202
- const hm = lines[i].match(/^(#{1,6})\s+/);
203
- const isTapMetadata = typeof POLICY.isTapMetadataHeading === 'function'
204
- && POLICY.isTapMetadataHeading(lines[i]);
205
- if (hm && hm[1].length <= level && !isTapMetadata) { end = i; break; }
206
- }
207
- return lines.slice(start, end).join('\n');
208
- }
209
-
210
- function validateCanonicalReceipt(body, options) {
211
- return POLICY.validateCanonicalReceipt(body, options);
212
- }
213
-
214
- // Receipt checks a–h → failed letter list.
215
- // a: file + Status header has "done"; b: Evidence heading; c: no {{...}} + unambiguous PASS; d: completed_at; e: command; f: exit/result; g: provenance; h: artifact hash
216
- function safeTaskFile(featureDir, taskPath) {
217
- const resolvedFeature = path.resolve(featureDir);
218
- const target = path.resolve(featureDir, taskPath);
219
- const relative = path.relative(resolvedFeature, target);
220
- if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
221
- if (!fs.existsSync(target)) return null;
222
- try {
223
- const canonicalFeature = fs.realpathSync(resolvedFeature);
224
- const canonicalTarget = fs.realpathSync(target);
225
- const rel = path.relative(canonicalFeature, canonicalTarget);
226
- if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return null;
227
- } catch {
228
- return null;
229
- }
230
- return target;
231
- }
232
-
233
- function checkReceipt(taskPath) {
234
- const fails = [];
235
- const featureDir = path.join(specsPath, featureName);
236
- const abs = safeTaskFile(featureDir, taskPath);
237
- if (!abs || !fs.existsSync(abs)) return ['a'];
238
- let text = '';
239
- try { text = fs.readFileSync(abs, 'utf8'); } catch { return ['a']; }
240
-
241
- const statusLine = text.split('\n').find((l) => /^\s*(\*\*)?Status(\*\*)?\s*:/i.test(l));
242
- if (!statusLine || !/\bdone\b/i.test(statusLine)) fails.push('a');
243
-
244
- const body = evidenceBody(text);
245
- if (body !== null) receiptBodies.set(taskPath, body);
246
- if (body === null || !EVID_RE.test(text)) {
247
- fails.push('b');
248
- } else if (
249
- /\{\{[^}]+\}\}/.test(body) ||
250
- !(PASS_MARKER_RE.test(body) || LEGACY_SUCCESS_RE.test(body))
251
- ) {
252
- fails.push('c');
253
- } else {
254
- // Canonical receipt checks for done tasks — delegated to shared validator (single authority)
255
- const canonicalFails = validateCanonicalReceipt(body, POLICY.receiptValidatorOptions(taskRegistry[taskPath], {
256
- runtimeContext,
257
- requireProvenanceBinding: true,
258
- }));
259
- 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' };
260
- for (const cf of canonicalFails) {
261
- if (cf === 'verification_state' || cf === 'shared_validator' || cf === 'validator_unavailable') {
262
- if (!fails.includes('c')) fails.push('c');
263
- continue;
264
- }
265
- if (cf === 'placeholder') {
266
- if (!fails.includes('c')) fails.push('c');
267
- continue;
268
- }
269
- const letter = map[cf];
270
- if (letter && !fails.includes(letter)) fails.push(letter);
271
- }
272
- // Enforce strict Verification: PASS, not just legacy success
273
- if (!PASS_MARKER_RE.test(body) && !fails.includes('c')) fails.push('c');
274
- }
275
-
276
- const at = taskRegistry[taskPath]?.completed_at;
277
- if (
278
- typeof at !== 'string' ||
279
- !/^\d{4}-\d{2}-\d{2}T/.test(at) ||
280
- Number.isNaN(Date.parse(at))
281
- ) fails.push('d');
282
- return fails;
283
- }
284
-
205
+ const featureDir = path.join(specsPath, featureName);
285
206
  const failures = allDoneTasks
286
- .map((tp) => ({ taskPath: tp, fails: checkReceipt(tp) }))
207
+ .map((taskPath) => ({
208
+ taskPath,
209
+ fails: RECEIPT.checkTaskReceipt(featureDir, taskPath, taskRegistry[taskPath], runtimeContext, POLICY).failures,
210
+ }))
287
211
  .filter((f) => f.fails.length > 0);
288
212
 
289
- const firstReceiptTask = allDoneTasks.find((taskPath) => receiptBodies.has(taskPath));
290
- const completion = allDoneTasks.length > 0 && Object.prototype.hasOwnProperty.call(activeSpec, 'workflow_policy')
213
+ const featureCloseoutRequired = explicitCloseout && allDoneTasks.length > 0;
214
+ const featureReceipt = featureCloseoutRequired
215
+ ? RECEIPT.checkFeatureReceipt(featureDir, runtimeContext, POLICY)
216
+ : null;
217
+ if (featureReceipt && featureReceipt.failures.length) {
218
+ failures.push({ taskPath: 'feature-receipt.md', fails: featureReceipt.failures });
219
+ }
220
+ const completion = featureCloseoutRequired && featureReceipt?.failures.length === 0
221
+ && Object.prototype.hasOwnProperty.call(activeSpec, 'workflow_policy')
291
222
  ? POLICY.completionDecisionForSpec(activeSpec, {
292
223
  runtimeContext,
293
- executionReceipt: firstReceiptTask ? receiptBodies.get(firstReceiptTask) : null,
294
- taskContext: firstReceiptTask ? taskRegistry[firstReceiptTask] : null,
224
+ executionReceipt: featureReceipt.body,
225
+ taskContext: {},
295
226
  })
296
227
  : null;
297
228
 
@@ -310,20 +241,19 @@ try {
310
241
  fs.writeFileSync(cacheFile, JSON.stringify(cache));
311
242
  } catch { /* fail-open */ }
312
243
 
313
- if (allDoneTasks.length === 0) process.exit(0);
314
244
  const completionBlocked = completion && completion.completion !== 'complete' && completion.completion !== 'not_applicable';
315
245
  if (failures.length === 0 && !completionBlocked) process.exit(0);
316
246
 
317
247
  const lines = [
318
248
  failures.length > 0
319
- ? `⚠️ Completion gate: ${failures.length} newly-done task(s) lack a verification receipt.`
249
+ ? `⚠️ Completion gate: ${failures.length} done task(s) lack a verification receipt.`
320
250
  : '⚠️ Completion gate: workflow completion proof is incomplete.',
321
251
  ];
322
252
  for (const { taskPath, fails } of failures) {
323
253
  lines.push(`- \`${taskPath}\`: failed check(s) ${fails.join(', ')}`);
324
- lines.push(
325
- ` Fix: add \`Verification: PASS\` plus commands and successful outcomes to \`## Evidence\` in \`specs/${featureName}/${taskPath}\`, then re-sync spec.json`
326
- );
254
+ lines.push(taskPath === 'feature-receipt.md'
255
+ ? ` Fix: run final integration proof, then write \`specs/${featureName}/feature-receipt.md\`.`
256
+ : ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
327
257
  }
328
258
  if (completionBlocked) {
329
259
  lines.push(`- Completion decision unfinished: ${completion.blocker || 'required workflow proof is missing.'}`);
@@ -54,9 +54,11 @@ try {
54
54
 
55
55
  let POLICY;
56
56
  let RESOLVER;
57
+ let RECEIPT;
57
58
  try {
58
59
  POLICY = require(path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs'));
59
60
  RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
61
+ RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
60
62
  if (typeof POLICY.flashState !== 'function') throw new Error('shared workflow policy has no flashState function');
61
63
  } catch (error) {
62
64
  logCrash(error);
@@ -132,6 +134,8 @@ try {
132
134
  const deps = Array.isArray(task?.dependencies) ? task.dependencies : [];
133
135
  return status === 'pending' && deps.every((dep) => taskStatusByPath.get(dep) === 'done');
134
136
  });
137
+ const featureDir = path.join(specsPath, featureName);
138
+ const featureReceiptPresent = RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
135
139
 
136
140
  // ── State-change gate: only emit the full tollgate when spec state changed ──
137
141
  // The cache is shared by installed hook copies, so its key must carry the
@@ -150,6 +154,7 @@ try {
150
154
  phase,
151
155
  done: taskCounts.done || 0,
152
156
  total: taskEntries.length,
157
+ featureReceiptPresent,
153
158
  });
154
159
  const cacheFile = path.join(__dirname, '.logs', 'tollgate-last.txt');
155
160
 
@@ -182,8 +187,9 @@ try {
182
187
  if (flashTasks.length > 0) {
183
188
  lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. A PASS proof keeps the persisted task in_progress until explicit sync-finalize.`);
184
189
  }
185
- lines.push(`- Sync \`spec.json\` + task file after verified work; run \`node .claude/scripts/validate-spec-output.cjs specs/${featureName}\` before \`ready_for_implementation=true\`.`);
186
- lines.push(`- A completion gate verifies receipts when you end a turn with newly-done tasks.`);
190
+ lines.push(`- Sync \`spec.json\` + task Markdown status after verified work; task proof belongs in \`receipts/<task-basename>.md\`.`);
191
+ lines.push(`- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`);
192
+ lines.push(`- Validate with \`node .claude/scripts/validate-spec-output.cjs specs/${featureName}\`; hooks revalidate receipt bytes but never grant approval.`);
187
193
  lines.push('');
188
194
 
189
195
  console.log(lines.join('\n'));
@@ -63,6 +63,11 @@
63
63
  "scan-staged-secrets.cjs",
64
64
  "workflow-policy.cjs",
65
65
  "spec-resolver.cjs",
66
+ "spec-receipt.cjs",
67
+ "spec-ground.cjs",
68
+ "spec-semantic-model.cjs",
69
+ "spec-final-state.cjs",
70
+ "spec-readiness.cjs",
66
71
  "provenance.cjs"
67
72
  ]
68
73
  },
@@ -107,6 +112,7 @@
107
112
  "hooks/completion-authority.cjs",
108
113
  "hooks/completion-authority-check.cjs",
109
114
  "hooks/completion-authority-state.cjs",
115
+ "hooks/semantic-review-authority.cjs",
110
116
  "hooks/state.cjs",
111
117
  "hooks/lib/color.cjs",
112
118
  "hooks/lib/parser.cjs",
@@ -114,7 +120,9 @@
114
120
  "hooks/lib/git.cjs",
115
121
  "hooks/lib/config.cjs",
116
122
  "hooks/lib/context.cjs",
117
- "hooks/lib/detect.cjs"
123
+ "hooks/lib/detect.cjs",
124
+ "hooks/lib/privacy-command-analysis.cjs",
125
+ "hooks/lib/runtime-path-safety.cjs"
118
126
  ]
119
127
  },
120
128
  "settings": {
@@ -1,31 +1,62 @@
1
- # Tollgate Protocol (State Sync)
1
+ # Specs v2.1 state synchronization
2
2
 
3
- ## Single Source of Truth
3
+ ## Authority layers
4
4
 
5
- In any Spec-driven workflow (`specs`), the state of the project is physically persisted in **two layers**:
6
- 1. **Machine Layer (`spec.json`)**: Tracks phase, status, overall completion, and per-task machine state via `task_registry`.
7
- 2. **Human Layer (`tasks/task-*.md`)**: Checkboxes indicating granular execution progress.
5
+ `spec.json` is machine semantic authority. Requirements, design, conditional
6
+ research, and conditional task Markdown are human projections. Host state may
7
+ record authority or integrity, but it never supplies product semantics.
8
8
 
9
- ## The Sync-back Rule (Mandatory)
9
+ Planning depth and assurance level remain independent. The artifact router runs
10
+ before discovery: None persists no spec; Compact and Full persist the bounded
11
+ core; research and tasks remain absent unless their semantic triggers exist.
10
12
 
11
- Whenever an agent finishes a task or blocks due to an issue, it **MUST NOT** simply respond with "Done" or "Blocked" in chat.
12
- Before returning control to the user or orchestrator, the agent **MUST**:
13
+ ## Canonical lifecycle
13
14
 
14
- ### On Success:
15
- 1. Update `spec.json`: Modify `current_phase` if moving forward, ensure `status` accurately reflects progress, keep `task_files` synchronized with the real files on disk, and update the corresponding `task_registry` entry (`status`, `blocker`, `started_at`, `completed_at`, `last_updated_at`).
16
- 2. Edit `task-R*.md`: Change `Status` only after real verification has passed (build/test/runtime/artifact). Then check `[x]` the sub-task boxes and relevant completion criteria.
17
- 3. Call `TaskUpdate` if Claude Tasks are active, setting the status to "completed" only after the physical files were updated.
15
+ The feature lifecycle is exactly `in_progress`, `paused`, `blocked`, or `done`.
16
+ Authoring state is independently `draft`, `validated`, or `absent`. Do not emit
17
+ legacy lifecycle aliases in a new 2.1 artifact.
18
18
 
19
- ### On Block/Failure (>3 retries):
20
- 1. Update `spec.json`: Set `"status": "blocked"` and fill out the `"blocker"` string with the root cause.
21
- 2. Update the corresponding `task_registry` entry to `blocked`, persist the blocker reason, and stamp `last_updated_at`.
22
- 3. Edit `task-R*.md`: Change `Status: pending` (or `in_progress`) to `Status: blocked` with a note.
23
- 4. Alert the orchestrator or user via `AskUserQuestion` or explicit warning.
19
+ Technical readiness is not lifecycle completion. `ready_for_implementation`
20
+ may change only through the installed machine finalization path after every
21
+ routed artifact, projection, semantic review, grounding check, and deterministic
22
+ gate passes. An author must not directly declare readiness, authority, approval,
23
+ review capability, execution proof, or closeout.
24
24
 
25
- **Canonical state values:** New specs MUST use `status: "in_progress"` for active work. Legacy `in-progress` may be read for compatibility, but must not be emitted in new files.
25
+ ## Projection sync
26
26
 
27
- **Golden Rule:** If the current phase changes, or a task completes, the agent must update the physical files. Never mark a task completed before there is execution proof, and never let `task_registry` disagree with the matching markdown task file. The context is intentionally NOT persisted in the chat to save tokens. An injected Hook (`spec-state.cjs`) constantly enforces and validates this state.
27
+ After any semantic Markdown edit:
28
28
 
29
- ### Completion gate (Stop)
29
+ 1. invoke the explicit installed machine semantic-sync step to promote `semantic_model`;
30
+ 2. regenerate or reconcile every affected Markdown projection;
31
+ 3. verify requirements, decisions, anchors, task registry, typed boundaries,
32
+ and verification refs round-trip without loss; and
33
+ 4. rerun the applicable deterministic validation and grounding checks.
30
34
 
31
- Marking a task `done` is now machine-gated on the **Stop** hook (`spec-gate.cjs`), not only reminded on each prompt. When a turn ends with *newly-done* tasks, the gate requires a verification receipt in each task markdown: `Status: done`, `## Evidence` (legacy heading aliases still parse) with real proof (no `{{...}}` placeholders; at least one fenced command block or PASS/FAIL line), and a non-empty `task_registry[path].completed_at`. First adoption (no cache yet) seeds history without blocking. Escape hatch for humans: `"spec": { "completion_gate": false }` in `.claude/runtime.json` (missing key keeps the gate ON).
35
+ For Claude Code, the isolated promotion is
36
+ `node .claude/scripts/spec-scaffold.cjs <feature> --sync-semantic-model`.
37
+
38
+ Task Markdown exists only when typed ownership, dependency, transition, proof,
39
+ or parallel topology requires it. Its status and dependencies project the
40
+ matching `task_registry` entry. Its Verification Plan is prospective and never
41
+ contains an execution verdict or proof claim. Topology comes only from typed
42
+ `coordination.boundaries`, never from legacy trigger fields, priority markers,
43
+ related-file lists, or prose.
44
+
45
+ ## Lifecycle updates
46
+
47
+ - Starting or continuing authored work keeps the feature `in_progress`.
48
+ - A deliberate resumable stop uses `paused` with the exact remaining decision.
49
+ - An external or semantic blocker uses `blocked` with a concrete cause.
50
+ - Task state changes update `task_registry` and the matching task projection
51
+ together, but only from observed work and verification.
52
+ - Feature `done` is reserved for final-state authority after implementation
53
+ closeout; authoring or readiness cannot produce it.
54
+
55
+ State synchronization never fabricates commands, PASS results, timestamps,
56
+ receipts, completed work, reviewer identity, or user approval. When execution
57
+ evidence is absent, preserve the unfinished state and report the missing proof.
58
+ Never hand-author the `semantic_model` shape or treat an unsynchronized Markdown
59
+ edit as machine authority.
60
+
61
+ Claude-native plan/task state may mirror physical state when available, but it
62
+ is never a substitute for `spec.json` or authority to advance the lifecycle.
@@ -13,14 +13,16 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
13
13
  ## 2. Plan
14
14
 
15
15
  - For non-trivial features, use `/specs` to create or validate the spec.
16
- - For approved specs, work one task file at a time.
16
+ - For technically ready specs, start implementation only after a new explicit Develop invocation and work one task file at a time.
17
17
  - Extract from the active task:
18
- - `Context`
19
- - `Steps`
20
- - `Requirements`
21
- - `Related Files`
22
- - `Completion Criteria`
23
- - `Evidence`
18
+ - `Status`
19
+ - `Outcome`
20
+ - `Scope`
21
+ - `Anchors and Ownership`
22
+ - `Changes`
23
+ - `Acceptance`
24
+ - `Dependencies`
25
+ - `Verification Plan`
24
26
  - If these are missing or too vague to verify, route back to spec correction.
25
27
 
26
28
  ## 3. Execute
@@ -32,7 +34,7 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
32
34
 
33
35
  ## 4. Verify
34
36
 
35
- - Run exact commands from `Evidence` first.
37
+ - Run exact commands from `Verification Plan` first.
36
38
  - Then run repo-level lint/test/build as needed for confidence.
37
39
  - Use only fresh verification from the current run when claiming completion.
38
40
  - `PRECHECK_FAIL` outranks `NO_TESTS`.
@@ -42,7 +44,13 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
42
44
  ## 5. Sync
43
45
 
44
46
  - Mark task state only after implementation, tests/evidence, and review pass.
45
- - Write a verification receipt with commands run, outcomes, and artifact/runtime proof.
47
+ - Write task proof to `receipts/<task-basename>.md`; use legacy task-local
48
+ `## Evidence` only when consuming an old task format.
49
+ - Create `feature-receipt.md` only for final integration closeout.
50
+ - Treat `spec.json` as machine authority; task Markdown is a human projection.
51
+ - Derive task ordering, ownership, proof, and parallel eligibility only from
52
+ typed `coordination.boundaries`, never `(P)`, `task_triggers`, `Related Files`,
53
+ or prose markers.
46
54
  - Keep `spec.json.task_registry` and markdown task files aligned.
47
55
  - Run docs checkpoint when a completed task affects public docs or architecture docs.
48
56