@haposoft/cafekit 0.18.0 → 0.18.2

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 (162) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/claude-settings.test.js +123 -0
  3. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  4. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  6. package/bin/__tests__/codex-hooks.test.js +938 -237
  7. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  8. package/bin/__tests__/codex-native.test.js +222 -149
  9. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  10. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  11. package/bin/__tests__/codex-research-projection.test.js +70 -0
  12. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  13. package/bin/__tests__/develop-contract.test.js +102 -1362
  14. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  15. package/bin/__tests__/inspect-paths.test.js +183 -0
  16. package/bin/__tests__/installer-safety.test.js +335 -0
  17. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  18. package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
  19. package/bin/__tests__/omp-bridge.test.js +66 -12
  20. package/bin/__tests__/omp-hooks.test.js +11 -5
  21. package/bin/__tests__/orca-session.test.js +7 -0
  22. package/bin/__tests__/package-inventory.test.js +1 -395
  23. package/bin/__tests__/privacy-paths.test.js +317 -0
  24. package/bin/__tests__/spec-narrowing.test.js +30 -134
  25. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  26. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  27. package/bin/__tests__/usage-hook-retired.test.js +5 -3
  28. package/bin/__tests__/validator-grounding.test.js +17 -2929
  29. package/bin/install.js +25 -0
  30. package/bin/lib/codex-install.js +134 -12
  31. package/bin/lib/context.js +1 -1
  32. package/bin/lib/managed-writer.js +2 -3
  33. package/bin/phases/claude-runtime.js +91 -25
  34. package/bin/phases/claude-settings.js +22 -1
  35. package/bin/phases/codex-hooks.js +96 -33
  36. package/bin/phases/codex-runtime.js +45 -49
  37. package/bin/phases/copy-payload.js +11 -28
  38. package/bin/phases/omp-runtime.js +6 -3
  39. package/package.json +2 -3
  40. package/src/claude/CLAUDE.md +4 -0
  41. package/src/claude/agents/code-auditor.md +9 -6
  42. package/src/claude/agents/debugger.md +3 -9
  43. package/src/claude/agents/docs-keeper.md +4 -7
  44. package/src/claude/agents/git-ops.md +2 -4
  45. package/src/claude/agents/implementer.md +6 -7
  46. package/src/claude/agents/inspector.md +2 -0
  47. package/src/claude/agents/project-manager.md +2 -2
  48. package/src/claude/agents/spec-maker.md +4 -4
  49. package/src/claude/agents/test-runner.md +8 -11
  50. package/src/claude/agents/ui-ux-designer.md +0 -1
  51. package/src/claude/gitignore +3 -0
  52. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  53. package/src/claude/hooks/__tests__/precompact.test.js +206 -10
  54. package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
  55. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  56. package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
  57. package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
  58. package/src/claude/hooks/__tests__/state.test.js +207 -7
  59. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  60. package/src/claude/hooks/agent.cjs +7 -6
  61. package/src/claude/hooks/inspect-block.cjs +15 -63
  62. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  63. package/src/claude/hooks/lib/parser.cjs +1 -1
  64. package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
  65. package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
  66. package/src/claude/hooks/lib/project-root.cjs +15 -0
  67. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  68. package/src/claude/hooks/lib/spec-touch.cjs +67 -0
  69. package/src/claude/hooks/lib/update-check.cjs +77 -0
  70. package/src/claude/hooks/precompact.cjs +8 -15
  71. package/src/claude/hooks/privacy-block.cjs +23 -205
  72. package/src/claude/hooks/rules.cjs +6 -28
  73. package/src/claude/hooks/session.cjs +34 -117
  74. package/src/claude/hooks/spec-gate.cjs +17 -89
  75. package/src/claude/hooks/spec-state.cjs +89 -96
  76. package/src/claude/hooks/state.cjs +96 -51
  77. package/src/claude/migration-manifest.json +20 -8
  78. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  79. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  80. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  81. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  82. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  83. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  84. package/src/claude/rules/ai-dev-rules.md +1 -2
  85. package/src/claude/rules/hook-protocols.md +1 -1
  86. package/src/claude/rules/workflow.md +1 -1
  87. package/src/claude/runtime.json +0 -3
  88. package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
  89. package/src/claude/scripts/provenance.cjs +14 -0
  90. package/src/claude/scripts/spec-receipt.cjs +0 -114
  91. package/src/claude/scripts/spec-resolver.cjs +41 -457
  92. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  93. package/src/claude/settings/settings.json +1 -1
  94. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  95. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  96. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  97. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  98. package/src/claude/skills/code-review/SKILL.md +25 -108
  99. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  100. package/src/claude/skills/develop/SKILL.md +22 -70
  101. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  102. package/src/claude/skills/git/SKILL.md +1 -1
  103. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  104. package/src/claude/skills/research/SKILL.md +24 -55
  105. package/src/claude/skills/sync/SKILL.md +12 -34
  106. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  107. package/src/claude/skills/test/SKILL.md +39 -123
  108. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  109. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  110. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  111. package/src/claude/skills/web-testing/SKILL.md +2 -20
  112. package/src/codex/AGENTS.md +8 -0
  113. package/src/codex/hooks/ask-guard.cjs +54 -0
  114. package/src/codex/hooks/inspect-block.cjs +11 -7
  115. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  116. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  117. package/src/codex/hooks/lib/privacy-state.cjs +98 -16
  118. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  119. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  120. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  121. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  122. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  123. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  124. package/src/codex/hooks/lib/update-check.cjs +1 -0
  125. package/src/codex/hooks/precompact.cjs +39 -0
  126. package/src/codex/hooks/privacy-approval.cjs +28 -17
  127. package/src/codex/hooks/privacy-block.cjs +9 -215
  128. package/src/codex/hooks/rules.cjs +4 -30
  129. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  130. package/src/codex/hooks/session.cjs +47 -11
  131. package/src/codex/hooks/spec-gate.cjs +32 -74
  132. package/src/codex/hooks/spec-state.cjs +68 -46
  133. package/src/codex/hooks/state.cjs +25 -4
  134. package/src/codex/hooks.json +39 -7
  135. package/src/codex/rules/hook-protocols.md +13 -6
  136. package/src/common/AGENTS.md +4 -10
  137. package/src/omp/extensions/cafekit-bridge.mjs +12 -4
  138. package/src/omp/hooks/privacy-block.cjs +24 -206
  139. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  140. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  141. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  142. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  143. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  144. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  145. package/src/claude/hooks/lib/context.cjs +0 -527
  146. package/src/claude/hooks/lib/detect.cjs +0 -474
  147. package/src/claude/scripts/change-firewall.cjs +0 -1748
  148. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  149. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  150. package/src/claude/scripts/spec-final-state.cjs +0 -290
  151. package/src/claude/scripts/spec-ground.cjs +0 -650
  152. package/src/claude/scripts/spec-readiness.cjs +0 -425
  153. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  154. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  155. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  156. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  157. package/src/claude/skills/specs/templates/design.md +0 -82
  158. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  159. package/src/claude/skills/specs/templates/requirements.md +0 -42
  160. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  161. package/src/claude/skills/specs/templates/task.md +0 -47
  162. package/src/codex/rules/state-sync.md +0 -60
@@ -11,11 +11,24 @@
11
11
  * Exit: 0 always (fail-open)
12
12
  */
13
13
 
14
+ function logCrash(error) {
15
+ try {
16
+ const fs = require('fs'), path = require('path');
17
+ const dir = require('./lib/hook-state-dir.cjs').hookStateDir();
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ fs.appendFileSync(path.join(dir, 'hook-log.jsonl'),
20
+ JSON.stringify({ ts: new Date().toISOString(), hook: 'session', status: 'crash', error: error.message }) + '\n');
21
+ } catch (_) {}
22
+ }
23
+
14
24
  try {
15
25
  const fs = require('fs');
16
26
  const path = require('path');
17
27
  const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
18
28
  const os = require('os');
29
+ const { projectRoot } = require('./lib/project-root.cjs');
30
+ const { checkCafeKitUpdate } = require('./lib/update-check.cjs');
31
+ const { clearSessionReservation } = require('./lib/rules-reservation.cjs');
19
32
  const { execSync } = require('child_process');
20
33
 
21
34
  // ── Utilities ─────────────────────────────────────────────────────────────
@@ -41,21 +54,6 @@ try {
41
54
  } catch { /* fail-open */ }
42
55
  }
43
56
 
44
- function projectRoot() {
45
- const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string'
46
- ? process.env.CLAUDE_PROJECT_DIR.trim()
47
- : '';
48
- if (configured) {
49
- try { return fs.realpathSync(path.resolve(configured)); } catch { /* continue */ }
50
- }
51
-
52
- const installedRoot = path.resolve(__dirname, '..', '..');
53
- const installedHook = runtimePath(installedRoot, 'hooks', path.basename(__filename));
54
- if (fs.existsSync(installedHook)) return installedRoot;
55
-
56
- try { return fs.realpathSync(process.cwd()); } catch { return path.resolve(process.cwd()); }
57
- }
58
-
59
57
  /** Read <runtime>/runtime.json config */
60
58
  function readRuntime(cwd) {
61
59
  try {
@@ -104,94 +102,6 @@ try {
104
102
  return '';
105
103
  }
106
104
 
107
- // ── CafeKit Update Check ──────────────────────────────────────────────────
108
-
109
- /**
110
- * Fetch the latest published version of @haposoft/cafekit from the npm registry.
111
- * Returns a string (e.g. "0.11.2") or null on any failure. Timeout: 3 s.
112
- */
113
- function fetchLatestVersion() {
114
- try {
115
- const https = require('https');
116
- return new Promise((resolve) => {
117
- const req = https.get(
118
- 'https://registry.npmjs.org/@haposoft/cafekit/latest',
119
- { timeout: 3000, headers: { Accept: 'application/json' } },
120
- (res) => {
121
- let body = '';
122
- res.on('data', (chunk) => { body += chunk; });
123
- res.on('end', () => {
124
- try { resolve(JSON.parse(body).version || null); }
125
- catch { resolve(null); }
126
- });
127
- }
128
- );
129
- req.on('error', () => resolve(null));
130
- req.on('timeout', () => { req.destroy(); resolve(null); });
131
- });
132
- } catch { return Promise.resolve(null); }
133
- }
134
-
135
- /** Simple semver compare: returns true when b > a. */
136
- function isNewer(a, b) {
137
- const n = (v) => String(v).split('.').map((x) => parseInt(x, 10) || 0);
138
- const [aN, bN] = [n(a), n(b)];
139
- for (let i = 0; i < 3; i++) {
140
- if (bN[i] > aN[i]) return true;
141
- if (bN[i] < aN[i]) return false;
142
- }
143
- return false;
144
- }
145
-
146
- /**
147
- * Read the installed CafeKit version from <runtime>/cafekit.json.
148
- * Returns null if not found.
149
- */
150
- function getInstalledVersion(cwd) {
151
- try {
152
- const p = runtimePath(cwd, 'cafekit.json');
153
- if (!fs.existsSync(p)) return null;
154
- const meta = JSON.parse(fs.readFileSync(p, 'utf8'));
155
- return typeof meta.version === 'string' ? meta.version : null;
156
- } catch { return null; }
157
- }
158
-
159
- /**
160
- * Check for a newer CafeKit version and return an update notice string,
161
- * or an empty string if already up to date or check fails.
162
- * Result is cached in <runtime>/.cafekit-update-cache.json for 12 hours.
163
- */
164
- async function checkCafeKitUpdate(cwd) {
165
- const installed = getInstalledVersion(cwd);
166
- if (!installed) return '';
167
-
168
- const CACHE_TTL = 12 * 60 * 60 * 1000; // 12 hours
169
- const cachePath = runtimePath(cwd, '.cafekit-update-cache.json');
170
-
171
- // Return cached result if still fresh.
172
- try {
173
- if (fs.existsSync(cachePath)) {
174
- const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
175
- if (Date.now() - (cache.ts || 0) < CACHE_TTL) {
176
- return cache.notice || '';
177
- }
178
- }
179
- } catch { /* stale or corrupt — refetch */ }
180
-
181
- // Fetch from npm registry.
182
- const latest = await fetchLatestVersion();
183
- const notice = (latest && isNewer(installed, latest))
184
- ? `\n⚡ CafeKit update available: ${installed} → ${latest}\n Run: npx @haposoft/cafekit\n`
185
- : '';
186
-
187
- // Persist cache (fail-open if write fails).
188
- try {
189
- fs.writeFileSync(cachePath, JSON.stringify({ ts: Date.now(), notice }), 'utf8');
190
- } catch { /* fail-open */ }
191
-
192
- return notice;
193
- }
194
-
195
105
  // ── Main ──────────────────────────────────────────────────────────────────
196
106
 
197
107
  (async () => {
@@ -199,12 +109,13 @@ try {
199
109
  const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
200
110
  const payload = stdin ? normalizeHookPayload(JSON.parse(stdin)) : {};
201
111
  const source = payload.source || 'unknown';
112
+ if (source === 'compact') clearSessionReservation(payload.session_id);
202
113
  const envFile = process.env.CLAUDE_ENV_FILE;
203
- const cwd = projectRoot();
114
+ const cwd = projectRoot(payload);
204
115
  const runtime = readRuntime(cwd);
205
116
 
206
117
  // Check CafeKit update in parallel with project detection (async, fail-open).
207
- const updateCheckPromise = checkCafeKitUpdate(cwd);
118
+ const updateCheckPromise = checkCafeKitUpdate(runtimeDir(cwd)).catch(() => '');
208
119
 
209
120
  // Project detection
210
121
  const projectType = runtime.project?.type !== 'auto'
@@ -221,7 +132,6 @@ try {
221
132
 
222
133
  // Static environment
223
134
  const gitBranch = run('git branch --show-current', '', cwd);
224
- const gitUrl = run('git remote get-url origin', '', cwd);
225
135
  const gitRoot = run('git rev-parse --show-toplevel', '', cwd);
226
136
  const nodeVer = process.version;
227
137
  const pythonVer = run('python3 --version', '', cwd) || run('python --version', '', cwd);
@@ -235,7 +145,6 @@ try {
235
145
  writeEnv(envFile, 'PACKAGE_MANAGER', packageManager);
236
146
  writeEnv(envFile, 'FRAMEWORK', framework);
237
147
  writeEnv(envFile, 'GIT_BRANCH', gitBranch);
238
- writeEnv(envFile, 'GIT_URL', gitUrl);
239
148
  writeEnv(envFile, 'GIT_ROOT', gitRoot);
240
149
  writeEnv(envFile, 'NODE_VERSION', nodeVer);
241
150
  writeEnv(envFile, 'PYTHON_VERSION', pythonVer);
@@ -262,6 +171,14 @@ try {
262
171
 
263
172
  console.log(`Session ${source}. ${parts.length ? parts.join(' | ') : 'No project info detected.'}${updateNotice}`);
264
173
 
174
+ if (typeof payload.cwd === 'string' && payload.cwd) {
175
+ let sessionCwd = path.resolve(payload.cwd);
176
+ try { sessionCwd = fs.realpathSync(sessionCwd); } catch { /* lexical fallback */ }
177
+ if (sessionCwd !== cwd) {
178
+ console.log(`CafeKit project root: ${cwd} (session working directory: ${sessionCwd}).`);
179
+ }
180
+ }
181
+
265
182
  // Compact warning — context compaction can lose pending approval state
266
183
  if (source === 'compact') {
267
184
  console.log('\n🚨 SESSION COMPRESSED — VERIFY PENDING AUTHORIZATIONS:');
@@ -274,8 +191,11 @@ try {
274
191
  // bearings, and nothing more.
275
192
  try {
276
193
  const fs2 = require('fs'), p2 = require('path');
277
- const file = p2.join(require('./lib/hook-state-dir.cjs').hookStateDir(), 'compact-recovery.json');
194
+ if (typeof payload.session_id !== 'string' || !payload.session_id.trim()) process.exit(0);
195
+ const key = require('crypto').createHash('sha256').update(payload.session_id).digest('hex');
196
+ const file = p2.join(require('./lib/hook-state-dir.cjs').hookStateDir(), `compact-recovery-${key}.json`);
278
197
  const record = JSON.parse(fs2.readFileSync(file, 'utf8'));
198
+ if (record.session_id !== payload.session_id) process.exit(0);
279
199
  const parts = [];
280
200
  if (record.branch) parts.push(`branch ${record.branch}`);
281
201
  if (record.head) parts.push(`HEAD ${record.head}`);
@@ -297,15 +217,12 @@ try {
297
217
  }
298
218
 
299
219
  process.exit(0);
300
- })(); // end async IIFE
220
+ })().catch((error) => {
221
+ logCrash(error);
222
+ process.exit(0);
223
+ });
301
224
 
302
- } catch (e) {
303
- try {
304
- const fs = require('fs'), p = require('path');
305
- const d = require('./lib/hook-state-dir.cjs').hookStateDir();
306
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
307
- fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
308
- JSON.stringify({ ts: new Date().toISOString(), hook: 'session', status: 'crash', error: e.message }) + '\n');
309
- } catch (_) {}
225
+ } catch (error) {
226
+ logCrash(error);
310
227
  process.exit(0);
311
228
  }
@@ -16,32 +16,7 @@
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
19
-
20
- function projectRoot(payload = {}) {
21
- const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string'
22
- ? process.env.CLAUDE_PROJECT_DIR.trim()
23
- : '';
24
- if (configured) {
25
- try { return fs.realpathSync(path.resolve(configured)); } catch { /* continue */ }
26
- }
27
-
28
- const installedRoot = path.resolve(__dirname, '..', '..');
29
- const installedHook = runtimePath(installedRoot, 'hooks', path.basename(__filename));
30
- if (fs.existsSync(installedHook)) return installedRoot;
31
-
32
- const sourceFixture = typeof process.env.PROJECT_ROOT === 'string'
33
- ? process.env.PROJECT_ROOT.trim()
34
- : '';
35
- if (sourceFixture) {
36
- try { return fs.realpathSync(path.resolve(sourceFixture)); } catch { /* continue */ }
37
- }
38
-
39
- const legacy = typeof payload.cwd === 'string' ? payload.cwd.trim() : '';
40
- if (legacy) {
41
- try { return fs.realpathSync(path.resolve(legacy)); } catch { /* continue */ }
42
- }
43
- try { return fs.realpathSync(process.cwd()); } catch { return path.resolve(process.cwd()); }
44
- }
19
+ const { projectRoot } = require('./lib/project-root.cjs');
45
20
 
46
21
  function logCrash(error) {
47
22
  try {
@@ -68,6 +43,7 @@ function sessionIdentity(payload) {
68
43
  return candidates.find((value) => typeof value === 'string' && value.trim() !== '') || null;
69
44
  }
70
45
 
46
+ let hasDoneTasks = false;
71
47
  try {
72
48
  const policyPath = path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs');
73
49
 
@@ -97,8 +73,7 @@ try {
97
73
  RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
98
74
  RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
99
75
  if (typeof POLICY.validateCanonicalReceipt !== 'function'
100
- || typeof POLICY.completionDecisionForSpec !== 'function'
101
- || typeof RECEIPT.checkTaskReceipt !== 'function'
76
+ || typeof RECEIPT.checkWorkflowTaskReceipt !== 'function'
102
77
  || typeof RECEIPT.checkWorkflowReceiptSet !== 'function'
103
78
  || typeof RESOLVER.resolveWorkflowCandidate !== 'function') {
104
79
  throw new Error('shared workflow policy lacks completion authority functions');
@@ -133,6 +108,8 @@ try {
133
108
  }
134
109
  if (!resolved) process.exit(0);
135
110
  if (resolved.layoutKind === 'process-v3-completed-set') {
111
+ hasDoneTasks = resolved.candidates.some(candidate =>
112
+ Object.values(candidate.taskRegistry || {}).some(task => task?.status === 'done'));
136
113
  const configuredGate = runtime.spec?.completion_gate;
137
114
  if (configuredGate !== undefined && configuredGate !== true) {
138
115
  emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
@@ -164,7 +141,7 @@ try {
164
141
  if (resolved.error === 'invalid_specs') {
165
142
  process.stdout.write(JSON.stringify({
166
143
  decision: 'block',
167
- reason: `Completion gate: invalid spec JSON detected (${resolved.candidates.join(', ')}): ${resolved.reason}. Fix or remove malformed spec.json before completing tasks.`
144
+ reason: `Completion gate: invalid workflow packet detected (${resolved.candidates.join(', ')}): ${resolved.reason}. Fix the packet's plan.md or task files before completing tasks.`,
168
145
  }) + '\n');
169
146
  process.exit(0);
170
147
  }
@@ -177,22 +154,13 @@ try {
177
154
  }
178
155
  if (resolved.error) process.exit(0);
179
156
 
180
- const activeSpec = resolved.spec || {};
181
- const processWorkflow = resolved.layoutKind === 'process-v3';
182
157
  const featureName = resolved.featureName;
183
158
  const specsPath = resolved.specsDir;
184
- const lifecyclePhase = activeSpec.current_phase || activeSpec.phase;
185
- const explicitCloseout = ['done', 'completed', 'complete'].includes(activeSpec.status)
186
- || ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
187
- // Legacy 2.1 closeout is retired; a 2.1 packet mid-execution keeps the ordinary receipt checks.
188
- if (!processWorkflow && activeSpec.schema_version === '2.1' && explicitCloseout) {
189
- emitBlock('Completion gate: Legacy spec.json closeout is no longer supported; move the packet to plan.md with flat task files');
190
- process.exit(0);
191
- }
159
+ hasDoneTasks = Object.values(resolved.taskRegistry || {}).some(task => task?.status === 'done');
192
160
  const runtimeContext = POLICY.deriveRuntimeContext({
193
161
  projectRoot: baseDir,
194
162
  specsRoot: specsPath,
195
- specFile: resolved.stateFile || resolved.specFile || path.join(specsPath, featureName, 'spec.json'),
163
+ specFile: resolved.stateFile,
196
164
  featureName,
197
165
  runtimeSession: sessionIdentity(payload),
198
166
  });
@@ -202,7 +170,7 @@ try {
202
170
  emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
203
171
  process.exit(0);
204
172
  }
205
- const taskRegistry = resolved.taskRegistry || activeSpec.task_registry || {};
173
+ const taskRegistry = resolved.taskRegistry || {};
206
174
  const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), 'spec-gate-last.json');
207
175
  const cacheExists = fs.existsSync(cacheFile);
208
176
  let cache = {};
@@ -215,17 +183,6 @@ try {
215
183
  currentStatuses[tp] = task?.status || 'pending';
216
184
  }
217
185
 
218
- const staleFlashTasks = processWorkflow ? [] : Object.entries(taskRegistry)
219
- .filter(([, task]) => POLICY.isStaleFlashDone(task))
220
- .map(([taskPath]) => taskPath);
221
- if (staleFlashTasks.length > 0) {
222
- process.stdout.write(JSON.stringify({
223
- decision: 'block',
224
- reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /cf:test for exact proof, then use explicit sync-finalize.`
225
- }) + '\n');
226
- process.exit(0);
227
- }
228
-
229
186
  // Cache hardening: every Stop re-reads and re-checks the receipt of every
230
187
  // task currently marked done, so a cached PASS cannot hide later mutations
231
188
  // (deletion, placeholder, changed Verification/Command/Exit, missing
@@ -241,29 +198,11 @@ try {
241
198
  const featureDir = path.join(specsPath, featureName);
242
199
  const failures = allDoneTasks
243
200
  .map((taskPath) => {
244
- const proof = processWorkflow
245
- ? RECEIPT.checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, POLICY)
246
- : RECEIPT.checkTaskReceipt(featureDir, taskPath, taskRegistry[taskPath], runtimeContext, POLICY);
201
+ const proof = RECEIPT.checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, POLICY);
247
202
  return { taskPath, fails: proof.failures, body: proof.body };
248
203
  })
249
204
  .filter((f) => f.fails.length > 0);
250
205
 
251
- const featureCloseoutRequired = !processWorkflow && explicitCloseout && allDoneTasks.length > 0;
252
- const featureReceipt = featureCloseoutRequired
253
- ? RECEIPT.checkFeatureReceipt(featureDir, runtimeContext, POLICY)
254
- : null;
255
- if (featureReceipt && featureReceipt.failures.length) {
256
- failures.push({ taskPath: 'feature-receipt.md', fails: featureReceipt.failures });
257
- }
258
- const completion = featureCloseoutRequired && featureReceipt?.failures.length === 0
259
- && Object.prototype.hasOwnProperty.call(activeSpec, 'workflow_policy')
260
- ? POLICY.completionDecisionForSpec(activeSpec, {
261
- runtimeContext,
262
- executionReceipt: featureReceipt.body,
263
- taskContext: {},
264
- })
265
- : null;
266
-
267
206
  // Always persist status transitions, including done → pending. Keep
268
207
  // failing done tasks at their previous cached status so a stale valid
269
208
  // entry cannot mask a mutated receipt on the next Stop; with
@@ -279,32 +218,21 @@ try {
279
218
  fs.writeFileSync(cacheFile, JSON.stringify(cache));
280
219
  } catch { /* fail-open */ }
281
220
 
282
- const completionBlocked = completion && completion.completion !== 'complete' && completion.completion !== 'not_applicable';
283
- if (failures.length === 0 && !completionBlocked) process.exit(0);
221
+ if (failures.length === 0) process.exit(0);
284
222
 
285
- const lines = [
286
- failures.length > 0
287
- ? `⚠️ Completion gate: ${failures.length} done task(s) lack a verification receipt.`
288
- : '⚠️ Completion gate: workflow completion proof is incomplete.',
289
- ];
223
+ const lines = [`⚠️ Completion gate: ${failures.length} done task(s) lack a verification receipt.`];
290
224
  for (const { taskPath, fails, body } of failures) {
291
225
  lines.push(`- \`${taskPath}\`: failed check(s) ${fails.join(', ')}`);
292
- lines.push(taskPath === 'feature-receipt.md'
293
- ? ` Fix: run final integration proof, then write \`specs/${featureName}/feature-receipt.md\`.`
294
- : processWorkflow
295
- ? ` Fix in \`specs/${featureName}/${path.posix.basename(taskPath)}\`: ${(typeof RECEIPT.receiptFixHint === 'function' && RECEIPT.receiptFixHint(fails, body)) || 'write a runtime-bound `## Receipt` with command output'}.`
296
- : ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
297
- }
298
- if (completionBlocked) {
299
- lines.push(`- Completion decision unfinished: ${completion.blocker || 'required workflow proof is missing.'}`);
300
- if (Array.isArray(completion.missingProof) && completion.missingProof.length > 0) {
301
- lines.push(` Missing proof: ${completion.missingProof.join(', ')}`);
302
- }
226
+ lines.push(` Fix in \`specs/${featureName}/${path.posix.basename(taskPath)}\`: ${(typeof RECEIPT.receiptFixHint === 'function' && RECEIPT.receiptFixHint(fails, body)) || 'write a runtime-bound `## Receipt` with command output'}.`);
303
227
  }
304
228
  process.stdout.write(JSON.stringify({ decision: 'block', reason: lines.slice(0, 8).join('\n') }) + '\n');
305
229
  process.exit(0);
306
230
 
307
231
  } catch (e) {
232
+ if (e.code === 'no_git_history') {
233
+ if (hasDoneTasks) emitBlock('Completion gate: repo has no commits yet; create the first commit, then record Base/Head after it.');
234
+ process.exit(0);
235
+ }
308
236
  logCrash(e);
309
237
  emitBlock(`Completion gate controlled failure: ${e.message}. Completion is blocked until the hook is repaired.`);
310
238
  process.exit(0);