@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
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } 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 SessionData {
15
15
  conversation_id: string;
@@ -62,7 +62,7 @@ async function main() {
62
62
  const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;
63
63
 
64
64
  // Ensure directory exists
65
- const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);
65
+ const sessionDir = join(historyDir(), 'sessions', yearMonth);
66
66
  if (!existsSync(sessionDir)) {
67
67
  mkdirSync(sessionDir, { recursive: true });
68
68
  }
@@ -87,7 +87,7 @@ async function main() {
87
87
 
88
88
  async function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {
89
89
  // Try to read raw outputs for this session
90
- const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);
90
+ const rawOutputsDir = join(historyDir(), 'raw-outputs', yearMonth);
91
91
 
92
92
  let filesChanged: string[] = [];
93
93
  let commandsExecuted: string[] = [];
@@ -196,7 +196,7 @@ ${info.commandsExecuted.length > 0 ? '```bash\n' + info.commandsExecuted.join('\
196
196
 
197
197
  This session summary was automatically generated by the UOCS SessionEnd hook.
198
198
 
199
- For detailed tool outputs, see: \`\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\`
199
+ For detailed tool outputs, see: \`${join(historyDir(), 'raw-outputs', timestamp.substring(0, 7))}/\`
200
200
 
201
201
  ---
202
202
 
@@ -14,7 +14,7 @@
14
14
  * Setup:
15
15
  * 1. Set environment variables in settings.json:
16
16
  * - DA: Your AI's name (e.g., "Kai", "Nova", "Assistant")
17
- * - PAI_DIR: Path to your PAI directory (defaults to $HOME/.claude)
17
+ * - ADAPTER_DIR: Path to your Claude Code harness adapter dir (defaults to $HOME/.claude)
18
18
  * 2. Ensure load-core-context.ts exists in hooks/session-start/ directory
19
19
  * 3. Add all three SessionStart hooks to settings.json in this order:
20
20
  * initialize-session.ts, load-core-context.ts, load-project-context.ts
@@ -24,7 +24,7 @@ import { isWorkerSession } from "../lib/worker-session.js";
24
24
  import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
25
25
  import { join } from 'path';
26
26
  import { tmpdir } from 'os';
27
- import { PAI_DIR } from '../lib/pai-paths';
27
+ import { ADAPTER_DIR } from '../lib/pai-paths';
28
28
  import { sendNtfyNotification, isWhatsAppEnabled, isProbeSession } from '../lib/project-utils';
29
29
 
30
30
  // Debounce duration in milliseconds (prevents duplicate SessionStart events)
@@ -61,7 +61,7 @@ function shouldDebounce(): boolean {
61
61
  }
62
62
 
63
63
  async function testStopHook() {
64
- const stopHookPath = join(PAI_DIR, 'hooks/stop-hook.ts');
64
+ const stopHookPath = join(ADAPTER_DIR, 'hooks/stop-hook.ts');
65
65
 
66
66
  console.error('\nTesting stop-hook configuration...');
67
67
 
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { formatContext, isNoiseEntry } from "./inject-observations.js";
3
+
4
+ function row(id: number, title: string, minutesAgo = 0) {
5
+ return {
6
+ id,
7
+ session_id: `s${id}`,
8
+ project_id: 1,
9
+ project_slug: "pai",
10
+ type: "discovery",
11
+ title,
12
+ narrative: null,
13
+ created_at: new Date(Date.now() - minutesAgo * 60_000).toISOString(),
14
+ };
15
+ }
16
+
17
+ describe("isNoiseEntry", () => {
18
+ it("flags Ran: and MCP: prefixed titles", () => {
19
+ expect(isNoiseEntry("Ran: ls -la")).toBe(true);
20
+ expect(isNoiseEntry("MCP: memory_search")).toBe(true);
21
+ expect(isNoiseEntry("Modified foo.ts")).toBe(false);
22
+ });
23
+ });
24
+
25
+ describe("formatContext", () => {
26
+ it("caps the timeline at 6 entries and drops noise entries", () => {
27
+ const observations = [
28
+ ...Array.from({ length: 10 }, (_, i) => row(i, `Ran: cmd ${i}`)),
29
+ ...Array.from({ length: 8 }, (_, i) => row(100 + i, `Modified file${i}.ts`)),
30
+ ];
31
+ const context = formatContext("pai", observations);
32
+ const timelineLines = context
33
+ .split("\n")
34
+ .filter(l => l.startsWith("- ["));
35
+ expect(timelineLines).toHaveLength(6);
36
+ expect(timelineLines.every(l => !l.includes("Ran:"))).toBe(true);
37
+ });
38
+
39
+ it("falls back to unfiltered entries when fewer than 3 non-noise entries remain", () => {
40
+ const observations = [
41
+ row(1, "Ran: cmd 1"),
42
+ row(2, "Ran: cmd 2"),
43
+ row(3, "Modified file.ts"),
44
+ ];
45
+ const context = formatContext("pai", observations);
46
+ const timelineLines = context.split("\n").filter(l => l.startsWith("- ["));
47
+ expect(timelineLines).toHaveLength(3);
48
+ });
49
+
50
+ it("truncates each timeline line to 80 characters", () => {
51
+ const longTitle = "Modified " + "x".repeat(200) + ".ts";
52
+ const context = formatContext("pai", [row(1, longTitle)]);
53
+ const timelineLine = context.split("\n").find(l => l.startsWith("- ["))!;
54
+ expect(timelineLine.length).toBeLessThanOrEqual(80);
55
+ });
56
+ });
@@ -20,6 +20,16 @@
20
20
  import { connect } from 'net';
21
21
  import { randomUUID } from 'crypto';
22
22
  import { isProbeSession } from '../lib/project-utils.js';
23
+ import { isWorkerSession } from '../lib/worker-session.js';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Tuning
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const MAX_TIMELINE_ENTRIES = 6;
30
+ const MAX_ENTRY_LENGTH = 80;
31
+ const MIN_FILTERED_ENTRIES = 3;
32
+ const NOISE_PREFIXES = ['Ran:', 'MCP:'];
23
33
 
24
34
  // ---------------------------------------------------------------------------
25
35
  // Types
@@ -134,11 +144,15 @@ function truncate(s: string, maxLen: number): string {
134
144
  return s.length > maxLen ? s.slice(0, maxLen - 1) + '\u2026' : s;
135
145
  }
136
146
 
147
+ export function isNoiseEntry(title: string): boolean {
148
+ return NOISE_PREFIXES.some(prefix => title.startsWith(prefix));
149
+ }
150
+
137
151
  // ---------------------------------------------------------------------------
138
152
  // Format observations as progressive disclosure context
139
153
  // ---------------------------------------------------------------------------
140
154
 
141
- function formatContext(
155
+ export function formatContext(
142
156
  projectSlug: string,
143
157
  observations: ObservationRow[]
144
158
  ): string {
@@ -152,19 +166,22 @@ function formatContext(
152
166
  const newest = new Date(observations[0].created_at);
153
167
  const lastActivity = timeAgo(newest);
154
168
 
155
- // Timeline: show most recent 15, keep titles to 80 chars
156
- const timelineObs = observations.slice(0, 15);
169
+ // Drop low-signal "Ran:"/"MCP:" entries, unless that leaves too few to show.
170
+ const filtered = observations.filter(o => !isNoiseEntry(o.title));
171
+ const source = filtered.length >= MIN_FILTERED_ENTRIES ? filtered : observations;
172
+
173
+ // Timeline: show at most MAX_TIMELINE_ENTRIES, each line capped to MAX_ENTRY_LENGTH.
174
+ const timelineObs = source.slice(0, MAX_TIMELINE_ENTRIES);
157
175
  const timeline = timelineObs
158
176
  .map(o => {
159
177
  const t = timeAgo(new Date(o.created_at));
160
178
  const label = typeLabel(o.type);
161
- const title = truncate(o.title, 80);
162
- return `- [${t}] ${label} ${title}`;
179
+ return truncate(`- [${t}] ${label} ${o.title}`, MAX_ENTRY_LENGTH);
163
180
  })
164
181
  .join('\n');
165
182
 
166
- const showingNote = observations.length > 15
167
- ? `(showing most recent 15 of ${observations.length}, use observation_search for more)`
183
+ const showingNote = observations.length > timelineObs.length
184
+ ? `(showing ${timelineObs.length} most recent of ${observations.length}, use observation_search for more)`
168
185
  : `(showing ${observations.length} observation${observations.length !== 1 ? 's' : ''})`;
169
186
 
170
187
  const lines: string[] = [
@@ -188,6 +205,7 @@ function formatContext(
188
205
  // ---------------------------------------------------------------------------
189
206
 
190
207
  async function main() {
208
+ if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
191
209
  try {
192
210
  // Skip probe/health-check sessions
193
211
  if (isProbeSession()) {
@@ -251,4 +269,6 @@ async function main() {
251
269
  }
252
270
  }
253
271
 
254
- main();
272
+ if (!process.env.VITEST) {
273
+ main();
274
+ }
@@ -13,9 +13,9 @@
13
13
  * - Bypass skill activation logic by directly injecting context
14
14
  *
15
15
  * Setup:
16
- * 1. Customize your ${PAI_DIR}/skills/CORE/SKILL.md with your personal context
16
+ * 1. Customize your ${ADAPTER_DIR}/skills/CORE/SKILL.md with your personal context
17
17
  * 2. Add this hook to settings.json SessionStart hooks
18
- * 3. Ensure PAI_DIR environment variable is set (defaults to $HOME/.claude)
18
+ * 3. Ensure ADAPTER_DIR environment variable is set (defaults to $HOME/.claude)
19
19
  *
20
20
  * How it works:
21
21
  * - Runs at the start of every Claude Code session
@@ -28,7 +28,7 @@
28
28
  import { isWorkerSession } from "../lib/worker-session.js";
29
29
  import { readFileSync, existsSync } from 'fs';
30
30
  import { join } from 'path';
31
- import { PAI_DIR, SKILLS_DIR } from '../lib/pai-paths';
31
+ import { SKILLS_DIR } from '../lib/pai-paths';
32
32
  import { isProbeSession } from '../lib/project-utils';
33
33
 
34
34
  async function main() {
@@ -57,7 +57,7 @@ async function main() {
57
57
  // Verify CORE skill file exists
58
58
  if (!existsSync(coreSkillPath)) {
59
59
  console.error(`CORE skill not found at: ${coreSkillPath}`);
60
- console.error(`Ensure CORE/SKILL.md exists or check PAI_DIR environment variable`);
60
+ console.error(`Ensure CORE/SKILL.md exists or check ADAPTER_DIR environment variable`);
61
61
  process.exit(1);
62
62
  }
63
63
 
@@ -23,8 +23,8 @@ import { homedir } from 'os';
23
23
  import { execSync } from 'child_process';
24
24
  import { buildWakeupContext } from '../../../memory/wakeup.js';
25
25
  import { readContinueCheckpoint } from '../../../session/checkpoint-block.js';
26
+ import { sessionRoutingPath } from '../lib/pai-paths.js';
26
27
  import {
27
- PAI_DIR,
28
28
  findNotesDir,
29
29
  getProjectDir,
30
30
  getCurrentNotePath,
@@ -62,7 +62,7 @@ function findPaiBinary(): string {
62
62
  * Returns the routed Notes path if set, or null to use default behavior.
63
63
  */
64
64
  function getRoutedNotesPath(): string | null {
65
- const routingFile = join(PAI_DIR, 'session-routing.json');
65
+ const routingFile = sessionRoutingPath();
66
66
  if (!existsSync(routingFile)) return null;
67
67
 
68
68
  try {
@@ -16,8 +16,10 @@
16
16
  */
17
17
 
18
18
  import { decideMcpDeferredReminder, type ReminderHookInput } from "../lib/mcp-deferred-reminder.js";
19
+ import { isWorkerSession } from "../lib/worker-session.js";
19
20
 
20
21
  async function main() {
22
+ if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
21
23
  let input: ReminderHookInput = {};
22
24
 
23
25
  try {
@@ -10,6 +10,10 @@
10
10
  * even when the full CORE skill gets truncated.
11
11
  */
12
12
 
13
+ import { isWorkerSession } from "../lib/worker-session.js";
14
+
15
+ if (isWorkerSession()) process.exit(0); // disposable worker: no per-session bookkeeping
16
+
13
17
  const commands = `<system-reminder>
14
18
  SESSION COMMANDS (always available):
15
19
 
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { execFileSync } from "node:child_process";
3
+
4
+ // Every SessionStart hook entrypoint calls main() unconditionally at module
5
+ // scope and (for most of them) blocks reading fd 0 — importing them in-process
6
+ // would run that read against the test runner's own stdin and hang the suite.
7
+ // Each case below runs the entrypoint as a real subprocess with its own piped
8
+ // stdin and env, exactly like production invocation (see
9
+ // user-prompt/whisper-rules.test.ts for the same pattern).
10
+ describe("session-start hooks: worker sessions get no injected context", () => {
11
+ const HOOKS = [
12
+ "src/hooks/ts/session-start/initialize-session.ts",
13
+ "src/hooks/ts/session-start/inject-observations.ts",
14
+ "src/hooks/ts/session-start/load-core-context.ts",
15
+ "src/hooks/ts/session-start/load-project-context.ts",
16
+ "src/hooks/ts/session-start/mcp-deferred-reminder.ts",
17
+ "src/hooks/ts/session-start/post-compact-inject.ts",
18
+ "src/hooks/ts/session-start/session-commands.ts",
19
+ ];
20
+
21
+ function runHook(entrypoint: string, env: NodeJS.ProcessEnv): string {
22
+ return execFileSync("bun", [entrypoint], {
23
+ input: JSON.stringify({ session_id: "t", hook_event_name: "SessionStart", source: "startup" }),
24
+ encoding: "utf8",
25
+ timeout: 15_000,
26
+ env: { ...process.env, ...env },
27
+ });
28
+ }
29
+
30
+ for (const entrypoint of HOOKS) {
31
+ it(`${entrypoint} prints nothing when PAI_WORKER=1`, () => {
32
+ const stdout = runHook(entrypoint, { PAI_WORKER: "1" });
33
+ expect(stdout).toBe("");
34
+ });
35
+ }
36
+ });
@@ -16,6 +16,7 @@ import {
16
16
  updateTodoContinue, sessionIdFromTranscript,
17
17
  WorkItem
18
18
  } from '../lib/project-utils';
19
+ import { paiHomePath, sessionStateDir } from '../../../config/pai-files.js';
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // Constants
@@ -57,20 +58,13 @@ const AUTO_SAVE_INTERVAL = (() => {
57
58
  // Session-state helpers (mid-session auto-save)
58
59
  // ---------------------------------------------------------------------------
59
60
 
60
- const SESSION_STATE_DIR = join(
61
- process.env.HOME ?? '/tmp',
62
- '.config',
63
- 'pai',
64
- 'session-state'
65
- );
66
-
67
61
  interface SessionState {
68
62
  humanMessageCount: number;
69
63
  }
70
64
 
71
65
  function readSessionState(sessionId: string): SessionState {
72
66
  try {
73
- const stateFile = join(SESSION_STATE_DIR, `${sessionId}.json`);
67
+ const stateFile = join(sessionStateDir(), `${sessionId}.json`);
74
68
  if (!existsSync(stateFile)) return { humanMessageCount: 0 };
75
69
  const raw = readFileSync(stateFile, 'utf-8');
76
70
  const parsed = JSON.parse(raw) as Partial<SessionState>;
@@ -84,8 +78,9 @@ function readSessionState(sessionId: string): SessionState {
84
78
 
85
79
  function writeSessionState(sessionId: string, state: SessionState): void {
86
80
  try {
87
- mkdirSync(SESSION_STATE_DIR, { recursive: true });
88
- const stateFile = join(SESSION_STATE_DIR, `${sessionId}.json`);
81
+ const dir = paiHomePath('session-state');
82
+ mkdirSync(dir, { recursive: true });
83
+ const stateFile = join(dir, `${sessionId}.json`);
89
84
  writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
90
85
  } catch (e) {
91
86
  console.error(`STOP-HOOK: Could not write session state: ${e}`);
@@ -94,7 +89,7 @@ function writeSessionState(sessionId: string, state: SessionState): void {
94
89
 
95
90
  function deleteSessionState(sessionId: string): void {
96
91
  try {
97
- const stateFile = join(SESSION_STATE_DIR, `${sessionId}.json`);
92
+ const stateFile = join(sessionStateDir(), `${sessionId}.json`);
98
93
  if (existsSync(stateFile)) {
99
94
  unlinkSync(stateFile);
100
95
  }
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { execFileSync } from "node:child_process";
3
+
4
+ // whisper-rules.ts calls main() unconditionally at module scope (it's a hook
5
+ // entrypoint, always run as a subprocess) — including a blocking read of fd 0.
6
+ // Importing it in-process here would run that read against the test runner's
7
+ // own stdin and hang the suite, so every case below goes through a real
8
+ // subprocess with its own piped stdin, exactly like production invocation.
9
+ describe("user-prompt/whisper-rules entrypoint", () => {
10
+ const ENTRYPOINT = "src/hooks/ts/user-prompt/whisper-rules.ts";
11
+
12
+ function runHook(stdin: string): string {
13
+ return execFileSync("bun", [ENTRYPOINT], {
14
+ input: stdin,
15
+ encoding: "utf8",
16
+ timeout: 15_000,
17
+ });
18
+ }
19
+
20
+ it("emits nothing for a worker-completion relay prompt", () => {
21
+ const stdout = runHook(JSON.stringify({ prompt: "[Session:x] worker y finished" }));
22
+ expect(stdout).toBe("");
23
+ });
24
+
25
+ it("emits nothing for a clickr control-handover line", () => {
26
+ const stdout = runHook(JSON.stringify({ prompt: "your controls." }));
27
+ expect(stdout).toBe("");
28
+ });
29
+
30
+ it("emits nothing for a harness [SYSTEM NOTIFICATION preamble", () => {
31
+ const stdout = runHook(
32
+ JSON.stringify({ prompt: "[SYSTEM NOTIFICATION - NOT USER INPUT]\nfoo" })
33
+ );
34
+ expect(stdout).toBe("");
35
+ });
36
+
37
+ it("emits nothing for a background task completion carrying <task-notification>", () => {
38
+ const stdout = runHook(
39
+ JSON.stringify({ prompt: "foo <task-notification>x</task-notification>" })
40
+ );
41
+ expect(stdout).toBe("");
42
+ });
43
+
44
+ it("still injects the system-reminder block for a normal prompt", () => {
45
+ const stdout = runHook(JSON.stringify({ prompt: "please fix the login bug" }));
46
+ expect(stdout).toContain("<system-reminder>");
47
+ expect(stdout).toContain("CURRENT LOCAL TIME");
48
+ });
49
+ });
@@ -4,8 +4,8 @@
4
4
  * whisper-rules.ts
5
5
  *
6
6
  * UserPromptSubmit hook that injects:
7
- * 1. User-defined whisper rules from ~/.claude/whisper-rules.md
8
- * 2. Budget-aware model tiering guidance from ~/.claude/advisor-mode.json
7
+ * 1. User-defined whisper rules from ~/.claude/pai/whisper-rules.md
8
+ * 2. Budget-aware model tiering guidance from ~/.claude/pai/advisor-mode.json
9
9
  *
10
10
  * The advisor mode implements the "advisor strategy" pattern:
11
11
  * - Normal (budget < 70%): use any model freely
@@ -13,16 +13,25 @@
13
13
  * - Strict (85-95%): haiku only for subagents, main context stays on current model
14
14
  * - Critical (>95%): minimize all subagent spawning, essential work only
15
15
  *
16
- * Budget percentage is written by the statusline or manually to advisor-mode.json.
17
- * If the file doesn't exist, no advisor guidance is injected.
16
+ * Budget percentage is written by the statusline or manually to advisor-mode.json,
17
+ * stamped with the epoch second it was read. If the file doesn't exist, or its
18
+ * reading is too old to still describe the current window, no advisor guidance
19
+ * is injected — see lib/advisor-budget.ts for why that direction is the safe one.
18
20
  */
19
21
 
20
22
  import { existsSync, readFileSync, unlinkSync } from "node:fs";
21
23
  import { join } from "node:path";
22
- import { homedir, tmpdir } from "node:os";
24
+ import { tmpdir } from "node:os";
25
+ import { resolveAdvisorMode, type AdvisorConfig } from "../lib/advisor-budget.js";
26
+ import { whisperRulesPath, advisorModePath } from "../../../config/pai-files.js";
23
27
 
24
- const WHISPER_FILE = join(homedir(), ".claude", "whisper-rules.md");
25
- const ADVISOR_FILE = join(homedir(), ".claude", "advisor-mode.json");
28
+ const WHISPER_FILE = whisperRulesPath();
29
+ const ADVISOR_FILE = advisorModePath();
30
+
31
+ /** A "# N. TITLE" section header: the start of a new section, tag reset. */
32
+ function isSectionHeader(line: string): boolean {
33
+ return /^#\s*\d+\.\s/.test(line);
34
+ }
26
35
 
27
36
  /**
28
37
  * Read the rule file, dropping everything that is there for the author rather
@@ -35,28 +44,42 @@ const ADVISOR_FILE = join(homedir(), ".claude", "advisor-mode.json");
35
44
  * maintainable, and none of it reaches the prompt. Only the rules do.
36
45
  *
37
46
  * A line whose rule text legitimately starts with "#" can be escaped as "\\#".
47
+ *
48
+ * Section tags: this file is injected into every claude process, spawned
49
+ * workers included, and some rules (e.g. "delegate to workers") are true only
50
+ * for the interactive orchestrating session. A comment line "# @orchestrator"
51
+ * marks every following rule as orchestrator-only, "# @worker" marks
52
+ * worker-only, until the next "# N. TITLE" section header resets the tag (a
53
+ * section's own header/divider box, which the tag line sits inside, does not
54
+ * count as the next section). An untagged section reaches both. `isWorker`
55
+ * selects which side of the tag this process is on — see PAI_WORKER in
56
+ * run-env.ts.
38
57
  */
39
- function getWhisperRules(): string {
58
+ function getWhisperRules(isWorker: boolean): string {
40
59
  if (!existsSync(WHISPER_FILE)) return "";
41
60
  try {
42
- return readFileSync(WHISPER_FILE, "utf-8")
43
- .split("\n")
44
- .map((l) => l.trim())
45
- .filter((l) => l.length > 0 && !l.startsWith("#"))
46
- .map((l) => (l.startsWith("\\#") ? l.slice(1) : l))
47
- .join("\n")
48
- .trim();
61
+ const lines = readFileSync(WHISPER_FILE, "utf-8").split("\n");
62
+ const out: string[] = [];
63
+ let tag: "orchestrator" | "worker" | null = null;
64
+ for (const raw of lines) {
65
+ const line = raw.trim();
66
+ if (line.length === 0) continue;
67
+ if (line.startsWith("#")) {
68
+ if (isSectionHeader(line)) tag = null;
69
+ else if (line === "# @orchestrator") tag = "orchestrator";
70
+ else if (line === "# @worker") tag = "worker";
71
+ continue;
72
+ }
73
+ if (tag === "orchestrator" && isWorker) continue;
74
+ if (tag === "worker" && !isWorker) continue;
75
+ out.push(line.startsWith("\\#") ? line.slice(1) : line);
76
+ }
77
+ return out.join("\n").trim();
49
78
  } catch {
50
79
  return "";
51
80
  }
52
81
  }
53
82
 
54
- interface AdvisorConfig {
55
- weeklyBudgetPercent?: number; // 0-100, written by statusline or manually
56
- mode?: "normal" | "conservative" | "strict" | "critical" | "auto";
57
- forceModel?: string; // override: always use this model for subagents
58
- }
59
-
60
83
  function getAdvisorGuidance(): string {
61
84
  if (!existsSync(ADVISOR_FILE)) return "";
62
85
 
@@ -67,24 +90,12 @@ function getAdvisorGuidance(): string {
67
90
  return "";
68
91
  }
69
92
 
70
- // Determine mode.
71
- //
72
- // `??` only substitutes on null/undefined, so a config written as
73
- // {"mode": ""} — which is what the statusline produces when it has no manual
74
- // override — left mode as the empty string. That matched no case below, so
75
- // the advisor silently returned nothing regardless of budget. A budget guard
76
- // that quietly does nothing is worse than none, because you believe you have
77
- // one. Treat anything that is not a recognised mode as "auto".
78
- const VALID = ["normal", "conservative", "strict", "critical", "auto"] as const;
79
- let mode: string =
80
- config.mode && (VALID as readonly string[]).includes(config.mode) ? config.mode : "auto";
81
- if (mode === "auto" && typeof config.weeklyBudgetPercent === "number") {
82
- const pct = config.weeklyBudgetPercent;
83
- if (pct < 60) mode = "normal";
84
- else if (pct < 80) mode = "conservative";
85
- else if (pct < 92) mode = "strict";
86
- else mode = "critical";
87
- }
93
+ // Determine mode: the manual one if set, else derived from a budget reading
94
+ // that is current. No current reading and no manual mode means say nothing —
95
+ // guessing is how a wrong number gets obeyed for a day and a half. See
96
+ // lib/advisor-budget.ts.
97
+ const { mode, percent: pct } = resolveAdvisorMode(config, Math.floor(Date.now() / 1000));
98
+ if (mode === undefined) return "";
88
99
 
89
100
  // Force model override
90
101
  if (config.forceModel) {
@@ -97,7 +108,7 @@ function getAdvisorGuidance(): string {
97
108
 
98
109
  case "conservative":
99
110
  return [
100
- `ADVISOR MODE (conservative — weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
111
+ `ADVISOR MODE (conservative — weekly budget at ${pct ?? "?"}%):`,
101
112
  "Main context is opus (most expensive — 20x haiku, 5x sonnet). Delegate aggressively to subagents.",
102
113
  "Default subagents to SONNET (Agent tool, model: sonnet). Use haiku for simple lookups/verification.",
103
114
  "For substantial tasks, use swarm mode: spawn a sonnet orchestrator that delegates to haiku workers.",
@@ -106,7 +117,7 @@ function getAdvisorGuidance(): string {
106
117
 
107
118
  case "strict":
108
119
  return [
109
- `ADVISOR MODE (strict — weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
120
+ `ADVISOR MODE (strict — weekly budget at ${pct ?? "?"}%):`,
110
121
  "Main context is opus (most expensive — 20x haiku, 5x sonnet). Minimize work done here.",
111
122
  "Default subagents to SONNET (Agent tool, model: sonnet) for implementation and research. Use haiku for simple tasks.",
112
123
  "For any substantial task, use swarm mode: spawn ONE sonnet orchestrator that delegates to haiku workers.",
@@ -116,7 +127,7 @@ function getAdvisorGuidance(): string {
116
127
 
117
128
  case "critical":
118
129
  return [
119
- `ADVISOR MODE (critical — weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
130
+ `ADVISOR MODE (critical — weekly budget at ${pct ?? "?"}%):`,
120
131
  "MINIMIZE ALL TOKEN USAGE. Main context is opus — the most expensive model (20x haiku, 5x sonnet).",
121
132
  "For ANY non-trivial task, immediately spawn a sonnet orchestrator agent and let it handle everything.",
122
133
  "Main context should only send the task and receive the final result — do not do work here.",
@@ -153,24 +164,54 @@ function currentLocalTime(): string {
153
164
  * true after the first turn — a reminder that misstates its own trigger is one
154
165
  * the reader learns to discount.
155
166
  */
156
- function resetReinjectCounter(): void {
167
+ function resetReinjectCounter(sessionId: string): void {
157
168
  try {
158
- const raw = readFileSync(0, "utf-8");
159
- const sessionId = raw.trim() ? (JSON.parse(raw) as { session_id?: string }).session_id ?? "" : "";
160
169
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "") || "nosession";
161
170
  const f = join(tmpdir(), "pai-whisper-reinject", `${safe}.count`);
162
171
  if (existsSync(f)) unlinkSync(f);
163
172
  } catch { /* best effort — a stale count is not worth failing the hook over */ }
164
173
  }
165
174
 
175
+ /**
176
+ * True for prompts this hook never authored a human at the keyboard: a
177
+ * worker-completion relay ("[Session: …] worker … finished …"), a clickr
178
+ * control-handover line ("your controls." / "my controls."), or a harness
179
+ * notification (a "[SYSTEM NOTIFICATION" preamble, or a background task
180
+ * completion carrying a <task-notification> block). All of these fire
181
+ * several times per turn during worker-heavy work, so injecting the ~5KB rule
182
+ * block on each one was landing it 3-5 times in a single turn.
183
+ */
184
+ export function isRelayedPrompt(prompt: string): boolean {
185
+ return (
186
+ /^\s*\[Session:[^\]]+\]/.test(prompt) ||
187
+ /^\s*(your|my) controls\b/i.test(prompt) ||
188
+ /^\s*\[SYSTEM NOTIFICATION/.test(prompt) ||
189
+ prompt.includes("<task-notification>")
190
+ );
191
+ }
192
+
166
193
  function main() {
167
- resetReinjectCounter();
194
+ let sessionId = "";
195
+ let prompt = "";
196
+ try {
197
+ const raw = readFileSync(0, "utf-8");
198
+ if (raw.trim()) {
199
+ const input = JSON.parse(raw) as { session_id?: string; prompt?: string };
200
+ sessionId = input.session_id ?? "";
201
+ prompt = input.prompt ?? "";
202
+ }
203
+ } catch { /* malformed/empty stdin — treat as no session, no prompt */ }
204
+
205
+ resetReinjectCounter(sessionId);
206
+
207
+ if (isRelayedPrompt(prompt)) return;
168
208
 
209
+ const isWorker = process.env.PAI_WORKER === "1";
169
210
  const parts: string[] = [];
170
211
 
171
212
  parts.push(`CURRENT LOCAL TIME: ${currentLocalTime()} — use this verbatim for any [YYYY-MM-DD HH:MM] stamp; never estimate or increment it.`);
172
213
 
173
- const rules = getWhisperRules();
214
+ const rules = getWhisperRules(isWorker);
174
215
  if (rules) parts.push(rules);
175
216
 
176
217
  const advisor = getAdvisorGuidance();