yatfa 1.0.87 → 1.0.89

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.
package/agents.rb CHANGED
@@ -1,243 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Common sections shared across all agents
4
- UNIVERSAL_CONSTRAINTS = <<~CONSTRAINTS
5
- ## UNIVERSAL OPERATIONAL CONSTRAINTS
6
- 1. **TOOL FORMATTING:** Do not use a colon before tool calls (e.g., write "Let me search." not "Let me search:").
7
- 2. **URL SAFETY:** NEVER guess or hallucinate URLs. Use only known valid URLs.
8
- 3. **REDIRECTS:** If `WebFetch` returns a redirect, follow it immediately.
9
- 4. **CODE REFS:** Use the format `file_path:line_number` (e.g., `app/models/user.rb:42`).
10
- CONSTRAINTS
11
-
12
- TASK_TRACKING = <<~TRACKING
13
- ## TASK TRACKING
14
- * **TodoWrite:** Use for TEMPORAL, SESSION-SPECIFIC thinking (your scratchpad). Always update `TodoWrite` to reflect your current immediate step.
15
- * **Ticket Tools:** Use for PERSISTENT project work (database storage).
16
- TRACKING
17
-
18
- # Planner sections
19
- PLANNER_HEADER = <<~HEADER
20
- **IDENTITY & ROLE**
21
- You are the YATFA PLANNER. You act as the Architect.
22
- Your goal is **Requirements Analysis and Work Definition**.
23
- You operate in an **Interactive Chat** mode with the human user.
24
- HEADER
25
-
26
- PLANNER_BOUNDARIES = <<~BOUNDARIES
27
- ## ROLE BOUNDARIES
28
- **ABSOLUTELY FORBIDDEN:**
29
- * Writing implementation code.
30
- * Making git commits.
31
- * Creating tickets without human confirmation (propose ticket in chat, then create after approval).
32
- * Making product, design, or UX decisions on behalf of the user.
33
-
34
- ## DECISION AUTHORITY
35
- You control **structure** (how work is organized, sequenced, and described).
36
- The user controls **substance** (what the product should be and how it should behave).
37
-
38
- **Rules:**
39
- * **Requirements** are things the user explicitly asked for. State them as-is in acceptance criteria.
40
- * **Assumptions** are things you inferred or chose yourself (e.g., "dark mode is the right default", "sidebar should be on the left"). Flag every assumption as an open question — never bake it into a ticket.
41
- * **Subjective preferences** — theme, layout, defaults, naming, behavior, or any other product choice — must never be embedded in a ticket without explicit user confirmation. These are examples, not an exhaustive list. If it shapes what the product *is* rather than how the work is *organized*, it belongs to the user.
42
-
43
- **When creating/updating tickets:**
44
- ALWAYS use the `ticket-management` skill first - it covers ticket writing best practices.
45
- BOUNDARIES
46
-
47
- # Worker sections
48
- WORKER_HEADER = <<~HEADER
49
- You are the **YATFA WORKER** operating in **FULLY AUTONOMOUS MODE**.
50
-
51
- **PRIMARY ROLE:** Code Implementation and Testing.
52
- **HARD CONSTRAINT:** You must NOT create new tasks or reorganize work.
53
-
54
- ### OPERATIONAL MODE (EVENT-DRIVEN)
55
- * **Execution Flow:** Receive work via message -> Complete task -> Submit PR -> STOP.
56
- * **No Polling:** Do not loop, check for new work, or wait for responses.
57
- * **Status Management:** Mark yourself as 'busy' when starting and 'idle' when submitting the PR.
58
-
59
- ### ENVIRONMENT
60
- * Sandboxed Docker container with ROOT privileges.
61
- * System dependencies may be installed freely.
62
- * Git configured (GH_TOKEN active, repo synced on main).
63
- HEADER
64
-
65
- WORKER_BOUNDARIES = <<~BOUNDARIES
66
- ### STRICT BOUNDARIES (FORBIDDEN)
67
- * Creating new tickets, tasks, or breaking down epics (Planner role).
68
- * Reorganizing or reprioritizing the backlog.
69
- * Making architectural decisions without approval.
70
- * Reviewing other workers' code or approving your own work.
71
- * Strategic planning or project coordination.
72
- * Committing directly to `main` branch.
73
- * Merging your own pull requests.
74
- * Splitting one task into multiple PRs (unless explicitly instructed).
75
- BOUNDARIES
76
-
77
- WORKER_ESCALATION = <<~ESCALATION
78
- ### ESCALATION PROTOCOL
79
- If blocked (e.g., missing tools, auth errors, ambiguous requirements):
80
- 1. Use `create_ticket()`.
81
- 2. **Title:** "Escalation: [brief description]"
82
- 3. **Priority:** High or Critical.
83
- 4. **Context:** Include action attempted, error details, related Ticket ID, and suggested fix.
84
- ESCALATION
85
-
86
- # Reviewer sections
87
- REVIEWER_HEADER = <<~HEADER
88
- You are the **YATFA REVIEWER** agent operating in **FULLY AUTONOMOUS MODE**.
89
-
90
- **ROLE:** Code Review and Quality Assurance.
91
- **PRIMARY CONSTRAINT:** You must **NOT** implement solutions. You **ONLY** review them.
92
-
93
- ### EXECUTION MODEL
94
- * **Event-Driven:** Do not wait, poll, or loop. Do not add "waiting" to TODOs.
95
- * **One-Shot:** Receive message -> Review PR -> Pass/Fail Audit -> STOP.
96
- * **Environment:** Sandboxed Docker container (Root privileges). System dependencies allowed. GH_TOKEN is configured.
97
- HEADER
98
-
99
- REVIEWER_BOUNDARIES = <<~BOUNDARIES
100
- ### FORBIDDEN ACTIONS (STRICT)
101
- * Do NOT implement new features or functionality.
102
- * Do NOT write production code to "fix" issues.
103
- * Do NOT modify reviewed code directly, make git commits, or create PRs.
104
- * Do NOT create or edit migrations.
105
- * Do NOT make architectural decisions (document them via comments/proposals instead).
106
- * Do NOT use the "approve" transition (reserved for humans/PO).
107
- * Do NOT use `gh pr review --approve` (GitHub forbids self-approval).
108
- * Do NOT approve any PR without running tests first.
109
- BOUNDARIES
110
-
111
- REVIEWER_ESCALATION = <<~ESCALATION
112
- ### ESCALATION PROTOCOL
113
- If blocked or workflow is broken:
114
- 1. Call `create_ticket()`.
115
- 2. Title: "Escalation: [brief description]"
116
- 3. Priority: High or Critical.
117
- 4. Context: What you tried, the error/blocker, and Ticket ID.
118
- ESCALATION
119
-
120
- # Researcher sections
121
- RESEARCHER_HEADER = <<~HEADER
122
- You are the YATFA RESEARCHER agent operating in FULLY AUTONOMOUS MODE.
123
- Your role is AUTONOMOUS ANALYSIS and PROPOSAL GENERATION.
124
-
125
- ### CORE CONSTRAINT
126
- You have READ-ONLY access to code, tickets, and memories.
127
- **YOU MUST NOT MODIFY CODE, FILES, OR TICKETS DIRECTLY.**
128
- You must use `create_proposal` to suggest actions.
129
-
130
- ### SESSION ENVIRONMENT
131
- - **Execution Model:** Event-driven. Do not wait, do not poll. When you receive a message: Analyze -> Create Proposals -> STOP.
132
- - **Access:** Sandboxed Docker container (Root privileges). Read access to all systems.
133
- - **Skills:** researcher-tactical, researcher-strategic, knowledge-management, memory.
134
- HEADER
135
-
136
- RESEARCHER_BOUNDARIES = <<~BOUNDARIES
137
- ### ROLE BOUNDARIES
138
-
139
- **ABSOLUTELY FORBIDDEN:**
140
- - Modifying code, tickets, or files directly.
141
- - Writing, editing, or refactoring code.
142
- - Making git commits or pull requests.
143
- - Sending messages to other agents.
144
- - Changing your own busy/idle status.
145
- BOUNDARIES
146
-
147
- RESEARCHER_ESCALATION = <<~ESCALATION
148
- ### ESCALATION PROCESS
149
- If you encounter blocking problems or need to improve workflow:
150
- 1. Use `create_proposal`.
151
- 2. **Title:** "Escalation: [brief description]"
152
- 3. **Type:** task
153
- 4. **Priority:** high or critical
154
- 5. **Context:** Include what you tried, the error/blocker, and suggested fix (e.g., "Escalation: Need additional MCP tools for research").
155
- ESCALATION
156
-
157
- CLAW_HEADER = <<~HEADER
158
- You are the **YATFA CLAW** agent.
159
-
160
- **ROLE:** OpenClaw Gateway Runner.
161
- Your purpose is to run an OpenClaw gateway inside this container, providing a Telegram-based chat interface with the Pi agent.
162
-
163
- ### OPERATIONAL MODE
164
- * **Execution:** The OpenClaw gateway starts automatically and runs persistently.
165
- * **Telegram Integration:** Chat messages from Telegram are forwarded to the Pi agent via the gateway.
166
- * **Configuration:** Set via environment variables or `~/.openclaw/openclaw.json`.
167
- * **Persistence:** Gateway data is stored in a Docker volume at `~/.openclaw/`.
168
-
169
- ### ENVIRONMENT
170
- * Sandboxed Docker container with ROOT privileges.
171
- * Network mode: host (port 18789 exposed for gateway).
172
- * OpenClaw installed globally via npm.
173
- HEADER
174
-
175
- CLAW_BOUNDARIES = <<~BOUNDARIES
176
- ### ROLE BOUNDARIES
177
- * This agent runs the OpenClaw gateway - it does NOT run Claude Code.
178
- * No Yatfa MCP tools are available (that is a future enhancement).
179
- * No git repositories are mounted.
180
- * No skills are loaded.
181
- BOUNDARIES
182
-
183
- # Dynamic banner builder
184
- def build_banner(header:, boundaries:, escalation: nil, extra_sections: [])
185
- [
186
- header,
187
- UNIVERSAL_CONSTRAINTS,
188
- TASK_TRACKING,
189
- *extra_sections,
190
- boundaries,
191
- escalation
192
- ].compact.join("\n\n")
193
- end
3
+ # Agent type definitions for the YATFA system.
4
+ #
5
+ # System prompts (banners) are defined as .md files in yatfa/prompts/<agent-type>/base.md
6
+ # and loaded into the database by AgentPromptLoader. At container startup,
7
+ # setup_system_prompt! fetches the prompt from the API instead of evaluating this file.
8
+ #
9
+ # This file defines agent metadata (name, skills, prompt reference) used by:
10
+ # - bin/run-yatfa-agent.rb (CLI) — to build Docker containers with correct skills
11
+ # - spec/system_prompt_spec.rb — to verify agent config structure
194
12
 
195
13
  AGENT_CONFIGS = {
196
14
  'planner' => {
197
15
  name: 'yatfa-planner',
198
16
  skills: ['ticket-management', 'memory', 'knowledge-management', 'sentry-investigation', 'agent-browser'],
199
- banner: build_banner(
200
- header: PLANNER_HEADER,
201
- boundaries: PLANNER_BOUNDARIES
202
- )
17
+ prompt_name: 'planner'
203
18
  },
204
19
 
205
20
  'worker' => {
206
21
  name: 'yatfa-autonomous-worker',
207
22
  skills: ['git-workflow', 'worker-workflow', 'memory', 'session-learnings', 'screenshot', 'agent-browser'],
208
- banner: build_banner(
209
- header: WORKER_HEADER,
210
- boundaries: WORKER_BOUNDARIES,
211
- escalation: WORKER_ESCALATION
212
- )
23
+ prompt_name: 'worker'
213
24
  },
214
25
 
215
26
  'reviewer' => {
216
27
  name: 'yatfa-autonomous-reviewer',
217
28
  skills: ['reviewer-workflow', 'qa-workflow', 'memory', 'session-learnings', 'proposal-reviewer', 'screenshot', 'agent-browser'],
218
- banner: build_banner(
219
- header: REVIEWER_HEADER,
220
- boundaries: REVIEWER_BOUNDARIES,
221
- escalation: REVIEWER_ESCALATION
222
- )
29
+ prompt_name: 'reviewer'
223
30
  },
224
31
 
225
32
  'researcher' => {
226
33
  name: 'yatfa-autonomous-researcher',
227
34
  skills: ['researcher-tactical', 'researcher-strategic', 'researcher-digest', 'researcher-telegram-processor', 'researcher-meta-learning', 'researcher-approval-review', 'researcher-task-proposal-review', 'memory', 'session-learnings', 'proposal-execution', 'proposal-rework-responder', 'memory-consolidation', 'retrospective', 'knowledge-management', 'sentry-investigation', 'agent-browser'],
228
- banner: build_banner(
229
- header: RESEARCHER_HEADER,
230
- boundaries: RESEARCHER_BOUNDARIES,
231
- escalation: RESEARCHER_ESCALATION
232
- )
35
+ prompt_name: 'researcher'
233
36
  },
234
37
 
235
38
  'claw' => {
236
39
  name: 'yatfa-openclaw',
237
40
  skills: [],
238
- banner: build_banner(
239
- header: CLAW_HEADER,
240
- boundaries: CLAW_BOUNDARIES
241
- )
41
+ prompt_name: 'claw'
242
42
  }
243
43
  }.freeze
@@ -357,7 +357,6 @@ module YatfaAgent
357
357
  docker_cmd += ["-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"]
358
358
  end
359
359
 
360
- add_development_mounts!(docker_cmd)
361
360
  else
362
361
  # Standard agent setup (worker, planner, reviewer, researcher)
363
362
  docker_cmd += [
@@ -371,16 +370,9 @@ module YatfaAgent
371
370
  add_github_auth!(docker_cmd, config)
372
371
  add_git_config!(docker_cmd, config)
373
372
  add_repository_mounts!(docker_cmd, config)
374
- add_development_mounts!(docker_cmd)
375
373
  end
376
374
 
377
- local_setup_dir = File.join(File.dirname(__FILE__), "..", "..", "setup")
378
-
379
- if File.directory?(local_setup_dir)
380
- docker_cmd += [Config.image_name(config), "ruby", "/tmp/setup/setup-agent.rb"]
381
- else
382
- docker_cmd += [Config.image_name(config)]
383
- end
375
+ docker_cmd += [Config.image_name(config)]
384
376
 
385
377
  docker_cmd
386
378
  end
@@ -442,16 +434,6 @@ module YatfaAgent
442
434
  end
443
435
  end
444
436
 
445
- def self.add_development_mounts!(docker_cmd)
446
- # Check for local setup/ directory (for development)
447
- setup_dir = File.join(File.dirname(__FILE__), "..", "..", "setup")
448
-
449
- if File.directory?(setup_dir)
450
- puts "🔧 Using local setup/ for development"
451
- docker_cmd.concat(["-v", "#{File.expand_path(setup_dir)}:/tmp/setup:ro"])
452
- end
453
- end
454
-
455
437
  def self.detect_agent_user(container_name)
456
438
  # Determine the user to attach as
457
439
  # Robust method: find the user running the agent process (tmux or bridge)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yatfa",
3
- "version": "1.0.87",
3
+ "version": "1.0.89",
4
4
  "description": "YATFA - Yet Another Tool For Agents",
5
5
  "bin": "./bin/run-yatfa-agent.rb",
6
6
  "files": [
@@ -315,19 +315,12 @@ def setup_claude_settings!
315
315
 
316
316
  # Configure model selection if specified
317
317
  # Each model tier can be configured independently
318
- if llm_credentials["model_haiku"] && !llm_credentials["model_haiku"].empty?
319
- settings_config["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = llm_credentials["model_haiku"]
320
- puts " Haiku model: #{llm_credentials['model_haiku']}"
321
- end
322
-
323
- if llm_credentials["model_sonnet"] && !llm_credentials["model_sonnet"].empty?
324
- settings_config["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"] = llm_credentials["model_sonnet"]
325
- puts " Sonnet model: #{llm_credentials['model_sonnet']}"
326
- end
327
-
328
- if llm_credentials["model_opus"] && !llm_credentials["model_opus"].empty?
329
- settings_config["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"] = llm_credentials["model_opus"]
330
- puts " Opus model: #{llm_credentials['model_opus']}"
318
+ %w[haiku sonnet opus].each do |tier|
319
+ key = "model_#{tier}"
320
+ if llm_credentials[key] && !llm_credentials[key].empty?
321
+ settings_config["env"]["ANTHROPIC_DEFAULT_#{tier.upcase}_MODEL"] = llm_credentials[key]
322
+ puts " #{tier.capitalize} model: #{llm_credentials[key]}"
323
+ end
331
324
  end
332
325
 
333
326
  # Configure context window if specified (value is in k-units, append 'k' suffix)
@@ -339,7 +332,7 @@ def setup_claude_settings!
339
332
  end
340
333
  end
341
334
 
342
- if !llm_credentials["model_haiku"] && !llm_credentials["model_sonnet"] && !llm_credentials["model_opus"]
335
+ unless %w[haiku sonnet opus].any? { |tier| llm_credentials["model_#{tier}"] }
343
336
  puts " Using default Anthropic models"
344
337
  end
345
338
  else
@@ -353,42 +346,22 @@ def setup_claude_settings!
353
346
 
354
347
  # Configure hooks
355
348
  settings_config["hooks"] ||= {}
356
-
357
-
358
- # Configure SessionStart hook for skill knowledge injection
359
- settings_config["hooks"]["SessionStart"] ||= []
360
- # Remove old hook if present
361
- settings_config["hooks"]["SessionStart"].reject! do |h|
362
- h["hooks"]&.first&.dig("command")&.include?("inject-skill-knowledge.rb")
363
- end
364
-
365
- # Install hook script to ~/.claude/hooks
366
349
  hooks_dir = File.join(settings_dir, "hooks")
367
350
  FileUtils.mkdir_p(hooks_dir)
368
-
369
- session_script_name = "inject-skill-knowledge.rb"
370
- dest_path = download_file(
371
- script_name: session_script_name,
372
- remote_url: "#{YATFA_RAW_URL}/hooks/#{session_script_name}",
373
- dest_dir: hooks_dir,
374
- label: "SessionStart hook"
375
- )
376
351
 
377
- if dest_path
378
- session_hook_to_add = {
379
- "hooks" => [{ "type" => "command", "command" => "ruby \"#{dest_path}\"" }]
380
- }
381
- settings_config["hooks"]["SessionStart"] << session_hook_to_add
382
- else
383
- puts "⚠️ Skipping SessionStart hook configuration (download failed)"
384
- end
352
+ # SessionStart hook: skill knowledge injection
353
+ install_hook!(
354
+ settings_config, hooks_dir,
355
+ hook_type: "SessionStart",
356
+ script_name: "inject-skill-knowledge.rb",
357
+ match_substring: "inject-skill-knowledge.rb",
358
+ command_prefix: "ruby"
359
+ )
385
360
 
386
361
  # Install sessionstart-input.sh for repository context injection
387
- # This provides repository information to Claude at session start
388
- input_script_name = "sessionstart-input.sh"
389
362
  download_file(
390
- script_name: input_script_name,
391
- remote_url: "#{YATFA_RAW_URL}/hooks/#{input_script_name}",
363
+ script_name: "sessionstart-input.sh",
364
+ remote_url: "#{YATFA_RAW_URL}/hooks/sessionstart-input.sh",
392
365
  dest_dir: hooks_dir,
393
366
  label: "SessionStart input hook"
394
367
  )
@@ -396,31 +369,15 @@ def setup_claude_settings!
396
369
  # Add agent-specific settings
397
370
  settings_config["skipDangerousModePermissionPrompt"] = true
398
371
 
399
- # Configure Stop hook for session-end learning capture
400
- settings_config["hooks"]["Stop"] ||= []
401
- # Remove old hook if present
402
- settings_config["hooks"]["Stop"].reject! do |h|
403
- h["hooks"]&.first&.dig("command")&.include?("sessionend-output.sh")
404
- end
405
-
406
- # Install session-end hook script
407
- sessionend_script_name = "sessionend-output.sh"
408
- sessionend_dest_path = download_file(
409
- script_name: sessionend_script_name,
410
- remote_url: "#{YATFA_RAW_URL}/hooks/#{sessionend_script_name}",
411
- dest_dir: hooks_dir,
412
- label: "Stop hook"
372
+ # Stop hook: session-end learning capture
373
+ install_hook!(
374
+ settings_config, hooks_dir,
375
+ hook_type: "Stop",
376
+ script_name: "sessionend-output.sh",
377
+ match_substring: "sessionend-output.sh",
378
+ command_prefix: "bash"
413
379
  )
414
380
 
415
- if sessionend_dest_path
416
- sessionend_hook_to_add = {
417
- "hooks" => [{ "type" => "command", "command" => "bash \"#{sessionend_dest_path}\"" }]
418
- }
419
- settings_config["hooks"]["Stop"] << sessionend_hook_to_add
420
- else
421
- puts "⚠️ Skipping Stop hook configuration (download failed)"
422
- end
423
-
424
381
  # Download and register project hooks wrapper
425
382
  # The wrapper fetches user-defined hooks from the API at runtime (with caching),
426
383
  # executes them, and reports results back for observability.
@@ -430,6 +387,39 @@ def setup_claude_settings!
430
387
  puts "✅ ~/.claude/settings.json configured with skill hooks"
431
388
  end
432
389
 
390
+ # Install a Claude settings hook by downloading the script and registering it.
391
+ #
392
+ # Handles the common pattern: ensure hook array → remove old entry → download → append.
393
+ #
394
+ # @param settings_config [Hash] the settings.json hash being built
395
+ # @param hooks_dir [String] directory to download hook scripts into
396
+ # @param hook_type [String] Claude hook type (e.g. "SessionStart", "Stop")
397
+ # @param script_name [String] filename of the hook script to download
398
+ # @param match_substring [String] substring to match when removing old hook entries
399
+ # @param command_prefix [String] command prefix ("ruby" or "bash")
400
+ def install_hook!(settings_config, hooks_dir, hook_type:, script_name:, match_substring:, command_prefix:)
401
+ settings_config["hooks"][hook_type] ||= []
402
+ # Remove old hook entry if present (idempotent re-runs)
403
+ settings_config["hooks"][hook_type].reject! do |h|
404
+ h["hooks"]&.first&.dig("command")&.include?(match_substring)
405
+ end
406
+
407
+ dest_path = download_file(
408
+ script_name: script_name,
409
+ remote_url: "#{YATFA_RAW_URL}/hooks/#{script_name}",
410
+ dest_dir: hooks_dir,
411
+ label: "#{hook_type} hook"
412
+ )
413
+
414
+ if dest_path
415
+ settings_config["hooks"][hook_type] << {
416
+ "hooks" => [{ "type" => "command", "command" => "#{command_prefix} \"#{dest_path}\"" }]
417
+ }
418
+ else
419
+ puts "⚠️ Skipping #{hook_type} hook configuration (download failed)"
420
+ end
421
+ end
422
+
433
423
  # Download and register the project hooks wrapper as a SessionStart hook.
434
424
  # The wrapper handles fetching user-defined hooks from the API at runtime,
435
425
  # executing them, and reporting results. Hook content is managed by users
@@ -456,39 +446,57 @@ def setup_system_prompt!
456
446
  agent_type = ENV["AGENT_TYPE"]
457
447
  system_prompt_path = File.expand_path("~/.yatfa/system-prompt.txt")
458
448
 
459
- # Download agents.rb from remote
460
- agents_rb_url = "#{YATFA_RAW_URL}/agents.rb"
461
- puts "📥 Downloading agents.rb from #{agents_rb_url}..."
462
- agents_rb_content = nil
463
- begin
464
- agents_rb_content = URI.open(agents_rb_url).read
465
- puts "✅ Downloaded agents.rb"
466
- rescue OpenURI::HTTPError => e
467
- puts "❌ Failed to download agents.rb: #{e.message}"
468
- puts " Cannot generate system prompt without agent definitions"
449
+ rails_api_url = ENV["YATFA_API_URL"]
450
+ rails_api_key = ENV["YATFA_API_KEY"]
451
+
452
+ unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
453
+ puts "❌ YATFA_API_URL or YATFA_API_KEY not set"
454
+ puts " Cannot fetch system prompt without API credentials"
469
455
  exit 1
470
456
  end
471
457
 
472
- # Evaluate agents.rb to get AGENT_CONFIGS
473
- agents_binding = binding
458
+ # Fetch system prompt from yatfa API
459
+ # Agent prompts are stored in yatfa/prompts/<agent-type>/base.md
460
+ # and loaded into the database by AgentPromptLoader
461
+ prompt_url = URI("#{rails_api_url}/internal/skill?tags%5B%5D=#{agent_type}&tags%5B%5D=base")
462
+ http = Net::HTTP.new(prompt_url.host, prompt_url.port)
463
+ http.use_ssl = prompt_url.scheme == "https"
464
+ http.open_timeout = 10
465
+ http.read_timeout = 15
466
+
467
+ request = Net::HTTP::Get.new(prompt_url)
468
+ request["X-API-Key"] = rails_api_key
469
+ request["Accept"] = "application/json"
470
+
471
+ banner_content = nil
474
472
  begin
475
- agents_binding.eval(agents_rb_content, "agents.rb")
476
- rescue => e
477
- puts "❌ Failed to evaluate agents.rb: #{e.message}"
478
- exit 1
479
- end
473
+ puts "📥 Fetching system prompt for #{agent_type}..."
474
+ response = http.request(request)
480
475
 
481
- agent_configs = agents_binding.eval("AGENT_CONFIGS")
476
+ if response.is_a?(Net::HTTPSuccess)
477
+ data = JSON.parse(response.body)
478
+ banner_content = data["content"]
479
+
480
+ unless banner_content && !banner_content.empty?
481
+ puts "❌ System prompt for '#{agent_type}' is empty"
482
+ exit 1
483
+ end
482
484
 
483
- unless agent_configs && agent_configs[agent_type]
484
- puts "❌ Unknown agent type: #{agent_type}"
485
- puts " Available: #{agent_configs.keys.join(', ')}"
485
+ puts "✅ Fetched system prompt for #{agent_type}"
486
+ else
487
+ puts "❌ Failed to fetch system prompt: #{response.code} #{response.message}"
488
+ puts " Response: #{response.body}"
489
+ puts " Make sure AgentPromptLoader has loaded prompts/ directory"
490
+ exit 1
491
+ end
492
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
493
+ puts "❌ Timeout fetching system prompt: #{e.message}"
494
+ exit 1
495
+ rescue => e
496
+ puts "❌ Error fetching system prompt: #{e.message}"
486
497
  exit 1
487
498
  end
488
499
 
489
- agent_def = agent_configs[agent_type]
490
- banner_content = agent_def[:banner].dup
491
-
492
500
  # Append repository context from REPOSITORIES_CONTEXT env var
493
501
  if ENV["REPOSITORIES_CONTEXT"] && !ENV["REPOSITORIES_CONTEXT"].empty?
494
502
  repos = ENV["REPOSITORIES_CONTEXT"].split(',').map do |repo|
@@ -506,7 +514,7 @@ def setup_system_prompt!
506
514
  # Ensure directory exists and write system prompt
507
515
  FileUtils.mkdir_p(File.dirname(system_prompt_path))
508
516
  File.write(system_prompt_path, banner_content)
509
- puts "✅ System prompt generated at #{system_prompt_path} (agent: #{agent_type})"
517
+ puts "✅ System prompt written to #{system_prompt_path} (agent: #{agent_type})"
510
518
 
511
519
  # Add repository context to CLAUDE.md for easy reference
512
520
  claude_md = File.expand_path("~/CLAUDE.md")
@@ -47,7 +47,7 @@ def setup_github_auth!
47
47
  end
48
48
 
49
49
  # Build GitHub token endpoint URL
50
- token_url = "#{rails_api_url.chomp('/')}/github/token"
50
+ token_url = "\#{rails_api_url.chomp('/')}/github/token"
51
51
 
52
52
  uri = URI(token_url)
53
53
  http = Net::HTTP.new(uri.host, uri.port)
@@ -62,7 +62,7 @@ def setup_github_auth!
62
62
  response = http.request(request)
63
63
 
64
64
  unless response.is_a?(Net::HTTPSuccess)
65
- STDERR.puts "❌ Error: Backend API returned #{response.code} for GitHub token"
65
+ STDERR.puts "❌ Error: Backend API returned \#{response.code} for GitHub token"
66
66
  exit 1
67
67
  end
68
68