@haposoft/cafekit 0.16.0-rc.7 → 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 (299) 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 +295 -0
  4. package/bin/__tests__/codex-native.test.js +2289 -34
  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 +2131 -16
  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 +24 -13
  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 +87 -4
  46. package/src/claude/hooks/__tests__/state.test.js +254 -3
  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 +32 -5
  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/spec-receipt.cjs +76 -21
  89. package/src/claude/scripts/spec-resolver.cjs +144 -4
  90. package/src/claude/scripts/workflow-policy.cjs +1 -1
  91. package/src/claude/settings/settings.json +16 -0
  92. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  93. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  94. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  95. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  96. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  97. package/src/claude/skills/code-review/SKILL.md +43 -25
  98. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  99. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  100. package/src/claude/skills/debug/SKILL.md +48 -19
  101. package/src/claude/skills/delegate/SKILL.md +1 -1
  102. package/src/claude/skills/develop/SKILL.md +92 -65
  103. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  104. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  105. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  106. package/src/claude/skills/docs/SKILL.md +42 -23
  107. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  108. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  109. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  110. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  111. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  112. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  113. package/src/claude/skills/docx/SKILL.md +1 -1
  114. package/src/claude/skills/git/SKILL.md +1 -1
  115. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  116. package/src/claude/skills/git/references/finish-branch.md +1 -1
  117. package/src/claude/skills/hotfix/SKILL.md +159 -170
  118. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  119. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  120. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  121. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  122. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  123. package/src/claude/skills/inspect/SKILL.md +55 -46
  124. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  125. package/src/claude/skills/loop/SKILL.md +114 -0
  126. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  127. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  128. package/src/claude/skills/pdf/SKILL.md +1 -1
  129. package/src/claude/skills/pptx/SKILL.md +1 -1
  130. package/src/claude/skills/question/SKILL.md +16 -13
  131. package/src/claude/skills/research/SKILL.md +70 -43
  132. package/src/claude/skills/route/SKILL.md +94 -0
  133. package/src/claude/skills/route/references/agent-timing.md +46 -0
  134. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  135. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  136. package/src/claude/skills/specs/SKILL.md +48 -57
  137. package/src/claude/skills/specs/references/review.md +26 -8
  138. package/src/claude/skills/specs/references/templates.md +76 -31
  139. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  140. package/src/claude/skills/specs/templates/research.md +2 -2
  141. package/src/claude/skills/sync/SKILL.md +6 -6
  142. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  143. package/src/claude/skills/test/SKILL.md +120 -120
  144. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  145. package/src/claude/skills/test/references/failure-triage.md +55 -121
  146. package/src/claude/skills/test/references/test-memory.md +20 -34
  147. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  148. package/src/claude/skills/web-testing/SKILL.md +1 -1
  149. package/src/claude/skills/xlsx/SKILL.md +1 -1
  150. package/src/claude/status.cjs +135 -33
  151. package/src/codex/AGENTS.md +14 -4
  152. package/src/codex/hooks/docs-sync.cjs +8 -3
  153. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  154. package/src/codex/hooks/rules.cjs +29 -1
  155. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  156. package/src/codex/hooks/spec-gate.cjs +3 -8
  157. package/src/codex/hooks/spec-state.cjs +34 -5
  158. package/src/codex/hooks.json +5 -0
  159. package/src/codex/runtime.json +2 -0
  160. package/src/common/.env.example +1 -1
  161. package/src/claude/skills/backend-development/SKILL.md +0 -101
  162. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  163. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  164. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  165. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  166. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  167. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  168. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  169. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  170. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  171. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  172. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  173. package/src/claude/skills/devops/.env.example +0 -76
  174. package/src/claude/skills/devops/SKILL.md +0 -102
  175. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  176. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  177. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  178. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  179. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  180. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  181. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  182. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  183. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  184. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  185. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  186. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  187. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  188. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  189. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  190. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  191. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  192. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  193. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  194. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  195. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  196. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  197. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  198. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  199. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  200. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  201. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  202. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  203. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  204. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  205. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  206. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  207. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  208. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  209. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  210. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  211. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  212. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  213. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  214. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  215. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  216. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  217. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  218. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  219. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  220. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  221. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  222. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  223. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  224. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  225. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  226. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  227. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  228. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  229. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  230. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  231. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  232. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  233. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  234. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  235. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  236. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  237. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  238. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  239. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  240. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  241. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  242. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  243. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  244. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  245. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  246. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  247. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  248. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  249. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  250. package/src/claude/skills/react-best-practices/README.md +0 -123
  251. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  252. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  253. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  254. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  255. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  256. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  257. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  258. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  259. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  260. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  261. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  262. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  263. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  264. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  265. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  266. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  267. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  268. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  269. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  270. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  271. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  272. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  273. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  274. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  275. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  276. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  277. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  278. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  279. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  280. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  281. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  282. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  283. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  284. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  285. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  286. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  287. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  288. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  289. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  290. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  291. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  292. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  293. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  294. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  295. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  296. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  297. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  298. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  299. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -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 };
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Haposoft. MIT License.
4
+ *
5
+ * PreCompact Hook — precompact.cjs
6
+ * Implements: https://docs.anthropic.com/en/docs/claude-code/hooks
7
+ *
8
+ * Compaction drops the transcript that told the session where it was. The
9
+ * existing SessionStart warning tells the operator something was lost but not
10
+ * what, so recovery starts from a blank slate and the session re-derives facts
11
+ * it already had. This hook writes down the machine-checkable anchors just
12
+ * before the drop: branch, HEAD, how dirty the tree is, and which task the
13
+ * packet says is in flight. `session.cjs` prints them back on the compact
14
+ * restart.
15
+ *
16
+ * Only facts the runtime can re-derive are recorded. Nothing here is proof of
17
+ * work and nothing here grants an authorization; a compacted session must still
18
+ * re-ask for a pending confirmation.
19
+ *
20
+ * Storage: the hook state directory (temp when run from source).
21
+ * Exit: 0 always (fail-open).
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { spawnSync } = require('child_process');
27
+
28
+ const RECOVERY_FILE = 'compact-recovery.json';
29
+
30
+ function gitLine(root, args) {
31
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
32
+ if (result.error || result.status !== 0) return null;
33
+ const value = String(result.stdout || '').trim();
34
+ return value === '' ? null : value;
35
+ }
36
+
37
+ function projectRoot(payload) {
38
+ const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string' ? process.env.CLAUDE_PROJECT_DIR.trim() : '';
39
+ const candidates = [configured, typeof payload.cwd === 'string' ? payload.cwd.trim() : '', process.cwd()];
40
+ for (const candidate of candidates) {
41
+ if (!candidate) continue;
42
+ try { return fs.realpathSync(path.resolve(candidate)); } catch { /* next */ }
43
+ }
44
+ return process.cwd();
45
+ }
46
+
47
+ /** The one task the packet currently says is in flight, if any. */
48
+ function activeTask(root) {
49
+ try {
50
+ const RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
51
+ if (typeof RESOLVER.resolveWorkflowCandidate !== 'function') return null;
52
+ const resolved = RESOLVER.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, target: null });
53
+ if (!resolved || resolved.error || resolved.layoutKind !== 'process-v3') return null;
54
+ const registry = resolved.taskRegistry || {};
55
+ const inProgress = Object.keys(registry).filter((taskPath) => registry[taskPath]?.status === 'in_progress');
56
+ return {
57
+ feature: resolved.featureName || null,
58
+ in_progress: inProgress,
59
+ counts: Object.values(registry).reduce((acc, task) => {
60
+ const status = task?.status || 'pending';
61
+ acc[status] = (acc[status] || 0) + 1;
62
+ return acc;
63
+ }, {}),
64
+ };
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ try {
71
+ const stdin = fs.readFileSync(0, 'utf8').trim();
72
+ const payload = stdin ? JSON.parse(stdin) : {};
73
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) process.exit(0);
74
+
75
+ const root = projectRoot(payload);
76
+ const status = gitLine(root, ['status', '--porcelain']);
77
+ const record = {
78
+ captured_at: new Date().toISOString(),
79
+ trigger: typeof payload.trigger === 'string' ? payload.trigger : null,
80
+ session_id: typeof payload.session_id === 'string' ? payload.session_id : null,
81
+ project_root: root,
82
+ branch: gitLine(root, ['rev-parse', '--abbrev-ref', 'HEAD']),
83
+ head: gitLine(root, ['rev-parse', '--short', 'HEAD']),
84
+ dirty_count: status === null ? null : status.split('\n').filter((line) => line.trim() !== '').length,
85
+ workflow: activeTask(root),
86
+ };
87
+
88
+ const dir = require('./lib/hook-state-dir.cjs').hookStateDir();
89
+ fs.mkdirSync(dir, { recursive: true });
90
+ const target = path.join(dir, RECOVERY_FILE);
91
+ const temp = `${target}.${process.pid}.tmp`;
92
+ fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`);
93
+ fs.renameSync(temp, target);
94
+ process.exit(0);
95
+ } catch (error) {
96
+ try {
97
+ const dir = require('./lib/hook-state-dir.cjs').hookStateDir();
98
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
99
+ fs.appendFileSync(
100
+ path.join(dir, 'hook-log.jsonl'),
101
+ `${JSON.stringify({ ts: new Date().toISOString(), hook: 'precompact', status: 'crash', error: error.message })}\n`,
102
+ );
103
+ } catch (_) { /* fail-open */ }
104
+ process.exit(0);
105
+ }
@@ -93,7 +93,7 @@ try {
93
93
  } catch (e) {
94
94
  try {
95
95
  const fs = require('fs'), p = require('path');
96
- const d = p.join(__dirname, '.logs');
96
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
97
97
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
98
98
  fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
99
99
  JSON.stringify({ ts: new Date().toISOString(), hook: 'rules', status: 'crash', error: e.message }) + '\n');
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Haposoft. MIT License.
4
+ *
5
+ * UserPromptSubmit Hook — secret-output-guardrail.cjs
6
+ * Implements: https://docs.anthropic.com/en/docs/claude-code/hooks
7
+ *
8
+ * The privacy hook gates whether a sensitive file may be read. Nothing gated what
9
+ * happens after that approval, so an approved read could still put a raw key into
10
+ * the transcript — where it survives compaction, is written to disk, and is replayed
11
+ * to the model on every later turn. This injects a reminder when the prompt itself is
12
+ * about credential handling.
13
+ *
14
+ * It reads the prompt and never echoes it: no prompt text and no matched value is
15
+ * written out, so the guardrail cannot itself become the leak.
16
+ *
17
+ * Exit: 0 always (fail-open)
18
+ */
19
+
20
+ try {
21
+ const fs = require('fs');
22
+ const { REMINDER, containsSecretKeyword } = require('./lib/secret-keywords.cjs');
23
+
24
+ const stdin = fs.readFileSync(0, 'utf8').trim();
25
+ if (!stdin) process.exit(0);
26
+
27
+ const payload = JSON.parse(stdin);
28
+ const prompt = String(payload.prompt || payload.user_prompt || '');
29
+ if (!containsSecretKeyword(prompt)) process.exit(0);
30
+
31
+ console.log(REMINDER);
32
+ process.exit(0);
33
+
34
+ } catch (e) {
35
+ try {
36
+ const fs = require('fs'), p = require('path');
37
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
38
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
39
+ fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
40
+ JSON.stringify({ ts: new Date().toISOString(), hook: 'secret-output-guardrail', status: 'crash', error: e.message }) + '\n');
41
+ } catch (_) {}
42
+ process.exit(0);
43
+ }
@@ -261,6 +261,32 @@ try {
261
261
  console.log('Any pending confirmations requested via AskUserQuestion might have been lost.');
262
262
  console.log('Do not proceed without explicitly asking the user again to ensure safety.');
263
263
  console.log('Use AskUserQuestion: "The chat context was compressed. Do I still have permission to proceed?"');
264
+
265
+ // Anchors captured by the PreCompact hook. These are re-derivable facts, not
266
+ // proof and not an authorization: they save the session from rebuilding its
267
+ // bearings, and nothing more.
268
+ try {
269
+ const fs2 = require('fs'), p2 = require('path');
270
+ const file = p2.join(require('./lib/hook-state-dir.cjs').hookStateDir(), 'compact-recovery.json');
271
+ const record = JSON.parse(fs2.readFileSync(file, 'utf8'));
272
+ const parts = [];
273
+ if (record.branch) parts.push(`branch ${record.branch}`);
274
+ if (record.head) parts.push(`HEAD ${record.head}`);
275
+ if (typeof record.dirty_count === 'number') parts.push(`${record.dirty_count} uncommitted path(s)`);
276
+ console.log('\n=== State before compaction ===');
277
+ if (record.captured_at) console.log(`Captured: ${record.captured_at}`);
278
+ if (parts.length) console.log(`Repository: ${parts.join(' | ')}`);
279
+ const workflow = record.workflow;
280
+ if (workflow && workflow.feature) {
281
+ const counts = Object.entries(workflow.counts || {}).map(([k, v]) => `${v} ${k}`).join(', ');
282
+ console.log(`Feature: ${workflow.feature}${counts ? ` (${counts})` : ''}`);
283
+ if (Array.isArray(workflow.in_progress) && workflow.in_progress.length > 0) {
284
+ console.log(`In flight: ${workflow.in_progress.join(', ')}`);
285
+ }
286
+ }
287
+ console.log('Re-read these from disk before acting; they describe the moment before compaction.');
288
+ console.log('=== End of pre-compaction state ===');
289
+ } catch (_) { /* no capture available */ }
264
290
  }
265
291
 
266
292
  process.exit(0);
@@ -269,7 +295,7 @@ try {
269
295
  } catch (e) {
270
296
  try {
271
297
  const fs = require('fs'), p = require('path');
272
- const d = p.join(__dirname, '.logs');
298
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
273
299
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
274
300
  fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
275
301
  JSON.stringify({ ts: new Date().toISOString(), hook: 'session', status: 'crash', error: e.message }) + '\n');
@@ -44,7 +44,7 @@ function projectRoot(payload = {}) {
44
44
 
45
45
  function logCrash(error) {
46
46
  try {
47
- const d = path.join(__dirname, '.logs');
47
+ const d = require('./lib/hook-state-dir.cjs').hookStateDir();
48
48
  if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
49
49
  fs.appendFileSync(
50
50
  path.join(d, 'hook-log.jsonl'),
@@ -202,7 +202,7 @@ try {
202
202
  process.exit(0);
203
203
  }
204
204
  const taskRegistry = resolved.taskRegistry || activeSpec.task_registry || {};
205
- const cacheFile = path.join(__dirname, '.logs', 'spec-gate-last.json');
205
+ const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), 'spec-gate-last.json');
206
206
  const cacheExists = fs.existsSync(cacheFile);
207
207
  let cache = {};
208
208
  if (cacheExists) {
@@ -220,7 +220,7 @@ try {
220
220
  if (staleFlashTasks.length > 0) {
221
221
  process.stdout.write(JSON.stringify({
222
222
  decision: 'block',
223
- reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /hapo:test for exact proof, then use explicit sync-finalize.`
223
+ reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /cf:test for exact proof, then use explicit sync-finalize.`
224
224
  }) + '\n');
225
225
  process.exit(0);
226
226
  }