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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ class Agent
5
+ # Scenario 2: Reflect on skill execution and suggest improvements.
6
+ #
7
+ # After a skill completes, forks a subagent to analyze:
8
+ # - Were instructions clear enough?
9
+ # - Any missing edge cases?
10
+ # - Any improvements needed?
11
+ #
12
+ # If the LLM identifies concrete improvements, it invokes skill-creator
13
+ # to update the skill.
14
+ module SkillReflector
15
+ # Minimum iterations for a skill execution to warrant reflection.
16
+ # This counts iterations within the skill execution only, not session-cumulative.
17
+ MIN_SKILL_ITERATIONS = 5
18
+
19
+ # Check if we should reflect on the skill that just executed
20
+ # Called from SkillEvolution#run_skill_evolution_hooks
21
+ def maybe_reflect_on_skill
22
+ return unless @skill_execution_context
23
+
24
+ # Only reflect on skills that the user explicitly invoked via slash command.
25
+ # Skills triggered by the LLM itself (e.g. as part of a broader task) or
26
+ # platform-management skills invoked incidentally should not be reflected on.
27
+ return unless @skill_execution_context[:slash_command]
28
+
29
+ # Skip default skills — they are system-owned and should not be
30
+ # auto-improved by the evolution system.
31
+ source = @skill_execution_context[:source]
32
+ return if source == :default
33
+
34
+ skill_name = @skill_execution_context[:skill_name]
35
+ start_iteration = @skill_execution_context[:start_iteration]
36
+
37
+ # Calculate iterations within the skill execution (not session-cumulative)
38
+ iterations = @iterations - start_iteration
39
+
40
+ # Only reflect if the skill actually ran for a meaningful number of iterations
41
+ return if iterations < MIN_SKILL_ITERATIONS
42
+
43
+ # Fork an isolated subagent to reflect + improve — does NOT touch main history
44
+ @ui&.show_info("Reflecting on skill execution: #{skill_name}")
45
+ subagent = fork_subagent
46
+ result = subagent.run(build_skill_reflection_prompt(skill_name))
47
+
48
+ # Clear the context so we don't reflect again
49
+ @skill_execution_context = nil
50
+ end
51
+
52
+ # Build the reflection prompt content
53
+ # @param skill_name [String]
54
+ # @return [String]
55
+ private def build_skill_reflection_prompt(skill_name)
56
+ <<~PROMPT
57
+ ═══════════════════════════════════════════════════════════════
58
+ SKILL REFLECTION MODE
59
+ ═══════════════════════════════════════════════════════════════
60
+ You just executed the skill "#{skill_name}".
61
+
62
+ ## Quick Analysis
63
+
64
+ Reflect on whether the skill could be improved:
65
+ - Were the instructions clear enough?
66
+ - Did you encounter any edge cases not covered?
67
+ - Were there any steps that could be streamlined?
68
+ - Is there missing context that would make it easier next time?
69
+ - Did the skill produce the expected results?
70
+
71
+ ## Decision
72
+
73
+ If you identified **concrete, actionable improvements**:
74
+ → Call invoke_skill("skill-creator", task: "Improve skill #{skill_name}: [describe specific improvements needed]")
75
+
76
+ If the skill worked well as-is:
77
+ → Respond briefly: "Skill #{skill_name} worked well, no improvements needed."
78
+
79
+ ## Constraints
80
+
81
+ - DO NOT spend more than 30 seconds on this reflection
82
+ - Be specific and actionable in your improvement suggestions
83
+ - Only suggest improvements that would make a meaningful difference
84
+ - If you're unsure, err on the side of "no improvements needed"
85
+ PROMPT
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../utils/workspace_rules"
4
+
5
+ module Octo
6
+ class Agent
7
+ # System prompt construction
8
+ # Builds system prompt by composing layers:
9
+ # 1. Agent-specific system_prompt.md (role & responsibilities)
10
+ # 2. base_prompt.md (universal rules: todo manager, tool usage, etc.)
11
+ # 3. Project rules (.octorules / .cursorrules / CLAUDE.md)
12
+ # 4. SOUL.md (agent personality — user override or built-in default)
13
+ # 5. USER.md (user profile — user override or built-in default)
14
+ # 6. Skills context (available skills list)
15
+ module SystemPromptBuilder
16
+ # Max characters loaded from each agent file (SOUL.md / USER.md)
17
+ MAX_MEMORY_FILE_CHARS = 1000
18
+
19
+ # Build complete system prompt with project rules and skills
20
+ # @return [String] Complete system prompt
21
+ def build_system_prompt
22
+ parts = []
23
+
24
+ # Layer 1: agent-specific role & responsibilities
25
+ parts << @agent_profile.system_prompt
26
+
27
+ # Layer 2: universal behavioral rules (todo manager, tool usage, etc.)
28
+ base = @agent_profile.base_prompt
29
+ parts << base unless base.empty?
30
+
31
+ # Layer 3: project-specific rules from working directory
32
+ project_rules = load_project_rules
33
+ if project_rules
34
+ parts << format_section("PROJECT-SPECIFIC RULES (from #{project_rules[:source]})",
35
+ project_rules[:content],
36
+ footer: "IMPORTANT: Follow these project-specific rules at all times!")
37
+ end
38
+
39
+ # Layer 4 & 5: SOUL.md and USER.md (with built-in defaults as fallback)
40
+ soul = truncate(@agent_profile.soul, MAX_MEMORY_FILE_CHARS)
41
+ parts << format_section("AGENT SOUL (from ~/.octo/agents/SOUL.md)", soul) unless soul.empty?
42
+
43
+ user_profile = truncate(@agent_profile.user_profile, MAX_MEMORY_FILE_CHARS)
44
+ parts << format_section("USER PROFILE (from ~/.octo/agents/USER.md)", user_profile) unless user_profile.empty?
45
+
46
+ # Layer 6: skills context
47
+ skill_context = build_skill_context
48
+ parts << skill_context if skill_context && !skill_context.empty?
49
+
50
+ parts.join("\n\n")
51
+ end
52
+
53
+ private def load_project_rules
54
+ main = Utils::WorkspaceRules.find_main(@working_dir)
55
+ sub_projects = Utils::WorkspaceRules.find_sub_projects(@working_dir)
56
+
57
+ return nil if main.nil? && sub_projects.empty?
58
+
59
+ combined_content = []
60
+ combined_content << main[:content] if main
61
+
62
+ unless sub_projects.empty?
63
+ n = Utils::WorkspaceRules::SUB_PROJECT_SUMMARY_LINES
64
+ summaries = sub_projects.map do |sp|
65
+ <<~SECTION.strip
66
+ ### Sub-project: #{sp[:sub_name]}/
67
+ Summary (first #{n} lines of #{sp[:relative_path]}):
68
+ #{sp[:summary]}
69
+ > IMPORTANT: Before working on any files under #{sp[:sub_name]}/, read the full rules file at `#{sp[:relative_path]}` using file_reader.
70
+ SECTION
71
+ end
72
+
73
+ combined_content << <<~BLOCK.strip
74
+ ## SUB-PROJECT AGENTS
75
+ This workspace contains sub-projects, each with their own rules.
76
+ When working in a sub-project, you MUST read its full .octorules first.
77
+
78
+ #{summaries.join("\n\n")}
79
+ BLOCK
80
+ end
81
+
82
+ source = main ? main[:name] : "sub-projects"
83
+ { content: combined_content.join("\n\n"), source: source }
84
+ end
85
+
86
+ private def format_section(title, content, footer: nil)
87
+ sep = "=" * 80
88
+ lines = ["", sep, title, sep, content, sep]
89
+ lines << footer if footer
90
+ lines << sep if footer
91
+ lines.join("\n")
92
+ end
93
+
94
+ private def truncate(text, max_chars)
95
+ return text if text.length <= max_chars
96
+
97
+ text[0, max_chars] + "\n... [truncated]"
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ class Agent
5
+ # Time Machine module for task history management with undo/redo support
6
+ # Stores complete file snapshots (AFTER state) to support message compression
7
+ module TimeMachine
8
+ # Initialize Time Machine state
9
+ private def init_time_machine
10
+ @task_parents ||= {} # { task_id => parent_id }
11
+ @current_task_id ||= 0 # Latest created task ID
12
+ @active_task_id ||= 0 # Current active task ID (for undo/redo)
13
+ end
14
+
15
+ # Start a new task and establish parent relationship
16
+ # Made public for testing
17
+ def start_new_task
18
+ parent_id = @active_task_id
19
+ @current_task_id += 1
20
+ @active_task_id = @current_task_id
21
+ @task_parents[@current_task_id] = parent_id
22
+
23
+ # Claim ownership of this task for the current thread.
24
+ # If a stale thread (e.g. a slow subagent) wakes up later it will see
25
+ # @task_thread != Thread.current via check_stale! and self-terminate
26
+ # before it can write to history.
27
+ @task_thread = Thread.current
28
+
29
+ @current_task_id
30
+ end
31
+
32
+ # Save snapshots of modified files (AFTER state)
33
+ # @param modified_files [Array<String>] List of file paths that were modified
34
+ # Made public for testing
35
+ def save_modified_files_snapshot(modified_files)
36
+ return if modified_files.nil? || modified_files.empty?
37
+
38
+ snapshot_dir = File.join(
39
+ Dir.home,
40
+ ".octo",
41
+ "snapshots",
42
+ @session_id,
43
+ "task-#{@current_task_id}"
44
+ )
45
+ FileUtils.mkdir_p(snapshot_dir)
46
+
47
+ modified_files.each do |file_path|
48
+ next unless File.exist?(file_path)
49
+
50
+ # Save file content to snapshot
51
+ relative_path = file_path.start_with?(@working_dir) ?
52
+ file_path.sub(@working_dir + "/", "") : File.basename(file_path)
53
+
54
+ snapshot_file = File.join(snapshot_dir, relative_path)
55
+ FileUtils.mkdir_p(File.dirname(snapshot_file))
56
+ FileUtils.cp(file_path, snapshot_file)
57
+ end
58
+ rescue StandardError => e
59
+ # Silently handle errors in tests
60
+ end
61
+
62
+ # Restore files to the state at given task
63
+ # @param task_id [Integer] Target task ID
64
+ # Made public for testing
65
+ def restore_to_task_state(task_id)
66
+ # Collect all modified files from task 1 to target task
67
+ files_to_restore = {}
68
+
69
+ (1..task_id).each do |tid|
70
+ snapshot_dir = File.join(
71
+ Dir.home,
72
+ ".octo",
73
+ "snapshots",
74
+ @session_id,
75
+ "task-#{tid}"
76
+ )
77
+
78
+ next unless Dir.exist?(snapshot_dir)
79
+
80
+ Dir.glob(File.join(snapshot_dir, "**", "*")).each do |snapshot_file|
81
+ next if File.directory?(snapshot_file)
82
+
83
+ relative_path = snapshot_file.sub(snapshot_dir + "/", "")
84
+ files_to_restore[relative_path] = snapshot_file
85
+ end
86
+ end
87
+
88
+ # Restore files
89
+ files_to_restore.each do |relative_path, snapshot_file|
90
+ target_file = File.join(@working_dir, relative_path)
91
+ FileUtils.mkdir_p(File.dirname(target_file))
92
+ FileUtils.cp(snapshot_file, target_file)
93
+ end
94
+ rescue StandardError => e
95
+ # Silently handle errors in tests
96
+ raise
97
+ end
98
+
99
+ # Filter messages to only show tasks up to active_task_id.
100
+ # This hides "future" messages when user has undone.
101
+ # Returns API-ready array (strips internal fields + handles orphaned tool_calls).
102
+ # @param force_reasoning_content_pad [Boolean] forwarded to MessageHistory,
103
+ # enables one-shot pad-and-retry for thinking-mode providers that
104
+ # require reasoning_content on every assistant message.
105
+ # Made public for testing
106
+ def active_messages(force_reasoning_content_pad: false)
107
+ if @active_task_id == @current_task_id
108
+ return @history.to_api(force_reasoning_content_pad: force_reasoning_content_pad)
109
+ end
110
+
111
+ stripped = @history.for_task(@active_task_id).map do |msg|
112
+ msg.reject { |k, _| MessageHistory::INTERNAL_FIELDS.include?(k) }
113
+ end
114
+ # Apply the same reasoning_content padding rule used by to_api so
115
+ # Time Machine replays satisfy thinking-mode providers after a
116
+ # 400 retry.
117
+ MessageHistory.pad_reasoning_content_if_needed(stripped, force: force_reasoning_content_pad)
118
+ end
119
+
120
+ # Undo to parent task
121
+ def undo_last_task
122
+ parent_id = @task_parents[@active_task_id]
123
+ return { success: false, message: "Already at root task" } if parent_id.nil? || parent_id == 0
124
+
125
+ restore_to_task_state(parent_id)
126
+ @active_task_id = parent_id
127
+
128
+ {
129
+ success: true,
130
+ message: "⏪ Undone to task #{parent_id}",
131
+ task_id: parent_id
132
+ }
133
+ end
134
+
135
+ # Switch to specific task (for redo or branch switching)
136
+ def switch_to_task(target_task_id)
137
+ if target_task_id > @current_task_id || target_task_id < 1
138
+ return { success: false, message: "Invalid task ID: #{target_task_id}" }
139
+ end
140
+
141
+ restore_to_task_state(target_task_id)
142
+ @active_task_id = target_task_id
143
+
144
+ {
145
+ success: true,
146
+ message: "⏩ Switched to task #{target_task_id}",
147
+ task_id: target_task_id
148
+ }
149
+ end
150
+
151
+ # Get children of a task (for branch detection)
152
+ def get_child_tasks(task_id)
153
+ @task_parents.select { |_, parent| parent == task_id }.keys
154
+ end
155
+
156
+ # Get task history with summaries for UI display
157
+ # @param limit [Integer] Maximum number of recent tasks to return
158
+ # @return [Array<Hash>] Task history with metadata
159
+ def get_task_history(limit: 10)
160
+ return [] if @current_task_id == 0
161
+
162
+ tasks = []
163
+ (1..@current_task_id).to_a.reverse.take(limit).reverse.each do |task_id|
164
+ # Find first user message for this task
165
+ first_user_msg = @history.to_a.find do |msg|
166
+ msg[:task_id] == task_id && msg[:role] == "user"
167
+ end
168
+
169
+ summary = if first_user_msg
170
+ content = extract_message_text(first_user_msg[:content])
171
+ # Truncate to 60 characters (including "...")
172
+ content.length > 60 ? "#{content[0...57]}..." : content
173
+ else
174
+ "Task #{task_id}"
175
+ end
176
+
177
+ # Determine task status
178
+ status = if task_id == @active_task_id
179
+ :current
180
+ elsif task_id < @active_task_id
181
+ :past
182
+ else
183
+ :future
184
+ end
185
+
186
+ # Check if task has branches (multiple children)
187
+ children = get_child_tasks(task_id)
188
+ has_branches = children.length > 1
189
+
190
+ tasks << {
191
+ task_id: task_id,
192
+ summary: summary,
193
+ status: status,
194
+ has_branches: has_branches
195
+ }
196
+ end
197
+
198
+ tasks
199
+ end
200
+
201
+ # Extract text from message content (handles both string and array formats)
202
+ private def extract_message_text(content)
203
+ if content.is_a?(String)
204
+ content
205
+ elsif content.is_a?(Array)
206
+ text_parts = content.select { |part| part[:type] == "text" }
207
+ text_parts.map { |part| part[:text] }.join(" ")
208
+ else
209
+ ""
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end