aia 1.0.0 → 1.1.0

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.
data/docs/index.md CHANGED
@@ -23,80 +23,48 @@ Welcome to AIA, your powerful CLI tool for dynamic prompt management and AI inte
23
23
 
24
24
  ---
25
25
 
26
- !!! danger "Breaking Changes in v1.0.0 (from v0.11.2)"
26
+ !!! tip "🚀 New: AI Assistant Scheduler (AIAS)"
27
27
 
28
- **Version 1.0.0 is a major release with breaking changes. Read this section before upgrading.**
28
+ **Schedule and automate your AIA prompts!** AIAS is a new Ruby gem that lets you run AIA prompts on a cron-like schedule — perfect for recurring AI tasks, automated reports, and timed workflows.
29
29
 
30
- !!! failure "Prompt File Format (Critical)"
30
+ **[View AIAS on GitHub →](https://github.com/madbomber/aias)**
31
31
 
32
- The prompt file format has changed completely. You **must** migrate your prompts.
33
-
34
- - **File extension** — `.txt` is now `.md`
35
- - **Parameters** — `[PLACEHOLDER]` is now `<%= placeholder %>`
36
- - **Metadata** — Separate `.json` history files are now YAML front matter inside the `.md` file
37
- - **Directive embedding** — `//config`, `//pipeline`, etc. are now YAML front matter keys
38
-
39
- **Before (v0.11.2):**
40
-
41
- # ~/.prompts/my_prompt.txt
42
- //config temperature 0.7
43
- //pipeline next_prompt, another_prompt
44
- Tell me about [TOPIC] in [LANGUAGE]
45
-
46
- **After (v1.0.0):**
47
-
48
- ---
49
- temperature: 0.7
50
- pipeline: [next_prompt, another_prompt]
51
- parameters:
52
- topic: null
53
- language: null
54
- ---
55
- Tell me about <%= topic %> in <%= language %>
56
-
57
- **Run the migration tool:**
58
-
59
- # Back up first (recommended)
60
- cp -r ~/.prompts ~/.prompts.backup
61
-
62
- # Migrate all prompts
63
- migrate_prompts --verbose ~/.prompts
64
-
65
- # Review flagged files (*.txt-review), then recover them
66
- migrate_prompts --reprocess ~/.prompts
67
-
68
- The `migrate_prompts` script is a standalone program in `bin/`. It handles placeholder conversion, directive migration, history file merging, and flags files with code fences for manual review. See [Migration Guide](guides/migrate-prompts.md) for details.
32
+ ---
69
33
 
70
- !!! warning "Removed and Deprecated CLI Options"
34
+ !!! tip "🎭 Roles: Give Your Robot a Personality"
71
35
 
72
- - **`--adapter` removed** AIA now exclusively uses `ruby_llm`. Remove `--adapter` from any scripts or aliases.
73
- - **`--regex` deprecated** — Parameter extraction uses ERB (`<%= param %>`) instead of regex. The flag prints a warning and has no effect.
74
- - **`--terse` deprecated** — No longer supported. The flag prints a warning and has no effect.
75
- - **`--metrics` renamed to `--tokens`** — Use `--tokens` to display token usage. `--cost` implies `--tokens`.
36
+ Roles are plain-text prompt files that define how your AI thinks, talks, and interacts with you. Drop one in `~/.prompts/roles/` and your robot instantly becomes someone new.
76
37
 
77
- !!! warning "Dependency: prompt_manager v0.5 to v1.0"
38
+ **For fun:**
39
+ ```bash
40
+ aia --chat --role pirate # Arrr, matey!
41
+ aia --chat --role nyc_cabbie # opinions about EVERYTHING
42
+ aia --chat --role stoned_hacker # solves it anyway, dude
43
+ ```
78
44
 
79
- The `prompt_manager` gem has been upgraded from `~> 0.5.8` to `~> 1.0`. This is the root cause of the prompt file format changes above. The new version uses ERB for parameter interpolation and YAML front matter for metadata. All prompt files must be in the new format.
45
+ **For serious work:**
46
+ ```bash
47
+ # Explains quantum physics to your 7-year-old
48
+ aia --chat --role first_grade_teacher
80
49
 
81
- !!! info "Environment Variables (from v0.10.0)"
50
+ # Three expert robots on the same design doc, simultaneously
51
+ aia --model gpt-4o=architect,claude=security,gemini=performance design.md
52
+ ```
82
53
 
83
- If upgrading from v0.9.x, environment variable names changed in v0.10.0 to use double underscore (`__`) for nested config sections:
54
+ Assign a different role to each model and get multiple expert perspectives in one command. **[Full Roles Guide →](guides/models.md)**
84
55
 
85
- | Old (v0.9.x) | New (v1.0.0) |
86
- |---|---|
87
- | `AIA_PROMPTS_DIR` | `AIA_PROMPTS__DIR` |
88
- | `AIA_OUT_FILE` | `AIA_OUTPUT__FILE` |
89
- | `AIA_VERBOSE` | `AIA_FLAGS__VERBOSE` |
90
- | `AIA_DEBUG` | `AIA_FLAGS__DEBUG` |
91
- | `AIA_CHAT` | `AIA_FLAGS__CHAT` |
92
- | `AIA_TEMPERATURE` | `AIA_LLM__TEMPERATURE` |
93
- | `AIA_MARKDOWN` | `AIA_OUTPUT__MARKDOWN` |
56
+ !!! tip "🎓 Skills: Teach Your Robot Your Process"
94
57
 
95
- `AIA_MODEL` is unchanged. See [Environment Variables](configuration.md#environment-variables) for the complete list.
58
+ Skills are structured instruction sets that tell your robot *exactly how* to approach a task — your workflow, your standards, every single time. Each skill is a directory with a `SKILL.md` file in `~/.prompts/skills/`.
96
59
 
97
- !!! info "Configuration Files (from v0.10.0)"
60
+ ```bash
61
+ aia -s code-quality my_prompt # one skill
62
+ aia -s code-quality,security-review my_prompt # stack them
63
+ aia --chat --role senior_dev -s code-quality # role + skill
64
+ /skill code-quality # add mid-chat
65
+ ```
98
66
 
99
- If upgrading from v0.9.x, configuration files now use a nested YAML structure with sections (`llm:`, `prompts:`, `output:`, `flags:`, etc.) and follow the XDG Base Directory Specification (`~/.config/aia/aia.yml`). See [Configuration Guide](configuration.md) for details.
67
+ Combine roles and skills: a pirate who follows your code review process, a first-grade teacher who uses your step-by-step explanation method, or multiple robots each with their own role and skill set all from the command line. **[Full Skills Guide](directives-reference.md)**
100
68
 
101
69
  ---
102
70
 
@@ -105,9 +73,15 @@ Welcome to AIA, your powerful CLI tool for dynamic prompt management and AI inte
105
73
  ### 🚀 Dynamic Prompt Management
106
74
  - **Hierarchical Configuration**: Embedded directives > CLI args > environment variables > config files > defaults
107
75
  - **Prompt Sequences and Workflows**: Chain prompts together for complex AI workflows
108
- - **Role-based Prompts**: Use predefined roles to context your AI interactions
109
76
  - **Fuzzy Search**: Find prompts quickly with fuzzy matching (requires `fzf`)
110
77
 
78
+ ### 🎭 Roles & 🎓 Skills
79
+ - **Robot Personalities**: Give any robot a voice — fun personas like a pirate or NYC cabbie, or professional ones like a senior architect or first-grade teacher
80
+ - **Per-Model Roles**: Assign a different role to each model in a multi-model session — `--model gpt-4o=architect,claude=security`
81
+ - **Reusable Skill Sets**: Encode your exact workflow once in a `SKILL.md` file; apply it to any prompt with `-s skill-name`
82
+ - **Role + Skill Combos**: A robot can be *who you want* (role) and *know exactly what to do* (skill) simultaneously
83
+ - **Mid-Chat Skills**: Add a skill to a running chat session with `/skill skill-name`
84
+
111
85
  ### 🔧 Powerful Integration
112
86
  - **Shell Integration**: Execute shell commands directly within prompts
113
87
  - **Ruby (ERB) Processing**: Use Ruby code in your prompts for dynamic content
@@ -8,10 +8,15 @@ AIA provides sophisticated prompt management capabilities through the PM gem, en
8
8
  ```
9
9
  ~/.prompts/
10
10
  ├── README.md # Documentation for your prompt collection
11
- ├── roles/ # Role-based prompts for context setting
11
+ ├── roles/ # Role definitions (LLM personality/persona)
12
12
  │ ├── assistant.md
13
13
  │ ├── code_expert.md
14
14
  │ └── teacher.md
15
+ ├── skills/ # Skill definitions (task instructions)
16
+ │ ├── code-review/
17
+ │ │ └── SKILL.md # YAML front matter + instruction body
18
+ │ └── summarizer/
19
+ │ └── SKILL.md
15
20
  ├── development/ # Development-related prompts
16
21
  │ ├── code_review.md
17
22
  │ ├── debug_help.md
@@ -292,6 +297,88 @@ Current Task:
292
297
  Please provide guidance consistent with the project architecture and your role as <%= role %>.
293
298
  ```
294
299
 
300
+ ## Skills
301
+
302
+ ### Roles vs Skills
303
+
304
+ These two concepts work together but serve distinct purposes:
305
+
306
+ | Concept | Defines | Loaded from | Injected as |
307
+ |---------|---------|-------------|-------------|
308
+ | **Role** | LLM *personality* — who the model is | `~/.prompts/roles/<id>.md` | First, before skills and prompt |
309
+ | **Skill** | Task *instructions* — how to approach the work | `~/.prompts/skills/<name>/SKILL.md` | After role, before user prompt |
310
+
311
+ A **role** sets the persona: "You are a senior Ruby developer with deep expertise in performance optimization."
312
+
313
+ A **skill** provides procedural guidance for that persona to follow when executing the user's request: "When reviewing code, always check for: N+1 queries, missing indexes, memory leaks, and security vulnerabilities. Present findings as a prioritized list."
314
+
315
+ The assembled prompt order is:
316
+
317
+ ```
318
+ 1. Role content ← WHO the LLM is (personality)
319
+ 2. Skill content(s) ← HOW to approach the task (instructions)
320
+ 3. User prompt ← WHAT to do (request)
321
+ 4. Context files ← supporting material
322
+ ```
323
+
324
+ ### Skill File Format
325
+
326
+ Each skill lives in its own subdirectory under `~/.prompts/skills/`. The subdirectory must contain a `SKILL.md` file with YAML front matter followed by the skill instruction body:
327
+
328
+ ```markdown
329
+ ---
330
+ name: code-review
331
+ description: Thorough code review focusing on correctness, security, and maintainability.
332
+ user-invocable: true
333
+ argument-hint: ["file or topic to review"]
334
+ ---
335
+
336
+ When reviewing code, systematically check:
337
+
338
+ 1. **Correctness** — Does the logic match the stated intent? Are edge cases handled?
339
+ 2. **Security** — Are there injection risks, unsafe deserialization, or exposed secrets?
340
+ 3. **Performance** — Are there N+1 queries, unbounded loops, or unnecessary allocations?
341
+ 4. **Maintainability** — Is the code readable? Are names clear? Is complexity justified?
342
+
343
+ Present findings as a prioritized list with file:line references where applicable.
344
+ Always suggest a concrete fix, not just identification of the problem.
345
+ ```
346
+
347
+ The YAML front matter is metadata only. Only the body (everything after the closing `---`) is injected into the prompt.
348
+
349
+ ### Using Skills
350
+
351
+ ```bash
352
+ # Prepend a skill before the user prompt
353
+ aia --skill code-review review_prompt my_code.rb
354
+
355
+ # Combine role + skill for maximum context
356
+ aia --role ruby_expert --skill code-review review_prompt my_code.rb
357
+
358
+ # Multiple skills (applied in order)
359
+ aia --skill code-review --skill security-audit review_prompt my_code.rb
360
+ aia -s code-review,security-audit review_prompt my_code.rb
361
+
362
+ # List available skills
363
+ aia --list-skills
364
+
365
+ # Use a skill from within a chat session
366
+ /skill code-review
367
+ ```
368
+
369
+ ### Skills in Chat Mode
370
+
371
+ In chat mode, use the `/skill` directive to inject a skill at any point in the conversation:
372
+
373
+ ```
374
+ > /skill summarizer
375
+ [Skill "summarizer" instructions are injected into the next message context]
376
+
377
+ > Please summarize the discussion so far.
378
+ ```
379
+
380
+ The `/skill` directive injects only the body content of `SKILL.md` — the YAML front matter is never sent to the LLM.
381
+
295
382
  ## Prompt Workflows and Pipelines
296
383
 
297
384
  ### Simple Workflows
data/lib/aia/chat_loop.rb CHANGED
@@ -6,6 +6,8 @@ require "pm"
6
6
 
7
7
  module AIA
8
8
  class ChatLoop
9
+ include AIA::SkillUtils
10
+
9
11
  def initialize(chat_processor, ui_presenter, directive_processor)
10
12
  @chat_processor = chat_processor
11
13
  @ui_presenter = ui_presenter
@@ -15,6 +17,8 @@ module AIA
15
17
  # Start the interactive chat session
16
18
  def start(skip_context_files: false)
17
19
  setup_session
20
+ process_role_context
21
+ process_skill_context
18
22
  process_initial_context(skip_context_files)
19
23
  handle_piped_input
20
24
  run_loop
@@ -39,6 +43,56 @@ module AIA
39
43
  Signal.trap("INT") { exit }
40
44
  end
41
45
 
46
+ def process_role_context
47
+ role = AIA.config.prompts.role
48
+ return if role.nil? || role.empty?
49
+
50
+ prompt_handler = AIA::PromptHandler.new
51
+ role_parsed = prompt_handler.fetch_role(role)
52
+ return if role_parsed.nil?
53
+
54
+ role_content = role_parsed.to_s
55
+ return if role_content.nil? || role_content.strip.empty?
56
+
57
+ return unless AIA.client.respond_to?(:chats)
58
+
59
+ system_msg = RubyLLM::Message.new(role: :system, content: role_content)
60
+
61
+ AIA.client.chats.each_value do |chat|
62
+ next if chat.messages.any? { |m| m.role == :system }
63
+ chat.add_message(system_msg)
64
+ end
65
+ end
66
+
67
+ def process_skill_context
68
+ skills = AIA.config.prompts.skills
69
+ return if skills.nil? || skills.empty?
70
+
71
+ skills_dir = AIA.config.skills.dir
72
+ bodies = Array(skills).filter_map do |skill_name|
73
+ skill_name = skill_name.to_s.strip
74
+ next if skill_name.empty?
75
+
76
+ skill_path = find_skill_dir(skill_name, skills_dir)
77
+ next unless skill_path
78
+
79
+ if File.file?(skill_path)
80
+ skill_body(File.read(skill_path))
81
+ else
82
+ md = File.join(skill_path, 'SKILL.md')
83
+ skill_body(File.read(md)) if File.exist?(md)
84
+ end
85
+ end
86
+
87
+ return if bodies.empty?
88
+
89
+ skill_content = bodies.join("\n\n")
90
+ response_data = @chat_processor.process_prompt(skill_content)
91
+ content = response_data.is_a?(Hash) ? response_data[:content] : response_data
92
+ @chat_processor.output_response(content)
93
+ @ui_presenter.display_separator
94
+ end
95
+
42
96
  def process_initial_context(skip_context_files)
43
97
  return if skip_context_files || !AIA.config.context_files || AIA.config.context_files.empty?
44
98
 
@@ -98,7 +98,10 @@ module AIA
98
98
  first_model = models.first
99
99
  model_name = first_model.respond_to?(:name) ? first_model.name : first_model.to_s
100
100
 
101
- unless model_name.downcase.include?(client_model.downcase)
101
+ # client_model is the full resolved ID (e.g. "claude-sonnet-4-20250514"),
102
+ # model_name is the configured alias (e.g. "claude-sonnet-4").
103
+ # The alias is always a prefix/substring of the resolved ID, so check that way.
104
+ unless client_model.downcase.include?(model_name.downcase)
102
105
  AIA.client = AIA.client.class.new
103
106
  end
104
107
  end
@@ -6,7 +6,9 @@
6
6
  # for the Config class.
7
7
 
8
8
  require 'optparse'
9
+ require 'yaml'
9
10
  require_relative 'model_spec'
11
+ require_relative '../skill_utils'
10
12
 
11
13
  module AIA
12
14
  module CLIParser
@@ -166,6 +168,23 @@ module AIA
166
168
  warn "Warning: --regex is deprecated. PM v1.0.0 uses ERB parameters (<%= param %>)."
167
169
  options[:parameter_regex] = pattern
168
170
  end
171
+
172
+ opts.on("--skills-dir DIR", "Set directory containing skill subdirectories") do |dir|
173
+ options[:skills_dir] = dir
174
+ end
175
+
176
+ opts.on("--skills-prefix PREFIX", "Set subdirectory name for skill files (default: skills)") do |prefix|
177
+ options[:skills_prefix] = prefix
178
+ end
179
+
180
+ opts.on("-s", "--skill SKILL_IDS", "Prepend skill(s) to prompt (comma-separated IDs or paths)") do |ids|
181
+ options[:skills] ||= []
182
+ options[:skills] += ids.split(',').map(&:strip)
183
+ end
184
+
185
+ opts.on("--list-skills", "List available skills and exit") do
186
+ options[:list_skills] = true
187
+ end
169
188
  end
170
189
 
171
190
  def setup_ai_parameters(opts, options)
@@ -410,6 +429,13 @@ module AIA
410
429
  end
411
430
 
412
431
  def validate_role_exists(role_id)
432
+ if AIA::SkillUtils.path_based_id?(role_id)
433
+ expanded = File.expand_path(role_id)
434
+ expanded += '.md' if File.extname(expanded).empty?
435
+ raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
436
+ return
437
+ end
438
+
413
439
  prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(ENV['HOME'], '.prompts'))
414
440
  roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
415
441
 
@@ -442,19 +468,30 @@ module AIA
442
468
  roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
443
469
  roles_dir = File.join(prompts_dir, roles_prefix)
444
470
 
445
- if Dir.exist?(roles_dir)
446
- roles = list_available_role_names(prompts_dir, roles_prefix)
447
-
448
- if roles.empty?
449
- puts "No role files found in #{roles_dir}"
450
- puts "Create .md files in this directory to define roles."
451
- else
452
- puts "Available roles in #{roles_dir}:"
453
- roles.each { |role| puts " - #{role}" }
454
- end
455
- else
471
+ unless Dir.exist?(roles_dir)
456
472
  puts "No roles directory found at #{roles_dir}"
457
473
  puts "Create this directory and add role files to use roles."
474
+ return
475
+ end
476
+
477
+ roles = list_available_role_names(prompts_dir, roles_prefix)
478
+
479
+ if roles.empty?
480
+ puts "No role files found in #{roles_dir}"
481
+ puts "Create .md files in this directory to define roles."
482
+ return
483
+ end
484
+
485
+ roles.each do |role_id|
486
+ role_file = File.join(roles_dir, "#{role_id}.md")
487
+ fm = AIA::SkillUtils.parse_front_matter(role_file)
488
+
489
+ puts "## #{role_id}"
490
+ puts
491
+ puts "| Key | Value |"
492
+ puts "|-----|-------|"
493
+ fm.each { |key, value| puts "| #{key} | #{value} |" }
494
+ puts
458
495
  end
459
496
  end
460
497
 
@@ -464,6 +501,7 @@ module AIA
464
501
 
465
502
  Dir.glob("**/*.md", base: roles_dir)
466
503
  .map { |f| f.chomp('.md') }
504
+ .reject { |f| f.split('/').any? { |part| part.start_with?('_') } }
467
505
  .sort
468
506
  end
469
507
 
@@ -59,12 +59,26 @@ defaults:
59
59
  prompts:
60
60
  dir: ~/.prompts
61
61
  extname: .md
62
- roles_prefix: roles
63
- roles_dir: ~/.prompts/roles
62
+ #
64
63
  role: ~
64
+ roles_dir: ~/.prompts/roles
65
+ roles_prefix: roles
66
+ #
67
+ skills: []
68
+ skills_prefix: skills
69
+ #
70
+ tool: ~
71
+ tools_prefix: tools
72
+ #
65
73
  system_prompt: ~
66
74
  parameter_regex: ~
67
75
 
76
+ roles:
77
+ dir: ~/.prompts/roles
78
+
79
+ skills:
80
+ dir: ~/.prompts/skills
81
+
68
82
  # ---------------------------------------------------------------------------
69
83
  # Output Configuration
70
84
  # Access: AIA.config.output.file, AIA.config.output.append, etc.
@@ -112,6 +126,7 @@ defaults:
112
126
  # Env: AIA_TOOLS__PATHS, AIA_TOOLS__ALLOWED, etc.
113
127
  # ---------------------------------------------------------------------------
114
128
  tools:
129
+ dir: ~/.prompts/tools
115
130
  paths: []
116
131
  allowed: ~
117
132
  rejected: ~
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'word_wrapper'
4
4
  require_relative '../adapter/gem_activator'
5
+ require_relative '../skill_utils'
5
6
 
6
7
  # lib/aia/config/validator.rb
7
8
  #
@@ -31,6 +32,7 @@ module AIA
31
32
  handle_dump_config(config)
32
33
  handle_mcp_list(config)
33
34
  handle_list_tools(config)
35
+ handle_list_skills(config)
34
36
  handle_completion_script(config)
35
37
  validate_required_prompt_id(config)
36
38
  process_role_configuration(config)
@@ -128,14 +130,18 @@ module AIA
128
130
  return if role.nil? || role.empty?
129
131
 
130
132
  roles_prefix = config.prompts.roles_prefix
131
- unless roles_prefix.nil? || roles_prefix.empty?
132
- unless role.start_with?(roles_prefix)
133
- config.prompts.role = "#{roles_prefix}/#{role}"
134
- role = config.prompts.role
135
- end
133
+
134
+ unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
135
+ config.prompts.role = "#{roles_prefix}/#{role}"
136
+ role = config.prompts.role
136
137
  end
137
138
 
138
- config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix)
139
+ config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix.to_s)
140
+
141
+ # In chat-only mode (no prompt_id), leave the role configured so ChatLoop
142
+ # can inject it as initial context. Promoting it to prompt_id would cause
143
+ # PM to receive the role path as a literal string rather than file content.
144
+ return if config.flags&.chat == true
139
145
 
140
146
  if config.prompt_id.nil? || config.prompt_id.empty?
141
147
  unless role.nil? || role.empty?
@@ -230,6 +236,40 @@ module AIA
230
236
  exit 0
231
237
  end
232
238
 
239
+ def handle_list_skills(config)
240
+ return unless config.list_skills
241
+
242
+ skills_dir = AIA.config.skills.dir
243
+
244
+ unless Dir.exist?(skills_dir)
245
+ $stderr.puts "No skills directory found at #{skills_dir}"
246
+ exit 0
247
+ end
248
+
249
+ skill_dirs = Dir.glob("*/SKILL.md", base: skills_dir).map { |f| File.dirname(f) }.sort
250
+
251
+ if skill_dirs.empty?
252
+ $stderr.puts "No skills found in #{skills_dir}"
253
+ exit 0
254
+ end
255
+
256
+ skill_dirs.each do |skill_name|
257
+ skill_md = File.join(skills_dir, skill_name, 'SKILL.md')
258
+ fm = AIA::SkillUtils.parse_front_matter(skill_md)
259
+
260
+ puts "## #{skill_name}"
261
+ puts
262
+ puts "| Key | Value |"
263
+ puts "|-----|-------|"
264
+ fm.each do |key, value|
265
+ puts "| #{key} | #{value} |"
266
+ end
267
+ puts
268
+ end
269
+
270
+ exit 0
271
+ end
272
+
233
273
  def list_tools_terminal(local_tools, mcp_tool_groups)
234
274
  width = (ENV['COLUMNS'] || 80).to_i - 4
235
275
  indent = ' '
@@ -518,6 +558,7 @@ module AIA
518
558
  File.write(file, content)
519
559
  puts "Config successfully dumped to #{file}"
520
560
  end
561
+
521
562
  end
522
563
  end
523
564
  end
data/lib/aia/config.rb CHANGED
@@ -48,7 +48,7 @@ module AIA
48
48
  # ==========================================================================
49
49
 
50
50
  # Nested section attributes (defined as hashes, converted to ConfigSection)
51
- attr_config :service, :llm, :prompts, :output, :audio, :image, :embedding,
51
+ attr_config :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
52
52
  :tools, :flags, :registry, :paths, :logger
53
53
 
54
54
  # Array/collection attributes
@@ -56,7 +56,7 @@ module AIA
56
56
 
57
57
  # Runtime attributes (not loaded from config files)
58
58
  attr_accessor :prompt_id, :stdin_content, :remaining_args, :dump_file,
59
- :completion, :mcp_list, :list_tools,
59
+ :completion, :mcp_list, :list_tools, :list_skills,
60
60
  :executable_prompt_content,
61
61
  :tool_names, :loaded_tools,
62
62
  :log_level_override, :log_file_override,
@@ -105,6 +105,8 @@ module AIA
105
105
  service: config_section_coercion(:service),
106
106
  llm: config_section_coercion(:llm),
107
107
  prompts: config_section_coercion(:prompts),
108
+ roles: config_section_coercion(:roles),
109
+ skills: config_section_coercion(:skills),
108
110
  output: config_section_coercion(:output),
109
111
  audio: config_section_coercion(:audio),
110
112
  image: config_section_coercion(:image),
@@ -169,6 +171,9 @@ module AIA
169
171
  prompts_dir: [:prompts, :dir],
170
172
  roles_prefix: [:prompts, :roles_prefix],
171
173
  role: [:prompts, :role],
174
+ skills_dir: [:skills, :dir],
175
+ skills_prefix: [:prompts, :skills_prefix],
176
+ skills: [:prompts, :skills],
172
177
  parameter_regex: [:prompts, :parameter_regex],
173
178
  system_prompt: [:prompts, :system_prompt],
174
179
  # output section
@@ -235,6 +240,8 @@ module AIA
235
240
  llm: llm.to_h,
236
241
  models: models.map(&:to_h),
237
242
  prompts: prompts.to_h,
243
+ roles: roles.to_h,
244
+ skills: skills.to_h,
238
245
  output: output.to_h,
239
246
  audio: audio.to_h,
240
247
  image: image.to_h,
@@ -296,7 +303,7 @@ module AIA
296
303
  send("#{key}=", Array(value)) if respond_to?("#{key}=")
297
304
  when :mcp_servers
298
305
  self.mcp_servers = Array(value)
299
- when :service, :llm, :prompts, :output, :audio, :image, :embedding,
306
+ when :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
300
307
  :tools, :flags, :registry, :paths, :logger
301
308
  section = send(key)
302
309
  if section.is_a?(MywayConfig::ConfigSection) && value.is_a?(Hash)
@@ -388,6 +395,18 @@ module AIA
388
395
  if output.history_file
389
396
  output.history_file = File.expand_path(output.history_file)
390
397
  end
398
+
399
+ if roles.dir
400
+ roles.dir = File.expand_path(roles.dir)
401
+ end
402
+
403
+ if skills.dir
404
+ skills.dir = File.expand_path(skills.dir)
405
+ end
406
+
407
+ if tools.dir
408
+ tools.dir = File.expand_path(tools.dir)
409
+ end
391
410
  end
392
411
 
393
412
  def ensure_arrays
@@ -401,6 +420,9 @@ module AIA
401
420
 
402
421
  # Ensure tools.paths is an array
403
422
  tools.paths = [] if tools.paths.nil?
423
+
424
+ # Ensure prompts.skills is an array
425
+ prompts.skills = [] if prompts.respond_to?(:skills) && prompts.skills.nil?
404
426
  end
405
427
 
406
428
  # Process MCP JSON files and merge servers into mcp_servers
@@ -441,6 +463,10 @@ module AIA
441
463
  registry.send("#{key}=", value) if registry.respond_to?("#{key}=")
442
464
  when :paths
443
465
  paths.send("#{key}=", value) if paths.respond_to?("#{key}=")
466
+ when :roles
467
+ roles.send("#{key}=", value) if roles.respond_to?("#{key}=")
468
+ when :skills
469
+ skills.send("#{key}=", value) if skills.respond_to?("#{key}=")
444
470
  end
445
471
  end
446
472
  end
data/lib/aia/directive.rb CHANGED
@@ -84,5 +84,34 @@ module AIA
84
84
  ""
85
85
  end
86
86
  end
87
+
88
+ private
89
+
90
+ # Split args into positive and negative search terms.
91
+ # Tokens prefixed with -, ~, or ! are negative (exclusion) terms.
92
+ # All other tokens (bare or +-prefixed) are positive (inclusion) terms.
93
+ # All terms are downcased.
94
+ #
95
+ # @param args [Array<String>] raw argument tokens
96
+ # @return [Array<Array<String>>] [positive_terms, negative_terms]
97
+ def parse_search_terms(args)
98
+ positive = []
99
+ negative = []
100
+
101
+ Array(args).each do |arg|
102
+ arg.split.each do |token|
103
+ downcased = token.downcase
104
+ if downcased =~ /\A[-~!]/
105
+ negative << downcased[1..]
106
+ elsif downcased.start_with?('+')
107
+ positive << downcased[1..]
108
+ else
109
+ positive << downcased
110
+ end
111
+ end
112
+ end
113
+
114
+ [positive, negative]
115
+ end
87
116
  end
88
117
  end