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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- 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
|