yatfa 1.0.67 → 1.0.68

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/README.md CHANGED
@@ -102,6 +102,16 @@ npx yatfa setup
102
102
 
103
103
  Launches interactive setup wizard to configure `yatfa.env.rb`.
104
104
 
105
+ ### Check Version
106
+
107
+ ```bash
108
+ npx yatfa version
109
+ npx yatfa -v
110
+ npx yatfa --version
111
+ ```
112
+
113
+ Prints the installed yatfa package version (e.g., `yatfa v1.0.67`).
114
+
105
115
  ### Run Agents
106
116
 
107
117
  When you run an agent, the CLI automatically:
package/agents.rb CHANGED
@@ -144,6 +144,32 @@ If you encounter blocking problems or need to improve workflow:
144
144
  5. **Context:** Include what you tried, the error/blocker, and suggested fix (e.g., "Escalation: Need additional MCP tools for research").
145
145
  ESCALATION
146
146
 
147
+ CLAW_HEADER = <<~HEADER
148
+ You are the **YATFA CLAW** agent.
149
+
150
+ **ROLE:** OpenClaw Gateway Runner.
151
+ Your purpose is to run an OpenClaw gateway inside this container, providing a Telegram-based chat interface with the Pi agent.
152
+
153
+ ### OPERATIONAL MODE
154
+ * **Execution:** The OpenClaw gateway starts automatically and runs persistently.
155
+ * **Telegram Integration:** Chat messages from Telegram are forwarded to the Pi agent via the gateway.
156
+ * **Configuration:** Set via environment variables or `~/.openclaw/openclaw.json`.
157
+ * **Persistence:** Gateway data is stored in a Docker volume at `~/.openclaw/`.
158
+
159
+ ### ENVIRONMENT
160
+ * Sandboxed Docker container with ROOT privileges.
161
+ * Network mode: host (port 18789 exposed for gateway).
162
+ * OpenClaw installed globally via npm.
163
+ HEADER
164
+
165
+ CLAW_BOUNDARIES = <<~BOUNDARIES
166
+ ### ROLE BOUNDARIES
167
+ * This agent runs the OpenClaw gateway - it does NOT run Claude Code.
168
+ * No Yatfa MCP tools are available (that is a future enhancement).
169
+ * No git repositories are mounted.
170
+ * No skills are loaded.
171
+ BOUNDARIES
172
+
147
173
  # Dynamic banner builder
148
174
  def build_banner(header:, boundaries:, escalation: nil, extra_sections: [])
149
175
  [
@@ -178,7 +204,7 @@ AGENT_CONFIGS = {
178
204
 
179
205
  'reviewer' => {
180
206
  name: 'yatfa-autonomous-reviewer',
181
- skills: ['reviewer-workflow', 'memory', 'proposal-reviewer'],
207
+ skills: ['reviewer-workflow', 'qa-workflow', 'memory', 'proposal-reviewer'],
182
208
  banner: build_banner(
183
209
  header: REVIEWER_HEADER,
184
210
  boundaries: REVIEWER_BOUNDARIES,
@@ -194,5 +220,14 @@ AGENT_CONFIGS = {
194
220
  boundaries: RESEARCHER_BOUNDARIES,
195
221
  escalation: RESEARCHER_ESCALATION
196
222
  )
223
+ },
224
+
225
+ 'claw' => {
226
+ name: 'yatfa-openclaw',
227
+ skills: [],
228
+ banner: build_banner(
229
+ header: CLAW_HEADER,
230
+ boundaries: CLAW_BOUNDARIES
231
+ )
197
232
  }
198
233
  }.freeze
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # YATFA Agent Runner (Yet Another Tool For Agents)
5
- # Usage: npx yatfa [worker|planner|reviewer|researcher]
5
+ # Usage: npx yatfa [worker|planner|reviewer|researcher|claw]
6
6
  # npx yatfa setup
7
7
  #
8
8
  # Requirements:
@@ -17,12 +17,22 @@ require_relative "../lib/yatfa_agent/agent"
17
17
  require_relative "../lib/yatfa_agent/setup"
18
18
  require_relative "../agents"
19
19
 
20
+ def show_version
21
+ require "json"
22
+ real_path = File.realpath(__FILE__)
23
+ package_json_path = File.join(File.dirname(real_path), "..", "package.json")
24
+ version = JSON.parse(File.read(package_json_path))["version"]
25
+ puts "yatfa v#{version}"
26
+ exit 0
27
+ end
28
+
20
29
  def show_usage
21
30
  puts "YATFA Agent Runner (Yet Another Tool For Agents)"
22
31
  puts ""
23
32
  puts "Usage:"
33
+ puts " npx yatfa version # Show version"
24
34
  puts " npx yatfa setup # Interactive setup wizard"
25
- puts " npx yatfa [worker|planner|reviewer|researcher] # Run agent (prompts to attach)"
35
+ puts " npx yatfa [worker|planner|reviewer|researcher|claw] # Run agent (prompts to attach)"
26
36
  puts ""
27
37
  puts "Options:"
28
38
  puts " --no-prompt, -d Run without prompts (for CI/scripts)"
@@ -44,6 +54,13 @@ show_usage if ARGV.empty?
44
54
 
45
55
  command = ARGV[0].downcase
46
56
 
57
+ # Set process title so ps/top shows the agent type instead of "node" (from npx)
58
+ agent_type_for_title = %w[worker planner reviewer researcher claw].include?(command) ? command : "yatfa"
59
+ Process.setproctitle("yatfa-#{agent_type_for_title}")
60
+
61
+ # Handle version command
62
+ show_version if %w[version -v --version].include?(command)
63
+
47
64
  # Handle setup command
48
65
  if command == "setup"
49
66
  wizard = YatfaAgent::Setup::SetupWizard.new
@@ -51,6 +68,15 @@ if command == "setup"
51
68
  exit 0
52
69
  end
53
70
 
71
+ # Resolve project root by walking up the directory tree.
72
+ # This allows running `npx yatfa` from subfolders inside the project.
73
+ # Skipped for setup (which creates new config in cwd).
74
+ project_root = YatfaAgent::Config.find_project_root
75
+ if project_root && project_root != Dir.pwd
76
+ puts "📂 Running from subfolder, using config from #{project_root}"
77
+ Dir.chdir(project_root)
78
+ end
79
+
54
80
  # Handle attach command (deprecated - will still work)
55
81
  if command == "attach"
56
82
  puts ""
@@ -63,7 +89,7 @@ if command == "attach"
63
89
  YatfaAgent::Config.fetch_repositories!(config)
64
90
  YatfaAgent::Agent.attach(agent_type, config, AGENT_CONFIGS)
65
91
  else
66
- # Handle run commands (worker, planner, reviewer, researcher)
92
+ # Handle run commands (worker, planner, reviewer, researcher, claw)
67
93
  config = YatfaAgent::Config.load
68
94
  # Fetch repositories from Rails API before building
69
95
  YatfaAgent::Config.fetch_repositories!(config)
package/bin/yatfa ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # yatfa - Agent runner wrapper
3
+ # Sets process name to yatfa-<agent-type> so ps/top/Activity Monitor shows it clearly.
4
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec -a "yatfa-${1:-yatfa}" ruby "$DIR/run-yatfa-agent.rb" "$@"
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
- require 'fileutils'
3
2
 
4
3
  module YatfaAgent
5
4
  module Agent
6
5
  # Check if container is currently running
7
6
  def self.container_running?(container_name)
8
- # Use IO.popen with array arguments to avoid shell injection
9
7
  result = IO.popen(["docker", "ps", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
10
8
  !result.empty?
11
9
  end
12
10
 
11
+ # Check if container exists (running or stopped)
12
+ def self.container_exists?(container_name)
13
+ result = IO.popen(["docker", "ps", "-a", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
14
+ !result.empty?
15
+ end
16
+
13
17
  # Prompt user for input with a default value
14
18
  def self.prompt(message, default: nil, options: nil)
15
19
  loop do
@@ -69,6 +73,41 @@ module YatfaAgent
69
73
  end
70
74
  end
71
75
 
76
+ # Banner file is no longer generated on the host.
77
+ # System prompt is generated inside the container by setup-agent.rb
78
+ # using AGENT_TYPE and REPOSITORIES_CONTEXT env vars.
79
+
80
+ # Handle stopped container: prompt user for action
81
+ def self.handle_stopped_container(agent_type, container_name, config, agent_configs)
82
+ puts "⚠️ #{agent_type} agent container exists but is stopped (#{container_name})"
83
+ puts ""
84
+
85
+ if non_interactive?
86
+ puts " Non-interactive mode: starting existing container."
87
+ system("docker", "start", container_name, out: File::NULL)
88
+ puts "✅ Agent started in background"
89
+ exit 0
90
+ end
91
+
92
+ choice = prompt(" [S]tart existing / [R]ecreate / [C]ancel? [S/r/c]: ", default: "S", options: %w[S R C])
93
+
94
+ case choice
95
+ when "S"
96
+ puts ""
97
+ puts "🔄 Starting existing container..."
98
+ system("docker", "start", container_name, out: File::NULL)
99
+ puts "✅ Agent started in background"
100
+ exit 0
101
+ when "R"
102
+ puts ""
103
+ puts "🔄 Removing stopped container..."
104
+ system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
105
+ when "C"
106
+ puts " Cancelled."
107
+ exit 0
108
+ end
109
+ end
110
+
72
111
  def self.run(agent_type, config, agent_configs)
73
112
  unless agent_configs.keys.include?(agent_type)
74
113
  puts "❌ Unknown agent type: #{agent_type}"
@@ -86,32 +125,13 @@ module YatfaAgent
86
125
  # If user chose Attach or Cancel, the method calls exec/exit, so we never reach here
87
126
  # If user chose Restart, should_continue is true and we proceed to start a new container
88
127
  return unless should_continue
128
+ elsif container_exists?(container_name)
129
+ handle_stopped_container(agent_type, container_name, config, agent_configs)
89
130
  end
90
131
 
91
132
  puts "🚀 Starting #{agent_type} agent..."
92
133
 
93
- # Write banner to a persistent temp file (not auto-deleted)
94
- banner_path = "/tmp/yatfa-banner-#{agent_type}.txt"
95
- FileUtils.rm_rf(banner_path)
96
-
97
- # Append repository context to banner
98
- banner_content = agent_def[:banner].dup
99
- if config["repositories"]&.any?
100
- sorted_repos = config["repositories"].sort_by { |r| r["position"] || 0 }
101
- repo_info = sorted_repos.map do |repo|
102
- name = repo["name"]
103
- path = repo["path_in_sandbox"] || "/workspace/#{name}"
104
- " - **#{name}**: #{path}"
105
- end.join("\n")
106
-
107
- banner_content += "\n\n## REPOSITORY STRUCTURE\n\n"
108
- banner_content += "This project has #{sorted_repos.count} repositories:\n\n"
109
- banner_content += "#{repo_info}\n\n"
110
- end
111
-
112
- File.write(banner_path, banner_content)
113
-
114
- docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
134
+ docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def)
115
135
 
116
136
  success = system(*docker_cmd)
117
137
 
@@ -197,7 +217,7 @@ module YatfaAgent
197
217
 
198
218
  private
199
219
 
200
- def self.build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
220
+ def self.build_docker_command(container_name, agent_type, config, agent_config, agent_def)
201
221
  docker_cmd = [
202
222
  "docker", "run", "-d",
203
223
  "--name", container_name,
@@ -212,7 +232,7 @@ module YatfaAgent
212
232
  merged_env = {}
213
233
  merged_env.merge!(config["env"]) if config["env"]
214
234
  merged_env.merge!(agent_config["env"]) if agent_config["env"]
215
-
235
+
216
236
  if merged_env.any?
217
237
  merged_env.each do |k, v|
218
238
  docker_cmd += ["-e", "#{k}=#{v}"]
@@ -226,22 +246,51 @@ module YatfaAgent
226
246
  end
227
247
  end
228
248
 
229
- docker_cmd += [
230
- "-v", "#{banner_path}:/etc/yatfa/system-prompt.txt:ro",
231
- "-e", "YATFA_VERSION=main",
232
- "-e", "SKILLS=#{agent_def[:skills]&.join(',')}",
233
- "-e", "AGENT_TYPE=#{agent_type}",
234
- "-e", "YATFA_URL=#{YatfaAgent::Config.yatfa_url(config)}",
235
- "-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"
236
- ]
249
+ if agent_type == "claw"
250
+ # Claw-specific Docker command setup
251
+ docker_cmd += [
252
+ "-e", "AGENT_TYPE=#{agent_type}",
253
+ "-e", "YATFA_VERSION=main"
254
+ ]
237
255
 
238
- add_github_auth!(docker_cmd, config)
239
- add_git_config!(docker_cmd, config)
240
- add_repository_mounts!(docker_cmd, config)
241
- add_development_mounts!(docker_cmd)
256
+ # Add persistence volume for OpenClaw config and data (scoped per container)
257
+ docker_cmd += ["-v", "#{container_name}-openclaw-data:/home/claude/.openclaw"]
258
+
259
+ # Auto-inject telegram_bot_token as env var so users don't need to duplicate in env block
260
+ if agent_config["telegram_bot_token"]
261
+ docker_cmd += ["-e", "OPENCLAW_TELEGRAM_BOT_TOKEN=#{agent_config['telegram_bot_token']}"]
262
+ end
263
+
264
+ # Pass YATFA_URL if configured (optional for claw)
265
+ if config["yatfa_url"]
266
+ docker_cmd += ["-e", "YATFA_URL=#{config['yatfa_url']}"]
267
+ docker_cmd += ["-e", "YATFA_API_URL=#{config['yatfa_url']}/api/v1"]
268
+ end
269
+
270
+ # Pass MCP API key if configured (for future MCP integration)
271
+ if agent_config["mcp_api_key"]
272
+ docker_cmd += ["-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"]
273
+ end
274
+
275
+ add_development_mounts!(docker_cmd)
276
+ else
277
+ # Standard agent setup (worker, planner, reviewer, researcher)
278
+ docker_cmd += [
279
+ "-e", "YATFA_VERSION=main",
280
+ "-e", "SKILLS=#{agent_def[:skills]&.join(',')}",
281
+ "-e", "AGENT_TYPE=#{agent_type}",
282
+ "-e", "YATFA_URL=#{YatfaAgent::Config.yatfa_url(config)}",
283
+ "-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"
284
+ ]
285
+
286
+ add_github_auth!(docker_cmd, config)
287
+ add_git_config!(docker_cmd, config)
288
+ add_repository_mounts!(docker_cmd, config)
289
+ add_development_mounts!(docker_cmd)
290
+ end
242
291
 
243
292
  local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
244
-
293
+
245
294
  if File.exist?(local_setup_script)
246
295
  docker_cmd += [Config.image_name(config), "ruby", "/tmp/setup-agent.rb"]
247
296
  else
@@ -4,6 +4,22 @@ require "json"
4
4
 
5
5
  module YatfaAgent
6
6
  module Config
7
+ # Walk up the directory tree from +start_dir+ (default: Dir.pwd)
8
+ # looking for yatfa.env.rb. Returns the directory containing the
9
+ # config file, or nil if not found.
10
+ def self.find_project_root(start_dir: Dir.pwd)
11
+ dir = File.expand_path(start_dir)
12
+ previous = nil
13
+
14
+ while dir != previous
15
+ return dir if File.exist?(File.join(dir, "yatfa.env.rb"))
16
+ previous = dir
17
+ dir = File.dirname(dir)
18
+ end
19
+
20
+ nil
21
+ end
22
+
7
23
  def self.load
8
24
  config_file = File.join(Dir.pwd, "yatfa.env.rb")
9
25
 
@@ -106,8 +106,13 @@ module YatfaAgent
106
106
 
107
107
  @config[:agents] = {}
108
108
 
109
- agent_types = ["worker", "planner", "reviewer", "researcher"]
109
+ agent_types = ["worker", "planner", "reviewer", "researcher", "claw"]
110
110
  agent_types.each do |agent_type|
111
+ if agent_type == "claw"
112
+ prompt_claw_agent
113
+ next
114
+ end
115
+
111
116
  print "#{agent_type.capitalize} API key (press Enter to skip): "
112
117
  key = STDIN.noecho(&:gets).chomp.strip
113
118
  puts ""
@@ -136,6 +141,43 @@ module YatfaAgent
136
141
  end
137
142
  end
138
143
 
144
+ def prompt_claw_agent
145
+ puts ""
146
+ puts "🐾 Claw Agent (OpenClaw Gateway)"
147
+ puts " This runs an OpenClaw gateway with Telegram integration."
148
+ puts " You'll need a Telegram Bot Token from @BotFather."
149
+ puts ""
150
+
151
+ print "Setup Claw agent? (press Enter to skip): "
152
+ confirm = Readline.readline.strip.downcase
153
+ return unless confirm == "y"
154
+
155
+ print "Telegram Bot Token: "
156
+ bot_token = STDIN.noecho(&:gets).chomp.strip
157
+ puts ""
158
+
159
+ if bot_token.empty?
160
+ puts "⊘ Skipped Claw agent (no token provided)"
161
+ return
162
+ end
163
+
164
+ agent_config = {
165
+ container_name: "#{@config[:project_name]}-claw",
166
+ telegram_bot_token: bot_token
167
+ }
168
+
169
+ # Optionally ask for MCP API key (for future Yatfa integration)
170
+ print "YATFA API key for MCP integration (optional, press Enter to skip): "
171
+ mcp_key = STDIN.noecho(&:gets).chomp.strip
172
+ puts ""
173
+ agent_config[:mcp_api_key] = mcp_key unless mcp_key.empty?
174
+
175
+ @config[:agents][:claw] = agent_config
176
+
177
+ puts "✅ Claw agent configured"
178
+ puts ""
179
+ end
180
+
139
181
  def prompt_git_config
140
182
  # Try to read from existing yatfa.env.rb
141
183
  existing_config = File.join(Dir.pwd, "yatfa.env.rb")
@@ -373,8 +415,19 @@ module YatfaAgent
373
415
  lines << " agents: {"
374
416
  @config[:agents].each_with_index do |(agent_type, agent_config), idx|
375
417
  lines << " #{agent_type}: {"
376
- lines << " mcp_api_key: #{agent_config[:mcp_api_key].inspect},"
377
- lines << " container_name: #{agent_config[:container_name].inspect}"
418
+ if agent_type.to_s == "claw"
419
+ lines << " container_name: #{agent_config[:container_name].inspect},"
420
+ lines << " telegram_bot_token: #{agent_config[:telegram_bot_token].inspect},"
421
+ lines << " env: {"
422
+ lines << " \"OPENCLAW_TELEGRAM_BOT_TOKEN\" => #{agent_config[:telegram_bot_token].inspect},"
423
+ if agent_config[:mcp_api_key]
424
+ lines << " \"YATFA_API_KEY\" => #{agent_config[:mcp_api_key].inspect},"
425
+ end
426
+ lines << " }"
427
+ else
428
+ lines << " mcp_api_key: #{agent_config[:mcp_api_key].inspect},"
429
+ lines << " container_name: #{agent_config[:container_name].inspect}"
430
+ end
378
431
  lines << " }#{',' if idx < @config[:agents].size - 1}"
379
432
  end
380
433
  lines << " }"
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "yatfa",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
4
4
  "description": "YATFA - Yet Another Tool For Agents",
5
- "bin": "./bin/run-yatfa-agent.rb",
5
+ "bin": "./bin/yatfa",
6
6
  "files": [
7
+ "bin/yatfa",
7
8
  "bin/run-yatfa-agent.rb",
8
9
  "agents.rb",
9
10
  "lib/",