@tekmidian/pai 0.42.4 → 0.42.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -2,27 +2,47 @@
|
|
|
2
2
|
* PAI Path Resolution - Single Source of Truth
|
|
3
3
|
*
|
|
4
4
|
* This module provides consistent path resolution across all PAI hooks.
|
|
5
|
-
* It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Two different things live here, and they must not be confused:
|
|
7
|
+
*
|
|
8
|
+
* - ADAPTER_DIR (~/.claude by default) is the Claude Code harness adapter —
|
|
9
|
+
* fixed, hardcoded paths the harness itself loads from (Hooks/, Skills/,
|
|
10
|
+
* Agents/, Commands/, settings.json, statusline-command.sh,
|
|
11
|
+
* tab-color-command.sh). It is harness-specific: a future harness would
|
|
12
|
+
* need its own adapter directory with its own conventions.
|
|
13
|
+
* - PAI_HOME (~/.claude/pai by default — see ../../../config/pai-home.ts) is
|
|
14
|
+
* where PAI's own state lives: everything hooks WRITE (History/,
|
|
15
|
+
* agent-sessions.json, session-routing.json, ...) resolves there, with a
|
|
16
|
+
* fallback to the pre-2026-09-19 ADAPTER_DIR location and a one-time
|
|
17
|
+
* stderr notice, exactly like every other PAI_HOME file.
|
|
18
|
+
*
|
|
19
|
+
* ALSO loads .env file from ADAPTER_DIR so all hooks get environment
|
|
20
|
+
* variables without relying on Claude Code's settings.json injection.
|
|
9
21
|
*
|
|
10
22
|
* Usage in hooks:
|
|
11
|
-
* import {
|
|
23
|
+
* import { ADAPTER_DIR, HOOKS_DIR, SKILLS_DIR, historyDir } from './lib/pai-paths';
|
|
12
24
|
*/
|
|
13
25
|
|
|
14
26
|
import { homedir } from 'os';
|
|
15
27
|
import { resolve, join } from 'path';
|
|
16
28
|
import { existsSync, readFileSync } from 'fs';
|
|
29
|
+
import {
|
|
30
|
+
paiHomePath,
|
|
31
|
+
resolvePaiFile,
|
|
32
|
+
migratePaiFile,
|
|
33
|
+
migratePaiDir,
|
|
34
|
+
type MigrateFileResult,
|
|
35
|
+
type MigrateDirResult,
|
|
36
|
+
} from '../../../config/pai-home.js';
|
|
17
37
|
|
|
18
38
|
/**
|
|
19
39
|
* Load .env file and inject into process.env
|
|
20
|
-
* Must run BEFORE
|
|
40
|
+
* Must run BEFORE ADAPTER_DIR resolution so .env can set ADAPTER_DIR/PAI_DIR if needed
|
|
21
41
|
*/
|
|
22
42
|
function loadEnvFile(): void {
|
|
23
43
|
// Check common locations for .env
|
|
24
44
|
const possiblePaths = [
|
|
25
|
-
resolve(process.env.PAI_DIR || '', '.env'),
|
|
45
|
+
resolve(process.env.ADAPTER_DIR || process.env.PAI_DIR || '', '.env'),
|
|
26
46
|
resolve(homedir(), '.claude', '.env'),
|
|
27
47
|
];
|
|
28
48
|
|
|
@@ -68,40 +88,102 @@ function loadEnvFile(): void {
|
|
|
68
88
|
// Load .env FIRST, before any other initialization
|
|
69
89
|
loadEnvFile();
|
|
70
90
|
|
|
91
|
+
function defaultAdapterDir(): string {
|
|
92
|
+
return resolve(homedir(), '.claude');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Never print when stdout/stderr is a data channel, not a human: hook
|
|
97
|
+
* bundles and worker-status-line.mjs set PAI_QUIET_NOTICES=1 via their
|
|
98
|
+
* esbuild banner (scripts/build-hooks.mjs), and a `pai worker run
|
|
99
|
+
* --output-format json` invocation is detected directly off argv since its
|
|
100
|
+
* env can't be set before this module's static imports resolve.
|
|
101
|
+
*/
|
|
102
|
+
function suppressDeprecationNotices(): boolean {
|
|
103
|
+
if (process.env.PAI_QUIET_NOTICES === '1') return true;
|
|
104
|
+
const idx = process.argv.indexOf('--output-format');
|
|
105
|
+
return idx !== -1 && process.argv[idx + 1] === 'json';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* At most once per process — a `globalThis` flag rather than a module-level
|
|
110
|
+
* `let` because hooks and the CLI can end up with more than one instance of
|
|
111
|
+
* this module loaded into the same process (separate bundles), each with its
|
|
112
|
+
* own module scope; only a property on the shared global survives that.
|
|
113
|
+
*/
|
|
114
|
+
function warnPaiDirDeprecatedOnce(message: string): void {
|
|
115
|
+
const g = globalThis as typeof globalThis & { __paiDirNoticePrinted?: boolean };
|
|
116
|
+
if (g.__paiDirNoticePrinted || suppressDeprecationNotices()) return;
|
|
117
|
+
g.__paiDirNoticePrinted = true;
|
|
118
|
+
process.stderr.write(`pai: ${message}\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
71
121
|
/**
|
|
72
|
-
* Smart
|
|
122
|
+
* Smart ADAPTER_DIR detection with fallback
|
|
73
123
|
* Priority:
|
|
74
|
-
* 1.
|
|
75
|
-
*
|
|
124
|
+
* 1. ADAPTER_DIR environment variable (if set) — warns once if PAI_DIR is
|
|
125
|
+
* ALSO set and resolves to a different path (naming both).
|
|
126
|
+
* 2. PAI_DIR environment variable (deprecated alias, one release) — warns
|
|
127
|
+
* once only when it differs from the default adapter root; PAI_DIR set to
|
|
128
|
+
* the same value as the default is the operator's current, correct
|
|
129
|
+
* setting and stays silent.
|
|
130
|
+
* 3. ~/.claude (standard location)
|
|
76
131
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
function resolveAdapterDir(): string {
|
|
133
|
+
if (process.env.ADAPTER_DIR) {
|
|
134
|
+
const adapterDir = resolve(process.env.ADAPTER_DIR);
|
|
135
|
+
if (process.env.PAI_DIR) {
|
|
136
|
+
const paiDir = resolve(process.env.PAI_DIR);
|
|
137
|
+
if (paiDir !== adapterDir) {
|
|
138
|
+
warnPaiDirDeprecatedOnce(
|
|
139
|
+
`ADAPTER_DIR (${adapterDir}) and PAI_DIR (${paiDir}) are both set and differ — ADAPTER_DIR wins. PAI_DIR still works for one release.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return adapterDir;
|
|
144
|
+
}
|
|
145
|
+
if (process.env.PAI_DIR) {
|
|
146
|
+
const paiDir = resolve(process.env.PAI_DIR);
|
|
147
|
+
if (paiDir !== defaultAdapterDir()) {
|
|
148
|
+
warnPaiDirDeprecatedOnce(
|
|
149
|
+
'PAI_DIR is deprecated — use ADAPTER_DIR for the harness adapter root (~/.claude). PAI_DIR still works for one release.'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return paiDir;
|
|
153
|
+
}
|
|
154
|
+
return defaultAdapterDir();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The Claude Code harness adapter root — NOT PAI's state home. See module doc above. */
|
|
158
|
+
export const ADAPTER_DIR = resolveAdapterDir();
|
|
159
|
+
|
|
160
|
+
/** @deprecated alias for ADAPTER_DIR, kept for one release. Prefer ADAPTER_DIR. */
|
|
161
|
+
export const PAI_DIR = ADAPTER_DIR;
|
|
80
162
|
|
|
81
163
|
/**
|
|
82
|
-
*
|
|
164
|
+
* Adapter directories — fixed paths the Claude Code harness itself loads
|
|
165
|
+
* from. These stay under ADAPTER_DIR; they are not PAI state.
|
|
83
166
|
*/
|
|
84
|
-
export const HOOKS_DIR = join(
|
|
85
|
-
export const SKILLS_DIR = join(
|
|
86
|
-
export const AGENTS_DIR = join(
|
|
87
|
-
export const
|
|
88
|
-
export const COMMANDS_DIR = join(PAI_DIR, 'Commands');
|
|
167
|
+
export const HOOKS_DIR = join(ADAPTER_DIR, 'Hooks');
|
|
168
|
+
export const SKILLS_DIR = join(ADAPTER_DIR, 'Skills');
|
|
169
|
+
export const AGENTS_DIR = join(ADAPTER_DIR, 'Agents');
|
|
170
|
+
export const COMMANDS_DIR = join(ADAPTER_DIR, 'Commands');
|
|
89
171
|
|
|
90
172
|
/**
|
|
91
173
|
* Validate PAI directory structure on first import
|
|
92
174
|
* This fails fast with a clear error if PAI is misconfigured
|
|
93
175
|
*/
|
|
94
176
|
function validatePAIStructure(): void {
|
|
95
|
-
if (!existsSync(
|
|
96
|
-
console.error(`
|
|
97
|
-
console.error(` Expected ~/.claude or set
|
|
177
|
+
if (!existsSync(ADAPTER_DIR)) {
|
|
178
|
+
console.error(`ADAPTER_DIR does not exist: ${ADAPTER_DIR}`);
|
|
179
|
+
console.error(` Expected ~/.claude or set ADAPTER_DIR environment variable`);
|
|
98
180
|
process.exit(1);
|
|
99
181
|
}
|
|
100
182
|
|
|
101
183
|
if (!existsSync(HOOKS_DIR)) {
|
|
102
184
|
console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
|
|
103
|
-
console.error(` Your
|
|
104
|
-
console.error(` Current
|
|
185
|
+
console.error(` Your ADAPTER_DIR may be misconfigured`);
|
|
186
|
+
console.error(` Current ADAPTER_DIR: ${ADAPTER_DIR}`);
|
|
105
187
|
process.exit(1);
|
|
106
188
|
}
|
|
107
189
|
}
|
|
@@ -110,6 +192,68 @@ function validatePAIStructure(): void {
|
|
|
110
192
|
// This ensures any hook that imports this module will fail fast if paths are wrong
|
|
111
193
|
validatePAIStructure();
|
|
112
194
|
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// PAI state written by hooks — resolves under PAI_HOME, falling back to the
|
|
197
|
+
// pre-2026-09-19 ADAPTER_DIR location (one-time stderr notice) until
|
|
198
|
+
// `pai config migrate --history` moves it. Actively written on every hook
|
|
199
|
+
// event across every session, so unlike most PAI_HOME files the live move is
|
|
200
|
+
// deliberately NOT automatic — see `pai config migrate --history`.
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function oldHistoryDir(): string {
|
|
204
|
+
return join(ADAPTER_DIR, 'History');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Read/write location for hook-captured history: PAI_HOME/History if
|
|
208
|
+
* present, else the old ADAPTER_DIR/History (one-time stderr notice). */
|
|
209
|
+
export function historyDir(): string {
|
|
210
|
+
return resolvePaiFile(paiHomePath('History'), [oldHistoryDir()], 'pai config migrate --history');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {
|
|
214
|
+
return migratePaiDir(paiHomePath('History'), [oldHistoryDir()], opts);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function oldAgentSessionsPath(): string {
|
|
218
|
+
return join(ADAPTER_DIR, 'agent-sessions.json');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function agentSessionsPath(): string {
|
|
222
|
+
return resolvePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], 'pai config migrate --history');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {
|
|
226
|
+
return migratePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], opts);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function oldSecurityEventsPath(): string {
|
|
230
|
+
return join(ADAPTER_DIR, 'history', 'security', 'security-events.jsonl');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function securityEventsPath(): string {
|
|
234
|
+
return resolvePaiFile(
|
|
235
|
+
paiHomePath('History', 'security', 'security-events.jsonl'),
|
|
236
|
+
[oldSecurityEventsPath()],
|
|
237
|
+
'pai config migrate --history'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {
|
|
242
|
+
return migratePaiFile(paiHomePath('History', 'security', 'security-events.jsonl'), [oldSecurityEventsPath()], opts);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function oldSessionRoutingPath(): string {
|
|
246
|
+
return join(ADAPTER_DIR, 'session-routing.json');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function sessionRoutingPath(): string {
|
|
250
|
+
return resolvePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], 'pai config migrate --history');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {
|
|
254
|
+
return migratePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], opts);
|
|
255
|
+
}
|
|
256
|
+
|
|
113
257
|
/**
|
|
114
258
|
* Helper to get history file path with date-based organization
|
|
115
259
|
*/
|
|
@@ -120,5 +264,5 @@ export function getHistoryFilePath(subdir: string, filename: string): string {
|
|
|
120
264
|
const year = localDate.getFullYear();
|
|
121
265
|
const month = String(localDate.getMonth() + 1).padStart(2, '0');
|
|
122
266
|
|
|
123
|
-
return join(
|
|
267
|
+
return join(historyDir(), subdir, `${year}-${month}`, filename);
|
|
124
268
|
}
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, linkSync, copyFileSync } from 'fs';
|
|
6
6
|
import { join, basename } from 'path';
|
|
7
|
-
import { PAI_DIR } from '../pai-paths.js';
|
|
7
|
+
import { ADAPTER_DIR, PAI_DIR } from '../pai-paths.js';
|
|
8
8
|
|
|
9
|
-
// Re-export
|
|
10
|
-
|
|
11
|
-
export
|
|
9
|
+
// Re-export for consumers. PROJECTS_DIR is a harness-adjacent data dir, not
|
|
10
|
+
// PAI_HOME state — out of scope for the PAI_DIR→PAI_HOME fold (see pai-paths.ts).
|
|
11
|
+
export { ADAPTER_DIR, PAI_DIR };
|
|
12
|
+
export const PROJECTS_DIR = join(ADAPTER_DIR, 'projects');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Directories known to be automated health-check / probe sessions.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
|
-
import {
|
|
12
|
+
import { historyDir } from '../lib/pai-paths';
|
|
13
13
|
|
|
14
14
|
interface ToolUseData {
|
|
15
15
|
tool_name: string;
|
|
@@ -20,7 +20,7 @@ interface ToolUseData {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Configuration
|
|
23
|
-
const CAPTURE_DIR = join(
|
|
23
|
+
const CAPTURE_DIR = join(historyDir(), 'raw-outputs');
|
|
24
24
|
const INTERESTING_TOOLS = ['Bash', 'Edit', 'Write', 'Read', 'Task', 'NotebookEdit'];
|
|
25
25
|
|
|
26
26
|
async function main() {
|
|
@@ -707,7 +707,7 @@ async function main() {
|
|
|
707
707
|
// BUG (live): a production digest was found with NO "HANDOVER SOURCE:"
|
|
708
708
|
// line at all — its cause was never pinned down for certain (the
|
|
709
709
|
// deployed hook is a compiled dist/ artifact reached through
|
|
710
|
-
// ${
|
|
710
|
+
// ${ADAPTER_DIR}/Hooks/..., a separate build step from editing this
|
|
711
711
|
// source, which is itself a plausible way for "works when I run the
|
|
712
712
|
// .ts source" and "missing in production" to diverge). Regardless of
|
|
713
713
|
// cause: sourceLabel and handoverBlock are now computed in their own
|
|
@@ -1,51 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* route-agents-to-worker.ts — PreToolUse hook on the
|
|
4
|
+
* route-agents-to-worker.ts — PreToolUse hook on the in-process subagent tool.
|
|
5
5
|
*
|
|
6
|
-
* Denies every in-process subagent
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Denies every in-process subagent, in every project, so that delegated work
|
|
7
|
+
* always goes through `pai worker run` and therefore shows up in
|
|
8
|
+
* `pai worker ps`, in the status line and in the worker log dir. The deny
|
|
9
|
+
* reason names the replacement command, `--provider anthropic` included, so
|
|
10
|
+
* there is never a reason to reach for the built-in tool.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - workers are off or no provider is configured
|
|
12
|
+
* This file is only I/O: read stdin, ask lib/agent-gate for the decision,
|
|
13
|
+
* ledger it, print it. The decision itself lives in lib/agent-gate.ts and is
|
|
14
|
+
* tested there.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
16
|
+
* Registered in ~/.claude/settings.json under a PreToolUse matcher that covers
|
|
17
|
+
* both tool names ("Agent|Task") — see src/workers/install.ts, which owns that
|
|
18
|
+
* registration.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
|
-
import { readWorkersSection
|
|
21
|
+
import { readWorkersSection } from "../../../workers/config.js";
|
|
21
22
|
import { workersLogDir, ledgerPath } from "../../../workers/paths.js";
|
|
22
23
|
import { appendLedger } from "../../../workers/ledger.js";
|
|
24
|
+
import {
|
|
25
|
+
decideAgentGate,
|
|
26
|
+
renderDecision,
|
|
27
|
+
shortLabel,
|
|
28
|
+
type AgentGateInput,
|
|
29
|
+
} from "../lib/agent-gate.js";
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function deny(reason: string): string {
|
|
34
|
-
return JSON.stringify({
|
|
35
|
-
hookSpecificOutput: {
|
|
36
|
-
hookEventName: "PreToolUse",
|
|
37
|
-
permissionDecision: "deny",
|
|
38
|
-
permissionDecisionReason: reason,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function shortLabel(input: HookInput): string {
|
|
44
|
-
const desc =
|
|
45
|
-
typeof input.tool_input === "object" && input.tool_input !== null
|
|
46
|
-
? input.tool_input.description ?? ""
|
|
47
|
-
: "";
|
|
48
|
-
return desc.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
31
|
+
/**
|
|
32
|
+
* What to print when the hook cannot read or parse its own input. This hook is
|
|
33
|
+
* matcher-scoped to the subagent tool, so anything that reaches it *is* a
|
|
34
|
+
* subagent launch: a broken event denies rather than allows, and the bypass
|
|
35
|
+
* env var is still honoured. A gate that opens whenever it stumbles is the
|
|
36
|
+
* failure this hook exists to prevent.
|
|
37
|
+
*/
|
|
38
|
+
function fallback(): string {
|
|
39
|
+
return renderDecision(decideAgentGate({}, process.env));
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
async function main(): Promise<void> {
|
|
@@ -53,80 +44,36 @@ async function main(): Promise<void> {
|
|
|
53
44
|
try {
|
|
54
45
|
for await (const chunk of process.stdin) text += chunk;
|
|
55
46
|
} catch {
|
|
56
|
-
process.stdout.write(
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (!text.trim()) {
|
|
60
|
-
process.stdout.write(ALLOW);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let input: HookInput;
|
|
65
|
-
try {
|
|
66
|
-
input = JSON.parse(text) as HookInput;
|
|
67
|
-
} catch {
|
|
68
|
-
process.stdout.write(ALLOW);
|
|
47
|
+
process.stdout.write(fallback());
|
|
69
48
|
return;
|
|
70
49
|
}
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
51
|
+
let input: AgentGateInput = {};
|
|
52
|
+
if (text.trim()) {
|
|
53
|
+
try {
|
|
54
|
+
input = JSON.parse(text) as AgentGateInput;
|
|
55
|
+
} catch {
|
|
56
|
+
// A malformed event on a matcher-scoped hook is still a subagent launch;
|
|
57
|
+
// an empty input denies, which is the safe direction here.
|
|
58
|
+
input = {};
|
|
59
|
+
}
|
|
75
60
|
}
|
|
76
61
|
|
|
77
|
-
const
|
|
78
|
-
const cwd = input.cwd ?? process.cwd();
|
|
62
|
+
const decision = decideAgentGate(input, process.env);
|
|
79
63
|
|
|
80
|
-
|
|
64
|
+
if (decision.ledger) {
|
|
65
|
+
const label = shortLabel(input);
|
|
81
66
|
try {
|
|
82
|
-
appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)),
|
|
83
|
-
cwd,
|
|
67
|
+
appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)), decision.ledger, {
|
|
68
|
+
cwd: input.cwd ?? process.cwd(),
|
|
84
69
|
...(label ? { label } : {}),
|
|
85
70
|
});
|
|
86
71
|
} catch {
|
|
87
|
-
// no config / no log dir yet — the decision
|
|
72
|
+
// no config / no log dir yet — the decision still stands
|
|
88
73
|
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (process.env.ALLOW_ANTHROPIC_AGENTS === "1") {
|
|
92
|
-
ledger("ALLOWED-ANTHROPIC-AGENT");
|
|
93
|
-
process.stdout.write(ALLOW);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let workers: ReturnType<typeof readWorkersSection>["workers"];
|
|
98
|
-
try {
|
|
99
|
-
workers = readWorkersSection().workers;
|
|
100
|
-
} catch {
|
|
101
|
-
// broken workers config must not break the session
|
|
102
|
-
process.stdout.write(ALLOW);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (
|
|
106
|
-
!workers.enabled ||
|
|
107
|
-
Object.keys(workers.providers).length === 0 ||
|
|
108
|
-
workers.active === ANTHROPIC_NATIVE
|
|
109
|
-
) {
|
|
110
|
-
process.stdout.write(ALLOW);
|
|
111
|
-
return;
|
|
112
74
|
}
|
|
113
75
|
|
|
114
|
-
|
|
115
|
-
process.stdout.write(
|
|
116
|
-
deny(
|
|
117
|
-
"Agent tool is disabled: subagents run on the configured worker provider, not Anthropic. " +
|
|
118
|
-
"Delegate with Bash instead, in the background:\n\n" +
|
|
119
|
-
`pai worker run --label "${label || "task"}" --class research -p '<full, self-contained task spec>' ` +
|
|
120
|
-
"--allowedTools 'Read,Edit,Write,Bash,Grep,Glob' --output-format json\n\n" +
|
|
121
|
-
"- Run it with run_in_background: true and always with a timeout.\n" +
|
|
122
|
-
"- Use --class spotcheck (or implement) as the task demands.\n" +
|
|
123
|
-
"- Web research: add WebSearch,WebFetch to --allowedTools.\n" +
|
|
124
|
-
"- The answer is in the `result` field of the JSON it prints. Review the diff yourself.\n" +
|
|
125
|
-
"- pai worker ps lists running workers; pai worker follow <id> shows one live.\n" +
|
|
126
|
-
"Keep only orchestration, review and synthesis in this session. " +
|
|
127
|
-
"To run subagents on Anthropic for one session: start it with ALLOW_ANTHROPIC_AGENTS=1."
|
|
128
|
-
)
|
|
129
|
-
);
|
|
76
|
+
process.stdout.write(renderDecision(decision));
|
|
130
77
|
}
|
|
131
78
|
|
|
132
|
-
main().catch(() => process.stdout.write(
|
|
79
|
+
main().catch(() => process.stdout.write(fallback()));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// The entry does its own stdin read and calls main() unconditionally at
|
|
6
|
+
// module scope — importing it in-process would run that read against the
|
|
7
|
+
// test runner's own stdin. Run it as a real subprocess with its own piped
|
|
8
|
+
// stdin, exactly like session-start/session-start-worker-guard.test.ts.
|
|
9
|
+
//
|
|
10
|
+
// Uses this checkout as the git work tree (it already has a .git dir), not a
|
|
11
|
+
// tmp-created one: a repo under os.tmpdir() would itself match the hook's
|
|
12
|
+
// /tmp exemption and mask the "inside a git work tree" case being tested.
|
|
13
|
+
describe("route-edits-to-worker", () => {
|
|
14
|
+
const ENTRYPOINT = "src/hooks/ts/pre-tool-use/route-edits-to-worker.ts";
|
|
15
|
+
const repo = process.cwd();
|
|
16
|
+
|
|
17
|
+
function runHook(filePath: string, env: NodeJS.ProcessEnv = {}): string {
|
|
18
|
+
// This test may itself run as a worker (PAI_WORKER=1 in its own env),
|
|
19
|
+
// which must not leak into the child and mask the case under test.
|
|
20
|
+
const { PAI_WORKER: _drop, ...baseEnv } = process.env;
|
|
21
|
+
return execFileSync("bun", [ENTRYPOINT], {
|
|
22
|
+
input: JSON.stringify({
|
|
23
|
+
cwd: repo,
|
|
24
|
+
tool_name: "Edit",
|
|
25
|
+
tool_input: { file_path: filePath, old_string: "a", new_string: "b" },
|
|
26
|
+
}),
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
timeout: 15_000,
|
|
29
|
+
env: { ...baseEnv, ...env },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it("allows a Notes/TODO.md edit (prints nothing)", () => {
|
|
34
|
+
expect(runHook(join(repo, "Notes/TODO.md"))).toBe("");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("blocks a source file edit with the standing message", () => {
|
|
38
|
+
const out = runHook(join(repo, "src/workers/run.ts"));
|
|
39
|
+
const parsed = JSON.parse(out);
|
|
40
|
+
expect(parsed.decision).toBe("block");
|
|
41
|
+
expect(parsed.reason).toContain("main session does not edit code");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("allows a source file edit when PAI_WORKER=1", () => {
|
|
45
|
+
expect(runHook(join(repo, "src/workers/run.ts"), { PAI_WORKER: "1" })).toBe("");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* route-edits-to-worker.ts — PreToolUse (Edit|MultiEdit|Write)
|
|
5
|
+
*
|
|
6
|
+
* The outer orchestrator session does not edit code files; code changes go
|
|
7
|
+
* through `pai worker run`. Allowed through silently:
|
|
8
|
+
* - worker sessions (the runner sets PAI_WORKER=1)
|
|
9
|
+
* - paths outside any git work tree
|
|
10
|
+
* - the exempt scratch prefixes (.claude incl. memory dirs, the local
|
|
11
|
+
* notes/tasks/scratchpad dirs, the OS temp dir)
|
|
12
|
+
* - prose: notes, session logs, handovers — see lib/edit-gate.ts isProseEdit
|
|
13
|
+
* Everything else inside a git work tree is blocked with the worker command.
|
|
14
|
+
*
|
|
15
|
+
* This file is only I/O: read stdin, resolve the path, ask lib/edit-gate for
|
|
16
|
+
* the decision, print it. The decision itself lives in lib/edit-gate.ts and
|
|
17
|
+
* is tested there.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
22
|
+
import { homedir, tmpdir } from "node:os";
|
|
23
|
+
import { decideEditGate, type EditGateContext } from "../lib/edit-gate.js";
|
|
24
|
+
|
|
25
|
+
function isUnder(p: string, prefix: string): boolean {
|
|
26
|
+
return p === prefix || p.startsWith(prefix + sep);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A .git entry in any ancestor marks a git work tree (.git is a directory in
|
|
30
|
+
// a normal repo, a file in a linked worktree — existsSync covers both).
|
|
31
|
+
function inGitWorkTree(p: string): boolean {
|
|
32
|
+
let dir = resolve(p);
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (existsSync(join(dir, ".git"))) return true;
|
|
35
|
+
const parent = dirname(dir);
|
|
36
|
+
if (parent === dir) return false;
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function exemptPrefixes(): string[] {
|
|
42
|
+
const home = homedir();
|
|
43
|
+
return [
|
|
44
|
+
join(home, ".claude"),
|
|
45
|
+
join(home, "colibri", "Notes"),
|
|
46
|
+
join(home, "colibri", "tasks"),
|
|
47
|
+
join(home, "colibri", "scratchpad"),
|
|
48
|
+
tmpdir(),
|
|
49
|
+
"/tmp",
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function allow(): void {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function block(reason: string): void {
|
|
58
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main(): Promise<void> {
|
|
63
|
+
let text = "";
|
|
64
|
+
try {
|
|
65
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
66
|
+
} catch {
|
|
67
|
+
return allow();
|
|
68
|
+
}
|
|
69
|
+
if (!text.trim()) return allow();
|
|
70
|
+
|
|
71
|
+
let input: { cwd?: string; tool_input?: { file_path?: string } };
|
|
72
|
+
try {
|
|
73
|
+
input = JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
return allow();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const filePath =
|
|
79
|
+
input.tool_input && typeof input.tool_input.file_path === "string" ? input.tool_input.file_path : "";
|
|
80
|
+
if (!filePath) return allow();
|
|
81
|
+
|
|
82
|
+
const abs = resolve(input.cwd ?? process.cwd(), filePath);
|
|
83
|
+
|
|
84
|
+
const ctx: EditGateContext = {
|
|
85
|
+
filePath: abs,
|
|
86
|
+
isPaiWorker: process.env.PAI_WORKER === "1",
|
|
87
|
+
isExemptPrefix: exemptPrefixes().some((p) => isUnder(abs, p)),
|
|
88
|
+
isGitWorkTree: inGitWorkTree(abs),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const decision = decideEditGate(ctx);
|
|
92
|
+
if (decision.decision === "block") return block(decision.reason);
|
|
93
|
+
return allow();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch(() => process.exit(0));
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
21
21
|
import { dirname } from 'path';
|
|
22
|
+
import { securityEventsPath } from '../lib/pai-paths.js';
|
|
22
23
|
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// ATTACK PATTERNS - CUSTOMIZE THESE FOR YOUR ENVIRONMENT
|
|
@@ -130,7 +131,7 @@ function detectAttack(content: string): DetectionResult {
|
|
|
130
131
|
// ============================================================================
|
|
131
132
|
|
|
132
133
|
function logSecurityEvent(event: Record<string, unknown>): void {
|
|
133
|
-
const logPath =
|
|
134
|
+
const logPath = securityEventsPath();
|
|
134
135
|
const entry = JSON.stringify({ timestamp: new Date().toISOString(), ...event }) + '\n';
|
|
135
136
|
|
|
136
137
|
try {
|