@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
@@ -13,6 +13,8 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const { projectRoot } = require('./lib/project-root.cjs');
17
+ const { packetAt, namedPackets, explicitFailure } = require('./lib/spec-touch.cjs');
16
18
  const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
17
19
 
18
20
  function logCrash(error) {
@@ -40,44 +42,6 @@ function sessionIdentity(payload) {
40
42
  return candidates.find((value) => typeof value === 'string' && value.length > 0) || null;
41
43
  }
42
44
 
43
- function canonicalPath(value) {
44
- try { return fs.realpathSync(value); } catch { return null; }
45
- }
46
-
47
- /** Real path of `value`, resolving its deepest existing ancestor (the file may not exist yet). */
48
- function canonicalTarget(value) {
49
- if (typeof value !== 'string' || !value) return null;
50
- let current = path.resolve(value);
51
- const rest = [];
52
- while (!fs.existsSync(current)) {
53
- const parent = path.dirname(current);
54
- if (parent === current) return null;
55
- rest.unshift(path.basename(current));
56
- current = parent;
57
- }
58
- const real = canonicalPath(current);
59
- return real ? path.join(real, ...rest) : null;
60
- }
61
-
62
- function escapeRegExp(text) {
63
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
64
- }
65
-
66
- /**
67
- * Packets the prompt names: `specs/<name>`, a `/cf:<command> <name>` token, or the bare
68
- * name when it carries `-`, `_` or a digit. A plain word such as `docs` must use `specs/`,
69
- * so an ordinary sentence does not touch a packet.
70
- */
71
- function packetsNamedIn(text, names) {
72
- if (typeof text !== 'string' || !text) return [];
73
- return names.filter((name) => {
74
- const n = escapeRegExp(name);
75
- if (new RegExp(`(^|[^\\w-])specs/${n}(?![\\w-])`).test(text)) return true;
76
- if (new RegExp(`/cf:[\\w-]+\\s+(?:specs/)?${n}(?![\\w-])`).test(text)) return true;
77
- return /[-_0-9]/.test(name) && new RegExp(`(^|[^\\w/-])${n}(?![\\w-])`).test(text);
78
- });
79
- }
80
-
81
45
  try {
82
46
  // ── Main ──────────────────────────────────────────────────────────────────
83
47
 
@@ -94,19 +58,22 @@ try {
94
58
  let RECEIPT;
95
59
  try {
96
60
  POLICY = require(path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs'));
61
+ require(path.join(__dirname, '..', 'scripts', 'provenance.cjs')).enableSnapshotMemo();
97
62
  RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
98
63
  RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
99
- if (typeof POLICY.flashState !== 'function') throw new Error('shared workflow policy has no flashState function');
64
+ if (typeof RESOLVER.resolveWorkflowCandidate !== 'function') throw new Error('shared spec resolver has no resolveWorkflowCandidate function');
100
65
  } catch (error) {
101
66
  logCrash(error);
102
67
  emitControlledFailure(error.message);
103
68
  process.exit(0);
104
69
  }
105
70
 
71
+ const baseDir = projectRoot(payload);
72
+
106
73
  // Read runtime configuration if exists
107
74
  let runtime = {};
108
75
  try {
109
- const p = runtimePath(cwd, 'runtime.json');
76
+ const p = runtimePath(baseDir, 'runtime.json');
110
77
  if (fs.existsSync(p)) runtime = JSON.parse(fs.readFileSync(p, 'utf8'));
111
78
  } catch { /* ignore */ }
112
79
 
@@ -114,20 +81,17 @@ try {
114
81
  // (The Stop completion gate has its own toggle: spec.completion_gate.)
115
82
  if (runtime.spec && runtime.spec.tollgate === false) process.exit(0);
116
83
 
117
- // Same rule as spec-gate: an installed hook trusts its own location over the environment.
118
- const installedRoot = path.basename(path.resolve(__dirname, '..')).startsWith('.') ? path.resolve(__dirname, '..', '..') : null;
119
- const baseDir = installedRoot || process.env.PROJECT_ROOT || cwd;
120
84
  const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
121
- const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
85
+ const explicitPath = payload.explicitPath || payload.specPath || payload.spec_path || payload.featurePath || null;
122
86
 
123
- // ── Per-session touch set (Claude only) ───────────────────────────────────
124
- // Several sessions can share one checkout. On Claude the tollgate speaks only about a
125
- // packet this session touched. omp and Codex run their own copies, and grok sends
126
- // `sessionId` before normalization, so all three keep the old behaviour.
87
+ // ── Per-session touch set (Claude and omp) ────────────────────────────────
88
+ // Several sessions can share one checkout. On Claude and omp the tollgate speaks only
89
+ // about a packet this session touched. Codex runs its own copy, and grok sends
90
+ // `sessionId` before normalization, so both keep the old behaviour.
127
91
  const claudeSession = rawPayload && typeof rawPayload.session_id === 'string' && rawPayload.session_id
128
92
  ? rawPayload.session_id
129
93
  : null;
130
- const touchFilter = runtimeDirName() === '.claude' && claudeSession !== null;
94
+ const touchFilter = ['.claude', '.omp'].includes(runtimeDirName()) && claudeSession !== null;
131
95
  const touchFile = touchFilter
132
96
  ? path.join(
133
97
  require('./lib/hook-state-dir.cjs').hookStateDir(),
@@ -155,32 +119,73 @@ try {
155
119
 
156
120
  if (payload.hook_event_name === 'PostToolUse') {
157
121
  // Record only; no resolve, no git, no tollgate cache.
158
- if (touchFilter && specsRoot && /^(Edit|Write|MultiEdit)$/.test(payload.tool_name || '')) {
159
- const realRoot = canonicalPath(specsRoot);
160
- const realTarget = canonicalTarget(payload.tool_input && payload.tool_input.file_path);
161
- if (realRoot && realTarget) {
162
- const rel = path.relative(realRoot, realTarget);
163
- const parts = rel.split(path.sep);
164
- if (rel && parts[0] !== '..' && !path.isAbsolute(rel) && parts.length > 1) addTouched([parts[0]]);
165
- }
122
+ if (touchFilter && specsRoot && !explicitFailure(payload) && /^(Edit|Write|MultiEdit)$/.test(payload.tool_name || '')) {
123
+ const name = packetAt(payload.tool_input && payload.tool_input.file_path, specsRoot, cwd);
124
+ if (name) addTouched([name]);
166
125
  }
167
126
  process.exit(0);
168
127
  }
169
128
 
129
+ // A legacy packet (a specs/<x>/ directory without plan.md that the old flow left
130
+ // behind) is no longer read. Say so once per session instead of on every prompt.
131
+ const legacyPackets = typeof RESOLVER.findLegacyPackets === 'function'
132
+ ? RESOLVER.findLegacyPackets(baseDir, runtime)
133
+ : [];
134
+ if (legacyPackets.length > 0) {
135
+ const sessionKey = sessionIdentity(payload);
136
+ const noticeFile = sessionKey
137
+ ? path.join(
138
+ require('./lib/hook-state-dir.cjs').hookStateDir(),
139
+ `legacy-notice-${require('crypto').createHash('sha256').update(sessionKey).digest('hex')}.json`,
140
+ )
141
+ : null;
142
+ let noticed = [];
143
+ if (noticeFile) {
144
+ try { noticed = JSON.parse(fs.readFileSync(noticeFile, 'utf8')); } catch { noticed = []; }
145
+ if (!Array.isArray(noticed)) noticed = [];
146
+ }
147
+ const fresh = legacyPackets.filter((name) => !noticed.includes(name));
148
+ if (fresh.length > 0) {
149
+ for (const name of fresh) {
150
+ console.log(`\n> ⚠️ \`specs/${name}\` is a legacy Specs packet without plan.md; CafeKit no longer reads it. Move ongoing work to a process-first plan.md packet.`);
151
+ }
152
+ if (noticeFile) {
153
+ try {
154
+ fs.mkdirSync(path.dirname(noticeFile), { recursive: true });
155
+ fs.writeFileSync(noticeFile, JSON.stringify([...noticed, ...fresh].sort()));
156
+ } catch { /* an unwritable state dir only repeats the notice */ }
157
+ }
158
+ }
159
+ }
160
+
170
161
  if (touchFilter) {
171
162
  let packets = [];
172
163
  try {
173
164
  packets = fs.readdirSync(specsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
174
165
  } catch { /* no specs root */ }
175
- const named = packetsNamedIn(payload.prompt, packets);
166
+ const named = specsRoot ? namedPackets(payload.prompt || payload.user_prompt, packets, specsRoot, baseDir) : [];
176
167
  if (typeof explicitFeature === 'string' && explicitFeature) named.push(explicitFeature);
168
+ const fromPath = specsRoot ? packetAt(explicitPath, specsRoot, baseDir) : null;
169
+ if (fromPath) named.push(fromPath);
177
170
  if (named.length) addTouched(named);
178
171
  }
179
172
 
180
- const resolveWorkflow = typeof RESOLVER.resolveWorkflowCandidate === 'function'
181
- ? RESOLVER.resolveWorkflowCandidate
182
- : RESOLVER.resolveActiveSpec;
183
- const resolved = resolveWorkflow({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
173
+ let resolved = RESOLVER.resolveWorkflowCandidate({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
174
+ if (touchFilter && !explicitFeature && !explicitPath
175
+ && ['multiple_active', 'invalid_specs'].includes(resolved?.error)) {
176
+ const touched = readTouched();
177
+ const candidates = resolved.candidates || [];
178
+ if (!candidates.includes('<specs>') && !candidates.some(name => touched.has(name))) {
179
+ const selected = [...touched].map(name => RESOLVER.resolveWorkflowCandidate({
180
+ projectRoot: baseDir, runtime, explicitFeature: name,
181
+ }));
182
+ resolved = selected.find(candidate => candidate?.error);
183
+ const active = selected.filter(candidate => candidate && !candidate.error && !candidate.allTasksDone);
184
+ if (!resolved) resolved = active.length > 1
185
+ ? { error: 'multiple_active', candidates: active.map(candidate => candidate.featureName) }
186
+ : active[0] || null;
187
+ }
188
+ }
184
189
 
185
190
  if (!resolved) {
186
191
  process.exit(0); // No active spec, do nothing
@@ -194,7 +199,7 @@ try {
194
199
 
195
200
  if (resolved.error === 'invalid_specs') {
196
201
  const cands = resolved.candidates.join(', ');
197
- console.log(`\n> ⚠️ Invalid spec JSON detected: ${cands}. ${resolved.reason}. Fix or remove malformed spec.json before continuing. Tollgate paused.\n`);
202
+ console.log(`\n> ⚠️ Invalid workflow packet detected: ${cands}. ${resolved.reason}. Fix the packet's plan.md or task files before continuing. Tollgate paused.\n`);
198
203
  process.exit(0);
199
204
  }
200
205
 
@@ -208,22 +213,19 @@ try {
208
213
  process.exit(0);
209
214
  }
210
215
 
211
- const activeSpec = resolved.spec || {};
212
- const processWorkflow = resolved.layoutKind === 'process-v3';
213
216
  const featureName = resolved.featureName;
214
217
  if (touchFilter && !readTouched().has(featureName)) process.exit(0);
215
218
  const specsPath = resolved.specsDir;
216
219
  const runtimeContext = POLICY.deriveRuntimeContext({
217
220
  projectRoot: baseDir,
218
221
  specsRoot: specsPath,
219
- specFile: resolved.stateFile || resolved.specFile || path.join(specsPath, featureName, 'spec.json'),
222
+ specFile: resolved.stateFile,
220
223
  featureName,
221
224
  runtimeSession: sessionIdentity(payload),
222
225
  });
223
226
 
224
- const phase = resolved.phase || activeSpec.current_phase || activeSpec.phase || 'unknown';
225
- const taskRegistry = resolved.taskRegistry || activeSpec.task_registry || {};
226
- const flashTasks = POLICY.flashState(taskRegistry);
227
+ const phase = resolved.phase || 'unknown';
228
+ const taskRegistry = resolved.taskRegistry || {};
227
229
  const taskEntries = Object.entries(taskRegistry);
228
230
  const taskCounts = taskEntries.reduce((acc, [, task]) => {
229
231
  const status = task?.status || 'pending';
@@ -231,10 +233,7 @@ try {
231
233
  return acc;
232
234
  }, {});
233
235
  const featureDir = path.join(specsPath, featureName);
234
- const taskStatusByPath = new Map(taskEntries.map(([taskPath, task]) => [taskPath, task?.status || 'pending']));
235
- const dependencyProof = processWorkflow
236
- ? RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY)
237
- : {};
236
+ const dependencyProof = RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY);
238
237
  const taskState = taskEntries.map(([taskPath, task]) => [
239
238
  taskPath,
240
239
  task?.status || 'pending',
@@ -250,12 +249,9 @@ try {
250
249
  const nextUnblocked = taskEntries.find(([, task]) => {
251
250
  const status = task?.status || 'pending';
252
251
  const deps = Array.isArray(task?.dependencies) ? task.dependencies : [];
253
- return resolved.queueReady !== false && status === 'pending' && deps.every((dep) => (
254
- processWorkflow ? dependencyProof[dep]?.eligible === true : taskStatusByPath.get(dep) === 'done'
255
- ));
252
+ return resolved.queueReady !== false && status === 'pending'
253
+ && deps.every((dep) => dependencyProof[dep]?.eligible === true);
256
254
  });
257
- const featureReceiptPresent = !processWorkflow
258
- && RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
259
255
 
260
256
  // ── State-change gate: only emit the full tollgate when spec state changed ──
261
257
  // The cache is shared by installed hook copies, so its key must carry the
@@ -278,9 +274,11 @@ try {
278
274
  proofState,
279
275
  done: taskCounts.done || 0,
280
276
  total: taskEntries.length,
281
- featureReceiptPresent,
282
277
  });
283
- const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), 'tollgate-last.txt');
278
+ const cacheName = touchFilter
279
+ ? `tollgate-last-${require('crypto').createHash('sha256').update(claudeSession).digest('hex').slice(0, 16)}.txt`
280
+ : 'tollgate-last.txt';
281
+ const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), cacheName);
284
282
 
285
283
  let lastKey = '';
286
284
  if (stateKey) {
@@ -288,11 +286,10 @@ try {
288
286
  }
289
287
 
290
288
  if (stateKey && lastKey === stateKey) {
291
- const stateTarget = processWorkflow ? 'task Markdown' : '`spec.json`';
292
- const migration = processWorkflow && resolved.queueReady === false
289
+ const migration = resolved.queueReady === false
293
290
  ? ' Add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.'
294
291
  : '';
295
- console.log(`\n> 🔵 Spec \`${featureName}\` @ \`${phase}\` (${taskCounts.done || 0}/${taskEntries.length} tasks done). Tollgate active — sync ${stateTarget} when state changes.${migration}\n`);
292
+ console.log(`\n> 🔵 Spec \`${featureName}\` @ \`${phase}\` (${taskCounts.done || 0}/${taskEntries.length} tasks done). Tollgate active — sync task Markdown when state changes.${migration}\n`);
296
293
  process.exit(0);
297
294
  }
298
295
 
@@ -312,26 +309,22 @@ try {
312
309
  if (nextUnblocked) {
313
310
  lines.push(`- Next unblocked: \`${nextUnblocked[0]}\``);
314
311
  }
315
- if (flashTasks.length > 0) {
316
- lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. A PASS proof keeps the persisted task in_progress until explicit sync-finalize.`);
317
- }
318
- if (processWorkflow) {
319
- if (resolved.queueReady === false) {
320
- lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
321
- }
322
- lines.push('- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.');
323
- lines.push(`- Workflow source: \`specs/${featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime only while a committed task file no longer matches its committed bytes.`);
324
- } else {
325
- lines.push('- Sync `spec.json` + task Markdown status after verified work; task proof belongs in `receipts/<task-basename>.md`.');
326
- lines.push(`- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`);
327
- lines.push(`- Validate with \`node ${runtimeDirName()}/scripts/validate-spec-output.cjs specs/${featureName}\`; hooks revalidate receipt bytes but never grant approval.`);
312
+ if (resolved.queueReady === false) {
313
+ lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
328
314
  }
315
+ lines.push('- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.');
316
+ lines.push(`- Workflow source: \`specs/${featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime only while a committed task file no longer matches its committed bytes.`);
317
+ lines.push('- Hooks revalidate receipt bytes but never grant approval.');
329
318
  lines.push('');
330
319
 
331
320
  console.log(lines.join('\n'));
332
321
  process.exit(0);
333
322
 
334
323
  } catch (e) {
324
+ if (e.code === 'no_git_history') {
325
+ console.log('Repo has no commits yet; create the first commit, then record Base/Head after it.');
326
+ process.exit(0);
327
+ }
335
328
  logCrash(e);
336
329
  emitControlledFailure(e.message);
337
330
  process.exit(0);
@@ -8,14 +8,26 @@
8
8
  *
9
9
  * Events:
10
10
  * SessionStart → load previous state and print to context
11
- * PostToolUse → refresh state after Task/TaskCreate/TaskUpdate/TodoWrite
11
+ * PostToolUse → refresh state after Agent/Task/TaskCreate/TaskUpdate/TodoWrite
12
12
  * Stop → persist full session state and archive
13
13
  * SubagentStop → append agent completion note to current state
14
14
  *
15
- * Storage: <runtime>/session-state/latest.md (+ archive/)
15
+ * Storage: <runtime>/session-state/[session hash/]latest.md (+ archive/)
16
16
  * Exit: 0 always (fail-open)
17
17
  */
18
18
 
19
+ function logCrash(error) {
20
+ try {
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const logDir = require('./lib/hook-state-dir.cjs').hookStateDir();
24
+ fs.mkdirSync(logDir, { recursive: true });
25
+ fs.appendFileSync(path.join(logDir, 'hook-log.jsonl'), JSON.stringify({
26
+ ts: new Date().toISOString(), hook: 'state', status: 'crash', error: error.message,
27
+ }) + '\n');
28
+ } catch (_) {}
29
+ }
30
+
19
31
  try {
20
32
  const fs = require('fs');
21
33
  const path = require('path');
@@ -23,34 +35,73 @@ try {
23
35
  const crypto = require('crypto');
24
36
  const { execSync } = require('child_process');
25
37
  const { parseTranscript } = require('./lib/parser.cjs');
38
+ const { projectRoot } = require('./lib/project-root.cjs');
39
+ const { containsSecretKeyword } = require('./lib/secret-keywords.cjs');
26
40
  const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
27
41
 
28
42
  const EXPIRY_DAYS = 7;
29
43
  const MAX_ARCHIVES = 5;
30
44
  const MAX_AGENT_SECTIONS = 3;
31
45
  const MAX_MODIFIED_FILES_DISPLAY = 5;
32
- const TRACKED_POST_TOOL_EVENTS = new Set(['Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
46
+ const TRACKED_POST_TOOL_EVENTS = new Set(['Agent', 'Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
47
+ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
33
48
 
34
- function stateDir(cwd) {
49
+ function withStateLock(dir, callback) {
50
+ const lock = path.join(dir, '.write-lock');
51
+ const deadline = Date.now() + 3000;
52
+ while (Date.now() < deadline) {
53
+ try {
54
+ fs.mkdirSync(lock);
55
+ } catch (error) {
56
+ if (error.code !== 'EEXIST') return;
57
+ try {
58
+ if (Date.now() - fs.statSync(lock).mtimeMs > 15000) {
59
+ fs.rmSync(lock, { recursive: true, force: true });
60
+ continue;
61
+ }
62
+ } catch { continue; }
63
+ Atomics.wait(WAIT_BUFFER, 0, 0, 20);
64
+ continue;
65
+ }
66
+ try { return callback(); } finally {
67
+ try { fs.rmdirSync(lock); } catch { /* already released */ }
68
+ }
69
+ }
70
+ }
71
+
72
+ function assistantSummary(value) {
73
+ if (value === null || value === undefined) return '';
74
+ let text;
75
+ if (typeof value === 'object') {
76
+ const preferred = value.message ?? value.output ?? value.result ?? value.content;
77
+ text = typeof preferred === 'string' ? preferred : JSON.stringify(value);
78
+ } else {
79
+ text = String(value);
80
+ }
81
+ if (containsSecretKeyword(text)) return '[omitted: possible secret]';
82
+ const clean = text.replace(/\0/g, '').trim();
83
+ return clean.length > 600 ? `${clean.slice(0, 600)}…` : clean;
84
+ }
85
+
86
+ function stateDir(cwd, sessionId, create = true) {
35
87
  try {
36
- const local = runtimePath(cwd, 'session-state');
88
+ let dir;
37
89
  if (fs.existsSync(runtimeDir(cwd))) {
38
- if (!fs.existsSync(local)) fs.mkdirSync(local, { recursive: true });
39
- return local;
90
+ dir = runtimePath(cwd, 'session-state');
91
+ } else {
92
+ const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
93
+ dir = path.join(os.homedir(), '.claude', 'session-states', hash);
40
94
  }
41
-
42
- const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
43
- const global = path.join(os.homedir(), '.claude', 'session-states', hash);
44
- if (!fs.existsSync(global)) fs.mkdirSync(global, { recursive: true });
45
- return global;
95
+ if (sessionId) dir = path.join(dir, crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 16));
96
+ if (create && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
97
+ return dir;
46
98
  } catch {
47
99
  return null;
48
100
  }
49
101
  }
50
102
 
51
- function loadLatest(cwd) {
103
+ function loadLatest(dir) {
52
104
  try {
53
- const dir = stateDir(cwd);
54
105
  if (!dir) return null;
55
106
 
56
107
  const file = path.join(dir, 'latest.md');
@@ -109,7 +160,8 @@ try {
109
160
  timestamp: new Date().toISOString(),
110
161
  branch: process.env.GIT_BRANCH || '',
111
162
  todos: [],
112
- modifiedFiles: []
163
+ modifiedFiles: [],
164
+ lastAssistant: assistantSummary(stdinData.last_assistant_message),
113
165
  };
114
166
 
115
167
  if (stdinData.transcript_path && fs.existsSync(stdinData.transcript_path)) {
@@ -178,6 +230,7 @@ try {
178
230
  "## What's Left",
179
231
  ...(pending.length ? pending.map((todo) => `- [ ] ${todo.content}`) : ['- (All tasks completed)']),
180
232
  '',
233
+ ...(data.lastAssistant ? ['## Latest Assistant Message', data.lastAssistant, ''] : []),
181
234
  '## Key Files Modified',
182
235
  ...(data.modifiedFiles.length ? data.modifiedFiles.map((file) => `- ${file}`) : ['- (No file changes detected)']),
183
236
  ''
@@ -186,9 +239,10 @@ try {
186
239
 
187
240
  function buildAgentSection(data) {
188
241
  const agentType = data.agent_type || '';
189
- if (!agentType || agentType === 'unknown') return '';
242
+ const message = assistantSummary(data.last_assistant_message);
243
+ if (!agentType || agentType === 'unknown') return message ? `\n## Latest Assistant Message\n${message}\n` : '';
190
244
  const time = new Date().toISOString().slice(11, 19);
191
- return `\n## Agent Result: ${agentType} (${time})\n- Completed at ${time}\n`;
245
+ return `\n## Agent Result: ${agentType} (${time})\n- ${message || `Completed at ${time}`}\n`;
192
246
  }
193
247
 
194
248
  function takeRecentAgentSections(sections) {
@@ -229,11 +283,13 @@ try {
229
283
  }
230
284
 
231
285
  async function persistSnapshot(dir, data, options = {}) {
232
- const file = path.join(dir, 'latest.md');
233
- const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
234
- const content = mergeAgentSections(existing, buildStateContent(data));
235
- writeAtomic(file, content);
236
- if (options.archive) archive(dir);
286
+ withStateLock(dir, () => {
287
+ const file = path.join(dir, 'latest.md');
288
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
289
+ const content = mergeAgentSections(existing, buildStateContent(data));
290
+ writeAtomic(file, content);
291
+ if (options.archive) archive(dir);
292
+ });
237
293
  }
238
294
 
239
295
  async function main() {
@@ -244,14 +300,14 @@ try {
244
300
  const rawPayload = JSON.parse(stdin);
245
301
  const data = normalizeHookPayload(rawPayload);
246
302
  const event = data.hook_event_name || '';
247
- const cwd = data.cwd || process.cwd();
248
- const dir = stateDir(cwd);
303
+ const cwd = projectRoot(data);
304
+ const claudeSession = ['.claude', '.omp'].includes(runtimeDirName())
305
+ && typeof rawPayload.session_id === 'string' && rawPayload.session_id.length > 0;
306
+ const dir = stateDir(cwd, claudeSession ? rawPayload.session_id : null, event !== 'SessionStart');
249
307
 
250
308
  if (event === 'SessionStart') {
251
- const previous = loadLatest(cwd);
252
- // Claude only: grok (camelCase sessionId), omp and Codex keep printing the block.
253
- const claudeSession = runtimeDirName() === '.claude'
254
- && typeof rawPayload.session_id === 'string' && rawPayload.session_id.length > 0;
309
+ const previous = loadLatest(dir);
310
+ // Claude and omp: grok (camelCase sessionId) and Codex keep printing the block.
255
311
  if (previous && (!claudeSession || hasPriorContent(previous))) {
256
312
  console.log('\n=== Prior Execution Context ===');
257
313
  console.log(previous.trim());
@@ -275,13 +331,15 @@ try {
275
331
  const agentSection = buildAgentSection(data);
276
332
  if (!agentSection) process.exit(0);
277
333
 
278
- const file = path.join(dir, 'latest.md');
279
- const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
280
- const updated = existing
281
- ? appendAgentSection(existing, agentSection)
282
- : `${buildStateContent(await extractSessionData(data))}\n${agentSection}`;
283
-
284
- writeAtomic(file, updated);
334
+ const sessionData = await extractSessionData(data);
335
+ withStateLock(dir, () => {
336
+ const file = path.join(dir, 'latest.md');
337
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
338
+ const updated = existing
339
+ ? appendAgentSection(existing, agentSection)
340
+ : `${buildStateContent(sessionData)}\n${agentSection}`;
341
+ writeAtomic(file, updated);
342
+ });
285
343
  process.exit(0);
286
344
  }
287
345
 
@@ -294,24 +352,11 @@ try {
294
352
  process.exit(0);
295
353
  }
296
354
 
297
- main().catch(() => {
355
+ main().catch((error) => {
356
+ logCrash(error);
298
357
  process.exit(0);
299
358
  });
300
359
  } catch (error) {
301
- try {
302
- const fs = require('fs');
303
- const path = require('path');
304
- const logDir = require('./lib/hook-state-dir.cjs').hookStateDir();
305
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
306
- fs.appendFileSync(
307
- path.join(logDir, 'hook-log.jsonl'),
308
- JSON.stringify({
309
- ts: new Date().toISOString(),
310
- hook: 'state',
311
- status: 'crash',
312
- error: error.message
313
- }) + '\n'
314
- );
315
- } catch (_) {}
360
+ logCrash(error);
316
361
  process.exit(0);
317
362
  }
@@ -59,15 +59,10 @@
59
59
  "validate-docs.cjs",
60
60
  "validate-docs-reconstruct.cjs",
61
61
  "generate-skill-catalog.cjs",
62
- "validate-spec-output.cjs",
63
62
  "scan-staged-secrets.cjs",
64
63
  "workflow-policy.cjs",
65
64
  "spec-resolver.cjs",
66
65
  "spec-receipt.cjs",
67
- "spec-ground.cjs",
68
- "spec-semantic-model.cjs",
69
- "spec-final-state.cjs",
70
- "spec-readiness.cjs",
71
66
  "provenance.cjs"
72
67
  ]
73
68
  },
@@ -97,6 +92,18 @@
97
92
  "codex": ["agents/god_developer.toml"]
98
93
  },
99
94
  "runtimeFiles": [
95
+ "scripts/validate-spec-output.cjs",
96
+ "scripts/spec-ground.cjs",
97
+ "scripts/spec-authoring-digest.cjs",
98
+ "scripts/spec-semantic-model.cjs",
99
+ "scripts/spec-final-state.cjs",
100
+ "skills/code-review/references/verification-gate.md",
101
+ "scripts/change-firewall.cjs",
102
+ "scripts/spec-scaffold.cjs",
103
+ "scripts/spec-readiness.cjs",
104
+ "scripts/spec-authoring-validation.cjs",
105
+ "hooks/lib/context.cjs",
106
+ "hooks/lib/detect.cjs",
100
107
  "hooks/skill-router.cjs",
101
108
  "hooks/lib/skill-router-routes.cjs",
102
109
  "scripts/browser-tool.cjs",
@@ -136,7 +143,8 @@
136
143
  ".codex\\hooks\\semantic-review-authority.cjs",
137
144
  ".claude/hooks/task-scaffold-guard.cjs",
138
145
  ".codex/hooks/task-scaffold-guard.cjs",
139
- ".codex\\hooks\\task-scaffold-guard.cjs"
146
+ ".codex\\hooks\\task-scaffold-guard.cjs",
147
+ ".claude/hooks/inspector-block.cjs"
140
148
  ]
141
149
  },
142
150
  "runtime": {
@@ -161,12 +169,16 @@
161
169
  "hooks/lib/counter.cjs",
162
170
  "hooks/lib/git.cjs",
163
171
  "hooks/lib/config.cjs",
164
- "hooks/lib/context.cjs",
165
- "hooks/lib/detect.cjs",
166
172
  "hooks/lib/hook-state-dir.cjs",
167
173
  "hooks/lib/runtime-dir.cjs",
174
+ "hooks/lib/spec-touch.cjs",
175
+ "hooks/lib/project-root.cjs",
176
+ "hooks/lib/update-check.cjs",
177
+ "hooks/lib/rules-reservation.cjs",
168
178
  "hooks/lib/hook-payload.cjs",
179
+ "hooks/lib/inspect-paths.cjs",
169
180
  "hooks/lib/privacy-command-analysis.cjs",
181
+ "hooks/lib/privacy-paths.cjs",
170
182
  "hooks/lib/secret-keywords.cjs",
171
183
  "hooks/lib/runtime-path-safety.cjs"
172
184
  ]