@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11

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 (352) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/dist/cli.js +23087 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +1 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +20 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -16
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/debug/terminal-info.d.ts +0 -1
  25. package/dist/types/eval/backend.d.ts +0 -2
  26. package/dist/types/eval/idle-timeout.d.ts +0 -4
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  28. package/dist/types/export/html/template.generated.d.ts +1 -1
  29. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  30. package/dist/types/hindsight/mental-models.d.ts +17 -8
  31. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  32. package/dist/types/internal-urls/types.d.ts +1 -1
  33. package/dist/types/lsp/edits.d.ts +9 -0
  34. package/dist/types/lsp/index.d.ts +2 -2
  35. package/dist/types/lsp/types.d.ts +2 -0
  36. package/dist/types/lsp/utils.d.ts +3 -0
  37. package/dist/types/mcp/json-rpc.d.ts +5 -0
  38. package/dist/types/mnemopi/state.d.ts +11 -1
  39. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  40. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  41. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  42. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  43. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  46. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  47. package/dist/types/modes/components/footer.d.ts +1 -1
  48. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  49. package/dist/types/modes/components/hook-input.d.ts +4 -0
  50. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  51. package/dist/types/modes/components/model-selector.d.ts +1 -1
  52. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  54. package/dist/types/modes/components/session-selector.d.ts +1 -1
  55. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  56. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  57. package/dist/types/modes/components/transcript-container.d.ts +31 -26
  58. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  60. package/dist/types/modes/components/user-message.d.ts +2 -1
  61. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  62. package/dist/types/modes/components/welcome.d.ts +19 -3
  63. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  64. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  65. package/dist/types/modes/interactive-mode.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  70. package/dist/types/modes/types.d.ts +2 -1
  71. package/dist/types/session/agent-session.d.ts +1 -1
  72. package/dist/types/session/auth-broker-config.d.ts +4 -0
  73. package/dist/types/session/session-manager.d.ts +1 -1
  74. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  75. package/dist/types/ssh/connection-manager.d.ts +8 -0
  76. package/dist/types/task/discovery.d.ts +1 -2
  77. package/dist/types/task/parallel.d.ts +2 -2
  78. package/dist/types/task/worktree.d.ts +2 -0
  79. package/dist/types/tiny/title-client.d.ts +1 -1
  80. package/dist/types/tools/ask.d.ts +4 -0
  81. package/dist/types/tools/conflict-detect.d.ts +16 -0
  82. package/dist/types/tools/github-cache.d.ts +7 -0
  83. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  84. package/dist/types/tools/todo.d.ts +2 -0
  85. package/dist/types/tui/output-block.d.ts +3 -3
  86. package/dist/types/utils/changelog.d.ts +8 -0
  87. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  88. package/dist/types/web/scrapers/types.d.ts +12 -0
  89. package/dist/types/web/search/providers/codex.d.ts +1 -1
  90. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  91. package/examples/extensions/tools.ts +5 -4
  92. package/package.json +14 -11
  93. package/scripts/build-binary.ts +18 -23
  94. package/scripts/bundle-dist.ts +81 -0
  95. package/scripts/{dev-launch → omp} +1 -1
  96. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  97. package/src/async/job-manager.ts +57 -3
  98. package/src/autoresearch/dashboard.ts +1 -1
  99. package/src/autoresearch/prompt-setup.md +6 -6
  100. package/src/autoresearch/prompt.md +6 -6
  101. package/src/capability/fs.ts +10 -0
  102. package/src/cli/args.ts +1 -1
  103. package/src/cli/auth-gateway-cli.ts +1 -3
  104. package/src/cli/dry-balance-cli.ts +1 -1
  105. package/src/cli/gallery-cli.ts +1 -1
  106. package/src/cli/gallery-fixtures/fs.ts +1 -1
  107. package/src/cli/gallery-fixtures/types.ts +5 -1
  108. package/src/cli/list-models.ts +7 -12
  109. package/src/cli/usage-cli.ts +603 -0
  110. package/src/cli-commands.ts +1 -0
  111. package/src/cli.ts +69 -5
  112. package/src/commands/complete.ts +1 -1
  113. package/src/commands/launch.ts +1 -1
  114. package/src/commands/read.ts +6 -3
  115. package/src/commands/usage.ts +35 -0
  116. package/src/commit/agentic/agent.ts +1 -1
  117. package/src/commit/model-selection.ts +1 -1
  118. package/src/config/append-only-context-mode.ts +6 -12
  119. package/src/config/model-discovery.ts +554 -0
  120. package/src/config/model-registry.ts +308 -1025
  121. package/src/config/model-resolver.ts +113 -156
  122. package/src/config/model-roles.ts +74 -0
  123. package/src/config/models-config-schema.ts +57 -8
  124. package/src/config/models-config.ts +129 -0
  125. package/src/config/settings-schema.ts +18 -14
  126. package/src/config/settings.ts +37 -1
  127. package/src/dap/client.ts +124 -37
  128. package/src/dap/session.ts +259 -158
  129. package/src/debug/log-viewer.ts +1 -1
  130. package/src/debug/raw-sse.ts +1 -1
  131. package/src/debug/terminal-info.ts +0 -3
  132. package/src/edit/diff.ts +95 -18
  133. package/src/edit/hashline/block-resolver.ts +20 -1
  134. package/src/edit/hashline/diff.ts +36 -1
  135. package/src/edit/hashline/execute.ts +8 -2
  136. package/src/edit/index.ts +16 -1
  137. package/src/edit/modes/patch.ts +52 -0
  138. package/src/edit/modes/replace.ts +56 -22
  139. package/src/edit/notebook.ts +22 -2
  140. package/src/edit/renderer.ts +36 -10
  141. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  142. package/src/eval/backend.ts +0 -2
  143. package/src/eval/completion-bridge.ts +2 -1
  144. package/src/eval/idle-timeout.ts +2 -9
  145. package/src/eval/js/context-manager.ts +6 -8
  146. package/src/eval/js/executor.ts +6 -2
  147. package/src/eval/js/index.ts +0 -2
  148. package/src/eval/js/shared/helpers.ts +5 -6
  149. package/src/eval/js/shared/local-module-loader.ts +1 -1
  150. package/src/eval/js/shared/prelude.txt +62 -1
  151. package/src/eval/js/shared/rewrite-imports.ts +49 -23
  152. package/src/eval/js/shared/runtime.ts +1 -1
  153. package/src/eval/py/index.ts +0 -2
  154. package/src/eval/py/kernel.ts +19 -0
  155. package/src/eval/py/runner.py +107 -3
  156. package/src/exec/bash-executor.ts +3 -1
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/export/html/template.js +3 -1
  159. package/src/extensibility/extensions/types.ts +3 -2
  160. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  161. package/src/hindsight/mental-models.ts +59 -12
  162. package/src/hindsight/state.ts +6 -1
  163. package/src/internal-urls/artifact-protocol.ts +11 -2
  164. package/src/internal-urls/docs-index.generated.ts +10 -10
  165. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  166. package/src/internal-urls/router.ts +1 -1
  167. package/src/internal-urls/types.ts +1 -1
  168. package/src/lib/xai-http.ts +1 -1
  169. package/src/lsp/client.ts +118 -38
  170. package/src/lsp/clients/biome-client.ts +101 -39
  171. package/src/lsp/edits.ts +143 -95
  172. package/src/lsp/index.ts +31 -22
  173. package/src/lsp/render.ts +1 -1
  174. package/src/lsp/types.ts +2 -0
  175. package/src/lsp/utils.ts +28 -10
  176. package/src/main.ts +165 -17
  177. package/src/mcp/json-rpc.ts +35 -5
  178. package/src/mcp/transports/stdio.ts +7 -1
  179. package/src/memories/index.ts +2 -1
  180. package/src/mnemopi/backend.ts +25 -3
  181. package/src/mnemopi/state.ts +38 -2
  182. package/src/modes/components/agent-dashboard.ts +10 -7
  183. package/src/modes/components/assistant-message.ts +19 -13
  184. package/src/modes/components/bash-execution.ts +1 -1
  185. package/src/modes/components/copy-selector.ts +1 -1
  186. package/src/modes/components/diff.ts +13 -2
  187. package/src/modes/components/dynamic-border.ts +12 -3
  188. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  189. package/src/modes/components/extensions/extension-list.ts +1 -1
  190. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  191. package/src/modes/components/footer.ts +1 -1
  192. package/src/modes/components/history-search.ts +1 -1
  193. package/src/modes/components/hook-editor.ts +8 -0
  194. package/src/modes/components/hook-input.ts +8 -0
  195. package/src/modes/components/hook-selector.ts +2 -2
  196. package/src/modes/components/model-selector.ts +66 -54
  197. package/src/modes/components/plan-review-overlay.ts +1 -1
  198. package/src/modes/components/session-observer-overlay.ts +2 -2
  199. package/src/modes/components/session-selector.ts +1 -1
  200. package/src/modes/components/settings-selector.ts +5 -1
  201. package/src/modes/components/status-line/component.ts +1 -1
  202. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  203. package/src/modes/components/transcript-container.ts +373 -141
  204. package/src/modes/components/tree-selector.ts +3 -3
  205. package/src/modes/components/user-message-selector.ts +1 -1
  206. package/src/modes/components/user-message.ts +17 -5
  207. package/src/modes/components/visual-truncate.ts +1 -1
  208. package/src/modes/components/welcome.ts +108 -26
  209. package/src/modes/controllers/command-controller.ts +10 -3
  210. package/src/modes/controllers/event-controller.ts +73 -49
  211. package/src/modes/controllers/input-controller.ts +5 -5
  212. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  213. package/src/modes/controllers/selector-controller.ts +1 -5
  214. package/src/modes/controllers/streaming-reveal.ts +85 -18
  215. package/src/modes/interactive-mode.ts +5 -19
  216. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  217. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  218. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  219. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  220. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  221. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  222. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  223. package/src/modes/types.ts +2 -1
  224. package/src/prompts/agents/explore.md +2 -2
  225. package/src/prompts/agents/librarian.md +1 -2
  226. package/src/prompts/agents/oracle.md +1 -1
  227. package/src/prompts/agents/plan.md +5 -5
  228. package/src/prompts/agents/task.md +5 -5
  229. package/src/prompts/ci-green-request.md +5 -7
  230. package/src/prompts/goals/goal-budget-limit.md +2 -2
  231. package/src/prompts/goals/goal-continuation.md +4 -4
  232. package/src/prompts/goals/goal-mode-active.md +1 -1
  233. package/src/prompts/memories/read-path.md +1 -1
  234. package/src/prompts/memories/stage_one_system.md +2 -2
  235. package/src/prompts/review-custom-request.md +1 -1
  236. package/src/prompts/system/agent-creation-architect.md +2 -2
  237. package/src/prompts/system/auto-continue.md +1 -1
  238. package/src/prompts/system/background-tan-dispatch.md +1 -1
  239. package/src/prompts/system/btw-user.md +2 -2
  240. package/src/prompts/system/commit-message-system.md +13 -1
  241. package/src/prompts/system/custom-system-prompt.md +1 -1
  242. package/src/prompts/system/eager-todo.md +2 -2
  243. package/src/prompts/system/irc-incoming.md +1 -1
  244. package/src/prompts/system/manual-continue.md +1 -1
  245. package/src/prompts/system/omfg-user.md +3 -4
  246. package/src/prompts/system/orchestrate-notice.md +9 -9
  247. package/src/prompts/system/plan-mode-active.md +4 -4
  248. package/src/prompts/system/plan-mode-subagent.md +4 -5
  249. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  250. package/src/prompts/system/project-prompt.md +2 -2
  251. package/src/prompts/system/subagent-system-prompt.md +4 -4
  252. package/src/prompts/system/system-prompt.md +15 -26
  253. package/src/prompts/system/title-system.md +2 -2
  254. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  255. package/src/prompts/system/workflow-notice.md +1 -1
  256. package/src/prompts/tools/ast-edit.md +1 -1
  257. package/src/prompts/tools/ast-grep.md +2 -2
  258. package/src/prompts/tools/bash.md +8 -10
  259. package/src/prompts/tools/browser.md +7 -7
  260. package/src/prompts/tools/debug.md +1 -1
  261. package/src/prompts/tools/eval.md +3 -3
  262. package/src/prompts/tools/find.md +0 -1
  263. package/src/prompts/tools/github.md +8 -7
  264. package/src/prompts/tools/goal.md +1 -1
  265. package/src/prompts/tools/image-gen.md +1 -1
  266. package/src/prompts/tools/inspect-image-system.md +1 -1
  267. package/src/prompts/tools/irc.md +15 -15
  268. package/src/prompts/tools/lsp.md +2 -2
  269. package/src/prompts/tools/patch.md +2 -2
  270. package/src/prompts/tools/read.md +3 -4
  271. package/src/prompts/tools/recall.md +1 -1
  272. package/src/prompts/tools/reflect.md +1 -1
  273. package/src/prompts/tools/render-mermaid.md +2 -2
  274. package/src/prompts/tools/replace.md +4 -10
  275. package/src/prompts/tools/rewind.md +2 -2
  276. package/src/prompts/tools/search-tool-bm25.md +1 -9
  277. package/src/prompts/tools/search.md +0 -1
  278. package/src/prompts/tools/ssh.md +0 -4
  279. package/src/prompts/tools/task.md +2 -3
  280. package/src/prompts/tools/todo.md +6 -2
  281. package/src/sdk.ts +23 -10
  282. package/src/session/agent-session.ts +44 -10
  283. package/src/session/auth-broker-config.ts +30 -1
  284. package/src/session/session-manager.ts +2 -2
  285. package/src/session/streaming-output.ts +23 -2
  286. package/src/slash-commands/builtin-registry.ts +20 -0
  287. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  288. package/src/ssh/connection-manager.ts +27 -0
  289. package/src/task/commands.ts +2 -1
  290. package/src/task/discovery.ts +17 -24
  291. package/src/task/executor.ts +61 -53
  292. package/src/task/index.ts +137 -60
  293. package/src/task/parallel.ts +3 -3
  294. package/src/task/render.ts +2 -2
  295. package/src/task/worktree.ts +64 -56
  296. package/src/thinking.ts +2 -1
  297. package/src/tiny/title-client.ts +32 -14
  298. package/src/tools/archive-reader.ts +30 -2
  299. package/src/tools/ask.ts +104 -21
  300. package/src/tools/ast-edit.ts +25 -5
  301. package/src/tools/auto-generated-guard.ts +20 -3
  302. package/src/tools/bash-interactive.ts +27 -7
  303. package/src/tools/bash.ts +54 -13
  304. package/src/tools/browser/launch.ts +11 -2
  305. package/src/tools/browser/readable.ts +19 -2
  306. package/src/tools/browser/registry.ts +4 -1
  307. package/src/tools/browser/render.ts +2 -2
  308. package/src/tools/browser/tab-supervisor.ts +55 -16
  309. package/src/tools/conflict-detect.ts +50 -4
  310. package/src/tools/debug.ts +1 -1
  311. package/src/tools/eval-render.ts +5 -5
  312. package/src/tools/eval.ts +0 -2
  313. package/src/tools/fetch.ts +33 -10
  314. package/src/tools/gh-cache-invalidation.ts +63 -8
  315. package/src/tools/gh-renderer.ts +1 -1
  316. package/src/tools/gh.ts +172 -29
  317. package/src/tools/github-cache.ts +70 -6
  318. package/src/tools/image-gen.ts +3 -9
  319. package/src/tools/irc.ts +5 -1
  320. package/src/tools/job.ts +1 -1
  321. package/src/tools/read.ts +202 -61
  322. package/src/tools/render-utils.ts +3 -3
  323. package/src/tools/resolve.ts +1 -1
  324. package/src/tools/search.ts +92 -29
  325. package/src/tools/sqlite-reader.ts +17 -5
  326. package/src/tools/ssh.ts +8 -8
  327. package/src/tools/todo.ts +51 -12
  328. package/src/tools/write.ts +118 -18
  329. package/src/tui/output-block.ts +4 -4
  330. package/src/utils/changelog.ts +27 -1
  331. package/src/utils/file-mentions.ts +2 -1
  332. package/src/web/scrapers/arxiv.ts +1 -1
  333. package/src/web/scrapers/go-pkg.ts +1 -1
  334. package/src/web/scrapers/iacr.ts +1 -1
  335. package/src/web/scrapers/readthedocs.ts +1 -1
  336. package/src/web/scrapers/twitter.ts +2 -1
  337. package/src/web/scrapers/types.ts +87 -8
  338. package/src/web/scrapers/wikipedia.ts +1 -1
  339. package/src/web/scrapers/youtube.ts +6 -1
  340. package/src/web/search/index.ts +1 -1
  341. package/src/web/search/providers/anthropic.ts +8 -2
  342. package/src/web/search/providers/codex.ts +2 -1
  343. package/src/web/search/providers/gemini.ts +2 -3
  344. package/src/web/search/render.ts +8 -6
  345. package/dist/types/config/model-equivalence.d.ts +0 -24
  346. package/dist/types/config/model-id-affixes.d.ts +0 -12
  347. package/dist/types/config/model-provider-priority.d.ts +0 -1
  348. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  349. package/src/config/model-equivalence.ts +0 -875
  350. package/src/config/model-id-affixes.ts +0 -81
  351. package/src/config/model-provider-priority.ts +0 -56
  352. package/src/exec/idle-timeout-watchdog.ts +0 -126
@@ -0,0 +1,85 @@
1
+ import * as stats from "@oh-my-pi/omp-stats";
2
+ import * as openUtils from "../../utils/open";
3
+
4
+ export const DEFAULT_STATS_DASHBOARD_PORT = 3847;
5
+
6
+ interface StatsDashboardServer {
7
+ port: number;
8
+ stop: () => void;
9
+ }
10
+
11
+ export interface StatsDashboardArgs {
12
+ port: number;
13
+ }
14
+
15
+ export interface StatsDashboardLaunchResult {
16
+ url: string;
17
+ message: string;
18
+ }
19
+
20
+ let activeStatsServer: StatsDashboardServer | undefined;
21
+
22
+ const STATS_DASHBOARD_USAGE = "Usage: /stats [--port <port>]";
23
+
24
+ function parsePort(value: string | undefined): number | string {
25
+ if (!value) return `Missing port. ${STATS_DASHBOARD_USAGE}`;
26
+ if (!/^\d+$/.test(value)) return `Invalid port: ${value}`;
27
+ const port = Number(value);
28
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) return `Invalid port: ${value}`;
29
+ return port;
30
+ }
31
+
32
+ export function parseStatsDashboardArgs(args: string): StatsDashboardArgs | { error: string } {
33
+ const tokens = args.split(/\s+/).filter(Boolean);
34
+ let port = DEFAULT_STATS_DASHBOARD_PORT;
35
+
36
+ for (let i = 0; i < tokens.length; i++) {
37
+ const token = tokens[i];
38
+ if (token === "--port" || token === "-p") {
39
+ const parsed = parsePort(tokens[++i]);
40
+ if (typeof parsed === "string") return { error: parsed };
41
+ port = parsed;
42
+ continue;
43
+ }
44
+ if (token.startsWith("--port=")) {
45
+ const parsed = parsePort(token.slice("--port=".length));
46
+ if (typeof parsed === "string") return { error: parsed };
47
+ port = parsed;
48
+ continue;
49
+ }
50
+ return { error: `Unknown option: ${token}. ${STATS_DASHBOARD_USAGE}` };
51
+ }
52
+
53
+ return { port };
54
+ }
55
+
56
+ export async function launchStatsDashboard(args: StatsDashboardArgs): Promise<StatsDashboardLaunchResult> {
57
+ const { processed, files } = await stats.syncAllSessions();
58
+ const total = await stats.getTotalMessageCount();
59
+ let requestedPortIgnored = false;
60
+
61
+ if (!activeStatsServer) {
62
+ activeStatsServer = await stats.startServer(args.port);
63
+ } else if (args.port !== activeStatsServer.port) {
64
+ requestedPortIgnored = true;
65
+ }
66
+
67
+ const url = `http://localhost:${activeStatsServer.port}`;
68
+ openUtils.openPath(url);
69
+
70
+ const serverLine = requestedPortIgnored
71
+ ? `Dashboard already running at: ${url} (requested port ${args.port} ignored)`
72
+ : `Dashboard available at: ${url}`;
73
+
74
+ return {
75
+ url,
76
+ message: `Synced ${processed} new entries from ${files} files (${total} total)\n${serverLine}`,
77
+ };
78
+ }
79
+
80
+ export function stopStatsDashboard(): void {
81
+ if (!activeStatsServer) return;
82
+ activeStatsServer.stop();
83
+ activeStatsServer = undefined;
84
+ stats.closeDb();
85
+ }
@@ -355,6 +355,33 @@ export async function getHostInfoForHost(host: SSHConnectionTarget): Promise<SSH
355
355
  return await loadHostInfoFromDisk(host);
356
356
  }
357
357
 
358
+ /**
359
+ * Synchronous, probe-free host info lookup for startup paths.
360
+ *
361
+ * Checks the in-memory cache, then falls back to a synchronous read of the
362
+ * persisted host-info cache file. Never opens a connection or probes the
363
+ * remote host — callers get `undefined` when nothing is cached yet.
364
+ */
365
+ export function getCachedHostInfoSync(host: SSHConnectionTarget): SSHHostInfo | undefined {
366
+ const cached = hostInfoCache.get(host.name);
367
+ if (cached) {
368
+ const resolved = applyCompatOverride(host, cached);
369
+ if (resolved !== cached) hostInfoCache.set(host.name, resolved);
370
+ return resolved;
371
+ }
372
+ try {
373
+ const parsed = parseHostInfo(JSON.parse(fs.readFileSync(getHostInfoPath(host.name), "utf-8")));
374
+ if (!parsed) return undefined;
375
+ const resolved = applyCompatOverride(host, parsed);
376
+ hostInfoCache.set(host.name, resolved);
377
+ return resolved;
378
+ } catch (err) {
379
+ if (isEnoent(err)) return undefined;
380
+ logger.warn("Failed to load SSH host info", { host: host.name, error: String(err) });
381
+ return undefined;
382
+ }
383
+ }
384
+
358
385
  export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
359
386
  const cached = hostInfoCache.get(host.name);
360
387
  if (cached) {
@@ -120,7 +120,8 @@ export function getCommand(commands: WorkflowCommand[], name: string): WorkflowC
120
120
  * Replaces $@ with the provided input.
121
121
  */
122
122
  export function expandCommand(command: WorkflowCommand, input: string): string {
123
- return command.instructions.replace(/\$@/g, input);
123
+ // Function replacement so `$`-patterns in user input ($$, $&, ...) stay literal.
124
+ return command.instructions.replace(/\$@/g, () => input);
124
125
  }
125
126
 
126
127
  /**
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Agent discovery from filesystem.
3
3
  *
4
- * Discovers agent definitions from:
5
- * - ~/.omp/agent/agents/*.md (user-level, primary)
6
- * - ~/.pi/agent/agents/*.md (user-level, legacy)
7
- * - ~/.claude/agents/*.md (user-level, legacy)
8
- * - .omp/agents/*.md (project-level, primary)
9
- * - .pi/agents/*.md (project-level, legacy)
10
- * - .claude/agents/*.md (project-level, legacy)
4
+ * Discovers agent definitions from OMP-native task-agent roots:
5
+ * - ~/.omp/agent/agents/*.md (user-level)
6
+ * - .omp/agents/*.md (project-level)
7
+ *
8
+ * Claude Code marketplace plugin agents are discovered separately via the
9
+ * claude-plugins provider. Direct cross-harness roots such as .claude/agents
10
+ * are intentionally skipped because their frontmatter schema is not the OMP
11
+ * task-agent contract.
11
12
  *
12
13
  * Agent files use markdown with YAML frontmatter.
13
14
  */
@@ -21,6 +22,8 @@ import { listClaudePluginRoots } from "../discovery/helpers";
21
22
  import { loadBundledAgents, parseAgent } from "./agents";
22
23
  import type { AgentDefinition, AgentSource } from "./types";
23
24
 
25
+ const TASK_AGENT_CONFIG_SOURCE = ".omp";
26
+
24
27
  /** Result of agent discovery */
25
28
  export interface DiscoveryResult {
26
29
  agents: AgentDefinition[];
@@ -52,41 +55,31 @@ async function loadAgentsFromDir(dir: string, source: AgentSource): Promise<Agen
52
55
  /**
53
56
  * Discover agents from filesystem and merge with bundled agents.
54
57
  *
55
- * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
56
- *
58
+ * Precedence (highest wins): project .omp, user .omp, Claude plugin agents, then bundled
57
59
  * @param cwd - Current working directory for project agent discovery
58
60
  */
59
61
  export async function discoverAgents(cwd: string, home: string = os.homedir()): Promise<DiscoveryResult> {
60
62
  const resolvedCwd = path.resolve(cwd);
61
- const agentSources = Array.from(new Set(getConfigDirs("", { project: false }).map(entry => entry.source)));
62
63
 
63
- // Get user directories (priority order: .omp, .pi, .claude, ...)
64
64
  const userDirs = getConfigDirs("agents", { project: false })
65
- .filter(entry => agentSources.includes(entry.source))
65
+ .filter(entry => entry.source === TASK_AGENT_CONFIG_SOURCE)
66
66
  .map(entry => ({
67
67
  ...entry,
68
68
  path: path.resolve(entry.path),
69
69
  }));
70
70
 
71
- // Get project directories by walking up from cwd (priority order)
72
71
  const projectDirs = findAllNearestProjectConfigDirs("agents", resolvedCwd)
73
- .filter(entry => agentSources.includes(entry.source))
72
+ .filter(entry => entry.source === TASK_AGENT_CONFIG_SOURCE)
74
73
  .map(entry => ({
75
74
  ...entry,
76
75
  path: path.resolve(entry.path),
77
76
  }));
78
77
 
79
- const orderedSources = agentSources.filter(
80
- source => userDirs.some(entry => entry.source === source) || projectDirs.some(entry => entry.source === source),
81
- );
82
-
83
78
  const orderedDirs: Array<{ dir: string; source: AgentSource }> = [];
84
- for (const source of orderedSources) {
85
- const project = projectDirs.find(entry => entry.source === source);
86
- if (project) orderedDirs.push({ dir: project.path, source: "project" });
87
- const user = userDirs.find(entry => entry.source === source);
88
- if (user) orderedDirs.push({ dir: user.path, source: "user" });
89
- }
79
+ const project = projectDirs[0];
80
+ if (project) orderedDirs.push({ dir: project.path, source: "project" });
81
+ const user = userDirs[0];
82
+ if (user) orderedDirs.push({ dir: user.path, source: "user" });
90
83
 
91
84
  // Load agents from Claude Code marketplace plugins (respects disabledProviders)
92
85
  const { roots: pluginRoots } = isProviderEnabled("claude-plugins")
@@ -1285,59 +1285,67 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1285
1285
 
1286
1286
  const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
1287
1287
 
1288
- const { session } = await awaitAbortable(
1289
- createAgentSession({
1290
- cwd: worktree ?? cwd,
1291
- authStorage,
1292
- modelRegistry,
1293
- settings: subagentSettings,
1294
- model,
1295
- thinkingLevel: effectiveThinkingLevel,
1296
- toolNames,
1297
- outputSchema,
1298
- requireYieldTool: true,
1299
- contextFiles: options.contextFiles,
1300
- skills: options.skills,
1301
- promptTemplates: options.promptTemplates,
1302
- workspaceTree: options.workspaceTree,
1303
- rules: options.rules,
1304
- preloadedExtensionPaths: options.preloadedExtensionPaths,
1305
- preloadedCustomToolPaths: options.preloadedCustomToolPaths,
1306
- systemPrompt: defaultPrompt => {
1307
- const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
1308
- agent: agent.systemPrompt,
1309
- context: options.context?.trim() ?? "",
1310
- planReference: options.planReference?.content ?? "",
1311
- planReferencePath: options.planReference?.path ?? "",
1312
- worktree: worktree ?? "",
1313
- outputSchema: normalizedOutputSchema,
1314
- contextFile: contextFileForPrompt,
1315
- ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
1316
- ircSelfId: ircEnabled ? id : "",
1317
- });
1318
- return defaultPrompt.length === 0
1319
- ? [subagentPrompt]
1320
- : [...defaultPrompt.slice(0, -1), subagentPrompt, defaultPrompt[defaultPrompt.length - 1]];
1321
- },
1322
- sessionManager,
1323
- hasUI: false,
1324
- spawns: spawnsEnv,
1325
- taskDepth: childDepth,
1326
- parentHindsightSessionState: options.parentHindsightSessionState,
1327
- parentMnemopiSessionState: options.parentMnemopiSessionState,
1328
- parentTaskPrefix: id,
1329
- agentId: id,
1330
- agentDisplayName: agent.name,
1331
- enableLsp: lspEnabled,
1332
- skipPythonPreflight,
1333
- enableMCP,
1334
- mcpManager: options.mcpManager,
1335
- customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
1336
- localProtocolOptions: options.localProtocolOptions,
1337
- telemetry: subagentTelemetry,
1338
- parentEvalSessionId: options.parentEvalSessionId,
1339
- }),
1340
- );
1288
+ const sessionPromise = createAgentSession({
1289
+ cwd: worktree ?? cwd,
1290
+ authStorage,
1291
+ modelRegistry,
1292
+ settings: subagentSettings,
1293
+ model,
1294
+ thinkingLevel: effectiveThinkingLevel,
1295
+ toolNames,
1296
+ outputSchema,
1297
+ requireYieldTool: true,
1298
+ contextFiles: options.contextFiles,
1299
+ skills: options.skills,
1300
+ promptTemplates: options.promptTemplates,
1301
+ workspaceTree: options.workspaceTree,
1302
+ rules: options.rules,
1303
+ preloadedExtensionPaths: options.preloadedExtensionPaths,
1304
+ preloadedCustomToolPaths: options.preloadedCustomToolPaths,
1305
+ systemPrompt: defaultPrompt => {
1306
+ const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
1307
+ agent: agent.systemPrompt,
1308
+ context: options.context?.trim() ?? "",
1309
+ planReference: options.planReference?.content ?? "",
1310
+ planReferencePath: options.planReference?.path ?? "",
1311
+ worktree: worktree ?? "",
1312
+ outputSchema: normalizedOutputSchema,
1313
+ contextFile: contextFileForPrompt,
1314
+ ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
1315
+ ircSelfId: ircEnabled ? id : "",
1316
+ });
1317
+ return defaultPrompt.length === 0
1318
+ ? [subagentPrompt]
1319
+ : [...defaultPrompt.slice(0, -1), subagentPrompt, defaultPrompt[defaultPrompt.length - 1]];
1320
+ },
1321
+ sessionManager,
1322
+ hasUI: false,
1323
+ spawns: spawnsEnv,
1324
+ taskDepth: childDepth,
1325
+ parentHindsightSessionState: options.parentHindsightSessionState,
1326
+ parentMnemopiSessionState: options.parentMnemopiSessionState,
1327
+ parentTaskPrefix: id,
1328
+ agentId: id,
1329
+ agentDisplayName: agent.name,
1330
+ enableLsp: lspEnabled,
1331
+ skipPythonPreflight,
1332
+ enableMCP,
1333
+ mcpManager: options.mcpManager,
1334
+ customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
1335
+ localProtocolOptions: options.localProtocolOptions,
1336
+ telemetry: subagentTelemetry,
1337
+ parentEvalSessionId: options.parentEvalSessionId,
1338
+ });
1339
+ let session: AgentSession;
1340
+ try {
1341
+ ({ session } = await awaitAbortable(sessionPromise));
1342
+ } catch (err) {
1343
+ // Abort raced session startup. The session may still resolve later
1344
+ // holding live LSP/MCP child processes — dispose it when it does so
1345
+ // a cancelled subagent cannot leak them.
1346
+ void sessionPromise.then(created => created.session.dispose()).catch(() => {});
1347
+ throw err;
1348
+ }
1341
1349
 
1342
1350
  activeSession = session;
1343
1351
 
package/src/task/index.ts CHANGED
@@ -43,7 +43,7 @@ import type { LocalProtocolOptions } from "../internal-urls";
43
43
  import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
44
44
  import { generateCommitMessage } from "../utils/commit-message-generator";
45
45
  import * as git from "../utils/git";
46
- import { discoverAgents, getAgent } from "./discovery";
46
+ import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
47
47
  import { runSubprocess } from "./executor";
48
48
  import { AgentOutputManager } from "./output-manager";
49
49
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
@@ -242,6 +242,88 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
242
242
  return "task.simple is set to independent, so the task tool does not accept `context` or `schema`. Put all required background and output expectations inside each task assignment or the selected agent definition.";
243
243
  }
244
244
 
245
+ /** Sentinel for async jobs whose subagent finished with a failing result; batch counters are already updated. */
246
+ class TaskJobError extends Error {}
247
+
248
+ /**
249
+ * Validate task ids: every task needs a non-empty id and ids must be unique
250
+ * (case-insensitive). Returns a problem description, or undefined when valid.
251
+ */
252
+ function validateTaskIds(tasks: TaskParams["tasks"]): string | undefined {
253
+ const missingTaskIndexes: number[] = [];
254
+ const idIndexes = new Map<string, number[]>();
255
+
256
+ for (let i = 0; i < tasks.length; i++) {
257
+ const id = tasks[i]?.id;
258
+ if (typeof id !== "string" || id.trim() === "") {
259
+ missingTaskIndexes.push(i);
260
+ continue;
261
+ }
262
+ const normalizedId = id.toLowerCase();
263
+ const indexes = idIndexes.get(normalizedId);
264
+ if (indexes) {
265
+ indexes.push(i);
266
+ } else {
267
+ idIndexes.set(normalizedId, [i]);
268
+ }
269
+ }
270
+
271
+ const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
272
+ for (const [normalizedId, indexes] of idIndexes.entries()) {
273
+ if (indexes.length > 1) {
274
+ duplicateIds.push({
275
+ id: tasks[indexes[0]]?.id ?? normalizedId,
276
+ indexes,
277
+ });
278
+ }
279
+ }
280
+
281
+ if (missingTaskIndexes.length === 0 && duplicateIds.length === 0) {
282
+ return undefined;
283
+ }
284
+
285
+ const problems: string[] = [];
286
+ if (missingTaskIndexes.length > 0) {
287
+ problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
288
+ }
289
+ if (duplicateIds.length > 0) {
290
+ const details = duplicateIds.map(entry => `${entry.id} (indexes ${entry.indexes.join(", ")})`).join("; ");
291
+ problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
292
+ }
293
+ return `Invalid tasks: ${problems.join(". ")}`;
294
+ }
295
+
296
+ /**
297
+ * Process-level memo for create-time agent discovery, keyed by resolved cwd.
298
+ *
299
+ * `TaskTool.create` runs for every (sub)agent session in this process and the
300
+ * walk-up + plugin-registry scan in `discoverAgents` is identical for a given
301
+ * cwd, so repeat creations reuse the first scan. Execution-time discovery
302
+ * (`#executeSync`) intentionally stays fresh. The memo also tracks the live
303
+ * `discoverAgents` binding: test spies swap that binding, which invalidates
304
+ * the memo automatically.
305
+ */
306
+ const discoveryMemo = new Map<string, Promise<DiscoveryResult>>();
307
+ let discoveryMemoFn: typeof discoverAgents | undefined;
308
+
309
+ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
310
+ const fn = discoverAgents;
311
+ if (discoveryMemoFn !== fn) {
312
+ discoveryMemoFn = fn;
313
+ discoveryMemo.clear();
314
+ }
315
+ const key = path.resolve(cwd);
316
+ let pending = discoveryMemo.get(key);
317
+ if (!pending) {
318
+ pending = fn(cwd);
319
+ discoveryMemo.set(key, pending);
320
+ pending.catch(() => {
321
+ if (discoveryMemo.get(key) === pending) discoveryMemo.delete(key);
322
+ });
323
+ }
324
+ return pending;
325
+ }
326
+
245
327
  // ═══════════════════════════════════════════════════════════════════════════
246
328
  // Tool Class
247
329
  // ═══════════════════════════════════════════════════════════════════════════
@@ -325,7 +407,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
325
407
  * Create a TaskTool instance with async agent discovery.
326
408
  */
327
409
  static async create(session: ToolSession): Promise<TaskTool> {
328
- const { agents } = await discoverAgents(session.cwd);
410
+ const { agents } = await discoverAgentsForCreate(session.cwd);
329
411
  return new TaskTool(session, agents);
330
412
  }
331
413
 
@@ -363,6 +445,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
363
445
  return this.#executeSync(_toolCallId, params, signal, onUpdate);
364
446
  }
365
447
 
448
+ const taskIdProblem = validateTaskIds(taskItems);
449
+ if (taskIdProblem) {
450
+ return createTaskModeError(taskIdProblem);
451
+ }
452
+
366
453
  const outputManager =
367
454
  this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
368
455
  const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
@@ -396,9 +483,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
396
483
  let failedJobs = 0;
397
484
 
398
485
  const getProgressSnapshot = (): AgentProgress[] => {
486
+ // Shallow copies: top-level fields are reassigned (never mutated in
487
+ // place) and the large nested payloads (extractedToolData) are
488
+ // immutable once attached — structuredClone here cost O(batch × payload)
489
+ // per progress event.
399
490
  return Array.from(progressByTaskId.values())
400
491
  .sort((a, b) => a.index - b.index)
401
- .map(progress => structuredClone(progress));
492
+ .map(progress => ({ ...progress }));
402
493
  };
403
494
 
404
495
  const buildAsyncDetails = (state: "running" | "completed" | "failed", jobId: string): TaskToolDetails => ({
@@ -424,6 +515,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
424
515
  const taskItem = taskItems[i];
425
516
  if (signal?.aborted) {
426
517
  failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
518
+ completedJobs += 1;
427
519
  const progress = progressByTaskId.get(taskItem.id);
428
520
  if (progress) {
429
521
  progress.status = "aborted";
@@ -438,7 +530,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
438
530
  const jobId = manager.register(
439
531
  "task",
440
532
  label,
441
- async ({ signal: runSignal, reportProgress }) => {
533
+ async ({ signal: runSignal, reportProgress, markRunning }) => {
442
534
  const startedAt = Date.now();
443
535
  const progress = progressByTaskId.get(taskItem.id);
444
536
  await semaphore.acquire();
@@ -447,8 +539,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
447
539
  if (progress) {
448
540
  progress.status = "aborted";
449
541
  }
542
+ completedJobs += 1;
543
+ failedJobs += 1;
450
544
  throw new Error("Aborted before execution");
451
545
  }
546
+ markRunning();
452
547
  if (progress) {
453
548
  progress.status = "running";
454
549
  }
@@ -462,12 +557,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
462
557
  ]);
463
558
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
464
559
  const singleResult = result.details?.results[0];
560
+ // A missing per-task result means #executeSync failed at the
561
+ // tool level (results: []) — treat it as a failure, not success.
562
+ const resultFailed =
563
+ !singleResult || (singleResult.aborted ?? false) || singleResult.exitCode !== 0;
465
564
  if (progress) {
466
- progress.status = singleResult?.aborted
467
- ? "aborted"
468
- : (singleResult?.exitCode ?? 0) === 0
469
- ? "completed"
470
- : "failed";
565
+ progress.status = singleResult?.aborted ? "aborted" : resultFailed ? "failed" : "completed";
471
566
  progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
472
567
  progress.tokens = singleResult?.tokens ?? 0;
473
568
  progress.contextTokens = singleResult?.contextTokens;
@@ -478,7 +573,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
478
573
  progress.retryState = undefined;
479
574
  }
480
575
  completedJobs += 1;
481
- if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
576
+ if (resultFailed) {
482
577
  failedJobs += 1;
483
578
  }
484
579
  const remaining = taskItems.length - completedJobs;
@@ -498,8 +593,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
498
593
  `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
499
594
  );
500
595
  }
596
+ if (resultFailed) {
597
+ // Mark the job itself failed; counters above are already updated.
598
+ throw new TaskJobError(finalText);
599
+ }
501
600
  return finalText;
502
601
  } catch (error) {
602
+ if (error instanceof TaskJobError) {
603
+ throw error;
604
+ }
503
605
  if (progress) {
504
606
  progress.status = "failed";
505
607
  progress.durationMs = Math.max(0, Date.now() - startedAt);
@@ -530,6 +632,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
530
632
  },
531
633
  {
532
634
  id: label,
635
+ queued: true,
533
636
  ownerId: this.session.getAgentId?.() ?? undefined,
534
637
  onProgress: (text, details) => {
535
638
  const progressDetails =
@@ -543,6 +646,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
543
646
  } catch (error) {
544
647
  const message = error instanceof Error ? error.message : String(error);
545
648
  failedSchedules.push(`${taskItem.id}: ${message}`);
649
+ completedJobs += 1;
546
650
  const progress = progressByTaskId.get(taskItem.id);
547
651
  if (progress) {
548
652
  progress.status = "failed";
@@ -734,45 +838,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
734
838
  }
735
839
 
736
840
  const tasks = params.tasks;
737
- const missingTaskIndexes: number[] = [];
738
- const idIndexes = new Map<string, number[]>();
739
-
740
- for (let i = 0; i < tasks.length; i++) {
741
- const id = tasks[i]?.id;
742
- if (typeof id !== "string" || id.trim() === "") {
743
- missingTaskIndexes.push(i);
744
- continue;
745
- }
746
- const normalizedId = id.toLowerCase();
747
- const indexes = idIndexes.get(normalizedId);
748
- if (indexes) {
749
- indexes.push(i);
750
- } else {
751
- idIndexes.set(normalizedId, [i]);
752
- }
753
- }
754
-
755
- const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
756
- for (const [normalizedId, indexes] of idIndexes.entries()) {
757
- if (indexes.length > 1) {
758
- duplicateIds.push({
759
- id: tasks[indexes[0]]?.id ?? normalizedId,
760
- indexes,
761
- });
762
- }
763
- }
764
-
765
- if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
766
- const problems: string[] = [];
767
- if (missingTaskIndexes.length > 0) {
768
- problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
769
- }
770
- if (duplicateIds.length > 0) {
771
- const details = duplicateIds.map(entry => `${entry.id} (indexes ${entry.indexes.join(", ")})`).join("; ");
772
- problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
773
- }
841
+ const taskIdProblem = validateTaskIds(tasks);
842
+ if (taskIdProblem) {
774
843
  return {
775
- content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
844
+ content: [{ type: "text", text: taskIdProblem }],
776
845
  details: {
777
846
  projectAgentsDir,
778
847
  results: [],
@@ -951,7 +1020,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
951
1020
  }
952
1021
  emitProgress();
953
1022
 
954
- const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
1023
+ const runTask = async (
1024
+ task: (typeof tasksWithUniqueIds)[number],
1025
+ index: number,
1026
+ workerSignal?: AbortSignal,
1027
+ ) => {
955
1028
  if (!isIsolated) {
956
1029
  return runSubprocess({
957
1030
  cwd: this.session.cwd,
@@ -973,12 +1046,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
973
1046
  artifactsDir: effectiveArtifactsDir,
974
1047
  contextFile: contextFilePath,
975
1048
  enableLsp: subagentLspEnabled,
976
- signal,
1049
+ signal: workerSignal ?? signal,
977
1050
  eventBus: this.session.eventBus,
978
1051
  onProgress: progress => {
979
- progressMap.set(index, {
980
- ...structuredClone(progress),
981
- });
1052
+ // Shallow snapshot; recentTools is mutated in place by the
1053
+ // executor, the rest is reassigned or immutable. A deep clone
1054
+ // here cost O(extractedToolData) per progress event.
1055
+ progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
982
1056
  emitProgress();
983
1057
  },
984
1058
  authStorage: this.session.authStorage,
@@ -1034,12 +1108,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1034
1108
  artifactsDir: effectiveArtifactsDir,
1035
1109
  contextFile: contextFilePath,
1036
1110
  enableLsp: subagentLspEnabled,
1037
- signal,
1111
+ signal: workerSignal ?? signal,
1038
1112
  eventBus: this.session.eventBus,
1039
1113
  onProgress: progress => {
1040
- progressMap.set(index, {
1041
- ...structuredClone(progress),
1042
- });
1114
+ progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
1043
1115
  emitProgress();
1044
1116
  },
1045
1117
  authStorage: this.session.authStorage,
@@ -1226,6 +1298,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1226
1298
  const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
1227
1299
  mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
1228
1300
  }
1301
+ if (mergeResult.stashConflict) {
1302
+ mergeSummary += `\n\n<system-notification>${mergeResult.stashConflict}</system-notification>`;
1303
+ }
1229
1304
  }
1230
1305
 
1231
1306
  // Clean up merged branches (keep failed ones for manual resolution)
@@ -1234,9 +1309,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1234
1309
  await cleanupTaskBranches(repoRoot, allBranches);
1235
1310
  }
1236
1311
  } else {
1237
- // Patch mode: combine and apply patches
1238
- const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
1239
- const missingPatch = results.some(result => !result.patchPath);
1312
+ // Patch mode: apply patches from successful tasks. Failed or
1313
+ // aborted siblings must not block completed work from landing.
1314
+ const successfulResults = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted);
1315
+ const patchesInOrder = successfulResults.map(result => result.patchPath).filter(Boolean) as string[];
1316
+ const missingPatch = successfulResults.some(result => !result.patchPath);
1240
1317
  if (missingPatch) {
1241
1318
  changesApplied = false;
1242
1319
  hadAnyChanges = false;