maf 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +4 -0
- data/assets/dispatcher +8 -0
- data/lib/maf/backup.rb +63 -0
- data/lib/maf/budget.rb +2 -0
- data/lib/maf/cli.rb +19 -1
- data/lib/maf/flow/prompt_text.rb +8 -4
- data/lib/maf/restore.rb +62 -0
- data/lib/maf/shared/sandbox/seatbelt.rb +5 -1
- data/lib/maf/shared/sandbox.rb +6 -0
- data/lib/maf/team.rb +10 -2
- data/lib/maf/team_command.rb +8 -1
- data/lib/maf/untrack.rb +5 -1
- data/lib/maf/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2844c9f128a0b025a9a1c933091642b9e885f069d3bfe7daff77f2fe3659c56
|
|
4
|
+
data.tar.gz: c464cfff417c58d40add531944b4ec12a93a5251510dc01d7f2d81a0a7905997
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d906ee9673550164f3c61c195eee88a41173f80443176c5be7809a89d2da32036f8c6ad03620ca8ddadb8994a345a565602b660e0a150563d4411c6c0e896fae
|
|
7
|
+
data.tar.gz: 3fe01e2df87c0676ced712531aa44ec1935d8a5d5982d1521e765a3d846a4d9fd524485076738ba1c35927f8eb2cb00fea6db174d3fb570ada0565b19ebdeff1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-10-21
|
|
6
|
+
|
|
7
|
+
- Fix: in the macOS sandbox, a worker that the project manager started failed on each run with
|
|
8
|
+
`sandbox_apply: Operation not permitted`. macOS cannot start a sandbox inside a sandbox. The
|
|
9
|
+
sandbox now sets `MAF_SEATBELT=1`. In the sandbox, `maf prepare --dispatch` prepares the worker
|
|
10
|
+
and prints the start commands, and a dispatcher stops at once with the reason in its log.
|
|
11
|
+
- The project manager no longer adds workers on its own. Without a team budget, `maf prepare`
|
|
12
|
+
refuses when an agent runs it. Only the user sets the budget: `maf team set` refuses when an
|
|
13
|
+
agent runs it.
|
|
14
|
+
- Backup of the flow config. Each maf command copies `.maf/config.json`, `roles.yml`,
|
|
15
|
+
`workflow.md`, and `sandbox.json` to `.git/maf/backup/` when they changed, and keeps 10
|
|
16
|
+
copies. New command `maf restore [--list] [BACKUP] [--force]` puts them back and runs
|
|
17
|
+
`maf update`. When `.maf/` exists without `config.json`, maf names the backup. Before, a
|
|
18
|
+
merge of the `maf untrack` commit into a branch that still tracked the flow deleted the
|
|
19
|
+
config, and the agents, `base_branch`, and `github` settings were lost.
|
|
20
|
+
|
|
5
21
|
## [0.3.0] - 2026-10-21
|
|
6
22
|
|
|
7
23
|
- Fix: each dispatched Hermes run failed. Hermes 0.22 refuses `--run-budget` with `--format
|
data/README.md
CHANGED
|
@@ -192,6 +192,8 @@ multi_agent_flow/
|
|
|
192
192
|
flow/workflow.rb # reads .maf/workflow.md for the architect prompt
|
|
193
193
|
flow/mcp_config.rb # writes the graphify MCP server into .maf/mcp/
|
|
194
194
|
untrack.rb # maf untrack: removes an older install from git
|
|
195
|
+
backup.rb # copies of the flow config in .git/maf/backup
|
|
196
|
+
restore.rb # maf restore: puts the flow config back from a backup
|
|
195
197
|
local_exclude.rb # the flow block in .git/info/exclude
|
|
196
198
|
shared/ # stdlib-only code that the maf CLI and the scripts share (installed as .maf/lib/maf/shared/)
|
|
197
199
|
shared/harness/ # the harness registry, the default of each harness method, shared modules
|
|
@@ -234,6 +236,7 @@ multi_agent_flow/
|
|
|
234
236
|
worker_control_test.rb # tests for maf worker
|
|
235
237
|
worker_list_test.rb # tests for maf worker list
|
|
236
238
|
untrack_test.rb # tests for maf untrack
|
|
239
|
+
backup_test.rb # tests for the backup and maf restore
|
|
237
240
|
analyst_test.rb # tests for the analyst
|
|
238
241
|
shared_test.rb # tests for lib/maf/shared/
|
|
239
242
|
gemspec_test.rb # tests that the gem holds every runtime file
|
|
@@ -371,6 +374,7 @@ ruby test/context_watch_test.rb # covers the context-watch hook
|
|
|
371
374
|
ruby test/worker_control_test.rb # covers maf worker
|
|
372
375
|
ruby test/worker_list_test.rb # covers maf worker list
|
|
373
376
|
ruby test/untrack_test.rb # covers maf untrack
|
|
377
|
+
ruby test/backup_test.rb # covers the backup and maf restore
|
|
374
378
|
ruby test/analyst_test.rb # covers the analyst
|
|
375
379
|
ruby test/shared_test.rb # covers lib/maf/shared/
|
|
376
380
|
```
|
data/assets/dispatcher
CHANGED
|
@@ -175,6 +175,13 @@ module Dispatcher
|
|
|
175
175
|
end
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
def self.refuse_nested
|
|
179
|
+
return unless Maf::Shared::Sandbox.nested?
|
|
180
|
+
|
|
181
|
+
Log.line(Maf::Shared::Sandbox::NESTED.delete_prefix("maf: "))
|
|
182
|
+
exit 1
|
|
183
|
+
end
|
|
184
|
+
|
|
178
185
|
# Project finds the main checkout from any worktree. Lead roles own no
|
|
179
186
|
# tasks: they get an inbox prompt and no task polling.
|
|
180
187
|
Project = Maf::Shared::Project
|
|
@@ -1512,5 +1519,6 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
1512
1519
|
ENV["PATH"] = [File.dirname(File.expand_path(__FILE__)), ENV["PATH"]].compact.join(File::PATH_SEPARATOR)
|
|
1513
1520
|
# Each run inherits the git persona of the agents.
|
|
1514
1521
|
Maf::Shared::GitIdentity.apply(Dispatcher::Project.manifest)
|
|
1522
|
+
Dispatcher.refuse_nested
|
|
1515
1523
|
Dispatcher::Main.new(Dispatcher::Options.parse(ARGV)).run
|
|
1516
1524
|
end
|
data/lib/maf/backup.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# backup.rb - copies of the flow config in the git directory, and maf restore.
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module Maf
|
|
9
|
+
# Backup keeps copies of the files in .maf/ that maf update cannot make again.
|
|
10
|
+
# Each copy is a folder in .git/maf/backup/, so no checkout, merge, or reset
|
|
11
|
+
# changes it. A copy is made only when config.json exists and the files differ
|
|
12
|
+
# from the newest copy.
|
|
13
|
+
class Backup
|
|
14
|
+
FILES = %w[config.json roles.yml workflow.md sandbox.json].freeze
|
|
15
|
+
KEEP = 10
|
|
16
|
+
MISSING = "maf: .maf/config.json is missing. A backup from %<time>s has it. Run: maf restore"
|
|
17
|
+
|
|
18
|
+
def initialize(root)
|
|
19
|
+
@root = root
|
|
20
|
+
@dir = git_dir&.then { |dir| File.join(dir, "maf", "backup") }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save
|
|
24
|
+
files = current
|
|
25
|
+
return if @dir.nil? || !files.key?("config.json") || files == read(latest)
|
|
26
|
+
|
|
27
|
+
write(File.join(@dir, Time.now.utc.strftime("%Y%m%dT%H%M%SZ")), files)
|
|
28
|
+
prune
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def snapshots = @dir ? Dir.glob(File.join(@dir, "*")).select { |path| File.directory?(path) }.sort : []
|
|
32
|
+
def latest = snapshots.last
|
|
33
|
+
def find(name) = snapshots.find { |path| File.basename(path) == name }
|
|
34
|
+
def read(dir) = dir ? FILES.to_h { |name| [name, file(dir, name)] }.compact : {}
|
|
35
|
+
def time(dir) = Time.strptime(File.basename(dir), "%Y%m%dT%H%M%S%z").localtime.strftime("%Y-%m-%d %H:%M")
|
|
36
|
+
|
|
37
|
+
def warn_missing
|
|
38
|
+
maf = File.join(@root, ".maf")
|
|
39
|
+
return if !Dir.exist?(maf) || File.exist?(File.join(maf, "config.json")) || !read(latest).key?("config.json")
|
|
40
|
+
|
|
41
|
+
warn format(MISSING, time: time(latest))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def current = read(File.join(@root, ".maf"))
|
|
47
|
+
def file(dir, name) = File.file?(File.join(dir, name)) ? File.read(File.join(dir, name)) : nil
|
|
48
|
+
|
|
49
|
+
def write(dir, files)
|
|
50
|
+
FileUtils.mkdir_p(dir)
|
|
51
|
+
files.each { |name, text| File.write(File.join(dir, name), text) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def prune = snapshots[0...-KEEP].each { |dir| FileUtils.rm_rf(dir) }
|
|
55
|
+
|
|
56
|
+
def git_dir
|
|
57
|
+
out, status = Open3.capture2("git", "-C", @root, "rev-parse", "--git-common-dir", err: File::NULL)
|
|
58
|
+
status.success? ? File.expand_path(out.strip, @root) : nil
|
|
59
|
+
rescue Errno::ENOENT
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/maf/budget.rb
CHANGED
data/lib/maf/cli.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "team_command"
|
|
|
14
14
|
require_relative "worker_control"
|
|
15
15
|
require_relative "worker_list"
|
|
16
16
|
require_relative "untrack"
|
|
17
|
+
require_relative "restore"
|
|
17
18
|
require_relative "vm"
|
|
18
19
|
require_relative "version"
|
|
19
20
|
|
|
@@ -59,6 +60,7 @@ module Maf
|
|
|
59
60
|
"uninstall" => "[--check] [--yes] [--force] remove the flow from the project",
|
|
60
61
|
"migrate" => "[--check] [--yes] move an old-layout install into .maf/",
|
|
61
62
|
"untrack" => "[--check] [--yes] remove the flow from git; the files stay (then commit)",
|
|
63
|
+
"restore" => "[--list] [BACKUP] [--force] put the flow config back from a backup in .git/maf/backup",
|
|
62
64
|
"vm" => "create|login|start|stop|delete|status|shell [ARGS] run the team in a Lima VM (macOS, Linux)",
|
|
63
65
|
"menu" => " interactive mode (also: maf without a command)",
|
|
64
66
|
"guide" => " print the setup guide for an AI agent (install.md)",
|
|
@@ -76,11 +78,26 @@ module Maf
|
|
|
76
78
|
abort "maf: unknown command '#{@command}'. Run: maf help" unless COMMANDS.key?(@command)
|
|
77
79
|
abort "maf: #{Migrate::HINT}" if old_layout?
|
|
78
80
|
|
|
79
|
-
send("run_#{@command}")
|
|
81
|
+
backed_up { send("run_#{@command}") }
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
NO_BACKUP = %w[version guide roles].freeze
|
|
85
|
+
|
|
82
86
|
private
|
|
83
87
|
|
|
88
|
+
def backed_up
|
|
89
|
+
backup = checked_backup unless NO_BACKUP.include?(@command)
|
|
90
|
+
yield
|
|
91
|
+
ensure
|
|
92
|
+
backup&.save
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def checked_backup
|
|
96
|
+
backup = Backup.new(SetupAgent::Project.root)
|
|
97
|
+
backup.warn_missing unless @command == "restore"
|
|
98
|
+
backup.tap(&:save)
|
|
99
|
+
end
|
|
100
|
+
|
|
84
101
|
# Every command except these needs the new layout.
|
|
85
102
|
# maf uninstall --check previews the migration on an old layout.
|
|
86
103
|
def old_layout?
|
|
@@ -132,6 +149,7 @@ module Maf
|
|
|
132
149
|
def run_guide = puts(File.read(File.join(Flow::ROOT, "install.md")))
|
|
133
150
|
def run_version = puts("maf #{VERSION}")
|
|
134
151
|
def run_untrack = Untrack.new(SetupAgent::Project.root, @args).run
|
|
152
|
+
def run_restore = Restore.new(SetupAgent::Project.root, @args).run
|
|
135
153
|
def run_migrate = Migrate::Runner.new(["--project", Dir.pwd, *@args]).run
|
|
136
154
|
|
|
137
155
|
def run_agents
|
data/lib/maf/flow/prompt_text.rb
CHANGED
|
@@ -212,13 +212,16 @@ module Maf
|
|
|
212
212
|
# run the team. Dispatched workers cost no tokens while idle, so the rules
|
|
213
213
|
# scale on backlog, not on cost.
|
|
214
214
|
TEAM_RULES = <<~TEXT.strip
|
|
215
|
-
-
|
|
216
|
-
|
|
215
|
+
- Only the user sets the team budget: `maf team set --max <n> --allow <harness[:model]> ...`.
|
|
216
|
+
If the user gives you a budget in words, give the user that command.
|
|
217
|
+
With a budget, manage the team yourself in dispatch mode. `maf prepare ... --dispatch` starts the worker in the background.
|
|
218
|
+
In the macOS sandbox, maf cannot start it. Give the user the two commands that it prints.
|
|
217
219
|
- Check the team with `maf team`. It shows the budget, each worker, and the tasks by role.
|
|
218
220
|
- Staff the architect first, with `--dispatch`. A goal needs the architect before any other worker.
|
|
219
221
|
A dispatched architect starts on each message. An idle interactive architect reads nothing.
|
|
220
222
|
- Check who runs with `coord who`. A role without a live worker does not read its messages.
|
|
221
|
-
- If coord reports "No worker runs role <role>", add a worker for that role.
|
|
223
|
+
- If coord reports "No worker runs role <role>", add a worker for that role if the budget has a free slot.
|
|
224
|
+
Without a budget, tell the user.
|
|
222
225
|
- If a role has more than three backlog tasks and the budget has a free slot, add a worker for that role.
|
|
223
226
|
- If a role has no tasks and no open goal needs it, retire its extra workers.
|
|
224
227
|
Keep one worker per role that an open goal needs.
|
|
@@ -242,7 +245,8 @@ module Maf
|
|
|
242
245
|
|
|
243
246
|
Rules:
|
|
244
247
|
- Never edit source files. Never create tasks; only the architect creates tasks.
|
|
245
|
-
- Change the team when the user asks
|
|
248
|
+
- Change the team only when the user asks, or within the team budget. Without a budget, `maf prepare` refuses.
|
|
249
|
+
Then give the user the command, or ask the user for a budget.
|
|
246
250
|
Add a worker: `maf prepare <harness> <role>_<n>`.
|
|
247
251
|
Replace a worker: `maf prepare <harness> <role>_<n> --replace <old-role>_<n>`.
|
|
248
252
|
Remove a worker: `maf retire <role>_<n>`.
|
data/lib/maf/restore.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# restore.rb - maf restore: put the flow config back from a backup.
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "backup"
|
|
6
|
+
|
|
7
|
+
module Maf
|
|
8
|
+
# Restore copies the files of a backup into .maf/. It keeps a file that
|
|
9
|
+
# exists, unless --force is given. Then it runs maf update.
|
|
10
|
+
class Restore
|
|
11
|
+
USAGE = "usage: maf restore [--list] [BACKUP] [--force]"
|
|
12
|
+
|
|
13
|
+
def initialize(root, argv)
|
|
14
|
+
@root = root
|
|
15
|
+
@backup = Backup.new(root)
|
|
16
|
+
@list = argv.include?("--list")
|
|
17
|
+
@force = argv.include?("--force")
|
|
18
|
+
@name = argv.find { |arg| !arg.start_with?("-") }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
return list if @list
|
|
23
|
+
|
|
24
|
+
dir = @name ? @backup.find(@name) : @backup.latest
|
|
25
|
+
abort(@name ? "maf: no backup #{@name}. Run: maf restore --list" : "maf: no backup in #{@root}") unless dir
|
|
26
|
+
restore(dir)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def list
|
|
32
|
+
return puts("No backup yet. maf makes one when a maf command runs.") if @backup.snapshots.empty?
|
|
33
|
+
|
|
34
|
+
@backup.snapshots.reverse_each { |dir| puts line(dir) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def line(dir)
|
|
38
|
+
files = @backup.read(dir)
|
|
39
|
+
agents = JSON.parse(files.fetch("config.json", "{}")).fetch("agents", []).size
|
|
40
|
+
"#{File.basename(dir)} #{@backup.time(dir)} #{agents} agents #{files.keys.join(", ")}"
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
"#{File.basename(dir)} #{@backup.time(dir)} #{files.keys.join(", ")}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def restore(dir)
|
|
46
|
+
puts "Backup #{File.basename(dir)} (#{@backup.time(dir)}):"
|
|
47
|
+
written = @backup.read(dir).select { |name, text| put(name, text) }
|
|
48
|
+
return puts("Nothing to restore. --force replaces the files that exist.") if written.empty?
|
|
49
|
+
|
|
50
|
+
Dir.chdir(@root) { Maf.flow } if written.key?("config.json")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def put(name, text)
|
|
54
|
+
path = File.join(@root, ".maf", name)
|
|
55
|
+
return puts(" keep .maf/#{name} (exists; --force replaces it)") || false if File.exist?(path) && !@force
|
|
56
|
+
|
|
57
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
58
|
+
File.write(path, text)
|
|
59
|
+
puts(" restore .maf/#{name}") || true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -11,6 +11,7 @@ module Maf
|
|
|
11
11
|
# Seatbelt runs a command in a macOS sandbox-exec profile.
|
|
12
12
|
module Seatbelt
|
|
13
13
|
COMMAND = "/usr/bin/sandbox-exec"
|
|
14
|
+
MARKER = "MAF_SEATBELT"
|
|
14
15
|
DEVICES = '(literal "/dev/null") (literal "/dev/zero") (literal "/dev/dtracehelper") ' \
|
|
15
16
|
'(regex #"^/dev/tty") (regex #"^/dev/fd/") (regex #"^/dev/ptmx")'
|
|
16
17
|
DNS = '(path-literal "/private/var/run/mDNSResponder")'
|
|
@@ -20,9 +21,12 @@ module Maf
|
|
|
20
21
|
|
|
21
22
|
def self.wrap(cmd, paths)
|
|
22
23
|
tmp = private_tmp(paths.root)
|
|
23
|
-
[COMMAND, "-p", profile(paths, tmp), "env", "TMPDIR=#{tmp}/", "XDG_CACHE_HOME=#{tmp}/cache",
|
|
24
|
+
[COMMAND, "-p", profile(paths, tmp), "env", "#{MARKER}=1", "TMPDIR=#{tmp}/", "XDG_CACHE_HOME=#{tmp}/cache",
|
|
25
|
+
*cmd]
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
def self.inside? = ENV[MARKER] == "1"
|
|
29
|
+
|
|
26
30
|
def self.available? = File.executable?(COMMAND)
|
|
27
31
|
def self.require! = available? || abort("maf: sandbox mode seatbelt needs #{COMMAND} (macOS only).")
|
|
28
32
|
|
data/lib/maf/shared/sandbox.rb
CHANGED
|
@@ -18,6 +18,8 @@ module Maf
|
|
|
18
18
|
BACKENDS = { "seatbelt" => Seatbelt, "bwrap" => Bwrap }.freeze
|
|
19
19
|
MODES = ["off", "auto", *BACKENDS.keys].freeze
|
|
20
20
|
POLICY = File.join(".maf", "sandbox.json")
|
|
21
|
+
NESTED = "maf: this process runs in the macOS sandbox, and macOS cannot start a second sandbox in it. " \
|
|
22
|
+
"Start the worker from your terminal: cd <worktree> && maf start"
|
|
21
23
|
|
|
22
24
|
def self.policy
|
|
23
25
|
path = File.join(Project.root, POLICY)
|
|
@@ -40,9 +42,13 @@ module Maf
|
|
|
40
42
|
def self.wrap(cmd, harness, coord_dir: ENV.fetch("COORD_DIR", nil))
|
|
41
43
|
policy = self.policy
|
|
42
44
|
mode = mode(policy)
|
|
45
|
+
refuse_nested!(mode)
|
|
43
46
|
mode == "off" ? cmd : BACKENDS[mode].wrap(harness.sandboxed(cmd), paths(harness, policy, coord_dir))
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
def self.nested?(mode = self.mode) = mode == "seatbelt" && Seatbelt.inside?
|
|
50
|
+
def self.refuse_nested!(mode = self.mode) = nested?(mode) && abort(NESTED)
|
|
51
|
+
|
|
46
52
|
def self.keeping_links(harness, coord_dir: ENV.fetch("COORD_DIR", nil))
|
|
47
53
|
links = protected_links(harness, coord_dir)
|
|
48
54
|
yield
|
data/lib/maf/team.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Maf
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def self.notify(root, text) = coord(root, "msg", "--from", "maf", "architect", text)
|
|
27
|
+
def self.agent? = !ENV["COORD_ROLE"].to_s.empty?
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# Prepare makes a worker ready to start. With --replace, it retires the
|
|
@@ -35,6 +36,8 @@ module Maf
|
|
|
35
36
|
# The architect never talks to the user. A dispatched architect gets fresh
|
|
36
37
|
# sessions with a handoff note, so its context stays small.
|
|
37
38
|
DISPATCH_DEFAULT = %w[architect].freeze
|
|
39
|
+
NO_BUDGET = "maf: an agent cannot change the team without a budget. Ask the user to set one " \
|
|
40
|
+
"(maf team set --max N), or to run this command in a terminal."
|
|
38
41
|
|
|
39
42
|
def self.mode_args(argv)
|
|
40
43
|
return argv - ["--interactive"] if argv.include?("--interactive")
|
|
@@ -52,7 +55,7 @@ module Maf
|
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
def run
|
|
55
|
-
|
|
58
|
+
check_budget
|
|
56
59
|
retire = (Retire.new(@root, @replace).tap(&:check!) if @replace)
|
|
57
60
|
Dir.chdir(@root) { prepare }
|
|
58
61
|
retire.run if retire && Workers.id(@replace) != @args.worker
|
|
@@ -61,11 +64,16 @@ module Maf
|
|
|
61
64
|
|
|
62
65
|
def announce
|
|
63
66
|
Team.notify(@root, "Team change: worker #{@args.worker} (#{@args.harness}) joined as #{@args.role}.")
|
|
64
|
-
@args.dispatch ? start_dispatcher : print_next_steps
|
|
67
|
+
@args.dispatch && !Shared::Sandbox.nested? ? start_dispatcher : print_next_steps
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
private
|
|
68
71
|
|
|
72
|
+
def check_budget
|
|
73
|
+
abort NO_BUDGET if Team.agent? && !@budget.limited?
|
|
74
|
+
@budget.check!(@args.harness, @args.model, @budget.count(workers_after))
|
|
75
|
+
end
|
|
76
|
+
|
|
69
77
|
def take_replace(argv)
|
|
70
78
|
index = argv.index("--replace") or return nil
|
|
71
79
|
argv.slice!(index, 2)[1] || abort("maf: --replace needs a worker, for example backend-developer_2")
|
data/lib/maf/team_command.rb
CHANGED
|
@@ -18,7 +18,14 @@ module Maf
|
|
|
18
18
|
@root = root
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
AGENT_SET = "maf: only the user sets the team budget. Give the user this command to run in a terminal."
|
|
22
|
+
|
|
23
|
+
def run
|
|
24
|
+
return show unless @argv.first == "set"
|
|
25
|
+
|
|
26
|
+
abort AGENT_SET if Team.agent?
|
|
27
|
+
set(@argv.drop(1))
|
|
28
|
+
end
|
|
22
29
|
|
|
23
30
|
private
|
|
24
31
|
|
data/lib/maf/untrack.rb
CHANGED
|
@@ -23,6 +23,10 @@ module Maf
|
|
|
23
23
|
|
|
24
24
|
def self.warn_tracked(root) = (warn TRACKED unless new(root, []).tracked.empty?)
|
|
25
25
|
|
|
26
|
+
DONE = ["", "The flow left git. Review the change with `git status`, then commit it.",
|
|
27
|
+
"A branch that still tracks the flow files can delete them in a checkout or a merge.",
|
|
28
|
+
"maf keeps a backup of the flow config in .git/maf/backup. Run `maf restore` to put it back."].freeze
|
|
29
|
+
|
|
26
30
|
def run
|
|
27
31
|
steps = plan
|
|
28
32
|
return puts("maf: git tracks no file of the flow in #{@root}.") if steps.empty?
|
|
@@ -55,7 +59,7 @@ module Maf
|
|
|
55
59
|
abort "maf: cancelled" unless @yes || confirmed?
|
|
56
60
|
steps.each(&:run)
|
|
57
61
|
Dir.chdir(@root) { Maf.flow }
|
|
58
|
-
puts
|
|
62
|
+
puts DONE
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
def confirmed?
|
data/lib/maf/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: maf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dominik Alberski
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- exe/maf
|
|
60
60
|
- install.md
|
|
61
61
|
- lib/maf.rb
|
|
62
|
+
- lib/maf/backup.rb
|
|
62
63
|
- lib/maf/bootstrap.rb
|
|
63
64
|
- lib/maf/bootstrap/claude_settings.rb
|
|
64
65
|
- lib/maf/bootstrap/dependencies.rb
|
|
@@ -115,6 +116,7 @@ files:
|
|
|
115
116
|
- lib/maf/migrate/runner.rb
|
|
116
117
|
- lib/maf/migrate/worktrees.rb
|
|
117
118
|
- lib/maf/prompt.rb
|
|
119
|
+
- lib/maf/restore.rb
|
|
118
120
|
- lib/maf/retire.rb
|
|
119
121
|
- lib/maf/role_limits.rb
|
|
120
122
|
- lib/maf/setup_agent.rb
|