@tekmidian/pai 0.42.5 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/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-CEC1a5mX.mjs} +5 -4
  9. package/dist/{chain-DTII7xeL.mjs.map → chain-CEC1a5mX.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-B8N1egl0.mjs → daemon-BhldpFvO.mjs} +52 -38
  26. package/dist/daemon-BhldpFvO.mjs.map +1 -0
  27. package/dist/daemon-QIvCXB5f.mjs +24 -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-BQJ8PbCK.mjs} +185 -123
  48. package/dist/fallback-BQJ8PbCK.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 +207 -122
  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 +11 -0
  96. package/dist/hooks/whisper-reinject.mjs.map +3 -3
  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 +7883 -169
  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-BlKGgpWI.mjs} +7 -6
  132. package/dist/{planner-D5Ui7DAm.mjs.map → planner-BlKGgpWI.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-C5jRQ1kg.mjs} +8880 -5913
  136. package/dist/program-C5jRQ1kg.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-ByBKowN5.mjs} +980 -247
  147. package/dist/run-ByBKowN5.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-CAG2sUjf.mjs} +4 -3
  175. package/dist/{tools-LlzcV_eK.mjs → tools-DiRC5glA.mjs} +30 -233
  176. package/dist/tools-DiRC5glA.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-BfElduEU.mjs +231 -0
  185. package/dist/wakeup-BfElduEU.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 +32 -3
  192. package/docs/commands/audit.md +113 -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 +97 -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 +58 -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/post-tool-use/whisper-reinject.test.ts +55 -0
  248. package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -0
  249. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  250. package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +46 -99
  251. package/src/hooks/ts/pre-tool-use/route-edits-to-worker.test.ts +47 -0
  252. package/src/hooks/ts/pre-tool-use/route-edits-to-worker.ts +96 -0
  253. package/src/hooks/ts/pre-tool-use/security-validator.ts +2 -1
  254. package/src/hooks/ts/session-end/capture-session-summary.ts +4 -4
  255. package/src/hooks/ts/session-start/initialize-session.ts +3 -3
  256. package/src/hooks/ts/session-start/inject-observations.test.ts +56 -0
  257. package/src/hooks/ts/session-start/inject-observations.ts +28 -8
  258. package/src/hooks/ts/session-start/load-core-context.ts +4 -4
  259. package/src/hooks/ts/session-start/load-project-context.ts +2 -2
  260. package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -0
  261. package/src/hooks/ts/session-start/session-commands.ts +4 -0
  262. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +36 -0
  263. package/src/hooks/ts/stop/stop-hook.ts +6 -11
  264. package/src/hooks/ts/user-prompt/whisper-rules.test.ts +49 -0
  265. package/src/hooks/ts/user-prompt/whisper-rules.ts +88 -47
  266. package/src/hooks/ztk-auto.sh +28 -0
  267. package/src/hooks/ztk-auto.test.ts +72 -0
  268. package/src/hooks/ztk-passthrough.mjs +71 -0
  269. package/statusline-command.sh +277 -116
  270. package/templates/agent-prefs.example.md +8 -8
  271. package/templates/claude-md.template.md +1 -1
  272. package/templates/voices.example.json +3 -3
  273. package/dist/config-C_ErGddD.mjs +0 -3
  274. package/dist/config-_lQI_W08.mjs +0 -640
  275. package/dist/config-_lQI_W08.mjs.map +0 -1
  276. package/dist/context-handover-cache-pNFdBNrU.mjs +0 -1548
  277. package/dist/context-handover-cache-pNFdBNrU.mjs.map +0 -1
  278. package/dist/daemon-B8N1egl0.mjs.map +0 -1
  279. package/dist/daemon-DHfyoTlk.mjs +0 -23
  280. package/dist/db-O-cyAPfS.mjs +0 -3
  281. package/dist/db-XEwJbuGO.mjs +0 -3
  282. package/dist/detector-C-7KRi42.mjs +0 -5
  283. package/dist/factory-Dbo8yNPi.mjs +0 -3
  284. package/dist/fallback-oJl3BaIN.mjs.map +0 -1
  285. package/dist/main-resolver-BAfOMppW.mjs +0 -7
  286. package/dist/main-resolver-DnkxPYkr.mjs.map +0 -1
  287. package/dist/program-zpoYnJGz.mjs.map +0 -1
  288. package/dist/query-feedback-DGoVRaWD.mjs +0 -3
  289. package/dist/query-feedback-Dqz-HC__.mjs.map +0 -1
  290. package/dist/router-BTIGZR2_.mjs +0 -3
  291. package/dist/run-D7K5UALn.mjs.map +0 -1
  292. package/dist/server-D4vAieF4.mjs.map +0 -1
  293. package/dist/tools-LlzcV_eK.mjs.map +0 -1
  294. package/dist/work-queue-worker-AEmfVE0F.mjs +0 -14
  295. package/dist/work-queue-worker-DIT-VLSh.mjs +0 -2136
  296. package/dist/work-queue-worker-DIT-VLSh.mjs.map +0 -1
  297. /package/dist/{indexer-D7MvSQPY.mjs → indexer-C5Le7XE7.mjs} +0 -0
@@ -1,12 +1,163 @@
1
- import { _ as readWorkersSection, c as classModelCapability, f as isModelCapability, g as providerKeyPath, o as WorkersConfigError, s as assertProviderRunnable, v as resolveModelCapability } from "./config-_lQI_W08.mjs";
2
- import { _ as workersLogDir, d as setCooldown, f as ensureNoMcpConfig, g as statusPath, h as panesDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, o as isQuotaFailure, p as eventsPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-D4vAieF4.mjs";
1
+ import { n as openRegistry, r as registryDbPath } from "./db-Ddns80yf.mjs";
2
+ import { _ as providerKeyPath, b as resolveProviderKey, c as classModelCapability, f as isModelCapability, o as WorkersConfigError, s as assertProviderRunnable, v as readWorkersSection, y as resolveModelCapability } from "./config-DxMZjpvY.mjs";
3
+ import { _ as workersLogDir, d as setCooldown, f as ensureNoMcpConfig, g as statusPath, h as panesDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, o as isQuotaFailure, p as eventsPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-Ch99xt5K.mjs";
3
4
  import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
5
  import { homedir, tmpdir } from "node:os";
5
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
- import { connect, createServer } from "node:net";
7
7
  import { execFileSync, spawn } from "node:child_process";
8
+ import { connect, createServer } from "node:net";
8
9
  import { createInterface } from "node:readline";
9
10
 
11
+ //#region src/workers/mcp.ts
12
+ /**
13
+ * mcp.ts — the MCP allowlist for headless workers.
14
+ *
15
+ * Headless workers start with NO MCP servers: every server definition is a
16
+ * prompt-time tool inventory the model pays to know, and a worker that only
17
+ * reads and edits files needs none of it. A run may opt in with `--mcp
18
+ * name[,name…]`, a role carrying `"mcp": [...]`, or implicitly by naming
19
+ * `mcp__server__tool` in --allowedTools (a grant without its server loaded is
20
+ * a dead letter). Names may be single servers from ~/.claude.json's
21
+ * `mcpServers` or `workers.mcpSets` set names, which expand to their member
22
+ * list. The filtered config lands in
23
+ * `<logDir>/<id>.mcp.json` and is passed with `--strict-mcp-config
24
+ * --mcp-config` so exactly those servers load. MCP servers are chosen at
25
+ * launch only — a mid-run `say` cannot add any.
26
+ *
27
+ * One name in that space is not a server at all: `claude-in-chrome` is the
28
+ * Chrome native-host bridge, which no config file can load and which the
29
+ * `--chrome` flag switches on instead. It is recognised here so a grant
30
+ * naming it becomes that flag rather than an "unknown MCP server" rejection.
31
+ */
32
+ /** ~/.claude.json — the user's MCP server definitions (top-level mcpServers). */
33
+ const CLAUDE_JSON = join(homedir(), ".claude.json");
34
+ /**
35
+ * The browser bridge is not an MCP server. It rides the Chrome native-host
36
+ * channel, so it never appears in `mcpServers`; a spawned claude has it off
37
+ * and the `--chrome` flag switches it on. Tool grants naming it must
38
+ * therefore neither resolve to a server nor be rejected as unknown — they
39
+ * select a flag. See `grantsChrome`.
40
+ */
41
+ const CHROME_SERVER = "claude-in-chrome";
42
+ /**
43
+ * True for a real server definition, false for anything else that happens to
44
+ * sit under `mcpServers`.
45
+ *
46
+ * The vendor config is not ours and has been observed carrying non-server
47
+ * entries under that key — tool-usage records keyed by tool name
48
+ * (`Read`, `Bash`, `mcp__server__tool`, …). Those are not servers: handing
49
+ * one to `--mcp-config` yields "invalid MCP server config", and listing one
50
+ * as available invites a run that cannot work. A server has a `command` (or a
51
+ * `url` for remote transports); an `mcp__…` key is a tool name whatever its
52
+ * shape.
53
+ */
54
+ function isServerDefinition(name, value) {
55
+ if (name.startsWith("mcp__")) return false;
56
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
57
+ const v = value;
58
+ return typeof v.command === "string" && v.command.length > 0 || typeof v.url === "string" && v.url.length > 0;
59
+ }
60
+ function readMcpServers(claudeJson = CLAUDE_JSON) {
61
+ try {
62
+ if (!existsSync(claudeJson)) return {};
63
+ const servers = JSON.parse(readFileSync(claudeJson, "utf8")).mcpServers;
64
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return {};
65
+ const out = {};
66
+ for (const [name, value] of Object.entries(servers)) if (isServerDefinition(name, value)) out[name] = value;
67
+ return out;
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+ /**
73
+ * True when any `--mcp` name or `--allowedTools` grant asks for the browser
74
+ * bridge: the bare server name, `mcp__claude-in-chrome`, or any
75
+ * `mcp__claude-in-chrome__<tool>`. The runner turns this into `--chrome` on
76
+ * the claude argv.
77
+ */
78
+ function grantsChrome(entries) {
79
+ for (const entry of entries) for (const name of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
80
+ if (name === CHROME_SERVER) return true;
81
+ if (name === `mcp__${CHROME_SERVER}`) return true;
82
+ if (name.startsWith(`mcp__${CHROME_SERVER}__`)) return true;
83
+ }
84
+ return false;
85
+ }
86
+ /**
87
+ * Split a `--mcp a,b,c` flag value (also accepts repeated flags already split
88
+ * by the caller) and expand set names from workers.mcpSets.
89
+ */
90
+ function expandMcpNames(names, config, claudeJson = CLAUDE_JSON) {
91
+ const available = readMcpServers(claudeJson);
92
+ const out = [];
93
+ for (const raw of names) for (const name of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
94
+ if (name === CHROME_SERVER) continue;
95
+ if (name in config.mcpSets) {
96
+ for (const member of config.mcpSets[name]) if (!out.includes(member)) out.push(member);
97
+ continue;
98
+ }
99
+ if (!(name in available)) {
100
+ const sets = Object.keys(config.mcpSets);
101
+ throw new WorkersConfigError(`unknown MCP server "${name}". Available servers: ${Object.keys(available).join(", ") || "(none in ~/.claude.json)"}${sets.length ? `; sets: ${sets.join(", ")}` : ""}`);
102
+ }
103
+ if (!out.includes(name)) out.push(name);
104
+ }
105
+ return out;
106
+ }
107
+ /**
108
+ * Derive server names from tool grants: every `mcp__<server>__<tool>` (or bare
109
+ * `mcp__<server>`, or `mcp__<server>__*`) in an --allowedTools list names a
110
+ * server the run expects to be loaded. A grant is a dead letter unless its
111
+ * server is in the filtered config, so the runner treats these as implicit
112
+ * --mcp names — and only these; non-mcp grants load nothing.
113
+ */
114
+ function mcpServersFromToolGrants(tools) {
115
+ const out = [];
116
+ for (const entry of tools) for (const name of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
117
+ if (!name.startsWith("mcp__")) continue;
118
+ const server = name.slice(5).split("__")[0];
119
+ if (server === CHROME_SERVER) continue;
120
+ if (server && !out.includes(server)) out.push(server);
121
+ }
122
+ return out;
123
+ }
124
+ /** Path of a run's filtered MCP config. */
125
+ function runMcpConfigPath(logDir, id) {
126
+ return join(logDir, `${id}.mcp.json`);
127
+ }
128
+ /**
129
+ * Write a config containing only `names` (already expanded) and return its
130
+ * path. Unknown names fail fast with the available list.
131
+ */
132
+ function writeMcpConfig(logDir, id, names, claudeJson = CLAUDE_JSON) {
133
+ const available = readMcpServers(claudeJson);
134
+ const unknown = names.filter((n) => !(n in available));
135
+ if (unknown.length) throw new WorkersConfigError(`unknown MCP server(s): ${unknown.join(", ")}. Available: ${Object.keys(available).join(", ") || "(none in ~/.claude.json)"}`);
136
+ const servers = {};
137
+ for (const n of names) servers[n] = available[n];
138
+ const path = runMcpConfigPath(logDir, id);
139
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
140
+ writeFileSync(path, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "utf8");
141
+ return path;
142
+ }
143
+ /** `pai worker mcp list`: the servers and sets a run could name. */
144
+ function describeMcp(config, claudeJson = CLAUDE_JSON) {
145
+ const servers = Object.keys(readMcpServers(claudeJson));
146
+ const lines = [];
147
+ if (servers.length) {
148
+ lines.push(`servers (~/.claude.json):`);
149
+ for (const s of servers) lines.push(` ${s}`);
150
+ } else lines.push(`no MCP servers defined in ~/.claude.json`);
151
+ const sets = Object.entries(config.mcpSets);
152
+ if (sets.length) {
153
+ lines.push(`sets (workers.mcpSets):`);
154
+ for (const [name, members] of sets) lines.push(` ${name} = ${members.join(", ")}`);
155
+ }
156
+ lines.push(`usage: pai worker run --mcp <name>[,<name>…] -p '<task>'`);
157
+ return lines;
158
+ }
159
+
160
+ //#endregion
10
161
  //#region src/workers/args.ts
11
162
  function parseRunnerArgs(argv) {
12
163
  let prompt = null;
@@ -16,6 +167,7 @@ function parseRunnerArgs(argv) {
16
167
  let callerModel = false;
17
168
  let callerMcpConfig = false;
18
169
  let callerSystemPrompt = false;
170
+ let callerTools = false;
19
171
  const mcp = [];
20
172
  const allowedTools = [];
21
173
  let i = 0;
@@ -61,6 +213,8 @@ function parseRunnerArgs(argv) {
61
213
  if (a.startsWith("--mcp-config=")) callerMcpConfig = true;
62
214
  if (a === "--append-system-prompt") callerSystemPrompt = true;
63
215
  if (a.startsWith("--append-system-prompt=")) callerSystemPrompt = true;
216
+ if (a === "--tools") callerTools = true;
217
+ if (a.startsWith("--tools=")) callerTools = true;
64
218
  if (prompt === null && !a.startsWith("-") && rest.length > 0 && (rest[rest.length - 1] === "-p" || rest[rest.length - 1] === "--print")) prompt = a;
65
219
  rest.push(a);
66
220
  }
@@ -74,6 +228,7 @@ function parseRunnerArgs(argv) {
74
228
  callerModel,
75
229
  callerMcpConfig,
76
230
  callerSystemPrompt,
231
+ callerTools,
77
232
  mcp,
78
233
  allowedTools
79
234
  };
@@ -94,82 +249,23 @@ function stripPromptValues(argv) {
94
249
  }
95
250
  return out;
96
251
  }
252
+ /**
253
+ * A one-line stderr hint (never blocking) for an inline -p prompt long or
254
+ * multi-line enough to be a shell-quoting risk — write it to a file and pass
255
+ * --spec instead. null when the prompt is short/simple enough to be fine.
256
+ */
257
+ function longInlinePromptHint(prompt) {
258
+ if (!prompt) return null;
259
+ const newlines = (prompt.match(/\n/g) ?? []).length;
260
+ if (prompt.length > 600 || newlines > 3) return "hint: long inline prompts break on shell quoting — write the spec to a file and use --spec <file>";
261
+ return null;
262
+ }
97
263
  /** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */
98
264
  function shortText(s, n) {
99
265
  const t = String(s ?? "").split(/\s+/).filter(Boolean).join(" ");
100
266
  return t.length <= n ? t : t.slice(0, n - 1) + "…";
101
267
  }
102
268
 
103
- //#endregion
104
- //#region src/workers/ledger.ts
105
- /**
106
- * ledger.ts — the routing ledger, one append-only log for every worker event.
107
- *
108
- * Line shapes (whitespace-aligned so `tail` reads as a table):
109
- *
110
- * 2026-09-17 12:00:00 WORKER-START id=<id> provider=<p> mode=headless model=<m> cwd=<cwd> label=<label>
111
- * 2026-09-17 12:01:00 WORKER-END id=<id> provider=<p> mode=headless model=<m> rc=0 secs=60 turns=3 tools=8 label=<label>
112
- * 2026-09-17 12:00:20 WORKER-REROUTE from=<a> to=<b> reason=quota
113
- * 2026-09-17 12:00:00 DENIED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>
114
- * 2026-09-17 12:00:00 ALLOWED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>
115
- *
116
- * `pai worker log` (and glm-log before it) counts these; keep the tags stable.
117
- */
118
- function ledgerStamp(d = /* @__PURE__ */ new Date()) {
119
- const p = (n) => String(n).padStart(2, "0");
120
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
121
- }
122
- /** Parse `2026-09-17 12:00:00 TAG key=value …` into its parts. */
123
- function parseLedgerLine(line) {
124
- const m = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\S+)(?: (.*))?$/);
125
- if (!m) return null;
126
- const fields = {};
127
- for (const part of (m[3] ?? "").split(" ")) {
128
- if (!part) continue;
129
- const eq = part.indexOf("=");
130
- if (eq <= 0) continue;
131
- fields[part.slice(0, eq)] = part.slice(eq + 1);
132
- }
133
- return {
134
- stamp: m[1],
135
- event: m[2],
136
- fields
137
- };
138
- }
139
- /** Append one ledger line. `kv` order is the caller's; values are flattened. */
140
- function appendLedger(path, event, kv, now = /* @__PURE__ */ new Date()) {
141
- const parts = [];
142
- for (const [k, v] of Object.entries(kv)) {
143
- if (v === void 0 || v === null) continue;
144
- parts.push(`${k}=${String(v).replace(/\s+/g, " ")}`);
145
- }
146
- const dir = dirname(path);
147
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
148
- const tail = parts.length ? " " + parts.join(" ") : "";
149
- appendFileSync(path, `${ledgerStamp(now)} ${event}${tail}\n`, "utf8");
150
- }
151
- /** The counts `pai worker log` prints, over today's lines or the whole file. */
152
- function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()) {
153
- if (!existsSync(path)) return null;
154
- const text = readFileSync(path, "utf8");
155
- const today = ledgerStamp(now).slice(0, 10);
156
- const lines = text.split("\n").filter((l) => l.trim());
157
- const parsed = (scope === "all" ? lines : lines.filter((l) => l.startsWith(today))).map(parseLedgerLine).filter((x) => x !== null);
158
- const count = (ev) => parsed.filter((p) => p.event === ev).length;
159
- const ends = parsed.filter((p) => p.event === "WORKER-END");
160
- const ok = ends.filter((p) => p.fields.rc === "0").length;
161
- return {
162
- scope: scope === "all" ? "all time" : `today ${today}`,
163
- started: count("WORKER-START"),
164
- endedOk: ok,
165
- endedFailed: ends.length - ok,
166
- denied: count("DENIED-ANTHROPIC-AGENT"),
167
- allowed: count("ALLOWED-ANTHROPIC-AGENT"),
168
- reroutes: count("WORKER-REROUTE"),
169
- lastLines: lines.slice(-lastN)
170
- };
171
- }
172
-
173
269
  //#endregion
174
270
  //#region src/workers/status.ts
175
271
  /**
@@ -228,13 +324,47 @@ function newWorkerId(d = /* @__PURE__ */ new Date(), pid = process.pid) {
228
324
  idSeq = 0;
229
325
  return base;
230
326
  }
327
+ let tmpSeq = 0;
328
+ /** A temp path only this call owns — never the same string twice. */
329
+ function statusTmpPath(logDir, id) {
330
+ return `${statusPath(logDir, id)}.${process.pid}.${tmpSeq++}.tmp`;
331
+ }
332
+ const firstWrittenLabel = /* @__PURE__ */ new Map();
231
333
  /** Write status atomically (temp + rename) and stamp `updated`. */
232
334
  function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
233
335
  status.updated = nowStamp(d);
234
336
  const path = statusPath(logDir, status.id);
235
- const tmp = `${path}.tmp`;
236
- writeFileSync(tmp, JSON.stringify(status), "utf8");
237
- renameSync(tmp, path);
337
+ const baseline = firstWrittenLabel.get(status.id);
338
+ if (baseline === void 0) firstWrittenLabel.set(status.id, status.label);
339
+ else if (status.label === baseline) {
340
+ const onDisk = loadStatus(logDir, status.id);
341
+ if (onDisk && onDisk.label !== status.label) status.label = onDisk.label;
342
+ }
343
+ const tmp = statusTmpPath(logDir, status.id);
344
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
345
+ try {
346
+ writeFileSync(tmp, JSON.stringify(status), "utf8");
347
+ renameSync(tmp, path);
348
+ } catch (e) {
349
+ try {
350
+ rmSync(tmp, { force: true });
351
+ } catch {}
352
+ throw e;
353
+ }
354
+ }
355
+ /**
356
+ * Set a worker's goal (the operator's `--label`), atomically, through the
357
+ * same saveStatus path everything else writes with. Used by `pai worker
358
+ * goal` and the `--goal` option of `say`: a fresh call in a fresh process, so
359
+ * it always carries no baseline yet and always wins over the worker's own
360
+ * next periodic write (see saveStatus above).
361
+ */
362
+ function setWorkerLabel(logDir, id, label) {
363
+ const status = loadStatus(logDir, id);
364
+ if (!status) throw new Error(`no worker named "${id}"`);
365
+ status.label = label;
366
+ saveStatus(logDir, status);
367
+ return status;
238
368
  }
239
369
  /** Load every status file in the logDir, oldest id first, skipping damage. */
240
370
  function loadStatuses(logDir) {
@@ -257,6 +387,20 @@ function loadStatus(logDir, id) {
257
387
  return null;
258
388
  }
259
389
  }
390
+ /**
391
+ * Wait for a worker to record its own terminal state (the run's signal
392
+ * handler writes killed/rc/secs). Returns that status, or null when it is
393
+ * still "running" after `timeoutMs` — the caller then writes the state itself.
394
+ */
395
+ async function waitForTerminalStatus(logDir, id, timeoutMs = 2e3, stepMs = 50) {
396
+ const deadline = Date.now() + timeoutMs;
397
+ for (;;) {
398
+ const s = loadStatus(logDir, id);
399
+ if (s && s.state !== "running") return s;
400
+ if (Date.now() >= deadline) return null;
401
+ await new Promise((r) => setTimeout(r, stepMs));
402
+ }
403
+ }
260
404
  function alive(pid) {
261
405
  if (!pid || pid <= 0) return false;
262
406
  try {
@@ -286,6 +430,76 @@ function describeTool(name, inp) {
286
430
  return name;
287
431
  }
288
432
 
433
+ //#endregion
434
+ //#region src/workers/ledger.ts
435
+ /**
436
+ * ledger.ts — the routing ledger, one append-only log for every worker event.
437
+ *
438
+ * Line shapes (whitespace-aligned so `tail` reads as a table):
439
+ *
440
+ * 2026-09-17 12:00:00 WORKER-START id=<id> provider=<p> mode=headless model=<m> cwd=<cwd> label=<label>
441
+ * 2026-09-17 12:01:00 WORKER-END id=<id> provider=<p> mode=headless model=<m> rc=0 secs=60 turns=3 tools=8 label=<label>
442
+ * 2026-09-17 12:00:20 WORKER-REROUTE from=<a> to=<b> reason=quota
443
+ * 2026-09-17 12:00:00 DENIED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>
444
+ * 2026-09-17 12:00:00 ALLOWED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>
445
+ *
446
+ * `pai worker log` (and glm-log before it) counts these; keep the tags stable.
447
+ */
448
+ function ledgerStamp(d = /* @__PURE__ */ new Date()) {
449
+ const p = (n) => String(n).padStart(2, "0");
450
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
451
+ }
452
+ /** Parse `2026-09-17 12:00:00 TAG key=value …` into its parts. */
453
+ function parseLedgerLine(line) {
454
+ const m = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\S+)(?: (.*))?$/);
455
+ if (!m) return null;
456
+ const fields = {};
457
+ for (const part of (m[3] ?? "").split(" ")) {
458
+ if (!part) continue;
459
+ const eq = part.indexOf("=");
460
+ if (eq <= 0) continue;
461
+ fields[part.slice(0, eq)] = part.slice(eq + 1);
462
+ }
463
+ return {
464
+ stamp: m[1],
465
+ event: m[2],
466
+ fields
467
+ };
468
+ }
469
+ /** Append one ledger line. `kv` order is the caller's; values are flattened. */
470
+ function appendLedger(path, event, kv, now = /* @__PURE__ */ new Date()) {
471
+ const parts = [];
472
+ for (const [k, v] of Object.entries(kv)) {
473
+ if (v === void 0 || v === null) continue;
474
+ parts.push(`${k}=${String(v).replace(/\s+/g, " ")}`);
475
+ }
476
+ const dir = dirname(path);
477
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
478
+ const tail = parts.length ? " " + parts.join(" ") : "";
479
+ appendFileSync(path, `${ledgerStamp(now)} ${event}${tail}\n`, "utf8");
480
+ }
481
+ /** The counts `pai worker log` prints, over today's lines or the whole file. */
482
+ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()) {
483
+ if (!existsSync(path)) return null;
484
+ const text = readFileSync(path, "utf8");
485
+ const today = ledgerStamp(now).slice(0, 10);
486
+ const lines = text.split("\n").filter((l) => l.trim());
487
+ const parsed = (scope === "all" ? lines : lines.filter((l) => l.startsWith(today))).map(parseLedgerLine).filter((x) => x !== null);
488
+ const count = (ev) => parsed.filter((p) => p.event === ev).length;
489
+ const ends = parsed.filter((p) => p.event === "WORKER-END");
490
+ const ok = ends.filter((p) => p.fields.rc === "0").length;
491
+ return {
492
+ scope: scope === "all" ? "all time" : `today ${today}`,
493
+ started: count("WORKER-START"),
494
+ endedOk: ok,
495
+ endedFailed: ends.length - ok,
496
+ denied: count("DENIED-ANTHROPIC-AGENT"),
497
+ allowed: count("ALLOWED-ANTHROPIC-AGENT"),
498
+ reroutes: count("WORKER-REROUTE"),
499
+ lastLines: lines.slice(-lastN)
500
+ };
501
+ }
502
+
289
503
  //#endregion
290
504
  //#region src/workers/scope.ts
291
505
  /**
@@ -1611,56 +1825,333 @@ function worktreeSystemPrompt(id, branch, dir) {
1611
1825
  ].join("\n");
1612
1826
  }
1613
1827
 
1828
+ //#endregion
1829
+ //#region src/workers/agentish.ts
1830
+ /**
1831
+ * agentish.ts — Agentish v2 (AG2), the wire format a worker's final report
1832
+ * uses by default: a kind line plus one-letter k=v lines instead of JSON
1833
+ * prose. Spec and validator live in the `aibroker` CLI
1834
+ * (see /Users/i052341/Daten/Cloud/Development/ai/AIBroker/docs/agentish.md);
1835
+ * this module shells out to it when present and falls back to a frozen copy
1836
+ * of the spec text otherwise, so a worker still gets a contract when
1837
+ * `aibroker` is not installed on its box.
1838
+ */
1839
+ /** Frozen copy of `aibroker agentish spec`'s first line, for when the CLI is absent. */
1840
+ const AG2_SPEC_FALLBACK = "AG2. msg=kind line+k=v lines. kinds T R S Q A X. keys i id g goal o own n forbid d steps p proof u out l limits r res c changes t tests G gate I inst m images # nums w worst x next z note(<200ch). sep |. outcomes + - ~ ? !. @n=path declared once then reused; @n:12=file:line. tests as Name+ Name-. no prose, no articles, never restate, unknown=?. r=+ only if all t +.";
1841
+ /** Frozen copy of the `y` (why) extension line. */
1842
+ const AG2_EXTENSIONS_FALLBACK = "y why(≤600ch)";
1843
+ let cachedSpec = null;
1844
+ /**
1845
+ * The AG2 spec + extensions lines: from `aibroker agentish spec` when the
1846
+ * CLI runs and its output looks right, else the frozen fallback. Memoised —
1847
+ * every caller in one process gets the same spec without re-spawning aibroker.
1848
+ */
1849
+ function ag2Spec() {
1850
+ if (cachedSpec) return cachedSpec;
1851
+ try {
1852
+ const lines = execFileSync("aibroker", ["agentish", "spec"], {
1853
+ timeout: 3e3,
1854
+ stdio: [
1855
+ "ignore",
1856
+ "pipe",
1857
+ "ignore"
1858
+ ],
1859
+ encoding: "utf8"
1860
+ }).split("\n").filter((l) => l.trim());
1861
+ if (lines[0]?.startsWith("AG2.")) {
1862
+ cachedSpec = {
1863
+ spec: lines[0],
1864
+ extensions: lines[1] ?? AG2_EXTENSIONS_FALLBACK,
1865
+ source: "aibroker"
1866
+ };
1867
+ return cachedSpec;
1868
+ }
1869
+ } catch {}
1870
+ cachedSpec = {
1871
+ spec: AG2_SPEC_FALLBACK,
1872
+ extensions: AG2_EXTENSIONS_FALLBACK,
1873
+ source: "builtin"
1874
+ };
1875
+ return cachedSpec;
1876
+ }
1877
+ /**
1878
+ * Parse one AG2 message: first non-empty line is the kind (T R S Q A X,
1879
+ * optionally followed by trailing text — only the first character is the
1880
+ * kind), later lines are `key=value` or `@n=path` symbol declarations.
1881
+ * ```-fenced lines are ignored so a message pasted inside a code fence still
1882
+ * parses. Returns null when the first non-empty line is not a known kind.
1883
+ */
1884
+ function parseAg2Report(text) {
1885
+ const kinds = new Set([
1886
+ "T",
1887
+ "R",
1888
+ "S",
1889
+ "Q",
1890
+ "A",
1891
+ "X"
1892
+ ]);
1893
+ const lines = (text ?? "").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("```"));
1894
+ if (!lines.length) return null;
1895
+ const kind = lines[0][0];
1896
+ if (!kinds.has(kind)) return null;
1897
+ const fields = {};
1898
+ const symbols = {};
1899
+ for (const line of lines.slice(1)) {
1900
+ const eq = line.indexOf("=");
1901
+ if (eq < 0) continue;
1902
+ const key = line.slice(0, eq).trim();
1903
+ const value = line.slice(eq + 1).trim();
1904
+ if (key.startsWith("@")) symbols[key.slice(1)] = value;
1905
+ else fields[key] = value;
1906
+ }
1907
+ return {
1908
+ kind,
1909
+ fields,
1910
+ symbols
1911
+ };
1912
+ }
1913
+ /** true iff the value ends with the AG2 pass outcome character. */
1914
+ function passed(entry) {
1915
+ return entry.endsWith("+");
1916
+ }
1917
+ /**
1918
+ * Map a parsed AG2 report onto the runner's WorkerReport shape, so the
1919
+ * viewer and `printResult` can render an AG2 `R` exactly like a JSON one.
1920
+ */
1921
+ function ag2ToWorkerReport(parsed) {
1922
+ const f = parsed.fields;
1923
+ const out = { format: "ag2" };
1924
+ if (f.c) out.changed = f.c.split("|").map((s) => s.trim()).filter(Boolean).map((entry) => {
1925
+ const colon = entry.indexOf(":");
1926
+ const space = entry.indexOf(" ");
1927
+ const cut = colon >= 0 && (space < 0 || colon < space) ? colon : space;
1928
+ if (cut < 0) return {
1929
+ path: entry,
1930
+ summary: ""
1931
+ };
1932
+ return {
1933
+ path: entry.slice(0, cut).trim(),
1934
+ summary: entry.slice(cut + 1).trim()
1935
+ };
1936
+ });
1937
+ if (f.t) out.checks = f.t.split(/\s+/).filter(Boolean).map((entry) => ({
1938
+ name: entry.slice(0, -1),
1939
+ ok: passed(entry),
1940
+ detail: entry.slice(-1)
1941
+ }));
1942
+ if (f.p) out.commands = f.p.split("|").map((s) => s.trim()).filter(Boolean);
1943
+ const open = [];
1944
+ if (f.x) open.push(f.x);
1945
+ for (const [k, v] of Object.entries(f)) if ((v.endsWith("!") || v.endsWith("?")) && k !== "r" && k !== "res") open.push(`${k}: ${v}`);
1946
+ if (open.length) out.open = open;
1947
+ if (f.z) out.notes = f.z;
1948
+ const res = f.r ?? f.res;
1949
+ if (res === "+" || res === "-" || res === "~" || res === "?" || res === "!") out.result = res;
1950
+ if (f.y) out.why = f.y;
1951
+ return out;
1952
+ }
1953
+ /**
1954
+ * Validate an AG2 message with `aibroker agentish check - --json`. Never
1955
+ * fails the caller: a missing/broken CLI reports ok:true, validator "none" —
1956
+ * a worker's run is never blocked on a local tooling gap, only its
1957
+ * `reportValid` marker goes unset.
1958
+ */
1959
+ function validateAg2(text) {
1960
+ try {
1961
+ const out = execFileSync("aibroker", [
1962
+ "agentish",
1963
+ "check",
1964
+ "-",
1965
+ "--json"
1966
+ ], {
1967
+ input: text,
1968
+ timeout: 5e3,
1969
+ stdio: [
1970
+ "pipe",
1971
+ "pipe",
1972
+ "ignore"
1973
+ ],
1974
+ encoding: "utf8"
1975
+ });
1976
+ const parsed = JSON.parse(out);
1977
+ const errors = (parsed.errors ?? []).map((e) => e.message ?? e.code ?? "unknown error");
1978
+ return {
1979
+ ok: parsed.ok !== false,
1980
+ errors,
1981
+ validator: "aibroker"
1982
+ };
1983
+ } catch (e) {
1984
+ const out = e.stdout;
1985
+ if (out) try {
1986
+ const parsed = JSON.parse(out.toString());
1987
+ const errors = (parsed.errors ?? []).map((er) => er.message ?? er.code ?? "unknown error");
1988
+ return {
1989
+ ok: parsed.ok !== false,
1990
+ errors,
1991
+ validator: "aibroker"
1992
+ };
1993
+ } catch {}
1994
+ return {
1995
+ ok: true,
1996
+ errors: [],
1997
+ validator: "none"
1998
+ };
1999
+ }
2000
+ }
2001
+
1614
2002
  //#endregion
1615
2003
  //#region src/workers/report.ts
1616
2004
  /**
1617
2005
  * report.ts — the worker contract: terse, structured final reports.
1618
2006
  *
1619
2007
  * Headless workers run with an appended system prompt that fixes their output
1620
- * shape: act, verify, then finish with ONE final message that is a JSON object
1621
- * and nothing else. The runner parses it out of the result; the viewer renders
1622
- * it as a compact block instead of a wall of text.
2008
+ * shape: act, verify, then finish with ONE final message — AG2 (see
2009
+ * agentish.ts) by default, JSON as the pre-AG2 fallback. The runner parses it
2010
+ * out of the result; the viewer renders it as a compact block instead of a
2011
+ * wall of text.
1623
2012
  */
1624
2013
  /** The marker the runner puts on operator messages the worker must answer. */
1625
2014
  const OPERATOR_MARK = "[operator]";
1626
- /** Appended to the caller's system prompt on every headless run (2h). */
1627
- const WORKER_CONTRACT_PROMPT = [
1628
- "You are a headless implementation worker, run non-interactively by an orchestrating session.",
1629
- "No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
1630
- "Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:",
1631
- "{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}",
1632
- "changed: files you touched (path + one-line summary). commands: the commands that verify the work.",
1633
- "checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.",
1634
- "notes: one line, the headline a reviewer reads first.",
1635
- "Never write shell heredocs — write the script to a file with the Write tool, then run it",
1636
- "(heredocs fail silently or get swallowed).",
1637
- "Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
1638
- "denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
1639
- "Write tool and run it (python3 script.py), or use jq for JSON inspection.",
1640
- "",
1641
- `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
1642
- "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
1643
- "a question gets its answer, an instruction gets one line naming what will change — then continue the",
1644
- "task you were on.",
1645
- "",
1646
- "Sub-workers: you may start your own workers with `pai worker run --class <class> -p '<prompt>'`",
1647
- "(Bash tool). Your children run on their own provider and report back to you automatically when",
1648
- "they finish. Send a handoff UP instead of doing work yourself when a cheaper provider would",
1649
- "suffice, the task is out of your scope, or a decision is needed:",
1650
- "`pai worker handoff '{\"kind\":\"proposal\",\"text\":\"…\",\"data\":{…}}'` (kinds: proposal,",
1651
- "question, blocker; results are sent for you when you finish). Sibling workers are not a",
1652
- "coordination path — there is no sideways channel; everything goes up to your parent."
1653
- ].join("\n");
1654
- /**
1655
- * Extract the JSON report from a worker's final message. Accepts a bare
1656
- * object, a ```json fenced block, or an object embedded in surrounding text.
1657
- * Only objects that look like the contract count (at least one of changed /
2015
+ /** Classes that get the short contract — cheap, bounded, single-answer tasks. */
2016
+ const SHORT_CONTRACT_CLASSES = new Set(["spotcheck", "simple"]);
2017
+ /**
2018
+ * The AG2 report block appended to a contract when format is "ag2" — field
2019
+ * set matches `aibroker agentish check`'s real R schema (verified 2026-09-20:
2020
+ * i/r/t/c are required, not optional as an early draft of this contract had
2021
+ * it; r=+ needs G=+ and non-empty p too, not just all-t-passing; R has no
2022
+ * `u`/out field at all — u is T-only — so the answer goes in z instead).
2023
+ */
2024
+ function ag2ReportBlock() {
2025
+ const spec = ag2Spec();
2026
+ return [
2027
+ spec.spec,
2028
+ spec.extensions,
2029
+ "Final message: one AG2 `R` message only — no prose, no code fence, no markdown.",
2030
+ "R requires i (id), r (+ - ~ ? !), t (tests, Name+ Name-), c (path summary|path summary).",
2031
+ "r=+ requires G=+ (gate) and p non-empty (proof command), and every t entry +. z= the answer/note,",
2032
+ "one line, always present. x= what is left (omit if nothing). y= one line why when r is not +.",
2033
+ "Example:",
2034
+ "R",
2035
+ "i=count-audit-ts",
2036
+ "r=+",
2037
+ "G=+",
2038
+ "c=src/audit summary of files counted",
2039
+ "t=Count+",
2040
+ "p=wc -l src/audit/*.ts",
2041
+ "z=19 files, 2523 lines"
2042
+ ];
2043
+ }
2044
+ /** The JSON report block appended to a contract when format is "json" (pre-AG2 fallback). */
2045
+ function jsonReportBlock(lead) {
2046
+ return [lead, "{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}"];
2047
+ }
2048
+ /** The two sentences that stop a worker from breaking on shell quoting — kept in both contracts. */
2049
+ function fileNotInlineBlock() {
2050
+ return [
2051
+ "NEVER put a multi-line or quoted payload inline in a shell command — no heredocs (<<EOF), no long",
2052
+ "-p '…' specs for child workers, no inline JSON, no inline scripts. ALWAYS write it to a file with",
2053
+ "the Write tool first and pass the file: -p \"$(cat spec.txt)\", --file spec.txt, or < spec.txt.",
2054
+ "Inline quoting fails silently or dies with a parse error and burns a worker launch; there are no",
2055
+ "exceptions.",
2056
+ "Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
2057
+ "denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
2058
+ "Write tool and run it (python3 script.py), or use jq for JSON inspection."
2059
+ ];
2060
+ }
2061
+ /** The file-not-inline rule, condensed for the short contract's tight length budget. */
2062
+ function fileNotInlineBlockShort() {
2063
+ return [
2064
+ "NEVER put a multi-line or quoted payload inline in a shell command (no heredocs, no inline",
2065
+ "scripts): write it to a file with the Write tool and pass it, e.g. -p \"$(cat spec.txt)\".",
2066
+ "Never inline code (python3 -c and friends) — write a script file and run it, or use jq."
2067
+ ];
2068
+ }
2069
+ /** spotcheck/simple: headless, bounded, no delegation language — under 1500 chars in ag2 format. */
2070
+ function shortContractPrompt(format) {
2071
+ const lines = [
2072
+ "You are a headless implementation worker, run non-interactively.",
2073
+ "This task is yours: answer the question or do the bounded task now. Run at most one command",
2074
+ "to confirm a result and never re-derive a number you already have. No child workers, no handoff.",
2075
+ ...fileNotInlineBlockShort()
2076
+ ];
2077
+ lines.push(...format === "ag2" ? ag2ReportBlock() : jsonReportBlock("Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:"));
2078
+ return lines.join("\n");
2079
+ }
2080
+ /** Every other class (and no class): the full worker/orchestrator contract. */
2081
+ function fullContractPrompt(format) {
2082
+ const lines = [
2083
+ "You are a headless implementation worker, run non-interactively by an orchestrating session.",
2084
+ "You are the worker: this task is yours to finish in this process. Never hand your whole task to a",
2085
+ "single child worker and step back — that is delegation, not work. Child workers are for two things",
2086
+ "only: (1) PARALLELISING independent parts of your task, each part a self-contained spec; (2) CHEAPER",
2087
+ "bounded sub-tasks — probes, renders, test runs, lookups — run one tier down with --class spotcheck or",
2088
+ "--class simple. Always pin --provider anthropic on children.",
2089
+ "You own your children's results: run them in the foreground or poll until each has finished, read",
2090
+ "their result, verify it, and fold it into your own report. When your turn ends this run ends and any",
2091
+ "running child is killed, so never end with a child still running.",
2092
+ "Ending your turn ends this run. There is no later wake-up: never call ScheduleWakeup, never say you",
2093
+ "will pick something up later, never leave a background command as your final action. Finish, then",
2094
+ "report.",
2095
+ "No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
2096
+ ...fileNotInlineBlock(),
2097
+ "",
2098
+ `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
2099
+ "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
2100
+ "a question gets its answer, an instruction gets one line naming what will change — then continue the",
2101
+ "task you were on.",
2102
+ "",
2103
+ "Sub-workers: you may start your own workers with `pai worker run --class <class> -p '<prompt>'`",
2104
+ "(Bash tool). Your children run on their own provider and report back to you automatically when",
2105
+ "they finish. Send a handoff UP instead of doing work yourself when a cheaper provider would",
2106
+ "suffice, the task is out of your scope, or a decision is needed:",
2107
+ "`pai worker handoff '{\"kind\":\"proposal\",\"text\":\"…\",\"data\":{…}}'` (kinds: proposal,",
2108
+ "question, blocker; results are sent for you when you finish). Sibling workers are not a",
2109
+ "coordination path — there is no sideways channel; everything goes up to your parent.",
2110
+ ""
2111
+ ];
2112
+ lines.push(...format === "ag2" ? ag2ReportBlock() : jsonReportBlock("Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:"));
2113
+ if (format === "json") lines.push("changed: files you touched (path + one-line summary). commands: the commands that verify the work.", "checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.", "notes: one line, the headline a reviewer reads first.");
2114
+ return lines.join("\n");
2115
+ }
2116
+ /**
2117
+ * The system prompt appended to every headless run, chosen by task class and
2118
+ * report format. `spotcheck`/`simple` get a short contract (no delegation
2119
+ * language, under 1200 chars in json format / 1500 in ag2); every other class (and no class) gets the
2120
+ * full contract. Both end with the AG2 report block unless `format` is
2121
+ * "json", the pre-AG2 fallback.
2122
+ */
2123
+ function workerContractPrompt(cls, format) {
2124
+ return SHORT_CONTRACT_CLASSES.has(cls ?? "") ? shortContractPrompt(format) : fullContractPrompt(format);
2125
+ }
2126
+ /**
2127
+ * The line appended to a headless prompt's own text (never the system
2128
+ * prompt): models weight an instruction at the end of the user turn more
2129
+ * than one buried in a long system-prompt paragraph, and this is the
2130
+ * difference between a worker that returns a valid AG2/JSON report and one
2131
+ * that answers in prose despite the contract (measured 2026-09-20 on both
2132
+ * Haiku and Sonnet with the AG2 contract alone).
2133
+ */
2134
+ function promptTrailer(format) {
2135
+ return format === "ag2" ? "\n\nFinal message: one AG2 R message only, no prose (format in system prompt)." : "\n\nFinal message: a JSON object only, no prose before or after, no code fence (format in system prompt).";
2136
+ }
2137
+ /** The exact text sent to resend a final message that failed AG2 validation. */
2138
+ const AG2_REASK_TEXT = "Your final message was not a valid AG2 R message. Resend it now as a single AG2 R message only: first line R, then i=id r=result G=+ (gate, if r=+) t=tests(Name+) c=path summary p=proof z=the answer/note. No prose.";
2139
+ /** The full-contract AG2 prompt — the keepalive path's identical-construction baseline. */
2140
+ const WORKER_CONTRACT_PROMPT = fullContractPrompt("ag2");
2141
+ /**
2142
+ * Extract the report from a worker's final message: AG2 first (an `R`
2143
+ * message, see agentish.ts), then the JSON contract as a fallback for
2144
+ * workers still on the pre-AG2 shape. Accepts a bare JSON object, a
2145
+ * ```json fenced block, or an object embedded in surrounding text — only
2146
+ * objects that look like the contract count (at least one of changed /
1658
2147
  * checks / notes) — any other JSON falls through to null so the raw text is
1659
2148
  * kept as-is.
1660
2149
  */
1661
2150
  function parseWorkerReport(text) {
1662
2151
  const trimmed = (text ?? "").trim();
1663
2152
  if (!trimmed) return null;
2153
+ const ag2 = parseAg2Report(trimmed);
2154
+ if (ag2 && ag2.kind === "R") return ag2ToWorkerReport(ag2);
1664
2155
  const candidates = [];
1665
2156
  const fence = trimmed.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
1666
2157
  if (fence) candidates.push(fence[1]);
@@ -1670,7 +2161,10 @@ function parseWorkerReport(text) {
1670
2161
  if (first >= 0 && last > first) candidates.push(trimmed.slice(first, last + 1));
1671
2162
  for (const cand of candidates) try {
1672
2163
  const v = JSON.parse(cand);
1673
- if (typeof v === "object" && v !== null && !Array.isArray(v) && looksLikeReport(v)) return v;
2164
+ if (typeof v === "object" && v !== null && !Array.isArray(v) && looksLikeReport(v)) return {
2165
+ ...v,
2166
+ format: "json"
2167
+ };
1674
2168
  } catch {}
1675
2169
  return null;
1676
2170
  }
@@ -1717,104 +2211,56 @@ function relShort(p, cwd) {
1717
2211
  }
1718
2212
 
1719
2213
  //#endregion
1720
- //#region src/workers/mcp.ts
2214
+ //#region src/workers/project-config.ts
1721
2215
  /**
1722
- * mcp.ts — the MCP allowlist for headless workers.
2216
+ * project-config.ts — the project registry's optional launch pin, read for
2217
+ * the interactive branch of run.ts only: which MCP servers and built-in
2218
+ * tools a project's supervisor session (`pai worker run`, no -p) loads. Set
2219
+ * with `pai project mcp` / `pai project tools` (src/cli/commands/project/
2220
+ * session-config.ts), stored in the same `projects.session_config` JSON
2221
+ * column as the rest of a project's launch config. Unset = today's
2222
+ * behavior: every server, every tool.
1723
2223
  *
1724
- * Headless workers start with NO MCP servers: every server definition is a
1725
- * prompt-time tool inventory the model pays to know, and a worker that only
1726
- * reads and edits files needs none of it. A run may opt in with `--mcp
1727
- * name[,name…]`, a role carrying `"mcp": [...]`, or implicitly by naming
1728
- * `mcp__server__tool` in --allowedTools (a grant without its server loaded is
1729
- * a dead letter). Names may be single servers from ~/.claude.json's
1730
- * `mcpServers` or `workers.mcpSets` set names, which expand to their member
1731
- * list. The filtered config lands in
1732
- * `<logDir>/<id>.mcp.json` and is passed with `--strict-mcp-config
1733
- * --mcp-config` so exactly those servers load. MCP servers are chosen at
1734
- * launch only — a mid-run `say` cannot add any.
2224
+ * A standalone SQL lookup rather than importing the CLI's detectProject:
2225
+ * workers/ is a dependency of cli/, not the other way around.
1735
2226
  */
1736
- /** ~/.claude.json — the user's MCP server definitions (top-level mcpServers). */
1737
- const CLAUDE_JSON = join(homedir(), ".claude.json");
1738
- function readMcpServers(claudeJson = CLAUDE_JSON) {
1739
- try {
1740
- if (!existsSync(claudeJson)) return {};
1741
- const servers = JSON.parse(readFileSync(claudeJson, "utf8")).mcpServers;
1742
- if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return {};
1743
- return servers;
1744
- } catch {
1745
- return {};
1746
- }
2227
+ function asStringArray(v) {
2228
+ if (!Array.isArray(v)) return void 0;
2229
+ const out = v.filter((x) => typeof x === "string");
2230
+ return out.length ? out : void 0;
1747
2231
  }
1748
2232
  /**
1749
- * Split a `--mcp a,b,c` flag value (also accepts repeated flags already split
1750
- * by the caller) and expand set names from workers.mcpSets.
2233
+ * The launch config of the project whose root_path is `cwd` or an ancestor
2234
+ * of it (longest match wins) — null when no registered project covers it,
2235
+ * or when the matching project has no mcp/tools pin set. `dbPath` is
2236
+ * injectable for tests; production callers take the default.
1751
2237
  */
1752
- function expandMcpNames(names, config, claudeJson = CLAUDE_JSON) {
1753
- const available = readMcpServers(claudeJson);
1754
- const out = [];
1755
- for (const raw of names) for (const name of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
1756
- if (name in config.mcpSets) {
1757
- for (const member of config.mcpSets[name]) if (!out.includes(member)) out.push(member);
1758
- continue;
1759
- }
1760
- if (!(name in available)) {
1761
- const sets = Object.keys(config.mcpSets);
1762
- throw new WorkersConfigError(`unknown MCP server "${name}". Available servers: ${Object.keys(available).join(", ") || "(none in ~/.claude.json)"}${sets.length ? `; sets: ${sets.join(", ")}` : ""}`);
2238
+ function projectLaunchConfig(cwd, dbPath = registryDbPath()) {
2239
+ const target = resolve(cwd);
2240
+ const db = openRegistry(dbPath);
2241
+ try {
2242
+ const rows = db.prepare(`SELECT root_path, session_config FROM projects WHERE status != 'archived' ORDER BY LENGTH(root_path) DESC`).all();
2243
+ for (const row of rows) {
2244
+ const root = resolve(row.root_path);
2245
+ if (target !== root && !target.startsWith(root + "/")) continue;
2246
+ if (!row.session_config) return null;
2247
+ let parsed;
2248
+ try {
2249
+ parsed = JSON.parse(row.session_config);
2250
+ } catch {
2251
+ return null;
2252
+ }
2253
+ const mcp = asStringArray(parsed.mcp);
2254
+ const tools = asStringArray(parsed.tools);
2255
+ return mcp || tools ? {
2256
+ mcp,
2257
+ tools
2258
+ } : null;
1763
2259
  }
1764
- if (!out.includes(name)) out.push(name);
1765
- }
1766
- return out;
1767
- }
1768
- /**
1769
- * Derive server names from tool grants: every `mcp__<server>__<tool>` (or bare
1770
- * `mcp__<server>`, or `mcp__<server>__*`) in an --allowedTools list names a
1771
- * server the run expects to be loaded. A grant is a dead letter unless its
1772
- * server is in the filtered config, so the runner treats these as implicit
1773
- * --mcp names — and only these; non-mcp grants load nothing.
1774
- */
1775
- function mcpServersFromToolGrants(tools) {
1776
- const out = [];
1777
- for (const entry of tools) for (const name of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
1778
- if (!name.startsWith("mcp__")) continue;
1779
- const server = name.slice(5).split("__")[0];
1780
- if (server && !out.includes(server)) out.push(server);
1781
- }
1782
- return out;
1783
- }
1784
- /** Path of a run's filtered MCP config. */
1785
- function runMcpConfigPath(logDir, id) {
1786
- return join(logDir, `${id}.mcp.json`);
1787
- }
1788
- /**
1789
- * Write a config containing only `names` (already expanded) and return its
1790
- * path. Unknown names fail fast with the available list.
1791
- */
1792
- function writeMcpConfig(logDir, id, names, claudeJson = CLAUDE_JSON) {
1793
- const available = readMcpServers(claudeJson);
1794
- const unknown = names.filter((n) => !(n in available));
1795
- if (unknown.length) throw new WorkersConfigError(`unknown MCP server(s): ${unknown.join(", ")}. Available: ${Object.keys(available).join(", ") || "(none in ~/.claude.json)"}`);
1796
- const servers = {};
1797
- for (const n of names) servers[n] = available[n];
1798
- const path = runMcpConfigPath(logDir, id);
1799
- if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
1800
- writeFileSync(path, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "utf8");
1801
- return path;
1802
- }
1803
- /** `pai worker mcp list`: the servers and sets a run could name. */
1804
- function describeMcp(config, claudeJson = CLAUDE_JSON) {
1805
- const servers = Object.keys(readMcpServers(claudeJson));
1806
- const lines = [];
1807
- if (servers.length) {
1808
- lines.push(`servers (~/.claude.json):`);
1809
- for (const s of servers) lines.push(` ${s}`);
1810
- } else lines.push(`no MCP servers defined in ~/.claude.json`);
1811
- const sets = Object.entries(config.mcpSets);
1812
- if (sets.length) {
1813
- lines.push(`sets (workers.mcpSets):`);
1814
- for (const [name, members] of sets) lines.push(` ${name} = ${members.join(", ")}`);
2260
+ return null;
2261
+ } finally {
2262
+ db.close();
1815
2263
  }
1816
- lines.push(`usage: pai worker run --mcp <name>[,<name>…] -p '<task>'`);
1817
- return lines;
1818
2264
  }
1819
2265
 
1820
2266
  //#endregion
@@ -1855,7 +2301,7 @@ function buildCodexEnv(provider) {
1855
2301
  delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
1856
2302
  delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
1857
2303
  const keyPath = providerKeyPath(provider);
1858
- if (keyPath && existsSync(keyPath)) env.OPENAI_API_KEY = readFileSync(keyPath, "utf8").trim();
2304
+ if (provider.key || keyPath && existsSync(keyPath)) env.OPENAI_API_KEY = resolveProviderKey(provider) ?? "";
1859
2305
  if (provider.upstreamUrl) env.OPENAI_BASE_URL = provider.upstreamUrl;
1860
2306
  for (const [k, v] of Object.entries(provider.env)) env[k] = v;
1861
2307
  env.PAI_WORKER = "1";
@@ -2014,11 +2460,24 @@ function codexInstalled() {
2014
2460
  * (context meter included), ledger lines, result printed in the caller's
2015
2461
  * --output-format (json adds the parsed `report`), and the follow pane
2016
2462
  * (unless --no-pane).
2017
- * - interactive: no MCP restriction, no pane, ENABLE_TOOL_SEARCH=true.
2463
+ * - interactive: no MCP/tool restriction by default, no pane,
2464
+ * ENABLE_TOOL_SEARCH=true. A project that pinned a list with `pai
2465
+ * project mcp` / `pai project tools` gets `--strict-mcp-config
2466
+ * --mcp-config <filtered>` / `--tools <list>` built the same way a
2467
+ * headless run's allowlist is; an explicit --mcp or --tools on this
2468
+ * command line still wins over the project's pin.
2018
2469
  *
2019
2470
  * Auto-routed runs that die of a quota error before the first tool call are
2020
2471
  * restarted on the next provider in routing order (WORKER-REROUTE ledger line).
2021
2472
  */
2473
+ /**
2474
+ * ag2 unless the caller passed --report json or PAI_WORKER_REPORT=json is
2475
+ * set in the environment; an explicit flag always wins over the env default.
2476
+ */
2477
+ function resolveReportFormat(flag, env = process.env) {
2478
+ if (flag) return flag;
2479
+ return env.PAI_WORKER_REPORT === "json" ? "json" : "ag2";
2480
+ }
2022
2481
  /** A stream-json user message for the child's stdin. */
2023
2482
  function stdinUserMessage(text) {
2024
2483
  return JSON.stringify({
@@ -2077,6 +2536,81 @@ function resetContextTokensOnCompact(status, e) {
2077
2536
  function isCompactBoundary(e) {
2078
2537
  return e.type === "system" && (e.subtype === "compact_boundary" || e.subtype === "compact");
2079
2538
  }
2539
+ /**
2540
+ * The model the init event announces, adopted into the status when the spawn
2541
+ * could not name one (a caller that passed `--model` itself in the claude
2542
+ * args, or a provider whose table has no entry for the capability). The run's
2543
+ * real model is then only knowable from the first event it sends. Never
2544
+ * overwrites an explicit model — a resolved or pinned model stays the
2545
+ * recorded truth.
2546
+ */
2547
+ function adoptInitModel(status, e) {
2548
+ if (status.model) return;
2549
+ const m = (e.model ?? "").trim();
2550
+ if (m) status.model = m;
2551
+ }
2552
+ /**
2553
+ * The model a run goes out on: an explicit `--model` wins; else the class
2554
+ * target's alias ("glm/fast") names the capability; else the capability the
2555
+ * class implies (image → image model, spotcheck/simple → fast, everything
2556
+ * else → default), resolved against the provider's model table. The built-in
2557
+ * anthropic provider resolves through the same table as any other, so a
2558
+ * worker never inherits the orchestrator session's model.
2559
+ */
2560
+ function resolveRunModel(target, className, modelFlag) {
2561
+ if (modelFlag) return modelFlag;
2562
+ const alias = target.modelAlias;
2563
+ const capability = alias && isModelCapability(alias) ? alias : classModelCapability(className);
2564
+ return resolveModelCapability(target.provider, capability);
2565
+ }
2566
+ /**
2567
+ * The `--model` part of the claude argv. Skipped when the caller already
2568
+ * pinned one in the claude args (it must not be clobbered) or when nothing
2569
+ * resolved — `--model ""` would break the spawn.
2570
+ */
2571
+ function modelArgs(model, callerPinned) {
2572
+ return !callerPinned && model ? ["--model", model] : [];
2573
+ }
2574
+ /**
2575
+ * Whether the run's assembled cmd gets a forced --model. Headless workers
2576
+ * always take the resolved class/provider model — that table exists so
2577
+ * spawned subagents stay cheap. An interactive launch (`pai worker run` with
2578
+ * no -p) IS the chat pane, not a subagent: with no --model of the caller's
2579
+ * own it gets none here either, so Claude Code's own settings.json model
2580
+ * applies, exactly as launching `claude` by hand would (2026-09-20: an
2581
+ * interactive supervisor came up on the class model instead of the
2582
+ * settings.json [1m] variant, then needed a manual /model switch that
2583
+ * rebuilt the whole cache). A caller's own --model, headless or not, is
2584
+ * already in the passthrough args and must not be duplicated here.
2585
+ */
2586
+ function modelFlagArgs(headless, model, callerPinned) {
2587
+ return headless ? modelArgs(model, callerPinned) : [];
2588
+ }
2589
+ /**
2590
+ * The prompt text actually sent to the worker and whether the trailer got
2591
+ * appended. An instruction at the END of the user turn outweighs one buried
2592
+ * in the system prompt (measured 2026-09-20: the AG2 contract alone was
2593
+ * ignored by both Haiku and Sonnet). Never applied to an interactive launch
2594
+ * (no -p) or when the caller brought its own --append-system-prompt — the
2595
+ * trailer's "format in system prompt" would then point at nothing.
2596
+ */
2597
+ function headlessPromptText(prompt, headless, callerSystemPrompt, format) {
2598
+ const applied = headless && prompt !== null && !callerSystemPrompt;
2599
+ return {
2600
+ text: applied ? prompt + promptTrailer(format) : prompt,
2601
+ applied
2602
+ };
2603
+ }
2604
+ /**
2605
+ * Whether an invalid final report earns the one bounded re-ask: only when a
2606
+ * real validator caught the problem (a missing/broken aibroker must never
2607
+ * block the run further), the caller did not opt out, and there is a session
2608
+ * to resume into. Cap of exactly one is enforced by the caller never calling
2609
+ * this twice for the same run.
2610
+ */
2611
+ function shouldRetryReport(validation, noReportRetry, hasSession) {
2612
+ return !validation.ok && validation.validator === "aibroker" && !noReportRetry && hasSession;
2613
+ }
2080
2614
  /** Context window announced by the init event, when the endpoint sends one. */
2081
2615
  function initContextWindow(e) {
2082
2616
  if (typeof e.context_window === "number" && e.context_window > 0) return e.context_window;
@@ -2094,7 +2628,7 @@ async function runWorker(opts) {
2094
2628
  const logDir = workersLogDir(config);
2095
2629
  mkdirSync(logDir, { recursive: true });
2096
2630
  if (opts.className === "plan" && !opts._planner) {
2097
- const { runPlanner } = await import("./planner-D5Ui7DAm.mjs");
2631
+ const { runPlanner } = await import("./planner-BlKGgpWI.mjs");
2098
2632
  return runPlanner(opts);
2099
2633
  }
2100
2634
  const parent = launchParent(opts.parent);
@@ -2106,9 +2640,8 @@ async function runWorker(opts) {
2106
2640
  assertProviderRunnable(target.providerName, target.provider);
2107
2641
  const parsed = parseRunnerArgs(opts.claudeArgs);
2108
2642
  const label = opts.label ?? shortText(parsed.prompt ?? UNLABELED, 70);
2109
- const alias = target.modelAlias;
2110
- const capability = alias && isModelCapability(alias) ? alias : classModelCapability(opts.className);
2111
- const model = opts.modelFlag ?? resolveModelCapability(target.provider, capability);
2643
+ const model = resolveRunModel(target, opts.className, opts.modelFlag);
2644
+ const reportFormat = resolveReportFormat(opts.reportFormatFlag);
2112
2645
  try {
2113
2646
  if (target.provider.engine === "codex") return await executeCodexRun({
2114
2647
  config,
@@ -2120,13 +2653,15 @@ async function runWorker(opts) {
2120
2653
  claudeArgs: opts.claudeArgs,
2121
2654
  noPane: opts.noPane ?? false,
2122
2655
  cwd: opts.cwd,
2656
+ specPath: opts.specPath,
2123
2657
  parent: parent ?? void 0,
2124
2658
  stage: opts.stage,
2125
2659
  quiet: opts.quiet,
2126
2660
  onWorkerStart: opts.onWorkerStart,
2127
2661
  id: opts.id,
2128
2662
  worktreeFlag: opts.worktreeFlag,
2129
- className: opts.className
2663
+ className: opts.className,
2664
+ reportFormat
2130
2665
  });
2131
2666
  return await executeRun({
2132
2667
  config,
@@ -2139,6 +2674,7 @@ async function runWorker(opts) {
2139
2674
  noPane: opts.noPane ?? false,
2140
2675
  mcpFlag: opts.mcpFlag,
2141
2676
  cwd: opts.cwd,
2677
+ specPath: opts.specPath,
2142
2678
  parent: parent ?? void 0,
2143
2679
  stage: opts.stage,
2144
2680
  quiet: opts.quiet,
@@ -2146,7 +2682,10 @@ async function runWorker(opts) {
2146
2682
  id: opts.id,
2147
2683
  worktreeFlag: opts.worktreeFlag,
2148
2684
  className: opts.className,
2149
- reroutes: opts._reroutes ?? 0
2685
+ reroutes: opts._reroutes ?? 0,
2686
+ printCmd: opts.printCmd,
2687
+ reportFormat,
2688
+ noReportRetry: opts.noReportRetry ?? false
2150
2689
  });
2151
2690
  } catch (e) {
2152
2691
  if (e instanceof Error && e.message.startsWith("key file")) throw new Error(`provider "${target.providerName}": ${e.message}\nPut the token in that file (chmod 600) or point keyFile elsewhere.`);
@@ -2166,6 +2705,92 @@ const DEFAULT_WORKER_TOOLS = "Read,Edit,Write,Bash,Grep,Glob";
2166
2705
  function headlessToolGrants(allowedTools) {
2167
2706
  return allowedTools.length ? [] : ["--allowedTools", DEFAULT_WORKER_TOOLS];
2168
2707
  }
2708
+ /**
2709
+ * `--tools` narrows which built-in tool *schemas* claude loads into the
2710
+ * system prompt — distinct from `--allowedTools`, which narrows which of
2711
+ * those loaded tools may run without asking. A headless worker with a small
2712
+ * grant (say, just Read) still pays for every built-in schema (Bash, Web*,
2713
+ * NotebookEdit, …) it never uses: ~9k tokens per spawn (29,270 -> 20,352,
2714
+ * measured 2026-09-20). Passing --tools built from the same grant removes
2715
+ * the unused schemas.
2716
+ *
2717
+ * MCP grants (`mcp__server__tool` / `mcp__server__*`) are left out of the
2718
+ * --tools list: they are not built-in tool names, --tools does not
2719
+ * recognize them, and the MCP server's tools load through --mcp-config
2720
+ * regardless. Tested 2026-09-20: with an --allowedTools grant of
2721
+ * `Read,mcp__pai__memory_search`, passing `--tools Read` (mcp name
2722
+ * omitted) still let the worker call memory_search successfully; the tool
2723
+ * loads from the filtered MCP config, not from --tools.
2724
+ */
2725
+ function headlessToolsFlag(allowedTools) {
2726
+ const names = [];
2727
+ for (const entry of allowedTools) for (const raw of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
2728
+ if (raw.startsWith("mcp__")) continue;
2729
+ const patterned = raw.match(/^([A-Za-z]+)\(.*\)$/);
2730
+ const name = patterned ? patterned[1] : raw;
2731
+ if (!names.includes(name)) names.push(name);
2732
+ }
2733
+ return names.length ? ["--tools", names.join(",")] : [];
2734
+ }
2735
+ /**
2736
+ * A `--tools` value with no ToolSearch silently disables MCP tool deferral
2737
+ * (measured 2026-09-20: a run with `--tools "Bash,Read,Grep,Glob,Agent"` paid
2738
+ * 65,300 first-turn tokens with 5 MCP servers, 230,448 with 16, versus
2739
+ * 24,889 / 37,133 with ToolSearch present). Applied to the fully-assembled
2740
+ * argv so it catches a `--tools` from any source — the computed
2741
+ * `headlessToolsFlag`/project-pin flag, or one the caller put directly on
2742
+ * the command line.
2743
+ */
2744
+ function ensureToolSearch(cmd) {
2745
+ const flagIdx = cmd.findIndex((a) => a === "--tools");
2746
+ if (flagIdx >= 0 && flagIdx + 1 < cmd.length) {
2747
+ const value = cmd[flagIdx + 1];
2748
+ if (value && !value.split(",").includes("ToolSearch")) {
2749
+ const out = [...cmd];
2750
+ out[flagIdx + 1] = `${value},ToolSearch`;
2751
+ process.stderr.write("pai: added ToolSearch to --tools (keeps MCP tool deferral on)\n");
2752
+ return out;
2753
+ }
2754
+ return cmd;
2755
+ }
2756
+ const eqIdx = cmd.findIndex((a) => a.startsWith("--tools="));
2757
+ if (eqIdx >= 0) {
2758
+ const value = cmd[eqIdx].slice(8);
2759
+ if (value && !value.split(",").includes("ToolSearch")) {
2760
+ const out = [...cmd];
2761
+ out[eqIdx] = `--tools=${value},ToolSearch`;
2762
+ process.stderr.write("pai: added ToolSearch to --tools (keeps MCP tool deferral on)\n");
2763
+ return out;
2764
+ }
2765
+ }
2766
+ return cmd;
2767
+ }
2768
+ /**
2769
+ * The claude flag that turns on the browser bridge, when the run asked for it.
2770
+ *
2771
+ * The bridge is not an MCP server — it rides the Chrome native-host channel,
2772
+ * is absent from `mcpServers`, and is off in a spawned claude until `--chrome`
2773
+ * is passed. A grant such as `mcp__claude-in-chrome__tabs_context_mcp` is
2774
+ * therefore a request for that flag, not for a server to load; without this
2775
+ * the grant names a tool that never exists. `rest` is the caller's own argv:
2776
+ * a `--chrome` they passed themselves is kept rather than duplicated.
2777
+ */
2778
+ function chromeGrantArgs(wanted, rest = []) {
2779
+ if (rest.includes("--chrome")) return [];
2780
+ return grantsChrome(wanted) ? ["--chrome"] : [];
2781
+ }
2782
+ /**
2783
+ * Which MCP server names and --tools list an interactive launch (`pai
2784
+ * worker run`, no -p) uses: an explicit --mcp on this command line beats
2785
+ * the project's `pai project mcp` pin; an explicit --tools beats `pai
2786
+ * project tools`. Neither given: [] — today's "everything loads" default.
2787
+ */
2788
+ function interactiveMcpTools(explicitMcp, callerTools, projectLaunch) {
2789
+ return {
2790
+ mcpNames: explicitMcp.length ? explicitMcp : projectLaunch?.mcp ?? [],
2791
+ tools: !callerTools && projectLaunch?.tools?.length ? projectLaunch.tools : []
2792
+ };
2793
+ }
2169
2794
  async function executeRun(a) {
2170
2795
  const { config, logDir, target, model, label, parsed, noPane } = a;
2171
2796
  const headless = parsed.headless;
@@ -2178,7 +2803,7 @@ async function executeRun(a) {
2178
2803
  const session = resolveSession(term);
2179
2804
  const spawnerSession = resolveSpawnerSession(logDir, cwd);
2180
2805
  let worktree = null;
2181
- if (headless && worktreeWanted(a.worktreeFlag, {
2806
+ if (headless && !a.printCmd && worktreeWanted(a.worktreeFlag, {
2182
2807
  cwd,
2183
2808
  className: a.className,
2184
2809
  prompt: parsed.prompt
@@ -2193,6 +2818,61 @@ async function executeRun(a) {
2193
2818
  });
2194
2819
  }
2195
2820
  env.PAI_WORKER_ID = wid;
2821
+ const chromeArgs = chromeGrantArgs([
2822
+ ...a.mcpFlag ? [a.mcpFlag] : [],
2823
+ ...parsed.mcp,
2824
+ ...target.classMcp ?? [],
2825
+ ...parsed.allowedTools
2826
+ ], parsed.rest);
2827
+ let mcpArgs = [];
2828
+ let toolsFlag = headless ? headlessToolsFlag(parsed.allowedTools) : [];
2829
+ if (headless && !parsed.callerMcpConfig) {
2830
+ const wanted = [
2831
+ ...a.mcpFlag ? [a.mcpFlag] : [],
2832
+ ...parsed.mcp,
2833
+ ...target.classMcp ?? [],
2834
+ ...mcpServersFromToolGrants(parsed.allowedTools)
2835
+ ];
2836
+ if (wanted.length) mcpArgs = [
2837
+ "--strict-mcp-config",
2838
+ "--mcp-config",
2839
+ writeMcpConfig(logDir, wid, expandMcpNames(wanted, config))
2840
+ ];
2841
+ else mcpArgs = [
2842
+ "--strict-mcp-config",
2843
+ "--mcp-config",
2844
+ ensureNoMcpConfig(logDir)
2845
+ ];
2846
+ } else if (!headless && !parsed.callerMcpConfig) {
2847
+ const explicitMcp = [...a.mcpFlag ? [a.mcpFlag] : [], ...parsed.mcp];
2848
+ const projectLaunch = projectLaunchConfig(cwd);
2849
+ const picked = interactiveMcpTools(explicitMcp, parsed.callerTools, projectLaunch);
2850
+ if (picked.mcpNames.length) mcpArgs = [
2851
+ "--strict-mcp-config",
2852
+ "--mcp-config",
2853
+ writeMcpConfig(logDir, wid, expandMcpNames(picked.mcpNames, config))
2854
+ ];
2855
+ if (picked.tools.length) toolsFlag = ["--tools", picked.tools.join(",")];
2856
+ }
2857
+ const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
2858
+ const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
2859
+ const { text: promptText, applied: trailerApplied } = headlessPromptText(parsed.prompt, headless, parsed.callerSystemPrompt, a.reportFormat);
2860
+ let cmd = ["claude"];
2861
+ cmd.push(...modelFlagArgs(headless, model, Boolean(parsed.callerModel)));
2862
+ cmd.push(...chromeArgs, ...mcpArgs, ...toolArgs, ...toolsFlag, ...restArgs);
2863
+ if (headless) {
2864
+ cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
2865
+ if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", workerContractPrompt(a.className, a.reportFormat));
2866
+ if (worktree) cmd.push("--append-system-prompt", worktreeSystemPrompt(wid, worktree.branch, worktree.dir));
2867
+ }
2868
+ cmd = ensureToolSearch(cmd);
2869
+ if (a.printCmd) {
2870
+ console.log(JSON.stringify({
2871
+ cmd,
2872
+ cwd: worktree?.dir ?? cwd
2873
+ }));
2874
+ return 0;
2875
+ }
2196
2876
  const status = {
2197
2877
  id: wid,
2198
2878
  pid: process.pid,
@@ -2210,6 +2890,9 @@ async function executeRun(a) {
2210
2890
  rc: null,
2211
2891
  secs: null,
2212
2892
  origin: headless ? "spawn" : "chat",
2893
+ outputFormat: parsed.outputFormat,
2894
+ ...headless ? { reportFormat: a.reportFormat } : {},
2895
+ ...trailerApplied ? { promptTrailer: true } : {},
2213
2896
  ...session ? { session } : {},
2214
2897
  ...spawnerSession ? { spawnerSession } : {},
2215
2898
  ...a.parent ? {
@@ -2220,7 +2903,8 @@ async function executeRun(a) {
2220
2903
  worktreeDir: worktree.dir,
2221
2904
  branch: worktree.branch,
2222
2905
  worktreeBase: worktree.base
2223
- } : {}
2906
+ } : {},
2907
+ ...a.specPath ? { spec: a.specPath } : {}
2224
2908
  };
2225
2909
  saveStatus(logDir, status);
2226
2910
  a.onWorkerStart?.(wid);
@@ -2231,38 +2915,10 @@ async function executeRun(a) {
2231
2915
  mode: headless ? "headless" : "interactive",
2232
2916
  model,
2233
2917
  cwd,
2234
- label
2918
+ label,
2919
+ ...a.specPath ? { spec: a.specPath } : {}
2235
2920
  });
2236
2921
  if (headless && !noPane && config.pane.enabled && term && process.env.PAI_WORKER_AUTOPANE !== "0") openPaneForWorker(logDir, config, wid, term).catch(() => {});
2237
- let mcpArgs = [];
2238
- if (headless && !parsed.callerMcpConfig) {
2239
- const wanted = [
2240
- ...a.mcpFlag ? [a.mcpFlag] : [],
2241
- ...parsed.mcp,
2242
- ...target.classMcp ?? [],
2243
- ...mcpServersFromToolGrants(parsed.allowedTools)
2244
- ];
2245
- if (wanted.length) mcpArgs = [
2246
- "--strict-mcp-config",
2247
- "--mcp-config",
2248
- writeMcpConfig(logDir, wid, expandMcpNames(wanted, config))
2249
- ];
2250
- else mcpArgs = [
2251
- "--strict-mcp-config",
2252
- "--mcp-config",
2253
- ensureNoMcpConfig(logDir)
2254
- ];
2255
- }
2256
- const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
2257
- const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
2258
- const cmd = ["claude"];
2259
- if (!parsed.callerModel && model) cmd.push("--model", model);
2260
- cmd.push(...mcpArgs, ...toolArgs, ...restArgs);
2261
- if (headless) {
2262
- cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
2263
- if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
2264
- if (worktree) cmd.push("--append-system-prompt", worktreeSystemPrompt(wid, worktree.branch, worktree.dir));
2265
- }
2266
2922
  const t0 = Date.now();
2267
2923
  const proc = spawn(cmd[0], cmd.slice(1), {
2268
2924
  env,
@@ -2347,8 +3003,8 @@ async function executeRun(a) {
2347
3003
  resultReport: null
2348
3004
  };
2349
3005
  if (headless) {
2350
- if (parsed.prompt !== null) try {
2351
- proc.stdin.write(stdinUserMessage(parsed.prompt) + "\n");
3006
+ if (promptText !== null) try {
3007
+ proc.stdin.write(stdinUserMessage(promptText) + "\n");
2352
3008
  } catch {}
2353
3009
  eventsFd = openSync(eventsPath(logDir, wid), "a");
2354
3010
  createInterface({ input: proc.stdout }).on("line", (line) => {
@@ -2363,6 +3019,7 @@ async function executeRun(a) {
2363
3019
  writeEvent(e);
2364
3020
  if (e.type === "system" && e.subtype === "init") {
2365
3021
  if (e.session_id) status.claudeSession = e.session_id;
3022
+ adoptInitModel(status, e);
2366
3023
  const cw = initContextWindow(e);
2367
3024
  if (cw) status.contextWindow = cw;
2368
3025
  saveStatus(logDir, status);
@@ -2406,6 +3063,40 @@ async function executeRun(a) {
2406
3063
  status.secs = secs;
2407
3064
  if (resultEvent) status.last = shortText(resultEvent.result ?? "", 90);
2408
3065
  if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
3066
+ if (headless && a.reportFormat === "ag2" && resultEvent?.result) {
3067
+ const v = validateAg2(resultEvent.result);
3068
+ status.reportValid = v.ok;
3069
+ status.reportErrors = v.errors;
3070
+ if (shouldRetryReport(v, a.noReportRetry ?? false, Boolean(status.claudeSession))) {
3071
+ status.reportRetried = true;
3072
+ saveStatus(logDir, status);
3073
+ appendLedger(ledger, "WORKER-REPORT-RETRY", {
3074
+ id: wid,
3075
+ reason: v.errors.join("; ") || "invalid AG2 message"
3076
+ });
3077
+ try {
3078
+ const reaskText = await reaskAg2Report({
3079
+ env,
3080
+ cwd: worktree?.dir ?? cwd,
3081
+ model,
3082
+ callerPinnedModel: Boolean(parsed.callerModel),
3083
+ chromeArgs,
3084
+ mcpArgs,
3085
+ toolArgs,
3086
+ toolsFlag,
3087
+ sessionId: status.claudeSession
3088
+ });
3089
+ const v2 = validateAg2(reaskText);
3090
+ status.reportValid = v2.ok;
3091
+ status.reportErrors = v2.errors;
3092
+ if (v2.ok) {
3093
+ resultEvent.result = reaskText;
3094
+ ctx.resultReport = parseWorkerReport(reaskText);
3095
+ if (ctx.resultReport?.notes) status.last = shortText(ctx.resultReport.notes, 90);
3096
+ }
3097
+ } catch {}
3098
+ }
3099
+ }
2409
3100
  saveStatus(logDir, status);
2410
3101
  appendLedger(ledger, "WORKER-END", {
2411
3102
  id: wid,
@@ -2436,6 +3127,7 @@ async function executeRun(a) {
2436
3127
  noPane: a.noPane,
2437
3128
  mcpFlag: a.mcpFlag,
2438
3129
  cwd: a.cwd,
3130
+ specPath: a.specPath,
2439
3131
  parent: a.parent,
2440
3132
  stage: a.stage,
2441
3133
  claudeArgs: a.claudeArgs,
@@ -2473,6 +3165,44 @@ function worktreeExtras(s) {
2473
3165
  commits: s.commits ?? 0
2474
3166
  } : void 0;
2475
3167
  }
3168
+ /**
3169
+ * The one bounded re-ask, cap exactly one: `claude --resume <session>` with
3170
+ * AG2_REASK_TEXT, single-shot `--output-format json` (one turn, no streaming
3171
+ * plumbing needed). Same env/model/mcp/tool flags as the original launch —
3172
+ * only --resume and the fixed re-ask prompt are new — so the resumed turn
3173
+ * sees the same grants the worker ran with.
3174
+ */
3175
+ async function reaskAg2Report(a) {
3176
+ let cmd = ["claude"];
3177
+ cmd.push(...modelFlagArgs(true, a.model, a.callerPinnedModel));
3178
+ cmd.push(...a.chromeArgs, ...a.mcpArgs, ...a.toolArgs, ...a.toolsFlag);
3179
+ cmd.push("--resume", a.sessionId, "-p", AG2_REASK_TEXT, "--output-format", "json");
3180
+ cmd = ensureToolSearch(cmd);
3181
+ const proc = spawn(cmd[0], cmd.slice(1), {
3182
+ env: a.env,
3183
+ cwd: a.cwd,
3184
+ stdio: [
3185
+ "ignore",
3186
+ "pipe",
3187
+ "inherit"
3188
+ ]
3189
+ });
3190
+ const timer = setTimeout(() => {
3191
+ try {
3192
+ proc.kill("SIGKILL");
3193
+ } catch {}
3194
+ }, a.timeoutMs ?? 6e4);
3195
+ let out = "";
3196
+ proc.stdout.on("data", (chunk) => {
3197
+ out += chunk.toString("utf8");
3198
+ });
3199
+ await new Promise((resolve) => {
3200
+ proc.on("error", () => resolve());
3201
+ proc.on("close", () => resolve());
3202
+ });
3203
+ clearTimeout(timer);
3204
+ return resultFromOutput(out);
3205
+ }
2476
3206
  async function executeCodexRun(a) {
2477
3207
  const { config, logDir, target, model, label, parsed, noPane } = a;
2478
3208
  if (!parsed.headless || parsed.prompt === null) throw new Error(`provider "${target.providerName}" (engine codex) supports headless runs only: pass the task with -p '<prompt>'`);
@@ -2516,6 +3246,8 @@ async function executeCodexRun(a) {
2516
3246
  rc: null,
2517
3247
  secs: null,
2518
3248
  origin: "spawn",
3249
+ outputFormat: parsed.outputFormat,
3250
+ reportFormat: "json",
2519
3251
  ...session ? { session } : {},
2520
3252
  ...spawnerSession ? { spawnerSession } : {},
2521
3253
  ...target.provider.contextWindow ? { contextWindow: target.provider.contextWindow } : {},
@@ -2527,7 +3259,8 @@ async function executeCodexRun(a) {
2527
3259
  worktreeDir: worktree.dir,
2528
3260
  branch: worktree.branch,
2529
3261
  worktreeBase: worktree.base
2530
- } : {}
3262
+ } : {},
3263
+ ...a.specPath ? { spec: a.specPath } : {}
2531
3264
  };
2532
3265
  saveStatus(logDir, status);
2533
3266
  a.onWorkerStart?.(wid);
@@ -2793,5 +3526,5 @@ function resultFromOutput(out) {
2793
3526
  }
2794
3527
 
2795
3528
  //#endregion
2796
- export { nowStamp as A, alive as C, loadStatus as D, isChatPane as E, shortText as F, appendLedger as M, ledgerSummary as N, loadStatuses as O, parseRunnerArgs as P, ageOf as S, contextPercent as T, itermForClaudeSession as _, parseWorkerReport as a, workerInScope as b, mergeWorker as c, sayToWorker as d, workerDepth as f, currentTabKey as g, openPaneForWorker as h, describeMcp as i, saveStatus as j, newWorkerId as k, handoffFromInside as l, openFollowPane as m, runWorker as n, renderReport as o, checkPaneForWorker as p, testProvider as r, discardWorker as s, printResult as t, readInbox as u, resolveSession as v, contextLabel as w, UNLABELED as x, sessionTag as y };
2797
- //# sourceMappingURL=run-D7K5UALn.mjs.map
3529
+ export { loadStatuses as A, expandMcpNames as B, UNLABELED as C, contextPercent as D, contextLabel as E, waitForTerminalStatus as F, longInlinePromptHint as I, parseRunnerArgs as L, nowStamp as M, saveStatus as N, isChatPane as O, setWorkerLabel as P, shortText as R, ledgerSummary as S, alive as T, itermForClaudeSession as _, parseWorkerReport as a, workerInScope as b, mergeWorker as c, sayToWorker as d, workerDepth as f, currentTabKey as g, openPaneForWorker as h, usageContextTokens as i, newWorkerId as j, loadStatus as k, handoffFromInside as l, openFollowPane as m, runWorker as n, renderReport as o, checkPaneForWorker as p, testProvider as r, discardWorker as s, printResult as t, readInbox as u, resolveSession as v, ageOf as w, appendLedger as x, sessionTag as y, describeMcp as z };
3530
+ //# sourceMappingURL=run-ByBKowN5.mjs.map