aircana 3.0.0.rc1 → 3.0.0.rc3

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.claude-plugin/plugin.json +7 -0
  3. data/.rspec_status +150 -153
  4. data/.rubocop.yml +12 -0
  5. data/CHANGELOG.md +24 -2
  6. data/CLAUDE.md +50 -19
  7. data/README.md +132 -55
  8. data/agents/apply_feedback.md +92 -0
  9. data/agents/executor.md +85 -0
  10. data/agents/jira.md +46 -0
  11. data/agents/planner.md +64 -0
  12. data/agents/reviewer.md +95 -0
  13. data/agents/sub-agent-coordinator.md +91 -0
  14. data/agents/test-agent/manifest.json +15 -0
  15. data/commands/air-apply-feedback.md +15 -0
  16. data/commands/air-ask-expert.md +42 -0
  17. data/commands/air-execute.md +13 -0
  18. data/commands/air-plan.md +33 -0
  19. data/commands/air-record.md +17 -0
  20. data/commands/air-review.md +12 -0
  21. data/commands/sample-command.md +1 -0
  22. data/hooks/hooks.json +31 -0
  23. data/lib/aircana/cli/app.rb +32 -4
  24. data/lib/aircana/cli/commands/agents.rb +41 -9
  25. data/lib/aircana/cli/commands/doctor_checks.rb +2 -3
  26. data/lib/aircana/cli/commands/hooks.rb +4 -4
  27. data/lib/aircana/cli/commands/init.rb +261 -0
  28. data/lib/aircana/cli/commands/plugin.rb +157 -0
  29. data/lib/aircana/cli/help_formatter.rb +1 -1
  30. data/lib/aircana/configuration.rb +29 -3
  31. data/lib/aircana/generators/agents_generator.rb +3 -2
  32. data/lib/aircana/hooks_manifest.rb +189 -0
  33. data/lib/aircana/plugin_manifest.rb +146 -0
  34. data/lib/aircana/system_checker.rb +0 -1
  35. data/lib/aircana/templates/agents/base_agent.erb +2 -2
  36. data/lib/aircana/templates/hooks/user_prompt_submit.erb +0 -6
  37. data/lib/aircana/version.rb +1 -1
  38. data/spec_target_1760205040_181/agents/test-agent/manifest.json +15 -0
  39. data/spec_target_1760205220_486/agents/test-agent/manifest.json +15 -0
  40. data/spec_target_1760205379_250/agents/test-agent/manifest.json +15 -0
  41. data/spec_target_1760205601_652/agents/test-agent/manifest.json +15 -0
  42. data/spec_target_1760205608_135/agents/test-agent/manifest.json +15 -0
  43. data/spec_target_1760205654_952/agents/test-agent/manifest.json +15 -0
  44. metadata +27 -2
  45. data/lib/aircana/cli/commands/install.rb +0 -169
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Aircana
7
+ # Manages Claude Code plugin manifest (plugin.json) files
8
+ class PluginManifest
9
+ REQUIRED_FIELDS = %w[name version].freeze
10
+ OPTIONAL_FIELDS = %w[description author homepage repository license keywords].freeze
11
+ ALL_FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
12
+
13
+ attr_reader :plugin_root
14
+
15
+ def initialize(plugin_root)
16
+ @plugin_root = plugin_root
17
+ end
18
+
19
+ # Creates a new plugin manifest with the given attributes
20
+ def create(attributes = {})
21
+ validate_required_fields!(attributes)
22
+
23
+ manifest_data = build_manifest_data(attributes)
24
+ write_manifest(manifest_data)
25
+
26
+ manifest_path
27
+ end
28
+
29
+ # Reads the existing plugin manifest
30
+ def read
31
+ return nil unless exists?
32
+
33
+ JSON.parse(File.read(manifest_path))
34
+ rescue JSON::ParserError => e
35
+ raise Aircana::Error, "Invalid JSON in plugin manifest: #{e.message}"
36
+ end
37
+
38
+ # Updates the plugin manifest with new values
39
+ def update(attributes = {})
40
+ current_data = read || {}
41
+ updated_data = current_data.merge(attributes.transform_keys(&:to_s))
42
+
43
+ validate_required_fields!(updated_data)
44
+ write_manifest(updated_data)
45
+
46
+ manifest_path
47
+ end
48
+
49
+ # Bumps the version number (major, minor, or patch)
50
+ def bump_version(type = :patch)
51
+ current_data = read
52
+ raise Aircana::Error, "No plugin manifest found at #{manifest_path}" unless current_data
53
+
54
+ current_version = current_data["version"]
55
+ new_version = bump_semantic_version(current_version, type)
56
+
57
+ update("version" => new_version)
58
+ new_version
59
+ end
60
+
61
+ # Checks if the plugin manifest exists
62
+ def exists?
63
+ File.exist?(manifest_path)
64
+ end
65
+
66
+ # Returns the path to the plugin manifest
67
+ def manifest_path
68
+ File.join(plugin_root, ".claude-plugin", "plugin.json")
69
+ end
70
+
71
+ # Returns the directory containing the manifest
72
+ def manifest_dir
73
+ File.join(plugin_root, ".claude-plugin")
74
+ end
75
+
76
+ # Validates the current manifest structure
77
+ def validate!
78
+ data = read
79
+ raise Aircana::Error, "No plugin manifest found" unless data
80
+
81
+ validate_required_fields!(data)
82
+ validate_version_format!(data["version"])
83
+
84
+ true
85
+ end
86
+
87
+ private
88
+
89
+ def build_manifest_data(attributes)
90
+ data = {
91
+ "name" => attributes[:name] || attributes["name"],
92
+ "version" => attributes[:version] || attributes["version"] || "0.1.0"
93
+ }
94
+
95
+ # Add optional fields if provided
96
+ OPTIONAL_FIELDS.each do |field|
97
+ value = attributes[field.to_sym] || attributes[field]
98
+ data[field] = value if value
99
+ end
100
+
101
+ data
102
+ end
103
+
104
+ def write_manifest(data)
105
+ FileUtils.mkdir_p(manifest_dir)
106
+ File.write(manifest_path, JSON.pretty_generate(data))
107
+ end
108
+
109
+ def validate_required_fields!(data)
110
+ REQUIRED_FIELDS.each do |field|
111
+ unless data[field] || data[field.to_sym]
112
+ raise Aircana::Error, "Plugin manifest missing required field: #{field}"
113
+ end
114
+ end
115
+ end
116
+
117
+ def validate_version_format!(version)
118
+ return if version.match?(/^\d+\.\d+\.\d+/)
119
+
120
+ raise Aircana::Error, "Invalid version format: #{version}. Must be semantic versioning (e.g., 1.0.0)"
121
+ end
122
+
123
+ def bump_semantic_version(version, type)
124
+ parts = version.split(".").map(&:to_i)
125
+ raise Aircana::Error, "Invalid version format: #{version}" if parts.size != 3
126
+
127
+ case type.to_sym
128
+ when :major
129
+ [parts[0] + 1, 0, 0].join(".")
130
+ when :minor
131
+ [parts[0], parts[1] + 1, 0].join(".")
132
+ when :patch
133
+ [parts[0], parts[1], parts[2] + 1].join(".")
134
+ else
135
+ raise Aircana::Error, "Invalid version bump type: #{type}. Must be major, minor, or patch"
136
+ end
137
+ end
138
+
139
+ class << self
140
+ # Creates a default plugin name from a directory path
141
+ def default_plugin_name(directory)
142
+ File.basename(directory).downcase.gsub(/[^a-z0-9]+/, "-")
143
+ end
144
+ end
145
+ end
146
+ end
@@ -98,7 +98,6 @@ module Aircana
98
98
  def check_configuration_directories
99
99
  {
100
100
  global: File.expand_path("~/.aircana"),
101
- project: File.join(Dir.pwd, ".aircana"),
102
101
  claude_global: File.expand_path("~/.claude"),
103
102
  claude_project: File.join(Dir.pwd, ".claude")
104
103
  }.transform_values { |path| Dir.exist?(path) }
@@ -5,7 +5,7 @@ model: <%= model %>
5
5
  color: <%= color %>
6
6
  ---
7
7
 
8
- <%= helpers.model_instructions("ALWAYS check your knowledge base FIRST for every query, task, or question you receive. Use `ls .claude/agents/#{agent_name}/knowledge/*.md` to list files from your knowledge base.
8
+ <%= helpers.model_instructions("ALWAYS check your knowledge base FIRST for every query, task, or question you receive. Use `ls ${CLAUDE_PLUGIN_ROOT}/agents/#{agent_name}/knowledge/*.md` to list files from your knowledge base.
9
9
 
10
10
  MANDATORY WORKFLOW:
11
11
  1. BEFORE responding to ANY request - search and read relevant files in your knowledge base
@@ -18,7 +18,7 @@ Your knowledge base contains domain-specific information that takes priority ove
18
18
 
19
19
  ## Knowledge Base Integration
20
20
 
21
- Your specialized knowledge is in the current project directory. Use `ls .claude/agents/<%= agent_name %>/knowledge/*.md` to list available files, then read them with the Read tool.
21
+ Your specialized knowledge is in the plugin directory. Use `ls ${CLAUDE_PLUGIN_ROOT}/agents/<%= agent_name %>/knowledge/*.md` to list available files, then read them with the Read tool.
22
22
 
23
23
  This knowledge base contains:
24
24
  - Domain-specific documentation from Confluence
@@ -22,12 +22,6 @@ if [ -f "Gemfile" ]; then
22
22
  fi
23
23
  fi
24
24
 
25
- # Check for relevant files context
26
- if [ -d ".aircana/relevant_files" ] && [ "$(ls -A .aircana/relevant_files 2>/dev/null)" ]; then
27
- RELEVANT_COUNT=$(ls .aircana/relevant_files | wc -l)
28
- CONTEXT_ADDITIONS="${CONTEXT_ADDITIONS}\n\nRelevant Files: $RELEVANT_COUNT files currently in context."
29
- fi
30
-
31
25
  # Output JSON response with additional context
32
26
  if [ -n "$CONTEXT_ADDITIONS" ]; then
33
27
  # Escape context for JSON
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aircana
4
- VERSION = "3.0.0.rc1"
4
+ VERSION = "3.0.0.rc3"
5
5
  end
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "1.0",
3
+ "agent": "test-agent",
4
+ "sources": [
5
+ {
6
+ "type": "confluence",
7
+ "label": "test-agent",
8
+ "pages": [
9
+ {
10
+ "id": "123"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "1.0",
3
+ "agent": "test-agent",
4
+ "sources": [
5
+ {
6
+ "type": "confluence",
7
+ "label": "test-agent",
8
+ "pages": [
9
+ {
10
+ "id": "123"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "1.0",
3
+ "agent": "test-agent",
4
+ "sources": [
5
+ {
6
+ "type": "confluence",
7
+ "label": "test-agent",
8
+ "pages": [
9
+ {
10
+ "id": "123"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "1.0",
3
+ "agent": "test-agent",
4
+ "sources": [
5
+ {
6
+ "type": "confluence",
7
+ "label": "test-agent",
8
+ "pages": [
9
+ {
10
+ "id": "123"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "1.0",
3
+ "agent": "test-agent",
4
+ "sources": [
5
+ {
6
+ "type": "confluence",
7
+ "label": "test-agent",
8
+ "pages": [
9
+ {
10
+ "id": "123"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "1.0",
3
+ "agent": "test-agent",
4
+ "sources": [
5
+ {
6
+ "type": "confluence",
7
+ "label": "test-agent",
8
+ "pages": [
9
+ {
10
+ "id": "123"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aircana
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc1
4
+ version: 3.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weston Dransfield
@@ -102,6 +102,7 @@ executables:
102
102
  extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
+ - ".claude-plugin/plugin.json"
105
106
  - ".devcontainer/devcontainer.json"
106
107
  - ".dockerignore"
107
108
  - ".rspec_status"
@@ -113,8 +114,23 @@ files:
113
114
  - README.md
114
115
  - Rakefile
115
116
  - SECURITY.md
117
+ - agents/apply_feedback.md
118
+ - agents/executor.md
119
+ - agents/jira.md
120
+ - agents/planner.md
121
+ - agents/reviewer.md
122
+ - agents/sub-agent-coordinator.md
123
+ - agents/test-agent/manifest.json
124
+ - commands/air-apply-feedback.md
125
+ - commands/air-ask-expert.md
126
+ - commands/air-execute.md
127
+ - commands/air-plan.md
128
+ - commands/air-record.md
129
+ - commands/air-review.md
130
+ - commands/sample-command.md
116
131
  - compose.yml
117
132
  - exe/aircana
133
+ - hooks/hooks.json
118
134
  - lib/aircana.rb
119
135
  - lib/aircana/cli.rb
120
136
  - lib/aircana/cli/app.rb
@@ -125,7 +141,8 @@ files:
125
141
  - lib/aircana/cli/commands/dump_context.rb
126
142
  - lib/aircana/cli/commands/generate.rb
127
143
  - lib/aircana/cli/commands/hooks.rb
128
- - lib/aircana/cli/commands/install.rb
144
+ - lib/aircana/cli/commands/init.rb
145
+ - lib/aircana/cli/commands/plugin.rb
129
146
  - lib/aircana/cli/help_formatter.rb
130
147
  - lib/aircana/cli/shell_command.rb
131
148
  - lib/aircana/cli/subcommand.rb
@@ -150,9 +167,11 @@ files:
150
167
  - lib/aircana/generators/plan_command_generator.rb
151
168
  - lib/aircana/generators/record_command_generator.rb
152
169
  - lib/aircana/generators/review_command_generator.rb
170
+ - lib/aircana/hooks_manifest.rb
153
171
  - lib/aircana/human_logger.rb
154
172
  - lib/aircana/initializers.rb
155
173
  - lib/aircana/llm/claude_client.rb
174
+ - lib/aircana/plugin_manifest.rb
156
175
  - lib/aircana/progress_tracker.rb
157
176
  - lib/aircana/system_checker.rb
158
177
  - lib/aircana/templates/agents/base_agent.erb
@@ -178,6 +197,12 @@ files:
178
197
  - lib/aircana/templates/hooks/user_prompt_submit.erb
179
198
  - lib/aircana/version.rb
180
199
  - sig/aircana.rbs
200
+ - spec_target_1760205040_181/agents/test-agent/manifest.json
201
+ - spec_target_1760205220_486/agents/test-agent/manifest.json
202
+ - spec_target_1760205379_250/agents/test-agent/manifest.json
203
+ - spec_target_1760205601_652/agents/test-agent/manifest.json
204
+ - spec_target_1760205608_135/agents/test-agent/manifest.json
205
+ - spec_target_1760205654_952/agents/test-agent/manifest.json
181
206
  homepage: https://github.com/westonkd/aircana
182
207
  licenses:
183
208
  - MIT
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require_relative "generate"
5
-
6
- module Aircana
7
- module CLI
8
- module Install
9
- class << self
10
- def run
11
- generate_files
12
- install_commands_to_claude
13
- install_hooks_to_claude
14
- end
15
-
16
- private
17
-
18
- def generate_files
19
- Aircana.human_logger.info("Generating files before installation...")
20
- Generate.run
21
- end
22
-
23
- def install_commands_to_claude
24
- claude_commands_dir = File.join(Aircana.configuration.claude_code_project_config_path, "commands")
25
- Aircana.create_dir_if_needed(claude_commands_dir)
26
-
27
- copy_command_files(claude_commands_dir)
28
- install_agents_to_claude
29
- end
30
-
31
- def copy_command_files(destination_dir)
32
- Dir.glob("#{Aircana.configuration.output_dir}/commands/*").each do |file|
33
- Aircana.human_logger.success("Installing #{file} to #{destination_dir}")
34
- FileUtils.cp(file, destination_dir)
35
- end
36
- end
37
-
38
- def install_agents_to_claude
39
- claude_agents_dir = File.join(Aircana.configuration.claude_code_project_config_path, "agents")
40
- Aircana.create_dir_if_needed(claude_agents_dir)
41
-
42
- copy_agent_files(claude_agents_dir)
43
- end
44
-
45
- def copy_agent_files(destination_dir)
46
- agent_files_pattern = File.join(Aircana.configuration.output_dir, "agents", "*.md")
47
- Dir.glob(agent_files_pattern).each do |file|
48
- agent_name = File.basename(file, ".md")
49
- next unless default_agent?(agent_name)
50
-
51
- destination_file = File.join(destination_dir, File.basename(file))
52
- # Skip copying if source and destination are the same
53
- next if File.expand_path(file) == File.expand_path(destination_file)
54
-
55
- Aircana.human_logger.success("Installing default agent #{file} to #{destination_dir}")
56
- FileUtils.cp(file, destination_dir)
57
- end
58
- end
59
-
60
- def default_agent?(agent_name)
61
- require_relative "../../generators/agents_generator"
62
- Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
63
- end
64
-
65
- def install_hooks_to_claude
66
- return unless Dir.exist?(Aircana.configuration.hooks_dir)
67
-
68
- settings_file = File.join(Aircana.configuration.claude_code_project_config_path, "settings.local.json")
69
- install_hooks_to_settings(settings_file)
70
- end
71
-
72
- def install_hooks_to_settings(settings_file)
73
- settings = load_settings(settings_file)
74
- hook_configs = build_hook_configs
75
-
76
- return if hook_configs.empty?
77
-
78
- settings["hooks"] = hook_configs
79
- save_settings(settings_file, settings)
80
-
81
- Aircana.human_logger.success("Installed hooks to #{settings_file}")
82
- end
83
-
84
- def load_settings(settings_file)
85
- if File.exist?(settings_file)
86
- JSON.parse(File.read(settings_file))
87
- else
88
- Aircana.create_dir_if_needed(File.dirname(settings_file))
89
- {}
90
- end
91
- rescue JSON::ParserError
92
- Aircana.human_logger.warn("Invalid JSON in #{settings_file}, creating new settings")
93
- {}
94
- end
95
-
96
- def save_settings(settings_file, settings)
97
- File.write(settings_file, JSON.pretty_generate(settings))
98
- end
99
-
100
- def build_hook_configs
101
- hooks = {}
102
-
103
- # Map hook files to Claude Code hook events and their properties
104
- hook_mappings = {
105
- "pre_tool_use" => { event: "PreToolUse", matcher: nil },
106
- "post_tool_use" => { event: "PostToolUse", matcher: nil },
107
- "user_prompt_submit" => { event: "UserPromptSubmit", matcher: nil },
108
- "session_start" => { event: "SessionStart", matcher: nil },
109
- "notification_sqs" => { event: "Notification", matcher: nil },
110
- "rubocop_pre_commit" => { event: "PreToolUse", matcher: "Bash" },
111
- "rspec_test" => { event: "PostToolUse", matcher: "Bash" },
112
- "bundle_install" => { event: "PostToolUse", matcher: "Bash" }
113
- }
114
-
115
- Dir.glob("#{Aircana.configuration.hooks_dir}/*.sh").each do |hook_file|
116
- hook_name = File.basename(hook_file, ".sh")
117
-
118
- # Determine mapping for this hook
119
- mapping = if hook_mappings.key?(hook_name)
120
- hook_mappings[hook_name]
121
- else
122
- # For custom hooks, try to infer the event type from the filename
123
- infer_hook_mapping(hook_name)
124
- end
125
-
126
- next unless mapping
127
-
128
- event_key = mapping[:event]
129
-
130
- # Create relative path from project root
131
- relative_path = File.join(".aircana", "hooks", "#{hook_name}.sh")
132
-
133
- hook_entry = {
134
- "hooks" => [
135
- {
136
- "type" => "command",
137
- "command" => relative_path
138
- }
139
- ]
140
- }
141
-
142
- # Add matcher if specified
143
- hook_entry["matcher"] = mapping[:matcher] if mapping[:matcher]
144
-
145
- hooks[event_key] ||= []
146
- hooks[event_key] << hook_entry
147
- end
148
-
149
- hooks
150
- end
151
-
152
- def infer_hook_mapping(hook_name)
153
- # Try to infer the event type from common patterns in the hook name
154
- case hook_name
155
- when /pre_tool_use|pre_tool|before_tool/i
156
- { event: "PreToolUse", matcher: nil }
157
- when /user_prompt|prompt_submit|before_prompt/i
158
- { event: "UserPromptSubmit", matcher: nil }
159
- when /session_start|session_init|startup/i
160
- { event: "SessionStart", matcher: nil }
161
- else
162
- # Default to PostToolUse for unknown custom hooks and post_tool patterns
163
- { event: "PostToolUse", matcher: nil }
164
- end
165
- end
166
- end
167
- end
168
- end
169
- end