@haposoft/cafekit 0.16.0-rc.8 → 0.16.0

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 (297) hide show
  1. package/README.md +116 -27
  2. package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
  3. package/bin/__tests__/codex-hooks.test.js +50 -0
  4. package/bin/__tests__/codex-native.test.js +1882 -70
  5. package/bin/__tests__/develop-contract.test.js +400 -23
  6. package/bin/__tests__/installer-safety.test.js +3 -3
  7. package/bin/__tests__/optional-skill-inventory.test.js +249 -0
  8. package/bin/__tests__/output-styles.test.js +214 -0
  9. package/bin/__tests__/package-inventory.test.js +1735 -98
  10. package/bin/__tests__/runtime-schema.test.js +111 -0
  11. package/bin/__tests__/secret-output-guardrail.test.js +122 -0
  12. package/bin/__tests__/skill-routing-source.test.js +172 -0
  13. package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
  14. package/bin/install.js +10 -0
  15. package/bin/lib/codex-install.js +170 -5
  16. package/bin/lib/context.js +14 -0
  17. package/bin/lib/i18n.js +12 -6
  18. package/bin/lib/skill-deps.js +5 -5
  19. package/bin/phases/claude-runtime.js +23 -0
  20. package/bin/phases/claude-settings.js +8 -0
  21. package/bin/phases/codex-hooks.js +156 -0
  22. package/bin/phases/codex-runtime.js +29 -38
  23. package/bin/phases/copy-payload.js +8 -2
  24. package/bin/phases/select-skill-bundles.js +96 -0
  25. package/bin/phases/skill-inventory.js +71 -0
  26. package/bin/phases/skills-setup.js +9 -2
  27. package/bin/phases/write-metadata.js +8 -5
  28. package/package.json +8 -2
  29. package/src/claude/CLAUDE.md +3 -3
  30. package/src/claude/agents/brainstormer.md +52 -69
  31. package/src/claude/agents/code-auditor.md +32 -14
  32. package/src/claude/agents/debugger.md +35 -20
  33. package/src/claude/agents/deployer.md +12 -5
  34. package/src/claude/agents/docs-keeper.md +11 -1
  35. package/src/claude/agents/implementer.md +25 -13
  36. package/src/claude/agents/inspector.md +6 -3
  37. package/src/claude/agents/project-manager.md +1 -1
  38. package/src/claude/agents/researcher.md +26 -55
  39. package/src/claude/agents/spec-maker.md +12 -7
  40. package/src/claude/agents/strategist.md +52 -0
  41. package/src/claude/agents/test-runner.md +117 -132
  42. package/src/claude/agents/ui-ux-designer.md +1 -1
  43. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
  44. package/src/claude/hooks/__tests__/precompact.test.js +158 -0
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +10 -3
  46. package/src/claude/hooks/__tests__/state.test.js +5 -0
  47. package/src/claude/hooks/__tests__/statusline.test.js +196 -0
  48. package/src/claude/hooks/agent.cjs +1 -1
  49. package/src/claude/hooks/docs-sync.cjs +12 -10
  50. package/src/claude/hooks/inspect-block.cjs +1 -1
  51. package/src/claude/hooks/lib/config.cjs +3 -0
  52. package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
  53. package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
  54. package/src/claude/hooks/precompact.cjs +105 -0
  55. package/src/claude/hooks/rules.cjs +1 -1
  56. package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
  57. package/src/claude/hooks/session.cjs +27 -1
  58. package/src/claude/hooks/spec-gate.cjs +3 -3
  59. package/src/claude/hooks/spec-state.cjs +2 -2
  60. package/src/claude/hooks/state.cjs +1 -1
  61. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  62. package/src/claude/hooks/usage.cjs +1 -1
  63. package/src/claude/migration-manifest.json +29 -15
  64. package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
  65. package/src/claude/output-styles/coding-level-1-junior.md +124 -0
  66. package/src/claude/output-styles/coding-level-2-mid.md +146 -0
  67. package/src/claude/output-styles/coding-level-3-senior.md +148 -0
  68. package/src/claude/output-styles/coding-level-4-lead.md +159 -0
  69. package/src/claude/output-styles/coding-level-5-god.md +91 -0
  70. package/src/claude/references/debugger/core-philosophy.md +5 -3
  71. package/src/claude/references/debugger/frontend-verification.md +2 -2
  72. package/src/claude/references/debugger/log-ci-analysis.md +13 -1
  73. package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
  74. package/src/claude/references/debugger/performance-diagnostics.md +2 -2
  75. package/src/claude/references/debugger/root-cause-tracing.md +37 -6
  76. package/src/claude/references/debugger/side-effect-gate.md +3 -1
  77. package/src/claude/references/debugger/verification-protocol.md +13 -12
  78. package/src/claude/rules/orchestrator.md +7 -1
  79. package/src/claude/rules/process-management.md +39 -0
  80. package/src/claude/rules/review-audit-self-decision.md +48 -0
  81. package/src/claude/rules/skill-domain-routing.md +26 -24
  82. package/src/claude/rules/skill-workflow-routing.md +22 -14
  83. package/src/claude/rules/workflow.md +3 -3
  84. package/src/claude/runtime.json +1 -0
  85. package/src/claude/runtime.schema.json +205 -0
  86. package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
  87. package/src/claude/scripts/provenance.cjs +18 -5
  88. package/src/claude/scripts/workflow-policy.cjs +1 -1
  89. package/src/claude/settings/settings.json +16 -0
  90. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  91. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  92. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  93. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  94. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  95. package/src/claude/skills/code-review/SKILL.md +43 -25
  96. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  97. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  98. package/src/claude/skills/debug/SKILL.md +48 -19
  99. package/src/claude/skills/delegate/SKILL.md +1 -1
  100. package/src/claude/skills/develop/SKILL.md +92 -65
  101. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  102. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  103. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  104. package/src/claude/skills/docs/SKILL.md +42 -23
  105. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  106. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  107. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  108. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  109. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  110. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  111. package/src/claude/skills/docx/SKILL.md +1 -1
  112. package/src/claude/skills/git/SKILL.md +1 -1
  113. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  114. package/src/claude/skills/git/references/finish-branch.md +1 -1
  115. package/src/claude/skills/hotfix/SKILL.md +159 -170
  116. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  117. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  118. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  119. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  120. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  121. package/src/claude/skills/inspect/SKILL.md +55 -46
  122. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  123. package/src/claude/skills/loop/SKILL.md +114 -0
  124. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  125. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  126. package/src/claude/skills/pdf/SKILL.md +1 -1
  127. package/src/claude/skills/pptx/SKILL.md +1 -1
  128. package/src/claude/skills/question/SKILL.md +16 -13
  129. package/src/claude/skills/research/SKILL.md +70 -43
  130. package/src/claude/skills/route/SKILL.md +94 -0
  131. package/src/claude/skills/route/references/agent-timing.md +46 -0
  132. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  133. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  134. package/src/claude/skills/specs/SKILL.md +41 -60
  135. package/src/claude/skills/specs/references/review.md +11 -8
  136. package/src/claude/skills/specs/references/templates.md +34 -18
  137. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  138. package/src/claude/skills/specs/templates/research.md +2 -2
  139. package/src/claude/skills/sync/SKILL.md +6 -6
  140. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  141. package/src/claude/skills/test/SKILL.md +120 -120
  142. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  143. package/src/claude/skills/test/references/failure-triage.md +55 -121
  144. package/src/claude/skills/test/references/test-memory.md +20 -34
  145. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  146. package/src/claude/skills/web-testing/SKILL.md +1 -1
  147. package/src/claude/skills/xlsx/SKILL.md +1 -1
  148. package/src/claude/status.cjs +135 -33
  149. package/src/codex/AGENTS.md +14 -4
  150. package/src/codex/hooks/docs-sync.cjs +8 -3
  151. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  152. package/src/codex/hooks/rules.cjs +29 -1
  153. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  154. package/src/codex/hooks/spec-gate.cjs +3 -8
  155. package/src/codex/hooks/spec-state.cjs +2 -1
  156. package/src/codex/hooks.json +5 -0
  157. package/src/codex/runtime.json +2 -0
  158. package/src/common/.env.example +1 -1
  159. package/src/claude/skills/backend-development/SKILL.md +0 -101
  160. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  161. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  162. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  163. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  164. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  165. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  166. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  167. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  168. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  169. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  170. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  171. package/src/claude/skills/devops/.env.example +0 -76
  172. package/src/claude/skills/devops/SKILL.md +0 -102
  173. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  174. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  175. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  176. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  177. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  178. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  179. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  180. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  181. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  182. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  183. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  184. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  185. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  186. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  187. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  188. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  189. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  190. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  191. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  192. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  193. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  194. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  195. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  196. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  197. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  198. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  199. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  200. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  201. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  202. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  203. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  204. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  205. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  206. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  207. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  208. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  209. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  210. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  211. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  212. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  213. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  214. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  215. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  216. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  217. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  218. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  219. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  220. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  221. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  222. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  223. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  224. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  225. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  226. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  227. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  228. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  229. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  230. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  231. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  232. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  233. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  234. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  235. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  236. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  237. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  238. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  239. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  240. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  241. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  242. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  243. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  244. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  245. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  246. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  247. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  248. package/src/claude/skills/react-best-practices/README.md +0 -123
  249. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  250. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  251. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  252. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  253. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  254. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  255. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  256. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  257. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  258. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  259. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  260. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  261. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  262. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  263. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  264. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  265. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  266. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  267. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  268. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  269. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  270. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  271. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  272. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  273. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  274. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  275. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  276. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  277. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  278. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  279. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  280. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  281. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  282. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  283. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  284. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  285. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  286. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  287. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  288. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  289. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  290. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  291. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  292. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  293. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  294. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  295. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  296. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  297. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ // Behavioral tests for precompact.cjs and the compact-restart readback in
4
+ // session.cjs. Both hooks run as real subprocesses against a temporary git
5
+ // fixture; the capture is asserted from the file it writes and from what the
6
+ // next session actually prints.
7
+
8
+ const { test } = require('node:test');
9
+ const assert = require('node:assert');
10
+ const { spawnSync } = require('node:child_process');
11
+ const fs = require('node:fs');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+
15
+ const PRECOMPACT = path.join(__dirname, '..', 'precompact.cjs');
16
+ const SESSION = path.join(__dirname, '..', 'session.cjs');
17
+ const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
18
+ const RECOVERY = path.join(hookStateDir(), 'compact-recovery.json');
19
+
20
+ function fixture() {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-'));
22
+ for (const args of [
23
+ ['init', '-q', '-b', 'work'],
24
+ ['config', 'user.email', 'cafekit@example.invalid'],
25
+ ['config', 'user.name', 'CafeKit Test'],
26
+ ]) {
27
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
28
+ assert.strictEqual(result.status, 0, result.stderr);
29
+ }
30
+ fs.writeFileSync(path.join(dir, 'tracked.txt'), 'one\n');
31
+ for (const args of [['add', '-A'], ['commit', '-qm', 'fixture']]) {
32
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
33
+ assert.strictEqual(result.status, 0, result.stderr);
34
+ }
35
+ return dir;
36
+ }
37
+
38
+ function run(hook, dir, payload) {
39
+ const result = spawnSync(process.execPath, [hook], {
40
+ cwd: dir,
41
+ input: JSON.stringify(payload),
42
+ encoding: 'utf8',
43
+ env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
44
+ });
45
+ return { code: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
46
+ }
47
+
48
+ function clearCapture() {
49
+ try { fs.unlinkSync(RECOVERY); } catch { /* absent */ }
50
+ }
51
+
52
+ function readCapture() {
53
+ return JSON.parse(fs.readFileSync(RECOVERY, 'utf8'));
54
+ }
55
+
56
+ test('PreCompact records branch, HEAD, dirty count and the in-flight task', () => {
57
+ const dir = fixture();
58
+ try {
59
+ fs.writeFileSync(path.join(dir, 'tracked.txt'), 'two\n');
60
+ fs.writeFileSync(path.join(dir, 'untracked.txt'), 'scratch\n');
61
+ const featureDir = path.join(dir, 'specs', 'demo');
62
+ fs.mkdirSync(featureDir, { recursive: true });
63
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
64
+ for (const [name, status] of [['task-01-one.md', 'in_progress'], ['task-02-two.md', 'pending']]) {
65
+ fs.writeFileSync(path.join(featureDir, name), [
66
+ `# ${name}`, '', `Status: ${status}`, '',
67
+ '## Dependencies', '', '- none', '',
68
+ '## Verification Plan', '', '- Command: node --test', '',
69
+ ].join('\n'));
70
+ }
71
+
72
+ clearCapture();
73
+ const result = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'auto', session_id: 'compact-session', cwd: dir });
74
+ assert.strictEqual(result.code, 0, result.stderr);
75
+
76
+ const record = readCapture();
77
+ assert.strictEqual(record.branch, 'work');
78
+ assert.match(record.head, /^[0-9a-f]{7,}$/);
79
+ assert.strictEqual(record.dirty_count, 3, 'one modified file, one untracked file, one untracked specs directory');
80
+ assert.strictEqual(record.trigger, 'auto');
81
+ assert.strictEqual(record.session_id, 'compact-session');
82
+ assert.strictEqual(record.workflow.feature, 'demo');
83
+ assert.deepStrictEqual(record.workflow.in_progress, ['task-01-one.md']);
84
+ assert.strictEqual(record.workflow.counts.in_progress, 1);
85
+ assert.strictEqual(record.workflow.counts.pending, 1);
86
+ } finally {
87
+ fs.rmSync(dir, { recursive: true, force: true });
88
+ clearCapture();
89
+ }
90
+ });
91
+
92
+ test('a compact restart prints the captured anchors alongside the authorization warning', () => {
93
+ const dir = fixture();
94
+ try {
95
+ clearCapture();
96
+ assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', cwd: dir }).code, 0);
97
+ const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
98
+ assert.strictEqual(restart.code, 0, restart.stderr);
99
+ assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/, 'the existing warning must survive');
100
+ assert.match(restart.stdout, /State before compaction/);
101
+ assert.match(restart.stdout, /branch work/);
102
+ assert.match(restart.stdout, /Re-read these from disk before acting/);
103
+ } finally {
104
+ fs.rmSync(dir, { recursive: true, force: true });
105
+ clearCapture();
106
+ }
107
+ });
108
+
109
+ test('a compact restart without a capture still prints the warning and nothing invented', () => {
110
+ const dir = fixture();
111
+ try {
112
+ clearCapture();
113
+ const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
114
+ assert.strictEqual(restart.code, 0, restart.stderr);
115
+ assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/);
116
+ assert.doesNotMatch(restart.stdout, /State before compaction/, 'no capture means no claim about prior state');
117
+ } finally {
118
+ fs.rmSync(dir, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ test('a normal session start prints no compaction anchors', () => {
123
+ const dir = fixture();
124
+ try {
125
+ clearCapture();
126
+ assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir }).code, 0);
127
+ const startup = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'startup', cwd: dir });
128
+ assert.strictEqual(startup.code, 0, startup.stderr);
129
+ assert.doesNotMatch(startup.stdout, /State before compaction/);
130
+ assert.doesNotMatch(startup.stdout, /VERIFY PENDING AUTHORIZATIONS/);
131
+ } finally {
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ clearCapture();
134
+ }
135
+ });
136
+
137
+ test('PreCompact fails open outside a repository and on malformed input', () => {
138
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-bare-'));
139
+ try {
140
+ clearCapture();
141
+ const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir });
142
+ assert.strictEqual(noGit.code, 0, 'a directory outside git must not fail the hook');
143
+ const record = readCapture();
144
+ assert.strictEqual(record.branch, null, 'unavailable git facts are recorded as null, never guessed');
145
+ assert.strictEqual(record.head, null);
146
+ assert.strictEqual(record.workflow, null);
147
+
148
+ clearCapture();
149
+ const malformed = spawnSync(process.execPath, [PRECOMPACT], {
150
+ cwd: dir, input: 'not json', encoding: 'utf8', env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
151
+ });
152
+ assert.strictEqual(malformed.status, 0, 'malformed payload must fail open');
153
+ assert.strictEqual(fs.existsSync(RECOVERY), false, 'a malformed payload writes no capture');
154
+ } finally {
155
+ fs.rmSync(dir, { recursive: true, force: true });
156
+ clearCapture();
157
+ }
158
+ });
@@ -2,8 +2,9 @@
2
2
 
3
3
  // Behavioral tests for spec-gate.cjs (Stop completion gate). The hook is run as
4
4
  // a real subprocess: a Stop payload is piped to stdin; we assert exit code and
5
- // whether stdout carries {"decision":"block",...}. Cache lives under the hook's
6
- // own .logs/ dir (shared), so each test seeds/clears it deliberately.
5
+ // whether stdout carries {"decision":"block",...}. The hook resolves its own
6
+ // state directory; running from source that is a temp directory rather than the
7
+ // source tree, and it is shared, so each test seeds/clears it deliberately.
7
8
 
8
9
  const { test, beforeEach, after } = require('node:test');
9
10
  const assert = require('node:assert');
@@ -14,7 +15,8 @@ const os = require('node:os');
14
15
  const path = require('node:path');
15
16
 
16
17
  const HOOK = path.join(__dirname, '..', 'spec-gate.cjs');
17
- const CACHE = path.join(__dirname, '..', '.logs', 'spec-gate-last.json');
18
+ const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
19
+ const CACHE = path.join(hookStateDir(), 'spec-gate-last.json');
18
20
  const PROVENANCE = require(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'));
19
21
  const POLICY = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'));
20
22
  const FEATURE = 'demo';
@@ -286,6 +288,11 @@ function installClaudeGate(root, policyMode = 'valid') {
286
288
  fs.mkdirSync(hooks, { recursive: true });
287
289
  fs.mkdirSync(scripts, { recursive: true });
288
290
  fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
291
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
292
+ fs.copyFileSync(
293
+ path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
294
+ path.join(hooks, 'lib', 'hook-state-dir.cjs'),
295
+ );
289
296
  for (const name of [
290
297
  'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
291
298
  ]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
@@ -43,6 +43,11 @@ function inInstalledProcessSpecStateFixture(run) {
43
43
  fs.mkdirSync(hooks, { recursive: true });
44
44
  fs.mkdirSync(scripts, { recursive: true });
45
45
  fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'spec-state.cjs'), path.join(hooks, 'spec-state.cjs'));
46
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
47
+ fs.copyFileSync(
48
+ path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-state-dir.cjs'),
49
+ path.join(hooks, 'lib', 'hook-state-dir.cjs'),
50
+ );
46
51
  for (const file of [
47
52
  'provenance.cjs',
48
53
  'spec-receipt.cjs',
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert');
5
+ const { spawnSync } = require('node:child_process');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+
10
+ const STATUS_PATH = path.resolve(__dirname, '..', '..', 'status.cjs');
11
+ const GOLDEN_PATH = path.join(__dirname, 'fixtures', 'statusline-default.golden');
12
+ const NBSP = / /g;
13
+
14
+ const cleanupDirs = [];
15
+ process.on('exit', () => {
16
+ for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true });
17
+ });
18
+
19
+ function basePayload(overrides = {}) {
20
+ return {
21
+ model: { display_name: 'TestModel' },
22
+ workspace: { current_dir: '/fixture/dir' },
23
+ cwd: '/fixture/dir',
24
+ context_window: {
25
+ context_window_size: 200000,
26
+ current_usage: {
27
+ input_tokens: 40000,
28
+ cache_creation_input_tokens: 0,
29
+ cache_read_input_tokens: 20000,
30
+ },
31
+ },
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Spawn status.cjs as a child process under a fully pinned environment:
38
+ * - cwd: a fixture root carrying its own .claude/runtime.json
39
+ * - TMPDIR/TEMP/TMP: an isolated per-case temp root (git + usage + context caches)
40
+ * - NO_COLOR/FORCE_COLOR scrubbed unless the case provides them
41
+ * - COLUMNS pinned so responsive wrapping is deterministic
42
+ */
43
+ function runStatus({ runtime = { statusline: 'full', statuslineColors: true }, payload = basePayload(), envExtra = {}, tmpFiles = {} } = {}) {
44
+ const caseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-case-'));
45
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-tmp-'));
46
+ try {
47
+ fs.mkdirSync(path.join(caseDir, '.claude'));
48
+ fs.writeFileSync(path.join(caseDir, '.claude', 'runtime.json'), JSON.stringify(runtime));
49
+ for (const [name, content] of Object.entries(tmpFiles)) {
50
+ fs.writeFileSync(path.join(tmpDir, name), JSON.stringify(content));
51
+ }
52
+ const env = {
53
+ PATH: process.env.PATH,
54
+ HOME: process.env.HOME,
55
+ TMPDIR: tmpDir,
56
+ TEMP: tmpDir,
57
+ TMP: tmpDir,
58
+ COLUMNS: '200',
59
+ ...envExtra,
60
+ };
61
+ const result = spawnSync(process.execPath, [STATUS_PATH], {
62
+ cwd: caseDir,
63
+ env,
64
+ input: Buffer.from(JSON.stringify(payload)),
65
+ });
66
+ return {
67
+ status: result.status,
68
+ stdoutBuffer: result.stdout,
69
+ stdout: result.stdout.toString('utf8'),
70
+ stderr: result.stderr.toString('utf8'),
71
+ readTmpFile(name) {
72
+ const p = path.join(tmpDir, name);
73
+ return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
74
+ },
75
+ };
76
+ } finally {
77
+ // Defer cleanup so readTmpFile still works after the spawn returns.
78
+ cleanupDirs.push(caseDir, tmpDir);
79
+ }
80
+ }
81
+
82
+ function freshUsageCache(now = Date.now()) {
83
+ return {
84
+ status: 'available',
85
+ timestamp: now,
86
+ data: {
87
+ five_hour: { utilization: 20, resets_at: new Date(now + 2 * 3600 * 1000 + 29 * 60 * 1000).toISOString() },
88
+ seven_day: { utilization: 45, resets_at: new Date(now + (2 * 86400 + 5 * 3600) * 1000).toISOString() },
89
+ },
90
+ };
91
+ }
92
+
93
+ test('statusline default output is byte-identical to the golden fixture when no layout is configured', () => {
94
+ const golden = fs.readFileSync(GOLDEN_PATH);
95
+ const run = runStatus();
96
+ assert.equal(run.status, 0, run.stderr);
97
+ assert.ok(run.stdoutBuffer.equals(golden),
98
+ `default output drifted from golden\nexpected: ${JSON.stringify(golden.toString('utf8'))}\nactual: ${JSON.stringify(run.stdout)}`);
99
+ });
100
+
101
+ test('statusline custom layout renders named sections in order per line', () => {
102
+ const run = runStatus({
103
+ runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['directory', 'model']] } },
104
+ });
105
+ assert.equal(run.status, 0, run.stderr);
106
+ const dirIndex = run.stdout.indexOf('📁');
107
+ const modelIndex = run.stdout.indexOf('🤖');
108
+ assert.ok(dirIndex >= 0 && modelIndex >= 0, run.stdout);
109
+ assert.ok(dirIndex < modelIndex, `directory must precede model: ${JSON.stringify(run.stdout)}`);
110
+ });
111
+
112
+ test('statusline ignores unknown section ids without crashing or rendering placeholders', () => {
113
+ const run = runStatus({
114
+ runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['model', 'bogus']] } },
115
+ });
116
+ assert.equal(run.status, 0, run.stderr);
117
+ assert.match(run.stdout, /🤖/);
118
+ assert.doesNotMatch(run.stdout, /bogus/);
119
+ });
120
+
121
+ test('statusline modes slice the layout line count (minimal renders only the first line)', () => {
122
+ const run = runStatus({
123
+ runtime: { statusline: 'minimal', statuslineColors: true, statuslineLayout: { lines: [['model'], ['directory']] } },
124
+ });
125
+ assert.equal(run.status, 0, run.stderr);
126
+ assert.match(run.stdout, /🤖/);
127
+ assert.doesNotMatch(run.stdout, /📁/);
128
+ });
129
+
130
+ test('statusline empty or invalid layout falls back to the default renderers', () => {
131
+ const golden = fs.readFileSync(GOLDEN_PATH);
132
+ for (const layout of [{}, { lines: [] }, { lines: [['bogus']] }, 'nonsense']) {
133
+ const run = runStatus({
134
+ runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: layout },
135
+ });
136
+ assert.equal(run.status, 0, run.stderr);
137
+ assert.ok(run.stdoutBuffer.equals(golden), `fallback drifted for layout ${JSON.stringify(layout)}: ${JSON.stringify(run.stdout)}`);
138
+ }
139
+ });
140
+
141
+ test('statusline shows five-hour and weekly windows with countdowns when the cache is fresh', () => {
142
+ const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': freshUsageCache() } });
143
+ assert.equal(run.status, 0, run.stderr);
144
+ const plain = run.stdout.replace(NBSP, ' ');
145
+ assert.match(plain, /⌛ \d+h \d+m left \(20% used\)/, plain);
146
+ assert.match(plain, /wk 45% \(\d+d \d+h\)/, plain);
147
+ });
148
+
149
+ test('statusline hides the quota area when the cache is stale', () => {
150
+ const stale = freshUsageCache(Date.now() - 10 * 60 * 1000);
151
+ stale.timestamp = Date.now() - 10 * 60 * 1000;
152
+ const run = runStatus({ tmpFiles: { 'ck-usage-limits-cache.json': stale } });
153
+ assert.equal(run.status, 0, run.stderr);
154
+ assert.doesNotMatch(run.stdout, /⌛/);
155
+ assert.doesNotMatch(run.stdout, /wk /);
156
+ });
157
+
158
+ test('statusline cost renders only when the layout enables it, billing is api, and data exists', () => {
159
+ const costLayout = { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['model', 'cost']] } };
160
+ const withCost = basePayload({ cost: { total_cost_usd: '1.2345' } });
161
+
162
+ const enabled = runStatus({ runtime: costLayout, payload: withCost, envExtra: { CLAUDE_BILLING_MODE: 'api' } });
163
+ assert.equal(enabled.status, 0, enabled.stderr);
164
+ const plain = enabled.stdout.replace(NBSP, ' ');
165
+ assert.match(plain, /💵 \$1\.23(?!\d)/, plain);
166
+
167
+ const wrongBilling = runStatus({ runtime: costLayout, payload: withCost, envExtra: { CLAUDE_BILLING_MODE: 'subscription' } });
168
+ assert.equal(wrongBilling.status, 0, wrongBilling.stderr);
169
+ assert.doesNotMatch(wrongBilling.stdout, /💵/);
170
+
171
+ const noData = runStatus({ runtime: costLayout, envExtra: { CLAUDE_BILLING_MODE: 'api' } });
172
+ assert.equal(noData.status, 0, noData.stderr);
173
+ assert.doesNotMatch(noData.stdout, /💵/);
174
+
175
+ const noLayout = runStatus({ payload: withCost, envExtra: { CLAUDE_BILLING_MODE: 'api' } });
176
+ assert.equal(noLayout.status, 0, noLayout.stderr);
177
+ assert.doesNotMatch(noLayout.stdout, /💵/);
178
+ });
179
+
180
+ test('statusline writes the ck-context session file regardless of layout', () => {
181
+ const run = runStatus({
182
+ runtime: { statusline: 'full', statuslineColors: true, statuslineLayout: { lines: [['model']] } },
183
+ payload: basePayload({ session_id: 'ctxcase' }),
184
+ });
185
+ assert.equal(run.status, 0, run.stderr);
186
+ const contextFile = run.readTmpFile('ck-context-ctxcase.json');
187
+ assert.ok(contextFile, 'ck-context-ctxcase.json must be written even under a custom layout');
188
+ assert.equal(typeof contextFile.percent, 'number');
189
+ assert.equal(typeof contextFile.tokens, 'number');
190
+ });
191
+
192
+ test('statusline honors NO_COLOR with no ANSI escapes in output', () => {
193
+ const run = runStatus({ envExtra: { NO_COLOR: '1' } });
194
+ assert.equal(run.status, 0, run.stderr);
195
+ assert.doesNotMatch(run.stdout, /\[/);
196
+ });
@@ -90,7 +90,7 @@ try {
90
90
  } catch (e) {
91
91
  try {
92
92
  const fs = require('fs'), p = require('path');
93
- const d = p.join(__dirname, '.logs');
93
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
94
94
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
95
95
  fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
96
96
  JSON.stringify({ ts: new Date().toISOString(), hook: 'agent', status: 'crash', error: e.message }) + '\n');
@@ -14,7 +14,7 @@
14
14
  try {
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
- const { execSync } = require('child_process');
17
+ const { execFileSync } = require('child_process');
18
18
  const { loadConfig } = require('./lib/config.cjs');
19
19
 
20
20
  // Đọc stdin theo chuẩn hook
@@ -24,6 +24,9 @@ try {
24
24
  const config = loadConfig({ cwd, includeProject: false, includeAssertions: false, includeLocale: false });
25
25
 
26
26
  const docsDir = path.join(cwd, config.paths?.docs || 'docs');
27
+ const specsDir = path.join(cwd, config.paths?.specs || 'specs');
28
+ const docsRelative = path.relative(cwd, docsDir).replace(/\\/g, '/');
29
+ const specsRelative = path.relative(cwd, specsDir).replace(/\\/g, '/');
27
30
 
28
31
  // Xác định dự án đã có cốt lõi code hay chưa?
29
32
  const hasCode = fs.existsSync(path.join(cwd, 'src')) ||
@@ -46,16 +49,18 @@ try {
46
49
  lines.push('> Source exists but `docs/` does not. Create baseline docs first:');
47
50
  lines.push('> 1. `docs/system-architecture.md` — system architecture and flows');
48
51
  lines.push('> 2. `docs/project-overview-pdr.md` — overview, goals, features');
49
- lines.push('> 3. Write current hash to `docs/.sync_hash` via `git log -1 --format="%H" -- . ":(exclude)docs"`');
52
+ lines.push('> 3. Write the latest non-docs/non-Specs source hash to `docs/.sync_hash`.');
50
53
  lines.push('');
51
54
  }
52
55
  // Case 2: docs exist — check continuous sync vs source-only git hash
53
56
  else {
54
57
  try {
55
58
  // Latest SOURCE-only hash (ignore docs-only commits)
56
- const currentHash = execSync('git log -1 --format="%H" -- . ":(exclude)docs"', {
57
- cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']
58
- }).trim();
59
+ const currentHash = execFileSync('git', [
60
+ 'log', '-1', '--format=%H', '--', '.',
61
+ `:(exclude,literal)${docsRelative}`,
62
+ `:(exclude,literal)${specsRelative}`,
63
+ ], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
59
64
 
60
65
  if (currentHash) {
61
66
  const syncTrackingFile = path.join(docsDir, '.sync_hash');
@@ -87,11 +92,8 @@ try {
87
92
  } catch (e) {
88
93
  // Ghi log lỗi ẩn danh nếu sập hook
89
94
  try {
90
- const fs = require('fs'), p = require('path'), os = require('os');
91
- const isSourceTree = __dirname.includes(`${p.sep}packages${p.sep}spec${p.sep}src`);
92
- const d = isSourceTree
93
- ? p.join(os.tmpdir(), 'cafekit-hook-logs', 'claude-docs-sync')
94
- : p.join(__dirname, '.logs');
95
+ const fs = require('fs'), p = require('path');
96
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
95
97
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
96
98
  fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
97
99
  JSON.stringify({ ts: new Date().toISOString(), hook: 'docs-sync', status: 'crash', error: e.message }) + '\n');
@@ -115,7 +115,7 @@ try {
115
115
  } catch (e) {
116
116
  try {
117
117
  const fs = require('fs'), p = require('path');
118
- const d = p.join(__dirname, '.logs');
118
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
119
119
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
120
120
  fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
121
121
  JSON.stringify({ ts: new Date().toISOString(), hook: 'inspect-block', status: 'crash', error: e.message }) + '\n');
@@ -520,6 +520,9 @@ function loadConfig(options = {}) {
520
520
  // Statusline mode
521
521
  result.statusline = merged.statusline || 'full';
522
522
  result.statuslineColors = merged.statuslineColors !== false;
523
+ if (merged.statuslineLayout && typeof merged.statuslineLayout === 'object') {
524
+ result.statuslineLayout = merged.statuslineLayout;
525
+ }
523
526
 
524
527
  return sanitizeConfig(result, projectRoot);
525
528
  } catch (e) {
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Where a Claude hook keeps its own generated state: crash logs and the
5
+ * gate/tollgate caches.
6
+ *
7
+ * Hooks run from two places. In an installed project they live in
8
+ * `.claude/hooks/`, whose `.logs/` is gitignored, so writing beside the hook is
9
+ * correct. During this package's tests they run straight from
10
+ * `packages/spec/src/claude/hooks/`, where the same write lands inside the
11
+ * source tree as an untracked directory. That changes the worktree digest the
12
+ * receipt provenance is bound to, and the completion gate then reports every
13
+ * done task as stale. It surfaced three times as a false alarm before the cause
14
+ * was found, so source-tree runs write to a temp directory instead.
15
+ *
16
+ * Keeping the rule here means a new hook cannot reintroduce the bug by copying
17
+ * the old `path.join(__dirname, '.logs')` line.
18
+ */
19
+
20
+ const os = require('os');
21
+ const path = require('path');
22
+
23
+ // This module ships in `hooks/lib/`, so the hook directory is always its parent.
24
+ const HOOKS_DIR = path.join(__dirname, '..');
25
+ const SOURCE_MARKER = `${path.sep}packages${path.sep}spec${path.sep}src`;
26
+
27
+ /** True when `dir` sits inside this package's source tree rather than an install. */
28
+ function isSourceTree(dir) {
29
+ return String(dir || '').includes(SOURCE_MARKER);
30
+ }
31
+
32
+ /** Absolute directory for hook-generated state; create it before writing. */
33
+ function hookStateDir() {
34
+ return isSourceTree(HOOKS_DIR)
35
+ ? path.join(os.tmpdir(), 'cafekit-hook-logs', 'claude')
36
+ : path.join(HOOKS_DIR, '.logs');
37
+ }
38
+
39
+ module.exports = { hookStateDir, isSourceTree };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright (c) 2026 Haposoft. MIT License.
3
+ *
4
+ * Secret-output guardrail keyword matcher.
5
+ *
6
+ * These patterns detect prompts about credential handling. They are deliberately
7
+ * separate from the privacy hooks: those gate whether a sensitive file may be READ,
8
+ * while this list drives a soft reminder about whether a value may be PRINTED.
9
+ * Approval for the first is not approval for the second.
10
+ */
11
+
12
+ const SECRET_KEYWORD_PATTERNS = [
13
+ /\.env(?:\b|$)/i,
14
+ /(?:^|[^A-Za-z0-9])credentials?(?:$|[^A-Za-z0-9])/i,
15
+ /(?:^|[^A-Za-z0-9])secrets?(?:$|[^A-Za-z0-9])/i,
16
+ /(?:^|[^A-Za-z0-9])api[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
17
+ /(?:^|[^A-Za-z0-9])access[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
18
+ /(?:^|[^A-Za-z0-9])tokens?(?:$|[^A-Za-z0-9])/i,
19
+ /(?:^|[^A-Za-z0-9])bearer(?:$|[^A-Za-z0-9])/i,
20
+ /(?:^|[^A-Za-z0-9])jwts?(?:$|[^A-Za-z0-9])/i,
21
+ /(?:^|[^A-Za-z0-9])private[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
22
+ /(?:^|[^A-Za-z0-9])oauth(?:[\s_-]+client)?[\s_-]*secrets?(?:$|[^A-Za-z0-9])/i,
23
+ /(?:^|[^A-Za-z0-9])client[\s_-]*secrets?(?:$|[^A-Za-z0-9])/i,
24
+ /(?:^|[^A-Za-z0-9])cloud[\s_-]*keys?(?:$|[^A-Za-z0-9])/i,
25
+ /(?:^|[^A-Za-z0-9])env[\s_-]+files?(?:$|[^A-Za-z0-9])/i,
26
+ ];
27
+
28
+ /** The reminder text. It names no value and quotes no prompt. */
29
+ const REMINDER = [
30
+ '## Secret handling',
31
+ '- Do not print raw credentials, API keys, tokens, JWTs, private keys, or secret values into the conversation.',
32
+ '- Use [redacted], variable names, counts, or high-level status instead.',
33
+ '- Approval to read a sensitive file or command output does not grant permission to print raw values.',
34
+ '- If a value is needed for a machine action, pass it through a non-echoing path and report only success or failure.',
35
+ ].join('\n');
36
+
37
+ function containsSecretKeyword(prompt) {
38
+ if (typeof prompt !== 'string' || prompt.trim() === '') return false;
39
+ return SECRET_KEYWORD_PATTERNS.some((pattern) => pattern.test(prompt));
40
+ }
41
+
42
+ module.exports = { SECRET_KEYWORD_PATTERNS, REMINDER, containsSecretKeyword };