aircana 4.0.0.rc1 → 4.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.
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../generators/plan_command_generator"
4
- require_relative "../../generators/record_command_generator"
5
- require_relative "../../generators/execute_command_generator"
6
- require_relative "../../generators/review_command_generator"
7
- require_relative "../../generators/apply_feedback_command_generator"
8
- require_relative "../../generators/ask_expert_command_generator"
3
+ # Commands that reference old agent system - commented out for now
4
+ # require_relative "../../generators/plan_command_generator"
5
+ # require_relative "../../generators/record_command_generator"
6
+ # require_relative "../../generators/execute_command_generator"
7
+ # require_relative "../../generators/review_command_generator"
8
+ # require_relative "../../generators/apply_feedback_command_generator"
9
+ # require_relative "../../generators/ask_expert_command_generator"
9
10
  require_relative "../../generators/hooks_generator"
10
11
 
11
12
  module Aircana
@@ -13,14 +14,18 @@ module Aircana
13
14
  module Generate
14
15
  class << self
15
16
  def generators
16
- @generators ||= [
17
- Aircana::Generators::PlanCommandGenerator.new,
18
- Aircana::Generators::RecordCommandGenerator.new,
19
- Aircana::Generators::ExecuteCommandGenerator.new,
20
- Aircana::Generators::ReviewCommandGenerator.new,
21
- Aircana::Generators::ApplyFeedbackCommandGenerator.new,
22
- Aircana::Generators::AskExpertCommandGenerator.new
23
- ]
17
+ # No default commands for now - they referenced the old agent system
18
+ @generators ||= []
19
+
20
+ # TODO: Re-enable these when we have a new command system
21
+ # @generators ||= [
22
+ # Aircana::Generators::PlanCommandGenerator.new,
23
+ # Aircana::Generators::RecordCommandGenerator.new,
24
+ # Aircana::Generators::ExecuteCommandGenerator.new,
25
+ # Aircana::Generators::ReviewCommandGenerator.new,
26
+ # Aircana::Generators::ApplyFeedbackCommandGenerator.new,
27
+ # Aircana::Generators::AskExpertCommandGenerator.new
28
+ # ]
24
29
  end
25
30
 
26
31
  def run
@@ -132,7 +132,6 @@ module Aircana
132
132
  Aircana.create_dir_if_needed(commands_dir)
133
133
 
134
134
  copy_command_files(commands_dir)
135
- install_default_agents
136
135
  end
137
136
 
138
137
  def copy_command_files(destination_dir)
@@ -142,33 +141,6 @@ module Aircana
142
141
  end
143
142
  end
144
143
 
145
- def install_default_agents
146
- agents_dir = Aircana.configuration.agents_dir
147
- Aircana.create_dir_if_needed(agents_dir)
148
-
149
- copy_agent_files(agents_dir)
150
- end
151
-
152
- def copy_agent_files(destination_dir)
153
- agent_files_pattern = File.join(Aircana.configuration.output_dir, "agents", "*.md")
154
- Dir.glob(agent_files_pattern).each do |file|
155
- agent_name = File.basename(file, ".md")
156
- next unless default_agent?(agent_name)
157
-
158
- destination_file = File.join(destination_dir, File.basename(file))
159
- # Skip copying if source and destination are the same
160
- next if File.expand_path(file) == File.expand_path(destination_file)
161
-
162
- Aircana.human_logger.success("Installing default agent: #{agent_name}")
163
- FileUtils.cp(file, destination_dir)
164
- end
165
- end
166
-
167
- def default_agent?(agent_name)
168
- require_relative "../../generators/agents_generator"
169
- Aircana::Generators::AgentsGenerator.available_default_agents.include?(agent_name)
170
- end
171
-
172
144
  def install_hooks
173
145
  scripts_dir = Aircana.configuration.scripts_dir
174
146
  return unless Dir.exist?(scripts_dir)
@@ -201,7 +173,7 @@ module Aircana
201
173
  "post_tool_use" => { event: "PostToolUse", matcher: nil },
202
174
  "user_prompt_submit" => { event: "UserPromptSubmit", matcher: nil },
203
175
  "session_start" => { event: "SessionStart", matcher: nil },
204
- "refresh_agents" => { event: "SessionStart", matcher: nil },
176
+ "refresh_skills" => { event: "SessionStart", matcher: nil },
205
177
  "notification_sqs" => { event: "Notification", matcher: nil },
206
178
  "rubocop_pre_commit" => { event: "PreToolUse", matcher: "Bash" },
207
179
  "rspec_test" => { event: "PostToolUse", matcher: "Bash" },
@@ -265,11 +237,11 @@ module Aircana
265
237
  Aircana.human_logger.info("\nPlugin structure:")
266
238
  Aircana.human_logger.info(" .claude-plugin/plugin.json - Plugin metadata")
267
239
  Aircana.human_logger.info(" commands/ - Slash commands")
268
- Aircana.human_logger.info(" agents/ - Specialized agents")
240
+ Aircana.human_logger.info(" agents/ - Knowledge bases")
269
241
  Aircana.human_logger.info(" hooks/ - Event hook configurations")
270
242
  Aircana.human_logger.info(" scripts/ - Hook scripts and utilities")
271
243
  Aircana.human_logger.info("\nNext steps:")
272
- Aircana.human_logger.info(" - Create agents: aircana agents create")
244
+ Aircana.human_logger.info(" - Create knowledge bases: aircana kb create")
273
245
  Aircana.human_logger.info(" - Install plugin in Claude Code")
274
246
  Aircana.human_logger.info(" - Run: aircana plugin info")
275
247
  end
@@ -29,7 +29,8 @@ module Aircana
29
29
  def create # rubocop:disable Metrics/MethodLength
30
30
  prompt = TTY::Prompt.new
31
31
 
32
- kb_name = prompt.ask("Knowledge base name:")
32
+ kb_name = prompt.ask("What topic should this knowledge base cover?",
33
+ default: "e.g., 'Canvas Backend Database', 'API Design'")
33
34
  short_description = prompt.ask("Briefly describe what this KB contains:")
34
35
 
35
36
  # Prompt for knowledge base type
@@ -48,13 +49,14 @@ module Aircana
48
49
  normalized_kb_name = normalize_string(kb_name)
49
50
 
50
51
  # Prompt for knowledge fetching
51
- prompt_for_knowledge_fetch(prompt, normalized_kb_name, kb_type, short_description)
52
+ fetched_confluence = prompt_for_knowledge_fetch(prompt, normalized_kb_name, kb_type, short_description)
52
53
 
53
54
  # Prompt for web URL fetching
54
- prompt_for_url_fetch(prompt, normalized_kb_name, kb_type)
55
+ fetched_urls = prompt_for_url_fetch(prompt, normalized_kb_name, kb_type)
55
56
 
56
- # Generate SKILL.md
57
- regenerate_skill_md(normalized_kb_name, short_description)
57
+ # Generate SKILL.md if no content was fetched during the prompts
58
+ # (the prompt functions already generate it when they successfully fetch content)
59
+ regenerate_skill_md(normalized_kb_name, short_description) unless fetched_confluence || fetched_urls
58
60
 
59
61
  # If remote kb_type, ensure SessionStart hook is installed
60
62
  ensure_remote_knowledge_refresh_hook if kb_type == "remote"
@@ -326,7 +328,7 @@ module Aircana
326
328
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
327
329
  # rubocop:disable Metrics/PerceivedComplexity
328
330
  def prompt_for_knowledge_fetch(prompt, normalized_kb_name, kb_type, short_description)
329
- return unless confluence_configured?
331
+ return false unless confluence_configured?
330
332
 
331
333
  if prompt.yes?("Would you like to fetch knowledge for this KB from Confluence now?")
332
334
  Aircana.human_logger.info "Fetching knowledge from Confluence..."
@@ -340,8 +342,11 @@ module Aircana
340
342
  end
341
343
 
342
344
  result = perform_refresh(normalized_kb_name, kb_type, label: label)
343
- ensure_gitignore_entry(kb_type) if result[:pages_count]&.positive?
344
- regenerate_skill_md(normalized_kb_name, short_description) if result[:pages_count]&.positive?
345
+ if result[:pages_count]&.positive?
346
+ ensure_gitignore_entry(kb_type)
347
+ regenerate_skill_md(normalized_kb_name, short_description)
348
+ return true
349
+ end
345
350
  else
346
351
  refresh_message = if kb_type == "local"
347
352
  "fetch knowledge"
@@ -352,6 +357,8 @@ module Aircana
352
357
  "Skipping knowledge fetch. You can #{refresh_message} later."
353
358
  )
354
359
  end
360
+
361
+ false
355
362
  rescue Aircana::Error => e
356
363
  Aircana.human_logger.warn "Failed to fetch knowledge: #{e.message}"
357
364
  refresh_message = if kb_type == "local"
@@ -360,6 +367,7 @@ module Aircana
360
367
  "try again later with 'aircana kb refresh #{normalized_kb_name}'"
361
368
  end
362
369
  Aircana.human_logger.info "You can #{refresh_message}"
370
+ false
363
371
  end
364
372
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
365
373
  # rubocop:enable Metrics/PerceivedComplexity
@@ -367,7 +375,7 @@ module Aircana
367
375
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
368
376
  # rubocop:disable Metrics/PerceivedComplexity
369
377
  def prompt_for_url_fetch(prompt, normalized_kb_name, kb_type)
370
- return unless prompt.yes?("Would you like to add web URLs for this KB's knowledge base?")
378
+ return false unless prompt.yes?("Would you like to add web URLs for this KB's knowledge base?")
371
379
 
372
380
  urls = []
373
381
  loop do
@@ -382,7 +390,7 @@ module Aircana
382
390
  end
383
391
  end
384
392
 
385
- return if urls.empty?
393
+ return false if urls.empty?
386
394
 
387
395
  begin
388
396
  Aircana.human_logger.info "Fetching #{urls.size} URL(s)..."
@@ -393,6 +401,7 @@ module Aircana
393
401
  Aircana.human_logger.success "Successfully fetched #{result[:pages_count]} URL(s)"
394
402
  ensure_gitignore_entry(kb_type)
395
403
  regenerate_skill_md(normalized_kb_name)
404
+ return true
396
405
  else
397
406
  Aircana.human_logger.warn "No URLs were successfully fetched"
398
407
  end
@@ -402,6 +411,8 @@ module Aircana
402
411
  "You can add URLs later with 'aircana kb add-url #{normalized_kb_name} <URL>'"
403
412
  )
404
413
  end
414
+
415
+ false
405
416
  end
406
417
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
407
418
  # rubocop:enable Metrics/PerceivedComplexity
@@ -24,8 +24,7 @@ module Aircana
24
24
  def command_groups
25
25
  {
26
26
  "Knowledge Base Management" => %w[kb],
27
- "Hook Management" => %w[hooks],
28
- "System" => %w[generate init doctor dump-context]
27
+ "System" => %w[init doctor]
29
28
  }
30
29
  end
31
30
 
@@ -14,9 +14,52 @@ module Aircana
14
14
  def convert_to_markdown(html_content)
15
15
  return "" if html_content.nil? || html_content.empty?
16
16
 
17
- ReverseMarkdown.convert(html_content, github_flavored: true)
17
+ # Preprocess Confluence macros before converting to Markdown
18
+ cleaned_html = preprocess_confluence_macros(html_content)
19
+ ReverseMarkdown.convert(cleaned_html, github_flavored: true)
18
20
  end
19
21
 
22
+ # rubocop:disable Layout/LineLength, Metrics/MethodLength
23
+ def preprocess_confluence_macros(html)
24
+ # Process Confluence structured macros to make them compatible with Markdown conversion
25
+ cleaned = html.dup
26
+
27
+ # Remove empty code blocks (common issue with Confluence API)
28
+ cleaned.gsub!(
29
+ %r{<ac:structured-macro[^>]*ac:name="code"[^>]*>.*?<ac:plain-text-body>\s*</ac:plain-text-body>.*?</ac:structured-macro>}m, ""
30
+ )
31
+
32
+ # Convert panel macros to blockquotes, preserving inner content
33
+ cleaned.gsub!(
34
+ %r{<ac:structured-macro[^>]*ac:name="panel"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote>\1</blockquote>'
35
+ )
36
+
37
+ # Convert info/note/warning macros to blockquotes with indicators
38
+ cleaned.gsub!(
39
+ %r{<ac:structured-macro[^>]*ac:name="info"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote><strong>ℹ️ Info:</strong> \1</blockquote>'
40
+ )
41
+ cleaned.gsub!(
42
+ %r{<ac:structured-macro[^>]*ac:name="note"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote><strong>📝 Note:</strong> \1</blockquote>'
43
+ )
44
+ cleaned.gsub!(
45
+ %r{<ac:structured-macro[^>]*ac:name="warning"[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '<blockquote><strong>⚠️ Warning:</strong> \1</blockquote>'
46
+ )
47
+
48
+ # Strip other structured macros but preserve rich text body content
49
+ cleaned.gsub!(
50
+ %r{<ac:structured-macro[^>]*>.*?<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?</ac:structured-macro>}m, '\1'
51
+ )
52
+
53
+ # Remove any remaining Confluence-specific tags
54
+ cleaned.gsub!(%r{</?ac:[^>]*>}m, "")
55
+
56
+ # Clean up Confluence parameter tags
57
+ cleaned.gsub!(%r{<ac:parameter[^>]*>.*?</ac:parameter>}m, "")
58
+
59
+ cleaned
60
+ end
61
+ # rubocop:enable Layout/LineLength, Metrics/MethodLength
62
+
20
63
  def log_pages_found(count, kb_name)
21
64
  Aircana.human_logger.info "Found #{count} pages for KB '#{kb_name}'"
22
65
  end
@@ -11,7 +11,7 @@ module Aircana
11
11
  post_tool_use
12
12
  user_prompt_submit
13
13
  session_start
14
- refresh_agents
14
+ refresh_skills
15
15
  notification_sqs
16
16
  rubocop_pre_commit
17
17
  rspec_test
@@ -21,7 +21,7 @@ module Aircana
21
21
  # Default hooks that are auto-installed
22
22
  DEFAULT_HOOK_TYPES = %w[
23
23
  session_start
24
- refresh_agents
24
+ refresh_skills
25
25
  notification_sqs
26
26
  ].freeze
27
27
 
@@ -5,6 +5,7 @@ require_relative "../contexts/manifest"
5
5
 
6
6
  module Aircana
7
7
  module Generators
8
+ # rubocop:disable Metrics/ClassLength
8
9
  class SkillsGenerator < BaseGenerator
9
10
  attr_reader :kb_name, :short_description, :skill_description, :knowledge_files
10
11
 
@@ -24,11 +25,20 @@ module Aircana
24
25
  # rubocop:enable Metrics/ParameterLists
25
26
 
26
27
  # Generate SKILL.md based on manifest data
28
+ # rubocop:disable Metrics/MethodLength
27
29
  def self.from_manifest(kb_name)
28
30
  manifest = Contexts::Manifest.read_manifest(kb_name)
29
31
  raise Error, "No manifest found for knowledge base '#{kb_name}'" unless manifest
30
32
 
31
33
  knowledge_files = extract_knowledge_files_from_manifest(manifest)
34
+
35
+ # Warn if no knowledge files were found
36
+ if knowledge_files.empty?
37
+ Aircana.human_logger.warn "No knowledge files found for KB '#{kb_name}'. " \
38
+ "SKILL.md will be generated but will be empty. " \
39
+ "Run 'aircana kb refresh #{kb_name}' to fetch knowledge."
40
+ end
41
+
32
42
  skill_description = generate_skill_description_from_manifest(manifest, kb_name)
33
43
 
34
44
  new(
@@ -37,11 +47,45 @@ module Aircana
37
47
  knowledge_files: knowledge_files
38
48
  )
39
49
  end
50
+ # rubocop:enable Metrics/MethodLength
40
51
 
41
52
  # Class methods for manifest processing
53
+ def self.extract_knowledge_files_from_manifest(manifest)
54
+ kb_name = manifest["name"]
55
+
56
+ # If kb_dir exists, scan actual files on disk (preferred method)
57
+ if kb_name && Aircana.configuration.kb_knowledge_dir
58
+ kb_dir = Aircana.configuration.kb_path(kb_name)
59
+
60
+ return extract_files_from_disk(manifest, kb_dir) if Dir.exist?(kb_dir)
61
+ end
62
+
63
+ # Fallback: extract from manifest metadata (for tests or before files are created)
64
+ extract_files_from_manifest_metadata(manifest)
65
+ end
66
+
67
+ # rubocop:disable Metrics/MethodLength
68
+ def self.extract_files_from_disk(manifest, kb_dir)
69
+ actual_files = Dir.glob(File.join(kb_dir, "*.md"))
70
+ .reject { |f| File.basename(f) == "SKILL.md" }
71
+ .sort
72
+
73
+ # Build file list with summaries from manifest
74
+ actual_files.map do |filepath|
75
+ filename = File.basename(filepath)
76
+ summary = find_summary_for_file(manifest, filename)
77
+
78
+ {
79
+ summary: summary || File.basename(filename, ".md").tr("-", " ").capitalize,
80
+ filename: filename
81
+ }
82
+ end
83
+ end
84
+ # rubocop:enable Metrics/MethodLength
85
+
42
86
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
43
87
  # rubocop:disable Metrics/PerceivedComplexity
44
- def self.extract_knowledge_files_from_manifest(manifest)
88
+ def self.extract_files_from_manifest_metadata(manifest)
45
89
  files = []
46
90
 
47
91
  manifest["sources"]&.each do |source|
@@ -68,6 +112,29 @@ module Aircana
68
112
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
69
113
  # rubocop:enable Metrics/PerceivedComplexity
70
114
 
115
+ # Find the summary for a given filename from the manifest
116
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
117
+ # rubocop:disable Metrics/PerceivedComplexity
118
+ def self.find_summary_for_file(manifest, filename)
119
+ manifest["sources"]&.each do |source|
120
+ case source["type"]
121
+ when "confluence"
122
+ source["pages"]&.each do |page|
123
+ return page["summary"] if filename.include?(page["id"])
124
+ end
125
+ when "web"
126
+ source["urls"]&.each do |url_entry|
127
+ sanitized_url_part = sanitize_filename_from_url(url_entry["url"])
128
+ return url_entry["summary"] if filename.include?(sanitized_url_part)
129
+ end
130
+ end
131
+ end
132
+
133
+ nil
134
+ end
135
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
136
+ # rubocop:enable Metrics/PerceivedComplexity
137
+
71
138
  def self.generate_skill_description_from_manifest(manifest, kb_name)
72
139
  # Generate a description optimized for Claude's skill discovery
73
140
  source_count = manifest["sources"]&.size || 0
@@ -91,6 +158,7 @@ module Aircana
91
158
  rescue URI::InvalidURIError
92
159
  "web_resource"
93
160
  end
161
+ # rubocop:enable Metrics/ClassLength
94
162
 
95
163
  protected
96
164
 
@@ -0,0 +1,121 @@
1
+ #!/bin/bash
2
+ # Auto-refresh remote knowledge bases on session start
3
+ # Runs at most once per 24 hours to avoid API rate limits
4
+ # Only refreshes remote KBs, skips local KBs
5
+
6
+ set -e
7
+
8
+ # Create log directory if it doesn't exist
9
+ mkdir -p ~/.aircana
10
+ LOG_FILE="$HOME/.aircana/hooks.log"
11
+
12
+ # Claude Code provides this environment variable
13
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
14
+
15
+ if [ -z "$PLUGIN_ROOT" ]; then
16
+ echo "$(date): Warning - CLAUDE_PLUGIN_ROOT not set, skipping KB refresh" >> "$LOG_FILE"
17
+ echo "{}"
18
+ exit 0
19
+ fi
20
+
21
+ TIMESTAMP_FILE="${PLUGIN_ROOT}/.last_refresh"
22
+ REFRESH_INTERVAL_SECONDS=86400 # 24 hours
23
+
24
+ # Check if we've refreshed recently
25
+ if [ -f "$TIMESTAMP_FILE" ]; then
26
+ LAST_REFRESH=$(cat "$TIMESTAMP_FILE")
27
+ CURRENT_TIME=$(date +%s)
28
+ TIME_DIFF=$((CURRENT_TIME - LAST_REFRESH))
29
+
30
+ if [ $TIME_DIFF -lt $REFRESH_INTERVAL_SECONDS ]; then
31
+ HOURS_SINCE=$((TIME_DIFF / 3600))
32
+ echo "$(date): Knowledge bases refreshed ${HOURS_SINCE}h ago, skipping refresh" >> "$LOG_FILE"
33
+ echo "{}"
34
+ exit 0
35
+ fi
36
+ fi
37
+
38
+ # Tell aircana where the plugin lives
39
+ export AIRCANA_PLUGIN_ROOT="$PLUGIN_ROOT"
40
+
41
+ echo "$(date): Starting knowledge base refresh from plugin root: $PLUGIN_ROOT" >> "$LOG_FILE"
42
+
43
+ # Find all knowledge bases in agents directory
44
+ AGENTS_DIR="${PLUGIN_ROOT}/agents"
45
+
46
+ if [ ! -d "$AGENTS_DIR" ]; then
47
+ echo "$(date): No agents directory found, skipping refresh" >> "$LOG_FILE"
48
+ echo "{}"
49
+ exit 0
50
+ fi
51
+
52
+ # Track if we refreshed any KBs
53
+ REFRESHED_COUNT=0
54
+ SKIPPED_COUNT=0
55
+
56
+ # Iterate through each KB directory
57
+ for kb_dir in "${AGENTS_DIR}"/*/ ; do
58
+ # Skip if no directories found
59
+ [ -d "$kb_dir" ] || continue
60
+
61
+ kb_name=$(basename "$kb_dir")
62
+ manifest_file="${kb_dir}manifest.json"
63
+
64
+ # Skip if no manifest exists
65
+ if [ ! -f "$manifest_file" ]; then
66
+ echo "$(date): No manifest found for KB '$kb_name', skipping" >> "$LOG_FILE"
67
+ continue
68
+ fi
69
+
70
+ # Determine KB type from manifest (default to remote if not specified)
71
+ kb_type="remote"
72
+
73
+ # Try to parse with jq if available, otherwise use grep
74
+ if command -v jq >/dev/null 2>&1; then
75
+ kb_type=$(jq -r '.kb_type // "remote"' "$manifest_file" 2>/dev/null || echo "remote")
76
+ else
77
+ # Fallback: grep for kb_type field
78
+ if grep -q '"kb_type"[[:space:]]*:[[:space:]]*"local"' "$manifest_file" 2>/dev/null; then
79
+ kb_type="local"
80
+ fi
81
+ fi
82
+
83
+ # Only refresh remote KBs
84
+ if [ "$kb_type" = "local" ]; then
85
+ echo "$(date): Skipping local KB '$kb_name'" >> "$LOG_FILE"
86
+ SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
87
+ continue
88
+ fi
89
+
90
+ # Refresh remote KB
91
+ echo "$(date): Refreshing remote KB '$kb_name'" >> "$LOG_FILE"
92
+ if aircana kb refresh "$kb_name" >> "$LOG_FILE" 2>&1; then
93
+ echo "$(date): Successfully refreshed KB '$kb_name'" >> "$LOG_FILE"
94
+ REFRESHED_COUNT=$((REFRESHED_COUNT + 1))
95
+ else
96
+ echo "$(date): Warning - Failed to refresh KB '$kb_name'" >> "$LOG_FILE"
97
+ fi
98
+ done
99
+
100
+ # Update timestamp on completion (even if some refreshes failed)
101
+ date +%s > "$TIMESTAMP_FILE"
102
+
103
+ # Log summary
104
+ echo "$(date): KB refresh completed - refreshed: $REFRESHED_COUNT, skipped (local): $SKIPPED_COUNT" >> "$LOG_FILE"
105
+
106
+ # Return success with context
107
+ if [ $REFRESHED_COUNT -gt 0 ]; then
108
+ CONTEXT="Refreshed $REFRESHED_COUNT remote knowledge base(s)"
109
+ else
110
+ CONTEXT="No remote knowledge bases to refresh"
111
+ fi
112
+
113
+ ESCAPED_CONTEXT=$(echo -n "$CONTEXT" | sed 's/"/\\"/g')
114
+ cat << EOF
115
+ {
116
+ "hookSpecificOutput": {
117
+ "hookEventName": "SessionStart",
118
+ "additionalContext": "$ESCAPED_CONTEXT"
119
+ }
120
+ }
121
+ EOF
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aircana
4
- VERSION = "4.0.0.rc1"
4
+ VERSION = "4.0.0.rc3"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aircana
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.rc1
4
+ version: 4.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weston Dransfield
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-10-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: httparty
@@ -115,15 +115,8 @@ files:
115
115
  - README.md
116
116
  - Rakefile
117
117
  - SECURITY.md
118
- - commands/apply-feedback.md
119
- - commands/ask-expert.md
120
- - commands/execute.md
121
- - commands/plan.md
122
- - commands/record.md
123
- - commands/review.md
124
118
  - compose.yml
125
119
  - exe/aircana
126
- - hooks/hooks.json
127
120
  - lib/aircana.rb
128
121
  - lib/aircana/cli.rb
129
122
  - lib/aircana/cli/app.rb
@@ -132,7 +125,6 @@ files:
132
125
  - lib/aircana/cli/commands/doctor_helpers.rb
133
126
  - lib/aircana/cli/commands/dump_context.rb
134
127
  - lib/aircana/cli/commands/generate.rb
135
- - lib/aircana/cli/commands/hooks.rb
136
128
  - lib/aircana/cli/commands/init.rb
137
129
  - lib/aircana/cli/commands/kb.rb
138
130
  - lib/aircana/cli/commands/plugin.rb
@@ -177,6 +169,7 @@ files:
177
169
  - lib/aircana/templates/hooks/notification_sqs.erb
178
170
  - lib/aircana/templates/hooks/post_tool_use.erb
179
171
  - lib/aircana/templates/hooks/pre_tool_use.erb
172
+ - lib/aircana/templates/hooks/refresh_skills.erb
180
173
  - lib/aircana/templates/hooks/rspec_test.erb
181
174
  - lib/aircana/templates/hooks/rubocop_pre_commit.erb
182
175
  - lib/aircana/templates/hooks/session_start.erb
@@ -207,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
200
  - !ruby/object:Gem::Version
208
201
  version: '0'
209
202
  requirements: []
210
- rubygems_version: 3.6.2
203
+ rubygems_version: 3.6.9
211
204
  specification_version: 4
212
205
  summary: CLI for creating and managing Claude Code plugins with per-agent knowledge
213
206
  bases
@@ -1,15 +0,0 @@
1
- INSTRUCTIONS : Use the Task tool with subagent_type 'apply-feedback' to apply code review feedback from the previous /air-review command.
2
-
3
- Pass the review feedback from the conversation context to the apply-feedback agent.
4
-
5
- The apply-feedback agent will:
6
- 1. Parse review feedback from the previous review
7
- 2. Create todo list of changes prioritized by severity
8
- 3. Present plan to user for approval
9
- 4. Apply approved changes
10
- 5. Re-run unit tests to verify changes
11
- 6. Fix any test failures
12
- 7. Amend the HEAD commit with improvements using 'git commit --amend --no-edit'
13
- 8. Summarize changes made
14
-
15
- IMPORTANT: This command reads the review output from the conversation context, so it must be run in the same conversation as /air-review.
@@ -1,42 +0,0 @@
1
- INSTRUCTIONS : You are coordinating expert consultation to answer a question by leveraging multiple specialized sub-agents. Follow this precise workflow:
2
-
3
- STEP 1: QUESTION VALIDATION
4
- Ask the user: \"What is your question?\" and wait for their response before proceeding.
5
-
6
- STEP 2: COORDINATION PHASE
7
- Use the Task tool with subagent_type 'sub-agent-coordinator' to analyze the question and identify relevant sub-agents. Provide the coordinator with the complete question context.
8
-
9
- STEP 3: PARALLEL EXPERT CONSULTATION
10
- Based on the coordinator's recommendations, use the Task tool to consult each identified relevant sub-agent in parallel. For each agent:
11
- - Use the appropriate subagent_type for each recommended agent
12
- - Provide the original question plus any agent-specific context the coordinator suggested
13
- - Execute multiple Task tool calls in a single message for parallel processing
14
-
15
- STEP 4: SYNTHESIS AND RESPONSE
16
- After receiving responses from all consulted agents:
17
- - Analyze and synthesize the expert feedback
18
- - Identify common themes, conflicting viewpoints, and complementary insights
19
- - Provide a comprehensive answer that leverages the collective expertise
20
- - Cite which agents contributed specific insights where relevant
21
- - Note any areas where experts disagreed and provide your assessment
22
-
23
- STEP 5: FOLLOW-UP GUIDANCE
24
- If the question requires further clarification or the expert responses suggest additional considerations:
25
- - Suggest specific follow-up questions
26
- - Recommend additional agents to consult if needed
27
- - Provide guidance on next steps based on the expert consensus
28
-
29
- IMPORTANT EXECUTION NOTES:
30
- - Always start with the sub-agent-coordinator for proper agent selection
31
- - Use parallel Task tool execution when consulting multiple agents (single message with multiple tool calls)
32
- - Ensure each agent receives context appropriate to their expertise domain
33
- - Synthesize responses rather than simply concatenating them
34
- - Maintain focus on providing actionable, comprehensive answers
35
-
36
- EXAMPLE PARALLEL EXECUTION:
37
- If coordinator recommends agents A, B, and C, send one message with three Task tool calls:
38
- 1. Task(subagent_type='agent-A', prompt='[question + A-specific context]')
39
- 2. Task(subagent_type='agent-B', prompt='[question + B-specific context]')
40
- 3. Task(subagent_type='agent-C', prompt='[question + C-specific context]')
41
-
42
- This approach ensures you leverage the full expertise available while maintaining efficient coordination.
data/commands/execute.md DELETED
@@ -1,13 +0,0 @@
1
- INSTRUCTIONS : Use the Task tool with subagent_type 'executor' to execute the implementation plan from a Jira ticket.
2
-
3
- INSTRUCTIONS FOR EXECUTOR AGENT:
4
- Ask the user to provide a Jira ticket key/ID to execute.
5
-
6
- The executor agent will:
7
- 1. Read the plan from the Jira ticket via the 'jira' sub-agent
8
- 2. Review and validate the plan structure
9
- 3. Create a detailed execution todo list in Claude Code planning mode
10
- 4. Present the plan for your approval
11
- 5. Execute the approved implementation tasks
12
-
13
- IMPORTANT: All Jira operations are delegated to the 'jira' sub-agent using Task tool with subagent_type 'jira'.