@tekmidian/pai 0.42.4 → 0.42.6

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 (292) hide show
  1. package/ARCHITECTURE.md +9 -9
  2. package/README.md +26 -8
  3. package/dist/{aibroker-client-BP74gGwD.mjs → aibroker-client-Bn9SnGMa.mjs} +3 -3
  4. package/dist/{aibroker-client-BP74gGwD.mjs.map → aibroker-client-Bn9SnGMa.mjs.map} +1 -1
  5. package/dist/{aibroker-client-C6m71p6O.mjs → aibroker-client-D5NT0S1N.mjs} +1 -1
  6. package/dist/{auto-route-_5zzJz5q.mjs → auto-route-CcD0OWWB.mjs} +4 -4
  7. package/dist/{auto-route-_5zzJz5q.mjs.map → auto-route-CcD0OWWB.mjs.map} +1 -1
  8. package/dist/{chain-DTII7xeL.mjs → chain-BbcfP77K.mjs} +5 -4
  9. package/dist/{chain-DTII7xeL.mjs.map → chain-BbcfP77K.mjs.map} +1 -1
  10. package/dist/cli/index.mjs +23 -21
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/cli/program.d.mts.map +1 -1
  13. package/dist/cli/program.mjs +23 -21
  14. package/dist/{clusters-j0ZuNYp9.mjs → clusters-Cw_P6lO-.mjs} +2 -2
  15. package/dist/{clusters-j0ZuNYp9.mjs.map → clusters-Cw_P6lO-.mjs.map} +1 -1
  16. package/dist/{config-B64vFg14.mjs → config-DCu8THKi.mjs} +41 -9
  17. package/dist/{config-B64vFg14.mjs.map → config-DCu8THKi.mjs.map} +1 -1
  18. package/dist/config-DeqFqa1N.mjs +3 -0
  19. package/dist/config-DxMZjpvY.mjs +1289 -0
  20. package/dist/config-DxMZjpvY.mjs.map +1 -0
  21. package/dist/context-handover-cache-B9NKSjwR.mjs +3242 -0
  22. package/dist/context-handover-cache-B9NKSjwR.mjs.map +1 -0
  23. package/dist/daemon/index.mjs +23 -22
  24. package/dist/daemon/index.mjs.map +1 -1
  25. package/dist/daemon-BNjbVsQU.mjs +24 -0
  26. package/dist/{daemon-B8N1egl0.mjs → daemon-BppziPi6.mjs} +52 -38
  27. package/dist/daemon-BppziPi6.mjs.map +1 -0
  28. package/dist/daemon-mcp/index.mjs +144 -117
  29. package/dist/daemon-mcp/index.mjs.map +1 -1
  30. package/dist/db-BOBfewFH.mjs +3 -0
  31. package/dist/{db-a1ixZQjr.mjs → db-DJ8rk0VM.mjs} +15 -6
  32. package/dist/{db-a1ixZQjr.mjs.map → db-DJ8rk0VM.mjs.map} +1 -1
  33. package/dist/db-DbY2O8QO.mjs +3 -0
  34. package/dist/{db-Ca5qfsMC.mjs → db-Ddns80yf.mjs} +15 -6
  35. package/dist/{db-Ca5qfsMC.mjs.map → db-Ddns80yf.mjs.map} +1 -1
  36. package/dist/{detect-CdaA48EI.mjs → detect-Bf2z-oKB.mjs} +1 -1
  37. package/dist/{detect-CdaA48EI.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
  38. package/dist/detector-CkwkXuGh.mjs +5 -0
  39. package/dist/{detector-C73Nxm94.mjs → detector-DWXkeTlk.mjs} +2 -2
  40. package/dist/{detector-C73Nxm94.mjs.map → detector-DWXkeTlk.mjs.map} +1 -1
  41. package/dist/{embeddings-CEBGrzwu.mjs → embeddings-ClqKtLZ8.mjs} +1 -1
  42. package/dist/{embeddings-DOLZnT1X.mjs → embeddings-DvRKcQyt.mjs} +1 -1
  43. package/dist/{embeddings-DOLZnT1X.mjs.map → embeddings-DvRKcQyt.mjs.map} +1 -1
  44. package/dist/factory-47Qf_df7.mjs +3 -0
  45. package/dist/{factory-DEb9NmHJ.mjs → factory-DNYAqjH0.mjs} +8 -8
  46. package/dist/{factory-DEb9NmHJ.mjs.map → factory-DNYAqjH0.mjs.map} +1 -1
  47. package/dist/{fallback-oJl3BaIN.mjs → fallback-Cwt4hmh5.mjs} +182 -122
  48. package/dist/fallback-Cwt4hmh5.mjs.map +1 -0
  49. package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
  50. package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
  51. package/dist/hooks/capture-all-events.mjs +117 -34
  52. package/dist/hooks/capture-all-events.mjs.map +4 -4
  53. package/dist/hooks/capture-session-summary.mjs +110 -33
  54. package/dist/hooks/capture-session-summary.mjs.map +4 -4
  55. package/dist/hooks/capture-tool-output.mjs +105 -28
  56. package/dist/hooks/capture-tool-output.mjs.map +4 -4
  57. package/dist/hooks/cleanup-session-files.mjs +54 -13
  58. package/dist/hooks/cleanup-session-files.mjs.map +2 -2
  59. package/dist/hooks/context-compression-hook.mjs +7565 -122
  60. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  61. package/dist/hooks/initialize-session.mjs +55 -14
  62. package/dist/hooks/initialize-session.mjs.map +2 -2
  63. package/dist/hooks/inject-observations.mjs +79 -18
  64. package/dist/hooks/inject-observations.mjs.map +3 -3
  65. package/dist/hooks/load-core-context.mjs +55 -14
  66. package/dist/hooks/load-core-context.mjs.map +2 -2
  67. package/dist/hooks/load-project-context.mjs +185 -103
  68. package/dist/hooks/load-project-context.mjs.map +4 -4
  69. package/dist/hooks/mcp-deferred-gate.mjs +54 -13
  70. package/dist/hooks/mcp-deferred-gate.mjs.map +2 -2
  71. package/dist/hooks/mcp-deferred-reminder.mjs +9 -0
  72. package/dist/hooks/mcp-deferred-reminder.mjs.map +3 -3
  73. package/dist/hooks/observe.mjs +54 -13
  74. package/dist/hooks/observe.mjs.map +2 -2
  75. package/dist/hooks/post-compact-inject.mjs +7419 -16
  76. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  77. package/dist/hooks/route-agents-to-worker.mjs +7778 -171
  78. package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
  79. package/dist/hooks/route-edits-to-worker.mjs +90 -0
  80. package/dist/hooks/route-edits-to-worker.mjs.map +7 -0
  81. package/dist/hooks/security-validator.mjs +147 -6
  82. package/dist/hooks/security-validator.mjs.map +4 -4
  83. package/dist/hooks/session-commands.mjs +9 -0
  84. package/dist/hooks/session-commands.mjs.map +3 -3
  85. package/dist/hooks/stop-hook.mjs +182 -105
  86. package/dist/hooks/stop-hook.mjs.map +4 -4
  87. package/dist/hooks/subagent-stop-hook.mjs +3 -0
  88. package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
  89. package/dist/hooks/sync-todo-to-md.mjs +54 -13
  90. package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
  91. package/dist/hooks/update-tab-on-action.mjs +3 -0
  92. package/dist/hooks/update-tab-on-action.mjs.map +1 -1
  93. package/dist/hooks/update-tab-titles.mjs +3 -0
  94. package/dist/hooks/update-tab-titles.mjs.map +1 -1
  95. package/dist/hooks/whisper-reinject.mjs +3 -0
  96. package/dist/hooks/whisper-reinject.mjs.map +1 -1
  97. package/dist/hooks/whisper-rules.mjs +134 -29
  98. package/dist/hooks/whisper-rules.mjs.map +4 -4
  99. package/dist/hooks/worker-proxy.mjs +7737 -117
  100. package/dist/hooks/worker-proxy.mjs.map +4 -4
  101. package/dist/hooks/worker-status-line.mjs +7791 -144
  102. package/dist/hooks/worker-status-line.mjs.map +4 -4
  103. package/dist/hooks/worker-supervision.mjs +7721 -115
  104. package/dist/hooks/worker-supervision.mjs.map +4 -4
  105. package/dist/index.d.mts +4 -2
  106. package/dist/index.d.mts.map +1 -1
  107. package/dist/index.mjs +10 -10
  108. package/dist/{indexer-backend-BPTzKFZU.mjs → indexer-backend-CdvCJCkD.mjs} +3 -3
  109. package/dist/{indexer-backend-BPTzKFZU.mjs.map → indexer-backend-CdvCJCkD.mjs.map} +1 -1
  110. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C6NryMMb.mjs} +2 -2
  111. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C6NryMMb.mjs.map} +1 -1
  112. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-BSjM5xZh.mjs} +1 -1
  113. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-BSjM5xZh.mjs.map} +1 -1
  114. package/dist/{latent-ideas-CtSca4qG.mjs → latent-ideas-BfL4nCQ3.mjs} +4 -4
  115. package/dist/{latent-ideas-CtSca4qG.mjs.map → latent-ideas-BfL4nCQ3.mjs.map} +1 -1
  116. package/dist/{link-boost-Bucnz3n8.mjs → link-boost-C5PaNdzm.mjs} +1 -1
  117. package/dist/{link-boost-Bucnz3n8.mjs.map → link-boost-C5PaNdzm.mjs.map} +1 -1
  118. package/dist/main-resolver-CnfnWa8C.mjs +7 -0
  119. package/dist/{main-resolver-DnkxPYkr.mjs → main-resolver-DvYu9jbl.mjs} +21 -11
  120. package/dist/main-resolver-DvYu9jbl.mjs.map +1 -0
  121. package/dist/{migrate-Cjzeefn9.mjs → migrate-CfERwXhY.mjs} +2 -2
  122. package/dist/{migrate-Cjzeefn9.mjs.map → migrate-CfERwXhY.mjs.map} +1 -1
  123. package/dist/{neighborhood-CM7Jgb51.mjs → neighborhood-CETR_P4y.mjs} +2 -2
  124. package/dist/{neighborhood-CM7Jgb51.mjs.map → neighborhood-CETR_P4y.mjs.map} +1 -1
  125. package/dist/{note-context-DCwE2A-M.mjs → note-context-DCcRWBoN.mjs} +1 -1
  126. package/dist/{note-context-DCwE2A-M.mjs.map → note-context-DCcRWBoN.mjs.map} +1 -1
  127. package/dist/pai-home-ChAOupZc.mjs +164 -0
  128. package/dist/pai-home-ChAOupZc.mjs.map +1 -0
  129. package/dist/{pai-marker-CHtbJMwJ.mjs → pai-marker-B20KqhA8.mjs} +1 -1
  130. package/dist/{pai-marker-CHtbJMwJ.mjs.map → pai-marker-B20KqhA8.mjs.map} +1 -1
  131. package/dist/{planner-D5Ui7DAm.mjs → planner-pXRc-8yt.mjs} +7 -6
  132. package/dist/{planner-D5Ui7DAm.mjs.map → planner-pXRc-8yt.mjs.map} +1 -1
  133. package/dist/{postgres-CtOzfoQU.mjs → postgres-CUSXWgTG.mjs} +2 -2
  134. package/dist/{postgres-CtOzfoQU.mjs.map → postgres-CUSXWgTG.mjs.map} +1 -1
  135. package/dist/{program-zpoYnJGz.mjs → program-LLNPpxlj.mjs} +8308 -6065
  136. package/dist/program-LLNPpxlj.mjs.map +1 -0
  137. package/dist/query-feedback-C7rSfiXU.mjs +3 -0
  138. package/dist/{query-feedback-Dqz-HC__.mjs → query-feedback-CYyOGi3y.mjs} +20 -6
  139. package/dist/query-feedback-CYyOGi3y.mjs.map +1 -0
  140. package/dist/{reranker-CwTCNsgA.mjs → reranker-B5qUTamW.mjs} +1 -1
  141. package/dist/{reranker-xPm04PXx.mjs → reranker-HHmhZk6v.mjs} +1 -1
  142. package/dist/{reranker-xPm04PXx.mjs.map → reranker-HHmhZk6v.mjs.map} +1 -1
  143. package/dist/router-CremHKTI.mjs +3 -0
  144. package/dist/{router-DD6D4j31.mjs → router-DBAZfE7X.mjs} +2 -2
  145. package/dist/{router-DD6D4j31.mjs.map → router-DBAZfE7X.mjs.map} +1 -1
  146. package/dist/{run-D7K5UALn.mjs → run-DDdT9teG.mjs} +530 -210
  147. package/dist/run-DDdT9teG.mjs.map +1 -0
  148. package/dist/{runtime-paths-rni52zHX.mjs → runtime-paths-B0P1TvUr.mjs} +1 -1
  149. package/dist/{runtime-paths-rni52zHX.mjs.map → runtime-paths-B0P1TvUr.mjs.map} +1 -1
  150. package/dist/{search-Rpk1cSBC.mjs → search-B69cQwDB.mjs} +3 -3
  151. package/dist/{search-Rpk1cSBC.mjs.map → search-B69cQwDB.mjs.map} +1 -1
  152. package/dist/{search-CfPpJAWQ.mjs → search-fb-5ovj4.mjs} +2 -2
  153. package/dist/{server-D4vAieF4.mjs → server-Ch99xt5K.mjs} +15 -14
  154. package/dist/server-Ch99xt5K.mjs.map +1 -0
  155. package/dist/skills/Advisor/SKILL.md +3 -3
  156. package/dist/skills/Providers/SKILL.md +30 -0
  157. package/dist/skills/Reconstruct/SKILL.md +1 -1
  158. package/dist/skills/VaultContext/SKILL.md +1 -1
  159. package/dist/skills/Whisper/SKILL.md +3 -3
  160. package/dist/skills/Worker/SKILL.md +1 -1
  161. package/dist/{sources-CS43CpuS.mjs → sources-KBGEA_E6.mjs} +2 -2
  162. package/dist/{sources-CS43CpuS.mjs.map → sources-KBGEA_E6.mjs.map} +1 -1
  163. package/dist/{sqlite-Cq3gJcq9.mjs → sqlite-By6r2YhZ.mjs} +3 -3
  164. package/dist/{sqlite-Cq3gJcq9.mjs.map → sqlite-By6r2YhZ.mjs.map} +1 -1
  165. package/dist/{state-CY6I-Hnc.mjs → state-DnYGRmS7.mjs} +1 -1
  166. package/dist/{state-BRdB-Bf1.mjs → state-PJrgfDLW.mjs} +1 -1
  167. package/dist/{state-BRdB-Bf1.mjs.map → state-PJrgfDLW.mjs.map} +1 -1
  168. package/dist/{stop-words-Hfu8u22w.mjs → stop-words-BaMEGVeY.mjs} +1 -1
  169. package/dist/{stop-words-Hfu8u22w.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
  170. package/dist/{sync-BWbe8JTg.mjs → sync-Btcb6xA4.mjs} +3 -3
  171. package/dist/{sync-BWbe8JTg.mjs.map → sync-Btcb6xA4.mjs.map} +1 -1
  172. package/dist/{themes-B7h9YDiy.mjs → themes-BVoAnLZj.mjs} +3 -3
  173. package/dist/{themes-B7h9YDiy.mjs.map → themes-BVoAnLZj.mjs.map} +1 -1
  174. package/dist/{tools-BW3NLMPk.mjs → tools-BradfQk7.mjs} +4 -3
  175. package/dist/{tools-LlzcV_eK.mjs → tools-UrSHIe29.mjs} +30 -233
  176. package/dist/tools-UrSHIe29.mjs.map +1 -0
  177. package/dist/{trace-u4hT-pLD.mjs → trace-Cy8quQTm.mjs} +1 -1
  178. package/dist/{trace-u4hT-pLD.mjs.map → trace-Cy8quQTm.mjs.map} +1 -1
  179. package/dist/{utils-DhMex3Ox.mjs → utils-BD8g8oSf.mjs} +1 -1
  180. package/dist/{utils-9Err2RBW.mjs → utils-CJymHkYm.mjs} +1 -1
  181. package/dist/{utils-9Err2RBW.mjs.map → utils-CJymHkYm.mjs.map} +1 -1
  182. package/dist/{vault-indexer-mata9S7b.mjs → vault-indexer-C5i-OAyr.mjs} +2 -2
  183. package/dist/{vault-indexer-mata9S7b.mjs.map → vault-indexer-C5i-OAyr.mjs.map} +1 -1
  184. package/dist/wakeup-TnGjJY3I.mjs +218 -0
  185. package/dist/wakeup-TnGjJY3I.mjs.map +1 -0
  186. package/dist/work-queue-worker-BA5IDBwQ.mjs +567 -0
  187. package/dist/work-queue-worker-BA5IDBwQ.mjs.map +1 -0
  188. package/dist/work-queue-worker-DLM2wlqM.mjs +14 -0
  189. package/dist/{zettelkasten-F0D_XfQ4.mjs → zettelkasten-BBjA7GcP.mjs} +4 -4
  190. package/dist/{zettelkasten-F0D_XfQ4.mjs.map → zettelkasten-BBjA7GcP.mjs.map} +1 -1
  191. package/docs/commands/README.md +30 -3
  192. package/docs/commands/audit.md +88 -0
  193. package/docs/commands/backup.md +2 -2
  194. package/docs/commands/clear-names.md +1 -1
  195. package/docs/commands/config.md +59 -0
  196. package/docs/commands/daemon.md +1 -1
  197. package/docs/commands/db.md +1 -1
  198. package/docs/commands/end.md +1 -1
  199. package/docs/commands/help.md +1 -1
  200. package/docs/commands/identity.md +1 -1
  201. package/docs/commands/kg.md +1 -1
  202. package/docs/commands/launch.md +45 -0
  203. package/docs/commands/mcp.md +1 -1
  204. package/docs/commands/memory.md +3 -3
  205. package/docs/commands/notify.md +1 -1
  206. package/docs/commands/observation.md +1 -1
  207. package/docs/commands/obsidian.md +1 -1
  208. package/docs/commands/pause.md +1 -1
  209. package/docs/commands/project.md +46 -1
  210. package/docs/commands/projects.md +46 -1
  211. package/docs/commands/registry.md +1 -1
  212. package/docs/commands/restore.md +1 -1
  213. package/docs/commands/session.md +1 -1
  214. package/docs/commands/sessions.md +1 -1
  215. package/docs/commands/setup.md +1 -1
  216. package/docs/commands/shell-init.md +1 -1
  217. package/docs/commands/skill.md +1 -1
  218. package/docs/commands/task.md +1 -1
  219. package/docs/commands/topic.md +1 -1
  220. package/docs/commands/update.md +1 -1
  221. package/docs/commands/worker.md +95 -6
  222. package/docs/commands/zettel.md +1 -1
  223. package/docs/provider-abstraction.md +1 -1
  224. package/docs/provider-independence-details.md +8 -5
  225. package/docs/task-bus.md +1 -1
  226. package/docs/worker.md +136 -83
  227. package/docs/workers-config.md +446 -0
  228. package/package.json +4 -2
  229. package/scripts/build-hooks.mjs +56 -3
  230. package/src/hooks/pre-compact.sh +5 -2
  231. package/src/hooks/session-stop.sh +29 -4
  232. package/src/hooks/ts/capture-all-events.ts +3 -3
  233. package/src/hooks/ts/lib/advisor-budget.test.ts +98 -0
  234. package/src/hooks/ts/lib/advisor-budget.ts +90 -0
  235. package/src/hooks/ts/lib/agent-gate.test.ts +101 -0
  236. package/src/hooks/ts/lib/agent-gate.ts +112 -0
  237. package/src/hooks/ts/lib/context-fill.ts +2 -2
  238. package/src/hooks/ts/lib/edit-gate.test.ts +52 -0
  239. package/src/hooks/ts/lib/edit-gate.ts +51 -0
  240. package/src/hooks/ts/lib/mcp-deferred-reminder.test.ts +8 -0
  241. package/src/hooks/ts/lib/pai-paths.test.ts +93 -0
  242. package/src/hooks/ts/lib/pai-paths.ts +168 -24
  243. package/src/hooks/ts/lib/project-utils/index.ts +1 -0
  244. package/src/hooks/ts/lib/project-utils/paths.ts +5 -4
  245. package/src/hooks/ts/lib/project-utils.ts +1 -0
  246. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +2 -2
  247. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  248. package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +46 -99
  249. package/src/hooks/ts/pre-tool-use/route-edits-to-worker.test.ts +47 -0
  250. package/src/hooks/ts/pre-tool-use/route-edits-to-worker.ts +96 -0
  251. package/src/hooks/ts/pre-tool-use/security-validator.ts +2 -1
  252. package/src/hooks/ts/session-end/capture-session-summary.ts +4 -4
  253. package/src/hooks/ts/session-start/initialize-session.ts +3 -3
  254. package/src/hooks/ts/session-start/inject-observations.test.ts +56 -0
  255. package/src/hooks/ts/session-start/inject-observations.ts +28 -8
  256. package/src/hooks/ts/session-start/load-core-context.ts +4 -4
  257. package/src/hooks/ts/session-start/load-project-context.ts +2 -2
  258. package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -0
  259. package/src/hooks/ts/session-start/session-commands.ts +4 -0
  260. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +36 -0
  261. package/src/hooks/ts/stop/stop-hook.ts +6 -11
  262. package/src/hooks/ts/user-prompt/whisper-rules.test.ts +49 -0
  263. package/src/hooks/ts/user-prompt/whisper-rules.ts +88 -47
  264. package/statusline-command.sh +277 -116
  265. package/templates/agent-prefs.example.md +8 -8
  266. package/templates/claude-md.template.md +1 -1
  267. package/templates/voices.example.json +3 -3
  268. package/dist/config-C_ErGddD.mjs +0 -3
  269. package/dist/config-_lQI_W08.mjs +0 -640
  270. package/dist/config-_lQI_W08.mjs.map +0 -1
  271. package/dist/context-handover-cache-pNFdBNrU.mjs +0 -1548
  272. package/dist/context-handover-cache-pNFdBNrU.mjs.map +0 -1
  273. package/dist/daemon-B8N1egl0.mjs.map +0 -1
  274. package/dist/daemon-DHfyoTlk.mjs +0 -23
  275. package/dist/db-O-cyAPfS.mjs +0 -3
  276. package/dist/db-XEwJbuGO.mjs +0 -3
  277. package/dist/detector-C-7KRi42.mjs +0 -5
  278. package/dist/factory-Dbo8yNPi.mjs +0 -3
  279. package/dist/fallback-oJl3BaIN.mjs.map +0 -1
  280. package/dist/main-resolver-BAfOMppW.mjs +0 -7
  281. package/dist/main-resolver-DnkxPYkr.mjs.map +0 -1
  282. package/dist/program-zpoYnJGz.mjs.map +0 -1
  283. package/dist/query-feedback-DGoVRaWD.mjs +0 -3
  284. package/dist/query-feedback-Dqz-HC__.mjs.map +0 -1
  285. package/dist/router-BTIGZR2_.mjs +0 -3
  286. package/dist/run-D7K5UALn.mjs.map +0 -1
  287. package/dist/server-D4vAieF4.mjs.map +0 -1
  288. package/dist/tools-LlzcV_eK.mjs.map +0 -1
  289. package/dist/work-queue-worker-AEmfVE0F.mjs +0 -14
  290. package/dist/work-queue-worker-DIT-VLSh.mjs +0 -2136
  291. package/dist/work-queue-worker-DIT-VLSh.mjs.map +0 -1
  292. /package/dist/{indexer-D7MvSQPY.mjs → indexer-C5Le7XE7.mjs} +0 -0
@@ -2,27 +2,47 @@
2
2
  * PAI Path Resolution - Single Source of Truth
3
3
  *
4
4
  * This module provides consistent path resolution across all PAI hooks.
5
- * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude
6
5
  *
7
- * ALSO loads .env file from PAI_DIR so all hooks get environment variables
8
- * without relying on Claude Code's settings.json injection.
6
+ * Two different things live here, and they must not be confused:
7
+ *
8
+ * - ADAPTER_DIR (~/.claude by default) is the Claude Code harness adapter —
9
+ * fixed, hardcoded paths the harness itself loads from (Hooks/, Skills/,
10
+ * Agents/, Commands/, settings.json, statusline-command.sh,
11
+ * tab-color-command.sh). It is harness-specific: a future harness would
12
+ * need its own adapter directory with its own conventions.
13
+ * - PAI_HOME (~/.claude/pai by default — see ../../../config/pai-home.ts) is
14
+ * where PAI's own state lives: everything hooks WRITE (History/,
15
+ * agent-sessions.json, session-routing.json, ...) resolves there, with a
16
+ * fallback to the pre-2026-09-19 ADAPTER_DIR location and a one-time
17
+ * stderr notice, exactly like every other PAI_HOME file.
18
+ *
19
+ * ALSO loads .env file from ADAPTER_DIR so all hooks get environment
20
+ * variables without relying on Claude Code's settings.json injection.
9
21
  *
10
22
  * Usage in hooks:
11
- * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';
23
+ * import { ADAPTER_DIR, HOOKS_DIR, SKILLS_DIR, historyDir } from './lib/pai-paths';
12
24
  */
13
25
 
14
26
  import { homedir } from 'os';
15
27
  import { resolve, join } from 'path';
16
28
  import { existsSync, readFileSync } from 'fs';
29
+ import {
30
+ paiHomePath,
31
+ resolvePaiFile,
32
+ migratePaiFile,
33
+ migratePaiDir,
34
+ type MigrateFileResult,
35
+ type MigrateDirResult,
36
+ } from '../../../config/pai-home.js';
17
37
 
18
38
  /**
19
39
  * Load .env file and inject into process.env
20
- * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed
40
+ * Must run BEFORE ADAPTER_DIR resolution so .env can set ADAPTER_DIR/PAI_DIR if needed
21
41
  */
22
42
  function loadEnvFile(): void {
23
43
  // Check common locations for .env
24
44
  const possiblePaths = [
25
- resolve(process.env.PAI_DIR || '', '.env'),
45
+ resolve(process.env.ADAPTER_DIR || process.env.PAI_DIR || '', '.env'),
26
46
  resolve(homedir(), '.claude', '.env'),
27
47
  ];
28
48
 
@@ -68,40 +88,102 @@ function loadEnvFile(): void {
68
88
  // Load .env FIRST, before any other initialization
69
89
  loadEnvFile();
70
90
 
91
+ function defaultAdapterDir(): string {
92
+ return resolve(homedir(), '.claude');
93
+ }
94
+
95
+ /**
96
+ * Never print when stdout/stderr is a data channel, not a human: hook
97
+ * bundles and worker-status-line.mjs set PAI_QUIET_NOTICES=1 via their
98
+ * esbuild banner (scripts/build-hooks.mjs), and a `pai worker run
99
+ * --output-format json` invocation is detected directly off argv since its
100
+ * env can't be set before this module's static imports resolve.
101
+ */
102
+ function suppressDeprecationNotices(): boolean {
103
+ if (process.env.PAI_QUIET_NOTICES === '1') return true;
104
+ const idx = process.argv.indexOf('--output-format');
105
+ return idx !== -1 && process.argv[idx + 1] === 'json';
106
+ }
107
+
108
+ /**
109
+ * At most once per process — a `globalThis` flag rather than a module-level
110
+ * `let` because hooks and the CLI can end up with more than one instance of
111
+ * this module loaded into the same process (separate bundles), each with its
112
+ * own module scope; only a property on the shared global survives that.
113
+ */
114
+ function warnPaiDirDeprecatedOnce(message: string): void {
115
+ const g = globalThis as typeof globalThis & { __paiDirNoticePrinted?: boolean };
116
+ if (g.__paiDirNoticePrinted || suppressDeprecationNotices()) return;
117
+ g.__paiDirNoticePrinted = true;
118
+ process.stderr.write(`pai: ${message}\n`);
119
+ }
120
+
71
121
  /**
72
- * Smart PAI_DIR detection with fallback
122
+ * Smart ADAPTER_DIR detection with fallback
73
123
  * Priority:
74
- * 1. PAI_DIR environment variable (if set)
75
- * 2. ~/.claude (standard location)
124
+ * 1. ADAPTER_DIR environment variable (if set) — warns once if PAI_DIR is
125
+ * ALSO set and resolves to a different path (naming both).
126
+ * 2. PAI_DIR environment variable (deprecated alias, one release) — warns
127
+ * once only when it differs from the default adapter root; PAI_DIR set to
128
+ * the same value as the default is the operator's current, correct
129
+ * setting and stays silent.
130
+ * 3. ~/.claude (standard location)
76
131
  */
77
- export const PAI_DIR = process.env.PAI_DIR
78
- ? resolve(process.env.PAI_DIR)
79
- : resolve(homedir(), '.claude');
132
+ function resolveAdapterDir(): string {
133
+ if (process.env.ADAPTER_DIR) {
134
+ const adapterDir = resolve(process.env.ADAPTER_DIR);
135
+ if (process.env.PAI_DIR) {
136
+ const paiDir = resolve(process.env.PAI_DIR);
137
+ if (paiDir !== adapterDir) {
138
+ warnPaiDirDeprecatedOnce(
139
+ `ADAPTER_DIR (${adapterDir}) and PAI_DIR (${paiDir}) are both set and differ — ADAPTER_DIR wins. PAI_DIR still works for one release.`
140
+ );
141
+ }
142
+ }
143
+ return adapterDir;
144
+ }
145
+ if (process.env.PAI_DIR) {
146
+ const paiDir = resolve(process.env.PAI_DIR);
147
+ if (paiDir !== defaultAdapterDir()) {
148
+ warnPaiDirDeprecatedOnce(
149
+ 'PAI_DIR is deprecated — use ADAPTER_DIR for the harness adapter root (~/.claude). PAI_DIR still works for one release.'
150
+ );
151
+ }
152
+ return paiDir;
153
+ }
154
+ return defaultAdapterDir();
155
+ }
156
+
157
+ /** The Claude Code harness adapter root — NOT PAI's state home. See module doc above. */
158
+ export const ADAPTER_DIR = resolveAdapterDir();
159
+
160
+ /** @deprecated alias for ADAPTER_DIR, kept for one release. Prefer ADAPTER_DIR. */
161
+ export const PAI_DIR = ADAPTER_DIR;
80
162
 
81
163
  /**
82
- * Common PAI directories
164
+ * Adapter directories — fixed paths the Claude Code harness itself loads
165
+ * from. These stay under ADAPTER_DIR; they are not PAI state.
83
166
  */
84
- export const HOOKS_DIR = join(PAI_DIR, 'Hooks');
85
- export const SKILLS_DIR = join(PAI_DIR, 'Skills');
86
- export const AGENTS_DIR = join(PAI_DIR, 'Agents');
87
- export const HISTORY_DIR = join(PAI_DIR, 'History');
88
- export const COMMANDS_DIR = join(PAI_DIR, 'Commands');
167
+ export const HOOKS_DIR = join(ADAPTER_DIR, 'Hooks');
168
+ export const SKILLS_DIR = join(ADAPTER_DIR, 'Skills');
169
+ export const AGENTS_DIR = join(ADAPTER_DIR, 'Agents');
170
+ export const COMMANDS_DIR = join(ADAPTER_DIR, 'Commands');
89
171
 
90
172
  /**
91
173
  * Validate PAI directory structure on first import
92
174
  * This fails fast with a clear error if PAI is misconfigured
93
175
  */
94
176
  function validatePAIStructure(): void {
95
- if (!existsSync(PAI_DIR)) {
96
- console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
97
- console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
177
+ if (!existsSync(ADAPTER_DIR)) {
178
+ console.error(`ADAPTER_DIR does not exist: ${ADAPTER_DIR}`);
179
+ console.error(` Expected ~/.claude or set ADAPTER_DIR environment variable`);
98
180
  process.exit(1);
99
181
  }
100
182
 
101
183
  if (!existsSync(HOOKS_DIR)) {
102
184
  console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
103
- console.error(` Your PAI_DIR may be misconfigured`);
104
- console.error(` Current PAI_DIR: ${PAI_DIR}`);
185
+ console.error(` Your ADAPTER_DIR may be misconfigured`);
186
+ console.error(` Current ADAPTER_DIR: ${ADAPTER_DIR}`);
105
187
  process.exit(1);
106
188
  }
107
189
  }
@@ -110,6 +192,68 @@ function validatePAIStructure(): void {
110
192
  // This ensures any hook that imports this module will fail fast if paths are wrong
111
193
  validatePAIStructure();
112
194
 
195
+ // ---------------------------------------------------------------------------
196
+ // PAI state written by hooks — resolves under PAI_HOME, falling back to the
197
+ // pre-2026-09-19 ADAPTER_DIR location (one-time stderr notice) until
198
+ // `pai config migrate --history` moves it. Actively written on every hook
199
+ // event across every session, so unlike most PAI_HOME files the live move is
200
+ // deliberately NOT automatic — see `pai config migrate --history`.
201
+ // ---------------------------------------------------------------------------
202
+
203
+ function oldHistoryDir(): string {
204
+ return join(ADAPTER_DIR, 'History');
205
+ }
206
+
207
+ /** Read/write location for hook-captured history: PAI_HOME/History if
208
+ * present, else the old ADAPTER_DIR/History (one-time stderr notice). */
209
+ export function historyDir(): string {
210
+ return resolvePaiFile(paiHomePath('History'), [oldHistoryDir()], 'pai config migrate --history');
211
+ }
212
+
213
+ export function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {
214
+ return migratePaiDir(paiHomePath('History'), [oldHistoryDir()], opts);
215
+ }
216
+
217
+ function oldAgentSessionsPath(): string {
218
+ return join(ADAPTER_DIR, 'agent-sessions.json');
219
+ }
220
+
221
+ export function agentSessionsPath(): string {
222
+ return resolvePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], 'pai config migrate --history');
223
+ }
224
+
225
+ export function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {
226
+ return migratePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], opts);
227
+ }
228
+
229
+ function oldSecurityEventsPath(): string {
230
+ return join(ADAPTER_DIR, 'history', 'security', 'security-events.jsonl');
231
+ }
232
+
233
+ export function securityEventsPath(): string {
234
+ return resolvePaiFile(
235
+ paiHomePath('History', 'security', 'security-events.jsonl'),
236
+ [oldSecurityEventsPath()],
237
+ 'pai config migrate --history'
238
+ );
239
+ }
240
+
241
+ export function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {
242
+ return migratePaiFile(paiHomePath('History', 'security', 'security-events.jsonl'), [oldSecurityEventsPath()], opts);
243
+ }
244
+
245
+ function oldSessionRoutingPath(): string {
246
+ return join(ADAPTER_DIR, 'session-routing.json');
247
+ }
248
+
249
+ export function sessionRoutingPath(): string {
250
+ return resolvePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], 'pai config migrate --history');
251
+ }
252
+
253
+ export function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {
254
+ return migratePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], opts);
255
+ }
256
+
113
257
  /**
114
258
  * Helper to get history file path with date-based organization
115
259
  */
@@ -120,5 +264,5 @@ export function getHistoryFilePath(subdir: string, filename: string): string {
120
264
  const year = localDate.getFullYear();
121
265
  const month = String(localDate.getMonth() + 1).padStart(2, '0');
122
266
 
123
- return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);
267
+ return join(historyDir(), subdir, `${year}-${month}`, filename);
124
268
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  export {
7
+ ADAPTER_DIR,
7
8
  PAI_DIR,
8
9
  PROJECTS_DIR,
9
10
  isProbeSession,
@@ -4,11 +4,12 @@
4
4
 
5
5
  import { existsSync, mkdirSync, readdirSync, linkSync, copyFileSync } from 'fs';
6
6
  import { join, basename } from 'path';
7
- import { PAI_DIR } from '../pai-paths.js';
7
+ import { ADAPTER_DIR, PAI_DIR } from '../pai-paths.js';
8
8
 
9
- // Re-export PAI_DIR for consumers
10
- export { PAI_DIR };
11
- export const PROJECTS_DIR = join(PAI_DIR, 'projects');
9
+ // Re-export for consumers. PROJECTS_DIR is a harness-adjacent data dir, not
10
+ // PAI_HOME state — out of scope for the PAI_DIR→PAI_HOME fold (see pai-paths.ts).
11
+ export { ADAPTER_DIR, PAI_DIR };
12
+ export const PROJECTS_DIR = join(ADAPTER_DIR, 'projects');
12
13
 
13
14
  /**
14
15
  * Directories known to be automated health-check / probe sessions.
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export type { WorkItem, TodoItem } from "./project-utils/index.js";
6
6
  export {
7
+ ADAPTER_DIR,
7
8
  PAI_DIR,
8
9
  PROJECTS_DIR,
9
10
  isProbeSession,
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { appendFileSync, mkdirSync, existsSync } from 'fs';
11
11
  import { join } from 'path';
12
- import { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';
12
+ import { historyDir } from '../lib/pai-paths';
13
13
 
14
14
  interface ToolUseData {
15
15
  tool_name: string;
@@ -20,7 +20,7 @@ interface ToolUseData {
20
20
  }
21
21
 
22
22
  // Configuration
23
- const CAPTURE_DIR = join(HISTORY_DIR, 'raw-outputs');
23
+ const CAPTURE_DIR = join(historyDir(), 'raw-outputs');
24
24
  const INTERESTING_TOOLS = ['Bash', 'Edit', 'Write', 'Read', 'Task', 'NotebookEdit'];
25
25
 
26
26
  async function main() {
@@ -707,7 +707,7 @@ async function main() {
707
707
  // BUG (live): a production digest was found with NO "HANDOVER SOURCE:"
708
708
  // line at all — its cause was never pinned down for certain (the
709
709
  // deployed hook is a compiled dist/ artifact reached through
710
- // ${PAI_DIR}/Hooks/..., a separate build step from editing this
710
+ // ${ADAPTER_DIR}/Hooks/..., a separate build step from editing this
711
711
  // source, which is itself a plausible way for "works when I run the
712
712
  // .ts source" and "missing in production" to diverge). Regardless of
713
713
  // cause: sourceLabel and handoverBlock are now computed in their own
@@ -1,51 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * route-agents-to-worker.ts — PreToolUse hook on the Agent tool.
4
+ * route-agents-to-worker.ts — PreToolUse hook on the in-process subagent tool.
5
5
  *
6
- * Denies every in-process subagent when worker providers are configured, so no
7
- * worker ever runs on Anthropic. The deny reason tells the orchestrator how to
8
- * delegate through `pai worker run` instead — same idea as the old
9
- * route-agents-to-glm.sh, but provider-neutral and ledgered in the workers
10
- * log dir.
6
+ * Denies every in-process subagent, in every project, so that delegated work
7
+ * always goes through `pai worker run` and therefore shows up in
8
+ * `pai worker ps`, in the status line and in the worker log dir. The deny
9
+ * reason names the replacement command, `--provider anthropic` included, so
10
+ * there is never a reason to reach for the built-in tool.
11
11
  *
12
- * Allows (and does not touch) when:
13
- * - the tool is not the Agent tool (this hook is Agent-matched, but stay safe)
14
- * - ALLOW_ANTHROPIC_AGENTS=1 is in the environment (one-session bypass)
15
- * - workers are off or no provider is configured
12
+ * This file is only I/O: read stdin, ask lib/agent-gate for the decision,
13
+ * ledger it, print it. The decision itself lives in lib/agent-gate.ts and is
14
+ * tested there.
16
15
  *
17
- * Every decision is appended to the routing ledger read by `pai worker log`.
16
+ * Registered in ~/.claude/settings.json under a PreToolUse matcher that covers
17
+ * both tool names ("Agent|Task") — see src/workers/install.ts, which owns that
18
+ * registration.
18
19
  */
19
20
 
20
- import { readWorkersSection, ANTHROPIC_NATIVE } from "../../../workers/config.js";
21
+ import { readWorkersSection } from "../../../workers/config.js";
21
22
  import { workersLogDir, ledgerPath } from "../../../workers/paths.js";
22
23
  import { appendLedger } from "../../../workers/ledger.js";
24
+ import {
25
+ decideAgentGate,
26
+ renderDecision,
27
+ shortLabel,
28
+ type AgentGateInput,
29
+ } from "../lib/agent-gate.js";
23
30
 
24
- interface HookInput {
25
- session_id?: string;
26
- cwd?: string;
27
- tool_name?: string;
28
- tool_input?: { description?: string; prompt?: string; subagent_type?: string } | string;
29
- }
30
-
31
- const ALLOW = JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } });
32
-
33
- function deny(reason: string): string {
34
- return JSON.stringify({
35
- hookSpecificOutput: {
36
- hookEventName: "PreToolUse",
37
- permissionDecision: "deny",
38
- permissionDecisionReason: reason,
39
- },
40
- });
41
- }
42
-
43
- function shortLabel(input: HookInput): string {
44
- const desc =
45
- typeof input.tool_input === "object" && input.tool_input !== null
46
- ? input.tool_input.description ?? ""
47
- : "";
48
- return desc.replace(/\s+/g, " ").trim().slice(0, 60);
31
+ /**
32
+ * What to print when the hook cannot read or parse its own input. This hook is
33
+ * matcher-scoped to the subagent tool, so anything that reaches it *is* a
34
+ * subagent launch: a broken event denies rather than allows, and the bypass
35
+ * env var is still honoured. A gate that opens whenever it stumbles is the
36
+ * failure this hook exists to prevent.
37
+ */
38
+ function fallback(): string {
39
+ return renderDecision(decideAgentGate({}, process.env));
49
40
  }
50
41
 
51
42
  async function main(): Promise<void> {
@@ -53,80 +44,36 @@ async function main(): Promise<void> {
53
44
  try {
54
45
  for await (const chunk of process.stdin) text += chunk;
55
46
  } catch {
56
- process.stdout.write(ALLOW);
57
- return;
58
- }
59
- if (!text.trim()) {
60
- process.stdout.write(ALLOW);
61
- return;
62
- }
63
-
64
- let input: HookInput;
65
- try {
66
- input = JSON.parse(text) as HookInput;
67
- } catch {
68
- process.stdout.write(ALLOW);
47
+ process.stdout.write(fallback());
69
48
  return;
70
49
  }
71
50
 
72
- if (input.tool_name && input.tool_name !== "Agent") {
73
- process.stdout.write(ALLOW);
74
- return;
51
+ let input: AgentGateInput = {};
52
+ if (text.trim()) {
53
+ try {
54
+ input = JSON.parse(text) as AgentGateInput;
55
+ } catch {
56
+ // A malformed event on a matcher-scoped hook is still a subagent launch;
57
+ // an empty input denies, which is the safe direction here.
58
+ input = {};
59
+ }
75
60
  }
76
61
 
77
- const label = shortLabel(input);
78
- const cwd = input.cwd ?? process.cwd();
62
+ const decision = decideAgentGate(input, process.env);
79
63
 
80
- const ledger = (event: string): void => {
64
+ if (decision.ledger) {
65
+ const label = shortLabel(input);
81
66
  try {
82
- appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)), event, {
83
- cwd,
67
+ appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)), decision.ledger, {
68
+ cwd: input.cwd ?? process.cwd(),
84
69
  ...(label ? { label } : {}),
85
70
  });
86
71
  } catch {
87
- // no config / no log dir yet — the decision below still stands
72
+ // no config / no log dir yet — the decision still stands
88
73
  }
89
- };
90
-
91
- if (process.env.ALLOW_ANTHROPIC_AGENTS === "1") {
92
- ledger("ALLOWED-ANTHROPIC-AGENT");
93
- process.stdout.write(ALLOW);
94
- return;
95
- }
96
-
97
- let workers: ReturnType<typeof readWorkersSection>["workers"];
98
- try {
99
- workers = readWorkersSection().workers;
100
- } catch {
101
- // broken workers config must not break the session
102
- process.stdout.write(ALLOW);
103
- return;
104
- }
105
- if (
106
- !workers.enabled ||
107
- Object.keys(workers.providers).length === 0 ||
108
- workers.active === ANTHROPIC_NATIVE
109
- ) {
110
- process.stdout.write(ALLOW);
111
- return;
112
74
  }
113
75
 
114
- ledger("DENIED-ANTHROPIC-AGENT");
115
- process.stdout.write(
116
- deny(
117
- "Agent tool is disabled: subagents run on the configured worker provider, not Anthropic. " +
118
- "Delegate with Bash instead, in the background:\n\n" +
119
- `pai worker run --label "${label || "task"}" --class research -p '<full, self-contained task spec>' ` +
120
- "--allowedTools 'Read,Edit,Write,Bash,Grep,Glob' --output-format json\n\n" +
121
- "- Run it with run_in_background: true and always with a timeout.\n" +
122
- "- Use --class spotcheck (or implement) as the task demands.\n" +
123
- "- Web research: add WebSearch,WebFetch to --allowedTools.\n" +
124
- "- The answer is in the `result` field of the JSON it prints. Review the diff yourself.\n" +
125
- "- pai worker ps lists running workers; pai worker follow <id> shows one live.\n" +
126
- "Keep only orchestration, review and synthesis in this session. " +
127
- "To run subagents on Anthropic for one session: start it with ALLOW_ANTHROPIC_AGENTS=1."
128
- )
129
- );
76
+ process.stdout.write(renderDecision(decision));
130
77
  }
131
78
 
132
- main().catch(() => process.stdout.write(ALLOW));
79
+ main().catch(() => process.stdout.write(fallback()));
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { execFileSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+
5
+ // The entry does its own stdin read and calls main() unconditionally at
6
+ // module scope — importing it in-process would run that read against the
7
+ // test runner's own stdin. Run it as a real subprocess with its own piped
8
+ // stdin, exactly like session-start/session-start-worker-guard.test.ts.
9
+ //
10
+ // Uses this checkout as the git work tree (it already has a .git dir), not a
11
+ // tmp-created one: a repo under os.tmpdir() would itself match the hook's
12
+ // /tmp exemption and mask the "inside a git work tree" case being tested.
13
+ describe("route-edits-to-worker", () => {
14
+ const ENTRYPOINT = "src/hooks/ts/pre-tool-use/route-edits-to-worker.ts";
15
+ const repo = process.cwd();
16
+
17
+ function runHook(filePath: string, env: NodeJS.ProcessEnv = {}): string {
18
+ // This test may itself run as a worker (PAI_WORKER=1 in its own env),
19
+ // which must not leak into the child and mask the case under test.
20
+ const { PAI_WORKER: _drop, ...baseEnv } = process.env;
21
+ return execFileSync("bun", [ENTRYPOINT], {
22
+ input: JSON.stringify({
23
+ cwd: repo,
24
+ tool_name: "Edit",
25
+ tool_input: { file_path: filePath, old_string: "a", new_string: "b" },
26
+ }),
27
+ encoding: "utf8",
28
+ timeout: 15_000,
29
+ env: { ...baseEnv, ...env },
30
+ });
31
+ }
32
+
33
+ it("allows a Notes/TODO.md edit (prints nothing)", () => {
34
+ expect(runHook(join(repo, "Notes/TODO.md"))).toBe("");
35
+ });
36
+
37
+ it("blocks a source file edit with the standing message", () => {
38
+ const out = runHook(join(repo, "src/workers/run.ts"));
39
+ const parsed = JSON.parse(out);
40
+ expect(parsed.decision).toBe("block");
41
+ expect(parsed.reason).toContain("main session does not edit code");
42
+ });
43
+
44
+ it("allows a source file edit when PAI_WORKER=1", () => {
45
+ expect(runHook(join(repo, "src/workers/run.ts"), { PAI_WORKER: "1" })).toBe("");
46
+ });
47
+ });
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * route-edits-to-worker.ts — PreToolUse (Edit|MultiEdit|Write)
5
+ *
6
+ * The outer orchestrator session does not edit code files; code changes go
7
+ * through `pai worker run`. Allowed through silently:
8
+ * - worker sessions (the runner sets PAI_WORKER=1)
9
+ * - paths outside any git work tree
10
+ * - the exempt scratch prefixes (.claude incl. memory dirs, the local
11
+ * notes/tasks/scratchpad dirs, the OS temp dir)
12
+ * - prose: notes, session logs, handovers — see lib/edit-gate.ts isProseEdit
13
+ * Everything else inside a git work tree is blocked with the worker command.
14
+ *
15
+ * This file is only I/O: read stdin, resolve the path, ask lib/edit-gate for
16
+ * the decision, print it. The decision itself lives in lib/edit-gate.ts and
17
+ * is tested there.
18
+ */
19
+
20
+ import { existsSync } from "node:fs";
21
+ import { dirname, join, resolve, sep } from "node:path";
22
+ import { homedir, tmpdir } from "node:os";
23
+ import { decideEditGate, type EditGateContext } from "../lib/edit-gate.js";
24
+
25
+ function isUnder(p: string, prefix: string): boolean {
26
+ return p === prefix || p.startsWith(prefix + sep);
27
+ }
28
+
29
+ // A .git entry in any ancestor marks a git work tree (.git is a directory in
30
+ // a normal repo, a file in a linked worktree — existsSync covers both).
31
+ function inGitWorkTree(p: string): boolean {
32
+ let dir = resolve(p);
33
+ for (;;) {
34
+ if (existsSync(join(dir, ".git"))) return true;
35
+ const parent = dirname(dir);
36
+ if (parent === dir) return false;
37
+ dir = parent;
38
+ }
39
+ }
40
+
41
+ function exemptPrefixes(): string[] {
42
+ const home = homedir();
43
+ return [
44
+ join(home, ".claude"),
45
+ join(home, "colibri", "Notes"),
46
+ join(home, "colibri", "tasks"),
47
+ join(home, "colibri", "scratchpad"),
48
+ tmpdir(),
49
+ "/tmp",
50
+ ];
51
+ }
52
+
53
+ function allow(): void {
54
+ process.exit(0);
55
+ }
56
+
57
+ function block(reason: string): void {
58
+ process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
59
+ process.exit(0);
60
+ }
61
+
62
+ async function main(): Promise<void> {
63
+ let text = "";
64
+ try {
65
+ for await (const chunk of process.stdin) text += chunk;
66
+ } catch {
67
+ return allow();
68
+ }
69
+ if (!text.trim()) return allow();
70
+
71
+ let input: { cwd?: string; tool_input?: { file_path?: string } };
72
+ try {
73
+ input = JSON.parse(text);
74
+ } catch {
75
+ return allow();
76
+ }
77
+
78
+ const filePath =
79
+ input.tool_input && typeof input.tool_input.file_path === "string" ? input.tool_input.file_path : "";
80
+ if (!filePath) return allow();
81
+
82
+ const abs = resolve(input.cwd ?? process.cwd(), filePath);
83
+
84
+ const ctx: EditGateContext = {
85
+ filePath: abs,
86
+ isPaiWorker: process.env.PAI_WORKER === "1",
87
+ isExemptPrefix: exemptPrefixes().some((p) => isUnder(abs, p)),
88
+ isGitWorkTree: inGitWorkTree(abs),
89
+ };
90
+
91
+ const decision = decideEditGate(ctx);
92
+ if (decision.decision === "block") return block(decision.reason);
93
+ return allow();
94
+ }
95
+
96
+ main().catch(() => process.exit(0));
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { appendFileSync, mkdirSync, existsSync } from 'fs';
21
21
  import { dirname } from 'path';
22
+ import { securityEventsPath } from '../lib/pai-paths.js';
22
23
 
23
24
  // ============================================================================
24
25
  // ATTACK PATTERNS - CUSTOMIZE THESE FOR YOUR ENVIRONMENT
@@ -130,7 +131,7 @@ function detectAttack(content: string): DetectionResult {
130
131
  // ============================================================================
131
132
 
132
133
  function logSecurityEvent(event: Record<string, unknown>): void {
133
- const logPath = `${process.env.PAI_DIR || '~/.claude'}/history/security/security-events.jsonl`;
134
+ const logPath = securityEventsPath();
134
135
  const entry = JSON.stringify({ timestamp: new Date().toISOString(), ...event }) + '\n';
135
136
 
136
137
  try {