@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
@@ -25,9 +25,16 @@ function cacheFile(projectRoot, sessionId) {
25
25
  return path.join(hookStateDir(projectRoot), `tollgate-${key}.txt`);
26
26
  }
27
27
 
28
+ let mutationObserver = false;
28
29
  try {
29
30
  const payload = readPayload();
31
+ mutationObserver = payload?.hook_event_name === 'PostToolUse';
30
32
  if (!payload) process.exit(0);
33
+ const context = getHookContext(payload);
34
+ const { touchSession } = require('./lib/spec-session-touch.cjs');
35
+ const touch = touchSession(payload, context);
36
+ // Mutation observers only persist paths. They must not inspect packet state or proof.
37
+ if (payload.hook_event_name === 'PostToolUse') process.exit(0);
31
38
  const loaded = loadSharedPolicy();
32
39
  if (!loaded.policy) {
33
40
  logCrash('spec-state', loaded.error);
@@ -35,22 +42,57 @@ try {
35
42
  process.exit(0);
36
43
  }
37
44
  const POLICY = loaded.policy;
45
+ require(path.join(path.dirname(loaded.path), 'provenance.cjs')).enableSnapshotMemo();
38
46
  const receiptLoaded = loadSharedReceipt();
39
47
  if (!receiptLoaded.receipt) throw receiptLoaded.error;
40
48
  const RECEIPT = receiptLoaded.receipt;
41
- const { projectRoot, runtime } = getHookContext(payload);
49
+ const { projectRoot, runtime } = context;
42
50
  if (runtime.spec?.tollgate === false) process.exit(0);
43
- const { resolveWorkflowCandidate } = require('./lib/spec-utils.cjs');
44
- const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
45
- const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
46
- const resolved = resolveWorkflowCandidate(projectRoot, runtime, explicitFeature, explicitPath);
51
+ const { findLegacyPackets, resolveWorkflowCandidate } = require('./lib/spec-utils.cjs');
52
+ const sessionId = touch.sessionId;
53
+ // A legacy packet (a specs/<x>/ directory without plan.md that the old flow left
54
+ // behind) is no longer read. Say so once per session instead of on every prompt.
55
+ const legacyPackets = findLegacyPackets(projectRoot, runtime);
56
+ if (legacyPackets.length > 0) {
57
+ const noticeFile = sessionId
58
+ ? path.join(hookStateDir(projectRoot), `legacy-notice-${crypto.createHash('sha256').update(String(sessionId)).digest('hex').slice(0, 16)}.json`)
59
+ : null;
60
+ let noticed = [];
61
+ if (noticeFile) {
62
+ try { noticed = JSON.parse(fs.readFileSync(noticeFile, 'utf8')); } catch { noticed = []; }
63
+ if (!Array.isArray(noticed)) noticed = [];
64
+ }
65
+ const fresh = legacyPackets.filter((name) => !noticed.includes(name));
66
+ for (const name of fresh) {
67
+ process.stdout.write(`> ⚠️ \`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.\n`);
68
+ }
69
+ if (noticeFile && fresh.length > 0) {
70
+ try { atomicWrite(noticeFile, `${JSON.stringify([...noticed, ...fresh].sort())}\n`); } catch { /* repeats the notice */ }
71
+ }
72
+ }
73
+ const explicitFeature = payload.explicitFeature ?? payload.featureName ?? payload.feature ?? null;
74
+ const explicitPath = payload.explicitPath ?? payload.specPath ?? payload.spec_path ?? payload.featurePath ?? null;
75
+ const hasExplicit = explicitFeature !== null || explicitPath !== null;
76
+ if (sessionId && !hasExplicit && touch.touched.length === 0) process.exit(0);
77
+ let resolved;
78
+ if (sessionId && !hasExplicit) {
79
+ const names = touch.names.length ? touch.names : touch.touched;
80
+ const candidates = names.map((name) => resolveWorkflowCandidate(projectRoot, runtime, name, null));
81
+ resolved = candidates.find((candidate) => candidate?.error);
82
+ const active = candidates.filter((candidate) => candidate && !candidate.error && !candidate.allTasksDone);
83
+ if (!resolved) resolved = active.length > 1
84
+ ? { error: 'multiple_active', candidates: active.map((candidate) => candidate.featureName) }
85
+ : active[0] || null;
86
+ } else {
87
+ resolved = resolveWorkflowCandidate(projectRoot, runtime, explicitFeature, explicitPath);
88
+ }
47
89
  if (!resolved) process.exit(0);
48
90
  if (resolved.error === 'multiple_active') {
49
91
  process.stdout.write(`> ⚠️ Multiple active specs detected: ${resolved.candidates.join(', ')}. Provide explicit feature target or resolve ambiguity. Tollgate paused.\n`);
50
92
  process.exit(0);
51
93
  }
52
94
  if (resolved.error === 'invalid_specs') {
53
- process.stdout.write(`> ⚠️ Invalid spec JSON detected: ${resolved.candidates.join(', ')}. ${resolved.reason}. Fix or remove malformed spec.json. Tollgate paused.\n`);
95
+ process.stdout.write(`> ⚠️ Invalid workflow packet detected: ${resolved.candidates.join(', ')}. ${resolved.reason}. Fix the packet's plan.md or task files. Tollgate paused.\n`);
54
96
  process.exit(0);
55
97
  }
56
98
  if (resolved.error === 'explicit_not_found' || resolved.error === 'explicit_malformed') {
@@ -60,18 +102,16 @@ try {
60
102
  }
61
103
  if (resolved.error) process.exit(0);
62
104
  const active = resolved;
63
- const processWorkflow = active.layoutKind === 'process-v3';
64
105
  const runtimeContext = POLICY.deriveRuntimeContext({
65
106
  projectRoot,
66
107
  specsRoot: active.specsDir,
67
- specFile: active.stateFile || active.specFile || path.join(active.specsDir, active.featureName, 'spec.json'),
108
+ specFile: active.stateFile,
68
109
  featureName: active.featureName,
69
- runtimeSession: payload.session_id || payload.sessionId || payload.sessionID || payload.session?.id,
110
+ runtimeSession: sessionId,
70
111
  });
71
112
 
72
- const phase = active.phase || active.spec?.current_phase || active.spec?.phase || 'unknown';
73
- const taskRegistry = active.taskRegistry || active.spec?.task_registry || {};
74
- const flashTasks = POLICY.flashState(taskRegistry);
113
+ const phase = active.phase || 'unknown';
114
+ const taskRegistry = active.taskRegistry || {};
75
115
  const tasks = Object.entries(taskRegistry);
76
116
  const counts = tasks.reduce((result, [, task]) => {
77
117
  const status = task?.status || 'pending';
@@ -79,13 +119,7 @@ try {
79
119
  return result;
80
120
  }, {});
81
121
  const featureDir = path.join(active.specsDir, active.featureName);
82
- const statuses = new Map(tasks.map(([taskPath, task]) => [
83
- taskPath,
84
- task?.status || 'pending'
85
- ]));
86
- const dependencyProof = processWorkflow
87
- ? RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY)
88
- : {};
122
+ const dependencyProof = RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY);
89
123
  const taskState = tasks.map(([taskPath, task]) => [
90
124
  taskPath,
91
125
  task?.status || 'pending',
@@ -101,12 +135,8 @@ try {
101
135
  const next = tasks.find(([, task]) => (
102
136
  active.queueReady !== false
103
137
  && (task?.status || 'pending') === 'pending'
104
- && (task?.dependencies || []).every((dependency) => (
105
- processWorkflow ? dependencyProof[dependency]?.eligible === true : statuses.get(dependency) === 'done'
106
- ))
138
+ && (task?.dependencies || []).every((dependency) => dependencyProof[dependency]?.eligible === true)
107
139
  ));
108
- const featureReceiptPresent = !processWorkflow
109
- && RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
110
140
  const stateKey = JSON.stringify({
111
141
  project_root: runtimeContext.project_root,
112
142
  specs_root: runtimeContext.specs_root,
@@ -124,14 +154,13 @@ try {
124
154
  proof_state: proofState,
125
155
  done: counts.done || 0,
126
156
  total: tasks.length,
127
- feature_receipt_present: featureReceiptPresent,
128
157
  });
129
158
  const cache = cacheFile(projectRoot, runtimeContext.runtime_session);
130
159
  let previous = '';
131
160
  try { previous = fs.readFileSync(cache, 'utf8').trim(); } catch { /* first run */ }
132
161
 
133
162
  if (previous === stateKey) {
134
- const migration = processWorkflow && active.queueReady === false
163
+ const migration = active.queueReady === false
135
164
  ? ' Add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.'
136
165
  : '';
137
166
  process.stdout.write(
@@ -145,30 +174,23 @@ try {
145
174
  const lines = [
146
175
  `### Spec state changed: \`${active.featureName}\``,
147
176
  `- Phase: \`${phase}\` | Tasks: ${counts.done || 0} done / ${tasks.length} total`
177
+ + (tasks.length > 0 ? ` (${counts.in_progress || 0} in_progress, ${counts.pending || 0} pending, ${counts.blocked || 0} blocked)` : '')
148
178
  ];
149
179
  if (next) lines.push(`- Next unblocked: \`${next[0]}\``);
150
- if (flashTasks.length > 0) {
151
- lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. A PASS proof keeps the persisted task in_progress until explicit sync-finalize.`);
152
- }
153
- if (processWorkflow) {
154
- if (active.queueReady === false) {
155
- lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
156
- }
157
- lines.push(
158
- '- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.',
159
- `- Workflow source: \`specs/${active.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.`,
160
- '- Hooks revalidate receipt bytes but never grant approval.'
161
- );
162
- } else {
163
- lines.push(
164
- '- Sync `spec.json` and task Markdown status only after verified work; task proof belongs in `receipts/<task-basename>.md`.',
165
- `- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`,
166
- `- Validate with \`node .codex/scripts/validate-spec-output.cjs specs/${active.featureName}\`.`,
167
- '- Hooks revalidate receipt bytes but never grant approval.'
168
- );
180
+ if (active.queueReady === false) {
181
+ lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
169
182
  }
183
+ lines.push(
184
+ '- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.',
185
+ `- Workflow source: \`specs/${active.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.`,
186
+ '- Hooks revalidate receipt bytes but never grant approval.'
187
+ );
170
188
  process.stdout.write(`${lines.join('\n')}\n`);
171
189
  } catch (error) {
190
+ if (error.code === 'no_git_history') {
191
+ console.log('Repo has no commits yet; create the first commit, then record Base/Head after it.');
192
+ process.exit(0);
193
+ }
172
194
  logCrash('spec-state', error);
173
- emitControlledFailure(error.message);
195
+ if (!mutationObserver) emitControlledFailure(error.message);
174
196
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { containsSecretKeyword } = require('./lib/secret-keywords.cjs');
6
7
  const {
7
8
  getHookContext,
8
9
  logCrash,
@@ -34,6 +35,11 @@ function summary(value, limit = 600) {
34
35
  return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
35
36
  }
36
37
 
38
+ function assistantSummary(value) {
39
+ const text = summary(value, Number.MAX_SAFE_INTEGER);
40
+ return containsSecretKeyword(text) ? '[omitted: possible secret]' : summary(text);
41
+ }
42
+
37
43
  function normalizeTodos(items) {
38
44
  if (!Array.isArray(items)) return [];
39
45
  return items.map((item, index) => ({
@@ -74,11 +80,24 @@ function isTrackedTool(toolName) {
74
80
  const name = String(toolName || '').toLowerCase();
75
81
  return name === 'agent'
76
82
  || name === 'spawn_agent'
83
+ || name === 'collaborationspawn_agent'
77
84
  || name === 'update_plan'
78
85
  || name.includes('task')
79
86
  || name.includes('todo');
80
87
  }
81
88
 
89
+ function hasPriorContent(text) {
90
+ const filler = [
91
+ /^#{1,6}\s/,
92
+ /^<!--.*-->$/,
93
+ /^- \((?:No completed tasks recorded|No pending tasks recorded|All tasks completed|No file changes detected)\)$/,
94
+ /^- Completed at \d{2}:\d{2}:\d{2}$/,
95
+ /^- Completed without a result message\.$/,
96
+ ];
97
+ return text.split('\n').map((line) => line.trim())
98
+ .some((line) => line && !filler.some((pattern) => pattern.test(line)));
99
+ }
100
+
82
101
  function loadPrevious(projectRoot, sessionId) {
83
102
  try {
84
103
  const dir = stateDir(projectRoot, sessionId);
@@ -87,7 +106,9 @@ function loadPrevious(projectRoot, sessionId) {
87
106
  if (!fs.existsSync(file)) return '';
88
107
  const text = fs.readFileSync(file, 'utf8');
89
108
  const generated = text.match(/<!-- Generated: (.+?) -->/)?.[1];
90
- if (!generated || Date.now() - Date.parse(generated) > EXPIRY_MS) return '';
109
+ const timestamp = Date.parse(generated);
110
+ if (!generated || !Number.isFinite(timestamp) || Date.now() - timestamp > EXPIRY_MS) return '';
111
+ if (!hasPriorContent(text)) return '';
91
112
  return text;
92
113
  } catch {
93
114
  return '';
@@ -118,7 +139,7 @@ try {
118
139
  const data = readData(dir);
119
140
  const input = payload.tool_input || {};
120
141
  const response = payload.tool_response;
121
- updateTaskState(data, input, response);
142
+ if (!['spawn_agent', 'collaborationspawn_agent'].includes(String(toolName).toLowerCase())) updateTaskState(data, input, response);
122
143
  data.lastToolEvent = `${toolName}: ${summary(response) || summary(input)}`.slice(0, 700);
123
144
  refreshGit(data, projectRoot);
124
145
  writeData(dir, data);
@@ -135,7 +156,7 @@ try {
135
156
  data.agentResults.push({
136
157
  id,
137
158
  type: String(payload.agent_type || 'unknown'),
138
- message: summary(payload.last_assistant_message)
159
+ message: assistantSummary(payload.last_assistant_message)
139
160
  });
140
161
  refreshGit(data, projectRoot);
141
162
  writeData(dir, data);
@@ -147,7 +168,7 @@ try {
147
168
  if (event === 'Stop') {
148
169
  withStateLock(projectRoot, sessionId, (dir) => {
149
170
  const data = readData(dir);
150
- data.lastAssistant = summary(payload.last_assistant_message);
171
+ data.lastAssistant = assistantSummary(payload.last_assistant_message);
151
172
  refreshGit(data, projectRoot);
152
173
  writeData(dir, data);
153
174
  archive(dir);
@@ -10,11 +10,6 @@
10
10
  "command": "node \".codex/hooks/session.cjs\"",
11
11
  "commandWindows": "node \".codex/hooks/session.cjs\""
12
12
  },
13
- {
14
- "type": "command",
15
- "command": "node \".codex/hooks/docs-sync.cjs\"",
16
- "commandWindows": "node \".codex/hooks/docs-sync.cjs\""
17
- },
18
13
  {
19
14
  "type": "command",
20
15
  "command": "node \".codex/hooks/state.cjs\"",
@@ -23,6 +18,18 @@
23
18
  ]
24
19
  }
25
20
  ],
21
+ "PreCompact": [
22
+ {
23
+ "matcher": "auto|manual",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "node \".codex/hooks/precompact.cjs\"",
28
+ "commandWindows": "node \".codex/hooks/precompact.cjs\""
29
+ }
30
+ ]
31
+ }
32
+ ],
26
33
  "SubagentStart": [
27
34
  {
28
35
  "matcher": "*",
@@ -57,13 +64,18 @@
57
64
  "type": "command",
58
65
  "command": "node \".codex/hooks/spec-state.cjs\"",
59
66
  "commandWindows": "node \".codex/hooks/spec-state.cjs\""
67
+ },
68
+ {
69
+ "type": "command",
70
+ "command": "node \".codex/hooks/ask-guard.cjs\"",
71
+ "commandWindows": "node \".codex/hooks/ask-guard.cjs\""
60
72
  }
61
73
  ]
62
74
  }
63
75
  ],
64
76
  "PreToolUse": [
65
77
  {
66
- "matcher": "Bash|exec_command|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*|shell",
78
+ "matcher": "Bash|exec_command|shell|apply_patch|mcp__.*|collaborationspawn_agent|collaborationwait_agent",
67
79
  "hooks": [
68
80
  {
69
81
  "type": "command",
@@ -76,11 +88,21 @@
76
88
  "commandWindows": "node \".codex/hooks/inspect-block.cjs\""
77
89
  }
78
90
  ]
91
+ },
92
+ {
93
+ "matcher": "apply_patch",
94
+ "hooks": [
95
+ {
96
+ "type": "command",
97
+ "command": "node \".codex/hooks/ask-guard.cjs\"",
98
+ "commandWindows": "node \".codex/hooks/ask-guard.cjs\""
99
+ }
100
+ ]
79
101
  }
80
102
  ],
81
103
  "PostToolUse": [
82
104
  {
83
- "matcher": "Agent|spawn_agent|update_plan|task|Task|TodoWrite",
105
+ "matcher": "collaborationspawn_agent|Agent|spawn_agent|update_plan",
84
106
  "hooks": [
85
107
  {
86
108
  "type": "command",
@@ -88,6 +110,16 @@
88
110
  "commandWindows": "node \".codex/hooks/state.cjs\""
89
111
  }
90
112
  ]
113
+ },
114
+ {
115
+ "matcher": "apply_patch",
116
+ "hooks": [
117
+ {
118
+ "type": "command",
119
+ "command": "node \".codex/hooks/spec-state.cjs\"",
120
+ "commandWindows": "node \".codex/hooks/spec-state.cjs\""
121
+ }
122
+ ]
91
123
  }
92
124
  ],
93
125
  "Stop": [
@@ -5,16 +5,22 @@ Hooks are instruction boundaries. Do not bypass or work around them.
5
5
  ## Privacy Block Hook
6
6
 
7
7
  Codex cannot pause a blocked tool call for an interactive permission question.
8
- CafeKit therefore denies the sensitive access and emits one exact approval
9
- phrase tied to the current session, requested path, tool, and expiry.
8
+ CafeKit denies sensitive access and offers a short reply and an exact fallback
9
+ phrase. A grant is reusable for one hour in the same session for exactly the
10
+ approved paths; it is not tied to a tool.
10
11
 
11
12
  Required flow:
12
13
 
13
14
  1. Stop the blocked access and show the user the hook's reason.
14
- 2. Ask the user to send the exact approval phrase if they want one retry.
15
- 3. Do not write, repeat as a user message, or otherwise simulate that phrase.
16
- 4. Retry only after a later real user prompt exactly matches it.
17
- 5. Use the resulting token once, only for the originally blocked operation.
15
+ 2. Ask one privacy question as the only question at the end of the turn, naming
16
+ exactly the files shown in the denial. Do not add any other yes/no question.
17
+ 3. Explain that the user's very next message may be `y`, `yes`, or `có` only
18
+ while exactly one request is pending; any other reply cancels the shortcut.
19
+ The exact `APPROVE CAFEKIT PRIVACY <id>` prompt remains the fallback.
20
+ 4. Never send, repeat as a user message, or simulate either a short approval or
21
+ the exact fallback phrase. Wait for a real user prompt and the approval hook.
22
+ 5. After approval, tell the user which paths were just granted before reading
23
+ them. Access only those exact paths during the one-hour session grant.
18
24
 
19
25
  Never use another command, path, encoding, subprocess, or side channel to read a
20
26
  privacy-blocked file without that explicit approval.
@@ -24,6 +30,7 @@ privacy-blocked file without that explicit approval.
24
30
  - If a hook reports state drift, run the appropriate sync or audit flow before continuing.
25
31
  - If a hook rejects task completion, keep the task `in_progress` or `blocked` until proof exists.
26
32
  - Do not mark a task `done` unless the matching task file contains a valid verification receipt.
33
+ - Read `state-sync.md` for the receipt contract.
27
34
 
28
35
  ## Hook Failure Handling
29
36
 
@@ -33,17 +33,11 @@ Do the work yourself when it takes a handful of tool calls. Delegate genuinely i
33
33
  - Runtime-specific instructions live in that runtime's own managed block, not in `CORE`.
34
34
  - In a combined install, consume `CORE` plus your native block only. Ignore managed blocks not owned by your runtime. If ownership is unclear, treat the file as `CORE`-only (fail-safe).
35
35
 
36
- ## Commands
36
+ ## Project notes
37
37
 
38
- <!-- Add project-specific install, test, lint, and build commands here. Keep commands executable. -->
39
-
40
- ## Do not touch
41
-
42
- <!-- List files, directories, generated artifacts, or secrets that tasks must leave unchanged. -->
43
-
44
- ## Slow or expensive
45
-
46
- <!-- Note commands, environments, or operations that need explicit planning before running. -->
38
+ Write project-specific commands, files tasks must leave unchanged, and slow or
39
+ expensive operations after the `CAFEKIT CORE END` marker. Everything between the
40
+ CafeKit markers is replaced on every install.
47
41
 
48
42
  ## Language Consistency <!-- cafekit:lang -->
49
43
 
@@ -38,16 +38,16 @@ export const BRIDGE_TIMEOUT_MS = 8000;
38
38
  * Mirrors src/claude/settings/settings.json; `omp-bridge.test.js` fails if the two drift.
39
39
  */
40
40
  export const DISPATCH = {
41
- session_start: { event: 'SessionStart', hooks: ['session.cjs', 'docs-sync.cjs', 'state.cjs'] },
41
+ session_start: { event: 'SessionStart', hooks: ['session.cjs', 'state.cjs'] },
42
42
  session_compact: { event: 'SessionStart', hooks: ['session.cjs'], source: 'compact' },
43
43
  session_before_compact: { event: 'PreCompact', hooks: ['precompact.cjs'] },
44
44
  input: { event: 'UserPromptSubmit', hooks: ['secret-output-guardrail.cjs', 'rules.cjs', 'spec-state.cjs'] },
45
45
  tool_call: { event: 'PreToolUse', hooks: ['privacy-block.cjs', 'inspect-block.cjs'] },
46
- tool_result: { event: 'PostToolUse', hooks: ['state.cjs'] },
46
+ tool_result: { event: 'PostToolUse', hooks: ['state.cjs', 'spec-state.cjs'] },
47
47
  session_stop: { event: 'Stop', hooks: ['spec-gate.cjs', 'state.cjs'] },
48
48
  };
49
49
 
50
- /** One id per extension load. omp loads an extension once per session, so this is per-session. */
50
+ /** Fallback id per extension load, for an omp whose context has no session manager. */
51
51
  const SESSION_ID = `omp-${randomUUID()}`;
52
52
 
53
53
  export function hooksDir() {
@@ -55,13 +55,21 @@ export function hooksDir() {
55
55
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
56
56
  }
57
57
 
58
+ function sessionIdFrom(ctx) {
59
+ try {
60
+ const id = ctx?.sessionManager?.getSessionId?.();
61
+ return typeof id === 'string' && id ? id : null;
62
+ } catch { return null; }
63
+ }
64
+
58
65
  /** Build the Claude-shaped payload a CafeKit hook reads on stdin. */
59
66
  export function shapePayload(kind, event, ctx) {
60
67
  const entry = DISPATCH[kind];
61
68
  const cwd = ctx?.cwd || process.cwd();
62
69
  const payload = {
63
70
  hook_event_name: entry.event,
64
- session_id: event?.session_id || SESSION_ID,
71
+ // omp's own id first: session_stop carries it, and touch and state must stay in one place.
72
+ session_id: event?.session_id || sessionIdFrom(ctx) || SESSION_ID,
65
73
  cwd,
66
74
  };
67
75
  switch (kind) {