mixdog 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +2 -2
  3. package/package.json +2 -11
  4. package/scripts/daemon-stub.mjs +1 -1
  5. package/scripts/gate-local.mjs +4 -2
  6. package/scripts/native-release-status.mjs +2 -2
  7. package/scripts/recall-history-cases.json +1 -1
  8. package/scripts/suite-health.mjs +10 -1
  9. package/scripts/test.mjs +7 -11
  10. package/src/agents/reviewer/AGENT.md +7 -11
  11. package/src/agents/reviewer/agent.json +1 -1
  12. package/src/defaults/shipped-history.json +17 -1
  13. package/src/defaults/skills/browser-use/SKILL.md +59 -11
  14. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  15. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  16. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  17. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  18. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  19. package/src/defaults/skills/setup/SKILL.md +22 -0
  20. package/src/defaults/skills/setup/references/actions.md +39 -5
  21. package/src/defaults/skills/setup/references/surfaces.md +11 -2
  22. package/src/defaults/skills/visualize/SKILL.md +2 -1
  23. package/src/lib/rules-builder.cjs +10 -8
  24. package/src/rules/agent/AGENT.md +6 -6
  25. package/src/rules/lead/LEAD.md +18 -20
  26. package/src/rules/shared/05-tool-calls.md +5 -6
  27. package/src/rules/shared/10-tool-workflow.md +2 -0
  28. package/src/rules/shared/60-verification.md +7 -5
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/config.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  35. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  36. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
  39. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
  40. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
  41. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  42. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  45. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
  46. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  47. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  48. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  49. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
  50. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  52. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
  53. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  54. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  55. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  56. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  57. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  58. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  64. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  65. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  68. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  69. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  72. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  76. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  77. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
  78. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  79. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
  82. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
  83. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  84. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  86. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
  88. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  90. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  92. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
  93. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  94. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  97. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  99. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  101. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  102. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  103. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  104. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  105. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  106. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  107. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  109. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  110. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  112. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  113. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  114. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  115. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  116. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  117. package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
  118. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  120. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  121. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  122. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  123. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  124. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  125. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  126. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  127. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
  128. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  129. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  130. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  131. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  132. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  133. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  135. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
  136. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  138. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  141. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  143. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  144. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  145. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  146. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  147. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  148. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  149. package/src/runtime/browser-bridge/client.mjs +73 -8
  150. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  151. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  152. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  153. package/src/runtime/channels/lib/config.mjs +6 -61
  154. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  155. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  156. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  157. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  158. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  159. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  160. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  161. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  162. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  163. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  164. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  165. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  166. package/src/runtime/channels/lib/scheduler.mjs +33 -147
  167. package/src/runtime/channels/lib/schedules-db.mjs +34 -11
  168. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  169. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
  170. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  171. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  172. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  173. package/src/runtime/channels/lib/worker-main.mjs +10 -34
  174. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  175. package/src/runtime/computer-bridge/actions.mjs +9 -1
  176. package/src/runtime/computer-bridge/client.mjs +180 -8
  177. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  178. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  179. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  180. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  181. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  182. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  183. package/src/runtime/connectors/google-mcp.mjs +10 -12
  184. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  185. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  186. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  187. package/src/runtime/session-search/session-search-db.mjs +73 -9
  188. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  189. package/src/runtime/shared/abort-race.mjs +89 -0
  190. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  191. package/src/runtime/shared/agent-kind.mjs +4 -3
  192. package/src/runtime/shared/bounded-download.mjs +8 -3
  193. package/src/runtime/shared/config.mjs +2 -13
  194. package/src/runtime/shared/connector-accounts.mjs +20 -10
  195. package/src/runtime/shared/connector-catalog.d.mts +29 -0
  196. package/src/runtime/shared/connector-catalog.mjs +150 -0
  197. package/src/runtime/shared/connector-triggers.mjs +22 -19
  198. package/src/runtime/shared/defaults-separation.mjs +2 -14
  199. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  200. package/src/runtime/shared/err-text.mjs +24 -15
  201. package/src/runtime/shared/file-permissions.d.mts +0 -8
  202. package/src/runtime/shared/file-permissions.mjs +0 -11
  203. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  204. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  205. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  206. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  207. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  208. package/src/runtime/shared/mixy-presence.mjs +22 -0
  209. package/src/runtime/shared/orchestration.d.mts +2 -0
  210. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  211. package/src/runtime/shared/profile-config.cjs +1 -1
  212. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  213. package/src/runtime/shared/prompt-origin.mjs +21 -0
  214. package/src/runtime/shared/provider-accounts.mjs +7 -1
  215. package/src/runtime/shared/provider-display-name.mjs +29 -0
  216. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  217. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  218. package/src/runtime/shared/routine-trigger.mjs +10 -0
  219. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  220. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  221. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  222. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  223. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  224. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  225. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  226. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  227. package/src/runtime/terminal-bridge/client.mjs +10 -1
  228. package/src/runtime/visualize-bridge/client.mjs +27 -8
  229. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  230. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  231. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  232. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  233. package/src/session-runtime/agent-home-delivery.mjs +448 -185
  234. package/src/session-runtime/agent-task-sessions.mjs +329 -180
  235. package/src/session-runtime/boot/apis.mjs +9 -0
  236. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  237. package/src/session-runtime/boot/providers.mjs +3 -0
  238. package/src/session-runtime/boot/tools.mjs +6 -1
  239. package/src/session-runtime/config-lifecycle.mjs +6 -0
  240. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  241. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  242. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  243. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  244. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  245. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  246. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  247. package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
  248. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  249. package/src/session-runtime/mixy-heartbeat.mjs +153 -33
  250. package/src/session-runtime/notification-bus.mjs +31 -117
  251. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  252. package/src/session-runtime/notification-listeners.mjs +76 -0
  253. package/src/session-runtime/owned-automation-run.mjs +2 -1
  254. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  255. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  256. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  257. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  258. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  259. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  260. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  261. package/src/session-runtime/routine-owner.mjs +40 -9
  262. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  263. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  264. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  265. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  266. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  267. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  268. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  269. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  270. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  271. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  272. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  273. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  274. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  275. package/src/session-runtime/services/channel-admin.mjs +7 -13
  276. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  277. package/src/session-runtime/services/seeds.mjs +0 -28
  278. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  279. package/src/session-runtime/session-create/session-options.mjs +1 -0
  280. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  281. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  282. package/src/session-runtime/settings-system-api.mjs +13 -1
  283. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  284. package/src/session-runtime/setup-tool/executor.mjs +145 -22
  285. package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
  286. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  287. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
  288. package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
  289. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  290. package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
  291. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  292. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  293. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  294. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  295. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  296. package/src/standalone/channel-transport.mjs +1 -1
  297. package/src/standalone/daemon-channels-loader.mjs +10 -8
  298. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  299. package/src/standalone/daemon-session-control/request.mjs +51 -0
  300. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  301. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  302. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  303. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  304. package/src/standalone/daemon-session-control.mjs +124 -188
  305. package/src/standalone/daemon.mjs +26 -3
  306. package/src/standalone/session-client.mjs +7 -3
  307. package/src/standalone/session-protocol.mjs +5 -0
  308. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  309. package/src/standalone/session-service/quota-resume.mjs +3 -0
  310. package/src/standalone/session-service/retention/busy.mjs +65 -1
  311. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  312. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  313. package/src/standalone/session-tool-output.mjs +15 -7
  314. package/src/tui/app/model-options.mjs +3 -28
  315. package/src/tui/dist/index.mjs +58 -64
  316. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  317. package/src/tui/session/initial-state.mjs +3 -0
  318. package/src/tui/session/notice-surface.mjs +7 -1
  319. package/src/tui/session/queue-helpers.mjs +3 -42
  320. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  321. package/src/tui/session/session-api/routes-write.mjs +5 -1
  322. package/src/tui/session/session-api/routes.mjs +1 -0
  323. package/src/tui/session/session-api/settings.mjs +17 -1
  324. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  325. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  326. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  327. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  328. package/src/tui/session/session-flow/drain.mjs +118 -149
  329. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  330. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  331. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  332. package/src/tui/session/tool-approval-request.mjs +39 -0
  333. package/src/tui/session/tool-approval-settle.mjs +62 -0
  334. package/src/tui/session/tool-approval.mjs +69 -115
  335. package/src/tui/session/tui-steering-persist.mjs +8 -1
  336. package/src/tui/session/turn-stream.mjs +19 -2
  337. package/src/tui/session/turn.mjs +1 -0
  338. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  339. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  340. package/scripts/code-graph-description-contract.mjs +0 -188
  341. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -0,0 +1,108 @@
1
+ // Thinking replay is bound to the request prefix that produced it.
2
+ //
3
+ // An Anthropic thinking block's signature records the conversation prefix it
4
+ // was generated under: the top-level system prompt, the tool set, and every
5
+ // earlier message. A request whose system or tools changed since then carries
6
+ // blocks the API drops before the model sees them (unbilled, so the provider's
7
+ // count no longer includes them) or, where the check is enforced, refuses with
8
+ // a 400. The drop also takes every LATER thinking block, so a transcript that
9
+ // keeps the stale blocks loses the reasoning of every turn that follows them.
10
+ //
11
+ // Where the runtime itself changes that prefix — a heir carrying a conversation
12
+ // into a prompt built for it, a folded or migrated prompt — the stored thinking
13
+ // can never be replayed again. It is stripped once, at that boundary, the
14
+ // documented recovery: each turn keeps its text and tool calls, and the next
15
+ // turns' thinking binds to the new prefix and is replayed normally. Leaving it
16
+ // in place also kept charging every signature to the local context estimate,
17
+ // which then ran far above what the provider actually counted.
18
+ //
19
+ // One round is never stripped: a trailing tool round whose next request still
20
+ // continues it (the latest assistant turn called tools and no final reply
21
+ // followed — an idle session can stop there after a provider error). Its
22
+ // latest assistant turn must go back starting with its thinking, so removing
23
+ // that thinking is a refused request too. When the prefix changed under such a
24
+ // round, the round is ended explicitly instead (`rebindToChangedPrefix`).
25
+ import { contextAckMessage } from './compact/user-message-kind.mjs';
26
+ import { collectAssistantToolCallIds } from './context-tool-pairs.mjs';
27
+ import { assistantMessageWithoutThinkingReplay } from './thinking-replay-recovery.mjs';
28
+
29
+ /** The runtime turn that ends an open tool round: a context ack, so the
30
+ * transcript never shows it and Compact never reads it as a reply. */
31
+ function toolRoundClosure() {
32
+ const ack = contextAckMessage();
33
+ return { ...ack, meta: { ...ack.meta, synthetic: true } };
34
+ }
35
+
36
+ const callsTools = (message) =>
37
+ collectAssistantToolCallIds(message).length > 0 ||
38
+ (Array.isArray(message?.providerReplay?.items) &&
39
+ message.providerReplay.items.some((item) => item?.type === 'tool_use'));
40
+
41
+ /**
42
+ * Index where the trailing open tool round starts (the assistant/tool run that
43
+ * ends with the latest assistant turn's results; later user or notification
44
+ * input may follow), or -1 when the latest assistant turn is a final reply.
45
+ * Read from the persisted transcript alone.
46
+ */
47
+ export function openToolRoundStart(messages) {
48
+ if (!Array.isArray(messages)) return -1;
49
+ const latest = messages.findLastIndex((message) => message?.role === 'assistant');
50
+ if (latest < 0 || !callsTools(messages[latest])) return -1;
51
+ let start = latest;
52
+ while (start > 0 && ['assistant', 'tool'].includes(messages[start - 1]?.role)) start -= 1;
53
+ return start;
54
+ }
55
+
56
+ /** What the next request sends ahead of the conversation: the system blocks and tools. */
57
+ export function requestPrefixIdentity(session) {
58
+ return JSON.stringify([
59
+ (Array.isArray(session?.messages) ? session.messages : [])
60
+ .filter((message) => message?.role === 'system')
61
+ .map((message) => message.content),
62
+ session?.tools,
63
+ ]);
64
+ }
65
+
66
+ /**
67
+ * `messages` without Anthropic thinking replay (thinking / redacted_thinking
68
+ * blocks, wherever an assistant turn stores them); text and tool calls stay.
69
+ * The trailing open tool round keeps its thinking: everything stripped is a
70
+ * leading run, which leaves the round's own blocks valid.
71
+ * Replaces changed messages, never mutates; the same array when none changed.
72
+ */
73
+ export function withoutThinkingReplay(messages) {
74
+ if (!Array.isArray(messages)) return messages;
75
+ const open = openToolRoundStart(messages);
76
+ const end = open < 0 ? messages.length : open;
77
+ let changed = false;
78
+ const out = messages.map((message, index) => {
79
+ const stripped = index < end ? assistantMessageWithoutThinkingReplay(message) : null;
80
+ if (!stripped) return message;
81
+ changed = true;
82
+ return stripped;
83
+ });
84
+ return changed ? out : messages;
85
+ }
86
+
87
+ /**
88
+ * `messages` for a request prefix that changed under them: no thinking replay
89
+ * at all. An open tool round that carries thinking can neither keep it (bound
90
+ * to the old prefix) nor lose it (its continuation must start with it), so it
91
+ * is ended first by a runtime turn right after its last tool result, ahead of
92
+ * any later input (a task notification stays pending after it); the next
93
+ * request then starts a new turn. Never mutates; the same array when nothing
94
+ * changed.
95
+ */
96
+ export function rebindToChangedPrefix(messages) {
97
+ if (!Array.isArray(messages)) return messages;
98
+ const open = openToolRoundStart(messages);
99
+ if (open < 0) return withoutThinkingReplay(messages);
100
+ let end = messages.findLastIndex((message) => message?.role === 'assistant') + 1;
101
+ while (messages[end]?.role === 'tool') end += 1;
102
+ const roundThinks = messages
103
+ .slice(open, end)
104
+ .some((message) => assistantMessageWithoutThinkingReplay(message) !== null);
105
+ return withoutThinkingReplay(
106
+ roundThinks ? [...messages.slice(0, end), toolRoundClosure(), ...messages.slice(end)] : messages
107
+ );
108
+ }
@@ -103,8 +103,8 @@ function crossTurnStub(batch, call, ctSig) {
103
103
  // (which spans ONE assistant turn): when the model re-issues an IDENTICAL
104
104
  // normalized call that has already failed repeatFailLimit times in a row
105
105
  // across iterations, stop re-executing — the result will not change, and
106
- // each retry burns a full LLM round-trip with no iteration cap to end it. The
107
- // call stays unresolved; the guard only skips re-execution.
106
+ // each retry burns a full LLM round-trip. The call stays unresolved; the
107
+ // guard only skips re-execution.
108
108
  function repeatFailureSkip(batch, call, sigs) {
109
109
  const { sessionRef, repeatFailLimit } = batch;
110
110
  const rfg = sessionRef?._repeatFailGuard;
@@ -4,6 +4,7 @@
4
4
  // consumes.
5
5
  import { resolve as resolvePath, isAbsolute } from 'node:path';
6
6
  import { normalizeToolEnvelope } from '../tool-envelope.mjs';
7
+ import { effectiveToolCwd } from '../../tools/builtin/bash-tool/shell-cwd.mjs';
7
8
  import {
8
9
  invalidatePathForSession,
9
10
  clearReadDedupSession,
@@ -85,7 +86,7 @@ async function fireToolFailureHook(batch, call, result) {
85
86
  await hook({
86
87
  name: call.name,
87
88
  args: call.arguments,
88
- cwd: batch.cwd,
89
+ cwd: effectiveToolCwd(call.name, call.arguments, batch.cwd),
89
90
  sessionId: batch.sessionId,
90
91
  toolCallId: call.id,
91
92
  result: typeof result === 'string' ? result : String(result ?? ''),
@@ -64,6 +64,20 @@ function restoredFailureFields(value) {
64
64
  };
65
65
  }
66
66
 
67
+ /** Whether `message` was posted into the thread outside any turn: it says so
68
+ * (`transcript.posted`, stamped by agent-home-delivery.mjs), or it is a row
69
+ * stored before that marker existed. Those carry the id agent-home-delivery
70
+ * mints for every posted row (`messageIdOf`: `routine_<runId>`) and are not
71
+ * prompts: an agent's own message (assistant role) or a result/held-request
72
+ * notice (stored under the user role, but not a prompt). The `routine_` scheme
73
+ * alone is not enough: routine-home-turn.mjs:144 gives the prompt of a routine
74
+ * fire (user role) the same prefix, and that prompt opens a real turn. */
75
+ function isPostedMessage(message, transcript) {
76
+ if (transcript.posted === true) return true;
77
+ if (typeof transcript.messageId !== 'string' || !transcript.messageId.startsWith('routine_')) return false;
78
+ return message?.role !== 'user' || isRoutineResultMessage(message) || isHeldRequestMessage(message);
79
+ }
80
+
67
81
  function restoredTranscriptMetadata(message) {
68
82
  const value = message?.meta?.transcript;
69
83
  if (!value || typeof value !== 'object') return {};
@@ -87,6 +101,8 @@ function restoredTranscriptMetadata(message) {
87
101
  // messages back to these rows.
88
102
  ...(typeof value.messageId === 'string' && value.messageId ? { messageId: value.messageId } : {}),
89
103
  ...(Number.isFinite(Number(value.at)) ? { at: Number(value.at) } : {}),
104
+ // Delivered into the thread outside any turn (agent-home-delivery.mjs).
105
+ ...(isPostedMessage(message, value) ? { posted: true } : {}),
90
106
  ...transcriptRouteMetadataFields(value),
91
107
  ...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
92
108
  ...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
@@ -400,7 +416,7 @@ function restoredUserTranscriptItems(message, nextId, userText) {
400
416
  // A schedule's result in its owner's home thread: a notice row.
401
417
  // A held request of an unattended run (human-gate/held-message.mjs) likewise.
402
418
  if (isRoutineResultMessage(message) || isHeldRequestMessage(message)) {
403
- const { messageId, at } = restoredTranscriptMetadata(message);
419
+ const { messageId, at, posted } = restoredTranscriptMetadata(message);
404
420
  return [
405
421
  {
406
422
  kind: 'notice',
@@ -408,6 +424,7 @@ function restoredUserTranscriptItems(message, nextId, userText) {
408
424
  ...(isHeldRequestMessage(message) ? heldRequestNotice(message) : routineResultNotice(message)),
409
425
  ...(messageId ? { messageId } : {}),
410
426
  ...(at ? { at } : {}),
427
+ ...(posted ? { posted } : {}),
411
428
  },
412
429
  ];
413
430
  }
@@ -13,6 +13,7 @@ import { formatAggregateDetail, summarizeToolResult, toolLoadingTargets } from '
13
13
  import { normalizeToolName } from '../../../../shared/tool-primitives.mjs';
14
14
  import { gitResultError, gitResultExitCode } from '../../../../shared/tool-card-model/git-result.mjs';
15
15
  import { readRowsForDisplay } from '../../../../shared/read-row-numbers.mjs';
16
+ import { visualizeOwnerMetaSessionId } from '../../../../visualize-bridge/tool-meta.mjs';
16
17
 
17
18
  const CANCELLED_RESULT_STATUS_LINE = '[status: cancelled]';
18
19
 
@@ -292,15 +293,16 @@ export function assignUiDiffFromMessage(target, source) {
292
293
  }
293
294
 
294
295
  /** Structured side channel a tool result may carry next to its text (the
295
- * Computer Use capability refusal): a stable code plus the capability names.
296
- * Anything else is dropped so stored history stays small and predictable. */
297
- export function toolMetaFromMessage(source) {
296
+ * Computer Use capability refusal): a stable code plus the capability names,
297
+ * and the owner conversation of a `visualize` call run for another session's
298
+ * work (visualize-bridge/tool-meta.mjs). Anything else is dropped so stored
299
+ * history stays small and predictable. */
300
+ function toolMetaFromMessage(source) {
298
301
  const meta = source?.toolMeta ?? source?.content?.toolMeta;
299
302
  if (!meta || typeof meta !== 'object' || typeof meta.code !== 'string' || !meta.code) return undefined;
300
- const missing = Array.isArray(meta.missing)
301
- ? meta.missing.filter((name) => typeof name === 'string' && name)
302
- : [];
303
- return { code: meta.code, missing };
303
+ const missing = Array.isArray(meta.missing) ? meta.missing.filter((name) => typeof name === 'string' && name) : [];
304
+ const ownerSessionId = visualizeOwnerMetaSessionId(meta.ownerSessionId);
305
+ return { code: meta.code, missing, ...(ownerSessionId ? { ownerSessionId } : {}) };
304
306
  }
305
307
 
306
308
  export function toolMetaPatchFromMessage(source) {
@@ -132,8 +132,8 @@ export function toolGroupedDisplayFallback(resultText, text, rawText) {
132
132
  return resultText;
133
133
  }
134
134
 
135
- export function toolErrorDisplay(value, surface = 'tool') {
136
- const text = presentErrorText(value, { surface });
135
+ export function toolErrorDisplay(value, surface = 'tool', { providerLabel } = {}) {
136
+ const text = presentErrorText(value, { surface, providerLabel });
137
137
  if (
138
138
  /^(?:Search failed|Fetch failed|No first response|No progress|Cancelled|The .+ went stale|(?:Web search agent|Agent|Tool|Request) (?:stopped|was cancelled))/i.test(
139
139
  text
@@ -3,6 +3,7 @@
3
3
  // session shows the same failure, reason and quota stop as the live one.
4
4
  import { errText } from '../../../shared/err-text.mjs';
5
5
  import { safeErrorDetails } from '../../../shared/error-presentation.mjs';
6
+ import { providerDisplayName } from '../../../shared/provider-display-name.mjs';
6
7
  import { providerErrorKind } from '../providers/provider-error-kind.mjs';
7
8
  import { toolErrorDisplay } from './transcript-restore/tool-result-text.mjs';
8
9
 
@@ -39,7 +40,10 @@ export function turnFailureRecord(error, route = {}) {
39
40
  providerKind,
40
41
  errorKind,
41
42
  quota,
42
- detail: toolErrorDisplay(error, 'turn').replace(/^Error:\s*/i, ''),
43
+ // The provider is named from the turn's own route, never from the error.
44
+ detail: toolErrorDisplay(error, 'turn', {
45
+ providerLabel: route.provider ? providerDisplayName(route.provider) : undefined,
46
+ }).replace(/^Error:\s*/i, ''),
43
47
  errorDetails: safeErrorDetails(errText(error)),
44
48
  };
45
49
  }
@@ -780,10 +780,13 @@ function guardRead(a) {
780
780
  }
781
781
 
782
782
  function guardShell(a) {
783
- const allowed = new Set(['command', 'timeout_ms', 'wait_ms', 'background']);
783
+ const allowed = new Set(['command', 'cwd', 'timeout_ms', 'wait_ms', 'background']);
784
784
  const unsupported = Object.keys(a).find((key) => !allowed.has(key));
785
785
  if (unsupported) {
786
- return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, wait_ms, and background`;
786
+ return `Error: shell arg "${unsupported}" is unsupported; use only command, cwd, timeout_ms, wait_ms, and background`;
787
+ }
788
+ if (hasOwn(a, 'cwd') && (typeof a.cwd !== 'string' || a.cwd.trim().length === 0)) {
789
+ return `Error: shell arg "cwd" must be a non-empty string (got ${describeType(a.cwd)})`;
787
790
  }
788
791
  if (hasOwn(a, 'background') && typeof a.background !== 'boolean') {
789
792
  return `Error: shell arg "background" must be a boolean (got ${describeType(a.background)})`;
@@ -52,7 +52,17 @@ function registerPromotedTask({ result, command, cwd, options, startedAtMs }) {
52
52
  // running, now promoted as a tracked shell-job. Surface the task_id + partial
53
53
  // output for manual task control instead of keeping the tool call open until
54
54
  // the hard timeout.
55
- export function renderBackgroundedResult({ result, command, cwd, options, startedAtMs, teePlan, stdout, stderr }) {
55
+ export function renderBackgroundedResult({
56
+ result,
57
+ command,
58
+ cwd,
59
+ options,
60
+ startedAtMs,
61
+ teePlan,
62
+ stdout,
63
+ stderr,
64
+ notes = '',
65
+ }) {
56
66
  let task = null;
57
67
  if (result.jobId) {
58
68
  task = registerPromotedTask({ result, command, cwd, options, startedAtMs });
@@ -91,6 +101,7 @@ export function renderBackgroundedResult({ result, command, cwd, options, starte
91
101
  )
92
102
  : renderBackgroundPartialOutput(stdout, stderr);
93
103
  const lines = _backgroundResultLines({
104
+ warning: notes,
94
105
  taskBlock,
95
106
  message: result.backgroundMessage || DEFAULT_BACKGROUND_MESSAGE,
96
107
  partialOutput,
@@ -0,0 +1,240 @@
1
+ // Runtime guards against shell-tool misuse, run in the shell tool's validation
2
+ // step before hooks, approvals and execution (tool-exec.mjs) and again, once,
3
+ // for callers that reach executeBashTool without that step:
4
+ // 1. a leading whole-second sleep of 2 s or more is rejected (a wait belongs
5
+ // to background + task wait);
6
+ // 2. a plain leading `cd <dir>` moves into the `cwd` argument;
7
+ // 3. a shell file read (cat/Get-Content/grep/ls …) gets a pointer to the
8
+ // read / grep / list tools — a note now, a rejection when the mode flips.
9
+ import { shellSplitPipelineSegments, shellSplitSegments, shellTokenize } from '../shell-analysis.mjs';
10
+ import { resolveShellFor } from '../shell-runtime.mjs';
11
+ import { formatShellToolFailure } from './result-format.mjs';
12
+
13
+ /** `warn` runs the read with a note; `reject` returns the block without running. */
14
+ export const SHELL_FILE_READ_MODE = 'warn';
15
+ export const SHELL_FILE_READ_NOTE = 'Note: use the read / grep / list tools for files instead of the shell.';
16
+ const SHELL_FILE_READ_BLOCK =
17
+ 'Blocked: use read / grep / list for files instead of the shell (read takes offset/limit for a window of a large file).';
18
+
19
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
20
+
21
+ // ── 1. leading sleep ─────────────────────────────────────────────────────────
22
+
23
+ // Sub-2-second pacing stays allowed: only a bare whole-second sleep of 2 or
24
+ // more is refused. Float seconds, `-Milliseconds`, unit suffixes, pipelines,
25
+ // subshells and later statements are not matched.
26
+ const LEADING_SLEEP_MIN_SECONDS = 2;
27
+ // The first statement ends the command or is followed by `;`, `&&` or a newline.
28
+ const STATEMENT_END = String.raw`(?=\s*(?:$|;|&&|\r?\n))`;
29
+ // The argument stays on the command's line: a number on the next line is its own statement.
30
+ // PowerShell takes any prefix of `-Seconds` (`-s`, `-sec`, …) as the seconds parameter.
31
+ const POSIX_SLEEP_RE = new RegExp(String.raw`^\s*sleep[ \t]+(\d+)${STATEMENT_END}`);
32
+ const POWERSHELL_SLEEP_RE = new RegExp(
33
+ String.raw`^\s*(?:start-sleep|sleep)[ \t]+(?:-s(?:e(?:c(?:o(?:n(?:d(?:s)?)?)?)?)?)?[ \t:]+)?(\d+)${STATEMENT_END}`,
34
+ 'i'
35
+ );
36
+
37
+ /** Whole seconds of the sleep the command starts with, or null when it does not start with one. */
38
+ function leadingSleepSeconds(command, shellType) {
39
+ const match = (shellType === 'powershell' ? POWERSHELL_SLEEP_RE : POSIX_SLEEP_RE).exec(String(command ?? ''));
40
+ return match ? Number(match[1]) : null;
41
+ }
42
+
43
+ function leadingSleepFailure(command, shellType) {
44
+ const seconds = leadingSleepSeconds(command, shellType);
45
+ if (seconds === null || seconds < LEADING_SLEEP_MIN_SECONDS) return null;
46
+ return formatShellToolFailure(
47
+ `Blocked: the leading sleep of ${seconds} s was not run. For a long-running command use background:true; its completion arrives automatically. To wait for a specific task when the result is needed now, use task wait — do not poll in a sleep loop. A deliberate delay under ${LEADING_SLEEP_MIN_SECONDS} seconds is allowed.`
48
+ );
49
+ }
50
+
51
+ // ── 2. leading cd → cwd ──────────────────────────────────────────────────────
52
+
53
+ // One plain path token: quoted (no variable or escape characters) or unquoted
54
+ // (no shell metacharacters, glob characters included), then `;` or `&&` and a remainder.
55
+ const CD_RE =
56
+ /^\s*(?:cd|set-location)\s+(?:-path\s+)?(?:"([^"$`]+)"|'([^'$`]+)'|([^\s;&|<>"'`$(){}*?[,@%#!]+))\s*(?:;|&&)\s*(\S[\s\S]*)$/i;
57
+ // `Set-Location -Path` expands wildcards even in a quoted path; a POSIX shell
58
+ // keeps a quoted glob literal, so only PowerShell leaves those unrewritten.
59
+ const POWERSHELL_WILDCARD_RE = /[*?[]/;
60
+ const CD_LIKE_RE = /^(?:cd|set-location|pushd|popd)(?:\s|;|&|$)/i;
61
+
62
+ /** `{ dir, rest }` for a command that starts with a plain `cd <dir>` and more, else null. */
63
+ function splitLeadingCd(command, shellType) {
64
+ const match = CD_RE.exec(String(command ?? ''));
65
+ if (!match) return null;
66
+ const dir = match[1] ?? match[2] ?? match[3];
67
+ const rest = match[4];
68
+ if (CD_LIKE_RE.test(rest)) return null;
69
+ if (dir.trim() === '' || dir.startsWith('-')) return null;
70
+ if (shellType === 'powershell' && POWERSHELL_WILDCARD_RE.test(dir)) return null;
71
+ // `~user` is another account's home in a shell, not a folder named so; a
72
+ // quoted `~` is a literal directory name, only an unquoted one is home.
73
+ if (dir.startsWith('~') && (match[3] === undefined || !/^~(?:[\\/]|$)/.test(dir))) return null;
74
+ // A POSIX backslash escapes; only single quotes keep it literal.
75
+ if (shellType !== 'powershell' && match[2] === undefined && dir.includes('\\')) return null;
76
+ return { dir, rest };
77
+ }
78
+
79
+ // ── 3. shell file reads ──────────────────────────────────────────────────────
80
+
81
+ const POWERSHELL_READERS = {
82
+ 'get-content': 'read',
83
+ gc: 'read',
84
+ cat: 'read',
85
+ type: 'read',
86
+ 'select-string': 'grep',
87
+ sls: 'grep',
88
+ 'get-childitem': 'list',
89
+ gci: 'list',
90
+ ls: 'list',
91
+ dir: 'list',
92
+ rg: 'grep',
93
+ };
94
+ const POSIX_READERS = {
95
+ cat: 'read',
96
+ head: 'read',
97
+ tail: 'read',
98
+ less: 'read',
99
+ grep: 'grep',
100
+ rg: 'grep',
101
+ ls: 'list',
102
+ find: 'list',
103
+ };
104
+ // Flags whose next token is a path or pattern operand, and flags whose next
105
+ // token is a plain value; both are read off the operand count.
106
+ const OPERAND_FLAGS = new Set(['-path', '-literalpath', '-pattern', '-e', '--regexp', '-f', '--file']);
107
+ const VALUE_FLAGS = new Set(['-encoding', '-filter', '-include', '-exclude', '-g', '--glob', '-t', '--type']);
108
+ // A later pipeline stage that only shapes text; anything else is a program
109
+ // the first stage feeds.
110
+ const TEXT_FILTERS = new Set([
111
+ 'select-object',
112
+ 'select',
113
+ 'select-string',
114
+ 'sls',
115
+ 'where-object',
116
+ 'sort-object',
117
+ 'sort',
118
+ 'measure-object',
119
+ 'format-table',
120
+ 'format-list',
121
+ 'ft',
122
+ 'fl',
123
+ 'out-string',
124
+ 'out-host',
125
+ 'head',
126
+ 'tail',
127
+ 'grep',
128
+ 'egrep',
129
+ 'rg',
130
+ 'wc',
131
+ 'uniq',
132
+ 'cut',
133
+ 'findstr',
134
+ ]);
135
+ const FIND_WRITERS = new Set(['-exec', '-execdir', '-ok', '-okdir', '-delete', '-fprint', '-fprintf', '-fls']);
136
+ const BENIGN_REDIRECT_TARGET = /^(?:&\d|\$null|\/dev\/null|nul)$/i;
137
+
138
+ const commandName = (token) =>
139
+ String(token ?? '')
140
+ .toLowerCase()
141
+ .replace(/^.*[\\/]/, '')
142
+ .replace(/\.exe$/, '');
143
+
144
+ // The path/pattern operands of a stage, or null when it writes a file.
145
+ function stageOperands(args) {
146
+ const operands = [];
147
+ for (let i = 0; i < args.length; i++) {
148
+ const token = args[i];
149
+ if (token === '>' || token === '>>') {
150
+ if (!BENIGN_REDIRECT_TARGET.test(args[i + 1] ?? '')) return null;
151
+ i++;
152
+ continue;
153
+ }
154
+ const flag = token.toLowerCase();
155
+ if (flag.startsWith('-') && flag.length > 1) {
156
+ if (OPERAND_FLAGS.has(flag) && i + 1 < args.length) operands.push(args[++i]);
157
+ else if (VALUE_FLAGS.has(flag)) i++;
158
+ continue;
159
+ }
160
+ // A bare count (`head -n 5`, `tail -n +5`) is a flag value, not an operand.
161
+ if (!/^[+-]?\d+$/.test(token)) operands.push(token);
162
+ }
163
+ return operands;
164
+ }
165
+
166
+ /**
167
+ * `{ tool, command }` when the command's first statement starts with a
168
+ * read-only file command on a path — the tool that does that job — else null.
169
+ * A pipeline whose later stage is a program, a write, and a first stage that
170
+ * reads piped input are not file reads.
171
+ */
172
+ function detectShellFileRead(command, shellType) {
173
+ const first = shellSplitSegments(String(command ?? ''))[0];
174
+ if (!first) return null;
175
+ const stages = shellSplitPipelineSegments(first);
176
+ const tokens = shellTokenize(stages[0] ?? '');
177
+ if (!tokens || tokens.length === 0) return null;
178
+ const name = commandName(tokens[0]);
179
+ const tool = (shellType === 'powershell' ? POWERSHELL_READERS : POSIX_READERS)[name];
180
+ if (!tool) return null;
181
+ for (const stage of stages.slice(1)) {
182
+ if (!TEXT_FILTERS.has(commandName(stage.trim().split(/\s+/)[0]))) return null;
183
+ }
184
+ const args = tokens.slice(1);
185
+ if (name === 'find') {
186
+ const lowered = args.map((token) => token.toLowerCase());
187
+ const matchesNames = lowered.some((token) => ['-name', '-iname', '-path', '-regex'].includes(token));
188
+ return matchesNames && !lowered.some((token) => FIND_WRITERS.has(token)) ? { tool, command: name } : null;
189
+ }
190
+ const operands = stageOperands(args);
191
+ if (!operands) return null;
192
+ // A search names its pattern and then the path it reads; with one operand it reads piped input.
193
+ const minOperands = tool === 'grep' ? 2 : 1;
194
+ return operands.length >= minOperands ? { tool, command: name } : null;
195
+ }
196
+
197
+ // ── the guard ────────────────────────────────────────────────────────────────
198
+
199
+ /** The syntax family of the shell a call asks for (`args.shell`, as foreign adapters set it), else the default shell's. */
200
+ export function requestedShellType(args) {
201
+ const requested = typeof args?.shell === 'string' && args.shell ? resolveShellFor(args.shell) : null;
202
+ return (requested ?? resolveShellFor('default'))?.shellType;
203
+ }
204
+
205
+ function recordFileRead(telemetry, entry) {
206
+ if (!telemetry || typeof telemetry !== 'object') return;
207
+ if (!Array.isArray(telemetry.shellMisuse)) telemetry.shellMisuse = [];
208
+ telemetry.shellMisuse.push({ kind: 'file-read', ...entry });
209
+ }
210
+
211
+ /**
212
+ * Apply the three guards to a shell call.
213
+ * @returns {{ failure: string } | { args: object, notes: string[] }} the
214
+ * rejection, or the call to run (a leading `cd` moved into `cwd`) with the
215
+ * lines to add to its result.
216
+ */
217
+ export function guardShellCall(
218
+ args,
219
+ { shellType, fileReadMode = SHELL_FILE_READ_MODE, agent = null, sessionId = null, telemetry = null } = {}
220
+ ) {
221
+ if (!isPlainObject(args) || typeof args.command !== 'string') return { args, notes: [] };
222
+ const notes = [];
223
+ let guarded = args;
224
+ const moved = args.cwd == null ? splitLeadingCd(args.command, shellType) : null;
225
+ if (moved) {
226
+ guarded = { ...args, command: moved.rest, cwd: moved.dir };
227
+ notes.push(`Moved \`cd ${moved.dir}\` into cwd.`);
228
+ }
229
+ if (args.background !== true) {
230
+ const sleepFailure = leadingSleepFailure(guarded.command, shellType);
231
+ if (sleepFailure) return { failure: sleepFailure };
232
+ }
233
+ const read = detectShellFileRead(guarded.command, shellType);
234
+ if (read) {
235
+ recordFileRead(telemetry, { mode: fileReadMode, tool: read.tool, command: read.command, agent, sessionId });
236
+ if (fileReadMode === 'reject') return { failure: formatShellToolFailure(SHELL_FILE_READ_BLOCK) };
237
+ notes.push(SHELL_FILE_READ_NOTE);
238
+ }
239
+ return { args: guarded, notes };
240
+ }
@@ -118,7 +118,7 @@ const _PS_UNIX_EQUIVALENTS = {
118
118
  head: 'Select-Object -First N',
119
119
  tail: 'Select-Object -Last N',
120
120
  wc: 'Measure-Object -Line',
121
- grep: 'Select-String (or the grep tool)',
121
+ grep: 'the grep tool',
122
122
  sed: 'the edit tool',
123
123
  awk: 'ForEach-Object / the edit tool',
124
124
  pgrep: 'Get-Process -Name <name>',
@@ -0,0 +1,38 @@
1
+ // The shell tool's optional `cwd` argument: where one command runs. Omitted,
2
+ // the command runs in the session cwd; a relative value resolves against it.
3
+ import { stat } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+
7
+ // `~`, `~/x` (and `~\x` on Windows) name the user's home folder.
8
+ function expandHome(path) {
9
+ if (path === '~') return homedir();
10
+ if (path.startsWith('~/') || (process.platform === 'win32' && path.startsWith('~\\'))) {
11
+ return resolve(homedir(), path.slice(2));
12
+ }
13
+ return path;
14
+ }
15
+
16
+ /** The directory a shell call runs in: its `cwd` argument resolved against the session cwd. */
17
+ export function effectiveShellCwd(args, sessionCwd) {
18
+ const requested = args?.cwd;
19
+ // Trim only decides blank; a name with edge spaces is used as given.
20
+ if (typeof requested !== 'string' || requested.trim() === '') return sessionCwd;
21
+ return resolve(sessionCwd || process.cwd(), expandHome(requested));
22
+ }
23
+
24
+ /** The cwd hooks and approvals see for a tool call: the effective one for shell, the session cwd otherwise. */
25
+ export function effectiveToolCwd(toolName, args, sessionCwd) {
26
+ return toolName === 'shell' ? effectiveShellCwd(args, sessionCwd) : sessionCwd;
27
+ }
28
+
29
+ /** An error naming the path when `dir` is not an existing directory, else null. */
30
+ export async function shellCwdError(dir) {
31
+ let info;
32
+ try {
33
+ info = await stat(dir);
34
+ } catch {
35
+ return `shell cwd "${dir}" does not exist`;
36
+ }
37
+ return info.isDirectory() ? null : `shell cwd "${dir}" is not a directory`;
38
+ }
@@ -25,6 +25,8 @@ import { planShellDeadlines } from './bash-tool/deadline-plan.mjs';
25
25
  import { planShellInvocation } from './bash-tool/invocation-plan.mjs';
26
26
  import { renderBackgroundedResult } from './bash-tool/backgrounded-result.mjs';
27
27
  import { renderCompletedResult } from './bash-tool/completed-result.mjs';
28
+ import { effectiveShellCwd, shellCwdError } from './bash-tool/shell-cwd.mjs';
29
+ import { guardShellCall, requestedShellType } from './bash-tool/misuse-guard.mjs';
28
30
 
29
31
  export {
30
32
  SURVIVING_DESCENDANTS_WARNING,
@@ -64,9 +66,26 @@ function settleMutationCaches(shellEffects, backgroundJobId, runSettled = null)
64
66
  }
65
67
 
66
68
  export async function executeBashTool(args, workDir, options = {}) {
67
- // Every call starts from the current Project root. A command-local `cd`
68
- // never creates a second session cwd authority beside the dedicated cwd tool.
69
- const bashWorkDir = workDir;
69
+ // The tool-exec layer already guarded the call before its hooks ran and left
70
+ // its notes; a direct caller is guarded here.
71
+ const guarded = Array.isArray(options?.shellGuardNotes)
72
+ ? { args, notes: options.shellGuardNotes }
73
+ : guardShellCall(args, {
74
+ shellType: requestedShellType(args),
75
+ agent: options?.agent,
76
+ sessionId: options?.sessionId,
77
+ telemetry: options?.resultTelemetry,
78
+ });
79
+ if (guarded.failure) return guarded.failure;
80
+ args = guarded.args;
81
+ const guardNotes = guarded.notes;
82
+ // A call starts from the current Project root unless it names a `cwd`. A
83
+ // command-local `cd` never creates a second session cwd authority beside it.
84
+ const bashWorkDir = effectiveShellCwd(args, workDir);
85
+ if (bashWorkDir !== workDir) {
86
+ const cwdError = await shellCwdError(bashWorkDir);
87
+ if (cwdError) return formatShellToolFailure(cwdError);
88
+ }
70
89
  // Approval comes from the runtime's options, never from model-supplied args.
71
90
  const elevationApproved = options?.elevationApproved === true;
72
91
  const prepared = prepareShellCommand(args, { elevationApproved });
@@ -106,6 +125,7 @@ export async function executeBashTool(args, workDir, options = {}) {
106
125
  shellArg: invocation.execShellArg,
107
126
  shellArgs: invocation.execShellArgs,
108
127
  command: invocation.wrappedCommand,
128
+ displayCommand: command,
109
129
  execScript: invocation.execScript,
110
130
  directArgv: invocation.directArgv,
111
131
  elevationApproved,
@@ -147,12 +167,12 @@ export async function executeBashTool(args, workDir, options = {}) {
147
167
  stdout,
148
168
  stderr,
149
169
  };
150
- if (backgrounded) return renderBackgroundedResult(rendering);
170
+ if (backgrounded) return renderBackgroundedResult({ ...rendering, notes: guardNotes.join('\n') });
151
171
  return renderCompletedResult({
152
172
  ...rendering,
153
173
  analysisCommand,
154
174
  timeout: deadlines.timeout,
155
- rewriteNote: [wmicRewrite?.note, bashismRewrite?.note].filter(Boolean).join('\n'),
175
+ rewriteNote: [wmicRewrite?.note, bashismRewrite?.note, ...guardNotes].filter(Boolean).join('\n'),
156
176
  });
157
177
  } finally {
158
178
  inputWake?.dispose();