@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.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 (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -6,6 +6,27 @@ const DELIVERY_RETRY_JITTER_MS = 200;
6
6
  const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
7
7
  const DEFAULT_MAX_RUNNING_JOBS = 15;
8
8
 
9
+ /**
10
+ * Adaptive ("smart") `job` poll-wait ladder (ms). A tight poll loop climbs
11
+ * these rungs so each immediate re-poll backs off and stops spending turns on
12
+ * "still running" frames; the floor (first rung) is the shortest wait and the
13
+ * top rung is the longest a smart poll will ever block. Only used when
14
+ * `async.pollWaitDuration` is set to `smart`; fixed durations wait verbatim.
15
+ */
16
+ const POLL_WAIT_LADDER_MS = [5_000, 10_000, 30_000, 60_000, 300_000] as const;
17
+ /**
18
+ * Going at least this long between poll calls means the agent stepped out of
19
+ * the poll loop to do real work — the next poll drops back to the ladder floor.
20
+ */
21
+ const POLL_ESCALATION_RESET_MS = 60_000;
22
+
23
+ interface PollEscalationState {
24
+ /** Index into POLL_WAIT_LADDER_MS used for the most recent poll wait. */
25
+ level: number;
26
+ /** Timestamp (ms) when the most recent poll wait returned. */
27
+ lastPollEndAt: number;
28
+ }
29
+
9
30
  export interface AsyncJob {
10
31
  id: string;
11
32
  type: "bash" | "task";
@@ -96,6 +117,7 @@ export class AsyncJobManager {
96
117
  readonly #suppressedDeliveries = new Set<string>();
97
118
  readonly #watchedJobs = new Set<string>();
98
119
  readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
120
+ readonly #pollEscalation = new Map<string | undefined, PollEscalationState>();
99
121
  readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
100
122
  readonly #maxRunningJobs: number;
101
123
  readonly #retentionMs: number;
@@ -295,6 +317,32 @@ export class AsyncJobManager {
295
317
  return removed;
296
318
  }
297
319
 
320
+ /**
321
+ * Compute the next adaptive ("smart") wait (ms) for a blocking `job` poll by
322
+ * the given owner. Consecutive polls — those starting within
323
+ * POLL_ESCALATION_RESET_MS of the previous poll returning — climb
324
+ * POLL_WAIT_LADDER_MS so a tight wait loop backs off; a longer gap means the
325
+ * agent left to do real work, so the wait resets to the floor. Pair each call
326
+ * with `recordPollWaitEnd()` once the wait returns.
327
+ */
328
+ nextPollWaitMs(ownerId: string | undefined, now: number = Date.now()): number {
329
+ const prev = this.#pollEscalation.get(ownerId);
330
+ const reset = !prev || now - prev.lastPollEndAt >= POLL_ESCALATION_RESET_MS;
331
+ const level = reset ? 0 : Math.min(prev.level + 1, POLL_WAIT_LADDER_MS.length - 1);
332
+ this.#pollEscalation.set(ownerId, { level, lastPollEndAt: prev?.lastPollEndAt ?? now });
333
+ return POLL_WAIT_LADDER_MS[level];
334
+ }
335
+
336
+ /**
337
+ * Mark a blocking poll wait as finished so the idle-reset window is measured
338
+ * from now. Polling again before POLL_ESCALATION_RESET_MS elapses keeps
339
+ * climbing the ladder; waiting longer resets it to the floor.
340
+ */
341
+ recordPollWaitEnd(ownerId: string | undefined, now: number = Date.now()): void {
342
+ const prev = this.#pollEscalation.get(ownerId);
343
+ this.#pollEscalation.set(ownerId, { level: prev?.level ?? 0, lastPollEndAt: now });
344
+ }
345
+
298
346
  acknowledgeDeliveries(jobIds: string[]): number {
299
347
  const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
300
348
  if (uniqueJobIds.length === 0) return 0;
@@ -405,6 +453,7 @@ export class AsyncJobManager {
405
453
  this.#inFlightDeliveries.length = 0;
406
454
  this.#suppressedDeliveries.clear();
407
455
  this.#watchedJobs.clear();
456
+ this.#pollEscalation.clear();
408
457
  return drained;
409
458
  }
410
459
 
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Auto-learn session controller (experimental).
3
+ *
4
+ * Subscribes to the session event stream and, after a substantive turn,
5
+ * nudges the agent to capture reusable lessons. Default posture is passive
6
+ * (a hidden reminder rides the next real turn); with `autolearn.autoContinue`
7
+ * it auto-runs exactly one synthetic capture turn at stop.
8
+ *
9
+ * Installed once per top-level session (taskDepth 0). The subscription lives
10
+ * for the session's lifetime — `newSession` resets the session in place
11
+ * without re-running startup — so the controller needs no disposal.
12
+ */
13
+ import { logger } from "@oh-my-pi/pi-utils";
14
+ import type { Settings } from "../config/settings";
15
+ import autolearnGuidance from "../prompts/system/autolearn-guidance.md" with { type: "text" };
16
+ import autolearnGuidanceLearn from "../prompts/system/autolearn-guidance-learn.md" with { type: "text" };
17
+ import autolearnNudge from "../prompts/system/autolearn-nudge.md" with { type: "text" };
18
+ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
19
+
20
+ const AUTOLEARN_NUDGE = autolearnNudge.trim();
21
+ const DEFAULT_MIN_TOOL_CALLS = 5;
22
+
23
+ /**
24
+ * Build the standing auto-learn guidance for the system prompt from the tools
25
+ * actually present in the active set, or null when `manage_skill` is absent.
26
+ *
27
+ * Driven by tool presence rather than live settings: the `learn`/`manage_skill`
28
+ * registry is built ONCE at session start (and only for top-level sessions), so
29
+ * keying the guidance on `autolearn.enabled` would let a mid-session enable — or
30
+ * a subagent that filtered the tools out — inject guidance pointing at tools the
31
+ * session never built. The `learn` addendum is included only when the `learn`
32
+ * tool is present (it requires a memory backend).
33
+ */
34
+ export function buildAutoLearnInstructions(available: { manageSkill: boolean; learn: boolean }): string | null {
35
+ if (!available.manageSkill) return null;
36
+ const parts = [autolearnGuidance.trim()];
37
+ if (available.learn) parts.push(autolearnGuidanceLearn.trim());
38
+ return parts.join("\n\n");
39
+ }
40
+
41
+ export interface AutoLearnControllerOptions {
42
+ session: AgentSession;
43
+ settings: Settings;
44
+ }
45
+
46
+ export class AutoLearnController {
47
+ readonly #session: AgentSession;
48
+ readonly #settings: Settings;
49
+ #toolCalls = 0;
50
+ /**
51
+ * Whether the in-flight turn BEGAN while goal mode was active. Captured at
52
+ * agent_start because a `goal` tool can complete or drop the goal mid-turn,
53
+ * clearing the live flag before agent_end — so the end-of-turn state alone
54
+ * would let a goal-continuation turn slip through and get nudged.
55
+ */
56
+ #turnStartedInGoalMode = false;
57
+ /** Swallow the agent_end produced by an auto-run capture turn so it cannot re-trigger. */
58
+ #suppressNext = false;
59
+
60
+ constructor(options: AutoLearnControllerOptions) {
61
+ this.#session = options.session;
62
+ this.#settings = options.settings;
63
+ // The listener closure captures `this`, so the session's listener array
64
+ // keeps the controller alive — no stored unsubscribe needed.
65
+ this.#session.subscribe(event => this.#onEvent(event));
66
+ }
67
+
68
+ #onEvent(event: AgentSessionEvent): void {
69
+ if (event.type === "agent_start") {
70
+ // Capture goal-mode state at the turn boundary, before any tool runs.
71
+ this.#turnStartedInGoalMode = this.#session.getGoalModeState()?.enabled === true;
72
+ return;
73
+ }
74
+ if (event.type === "tool_execution_end") {
75
+ this.#toolCalls++;
76
+ return;
77
+ }
78
+ if (event.type === "agent_end") {
79
+ this.#onAgentEnd();
80
+ }
81
+ }
82
+
83
+ #onAgentEnd(): void {
84
+ // Snapshot and reset every turn: the counter describes only the
85
+ // just-finished turn, so below-threshold, disabled, and plan-mode stops
86
+ // must not let tool calls accumulate into a later turn.
87
+ const toolCalls = this.#toolCalls;
88
+ this.#toolCalls = 0;
89
+ // Snapshot the turn-start goal flag alongside the counter so a turn that
90
+ // observed no agent_start can never inherit a stale value.
91
+ const startedInGoalMode = this.#turnStartedInGoalMode;
92
+ this.#turnStartedInGoalMode = false;
93
+
94
+ if (this.#suppressNext) {
95
+ this.#suppressNext = false;
96
+ return;
97
+ }
98
+ // Honor a live opt-out: the subscription outlives the setting, so re-check
99
+ // the current flag rather than trusting install-time state.
100
+ if (!this.#settings.get("autolearn.enabled")) return;
101
+ const minToolCalls = this.#settings.get("autolearn.minToolCalls") ?? DEFAULT_MIN_TOOL_CALLS;
102
+ if (toolCalls < minToolCalls) return;
103
+ // Never interrupt plan-mode review.
104
+ if (this.#session.getPlanModeState()?.enabled) return;
105
+ // Never divert a goal loop. Skip when the turn STARTED in goal mode — a
106
+ // `goal` tool may have completed/dropped the goal before this stop — or is
107
+ // still in it: a passive nudge would ride the goal continuation, and
108
+ // auto-continue would compete with it.
109
+ if (startedInGoalMode || this.#session.getGoalModeState()?.enabled) return;
110
+
111
+ // Auto-run a capture turn only when explicitly enabled; otherwise the
112
+ // hidden reminder rides the next real turn passively.
113
+ const autoContinue = this.#settings.get("autolearn.autoContinue") === true;
114
+ // Arm suppression synchronously: the synthetic capture turn's agent_end
115
+ // fires inside sendCustomMessage (before it resolves), so the flag must be
116
+ // set before then. Disarm when no turn actually started — a deferred/queued
117
+ // dispatch or a failed send produces no agent_end, and a latched flag would
118
+ // otherwise swallow the next real stop.
119
+ if (autoContinue) this.#suppressNext = true;
120
+
121
+ this.#session
122
+ .sendCustomMessage(
123
+ {
124
+ customType: "autolearn-nudge",
125
+ content: AUTOLEARN_NUDGE,
126
+ display: false,
127
+ attribution: "user",
128
+ },
129
+ { deliverAs: "nextTurn", triggerTurn: autoContinue },
130
+ )
131
+ .then(started => {
132
+ if (!started) this.#suppressNext = false;
133
+ })
134
+ .catch(err => {
135
+ this.#suppressNext = false;
136
+ logger.warn("auto-learn nudge delivery failed", { err });
137
+ });
138
+ }
139
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Managed-skills primitives for the experimental auto-learn feature.
3
+ *
4
+ * Managed skills are auto-generated/enhanced `SKILL.md` files kept in an
5
+ * isolated directory (`~/.omp/agent/managed-skills`) separate from
6
+ * user-authored skills (`~/.omp/agent/skills`). They are discovered and
7
+ * surfaced like normal skills, but every write here is confined to
8
+ * `getManagedSkillsDir()` — auto-management can never touch authored skills.
9
+ */
10
+ import { constants as fsConstants, type Stats } from "node:fs";
11
+ import * as fs from "node:fs/promises";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { isEnoent } from "@oh-my-pi/pi-utils";
15
+ import { YAML } from "bun";
16
+ import { SOURCE_PATHS } from "../discovery/helpers";
17
+
18
+ /** Provider id stamped on discovered managed skills (distinguishes them from authored). */
19
+ export const MANAGED_SKILLS_PROVIDER_ID = "omp-managed";
20
+
21
+ /** Hard cap on a managed SKILL.md body to keep generated skills bounded. */
22
+ export const MAX_MANAGED_SKILL_BYTES = 64_000;
23
+
24
+ const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
25
+
26
+ /** Resolve the isolated managed-skills directory (`~/.omp/agent/managed-skills`). */
27
+ export function getManagedSkillsDir(home: string = os.homedir()): string {
28
+ return path.join(home, SOURCE_PATHS.native.userAgent, "managed-skills");
29
+ }
30
+
31
+ /**
32
+ * Validate + normalize a managed-skill name. Throws on anything outside the
33
+ * strict allowlist so a bad name can never escape `getManagedSkillsDir()`
34
+ * (blocks `..`, slashes, empty, and uppercase).
35
+ */
36
+ export function sanitizeSkillName(raw: string): string {
37
+ const name = raw.trim().toLowerCase();
38
+ if (!SKILL_NAME_PATTERN.test(name)) {
39
+ throw new Error(
40
+ `Invalid skill name "${raw}". Use lowercase letters, digits, and hyphens (1-64 chars, starting with a letter or digit).`,
41
+ );
42
+ }
43
+ return name;
44
+ }
45
+
46
+ /**
47
+ * Whether `name` is a safe managed-skill name (the exact post-sanitize shape).
48
+ * Used to validate names read from disk at discovery time — a managed
49
+ * `SKILL.md` whose `frontmatter.name` was not produced by `sanitizeSkillName`
50
+ * (e.g. hand-placed) must not render unescaped into the system prompt.
51
+ */
52
+ export function isValidManagedSkillName(name: string): boolean {
53
+ return SKILL_NAME_PATTERN.test(name);
54
+ }
55
+
56
+ /**
57
+ * Neutralize a machine-generated managed-skill description so it cannot break
58
+ * out of the system prompt's `<skills>` listing. Managed descriptions are
59
+ * generated from prior task content and persist across sessions, so this is a
60
+ * trust boundary: strip control/format chars, angle brackets (`<system-directive>`
61
+ * / `</skills>`), and Markdown fence delimiters (backticks, `~~~`), then collapse
62
+ * to a single line. Applied on BOTH write and read so existing files are safe too.
63
+ */
64
+ export function sanitizeManagedDescription(raw: string): string {
65
+ return raw
66
+ .replace(/[\p{Cc}\p{Cf}]/gu, " ")
67
+ .replace(/[<>`]/g, "")
68
+ .replace(/~{2,}/g, "~")
69
+ .replace(/\s+/g, " ")
70
+ .trim();
71
+ }
72
+
73
+ /**
74
+ * Serialize the minimal `name`/`description` frontmatter block via the repo's
75
+ * YAML helper (round-trips through `parseFrontmatter`).
76
+ */
77
+ export function toSkillFrontmatter(name: string, description: string): string {
78
+ const frontmatter = YAML.stringify(
79
+ { name, description: sanitizeManagedDescription(description) },
80
+ null,
81
+ 2,
82
+ ).trimEnd();
83
+ return `---\n${frontmatter}\n---\n`;
84
+ }
85
+
86
+ export interface WriteManagedSkillInput {
87
+ action: "create" | "update";
88
+ name: string;
89
+ description: string;
90
+ body: string;
91
+ }
92
+
93
+ /**
94
+ * Serialize create/update/delete on the same skill name. Both tools are
95
+ * non-exclusive, so a parallel tool batch in one turn can run two mutations on
96
+ * the same skill at once (e.g. an update observing the file mid-delete). This
97
+ * per-name promise chain runs same-skill mutations in submission order while
98
+ * different names still proceed in parallel. In-process only; cross-process
99
+ * races are out of scope.
100
+ */
101
+ const skillMutationChains = new Map<string, Promise<unknown>>();
102
+ function serializeSkillMutation<T>(name: string, op: () => Promise<T>): Promise<T> {
103
+ const prev = skillMutationChains.get(name) ?? Promise.resolve();
104
+ const run = prev.then(op, op);
105
+ const guarded = run.catch(() => {});
106
+ skillMutationChains.set(name, guarded);
107
+ void guarded.finally(() => {
108
+ if (skillMutationChains.get(name) === guarded) skillMutationChains.delete(name);
109
+ });
110
+ return run;
111
+ }
112
+
113
+ /**
114
+ * Reject when the managed-skills root itself is a symlink. lstat on a child
115
+ * follows intermediate components, so a symlinked root would let an otherwise
116
+ * valid name write/delete outside the isolated directory (e.g. onto authored
117
+ * skills). Checked before composing any child path.
118
+ */
119
+ async function assertManagedRootSafe(): Promise<void> {
120
+ const rootStat = await fs.lstat(getManagedSkillsDir()).catch(err => {
121
+ if (isEnoent(err)) return null;
122
+ throw err;
123
+ });
124
+ if (rootStat?.isSymbolicLink()) {
125
+ throw new Error("The managed-skills root is a symlink; refusing to operate outside the managed directory.");
126
+ }
127
+ }
128
+
129
+ const UPDATE_FILE_OPEN_FLAGS = fsConstants.O_WRONLY | fsConstants.O_NOFOLLOW;
130
+
131
+ function assertManagedSkillFileSafeForUpdate(name: string, fileStat: Stats): void {
132
+ if (!fileStat.isFile()) {
133
+ throw new Error(`Managed skill "${name}" SKILL.md is not a regular file; refusing to overwrite it.`);
134
+ }
135
+ if (fileStat.nlink > 1) {
136
+ throw new Error(
137
+ `Managed skill "${name}" SKILL.md has ${fileStat.nlink} hard links; refusing to overwrite a file that may be user-authored elsewhere.`,
138
+ );
139
+ }
140
+ }
141
+
142
+ async function openManagedSkillFileForUpdate(name: string, file: string) {
143
+ try {
144
+ return await fs.open(file, UPDATE_FILE_OPEN_FLAGS);
145
+ } catch (err) {
146
+ if ((err as { code?: string }).code === "ELOOP") {
147
+ throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);
148
+ }
149
+ throw err;
150
+ }
151
+ }
152
+
153
+ /** Create or update a managed `SKILL.md`. Returns the resolved file path. */
154
+ export async function writeManagedSkill(input: WriteManagedSkillInput): Promise<{ path: string }> {
155
+ const name = sanitizeSkillName(input.name);
156
+ const description = sanitizeManagedDescription(input.description);
157
+ const body = input.body.trim();
158
+ // Reject empty content: an all-whitespace/control description sanitizes to ""
159
+ // and the `requireDescription` discovery scan then silently drops the skill,
160
+ // so the tool would report success for a skill that never appears.
161
+ if (!description) {
162
+ throw new Error(`Managed skill "${name}" needs a non-empty description.`);
163
+ }
164
+ if (!body) {
165
+ throw new Error(`Managed skill "${name}" needs a non-empty body.`);
166
+ }
167
+ const content = `${toSkillFrontmatter(name, description)}\n${body}\n`;
168
+ // Cap the UTF-8 byte size of the FINAL file (body + description + frontmatter),
169
+ // not the UTF-16 code-unit length of the body alone.
170
+ const bytes = Buffer.byteLength(content, "utf8");
171
+ if (bytes > MAX_MANAGED_SKILL_BYTES) {
172
+ throw new Error(
173
+ `Managed skill is ${bytes} bytes; the limit is ${MAX_MANAGED_SKILL_BYTES}. Trim the body or description.`,
174
+ );
175
+ }
176
+ return serializeSkillMutation(name, async () => {
177
+ await assertManagedRootSafe();
178
+ const dir = path.join(getManagedSkillsDir(), name);
179
+ const file = path.join(dir, "SKILL.md");
180
+ // Reject a symlinked skill directory: an intermediate symlink would let the
181
+ // write escape the isolated managed root. lstat does not follow the final
182
+ // component, so a symlinked `dir` is caught here.
183
+ const dirStat = await fs.lstat(dir).catch(err => {
184
+ if (isEnoent(err)) return null;
185
+ throw err;
186
+ });
187
+ if (dirStat?.isSymbolicLink()) {
188
+ throw new Error(
189
+ `Managed skill "${name}" resolves through a symlink; refusing to write outside the managed directory.`,
190
+ );
191
+ }
192
+ if (input.action === "create") {
193
+ await fs.mkdir(dir, { recursive: true });
194
+ // O_CREAT|O_EXCL ("wx"): atomic create that fails if the file already
195
+ // exists (closing the check-then-write race) and refuses a symlinked SKILL.md.
196
+ try {
197
+ await fs.writeFile(file, content, { flag: "wx" });
198
+ } catch (err) {
199
+ if ((err as { code?: string }).code === "EEXIST") {
200
+ throw new Error(`Managed skill "${name}" already exists. Use action "update" to change it.`);
201
+ }
202
+ throw err;
203
+ }
204
+ return { path: file };
205
+ }
206
+ // update: the file must already exist, be a plain managed file, and must
207
+ // not share an inode with a user-authored file via hard link. Open the
208
+ // checked file handle before truncating so a path swap after lstat cannot
209
+ // redirect the write into a symlink or newly hard-linked target.
210
+ const fileStat = await fs.lstat(file).catch(err => {
211
+ if (isEnoent(err)) return null;
212
+ throw err;
213
+ });
214
+ if (fileStat === null) {
215
+ throw new Error(`Managed skill "${name}" does not exist. Use action "create" to add it.`);
216
+ }
217
+ if (fileStat.isSymbolicLink()) {
218
+ throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);
219
+ }
220
+ assertManagedSkillFileSafeForUpdate(name, fileStat);
221
+ const handle = await openManagedSkillFileForUpdate(name, file);
222
+ try {
223
+ const openStat = await handle.stat();
224
+ assertManagedSkillFileSafeForUpdate(name, openStat);
225
+ await handle.truncate(0);
226
+ await handle.writeFile(content);
227
+ } finally {
228
+ await handle.close();
229
+ }
230
+ return { path: file };
231
+ });
232
+ }
233
+
234
+ /** Delete a managed skill directory. Throws when it does not exist. */
235
+ export async function deleteManagedSkill(name: string): Promise<void> {
236
+ const safe = sanitizeSkillName(name);
237
+ await serializeSkillMutation(safe, async () => {
238
+ await assertManagedRootSafe();
239
+ const dir = path.join(getManagedSkillsDir(), safe);
240
+ // Refuse to follow a symlinked skill directory (rm would delete the target).
241
+ const dirStat = await fs.lstat(dir).catch(err => {
242
+ if (isEnoent(err)) return null;
243
+ throw err;
244
+ });
245
+ if (dirStat?.isSymbolicLink()) {
246
+ throw new Error(`Managed skill "${safe}" is a symlink; refusing to delete outside the managed directory.`);
247
+ }
248
+ try {
249
+ await fs.rm(dir, { recursive: true });
250
+ } catch (err) {
251
+ if (isEnoent(err)) {
252
+ throw new Error(`Managed skill "${safe}" does not exist.`);
253
+ }
254
+ throw err;
255
+ }
256
+ });
257
+ }
@@ -1,4 +1,4 @@
1
- import type { SessionEntry } from "../session/session-manager";
1
+ import type { SessionEntry } from "../session/session-entries";
2
2
  import { inferMetricUnitFromName, isBetter } from "./helpers";
3
3
  import type { RunRow, SessionRow } from "./storage";
4
4
  import type {
@@ -194,7 +194,6 @@ const SCHEMA_VERSION = 1;
194
194
  const SCHEMA_SQL = `
195
195
  PRAGMA journal_mode=WAL;
196
196
  PRAGMA synchronous=NORMAL;
197
- PRAGMA busy_timeout=5000;
198
197
  PRAGMA foreign_keys=ON;
199
198
 
200
199
  CREATE TABLE IF NOT EXISTS sessions (
@@ -263,6 +262,8 @@ export class AutoresearchStorage {
263
262
  this.#projectDir = projectDir;
264
263
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
265
264
  this.#db = new Database(dbPath);
265
+ // Install the busy handler BEFORE any lock-taking statement. See #2421.
266
+ this.#db.run("PRAGMA busy_timeout = 5000");
266
267
  this.#db.run(SCHEMA_SQL);
267
268
  const versionRow = this.#db.query("PRAGMA user_version").get() as { user_version: number } | null;
268
269
  const currentVersion = versionRow?.user_version ?? 0;
@@ -1,7 +1,7 @@
1
1
  import * as path from "node:path";
2
2
 
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
- import * as z from "zod/v4";
4
+ import { z } from "zod/v4";
5
5
  import type { ToolDefinition } from "../../extensibility/extensions";
6
6
  import type { Theme } from "../../modes/theme/theme";
7
7
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
- import * as z from "zod/v4";
5
+ import { z } from "zod/v4";
6
6
  import type { ToolDefinition } from "../../extensibility/extensions";
7
7
  import type { Theme } from "../../modes/theme/theme";
8
8
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { formatBytes } from "@oh-my-pi/pi-utils";
5
- import * as z from "zod/v4";
5
+ import { z } from "zod/v4";
6
6
  import { executeBash } from "../../exec/bash-executor";
7
7
  import type { ToolDefinition } from "../../extensibility/extensions";
8
8
  import type { Theme } from "../../modes/theme/theme";
@@ -1,5 +1,5 @@
1
1
  import { Text } from "@oh-my-pi/pi-tui";
2
- import * as z from "zod/v4";
2
+ import { z } from "zod/v4";
3
3
  import type { ToolDefinition } from "../../extensibility/extensions";
4
4
  import type { Theme } from "../../modes/theme/theme";
5
5
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -1,6 +1,6 @@
1
1
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ExtensionAPI, ExtensionContext } from "../extensibility/extensions";
3
- import type { SessionEntry } from "../session/session-manager";
3
+ import type { SessionEntry } from "../session/session-entries";
4
4
  import type { TruncationResult } from "../session/streaming-output";
5
5
 
6
6
  export type MetricDirection = "lower" | "higher";
package/src/cli/args.ts CHANGED
@@ -48,14 +48,24 @@ export interface Args {
48
48
  noSkills?: boolean;
49
49
  skills?: string[];
50
50
  noRules?: boolean;
51
- listModels?: string | true;
52
51
  noTitle?: boolean;
53
52
  autoApprove?: boolean;
54
53
  approvalMode?: "always-ask" | "write" | "yolo";
55
54
  messages: string[];
56
55
  fileArgs: string[];
57
- /** Unknown flags (potentially extension flags) - map of flag name to value */
56
+ /** Extension-registered flags this parse recognized name to value. */
58
57
  unknownFlags: Map<string, boolean | string>;
58
+ /**
59
+ * `--`/`-` prefixed tokens this parse could not match against any built-in
60
+ * or {@link extensionFlags} entry. The startup parse runs *before*
61
+ * extensions load, so it always lists every extension-registered flag here;
62
+ * the post-extension reparse in {@link applyExtensionFlags} clears those
63
+ * once the real flag set is known. Anything still present after that
64
+ * reparse is a genuine typo or stale flag and {@link reportUnrecognizedFlags}
65
+ * surfaces it as a hard error so the agent does not silently start a
66
+ * session with the misparsed positionals as a prompt (issue #2459).
67
+ */
68
+ unrecognizedFlags: string[];
59
69
  }
60
70
 
61
71
  export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
@@ -68,12 +78,23 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
68
78
  messages: [],
69
79
  fileArgs: [],
70
80
  unknownFlags: new Map(),
81
+ unrecognizedFlags: [],
71
82
  };
72
83
 
84
+ let sawSeparator = false;
73
85
  for (let i = 0; i < args.length; i++) {
74
86
  let arg = args[i];
75
87
  const flagIndex = i;
76
88
 
89
+ // POSIX positional separator: once `--` lands, every remaining token is
90
+ // a positional regardless of shape. Without this, a flag-looking message
91
+ // (`omp -p -- --explain-this`) would be re-validated by the loop below
92
+ // and rejected by the unknown-flag guard (#2461 review).
93
+ if (sawSeparator) {
94
+ result.messages.push(arg);
95
+ continue;
96
+ }
97
+
77
98
  // Support --flag=value syntax (e.g. --tools=ask,read). The value is
78
99
  // spliced in as the next token so value-consuming flags pick it up via
79
100
  // `args[++i]`; a non-consuming flag (e.g. a boolean) leaves it behind and
@@ -228,17 +249,24 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
228
249
  } else if (arg === "--skills" && i + 1 < args.length) {
229
250
  // Comma-separated glob patterns for skill filtering
230
251
  result.skills = args[++i].split(",").map(s => s.trim());
231
- } else if (arg === "--list-models") {
232
- // Check if next arg is a search pattern (not a flag or file arg)
233
- if (i + 1 < args.length && !args[i + 1].startsWith("-") && !args[i + 1].startsWith("@")) {
234
- result.listModels = args[++i];
235
- } else {
236
- result.listModels = true;
237
- }
238
252
  } else if (arg.startsWith("@")) {
239
253
  result.fileArgs.push(arg.slice(1)); // Remove @ prefix
240
- } else if (!arg.startsWith("-")) {
254
+ } else if (!arg.startsWith("-") || arg === "-") {
255
+ // Plain positional or lone `-` (stdin marker) — pass through as a
256
+ // message rather than flagging it.
241
257
  result.messages.push(arg);
258
+ } else if (arg === "--") {
259
+ // POSIX positional separator: drop the token and switch the loop
260
+ // into "everything from here is a positional" mode. The guard at
261
+ // the top of the loop body handles the remaining tokens.
262
+ sawSeparator = true;
263
+ } else {
264
+ // Flag-shaped (`-x`, `--name`) but unrecognized at this parse. Record
265
+ // it so the post-extension reparse can decide whether to surface it
266
+ // as a hard error. `--flag=value` already split `value` into the next
267
+ // slot; the standard "drop unconsumed equals value" guard below
268
+ // removes it so it does not leak into messages (issue #2459).
269
+ result.unrecognizedFlags.push(arg);
242
270
  }
243
271
  // Drop an unconsumed `--flag=value` value (e.g. a boolean flag): when no
244
272
  // branch advanced past the spliced token, remove it so it does not fall
@@ -251,6 +279,24 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
251
279
  return result;
252
280
  }
253
281
 
282
+ /**
283
+ * Emit a stderr error listing the unrecognized flags and return `true` when
284
+ * there were any. Caller is expected to exit with a non-zero status. Splitting
285
+ * the print from the exit keeps the helper unit-testable without forking a
286
+ * process (issue #2459).
287
+ */
288
+ export function reportUnrecognizedFlags(
289
+ args: Pick<Args, "unrecognizedFlags">,
290
+ write: (text: string) => void = text => process.stderr.write(text),
291
+ ): boolean {
292
+ if (args.unrecognizedFlags.length === 0) return false;
293
+ const flags = args.unrecognizedFlags;
294
+ const plural = flags.length === 1 ? "" : "s";
295
+ write(`${chalk.red(`Error: unknown flag${plural}: ${flags.join(", ")}`)}\n`);
296
+ write(`Run \`${APP_NAME} --help\` for available flags.\n`);
297
+ return true;
298
+ }
299
+
254
300
  export function getExtraHelpText(): string {
255
301
  return `${chalk.bold("Environment Variables:")}
256
302
  ${chalk.dim("# Core Providers")}
@@ -409,7 +409,7 @@ function pickProbeCandidates(provider: string): Model<Api>[] {
409
409
  if (!model.input.includes("text")) return false;
410
410
  const totalCost = (model.cost?.input ?? 0) + (model.cost?.output ?? 0);
411
411
  if (!Number.isFinite(totalCost) || totalCost < 0) return false;
412
- if (model.maxTokens <= 0) return false;
412
+ if (model.maxTokens !== null && model.maxTokens <= 0) return false;
413
413
  return true;
414
414
  });
415
415
  candidates.sort((a, b) => a.cost.input + a.cost.output - (b.cost.input + b.cost.output) || a.id.localeCompare(b.id));
@@ -180,7 +180,7 @@ async function runBenchRequest(
180
180
  apiKey: options.apiKey,
181
181
  sessionId: options.sessionId,
182
182
  maxTokens:
183
- Number.isFinite(model.maxTokens) && model.maxTokens > 0
183
+ model.maxTokens !== null && Number.isFinite(model.maxTokens) && model.maxTokens > 0
184
184
  ? Math.min(options.maxTokens, model.maxTokens)
185
185
  : options.maxTokens,
186
186
  reasoning: options.reasoning,