@tekmidian/pai 0.42.5 → 0.42.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +9 -9
- package/README.md +26 -8
- package/dist/{aibroker-client-BP74gGwD.mjs → aibroker-client-Bn9SnGMa.mjs} +3 -3
- package/dist/{aibroker-client-BP74gGwD.mjs.map → aibroker-client-Bn9SnGMa.mjs.map} +1 -1
- package/dist/{aibroker-client-C6m71p6O.mjs → aibroker-client-D5NT0S1N.mjs} +1 -1
- package/dist/{auto-route-_5zzJz5q.mjs → auto-route-CcD0OWWB.mjs} +4 -4
- package/dist/{auto-route-_5zzJz5q.mjs.map → auto-route-CcD0OWWB.mjs.map} +1 -1
- package/dist/{chain-DTII7xeL.mjs → chain-BbcfP77K.mjs} +5 -4
- package/dist/{chain-DTII7xeL.mjs.map → chain-BbcfP77K.mjs.map} +1 -1
- package/dist/cli/index.mjs +23 -21
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +23 -21
- package/dist/{clusters-j0ZuNYp9.mjs → clusters-Cw_P6lO-.mjs} +2 -2
- package/dist/{clusters-j0ZuNYp9.mjs.map → clusters-Cw_P6lO-.mjs.map} +1 -1
- package/dist/{config-B64vFg14.mjs → config-DCu8THKi.mjs} +41 -9
- package/dist/{config-B64vFg14.mjs.map → config-DCu8THKi.mjs.map} +1 -1
- package/dist/config-DeqFqa1N.mjs +3 -0
- package/dist/config-DxMZjpvY.mjs +1289 -0
- package/dist/config-DxMZjpvY.mjs.map +1 -0
- package/dist/context-handover-cache-B9NKSjwR.mjs +3242 -0
- package/dist/context-handover-cache-B9NKSjwR.mjs.map +1 -0
- package/dist/daemon/index.mjs +23 -22
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BNjbVsQU.mjs +24 -0
- package/dist/{daemon-B8N1egl0.mjs → daemon-BppziPi6.mjs} +52 -38
- package/dist/daemon-BppziPi6.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +144 -117
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/db-BOBfewFH.mjs +3 -0
- package/dist/{db-a1ixZQjr.mjs → db-DJ8rk0VM.mjs} +15 -6
- package/dist/{db-a1ixZQjr.mjs.map → db-DJ8rk0VM.mjs.map} +1 -1
- package/dist/db-DbY2O8QO.mjs +3 -0
- package/dist/{db-Ca5qfsMC.mjs → db-Ddns80yf.mjs} +15 -6
- package/dist/{db-Ca5qfsMC.mjs.map → db-Ddns80yf.mjs.map} +1 -1
- package/dist/{detect-CdaA48EI.mjs → detect-Bf2z-oKB.mjs} +1 -1
- package/dist/{detect-CdaA48EI.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
- package/dist/detector-CkwkXuGh.mjs +5 -0
- package/dist/{detector-C73Nxm94.mjs → detector-DWXkeTlk.mjs} +2 -2
- package/dist/{detector-C73Nxm94.mjs.map → detector-DWXkeTlk.mjs.map} +1 -1
- package/dist/{embeddings-CEBGrzwu.mjs → embeddings-ClqKtLZ8.mjs} +1 -1
- package/dist/{embeddings-DOLZnT1X.mjs → embeddings-DvRKcQyt.mjs} +1 -1
- package/dist/{embeddings-DOLZnT1X.mjs.map → embeddings-DvRKcQyt.mjs.map} +1 -1
- package/dist/factory-47Qf_df7.mjs +3 -0
- package/dist/{factory-DEb9NmHJ.mjs → factory-DNYAqjH0.mjs} +8 -8
- package/dist/{factory-DEb9NmHJ.mjs.map → factory-DNYAqjH0.mjs.map} +1 -1
- package/dist/{fallback-oJl3BaIN.mjs → fallback-Cwt4hmh5.mjs} +182 -122
- package/dist/fallback-Cwt4hmh5.mjs.map +1 -0
- package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
- package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
- package/dist/hooks/capture-all-events.mjs +117 -34
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/capture-session-summary.mjs +110 -33
- package/dist/hooks/capture-session-summary.mjs.map +4 -4
- package/dist/hooks/capture-tool-output.mjs +105 -28
- package/dist/hooks/capture-tool-output.mjs.map +4 -4
- package/dist/hooks/cleanup-session-files.mjs +54 -13
- package/dist/hooks/cleanup-session-files.mjs.map +2 -2
- package/dist/hooks/context-compression-hook.mjs +7565 -122
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +55 -14
- package/dist/hooks/initialize-session.mjs.map +2 -2
- package/dist/hooks/inject-observations.mjs +79 -18
- package/dist/hooks/inject-observations.mjs.map +3 -3
- package/dist/hooks/load-core-context.mjs +55 -14
- package/dist/hooks/load-core-context.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +185 -103
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/mcp-deferred-gate.mjs +54 -13
- package/dist/hooks/mcp-deferred-gate.mjs.map +2 -2
- package/dist/hooks/mcp-deferred-reminder.mjs +9 -0
- package/dist/hooks/mcp-deferred-reminder.mjs.map +3 -3
- package/dist/hooks/observe.mjs +54 -13
- package/dist/hooks/observe.mjs.map +2 -2
- package/dist/hooks/post-compact-inject.mjs +7419 -16
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +7778 -171
- package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
- package/dist/hooks/route-edits-to-worker.mjs +90 -0
- package/dist/hooks/route-edits-to-worker.mjs.map +7 -0
- package/dist/hooks/security-validator.mjs +147 -6
- package/dist/hooks/security-validator.mjs.map +4 -4
- package/dist/hooks/session-commands.mjs +9 -0
- package/dist/hooks/session-commands.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +182 -105
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/subagent-stop-hook.mjs +3 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
- package/dist/hooks/sync-todo-to-md.mjs +54 -13
- package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
- package/dist/hooks/update-tab-on-action.mjs +3 -0
- package/dist/hooks/update-tab-on-action.mjs.map +1 -1
- package/dist/hooks/update-tab-titles.mjs +3 -0
- package/dist/hooks/update-tab-titles.mjs.map +1 -1
- package/dist/hooks/whisper-reinject.mjs +3 -0
- package/dist/hooks/whisper-reinject.mjs.map +1 -1
- package/dist/hooks/whisper-rules.mjs +134 -29
- package/dist/hooks/whisper-rules.mjs.map +4 -4
- package/dist/hooks/worker-proxy.mjs +7737 -117
- package/dist/hooks/worker-proxy.mjs.map +4 -4
- package/dist/hooks/worker-status-line.mjs +7791 -144
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/hooks/worker-supervision.mjs +7721 -115
- package/dist/hooks/worker-supervision.mjs.map +4 -4
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +10 -10
- package/dist/{indexer-backend-BPTzKFZU.mjs → indexer-backend-CdvCJCkD.mjs} +3 -3
- package/dist/{indexer-backend-BPTzKFZU.mjs.map → indexer-backend-CdvCJCkD.mjs.map} +1 -1
- package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C6NryMMb.mjs} +2 -2
- package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C6NryMMb.mjs.map} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-BSjM5xZh.mjs} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-BSjM5xZh.mjs.map} +1 -1
- package/dist/{latent-ideas-CtSca4qG.mjs → latent-ideas-BfL4nCQ3.mjs} +4 -4
- package/dist/{latent-ideas-CtSca4qG.mjs.map → latent-ideas-BfL4nCQ3.mjs.map} +1 -1
- package/dist/{link-boost-Bucnz3n8.mjs → link-boost-C5PaNdzm.mjs} +1 -1
- package/dist/{link-boost-Bucnz3n8.mjs.map → link-boost-C5PaNdzm.mjs.map} +1 -1
- package/dist/main-resolver-CnfnWa8C.mjs +7 -0
- package/dist/{main-resolver-DnkxPYkr.mjs → main-resolver-DvYu9jbl.mjs} +21 -11
- package/dist/main-resolver-DvYu9jbl.mjs.map +1 -0
- package/dist/{migrate-Cjzeefn9.mjs → migrate-CfERwXhY.mjs} +2 -2
- package/dist/{migrate-Cjzeefn9.mjs.map → migrate-CfERwXhY.mjs.map} +1 -1
- package/dist/{neighborhood-CM7Jgb51.mjs → neighborhood-CETR_P4y.mjs} +2 -2
- package/dist/{neighborhood-CM7Jgb51.mjs.map → neighborhood-CETR_P4y.mjs.map} +1 -1
- package/dist/{note-context-DCwE2A-M.mjs → note-context-DCcRWBoN.mjs} +1 -1
- package/dist/{note-context-DCwE2A-M.mjs.map → note-context-DCcRWBoN.mjs.map} +1 -1
- package/dist/pai-home-ChAOupZc.mjs +164 -0
- package/dist/pai-home-ChAOupZc.mjs.map +1 -0
- package/dist/{pai-marker-CHtbJMwJ.mjs → pai-marker-B20KqhA8.mjs} +1 -1
- package/dist/{pai-marker-CHtbJMwJ.mjs.map → pai-marker-B20KqhA8.mjs.map} +1 -1
- package/dist/{planner-D5Ui7DAm.mjs → planner-pXRc-8yt.mjs} +7 -6
- package/dist/{planner-D5Ui7DAm.mjs.map → planner-pXRc-8yt.mjs.map} +1 -1
- package/dist/{postgres-CtOzfoQU.mjs → postgres-CUSXWgTG.mjs} +2 -2
- package/dist/{postgres-CtOzfoQU.mjs.map → postgres-CUSXWgTG.mjs.map} +1 -1
- package/dist/{program-zpoYnJGz.mjs → program-LLNPpxlj.mjs} +8308 -6065
- package/dist/program-LLNPpxlj.mjs.map +1 -0
- package/dist/query-feedback-C7rSfiXU.mjs +3 -0
- package/dist/{query-feedback-Dqz-HC__.mjs → query-feedback-CYyOGi3y.mjs} +20 -6
- package/dist/query-feedback-CYyOGi3y.mjs.map +1 -0
- package/dist/{reranker-CwTCNsgA.mjs → reranker-B5qUTamW.mjs} +1 -1
- package/dist/{reranker-xPm04PXx.mjs → reranker-HHmhZk6v.mjs} +1 -1
- package/dist/{reranker-xPm04PXx.mjs.map → reranker-HHmhZk6v.mjs.map} +1 -1
- package/dist/router-CremHKTI.mjs +3 -0
- package/dist/{router-DD6D4j31.mjs → router-DBAZfE7X.mjs} +2 -2
- package/dist/{router-DD6D4j31.mjs.map → router-DBAZfE7X.mjs.map} +1 -1
- package/dist/{run-D7K5UALn.mjs → run-DDdT9teG.mjs} +530 -210
- package/dist/run-DDdT9teG.mjs.map +1 -0
- package/dist/{runtime-paths-rni52zHX.mjs → runtime-paths-B0P1TvUr.mjs} +1 -1
- package/dist/{runtime-paths-rni52zHX.mjs.map → runtime-paths-B0P1TvUr.mjs.map} +1 -1
- package/dist/{search-Rpk1cSBC.mjs → search-B69cQwDB.mjs} +3 -3
- package/dist/{search-Rpk1cSBC.mjs.map → search-B69cQwDB.mjs.map} +1 -1
- package/dist/{search-CfPpJAWQ.mjs → search-fb-5ovj4.mjs} +2 -2
- package/dist/{server-D4vAieF4.mjs → server-Ch99xt5K.mjs} +15 -14
- package/dist/server-Ch99xt5K.mjs.map +1 -0
- package/dist/skills/Advisor/SKILL.md +3 -3
- package/dist/skills/Providers/SKILL.md +30 -0
- package/dist/skills/Reconstruct/SKILL.md +1 -1
- package/dist/skills/VaultContext/SKILL.md +1 -1
- package/dist/skills/Whisper/SKILL.md +3 -3
- package/dist/skills/Worker/SKILL.md +1 -1
- package/dist/{sources-CS43CpuS.mjs → sources-KBGEA_E6.mjs} +2 -2
- package/dist/{sources-CS43CpuS.mjs.map → sources-KBGEA_E6.mjs.map} +1 -1
- package/dist/{sqlite-Cq3gJcq9.mjs → sqlite-By6r2YhZ.mjs} +3 -3
- package/dist/{sqlite-Cq3gJcq9.mjs.map → sqlite-By6r2YhZ.mjs.map} +1 -1
- package/dist/{state-CY6I-Hnc.mjs → state-DnYGRmS7.mjs} +1 -1
- package/dist/{state-BRdB-Bf1.mjs → state-PJrgfDLW.mjs} +1 -1
- package/dist/{state-BRdB-Bf1.mjs.map → state-PJrgfDLW.mjs.map} +1 -1
- package/dist/{stop-words-Hfu8u22w.mjs → stop-words-BaMEGVeY.mjs} +1 -1
- package/dist/{stop-words-Hfu8u22w.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
- package/dist/{sync-BWbe8JTg.mjs → sync-Btcb6xA4.mjs} +3 -3
- package/dist/{sync-BWbe8JTg.mjs.map → sync-Btcb6xA4.mjs.map} +1 -1
- package/dist/{themes-B7h9YDiy.mjs → themes-BVoAnLZj.mjs} +3 -3
- package/dist/{themes-B7h9YDiy.mjs.map → themes-BVoAnLZj.mjs.map} +1 -1
- package/dist/{tools-BW3NLMPk.mjs → tools-BradfQk7.mjs} +4 -3
- package/dist/{tools-LlzcV_eK.mjs → tools-UrSHIe29.mjs} +30 -233
- package/dist/tools-UrSHIe29.mjs.map +1 -0
- package/dist/{trace-u4hT-pLD.mjs → trace-Cy8quQTm.mjs} +1 -1
- package/dist/{trace-u4hT-pLD.mjs.map → trace-Cy8quQTm.mjs.map} +1 -1
- package/dist/{utils-DhMex3Ox.mjs → utils-BD8g8oSf.mjs} +1 -1
- package/dist/{utils-9Err2RBW.mjs → utils-CJymHkYm.mjs} +1 -1
- package/dist/{utils-9Err2RBW.mjs.map → utils-CJymHkYm.mjs.map} +1 -1
- package/dist/{vault-indexer-mata9S7b.mjs → vault-indexer-C5i-OAyr.mjs} +2 -2
- package/dist/{vault-indexer-mata9S7b.mjs.map → vault-indexer-C5i-OAyr.mjs.map} +1 -1
- package/dist/wakeup-TnGjJY3I.mjs +218 -0
- package/dist/wakeup-TnGjJY3I.mjs.map +1 -0
- package/dist/work-queue-worker-BA5IDBwQ.mjs +567 -0
- package/dist/work-queue-worker-BA5IDBwQ.mjs.map +1 -0
- package/dist/work-queue-worker-DLM2wlqM.mjs +14 -0
- package/dist/{zettelkasten-F0D_XfQ4.mjs → zettelkasten-BBjA7GcP.mjs} +4 -4
- package/dist/{zettelkasten-F0D_XfQ4.mjs.map → zettelkasten-BBjA7GcP.mjs.map} +1 -1
- package/docs/commands/README.md +30 -3
- package/docs/commands/audit.md +88 -0
- package/docs/commands/backup.md +2 -2
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/config.md +59 -0
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +1 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/help.md +1 -1
- package/docs/commands/identity.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/launch.md +45 -0
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +3 -3
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/project.md +46 -1
- package/docs/commands/projects.md +46 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/session.md +1 -1
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +95 -6
- package/docs/commands/zettel.md +1 -1
- package/docs/provider-abstraction.md +1 -1
- package/docs/provider-independence-details.md +8 -5
- package/docs/task-bus.md +1 -1
- package/docs/worker.md +136 -83
- package/docs/workers-config.md +446 -0
- package/package.json +4 -2
- package/scripts/build-hooks.mjs +56 -3
- package/src/hooks/pre-compact.sh +5 -2
- package/src/hooks/session-stop.sh +29 -4
- package/src/hooks/ts/capture-all-events.ts +3 -3
- package/src/hooks/ts/lib/advisor-budget.test.ts +98 -0
- package/src/hooks/ts/lib/advisor-budget.ts +90 -0
- package/src/hooks/ts/lib/agent-gate.test.ts +101 -0
- package/src/hooks/ts/lib/agent-gate.ts +112 -0
- package/src/hooks/ts/lib/context-fill.ts +2 -2
- package/src/hooks/ts/lib/edit-gate.test.ts +52 -0
- package/src/hooks/ts/lib/edit-gate.ts +51 -0
- package/src/hooks/ts/lib/mcp-deferred-reminder.test.ts +8 -0
- package/src/hooks/ts/lib/pai-paths.test.ts +93 -0
- package/src/hooks/ts/lib/pai-paths.ts +168 -24
- package/src/hooks/ts/lib/project-utils/index.ts +1 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +5 -4
- package/src/hooks/ts/lib/project-utils.ts +1 -0
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +2 -2
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +46 -99
- package/src/hooks/ts/pre-tool-use/route-edits-to-worker.test.ts +47 -0
- package/src/hooks/ts/pre-tool-use/route-edits-to-worker.ts +96 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +2 -1
- package/src/hooks/ts/session-end/capture-session-summary.ts +4 -4
- package/src/hooks/ts/session-start/initialize-session.ts +3 -3
- package/src/hooks/ts/session-start/inject-observations.test.ts +56 -0
- package/src/hooks/ts/session-start/inject-observations.ts +28 -8
- package/src/hooks/ts/session-start/load-core-context.ts +4 -4
- package/src/hooks/ts/session-start/load-project-context.ts +2 -2
- package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -0
- package/src/hooks/ts/session-start/session-commands.ts +4 -0
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +6 -11
- package/src/hooks/ts/user-prompt/whisper-rules.test.ts +49 -0
- package/src/hooks/ts/user-prompt/whisper-rules.ts +88 -47
- package/statusline-command.sh +277 -116
- package/templates/agent-prefs.example.md +8 -8
- package/templates/claude-md.template.md +1 -1
- package/templates/voices.example.json +3 -3
- package/dist/config-C_ErGddD.mjs +0 -3
- package/dist/config-_lQI_W08.mjs +0 -640
- package/dist/config-_lQI_W08.mjs.map +0 -1
- package/dist/context-handover-cache-pNFdBNrU.mjs +0 -1548
- package/dist/context-handover-cache-pNFdBNrU.mjs.map +0 -1
- package/dist/daemon-B8N1egl0.mjs.map +0 -1
- package/dist/daemon-DHfyoTlk.mjs +0 -23
- package/dist/db-O-cyAPfS.mjs +0 -3
- package/dist/db-XEwJbuGO.mjs +0 -3
- package/dist/detector-C-7KRi42.mjs +0 -5
- package/dist/factory-Dbo8yNPi.mjs +0 -3
- package/dist/fallback-oJl3BaIN.mjs.map +0 -1
- package/dist/main-resolver-BAfOMppW.mjs +0 -7
- package/dist/main-resolver-DnkxPYkr.mjs.map +0 -1
- package/dist/program-zpoYnJGz.mjs.map +0 -1
- package/dist/query-feedback-DGoVRaWD.mjs +0 -3
- package/dist/query-feedback-Dqz-HC__.mjs.map +0 -1
- package/dist/router-BTIGZR2_.mjs +0 -3
- package/dist/run-D7K5UALn.mjs.map +0 -1
- package/dist/server-D4vAieF4.mjs.map +0 -1
- package/dist/tools-LlzcV_eK.mjs.map +0 -1
- package/dist/work-queue-worker-AEmfVE0F.mjs +0 -14
- package/dist/work-queue-worker-DIT-VLSh.mjs +0 -2136
- package/dist/work-queue-worker-DIT-VLSh.mjs.map +0 -1
- /package/dist/{indexer-D7MvSQPY.mjs → indexer-C5Le7XE7.mjs} +0 -0
|
@@ -1,12 +1,163 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
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
|
/**
|
|
@@ -1626,14 +1840,28 @@ const OPERATOR_MARK = "[operator]";
|
|
|
1626
1840
|
/** Appended to the caller's system prompt on every headless run (2h). */
|
|
1627
1841
|
const WORKER_CONTRACT_PROMPT = [
|
|
1628
1842
|
"You are a headless implementation worker, run non-interactively by an orchestrating session.",
|
|
1843
|
+
"You are the worker: this task is yours to finish in this process. Never hand your whole task to a",
|
|
1844
|
+
"single child worker and step back — that is delegation, not work. Child workers are for two things",
|
|
1845
|
+
"only: (1) PARALLELISING independent parts of your task, each part a self-contained spec; (2) CHEAPER",
|
|
1846
|
+
"bounded sub-tasks — probes, renders, test runs, lookups — run one tier down with --class spotcheck or",
|
|
1847
|
+
"--class simple. Always pin --provider anthropic on children.",
|
|
1848
|
+
"You own your children's results: run them in the foreground or poll until each has finished, read",
|
|
1849
|
+
"their result, verify it, and fold it into your own report. When your turn ends this run ends and any",
|
|
1850
|
+
"running child is killed, so never end with a child still running.",
|
|
1851
|
+
"Ending your turn ends this run. There is no later wake-up: never call ScheduleWakeup, never say you",
|
|
1852
|
+
"will pick something up later, never leave a background command as your final action. Finish, then",
|
|
1853
|
+
"report.",
|
|
1629
1854
|
"No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
|
|
1630
1855
|
"Your ONE final message is a JSON object and nothing else — no prose before or after, no code fence:",
|
|
1631
1856
|
"{\"changed\":[{\"path\":\"…\",\"summary\":\"…\"}],\"commands\":[\"…\"],\"checks\":[{\"name\":\"…\",\"ok\":true,\"detail\":\"…\"}],\"open\":[\"…\"],\"notes\":\"one line\"}",
|
|
1632
1857
|
"changed: files you touched (path + one-line summary). commands: the commands that verify the work.",
|
|
1633
1858
|
"checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.",
|
|
1634
1859
|
"notes: one line, the headline a reviewer reads first.",
|
|
1635
|
-
"
|
|
1636
|
-
"
|
|
1860
|
+
"NEVER put a multi-line or quoted payload inline in a shell command — no heredocs (<<EOF), no long",
|
|
1861
|
+
"-p '…' specs for child workers, no inline JSON, no inline scripts. ALWAYS write it to a file with",
|
|
1862
|
+
"the Write tool first and pass the file: -p \"$(cat spec.txt)\", --file spec.txt, or < spec.txt.",
|
|
1863
|
+
"Inline quoting fails silently or dies with a parse error and burns a worker launch; there are no",
|
|
1864
|
+
"exceptions.",
|
|
1637
1865
|
"Never inline code one-liners (python3 -c, node -e, ruby -e and friends) — the permission layer",
|
|
1638
1866
|
"denies them (\"ztk: command denied by permission rules\"); write the script to a file with the",
|
|
1639
1867
|
"Write tool and run it (python3 script.py), or use jq for JSON inspection.",
|
|
@@ -1717,104 +1945,56 @@ function relShort(p, cwd) {
|
|
|
1717
1945
|
}
|
|
1718
1946
|
|
|
1719
1947
|
//#endregion
|
|
1720
|
-
//#region src/workers/
|
|
1948
|
+
//#region src/workers/project-config.ts
|
|
1721
1949
|
/**
|
|
1722
|
-
*
|
|
1950
|
+
* project-config.ts — the project registry's optional launch pin, read for
|
|
1951
|
+
* the interactive branch of run.ts only: which MCP servers and built-in
|
|
1952
|
+
* tools a project's supervisor session (`pai worker run`, no -p) loads. Set
|
|
1953
|
+
* with `pai project mcp` / `pai project tools` (src/cli/commands/project/
|
|
1954
|
+
* session-config.ts), stored in the same `projects.session_config` JSON
|
|
1955
|
+
* column as the rest of a project's launch config. Unset = today's
|
|
1956
|
+
* behavior: every server, every tool.
|
|
1723
1957
|
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
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.
|
|
1958
|
+
* A standalone SQL lookup rather than importing the CLI's detectProject:
|
|
1959
|
+
* workers/ is a dependency of cli/, not the other way around.
|
|
1735
1960
|
*/
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
}
|
|
1961
|
+
function asStringArray(v) {
|
|
1962
|
+
if (!Array.isArray(v)) return void 0;
|
|
1963
|
+
const out = v.filter((x) => typeof x === "string");
|
|
1964
|
+
return out.length ? out : void 0;
|
|
1747
1965
|
}
|
|
1748
1966
|
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1967
|
+
* The launch config of the project whose root_path is `cwd` or an ancestor
|
|
1968
|
+
* of it (longest match wins) — null when no registered project covers it,
|
|
1969
|
+
* or when the matching project has no mcp/tools pin set. `dbPath` is
|
|
1970
|
+
* injectable for tests; production callers take the default.
|
|
1751
1971
|
*/
|
|
1752
|
-
function
|
|
1753
|
-
const
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1972
|
+
function projectLaunchConfig(cwd, dbPath = registryDbPath()) {
|
|
1973
|
+
const target = resolve(cwd);
|
|
1974
|
+
const db = openRegistry(dbPath);
|
|
1975
|
+
try {
|
|
1976
|
+
const rows = db.prepare(`SELECT root_path, session_config FROM projects WHERE status != 'archived' ORDER BY LENGTH(root_path) DESC`).all();
|
|
1977
|
+
for (const row of rows) {
|
|
1978
|
+
const root = resolve(row.root_path);
|
|
1979
|
+
if (target !== root && !target.startsWith(root + "/")) continue;
|
|
1980
|
+
if (!row.session_config) return null;
|
|
1981
|
+
let parsed;
|
|
1982
|
+
try {
|
|
1983
|
+
parsed = JSON.parse(row.session_config);
|
|
1984
|
+
} catch {
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
const mcp = asStringArray(parsed.mcp);
|
|
1988
|
+
const tools = asStringArray(parsed.tools);
|
|
1989
|
+
return mcp || tools ? {
|
|
1990
|
+
mcp,
|
|
1991
|
+
tools
|
|
1992
|
+
} : null;
|
|
1763
1993
|
}
|
|
1764
|
-
|
|
1765
|
-
}
|
|
1766
|
-
|
|
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(", ")}`);
|
|
1994
|
+
return null;
|
|
1995
|
+
} finally {
|
|
1996
|
+
db.close();
|
|
1815
1997
|
}
|
|
1816
|
-
lines.push(`usage: pai worker run --mcp <name>[,<name>…] -p '<task>'`);
|
|
1817
|
-
return lines;
|
|
1818
1998
|
}
|
|
1819
1999
|
|
|
1820
2000
|
//#endregion
|
|
@@ -1855,7 +2035,7 @@ function buildCodexEnv(provider) {
|
|
|
1855
2035
|
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
1856
2036
|
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
1857
2037
|
const keyPath = providerKeyPath(provider);
|
|
1858
|
-
if (keyPath && existsSync(keyPath)) env.OPENAI_API_KEY =
|
|
2038
|
+
if (provider.key || keyPath && existsSync(keyPath)) env.OPENAI_API_KEY = resolveProviderKey(provider) ?? "";
|
|
1859
2039
|
if (provider.upstreamUrl) env.OPENAI_BASE_URL = provider.upstreamUrl;
|
|
1860
2040
|
for (const [k, v] of Object.entries(provider.env)) env[k] = v;
|
|
1861
2041
|
env.PAI_WORKER = "1";
|
|
@@ -2014,7 +2194,12 @@ function codexInstalled() {
|
|
|
2014
2194
|
* (context meter included), ledger lines, result printed in the caller's
|
|
2015
2195
|
* --output-format (json adds the parsed `report`), and the follow pane
|
|
2016
2196
|
* (unless --no-pane).
|
|
2017
|
-
* - interactive: no MCP restriction, no pane,
|
|
2197
|
+
* - interactive: no MCP/tool restriction by default, no pane,
|
|
2198
|
+
* ENABLE_TOOL_SEARCH=true. A project that pinned a list with `pai
|
|
2199
|
+
* project mcp` / `pai project tools` gets `--strict-mcp-config
|
|
2200
|
+
* --mcp-config <filtered>` / `--tools <list>` built the same way a
|
|
2201
|
+
* headless run's allowlist is; an explicit --mcp or --tools on this
|
|
2202
|
+
* command line still wins over the project's pin.
|
|
2018
2203
|
*
|
|
2019
2204
|
* Auto-routed runs that die of a quota error before the first tool call are
|
|
2020
2205
|
* restarted on the next provider in routing order (WORKER-REROUTE ledger line).
|
|
@@ -2077,6 +2262,56 @@ function resetContextTokensOnCompact(status, e) {
|
|
|
2077
2262
|
function isCompactBoundary(e) {
|
|
2078
2263
|
return e.type === "system" && (e.subtype === "compact_boundary" || e.subtype === "compact");
|
|
2079
2264
|
}
|
|
2265
|
+
/**
|
|
2266
|
+
* The model the init event announces, adopted into the status when the spawn
|
|
2267
|
+
* could not name one (a caller that passed `--model` itself in the claude
|
|
2268
|
+
* args, or a provider whose table has no entry for the capability). The run's
|
|
2269
|
+
* real model is then only knowable from the first event it sends. Never
|
|
2270
|
+
* overwrites an explicit model — a resolved or pinned model stays the
|
|
2271
|
+
* recorded truth.
|
|
2272
|
+
*/
|
|
2273
|
+
function adoptInitModel(status, e) {
|
|
2274
|
+
if (status.model) return;
|
|
2275
|
+
const m = (e.model ?? "").trim();
|
|
2276
|
+
if (m) status.model = m;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* The model a run goes out on: an explicit `--model` wins; else the class
|
|
2280
|
+
* target's alias ("glm/fast") names the capability; else the capability the
|
|
2281
|
+
* class implies (image → image model, spotcheck/simple → fast, everything
|
|
2282
|
+
* else → default), resolved against the provider's model table. The built-in
|
|
2283
|
+
* anthropic provider resolves through the same table as any other, so a
|
|
2284
|
+
* worker never inherits the orchestrator session's model.
|
|
2285
|
+
*/
|
|
2286
|
+
function resolveRunModel(target, className, modelFlag) {
|
|
2287
|
+
if (modelFlag) return modelFlag;
|
|
2288
|
+
const alias = target.modelAlias;
|
|
2289
|
+
const capability = alias && isModelCapability(alias) ? alias : classModelCapability(className);
|
|
2290
|
+
return resolveModelCapability(target.provider, capability);
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* The `--model` part of the claude argv. Skipped when the caller already
|
|
2294
|
+
* pinned one in the claude args (it must not be clobbered) or when nothing
|
|
2295
|
+
* resolved — `--model ""` would break the spawn.
|
|
2296
|
+
*/
|
|
2297
|
+
function modelArgs(model, callerPinned) {
|
|
2298
|
+
return !callerPinned && model ? ["--model", model] : [];
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Whether the run's assembled cmd gets a forced --model. Headless workers
|
|
2302
|
+
* always take the resolved class/provider model — that table exists so
|
|
2303
|
+
* spawned subagents stay cheap. An interactive launch (`pai worker run` with
|
|
2304
|
+
* no -p) IS the chat pane, not a subagent: with no --model of the caller's
|
|
2305
|
+
* own it gets none here either, so Claude Code's own settings.json model
|
|
2306
|
+
* applies, exactly as launching `claude` by hand would (2026-09-20: an
|
|
2307
|
+
* interactive supervisor came up on the class model instead of the
|
|
2308
|
+
* settings.json [1m] variant, then needed a manual /model switch that
|
|
2309
|
+
* rebuilt the whole cache). A caller's own --model, headless or not, is
|
|
2310
|
+
* already in the passthrough args and must not be duplicated here.
|
|
2311
|
+
*/
|
|
2312
|
+
function modelFlagArgs(headless, model, callerPinned) {
|
|
2313
|
+
return headless ? modelArgs(model, callerPinned) : [];
|
|
2314
|
+
}
|
|
2080
2315
|
/** Context window announced by the init event, when the endpoint sends one. */
|
|
2081
2316
|
function initContextWindow(e) {
|
|
2082
2317
|
if (typeof e.context_window === "number" && e.context_window > 0) return e.context_window;
|
|
@@ -2094,7 +2329,7 @@ async function runWorker(opts) {
|
|
|
2094
2329
|
const logDir = workersLogDir(config);
|
|
2095
2330
|
mkdirSync(logDir, { recursive: true });
|
|
2096
2331
|
if (opts.className === "plan" && !opts._planner) {
|
|
2097
|
-
const { runPlanner } = await import("./planner-
|
|
2332
|
+
const { runPlanner } = await import("./planner-pXRc-8yt.mjs");
|
|
2098
2333
|
return runPlanner(opts);
|
|
2099
2334
|
}
|
|
2100
2335
|
const parent = launchParent(opts.parent);
|
|
@@ -2106,9 +2341,7 @@ async function runWorker(opts) {
|
|
|
2106
2341
|
assertProviderRunnable(target.providerName, target.provider);
|
|
2107
2342
|
const parsed = parseRunnerArgs(opts.claudeArgs);
|
|
2108
2343
|
const label = opts.label ?? shortText(parsed.prompt ?? UNLABELED, 70);
|
|
2109
|
-
const
|
|
2110
|
-
const capability = alias && isModelCapability(alias) ? alias : classModelCapability(opts.className);
|
|
2111
|
-
const model = opts.modelFlag ?? resolveModelCapability(target.provider, capability);
|
|
2344
|
+
const model = resolveRunModel(target, opts.className, opts.modelFlag);
|
|
2112
2345
|
try {
|
|
2113
2346
|
if (target.provider.engine === "codex") return await executeCodexRun({
|
|
2114
2347
|
config,
|
|
@@ -2120,6 +2353,7 @@ async function runWorker(opts) {
|
|
|
2120
2353
|
claudeArgs: opts.claudeArgs,
|
|
2121
2354
|
noPane: opts.noPane ?? false,
|
|
2122
2355
|
cwd: opts.cwd,
|
|
2356
|
+
specPath: opts.specPath,
|
|
2123
2357
|
parent: parent ?? void 0,
|
|
2124
2358
|
stage: opts.stage,
|
|
2125
2359
|
quiet: opts.quiet,
|
|
@@ -2139,6 +2373,7 @@ async function runWorker(opts) {
|
|
|
2139
2373
|
noPane: opts.noPane ?? false,
|
|
2140
2374
|
mcpFlag: opts.mcpFlag,
|
|
2141
2375
|
cwd: opts.cwd,
|
|
2376
|
+
specPath: opts.specPath,
|
|
2142
2377
|
parent: parent ?? void 0,
|
|
2143
2378
|
stage: opts.stage,
|
|
2144
2379
|
quiet: opts.quiet,
|
|
@@ -2146,7 +2381,8 @@ async function runWorker(opts) {
|
|
|
2146
2381
|
id: opts.id,
|
|
2147
2382
|
worktreeFlag: opts.worktreeFlag,
|
|
2148
2383
|
className: opts.className,
|
|
2149
|
-
reroutes: opts._reroutes ?? 0
|
|
2384
|
+
reroutes: opts._reroutes ?? 0,
|
|
2385
|
+
printCmd: opts.printCmd
|
|
2150
2386
|
});
|
|
2151
2387
|
} catch (e) {
|
|
2152
2388
|
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 +2402,59 @@ const DEFAULT_WORKER_TOOLS = "Read,Edit,Write,Bash,Grep,Glob";
|
|
|
2166
2402
|
function headlessToolGrants(allowedTools) {
|
|
2167
2403
|
return allowedTools.length ? [] : ["--allowedTools", DEFAULT_WORKER_TOOLS];
|
|
2168
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* `--tools` narrows which built-in tool *schemas* claude loads into the
|
|
2407
|
+
* system prompt — distinct from `--allowedTools`, which narrows which of
|
|
2408
|
+
* those loaded tools may run without asking. A headless worker with a small
|
|
2409
|
+
* grant (say, just Read) still pays for every built-in schema (Bash, Web*,
|
|
2410
|
+
* NotebookEdit, …) it never uses: ~9k tokens per spawn (29,270 -> 20,352,
|
|
2411
|
+
* measured 2026-09-20). Passing --tools built from the same grant removes
|
|
2412
|
+
* the unused schemas.
|
|
2413
|
+
*
|
|
2414
|
+
* MCP grants (`mcp__server__tool` / `mcp__server__*`) are left out of the
|
|
2415
|
+
* --tools list: they are not built-in tool names, --tools does not
|
|
2416
|
+
* recognize them, and the MCP server's tools load through --mcp-config
|
|
2417
|
+
* regardless. Tested 2026-09-20: with an --allowedTools grant of
|
|
2418
|
+
* `Read,mcp__pai__memory_search`, passing `--tools Read` (mcp name
|
|
2419
|
+
* omitted) still let the worker call memory_search successfully; the tool
|
|
2420
|
+
* loads from the filtered MCP config, not from --tools.
|
|
2421
|
+
*/
|
|
2422
|
+
function headlessToolsFlag(allowedTools) {
|
|
2423
|
+
const names = [];
|
|
2424
|
+
for (const entry of allowedTools) for (const raw of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
2425
|
+
if (raw.startsWith("mcp__")) continue;
|
|
2426
|
+
const patterned = raw.match(/^([A-Za-z]+)\(.*\)$/);
|
|
2427
|
+
const name = patterned ? patterned[1] : raw;
|
|
2428
|
+
if (!names.includes(name)) names.push(name);
|
|
2429
|
+
}
|
|
2430
|
+
return names.length ? ["--tools", names.join(",")] : [];
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* The claude flag that turns on the browser bridge, when the run asked for it.
|
|
2434
|
+
*
|
|
2435
|
+
* The bridge is not an MCP server — it rides the Chrome native-host channel,
|
|
2436
|
+
* is absent from `mcpServers`, and is off in a spawned claude until `--chrome`
|
|
2437
|
+
* is passed. A grant such as `mcp__claude-in-chrome__tabs_context_mcp` is
|
|
2438
|
+
* therefore a request for that flag, not for a server to load; without this
|
|
2439
|
+
* the grant names a tool that never exists. `rest` is the caller's own argv:
|
|
2440
|
+
* a `--chrome` they passed themselves is kept rather than duplicated.
|
|
2441
|
+
*/
|
|
2442
|
+
function chromeGrantArgs(wanted, rest = []) {
|
|
2443
|
+
if (rest.includes("--chrome")) return [];
|
|
2444
|
+
return grantsChrome(wanted) ? ["--chrome"] : [];
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Which MCP server names and --tools list an interactive launch (`pai
|
|
2448
|
+
* worker run`, no -p) uses: an explicit --mcp on this command line beats
|
|
2449
|
+
* the project's `pai project mcp` pin; an explicit --tools beats `pai
|
|
2450
|
+
* project tools`. Neither given: [] — today's "everything loads" default.
|
|
2451
|
+
*/
|
|
2452
|
+
function interactiveMcpTools(explicitMcp, callerTools, projectLaunch) {
|
|
2453
|
+
return {
|
|
2454
|
+
mcpNames: explicitMcp.length ? explicitMcp : projectLaunch?.mcp ?? [],
|
|
2455
|
+
tools: !callerTools && projectLaunch?.tools?.length ? projectLaunch.tools : []
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2169
2458
|
async function executeRun(a) {
|
|
2170
2459
|
const { config, logDir, target, model, label, parsed, noPane } = a;
|
|
2171
2460
|
const headless = parsed.headless;
|
|
@@ -2178,7 +2467,7 @@ async function executeRun(a) {
|
|
|
2178
2467
|
const session = resolveSession(term);
|
|
2179
2468
|
const spawnerSession = resolveSpawnerSession(logDir, cwd);
|
|
2180
2469
|
let worktree = null;
|
|
2181
|
-
if (headless && worktreeWanted(a.worktreeFlag, {
|
|
2470
|
+
if (headless && !a.printCmd && worktreeWanted(a.worktreeFlag, {
|
|
2182
2471
|
cwd,
|
|
2183
2472
|
className: a.className,
|
|
2184
2473
|
prompt: parsed.prompt
|
|
@@ -2193,6 +2482,59 @@ async function executeRun(a) {
|
|
|
2193
2482
|
});
|
|
2194
2483
|
}
|
|
2195
2484
|
env.PAI_WORKER_ID = wid;
|
|
2485
|
+
const chromeArgs = chromeGrantArgs([
|
|
2486
|
+
...a.mcpFlag ? [a.mcpFlag] : [],
|
|
2487
|
+
...parsed.mcp,
|
|
2488
|
+
...target.classMcp ?? [],
|
|
2489
|
+
...parsed.allowedTools
|
|
2490
|
+
], parsed.rest);
|
|
2491
|
+
let mcpArgs = [];
|
|
2492
|
+
let toolsFlag = headless ? headlessToolsFlag(parsed.allowedTools) : [];
|
|
2493
|
+
if (headless && !parsed.callerMcpConfig) {
|
|
2494
|
+
const wanted = [
|
|
2495
|
+
...a.mcpFlag ? [a.mcpFlag] : [],
|
|
2496
|
+
...parsed.mcp,
|
|
2497
|
+
...target.classMcp ?? [],
|
|
2498
|
+
...mcpServersFromToolGrants(parsed.allowedTools)
|
|
2499
|
+
];
|
|
2500
|
+
if (wanted.length) mcpArgs = [
|
|
2501
|
+
"--strict-mcp-config",
|
|
2502
|
+
"--mcp-config",
|
|
2503
|
+
writeMcpConfig(logDir, wid, expandMcpNames(wanted, config))
|
|
2504
|
+
];
|
|
2505
|
+
else mcpArgs = [
|
|
2506
|
+
"--strict-mcp-config",
|
|
2507
|
+
"--mcp-config",
|
|
2508
|
+
ensureNoMcpConfig(logDir)
|
|
2509
|
+
];
|
|
2510
|
+
} else if (!headless && !parsed.callerMcpConfig) {
|
|
2511
|
+
const explicitMcp = [...a.mcpFlag ? [a.mcpFlag] : [], ...parsed.mcp];
|
|
2512
|
+
const projectLaunch = projectLaunchConfig(cwd);
|
|
2513
|
+
const picked = interactiveMcpTools(explicitMcp, parsed.callerTools, projectLaunch);
|
|
2514
|
+
if (picked.mcpNames.length) mcpArgs = [
|
|
2515
|
+
"--strict-mcp-config",
|
|
2516
|
+
"--mcp-config",
|
|
2517
|
+
writeMcpConfig(logDir, wid, expandMcpNames(picked.mcpNames, config))
|
|
2518
|
+
];
|
|
2519
|
+
if (picked.tools.length) toolsFlag = ["--tools", picked.tools.join(",")];
|
|
2520
|
+
}
|
|
2521
|
+
const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
|
|
2522
|
+
const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
|
|
2523
|
+
const cmd = ["claude"];
|
|
2524
|
+
cmd.push(...modelFlagArgs(headless, model, Boolean(parsed.callerModel)));
|
|
2525
|
+
cmd.push(...chromeArgs, ...mcpArgs, ...toolArgs, ...toolsFlag, ...restArgs);
|
|
2526
|
+
if (headless) {
|
|
2527
|
+
cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
|
|
2528
|
+
if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
|
|
2529
|
+
if (worktree) cmd.push("--append-system-prompt", worktreeSystemPrompt(wid, worktree.branch, worktree.dir));
|
|
2530
|
+
}
|
|
2531
|
+
if (a.printCmd) {
|
|
2532
|
+
console.log(JSON.stringify({
|
|
2533
|
+
cmd,
|
|
2534
|
+
cwd: worktree?.dir ?? cwd
|
|
2535
|
+
}));
|
|
2536
|
+
return 0;
|
|
2537
|
+
}
|
|
2196
2538
|
const status = {
|
|
2197
2539
|
id: wid,
|
|
2198
2540
|
pid: process.pid,
|
|
@@ -2210,6 +2552,7 @@ async function executeRun(a) {
|
|
|
2210
2552
|
rc: null,
|
|
2211
2553
|
secs: null,
|
|
2212
2554
|
origin: headless ? "spawn" : "chat",
|
|
2555
|
+
outputFormat: parsed.outputFormat,
|
|
2213
2556
|
...session ? { session } : {},
|
|
2214
2557
|
...spawnerSession ? { spawnerSession } : {},
|
|
2215
2558
|
...a.parent ? {
|
|
@@ -2220,7 +2563,8 @@ async function executeRun(a) {
|
|
|
2220
2563
|
worktreeDir: worktree.dir,
|
|
2221
2564
|
branch: worktree.branch,
|
|
2222
2565
|
worktreeBase: worktree.base
|
|
2223
|
-
} : {}
|
|
2566
|
+
} : {},
|
|
2567
|
+
...a.specPath ? { spec: a.specPath } : {}
|
|
2224
2568
|
};
|
|
2225
2569
|
saveStatus(logDir, status);
|
|
2226
2570
|
a.onWorkerStart?.(wid);
|
|
@@ -2231,38 +2575,10 @@ async function executeRun(a) {
|
|
|
2231
2575
|
mode: headless ? "headless" : "interactive",
|
|
2232
2576
|
model,
|
|
2233
2577
|
cwd,
|
|
2234
|
-
label
|
|
2578
|
+
label,
|
|
2579
|
+
...a.specPath ? { spec: a.specPath } : {}
|
|
2235
2580
|
});
|
|
2236
2581
|
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
2582
|
const t0 = Date.now();
|
|
2267
2583
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
2268
2584
|
env,
|
|
@@ -2363,6 +2679,7 @@ async function executeRun(a) {
|
|
|
2363
2679
|
writeEvent(e);
|
|
2364
2680
|
if (e.type === "system" && e.subtype === "init") {
|
|
2365
2681
|
if (e.session_id) status.claudeSession = e.session_id;
|
|
2682
|
+
adoptInitModel(status, e);
|
|
2366
2683
|
const cw = initContextWindow(e);
|
|
2367
2684
|
if (cw) status.contextWindow = cw;
|
|
2368
2685
|
saveStatus(logDir, status);
|
|
@@ -2436,6 +2753,7 @@ async function executeRun(a) {
|
|
|
2436
2753
|
noPane: a.noPane,
|
|
2437
2754
|
mcpFlag: a.mcpFlag,
|
|
2438
2755
|
cwd: a.cwd,
|
|
2756
|
+
specPath: a.specPath,
|
|
2439
2757
|
parent: a.parent,
|
|
2440
2758
|
stage: a.stage,
|
|
2441
2759
|
claudeArgs: a.claudeArgs,
|
|
@@ -2516,6 +2834,7 @@ async function executeCodexRun(a) {
|
|
|
2516
2834
|
rc: null,
|
|
2517
2835
|
secs: null,
|
|
2518
2836
|
origin: "spawn",
|
|
2837
|
+
outputFormat: parsed.outputFormat,
|
|
2519
2838
|
...session ? { session } : {},
|
|
2520
2839
|
...spawnerSession ? { spawnerSession } : {},
|
|
2521
2840
|
...target.provider.contextWindow ? { contextWindow: target.provider.contextWindow } : {},
|
|
@@ -2527,7 +2846,8 @@ async function executeCodexRun(a) {
|
|
|
2527
2846
|
worktreeDir: worktree.dir,
|
|
2528
2847
|
branch: worktree.branch,
|
|
2529
2848
|
worktreeBase: worktree.base
|
|
2530
|
-
} : {}
|
|
2849
|
+
} : {},
|
|
2850
|
+
...a.specPath ? { spec: a.specPath } : {}
|
|
2531
2851
|
};
|
|
2532
2852
|
saveStatus(logDir, status);
|
|
2533
2853
|
a.onWorkerStart?.(wid);
|
|
@@ -2793,5 +3113,5 @@ function resultFromOutput(out) {
|
|
|
2793
3113
|
}
|
|
2794
3114
|
|
|
2795
3115
|
//#endregion
|
|
2796
|
-
export {
|
|
2797
|
-
//# sourceMappingURL=run-
|
|
3116
|
+
export { newWorkerId as A, ageOf as C, isChatPane as D, contextPercent as E, longInlinePromptHint as F, parseRunnerArgs as I, shortText as L, saveStatus as M, setWorkerLabel as N, loadStatus as O, waitForTerminalStatus as P, describeMcp as R, UNLABELED as S, contextLabel as T, resolveSession as _, renderReport as a, appendLedger as b, handoffFromInside as c, workerDepth as d, checkPaneForWorker as f, itermForClaudeSession as g, currentTabKey as h, parseWorkerReport as i, nowStamp as j, loadStatuses as k, readInbox as l, openPaneForWorker as m, runWorker as n, discardWorker as o, openFollowPane as p, testProvider as r, mergeWorker as s, printResult as t, sayToWorker as u, sessionTag as v, alive as w, ledgerSummary as x, workerInScope as y, expandMcpNames as z };
|
|
3117
|
+
//# sourceMappingURL=run-DDdT9teG.mjs.map
|