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
data/lib/octo/skill.rb ADDED
@@ -0,0 +1,466 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+ require_relative "utils/file_ignore_helper"
6
+ require_relative "utils/gitignore_parser"
7
+
8
+ module Octo
9
+ # Represents a skill with its metadata and content.
10
+ # A skill is defined by a SKILL.md file with optional YAML frontmatter.
11
+ class Skill
12
+ # Frontmatter fields that are recognized
13
+ FRONTMATTER_FIELDS = %w[
14
+ name
15
+ name_zh
16
+ description
17
+ description_zh
18
+ disable-model-invocation
19
+ user-invocable
20
+ allowed-tools
21
+ context
22
+ agent
23
+ argument-hint
24
+ hooks
25
+ fork_agent
26
+ model
27
+ forbidden_tools
28
+ auto_summarize
29
+
30
+ ].freeze
31
+
32
+ attr_reader :directory, :frontmatter, :source_path
33
+ attr_reader :name, :description, :name_zh, :description_zh, :content
34
+ attr_reader :disable_model_invocation, :user_invocable
35
+ attr_reader :allowed_tools, :context, :agent_type, :argument_hint, :hooks
36
+ attr_reader :fork_agent, :model, :forbidden_tools, :auto_summarize
37
+
38
+ # Source location of this skill — set by SkillLoader after registration.
39
+ # One of: :default, :global_claude, :global_octo, :project_claude, :project_octo
40
+ # @return [Symbol, nil]
41
+ attr_accessor :source
42
+
43
+ # Warnings accumulated during load (e.g. name was invalid and fell back to dir name).
44
+ # Non-empty means the skill loaded but something was auto-corrected.
45
+ # @return [Array<String>]
46
+ attr_reader :warnings
47
+
48
+ # When true the skill has an unrecoverable metadata problem (e.g. directory name
49
+ # is itself an invalid slug). The skill is still registered so it can be shown
50
+ # in the UI (greyed-out with an explanation), but it is excluded from the system
51
+ # prompt and slash command dispatch.
52
+ # @return [Boolean]
53
+ attr_reader :invalid
54
+
55
+ # Human-readable reason why the skill is invalid (nil when valid).
56
+ # @return [String, nil]
57
+ attr_reader :invalid_reason
58
+
59
+ # Check if this skill is disabled (disable-model-invocation: true)
60
+ # @return [Boolean]
61
+ def disabled?
62
+ @disable_model_invocation == true
63
+ end
64
+
65
+ # @return [Boolean]
66
+ def invalid?
67
+ @invalid == true
68
+ end
69
+
70
+ # @return [Boolean]
71
+ def has_warnings?
72
+ @warnings&.any?
73
+ end
74
+
75
+
76
+ # @param directory [Pathname, String] Path to the skill directory
77
+ # @param source_path [Pathname, String, nil] Optional source path for priority resolution
78
+ def initialize(directory, source_path: nil)
79
+ @directory = Pathname.new(directory)
80
+ @source_path = source_path ? Pathname.new(source_path) : @directory
81
+ @warnings = []
82
+ @invalid = false
83
+ @invalid_reason = nil
84
+
85
+ load_skill
86
+ end
87
+
88
+ # Get the skill identifier (uses name from frontmatter or directory name)
89
+ # @return [String]
90
+ def identifier
91
+ @name || @directory.basename.to_s
92
+ end
93
+
94
+ # Check if skill can be invoked by user via slash command
95
+ # @return [Boolean]
96
+ def user_invocable?
97
+ @user_invocable.nil? || @user_invocable
98
+ end
99
+
100
+ # Check if skill can be automatically invoked by the model
101
+ # @return [Boolean]
102
+ def model_invocation_allowed?
103
+ !@disable_model_invocation
104
+ end
105
+
106
+ # Check if this skill should fork a subagent
107
+ # @return [Boolean]
108
+ def fork_agent?
109
+ @fork_agent == true
110
+ end
111
+
112
+ # Get the model to use for the subagent (if fork_agent is true)
113
+ # @return [String, nil]
114
+ def subagent_model
115
+ @model
116
+ end
117
+
118
+ # Get the list of forbidden tools for the subagent
119
+ # @return [Array<String>]
120
+ def forbidden_tools_list
121
+ @forbidden_tools || []
122
+ end
123
+
124
+ # Check if subagent should auto-summarize results
125
+ # @return [Boolean]
126
+ def auto_summarize?
127
+ @auto_summarize != false
128
+ end
129
+
130
+ # Get the agent scope for this skill.
131
+ # Parsed from the `agent:` frontmatter field.
132
+ # Returns an array of agent names, or ["all"] if not specified.
133
+ # @return [Array<String>]
134
+ def agents_scope
135
+ return ["all"] if @agent_type.nil?
136
+
137
+ case @agent_type
138
+ when "all" then ["all"]
139
+ when Array then @agent_type.map(&:to_s)
140
+ else [@agent_type.to_s]
141
+ end
142
+ end
143
+
144
+ # Check if this skill is allowed for the given agent profile name.
145
+ # Returns true when the skill's `agent:` field is "all" (default) or
146
+ # includes the given profile name.
147
+ # @param profile_name [String] e.g. "coding", "general"
148
+ # @return [Boolean]
149
+ def allowed_for_agent?(profile_name)
150
+ scope = agents_scope
151
+ scope.include?("all") || scope.include?(profile_name.to_s)
152
+ end
153
+
154
+ # Get the slash command for this skill
155
+ # @return [String] e.g., "/explain-code"
156
+ def slash_command
157
+ "/#{identifier}"
158
+ end
159
+
160
+ # Maximum length for a skill's description when injected into the system
161
+ # prompt. Descriptions longer than this are truncated to protect the token
162
+ # budget — a good description is a trigger hint, not a tutorial. Authors
163
+ # still see their full description via `skill.description`; only the
164
+ # system-prompt rendering is truncated.
165
+ #
166
+ # Anthropic's hard limit is 1024, but empirically ~300 chars is enough for
167
+ # reliable triggering (including trigger-phrase lists); longer content
168
+ # belongs in the SKILL.md body.
169
+ DESCRIPTION_MAX_CHARS = 300
170
+
171
+ # Get the description for context loading.
172
+ # Returns the description from frontmatter (or first paragraph of content),
173
+ # hard-capped at {DESCRIPTION_MAX_CHARS} so a single overlong skill can't
174
+ # blow up the system prompt. Truncation is marked with an ellipsis.
175
+ # @return [String]
176
+ def context_description
177
+ raw = @description || extract_first_paragraph
178
+ return raw if raw.nil? || raw.length <= DESCRIPTION_MAX_CHARS
179
+
180
+ raw[0, DESCRIPTION_MAX_CHARS - 1] + "…"
181
+ end
182
+
183
+ # Get all supporting files in the skill directory (excluding SKILL.md)
184
+ # @return [Array<Pathname>]
185
+ def supporting_files
186
+ return [] unless @directory.exist?
187
+
188
+ dir = @directory.to_s
189
+ gitignore_path = Utils::FileIgnoreHelper.find_gitignore(dir)
190
+ gitignore = gitignore_path ? GitignoreParser.new(gitignore_path) : nil
191
+
192
+ Dir.glob(File.join(dir, "**", "*"))
193
+ .reject { |f| File.directory?(f) }
194
+ .reject { |f| File.basename(f) == "SKILL.md" }
195
+ .reject { |f| Utils::FileIgnoreHelper.should_ignore_file?(f, dir, gitignore) }
196
+ .map { |f| Pathname.new(f) }
197
+ .sort
198
+ end
199
+
200
+ # Check if this skill has any supporting files/scripts beyond SKILL.md.
201
+ # @return [Boolean]
202
+ def has_supporting_files?
203
+ return false unless @directory.exist?
204
+
205
+ supporting_files.any?
206
+ end
207
+
208
+
209
+
210
+ # Process the skill content with argument substitution and template expansion
211
+ # @param arguments [String] Arguments passed to the skill
212
+ # @param shell_output [Hash] Shell command outputs for !command` syntax (optional)
213
+ # @param template_context [Hash] Named values for <%= key %> template expansion (optional)
214
+ # @return [String] Processed content
215
+ def process_content(shell_output: {}, template_context: {})
216
+ processed_content = @content.dup
217
+
218
+ # Expand <%= key %> templates
219
+ processed_content = expand_templates(processed_content, template_context)
220
+
221
+ # Replace shell command outputs
222
+ shell_output.each do |command, output|
223
+ placeholder = "!`#{command}`"
224
+ processed_content.gsub!(placeholder, output.to_s)
225
+ end
226
+
227
+ # Append supporting files list if any exist.
228
+ effective_files = supporting_files.map { |p| p.relative_path_from(@directory).to_s }
229
+
230
+ if effective_files.any?
231
+ max_files = 20
232
+ truncated = effective_files.length > max_files
233
+ listed_files = effective_files.first(max_files)
234
+
235
+ processed_content += "\n\n## Supporting Files\n\n"
236
+ processed_content += "The following files are available in this skill's directory (`#{@directory}`):\n\n"
237
+ listed_files.each do |file|
238
+ processed_content += "- `#{file}`\n"
239
+ end
240
+ if truncated
241
+ processed_content += "\n_(#{effective_files.length - max_files} more files not shown)_\n"
242
+ end
243
+ end
244
+
245
+ # Environment hint: if the skill references ${OCTO_SERVER_HOST/PORT} but
246
+ # those vars were not injected (bare-CLI mode without a running server),
247
+ # the `${...}` placeholders will survive expansion as literal text. In that
248
+ # case append a non-fatal note so the LLM knows the skill's HTTP callbacks
249
+ # will not work, without blocking the skill entirely (the user may still
250
+ # want to read instructions, explore files, etc.).
251
+ if processed_content.match?(/\$\{OCTO_SERVER_(HOST|PORT)\}/)
252
+ processed_content += <<~HINT
253
+
254
+
255
+ ---
256
+
257
+ > ⚠️ **Environment note (auto-injected)**: this skill calls back into the
258
+ > Octo HTTP server (via `${OCTO_SERVER_HOST}` / `${OCTO_SERVER_PORT}`),
259
+ > but those variables are **not set** in the current process. That means
260
+ > no local Octo server was detected.
261
+ >
262
+ > Any `curl http://${OCTO_SERVER_HOST}:...` command in the steps above
263
+ > will fail with a DNS/connection error. Before running those steps you
264
+ > should either:
265
+ >
266
+ > 1. Ask the user to start the server in another terminal: `octo server`
267
+ > (then retry — the CLI auto-detects it via `/tmp/octo-master-*.pid`), or
268
+ > 2. If the task can be completed without the server API, skip those steps
269
+ > and tell the user which parts require the server.
270
+ >
271
+ > This is an informational hint, not an error. Proceed with judgment.
272
+ HINT
273
+ end
274
+
275
+ processed_content
276
+ end
277
+
278
+ # Convert to a hash representation
279
+ # @return [Hash]
280
+ def to_h
281
+ {
282
+ name: identifier,
283
+ name_zh: @name_zh,
284
+ description: context_description,
285
+ directory: @directory.to_s,
286
+ source_path: @source_path.to_s,
287
+ user_invocable: user_invocable?,
288
+ model_invocation_allowed: model_invocation_allowed?,
289
+ fork_agent: fork_agent?,
290
+ subagent_model: @model,
291
+ forbidden_tools: @forbidden_tools,
292
+ allowed_tools: @allowed_tools,
293
+ argument_hint: @argument_hint,
294
+ content_length: @content&.length
295
+ }
296
+ end
297
+
298
+ # Load content of a supporting file
299
+ # @param filename [String] Relative path from skill directory
300
+ # @return [String, nil] File contents or nil if not found
301
+ def read_supporting_file(filename)
302
+ file_path = @directory.join(filename)
303
+ file_path.exist? ? file_path.read : nil
304
+ end
305
+
306
+
307
+ def load_skill
308
+ skill_file = @directory.join("SKILL.md")
309
+
310
+ unless skill_file.exist?
311
+ raise Octo::AgentError, "SKILL.md not found in skill directory: #{@directory}"
312
+ end
313
+
314
+ content = skill_file.read
315
+ parse_frontmatter(content)
316
+
317
+ # Set defaults
318
+ @user_invocable = true if @user_invocable.nil?
319
+ @disable_model_invocation = false if @disable_model_invocation.nil?
320
+
321
+ sanitize_frontmatter
322
+ end
323
+
324
+ # Extract only the body content from a SKILL.md, stripping YAML frontmatter.
325
+ private def extract_content_only(raw)
326
+ match = raw.match(/\A---\n.*?\n---[ \t]*\n?/m)
327
+ match ? raw[match.end(0)..].strip : raw.strip
328
+ end
329
+
330
+ # Parse content that may or may not have YAML frontmatter.
331
+ # This method is lenient: bad frontmatter format or YAML errors just produce
332
+ # warnings rather than raising — the raw text becomes the skill content instead.
333
+ def parse_frontmatter(content)
334
+ frontmatter_match = content.match(/\A---\n(.*?)\n---[ \t]*\n?/m)
335
+
336
+ if frontmatter_match
337
+ yaml_content = frontmatter_match[1]
338
+
339
+ begin
340
+ @frontmatter = YAML.safe_load(yaml_content) || {}
341
+ rescue Psych::Exception => e
342
+ # Bad YAML — treat whole file as plain content, record warning
343
+ @warnings << "Could not parse YAML frontmatter: #{e.message}. Treating file as plain content."
344
+ @frontmatter = {}
345
+ @content = content
346
+ extract_fields_from_frontmatter
347
+ return
348
+ end
349
+
350
+ @content = content[frontmatter_match.end(0)..-1].to_s.strip
351
+ else
352
+ # No valid frontmatter block — treat everything as content (no YAML at all,
353
+ # or an unclosed --- block). We record a warning only if it looked like the
354
+ # author tried to write frontmatter but made a mistake.
355
+ if content.start_with?("---")
356
+ @warnings << "Frontmatter block started with '---' but no closing '---' was found. Treating file as plain content."
357
+ end
358
+ @frontmatter = {}
359
+ @content = content
360
+ end
361
+
362
+ extract_fields_from_frontmatter
363
+ end
364
+
365
+ # Pull known fields out of @frontmatter into instance variables.
366
+ private def extract_fields_from_frontmatter
367
+ @name = @frontmatter["name"]
368
+ @name_zh = @frontmatter["name_zh"]
369
+ @description = @frontmatter["description"]
370
+ @description_zh = @frontmatter["description_zh"]
371
+ @disable_model_invocation = @frontmatter["disable-model-invocation"]
372
+ @user_invocable = @frontmatter["user-invocable"]
373
+ @allowed_tools = @frontmatter["allowed-tools"]
374
+ @context = @frontmatter["context"]
375
+ @agent_type = @frontmatter["agent"]
376
+ @argument_hint = @frontmatter["argument-hint"]
377
+ @hooks = @frontmatter["hooks"]
378
+ @fork_agent = @frontmatter["fork_agent"]
379
+ @model = @frontmatter["model"]
380
+ @forbidden_tools = @frontmatter["forbidden_tools"]
381
+ @auto_summarize = @frontmatter["auto_summarize"]
382
+ end
383
+
384
+ # Sanitize and auto-correct frontmatter fields instead of raising on bad data.
385
+ # Skills should always load — invalid fields are corrected with a warning, or
386
+ # the skill is marked @invalid so the UI can display it greyed-out.
387
+ def sanitize_frontmatter
388
+ dir_slug = @directory.basename.to_s
389
+ valid_slug = ->(s) { s.to_s.match?(/\A[a-z0-9][a-z0-9_-]*\z/) }
390
+
391
+ # --- name ---
392
+ if @name
393
+ name_invalid = !valid_slug.call(@name) || @name.length > 64
394
+
395
+ if name_invalid
396
+ if valid_slug.call(dir_slug)
397
+ # Recoverable: fall back to directory name, record a warning
398
+ @warnings << "Invalid name '#{@name}' in metadata; using directory name '#{dir_slug}' instead."
399
+ @name = dir_slug
400
+ else
401
+ # Both name and directory slug are invalid (e.g. contains dots from version suffix).
402
+ # Record a warning but keep the skill usable — do not mark as invalid.
403
+ @warnings << "Invalid skill name '#{@name}' and directory name '#{dir_slug}' is also not a valid slug. " \
404
+ "Expected lowercase letters, numbers, and hyphens (e.g. 'my-skill')."
405
+ @name = dir_slug
406
+ end
407
+ end
408
+ else
409
+ # No name in frontmatter — check the directory slug itself.
410
+ # Non-conforming names (e.g. version-suffixed dirs like "test-runner-1.0.0")
411
+ # are allowed with a warning rather than being rejected outright.
412
+ unless valid_slug.call(dir_slug)
413
+ @warnings << "Directory name '#{dir_slug}' is not a valid skill slug. " \
414
+ "Expected lowercase letters, numbers, and hyphens (e.g. 'my-skill')."
415
+ end
416
+ end
417
+
418
+ # --- forbidden_tools ---
419
+ if @forbidden_tools && !@forbidden_tools.is_a?(Array)
420
+ @warnings << "forbidden_tools must be an array; ignoring value: #{@forbidden_tools.inspect}"
421
+ @forbidden_tools = nil
422
+ end
423
+
424
+ # --- allowed-tools ---
425
+ if @allowed_tools && !@allowed_tools.is_a?(Array)
426
+ @warnings << "allowed-tools must be an array; ignoring value: #{@allowed_tools.inspect}"
427
+ @allowed_tools = nil
428
+ end
429
+ end
430
+
431
+ def extract_first_paragraph
432
+ @content.split(/\n\n/).first.to_s
433
+ end
434
+
435
+ # Expand <%= key %> template placeholders via ERB.
436
+ # context is a Hash<String|Symbol, String|Proc> — Proc values are called lazily.
437
+ # Unknown bindings raise no error; ERB just leaves them blank (nil.to_s).
438
+ # @param content [String]
439
+ # @param context [Hash]
440
+ # @return [String]
441
+ def expand_templates(content, context)
442
+ # Shell-style ${VAR} substitution from ENV — handles variables like
443
+ # ${OCTO_SERVER_PORT}, ${OCTO_SERVER_HOST} used in SKILL.md files.
444
+ # Unknown variables are left as-is (no substitution).
445
+ content = content.gsub(/\$\{([A-Z_][A-Z0-9_]*)\}/) { ENV[$1] || $& }
446
+
447
+ return content if context.nil? || context.empty?
448
+
449
+ # Build a lightweight binding that exposes each context key as a local method
450
+ scope = Object.new
451
+ context.each do |key, value|
452
+ resolved = value.respond_to?(:call) ? value.call : value
453
+ scope.define_singleton_method(key.to_s) { resolved.to_s }
454
+ scope.define_singleton_method(key.to_sym) { resolved.to_s }
455
+ end
456
+
457
+ require "erb"
458
+ ERB.new(content, trim_mode: "-").result(scope.instance_eval { binding })
459
+ rescue => e
460
+ # If ERB fails (e.g. unknown variable), return content as-is
461
+ content
462
+ end
463
+
464
+
465
+ end
466
+ end