@tekmidian/pai 0.42.5 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-CEC1a5mX.mjs} +5 -4
- package/dist/{chain-DTII7xeL.mjs.map → chain-CEC1a5mX.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-B8N1egl0.mjs → daemon-BhldpFvO.mjs} +52 -38
- package/dist/daemon-BhldpFvO.mjs.map +1 -0
- package/dist/daemon-QIvCXB5f.mjs +24 -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-BQJ8PbCK.mjs} +185 -123
- package/dist/fallback-BQJ8PbCK.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 +207 -122
- 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 +11 -0
- package/dist/hooks/whisper-reinject.mjs.map +3 -3
- 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 +7883 -169
- 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-BlKGgpWI.mjs} +7 -6
- package/dist/{planner-D5Ui7DAm.mjs.map → planner-BlKGgpWI.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-C5jRQ1kg.mjs} +8880 -5913
- package/dist/program-C5jRQ1kg.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-ByBKowN5.mjs} +980 -247
- package/dist/run-ByBKowN5.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-CAG2sUjf.mjs} +4 -3
- package/dist/{tools-LlzcV_eK.mjs → tools-DiRC5glA.mjs} +30 -233
- package/dist/tools-DiRC5glA.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-BfElduEU.mjs +231 -0
- package/dist/wakeup-BfElduEU.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 +32 -3
- package/docs/commands/audit.md +113 -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 +97 -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 +58 -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/post-tool-use/whisper-reinject.test.ts +55 -0
- package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -0
- 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/src/hooks/ztk-auto.sh +28 -0
- package/src/hooks/ztk-auto.test.ts +72 -0
- package/src/hooks/ztk-passthrough.mjs +71 -0
- 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
|
@@ -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 {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
|
-
import {
|
|
12
|
+
import { historyDir } from '../lib/pai-paths';
|
|
13
13
|
|
|
14
14
|
interface SessionData {
|
|
15
15
|
conversation_id: string;
|
|
@@ -62,7 +62,7 @@ async function main() {
|
|
|
62
62
|
const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;
|
|
63
63
|
|
|
64
64
|
// Ensure directory exists
|
|
65
|
-
const sessionDir = join(
|
|
65
|
+
const sessionDir = join(historyDir(), 'sessions', yearMonth);
|
|
66
66
|
if (!existsSync(sessionDir)) {
|
|
67
67
|
mkdirSync(sessionDir, { recursive: true });
|
|
68
68
|
}
|
|
@@ -87,7 +87,7 @@ async function main() {
|
|
|
87
87
|
|
|
88
88
|
async function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {
|
|
89
89
|
// Try to read raw outputs for this session
|
|
90
|
-
const rawOutputsDir = join(
|
|
90
|
+
const rawOutputsDir = join(historyDir(), 'raw-outputs', yearMonth);
|
|
91
91
|
|
|
92
92
|
let filesChanged: string[] = [];
|
|
93
93
|
let commandsExecuted: string[] = [];
|
|
@@ -196,7 +196,7 @@ ${info.commandsExecuted.length > 0 ? '```bash\n' + info.commandsExecuted.join('\
|
|
|
196
196
|
|
|
197
197
|
This session summary was automatically generated by the UOCS SessionEnd hook.
|
|
198
198
|
|
|
199
|
-
For detailed tool outputs, see:
|
|
199
|
+
For detailed tool outputs, see: \`${join(historyDir(), 'raw-outputs', timestamp.substring(0, 7))}/\`
|
|
200
200
|
|
|
201
201
|
---
|
|
202
202
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Setup:
|
|
15
15
|
* 1. Set environment variables in settings.json:
|
|
16
16
|
* - DA: Your AI's name (e.g., "Kai", "Nova", "Assistant")
|
|
17
|
-
* -
|
|
17
|
+
* - ADAPTER_DIR: Path to your Claude Code harness adapter dir (defaults to $HOME/.claude)
|
|
18
18
|
* 2. Ensure load-core-context.ts exists in hooks/session-start/ directory
|
|
19
19
|
* 3. Add all three SessionStart hooks to settings.json in this order:
|
|
20
20
|
* initialize-session.ts, load-core-context.ts, load-project-context.ts
|
|
@@ -24,7 +24,7 @@ import { isWorkerSession } from "../lib/worker-session.js";
|
|
|
24
24
|
import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { tmpdir } from 'os';
|
|
27
|
-
import {
|
|
27
|
+
import { ADAPTER_DIR } from '../lib/pai-paths';
|
|
28
28
|
import { sendNtfyNotification, isWhatsAppEnabled, isProbeSession } from '../lib/project-utils';
|
|
29
29
|
|
|
30
30
|
// Debounce duration in milliseconds (prevents duplicate SessionStart events)
|
|
@@ -61,7 +61,7 @@ function shouldDebounce(): boolean {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
async function testStopHook() {
|
|
64
|
-
const stopHookPath = join(
|
|
64
|
+
const stopHookPath = join(ADAPTER_DIR, 'hooks/stop-hook.ts');
|
|
65
65
|
|
|
66
66
|
console.error('\nTesting stop-hook configuration...');
|
|
67
67
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatContext, isNoiseEntry } from "./inject-observations.js";
|
|
3
|
+
|
|
4
|
+
function row(id: number, title: string, minutesAgo = 0) {
|
|
5
|
+
return {
|
|
6
|
+
id,
|
|
7
|
+
session_id: `s${id}`,
|
|
8
|
+
project_id: 1,
|
|
9
|
+
project_slug: "pai",
|
|
10
|
+
type: "discovery",
|
|
11
|
+
title,
|
|
12
|
+
narrative: null,
|
|
13
|
+
created_at: new Date(Date.now() - minutesAgo * 60_000).toISOString(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("isNoiseEntry", () => {
|
|
18
|
+
it("flags Ran: and MCP: prefixed titles", () => {
|
|
19
|
+
expect(isNoiseEntry("Ran: ls -la")).toBe(true);
|
|
20
|
+
expect(isNoiseEntry("MCP: memory_search")).toBe(true);
|
|
21
|
+
expect(isNoiseEntry("Modified foo.ts")).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("formatContext", () => {
|
|
26
|
+
it("caps the timeline at 6 entries and drops noise entries", () => {
|
|
27
|
+
const observations = [
|
|
28
|
+
...Array.from({ length: 10 }, (_, i) => row(i, `Ran: cmd ${i}`)),
|
|
29
|
+
...Array.from({ length: 8 }, (_, i) => row(100 + i, `Modified file${i}.ts`)),
|
|
30
|
+
];
|
|
31
|
+
const context = formatContext("pai", observations);
|
|
32
|
+
const timelineLines = context
|
|
33
|
+
.split("\n")
|
|
34
|
+
.filter(l => l.startsWith("- ["));
|
|
35
|
+
expect(timelineLines).toHaveLength(6);
|
|
36
|
+
expect(timelineLines.every(l => !l.includes("Ran:"))).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("falls back to unfiltered entries when fewer than 3 non-noise entries remain", () => {
|
|
40
|
+
const observations = [
|
|
41
|
+
row(1, "Ran: cmd 1"),
|
|
42
|
+
row(2, "Ran: cmd 2"),
|
|
43
|
+
row(3, "Modified file.ts"),
|
|
44
|
+
];
|
|
45
|
+
const context = formatContext("pai", observations);
|
|
46
|
+
const timelineLines = context.split("\n").filter(l => l.startsWith("- ["));
|
|
47
|
+
expect(timelineLines).toHaveLength(3);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("truncates each timeline line to 80 characters", () => {
|
|
51
|
+
const longTitle = "Modified " + "x".repeat(200) + ".ts";
|
|
52
|
+
const context = formatContext("pai", [row(1, longTitle)]);
|
|
53
|
+
const timelineLine = context.split("\n").find(l => l.startsWith("- ["))!;
|
|
54
|
+
expect(timelineLine.length).toBeLessThanOrEqual(80);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
import { connect } from 'net';
|
|
21
21
|
import { randomUUID } from 'crypto';
|
|
22
22
|
import { isProbeSession } from '../lib/project-utils.js';
|
|
23
|
+
import { isWorkerSession } from '../lib/worker-session.js';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Tuning
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const MAX_TIMELINE_ENTRIES = 6;
|
|
30
|
+
const MAX_ENTRY_LENGTH = 80;
|
|
31
|
+
const MIN_FILTERED_ENTRIES = 3;
|
|
32
|
+
const NOISE_PREFIXES = ['Ran:', 'MCP:'];
|
|
23
33
|
|
|
24
34
|
// ---------------------------------------------------------------------------
|
|
25
35
|
// Types
|
|
@@ -134,11 +144,15 @@ function truncate(s: string, maxLen: number): string {
|
|
|
134
144
|
return s.length > maxLen ? s.slice(0, maxLen - 1) + '\u2026' : s;
|
|
135
145
|
}
|
|
136
146
|
|
|
147
|
+
export function isNoiseEntry(title: string): boolean {
|
|
148
|
+
return NOISE_PREFIXES.some(prefix => title.startsWith(prefix));
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
// ---------------------------------------------------------------------------
|
|
138
152
|
// Format observations as progressive disclosure context
|
|
139
153
|
// ---------------------------------------------------------------------------
|
|
140
154
|
|
|
141
|
-
function formatContext(
|
|
155
|
+
export function formatContext(
|
|
142
156
|
projectSlug: string,
|
|
143
157
|
observations: ObservationRow[]
|
|
144
158
|
): string {
|
|
@@ -152,19 +166,22 @@ function formatContext(
|
|
|
152
166
|
const newest = new Date(observations[0].created_at);
|
|
153
167
|
const lastActivity = timeAgo(newest);
|
|
154
168
|
|
|
155
|
-
//
|
|
156
|
-
const
|
|
169
|
+
// Drop low-signal "Ran:"/"MCP:" entries, unless that leaves too few to show.
|
|
170
|
+
const filtered = observations.filter(o => !isNoiseEntry(o.title));
|
|
171
|
+
const source = filtered.length >= MIN_FILTERED_ENTRIES ? filtered : observations;
|
|
172
|
+
|
|
173
|
+
// Timeline: show at most MAX_TIMELINE_ENTRIES, each line capped to MAX_ENTRY_LENGTH.
|
|
174
|
+
const timelineObs = source.slice(0, MAX_TIMELINE_ENTRIES);
|
|
157
175
|
const timeline = timelineObs
|
|
158
176
|
.map(o => {
|
|
159
177
|
const t = timeAgo(new Date(o.created_at));
|
|
160
178
|
const label = typeLabel(o.type);
|
|
161
|
-
|
|
162
|
-
return `- [${t}] ${label} ${title}`;
|
|
179
|
+
return truncate(`- [${t}] ${label} ${o.title}`, MAX_ENTRY_LENGTH);
|
|
163
180
|
})
|
|
164
181
|
.join('\n');
|
|
165
182
|
|
|
166
|
-
const showingNote = observations.length >
|
|
167
|
-
? `(showing most recent
|
|
183
|
+
const showingNote = observations.length > timelineObs.length
|
|
184
|
+
? `(showing ${timelineObs.length} most recent of ${observations.length}, use observation_search for more)`
|
|
168
185
|
: `(showing ${observations.length} observation${observations.length !== 1 ? 's' : ''})`;
|
|
169
186
|
|
|
170
187
|
const lines: string[] = [
|
|
@@ -188,6 +205,7 @@ function formatContext(
|
|
|
188
205
|
// ---------------------------------------------------------------------------
|
|
189
206
|
|
|
190
207
|
async function main() {
|
|
208
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
191
209
|
try {
|
|
192
210
|
// Skip probe/health-check sessions
|
|
193
211
|
if (isProbeSession()) {
|
|
@@ -251,4 +269,6 @@ async function main() {
|
|
|
251
269
|
}
|
|
252
270
|
}
|
|
253
271
|
|
|
254
|
-
|
|
272
|
+
if (!process.env.VITEST) {
|
|
273
|
+
main();
|
|
274
|
+
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* - Bypass skill activation logic by directly injecting context
|
|
14
14
|
*
|
|
15
15
|
* Setup:
|
|
16
|
-
* 1. Customize your ${
|
|
16
|
+
* 1. Customize your ${ADAPTER_DIR}/skills/CORE/SKILL.md with your personal context
|
|
17
17
|
* 2. Add this hook to settings.json SessionStart hooks
|
|
18
|
-
* 3. Ensure
|
|
18
|
+
* 3. Ensure ADAPTER_DIR environment variable is set (defaults to $HOME/.claude)
|
|
19
19
|
*
|
|
20
20
|
* How it works:
|
|
21
21
|
* - Runs at the start of every Claude Code session
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { isWorkerSession } from "../lib/worker-session.js";
|
|
29
29
|
import { readFileSync, existsSync } from 'fs';
|
|
30
30
|
import { join } from 'path';
|
|
31
|
-
import {
|
|
31
|
+
import { SKILLS_DIR } from '../lib/pai-paths';
|
|
32
32
|
import { isProbeSession } from '../lib/project-utils';
|
|
33
33
|
|
|
34
34
|
async function main() {
|
|
@@ -57,7 +57,7 @@ async function main() {
|
|
|
57
57
|
// Verify CORE skill file exists
|
|
58
58
|
if (!existsSync(coreSkillPath)) {
|
|
59
59
|
console.error(`CORE skill not found at: ${coreSkillPath}`);
|
|
60
|
-
console.error(`Ensure CORE/SKILL.md exists or check
|
|
60
|
+
console.error(`Ensure CORE/SKILL.md exists or check ADAPTER_DIR environment variable`);
|
|
61
61
|
process.exit(1);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -23,8 +23,8 @@ import { homedir } from 'os';
|
|
|
23
23
|
import { execSync } from 'child_process';
|
|
24
24
|
import { buildWakeupContext } from '../../../memory/wakeup.js';
|
|
25
25
|
import { readContinueCheckpoint } from '../../../session/checkpoint-block.js';
|
|
26
|
+
import { sessionRoutingPath } from '../lib/pai-paths.js';
|
|
26
27
|
import {
|
|
27
|
-
PAI_DIR,
|
|
28
28
|
findNotesDir,
|
|
29
29
|
getProjectDir,
|
|
30
30
|
getCurrentNotePath,
|
|
@@ -62,7 +62,7 @@ function findPaiBinary(): string {
|
|
|
62
62
|
* Returns the routed Notes path if set, or null to use default behavior.
|
|
63
63
|
*/
|
|
64
64
|
function getRoutedNotesPath(): string | null {
|
|
65
|
-
const routingFile =
|
|
65
|
+
const routingFile = sessionRoutingPath();
|
|
66
66
|
if (!existsSync(routingFile)) return null;
|
|
67
67
|
|
|
68
68
|
try {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { decideMcpDeferredReminder, type ReminderHookInput } from "../lib/mcp-deferred-reminder.js";
|
|
19
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
19
20
|
|
|
20
21
|
async function main() {
|
|
22
|
+
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
21
23
|
let input: ReminderHookInput = {};
|
|
22
24
|
|
|
23
25
|
try {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* even when the full CORE skill gets truncated.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { isWorkerSession } from "../lib/worker-session.js";
|
|
14
|
+
|
|
15
|
+
if (isWorkerSession()) process.exit(0); // disposable worker: no per-session bookkeeping
|
|
16
|
+
|
|
13
17
|
const commands = `<system-reminder>
|
|
14
18
|
SESSION COMMANDS (always available):
|
|
15
19
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
// Every SessionStart hook entrypoint calls main() unconditionally at module
|
|
5
|
+
// scope and (for most of them) blocks reading fd 0 — importing them in-process
|
|
6
|
+
// would run that read against the test runner's own stdin and hang the suite.
|
|
7
|
+
// Each case below runs the entrypoint as a real subprocess with its own piped
|
|
8
|
+
// stdin and env, exactly like production invocation (see
|
|
9
|
+
// user-prompt/whisper-rules.test.ts for the same pattern).
|
|
10
|
+
describe("session-start hooks: worker sessions get no injected context", () => {
|
|
11
|
+
const HOOKS = [
|
|
12
|
+
"src/hooks/ts/session-start/initialize-session.ts",
|
|
13
|
+
"src/hooks/ts/session-start/inject-observations.ts",
|
|
14
|
+
"src/hooks/ts/session-start/load-core-context.ts",
|
|
15
|
+
"src/hooks/ts/session-start/load-project-context.ts",
|
|
16
|
+
"src/hooks/ts/session-start/mcp-deferred-reminder.ts",
|
|
17
|
+
"src/hooks/ts/session-start/post-compact-inject.ts",
|
|
18
|
+
"src/hooks/ts/session-start/session-commands.ts",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function runHook(entrypoint: string, env: NodeJS.ProcessEnv): string {
|
|
22
|
+
return execFileSync("bun", [entrypoint], {
|
|
23
|
+
input: JSON.stringify({ session_id: "t", hook_event_name: "SessionStart", source: "startup" }),
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
timeout: 15_000,
|
|
26
|
+
env: { ...process.env, ...env },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const entrypoint of HOOKS) {
|
|
31
|
+
it(`${entrypoint} prints nothing when PAI_WORKER=1`, () => {
|
|
32
|
+
const stdout = runHook(entrypoint, { PAI_WORKER: "1" });
|
|
33
|
+
expect(stdout).toBe("");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
updateTodoContinue, sessionIdFromTranscript,
|
|
17
17
|
WorkItem
|
|
18
18
|
} from '../lib/project-utils';
|
|
19
|
+
import { paiHomePath, sessionStateDir } from '../../../config/pai-files.js';
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Constants
|
|
@@ -57,20 +58,13 @@ const AUTO_SAVE_INTERVAL = (() => {
|
|
|
57
58
|
// Session-state helpers (mid-session auto-save)
|
|
58
59
|
// ---------------------------------------------------------------------------
|
|
59
60
|
|
|
60
|
-
const SESSION_STATE_DIR = join(
|
|
61
|
-
process.env.HOME ?? '/tmp',
|
|
62
|
-
'.config',
|
|
63
|
-
'pai',
|
|
64
|
-
'session-state'
|
|
65
|
-
);
|
|
66
|
-
|
|
67
61
|
interface SessionState {
|
|
68
62
|
humanMessageCount: number;
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
function readSessionState(sessionId: string): SessionState {
|
|
72
66
|
try {
|
|
73
|
-
const stateFile = join(
|
|
67
|
+
const stateFile = join(sessionStateDir(), `${sessionId}.json`);
|
|
74
68
|
if (!existsSync(stateFile)) return { humanMessageCount: 0 };
|
|
75
69
|
const raw = readFileSync(stateFile, 'utf-8');
|
|
76
70
|
const parsed = JSON.parse(raw) as Partial<SessionState>;
|
|
@@ -84,8 +78,9 @@ function readSessionState(sessionId: string): SessionState {
|
|
|
84
78
|
|
|
85
79
|
function writeSessionState(sessionId: string, state: SessionState): void {
|
|
86
80
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
const dir = paiHomePath('session-state');
|
|
82
|
+
mkdirSync(dir, { recursive: true });
|
|
83
|
+
const stateFile = join(dir, `${sessionId}.json`);
|
|
89
84
|
writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
|
|
90
85
|
} catch (e) {
|
|
91
86
|
console.error(`STOP-HOOK: Could not write session state: ${e}`);
|
|
@@ -94,7 +89,7 @@ function writeSessionState(sessionId: string, state: SessionState): void {
|
|
|
94
89
|
|
|
95
90
|
function deleteSessionState(sessionId: string): void {
|
|
96
91
|
try {
|
|
97
|
-
const stateFile = join(
|
|
92
|
+
const stateFile = join(sessionStateDir(), `${sessionId}.json`);
|
|
98
93
|
if (existsSync(stateFile)) {
|
|
99
94
|
unlinkSync(stateFile);
|
|
100
95
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
// whisper-rules.ts calls main() unconditionally at module scope (it's a hook
|
|
5
|
+
// entrypoint, always run as a subprocess) — including a blocking read of fd 0.
|
|
6
|
+
// Importing it in-process here would run that read against the test runner's
|
|
7
|
+
// own stdin and hang the suite, so every case below goes through a real
|
|
8
|
+
// subprocess with its own piped stdin, exactly like production invocation.
|
|
9
|
+
describe("user-prompt/whisper-rules entrypoint", () => {
|
|
10
|
+
const ENTRYPOINT = "src/hooks/ts/user-prompt/whisper-rules.ts";
|
|
11
|
+
|
|
12
|
+
function runHook(stdin: string): string {
|
|
13
|
+
return execFileSync("bun", [ENTRYPOINT], {
|
|
14
|
+
input: stdin,
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
timeout: 15_000,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
it("emits nothing for a worker-completion relay prompt", () => {
|
|
21
|
+
const stdout = runHook(JSON.stringify({ prompt: "[Session:x] worker y finished" }));
|
|
22
|
+
expect(stdout).toBe("");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("emits nothing for a clickr control-handover line", () => {
|
|
26
|
+
const stdout = runHook(JSON.stringify({ prompt: "your controls." }));
|
|
27
|
+
expect(stdout).toBe("");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("emits nothing for a harness [SYSTEM NOTIFICATION preamble", () => {
|
|
31
|
+
const stdout = runHook(
|
|
32
|
+
JSON.stringify({ prompt: "[SYSTEM NOTIFICATION - NOT USER INPUT]\nfoo" })
|
|
33
|
+
);
|
|
34
|
+
expect(stdout).toBe("");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("emits nothing for a background task completion carrying <task-notification>", () => {
|
|
38
|
+
const stdout = runHook(
|
|
39
|
+
JSON.stringify({ prompt: "foo <task-notification>x</task-notification>" })
|
|
40
|
+
);
|
|
41
|
+
expect(stdout).toBe("");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("still injects the system-reminder block for a normal prompt", () => {
|
|
45
|
+
const stdout = runHook(JSON.stringify({ prompt: "please fix the login bug" }));
|
|
46
|
+
expect(stdout).toContain("<system-reminder>");
|
|
47
|
+
expect(stdout).toContain("CURRENT LOCAL TIME");
|
|
48
|
+
});
|
|
49
|
+
});
|