octo-agent 0.11.2

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 (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. metadata +614 -0
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Octo
7
+ module Tools
8
+ class TodoManager < Base
9
+ self.tool_name = "todo_manager"
10
+ self.tool_description = <<~DESC.strip
11
+ Plan and track multi-step tasks. Skip for trivial single-step requests.
12
+
13
+ `task` and `id` accept a single value OR an array — always batch when you can
14
+ (e.g. `complete id:[1,2,3]` in one call, not three).
15
+
16
+ Only `complete` a todo once it's truly done end-to-end, not per sub-step.
17
+ DESC
18
+ self.tool_category = "task_management"
19
+ self.tool_parameters = {
20
+ type: "object",
21
+ properties: {
22
+ action: {
23
+ type: "string",
24
+ enum: ["add", "list", "complete", "remove", "clear"],
25
+ description: "add | list | complete | remove | clear"
26
+ },
27
+ task: {
28
+ description: "add: task description(s). Accepts a string OR an array of strings for batch add.",
29
+ oneOf: [
30
+ { type: "string" },
31
+ { type: "array", items: { type: "string" } }
32
+ ]
33
+ },
34
+ id: {
35
+ description: "complete/remove: task id(s). Accepts an integer OR an array of integers for batch ops.",
36
+ oneOf: [
37
+ { type: "integer" },
38
+ { type: "array", items: { type: "integer" } }
39
+ ]
40
+ }
41
+ },
42
+ required: ["action"]
43
+ }
44
+
45
+ def execute(action:, task: nil, id: nil, todos_storage: nil, working_dir: nil, **_extra)
46
+ # todos_storage is injected by Agent, stores todos in memory
47
+ @todos = todos_storage || []
48
+
49
+ # Normalize polymorphic inputs: callers may pass scalar or array for
50
+ # `task` and `id`. We coerce both into arrays internally.
51
+ tasks_input = normalize_to_array(task)
52
+ ids_input = normalize_to_array(id)
53
+
54
+ case action
55
+ when "add"
56
+ add_todos(tasks_input)
57
+ when "list"
58
+ list_todos
59
+ when "complete"
60
+ if ids_input.size > 1
61
+ complete_todos(ids_input)
62
+ else
63
+ complete_todo(ids_input.first)
64
+ end
65
+ when "remove"
66
+ if ids_input.size > 1
67
+ remove_todos(ids_input)
68
+ else
69
+ remove_todo(ids_input.first)
70
+ end
71
+ when "clear"
72
+ clear_todos
73
+ else
74
+ { error: "Unknown action: #{action}" }
75
+ end
76
+ end
77
+
78
+ # Coerce scalar/array/nil into an Array. Filters nil entries.
79
+ private def normalize_to_array(value)
80
+ return [] if value.nil?
81
+ Array(value).reject(&:nil?)
82
+ end
83
+
84
+ def format_call(args)
85
+ action = args[:action] || args['action']
86
+ case action
87
+ when 'add'
88
+ task_arg = args[:task] || args['task']
89
+ count = task_arg.is_a?(Array) ? task_arg.size : 1
90
+ "TodoManager(add #{count} task#{count > 1 ? 's' : ''})"
91
+ when 'complete'
92
+ id_arg = args[:id] || args['id']
93
+ if id_arg.is_a?(Array) && id_arg.size > 1
94
+ "TodoManager(complete #{id_arg.size} tasks: #{id_arg.join(', ')})"
95
+ else
96
+ single = id_arg.is_a?(Array) ? id_arg.first : id_arg
97
+ "TodoManager(complete ##{single})"
98
+ end
99
+ when 'list'
100
+ "TodoManager(list)"
101
+ when 'remove'
102
+ id_arg = args[:id] || args['id']
103
+ if id_arg.is_a?(Array) && id_arg.size > 1
104
+ "TodoManager(remove #{id_arg.size} tasks: #{id_arg.join(', ')})"
105
+ else
106
+ single = id_arg.is_a?(Array) ? id_arg.first : id_arg
107
+ "TodoManager(remove ##{single})"
108
+ end
109
+ when 'clear'
110
+ "TodoManager(clear all)"
111
+ else
112
+ "TodoManager(#{action})"
113
+ end
114
+ end
115
+
116
+ def format_result(result)
117
+ return result[:error] if result[:error]
118
+
119
+ if result[:message]
120
+ result[:message]
121
+ else
122
+ "Done"
123
+ end
124
+ end
125
+
126
+ def format_result_for_ui(result)
127
+ return nil if result[:error]
128
+ todos = result[:todos] || result[:completed] || []
129
+ todos = [result[:todo]] if todos.empty? && result[:todo]
130
+ {
131
+ type: "todo",
132
+ action: result[:action],
133
+ todos: todos.map { |t| { id: t[:id], task: t[:task], status: t[:status] } },
134
+ progress: result[:progress]
135
+ }
136
+ end
137
+
138
+
139
+ def load_todos
140
+ @todos
141
+ end
142
+
143
+ def save_todos(todos)
144
+ # Modify the array in-place so Agent's @todos is updated
145
+ # Important: Don't use @todos.clear first because todos might be @todos itself!
146
+ @todos.replace(todos)
147
+ end
148
+
149
+ def add_todos(tasks_input)
150
+ # tasks_input is already a normalized array (possibly empty)
151
+ tasks_to_add = Array(tasks_input)
152
+ .map { |t| t.is_a?(String) ? t.strip : t.to_s.strip }
153
+ .reject(&:empty?)
154
+
155
+ return { error: "At least one task description is required" } if tasks_to_add.empty?
156
+
157
+ existing_todos = load_todos
158
+
159
+ # Auto-clear old completed todos from previous task cycles before adding new ones
160
+ completed_before = existing_todos.count { |t| t[:status] == "completed" }
161
+ if completed_before > 0
162
+ existing_todos.reject! { |t| t[:status] == "completed" }
163
+ end
164
+
165
+ next_id = existing_todos.empty? ? 1 : existing_todos.map { |t| t[:id] }.max + 1
166
+
167
+ added_todos = []
168
+ tasks_to_add.each_with_index do |task_desc, index|
169
+ new_todo = {
170
+ id: next_id + index,
171
+ task: task_desc,
172
+ status: "pending",
173
+ created_at: Time.now.iso8601
174
+ }
175
+ existing_todos << new_todo
176
+ added_todos << new_todo
177
+ end
178
+
179
+ save_todos(existing_todos)
180
+
181
+ {
182
+ message: added_todos.size == 1 ? "TODO added successfully" : "#{added_todos.size} TODOs added successfully",
183
+ todos: added_todos,
184
+ total: existing_todos.size,
185
+ reminder: "⚠️ IMPORTANT: You have added TODO(s) but have NOT started working yet! You MUST now use other tools (write, edit, shell, etc.) to actually complete these tasks. DO NOT stop here!"
186
+ }
187
+ end
188
+
189
+ def list_todos
190
+ todos = load_todos
191
+
192
+ if todos.empty?
193
+ return {
194
+ message: "No TODO items",
195
+ todos: [],
196
+ total: 0
197
+ }
198
+ end
199
+
200
+ {
201
+ message: "TODO list",
202
+ todos: todos,
203
+ total: todos.size,
204
+ pending: todos.count { |t| t[:status] == "pending" },
205
+ completed: todos.count { |t| t[:status] == "completed" }
206
+ }
207
+ end
208
+
209
+ def complete_todo(id)
210
+ return { error: "Task ID is required" } if id.nil?
211
+
212
+ todos = load_todos
213
+ todo = todos.find { |t| t[:id] == id }
214
+
215
+ return { error: "Task not found: #{id}" } unless todo
216
+
217
+ if todo[:status] == "completed"
218
+ return { message: "Task already completed", todo: todo }
219
+ end
220
+
221
+ todo[:status] = "completed"
222
+ todo[:completed_at] = Time.now.iso8601
223
+ save_todos(todos)
224
+
225
+ # Find the next pending task
226
+ next_pending = todos.find { |t| t[:status] == "pending" }
227
+
228
+ # Count statistics
229
+ completed_count = todos.count { |t| t[:status] == "completed" }
230
+ total_count = todos.size
231
+
232
+ result = {
233
+ message: "Task marked as completed",
234
+ todo: todo,
235
+ todos: todos,
236
+ progress: "#{completed_count}/#{total_count}",
237
+ reminder: "⚠️ REMINDER: Check the PROJECT-SPECIFIC RULES section in your system prompt before continuing to the next task"
238
+ }
239
+
240
+ if next_pending
241
+ result[:next_task] = next_pending
242
+ result[:next_task_info] = "Progress: #{completed_count}/#{total_count}. Next task: ##{next_pending[:id]} - #{next_pending[:task]}"
243
+ else
244
+ # All tasks completed — auto-clear so the agent doesn't need to call clear manually
245
+ save_todos([])
246
+ result[:all_completed] = true
247
+ result[:completion_message] = "All tasks completed and cleared! (#{completed_count}/#{total_count})"
248
+ end
249
+
250
+ result
251
+ end
252
+
253
+ def remove_todo(id)
254
+ return { error: "Task ID is required" } if id.nil?
255
+
256
+ todos = load_todos
257
+ todo = todos.find { |t| t[:id] == id }
258
+
259
+ return { error: "Task not found: #{id}" } unless todo
260
+
261
+ todos.reject! { |t| t[:id] == id }
262
+ save_todos(todos)
263
+
264
+ {
265
+ message: "Task removed",
266
+ todo: todo,
267
+ remaining: todos.size
268
+ }
269
+ end
270
+
271
+ def clear_todos
272
+ todos = load_todos
273
+ count = todos.size
274
+
275
+ # Clear the in-memory storage
276
+ save_todos([])
277
+
278
+ {
279
+ message: "All TODOs cleared",
280
+ cleared_count: count
281
+ }
282
+ end
283
+
284
+ def remove_todos(ids)
285
+ return { error: "Task IDs array is required" } if ids.nil? || ids.empty?
286
+
287
+ todos = load_todos
288
+ removed_todos = []
289
+ not_found_ids = []
290
+
291
+ ids.each do |id|
292
+ todo = todos.find { |t| t[:id] == id }
293
+ if todo
294
+ removed_todos << todo
295
+ else
296
+ not_found_ids << id
297
+ end
298
+ end
299
+
300
+ # Remove all found todos
301
+ todos.reject! { |t| ids.include?(t[:id]) }
302
+ save_todos(todos)
303
+
304
+ result = {
305
+ message: "#{removed_todos.size} task(s) removed",
306
+ removed: removed_todos,
307
+ remaining: todos.size
308
+ }
309
+
310
+ # Add warning about not found IDs
311
+ result[:not_found] = not_found_ids unless not_found_ids.empty?
312
+
313
+ result
314
+ end
315
+
316
+ # Mark several tasks completed in one call.
317
+ # Behavior mirrors `complete_todo` but aggregates over `ids`.
318
+ # Tolerates already-completed and not-found ids (returned in result).
319
+ def complete_todos(ids)
320
+ return { error: "Task IDs array is required" } if ids.nil? || ids.empty?
321
+
322
+ todos = load_todos
323
+ now = Time.now.iso8601
324
+ completed_now = []
325
+ already_completed = []
326
+ not_found = []
327
+
328
+ ids.each do |id|
329
+ todo = todos.find { |t| t[:id] == id }
330
+ if todo.nil?
331
+ not_found << id
332
+ elsif todo[:status] == "completed"
333
+ already_completed << todo
334
+ else
335
+ todo[:status] = "completed"
336
+ todo[:completed_at] = now
337
+ completed_now << todo
338
+ end
339
+ end
340
+
341
+ save_todos(todos)
342
+
343
+ completed_count = todos.count { |t| t[:status] == "completed" }
344
+ total_count = todos.size
345
+ next_pending = todos.find { |t| t[:status] == "pending" }
346
+
347
+ result = {
348
+ message: "#{completed_now.size} task(s) marked as completed",
349
+ completed: completed_now,
350
+ todos: todos,
351
+ progress: "#{completed_count}/#{total_count}"
352
+ }
353
+
354
+ result[:already_completed] = already_completed unless already_completed.empty?
355
+ result[:not_found] = not_found unless not_found.empty?
356
+
357
+ if next_pending
358
+ result[:next_task] = next_pending
359
+ result[:next_task_info] =
360
+ "Progress: #{completed_count}/#{total_count}. " \
361
+ "Next task: ##{next_pending[:id]} - #{next_pending[:task]}"
362
+ else
363
+ # All tasks completed — auto-clear to match single-complete behavior
364
+ save_todos([])
365
+ result[:all_completed] = true
366
+ result[:completion_message] =
367
+ "All tasks completed and cleared! (#{completed_count}/#{total_count})"
368
+ end
369
+
370
+ result
371
+ end
372
+ end
373
+ end
374
+ end