simple-agent-extension 0.2.1

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +16 -0
  3. data/.standard.yml +5 -0
  4. data/AGENTS.md +29 -0
  5. data/CHANGELOG.md +19 -0
  6. data/LICENSE +24 -0
  7. data/README.md +215 -0
  8. data/Rakefile +43 -0
  9. data/adr/adr-0001-single-source-multi-agent-compilation.md +118 -0
  10. data/adr/adr-0002-source-package-model-and-metadata-dsl.md +92 -0
  11. data/adr/adr-0003-compile-time-artifact-scope.md +67 -0
  12. data/adr/adr-0004-external-agents-and-run-time-agent-selection.md +78 -0
  13. data/config/geminabox.ru +9 -0
  14. data/exe/simple-agent-extension +8 -0
  15. data/lib/simple-agent-extension.rb +1 -0
  16. data/lib/simple_agent_extension/agent_base.rb +119 -0
  17. data/lib/simple_agent_extension/agent_directory_loader.rb +60 -0
  18. data/lib/simple_agent_extension/agent_property/deployment.rb +23 -0
  19. data/lib/simple_agent_extension/agent_property/metadata_translator.rb +55 -0
  20. data/lib/simple_agent_extension/agent_property/skill.rb +29 -0
  21. data/lib/simple_agent_extension/agent_property.rb +8 -0
  22. data/lib/simple_agent_extension/agent_registry.rb +99 -0
  23. data/lib/simple_agent_extension/agents/claude_code.rb +62 -0
  24. data/lib/simple_agent_extension/agents/copilot.rb +30 -0
  25. data/lib/simple_agent_extension/agents/opencode.rb +35 -0
  26. data/lib/simple_agent_extension/agents.rb +11 -0
  27. data/lib/simple_agent_extension/cli.rb +145 -0
  28. data/lib/simple_agent_extension/collector.rb +86 -0
  29. data/lib/simple_agent_extension/compiler.rb +185 -0
  30. data/lib/simple_agent_extension/deployer.rb +60 -0
  31. data/lib/simple_agent_extension/deployment_report.rb +52 -0
  32. data/lib/simple_agent_extension/extensions/agent.rb +23 -0
  33. data/lib/simple_agent_extension/extensions/base.rb +73 -0
  34. data/lib/simple_agent_extension/extensions/skill.rb +10 -0
  35. data/lib/simple_agent_extension/extensions.rb +29 -0
  36. data/lib/simple_agent_extension/frontmatter.rb +48 -0
  37. data/lib/simple_agent_extension/metadata.rb +27 -0
  38. data/lib/simple_agent_extension/metadata_composer.rb +32 -0
  39. data/lib/simple_agent_extension/metadata_loader.rb +63 -0
  40. data/lib/simple_agent_extension/runner.rb +95 -0
  41. data/lib/simple_agent_extension/version.rb +5 -0
  42. data/lib/simple_agent_extension.rb +23 -0
  43. data/packages/deep-review/agent/deep-design-reviewer.md +184 -0
  44. data/packages/deep-review/agent/metadata.yaml +9 -0
  45. data/packages/deep-review/skill/SKILL.md +122 -0
  46. data/packages/deep-review/skill/metadata.yaml +6 -0
  47. metadata +92 -0
@@ -0,0 +1,78 @@
1
+ ---
2
+ title: "ADR-0004: 外部 Agent の読み込みと実行時の Agent 選択"
3
+ status: "Accepted"
4
+ date: "2026-10-02"
5
+ authors: "T.Watanabe (maintainer)"
6
+ tags: ["architecture", "decision", "extensibility", "security", "agent"]
7
+ supersedes: ""
8
+ superseded_by: ""
9
+ ---
10
+
11
+ # ADR-0004: 外部 Agent の読み込みと実行時の Agent 選択
12
+
13
+ ## Status
14
+
15
+ Proposed | **Accepted** | Rejected | Superseded | Deprecated
16
+
17
+ ## Context
18
+
19
+ - 利用者は、組み込み以外の Agent 製品(社内ツールなど)にも対応させたい。
20
+ - Agent class は Ruby コードであり、読み込むことは任意コードの実行を意味する。
21
+ - 一回の実行で対象とする Agent を絞りたい場合がある。これを、source 側の配布宣言(`deploy_to`)と混同したくない。
22
+ - インストールされていない Agent の設定ディレクトリを誤って作りたくない。
23
+
24
+ ## Decision
25
+
26
+ 本 ADR は ADR-0001 の PRI-002(新しい製品には Agent class の追加で対応する)を、本体の外にある Agent class にまで広げる。
27
+
28
+ ### 外部 Agent は信頼されたローカルコードとして明示的に読み込む
29
+
30
+ - 利用者が指定したディレクトリの直下にある Ruby ファイルだけを読み込む。自動探索、再帰読み込み、リモート取得は行わない。
31
+ - 各ファイルは自己完結し、新しい Agent class を登録しなければならない。他のファイルの Agent class に依存する定義はサポートしない(読み込み順は契約ではない)。読み込み後は組み込みの Agent と区別せずに扱う。
32
+ - 読み込みの失敗、Agent の未登録、Agent 名の重複が起きた場合は、build や deploy を始める前に停止する。
33
+ - sandbox は提供しない。信頼できるコードだけを読み込むのは利用者の責任とし、そのことを文書に明記する。
34
+
35
+ ### 実行時の選択と配布宣言を分ける
36
+
37
+ - 実行時の Agent 指定は、その回の実行対象を絞るだけとする。artifact の内容や scope は変えない。
38
+ - 未知の Agent 名の扱いは、層によって変える。実行時の指定は利用者のその場の意図なのでエラーにする。`deploy_to` はどの Agent が構成されているか(外部 Agent を含む)に左右される source 側の宣言なので、警告を出して無視する(ADR-0002)。
39
+
40
+ ### 未インストールの Agent には既定で配置しない
41
+
42
+ - Agent の設定ルートが存在しなければ、その Agent への配置を警告付きでスキップする。明示的に指定したときだけ、ディレクトリを作成して配置する。
43
+
44
+ ## Consequences
45
+
46
+ ### Positive
47
+
48
+ - **POS-001**: 本体を変更せずに、任意の Agent 製品に対応できる。
49
+ - **POS-002**: 読み込まれるコードが利用者から常に見える。
50
+ - **POS-003**: 構成の誤りを実行前に検出でき、build や deploy の途中結果を残さない。
51
+ - **POS-004**: 一回の実行で対象を絞っても、source の配布宣言の意味は変わらない。
52
+ - **POS-005**: インストールされていない製品のディレクトリを誤って作成しない。
53
+
54
+ ### Negative
55
+
56
+ - **NEG-001**: 外部 Agent の読み込みは任意コードの実行であり、安全性は利用者の判断に依存する。
57
+
58
+ ## Alternatives Considered
59
+
60
+ ### 外部 Agent の自動探索・再帰読み込み・リモート取得
61
+
62
+ - **ALT-001**: **Description**: 規約上の場所を自動で探索する、サブディレクトリを再帰的に読み込む、マーケットプレイスやリモートから取得する。
63
+ - **ALT-002**: **Rejection Reason**: どのコードが読み込まれるのかが利用者から見えにくくなり、信頼境界が曖昧になる。
64
+
65
+ ### 外部 Agent の sandbox 実行
66
+
67
+ - **ALT-003**: **Description**: 外部 Agent class を隔離環境で評価する。
68
+ - **ALT-004**: **Rejection Reason**: Ruby で実用的な sandbox を作るのは難しく、コストに見合わない。信頼モデルを文書化する方針とした。
69
+
70
+ ### インストール判定に extension 用サブディレクトリを用いる
71
+
72
+ - **ALT-005**: **Description**: 設定ルート配下の `agents/` や `skills/` があるかどうかで判定する。
73
+ - **ALT-006**: **Rejection Reason**: インストール直後の製品はこれらのサブディレクトリをまだ持たないことがあり、誤判定する。
74
+
75
+ ## References
76
+
77
+ - **REF-001**: ADR-0001、ADR-0002(`deploy_to`)、ADR-0003(artifact scope)
78
+ - **REF-002**: `README.md`「Commands」「External Agent registrations」
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "geminabox"
4
+
5
+ Geminabox.data = ENV.fetch("GEMINABOX_DATA") do
6
+ File.expand_path("../tmp/geminabox", __dir__)
7
+ end
8
+
9
+ run Geminabox::Server
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "simple_agent_extension/cli"
7
+
8
+ exit SimpleAgentExtension::CLI.run(ARGV)
@@ -0,0 +1 @@
1
+ require_relative "simple_agent_extension"
@@ -0,0 +1,119 @@
1
+ module SimpleAgentExtension
2
+ # An Agent provides metadata translation, artifact core-file placement, and
3
+ # deployment facts for one type at a time. Compiler and Deployer consume the
4
+ # Agent interface; they do not call the type-specific methods below directly.
5
+ #
6
+ # +extension_type+ selects between "skill" and "agent" through the corresponding
7
+ # `<type>_metadata`, `<type>_core_file`, `<type>_shared?`, and
8
+ # `<type>_destination` methods.
9
+ class AgentBase
10
+ class MissingMetadataTranslator < Error; end
11
+
12
+ include AgentProperty::Skill
13
+
14
+ # @param [String, nil] config_dir product-specific configuration directory
15
+ # @param [String, nil] shared_skill_dir common skill destination
16
+ def initialize(config_dir: nil, shared_skill_dir: nil)
17
+ @config_dir = File.expand_path(config_dir) if config_dir
18
+ @shared_skill_dir = File.expand_path(shared_skill_dir) if shared_skill_dir
19
+ end
20
+
21
+ # @return [String]
22
+ def config_dir
23
+ @config_dir || default_config_dir
24
+ end
25
+
26
+ # @return [Boolean] whether the Agent product is installed locally
27
+ def installed?
28
+ File.directory?(config_dir)
29
+ end
30
+
31
+ # @return [String]
32
+ def shared_skill_dir
33
+ @shared_skill_dir || File.expand_path("~/.agents/skills")
34
+ end
35
+
36
+ # @return [String]
37
+ def name
38
+ self.class::NAME
39
+ end
40
+
41
+ # @return [String, nil] one-line summary of the Agent product
42
+ def description
43
+ self.class::DESCRIPTION if self.class.const_defined?(:DESCRIPTION)
44
+ end
45
+
46
+ # @return [String, nil] official site of the Agent product
47
+ def homepage
48
+ self.class::HOMEPAGE if self.class.const_defined?(:HOMEPAGE)
49
+ end
50
+
51
+ # @param [String, Symbol] type
52
+ # @param [String, Symbol] field
53
+ # @return [Boolean] whether this Agent has a translation rule for the field
54
+ def can_trans_meta?(type:, field:)
55
+ metadata_translator&.can_translate?(type: type, field: field) || false
56
+ end
57
+
58
+ # @param [String, Symbol] type
59
+ # @param [String, Symbol] field
60
+ # @param [Object] value
61
+ # @return [Hash, nil]
62
+ # @raise [MissingMetadataTranslator]
63
+ def trans_meta(type:, field:, value:)
64
+ translator = metadata_translator
65
+ raise MissingMetadataTranslator, "#{self.class} has no metadata translator" unless translator
66
+
67
+ translator.translate(type: type, field: field, value: value)
68
+ end
69
+
70
+ # @return [AgentProperty::Deployment]
71
+ def deployment
72
+ @deployment ||= AgentProperty::Deployment.new(self)
73
+ end
74
+
75
+ # @param [String, Symbol] extension_type
76
+ # @param [String] extension_name name the extension is deployed under
77
+ # @param [String] source_file file relative to the source extension directory
78
+ # @return [String] core file relative to the artifact type directory
79
+ def artifact_core_file(extension_type:, extension_name:, source_file:)
80
+ public_send(
81
+ "#{extension_type}_core_file",
82
+ extension_name: extension_name,
83
+ source_file: source_file
84
+ )
85
+ end
86
+
87
+ def agent_shared?
88
+ false
89
+ end
90
+
91
+ def agent_destination(scope)
92
+ return agent_dir if scope == :own
93
+
94
+ raise KeyError, "agent has no #{scope} destination"
95
+ end
96
+
97
+ # extension type agent placed in
98
+ #
99
+ # @return [String]
100
+ def agent_dir
101
+ File.join(config_dir, "agents")
102
+ end
103
+
104
+ def default_config_dir
105
+ raise NotImplementedError
106
+ end
107
+
108
+ private
109
+
110
+ def metadata_translator
111
+ translator_class = metadata_translator_class
112
+ translator_class && (@metadata_translator ||= translator_class.new)
113
+ end
114
+
115
+ def metadata_translator_class
116
+ self.class::MetadataTranslator if self.class.const_defined?(:MetadataTranslator, false)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAgentExtension
4
+ class InvalidAgentDirectory < Error; end
5
+ class MissingAgentRegistration < Error; end
6
+ class UnloadableAgentRegistration < Error; end
7
+
8
+ # Loads trusted, explicitly selected Agent registration files for one run.
9
+ # Discovery is intentionally flat so a directory's registration surface is
10
+ # visible without implicit recursive loading. Each file must be self-contained:
11
+ # a registration cannot define an Agent that refers to an Agent defined in a
12
+ # sibling file, because load order is an implementation detail rather than a
13
+ # contract. Files are still sorted to keep a run reproducible.
14
+ class AgentDirectoryLoader
15
+ def initialize(directories)
16
+ @directories = Array(directories)
17
+ end
18
+
19
+ def load
20
+ directories.each do |directory|
21
+ agent_files(directory).each { |file| load_file(file) }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def directories
28
+ @directories.map { |directory| canonical_directory(directory) }.uniq
29
+ end
30
+
31
+ def canonical_directory(directory)
32
+ path = File.expand_path(directory)
33
+ raise InvalidAgentDirectory, "invalid agent directory: #{path}" unless File.directory?(path)
34
+
35
+ path = File.realpath(path)
36
+ raise InvalidAgentDirectory, "invalid agent directory: #{path}" unless File.readable?(path)
37
+
38
+ path
39
+ end
40
+
41
+ def agent_files(directory)
42
+ Dir.glob(File.join(directory, "*.rb")).sort.map { |file| File.realpath(file) }
43
+ end
44
+
45
+ def load_file(file)
46
+ known_agent_classes = AgentRegistry.agent_classes
47
+ require_registration(file)
48
+
49
+ return unless (AgentRegistry.agent_classes - known_agent_classes).empty?
50
+
51
+ raise MissingAgentRegistration, "#{file}: did not register an Agent"
52
+ end
53
+
54
+ def require_registration(file)
55
+ require file
56
+ rescue StandardError, ScriptError => error
57
+ raise UnloadableAgentRegistration, "#{file}: #{error.class}: #{error.message}"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ module SimpleAgentExtension
2
+ module AgentProperty
3
+ # Target-specific destinations for already compiled build artifacts.
4
+ class Deployment
5
+ def initialize(agent)
6
+ @agent = agent
7
+ end
8
+
9
+ # @param [String, Symbol] type
10
+ # @return [Boolean]
11
+ def shared?(type)
12
+ @agent.public_send("#{type}_shared?")
13
+ end
14
+
15
+ # @param [String, Symbol] type
16
+ # @param [Symbol] scope
17
+ # @return [String]
18
+ def destination(type, scope)
19
+ @agent.public_send("#{type}_destination", scope)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ module SimpleAgentExtension
2
+ module AgentProperty
3
+ # Converts one source metadata field into Agent metadata fragments. Callers
4
+ # retain ownership of field traversal and fragment composition.
5
+ class MetadataTranslator
6
+ class UnknownTypeAndFieldCombination < Error; end
7
+
8
+ # Class methods for defining translations
9
+ class << self
10
+ # @param [String, Symbol] type ( `skill' or `agent' or ... )
11
+ # @param [String, Symbol] field ( metadata's field name )
12
+ # @param [#call] transform
13
+ # @yieldparam value [Object] value of the declared source metadata field
14
+ # @yieldreturn [Hash, nil] Agent metadata fragment collection, or field omission
15
+ def define_translation(type:, field:, &transform)
16
+ raise ArgumentError, "define_translation requires a block" unless transform
17
+
18
+ translations[[type.to_sym, field.to_s]] = transform
19
+ end
20
+
21
+ # @return [Hash{[String, String] => #call}]
22
+ def translations
23
+ @translations ||= {}
24
+ end
25
+ end
26
+
27
+ # @return [Hash{[String, String] => #call}]
28
+ def translations
29
+ self.class.translations
30
+ end
31
+
32
+ # @param [String] type
33
+ # @param [String] field
34
+ # @return [Boolean] whether a translation rule is registered
35
+ def can_translate?(type:, field:)
36
+ self.class.translations.key?([type.to_sym, field.to_s])
37
+ end
38
+
39
+ # translation entrypoint
40
+ #
41
+ # @param [String, Symbol] type
42
+ # @param [String, Symbol] field
43
+ # @param [Object] value
44
+ # @return [Hash, nil] Agent metadata fragment collection, or omission
45
+ # @raise [UnknownTypeAndFieldCombination] when no translation is registered for type and field
46
+ def translate(type:, field:, value:)
47
+ transform = self.class.translations.fetch([type.to_sym, field.to_s]) {
48
+ raise UnknownTypeAndFieldCombination.new("Unknown combination of type: `#{type}' and field: `#{field}'")
49
+ }
50
+
51
+ transform.call(value)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ module SimpleAgentExtension
2
+ module AgentProperty
3
+ # The area several targets read, and the shared shape of a skill's
4
+ # metadata.
5
+ #
6
+ # Skills live in the shared area until a target needs something of its
7
+ # own lowered into them; at that point compile sends them to +own+.
8
+ module Skill
9
+ def skill_shared?
10
+ true
11
+ end
12
+
13
+ def skill_destination(scope)
14
+ case scope
15
+ when :shared then shared_skill_dir
16
+ when :own then File.join(config_dir, "skills")
17
+ else raise KeyError, "skill has no #{scope} destination"
18
+ end
19
+ end
20
+
21
+ # @param [String] extension_name
22
+ # @param [String] source_file
23
+ # @return [String] core file relative to the artifact type directory
24
+ def skill_core_file(extension_name:, source_file:)
25
+ File.join(extension_name, source_file)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ module SimpleAgentExtension
2
+ module AgentProperty
3
+ end
4
+ end
5
+
6
+ require_relative "agent_property/metadata_translator"
7
+ require_relative "agent_property/skill"
8
+ require_relative "agent_property/deployment"
@@ -0,0 +1,99 @@
1
+ module SimpleAgentExtension
2
+ class DuplicatedAgentName < Error; end
3
+ class DuplicatedAgentConfigurationName < Error; end
4
+ class UnknownAgentName < Error; end
5
+
6
+ # A configured set of deployment Agent instances for one run.
7
+ #
8
+ # Agents remains the namespace for product classes. This registry owns the
9
+ # runtime concerns that need an instance collection: lookup by name,
10
+ # membership, and duplicate-name rejection.
11
+ class AgentRegistry
12
+ # @return [AgentRegistry] all known Agents at their default destinations
13
+ def self.default
14
+ new(agent_classes.map(&:new))
15
+ end
16
+
17
+ # Applies destination overrides without modifying the default registry or
18
+ # any other registry. Missing entries retain their Agent defaults; entries
19
+ # do not select which Agents are present.
20
+ #
21
+ # @param [Hash{String, Symbol => Hash}] overrides per-Agent initializer options
22
+ # @return [AgentRegistry]
23
+ def self.configure(overrides = {})
24
+ options = options_by_agent(overrides)
25
+ defaults = default
26
+ unknown = options.keys - defaults.all.map(&:name)
27
+ raise UnknownAgentName, "unknown agent: #{unknown.first}" unless unknown.empty?
28
+
29
+ new(defaults.all.map { |agent|
30
+ agent.class.new(**options.fetch(agent.name, {}))
31
+ })
32
+ end
33
+
34
+ # @return [Array<Class>] concrete Agent classes
35
+ def self.agent_classes
36
+ Agents.constants(false)
37
+ .map { |const| Agents.const_get(const) }
38
+ .select { |klass| klass.is_a?(Class) && klass < AgentBase }
39
+ .sort_by(&:name)
40
+ end
41
+
42
+ # Converts Agent-name keys and Agent initializer option keys to their
43
+ # canonical String and Symbol forms, as below.
44
+ #
45
+ # ```
46
+ # name[String] => {
47
+ # key[Symbol] => val,
48
+ # key[Symbol] => val
49
+ # }
50
+ # ```
51
+ #
52
+ # @param [Hash{String, Symbol => Hash}] overrides per-Agent destination options
53
+ # @return [Hash{String => Hash{Symbol => Object}}]
54
+ # @raise [ArgumentError] when names are duplicated after normalization
55
+ def self.options_by_agent(overrides)
56
+ overrides.each_with_object({}) do |(name, agent_options), options|
57
+ key = name.to_s
58
+ raise DuplicatedAgentConfigurationName, "duplicate agent configuration: #{key}" if options.key?(key)
59
+
60
+ options[key] = agent_options.transform_keys(&:to_sym)
61
+ end
62
+ end
63
+
64
+ # @param [Array<AgentBase>] agents configured instances for one run
65
+ # @raise [ArgumentError] when names are duplicated
66
+ def initialize(agents)
67
+ @agents = agents.dup.freeze
68
+ @by_name = @agents.each_with_object({}) do |agent, index|
69
+ raise DuplicatedAgentName, "duplicate agent: #{agent.name}" if index.key?(agent.name)
70
+
71
+ index[agent.name] = agent
72
+ end.freeze
73
+ end
74
+
75
+ # @yield [AgentBase]
76
+ # @return [Enumerator]
77
+ def each(&block)
78
+ @agents.each(&block)
79
+ end
80
+
81
+ # @return [Array<AgentBase>]
82
+ def all
83
+ @agents
84
+ end
85
+
86
+ # @param [String, Symbol] name
87
+ # @return [AgentBase]
88
+ # @raise [KeyError] when no configured instance has this name
89
+ def fetch(name)
90
+ @by_name.fetch(name.to_s) { raise UnknownAgentName, "unknown agent: #{name}" }
91
+ end
92
+
93
+ # @param [String, Symbol] name
94
+ # @return [Boolean]
95
+ def include?(name)
96
+ @by_name.key?(name.to_s)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,62 @@
1
+ module SimpleAgentExtension
2
+ module Agents
3
+ class ClaudeCode < AgentBase
4
+ NAME = "claude".freeze
5
+ DESCRIPTION = "Claude Code, Anthropic's terminal coding agent".freeze
6
+ HOMEPAGE = "https://www.anthropic.com/claude-code".freeze
7
+
8
+ class MetadataTranslator < ::SimpleAgentExtension::AgentProperty::MetadataTranslator
9
+ # Source tool names that differ from Claude Code's exact tool names.
10
+ # Names not listed here pass through unchanged.
11
+ TOOL_NAMES = {
12
+ "read" => "Read",
13
+ "edit" => "Edit",
14
+ "write" => "Write",
15
+ "bash" => "Bash",
16
+ "glob" => "Glob",
17
+ "grep" => "Grep",
18
+ "webfetch" => "WebFetch",
19
+ "websearch" => "WebSearch",
20
+ "task" => "Agent",
21
+ "todowrite" => "TodoWrite"
22
+ }.freeze
23
+
24
+ # A subagent's `tools` limits which tools it has without pre-approving
25
+ # them, so `allow` and `ask` become indistinguishable here.
26
+ define_translation type: :agent, field: "permissions" do |permissions|
27
+ tool_fields(permissions, "tools" => %w[allow ask], "disallowedTools" => %w[deny])
28
+ end
29
+
30
+ define_translation type: :skill, field: "permissions" do |permissions|
31
+ tool_fields(permissions, "allowed-tools" => %w[allow], "disallowed-tools" => %w[deny])
32
+ end
33
+
34
+ # @param [Hash{String => String}] permissions tool name to action
35
+ # @param [Hash{String => Array<String>}] actions_by_field output field to the actions it lists
36
+ # @return [Hash{String => String}] comma-separated tool names; empty fields are omitted
37
+ def self.tool_fields(permissions, actions_by_field)
38
+ actions_by_field.each_with_object({}) do |(field, actions), fields|
39
+ tools = permissions.filter_map { |tool, action| TOOL_NAMES.fetch(tool, tool) if actions.include?(action) }
40
+ fields[field] = tools.join(", ") unless tools.empty?
41
+ end
42
+ end
43
+ end
44
+
45
+ def skill_shared?
46
+ false
47
+ end
48
+
49
+ # @return [String]
50
+ def default_config_dir
51
+ File.expand_path("~/.claude")
52
+ end
53
+
54
+ # @param [String] extension_name
55
+ # @param [String] source_file
56
+ # @return [String] core file relative to the artifact type directory
57
+ def agent_core_file(extension_name:, source_file: _source_file)
58
+ "#{extension_name}.md"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,30 @@
1
+ module SimpleAgentExtension
2
+ module Agents
3
+ class Copilot < AgentBase
4
+ NAME = "copilot".freeze
5
+ DESCRIPTION = "GitHub Copilot CLI".freeze
6
+ HOMEPAGE = "https://github.com/features/copilot/cli".freeze
7
+
8
+ class MetadataTranslator < ::SimpleAgentExtension::AgentProperty::MetadataTranslator
9
+ define_translation type: :skill, field: "permissions" do |permissions|
10
+ {"tools" => permissions.filter_map { |tool, action| tool if action == "allow" }}
11
+ end
12
+
13
+ define_translation type: :agent, field: "permissions" do |permissions|
14
+ {"tools" => permissions.filter_map { |tool, action| tool if action == "allow" }}
15
+ end
16
+ end
17
+
18
+ def default_config_dir
19
+ File.expand_path("~/.copilot")
20
+ end
21
+
22
+ # @param [String] extension_name
23
+ # @param [String] source_file
24
+ # @return [String] core file relative to the artifact type directory
25
+ def agent_core_file(extension_name:, source_file: _source_file)
26
+ "#{extension_name}.agent.md"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module SimpleAgentExtension
2
+ module Agents
3
+ class OpenCode < AgentBase
4
+ NAME = "opencode".freeze
5
+ DESCRIPTION = "OpenCode, an open source terminal coding agent".freeze
6
+ HOMEPAGE = "https://opencode.ai".freeze
7
+
8
+ class MetadataTranslator < ::SimpleAgentExtension::AgentProperty::MetadataTranslator
9
+ define_translation type: :skill, field: "permissions" do |permissions|
10
+ {"permission" => permissions}
11
+ end
12
+
13
+ define_translation type: :agent, field: "name" do |_name|
14
+ nil
15
+ end
16
+
17
+ define_translation type: :agent, field: "permissions" do |permissions|
18
+ {"permission" => permissions}
19
+ end
20
+ end
21
+
22
+ # @return [String]
23
+ def default_config_dir
24
+ File.join(File.expand_path("~/.config"), "opencode")
25
+ end
26
+
27
+ # @param [String] extension_name
28
+ # @param [String] source_file
29
+ # @return [String] core file relative to the artifact type directory
30
+ def agent_core_file(extension_name:, source_file: _source_file)
31
+ "#{extension_name}.md"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ module SimpleAgentExtension
2
+ # Concrete coding-agent product classes.
3
+ module Agents
4
+ end
5
+ end
6
+
7
+ require_relative "agent_property"
8
+ require_relative "agent_base"
9
+ require_relative "agents/claude_code"
10
+ require_relative "agents/copilot"
11
+ require_relative "agents/opencode"