maf 0.2.0 → 0.3.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 +43 -0
- data/README.md +18 -1
- data/assets/agents-contract.md +2 -0
- data/assets/coord +145 -21
- data/assets/dispatcher +59 -13
- data/install.md +6 -1
- data/lib/maf/bootstrap/claude_settings.rb +17 -9
- data/lib/maf/bootstrap.rb +4 -0
- data/lib/maf/cli.rb +19 -9
- data/lib/maf/flow/effort_report.rb +20 -0
- data/lib/maf/flow/generator.rb +2 -1
- data/lib/maf/flow/hermes_env_setup.rb +46 -0
- data/lib/maf/flow/manifest.rb +1 -1
- data/lib/maf/flow/models.rb +11 -0
- data/lib/maf/flow/options.rb +29 -3
- data/lib/maf/flow/prompt_text.rb +6 -3
- data/lib/maf/flow/report.rb +6 -5
- data/lib/maf/flow/role_catalog.rb +1 -1
- data/lib/maf/flow/role_files.rb +5 -5
- data/lib/maf/flow/roster.rb +6 -3
- data/lib/maf/flow.rb +3 -1
- data/lib/maf/menu.rb +48 -14
- data/lib/maf/setup_agent/args.rb +16 -10
- data/lib/maf/setup_agent/dispatch.rb +2 -2
- data/lib/maf/setup_agent/launcher.rb +6 -4
- data/lib/maf/setup_agent/manifest.rb +9 -3
- data/lib/maf/setup_agent.rb +11 -11
- data/lib/maf/shared/harness/defaults.rb +10 -3
- data/lib/maf/shared/harnesses/claude.rb +6 -3
- data/lib/maf/shared/harnesses/codex.rb +39 -10
- data/lib/maf/shared/harnesses/cursor.rb +60 -4
- data/lib/maf/shared/harnesses/hermes.rb +21 -11
- data/lib/maf/shared/harnesses/opencode.rb +25 -2
- data/lib/maf/shared/roles.rb +4 -0
- data/lib/maf/team.rb +5 -4
- data/lib/maf/team_command.rb +3 -1
- data/lib/maf/untrack.rb +7 -0
- data/lib/maf/version.rb +1 -1
- data/lib/maf/worker_control.rb +12 -1
- data/lib/maf/worker_list.rb +83 -0
- data/templates/roles.yml +14 -0
- metadata +4 -1
data/install.md
CHANGED
|
@@ -198,7 +198,12 @@ list with the existing entries plus the new entry.
|
|
|
198
198
|
The second command approves the hook one time. Hermes stores the consent for this
|
|
199
199
|
version of the script. A new script version needs a new approval.
|
|
200
200
|
|
|
201
|
-
Confirm that every check from `hermes hooks doctor` passes.
|
|
201
|
+
Confirm that every check from `hermes hooks doctor` passes.
|
|
202
|
+
|
|
203
|
+
Hermes gives the commands of an agent only the variables in
|
|
204
|
+
`terminal.client_env_passthrough`. If `maf add` prints a `hermes config set
|
|
205
|
+
terminal.client_env_passthrough` command, run it. It keeps the names that the
|
|
206
|
+
list has now. Then continue.
|
|
202
207
|
|
|
203
208
|
## Step 5c - Set the Gemini key for the doc-graph refresh
|
|
204
209
|
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Maf
|
|
4
4
|
module Bootstrap
|
|
5
|
-
# ClaudeSettings plans and applies the Claude Code hooks
|
|
6
|
-
# file of the flow, .maf/claude/settings.json. `maf start`
|
|
7
|
-
# with --settings. Claude Code runs these hooks next to the
|
|
5
|
+
# ClaudeSettings plans and applies the Claude Code hooks and environment in
|
|
6
|
+
# the settings file of the flow, .maf/claude/settings.json. `maf start`
|
|
7
|
+
# passes the file with --settings. Claude Code runs these hooks next to the
|
|
8
|
+
# project's own. The --settings env wins over the env of the user settings.
|
|
8
9
|
class ClaudeSettings
|
|
9
10
|
def initialize(project)
|
|
10
11
|
@project = project
|
|
@@ -12,23 +13,30 @@ module Maf
|
|
|
12
13
|
|
|
13
14
|
def plan
|
|
14
15
|
file = @project.path(CLAUDE_SETTINGS)
|
|
15
|
-
[@project.action(status(file), file, "#{file} (next-task + board-watch hooks)", :claude_stop_hook)]
|
|
16
|
+
[@project.action(status(file), file, "#{file} (next-task + board-watch hooks, env)", :claude_stop_hook)]
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def configure(file)
|
|
19
20
|
settings = read(file)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
write(file, settings)
|
|
23
|
-
|
|
21
|
+
return false if current?(settings)
|
|
22
|
+
|
|
23
|
+
write(file, update(settings))
|
|
24
|
+
true
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
private
|
|
27
28
|
|
|
28
29
|
def status(file)
|
|
29
|
-
File.exist?(file) &&
|
|
30
|
+
File.exist?(file) && current?(read(file)) ? :skip : :configure_claude_hook
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update(settings)
|
|
34
|
+
missing_hooks(settings).each { |event, hook| add_hook(settings, event, hook) }
|
|
35
|
+
settings.merge("env" => (settings["env"] || {}).merge(CLAUDE_ENV))
|
|
30
36
|
end
|
|
31
37
|
|
|
38
|
+
def current?(settings) = missing_hooks(settings).empty? && (settings["env"] || {}) >= CLAUDE_ENV
|
|
39
|
+
|
|
32
40
|
def read(file)
|
|
33
41
|
File.exist?(file) ? (JSON.parse(File.read(file)) rescue {}) : {}
|
|
34
42
|
end
|
data/lib/maf/bootstrap.rb
CHANGED
|
@@ -64,6 +64,10 @@ module Maf
|
|
|
64
64
|
end
|
|
65
65
|
].freeze
|
|
66
66
|
|
|
67
|
+
# Claude Code environment of a flow session. Agent teams stay off: with
|
|
68
|
+
# them on, a subagent that Claude names starts as a teammate.
|
|
69
|
+
CLAUDE_ENV = { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" => "0" }.freeze
|
|
70
|
+
|
|
67
71
|
# The knowledge graph and the Obsidian vault, at the project root where graphify looks by default.
|
|
68
72
|
GRAPH_DIR = "graphify-out"
|
|
69
73
|
|
data/lib/maf/cli.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "menu"
|
|
|
12
12
|
require_relative "team"
|
|
13
13
|
require_relative "team_command"
|
|
14
14
|
require_relative "worker_control"
|
|
15
|
+
require_relative "worker_list"
|
|
15
16
|
require_relative "untrack"
|
|
16
17
|
require_relative "vm"
|
|
17
18
|
require_relative "version"
|
|
@@ -21,6 +22,7 @@ module Maf
|
|
|
21
22
|
|
|
22
23
|
def self.flow(*args) = Flow::Generator.new(["--project", Dir.pwd, *args]).run
|
|
23
24
|
def self.role_names = Flow::RoleCatalog.new(Dir.pwd).roles.keys
|
|
25
|
+
def self.role_effort(role) = Flow::RoleCatalog.new(Dir.pwd).roles.dig(role, "effort")
|
|
24
26
|
|
|
25
27
|
def self.agents = File.exist?(MANIFEST) ? JSON.parse(File.read(MANIFEST)).fetch("agents") : []
|
|
26
28
|
def self.agent_specs = agents.map { |a| "#{a["harness"]}:#{a["role"]}" }
|
|
@@ -38,19 +40,21 @@ module Maf
|
|
|
38
40
|
|
|
39
41
|
class CLI
|
|
40
42
|
COMMANDS = {
|
|
41
|
-
"add" => "[HARNESS:ROLE[:MODEL] ...] [--model ROLE=MODEL] add agents and install
|
|
42
|
-
"without arguments it asks",
|
|
43
|
+
"add" => "[HARNESS:ROLE[:MODEL] ...] [--model ROLE=MODEL] [--effort ROLE=LEVEL] add agents and install " \
|
|
44
|
+
"the flow; without arguments it asks",
|
|
43
45
|
"remove" => "HARNESS:ROLE ... remove agents",
|
|
44
46
|
"update" => " regenerate the files of the current agents",
|
|
45
47
|
"agents" => " list the current agents",
|
|
46
48
|
"roles" => " list the built-in roles and the project roles",
|
|
47
49
|
"role" => "add NAME add a stub role to .maf/roles.yml",
|
|
48
|
-
"start" => "[HARNESS ROLE[_WORKER]] [--dispatch [--detach]] start one agent in its worktree",
|
|
49
|
-
"prepare" => "HARNESS ROLE[_WORKER] [--dispatch|--interactive] [--replace W]
|
|
50
|
+
"start" => "[HARNESS ROLE[_WORKER]] [--effort LEVEL] [--dispatch [--detach]] start one agent in its worktree",
|
|
51
|
+
"prepare" => "HARNESS ROLE[_WORKER] [--effort LEVEL] [--dispatch|--interactive] [--replace W] " \
|
|
52
|
+
"prepare a worker; " \
|
|
50
53
|
"--dispatch starts it (default for the architect)",
|
|
51
54
|
"retire" => "ROLE[_WORKER] remove a worker; its tasks return to the pool",
|
|
52
|
-
"worker" => "status|stop|start|restart ROLE[_WORKER] [--force] [--max-context N]
|
|
53
|
-
"[--cache-window S]
|
|
55
|
+
"worker" => "list | status|stop|start|restart ROLE[_WORKER] [--force] [--max-context N] " \
|
|
56
|
+
"[--max-session-runs N] [--cache-window S] list the running workers, or control one worker; " \
|
|
57
|
+
"start and restart save the session limits of the role",
|
|
54
58
|
"team" => "[set --max N --allow HARNESS[:MODEL]] show the team, or set its budget",
|
|
55
59
|
"uninstall" => "[--check] [--yes] [--force] remove the flow from the project",
|
|
56
60
|
"migrate" => "[--check] [--yes] move an old-layout install into .maf/",
|
|
@@ -95,7 +99,10 @@ module Maf
|
|
|
95
99
|
# Without arguments on a terminal, maf add asks for the harness, the roles, and the models.
|
|
96
100
|
def run_add = @args.empty? && $stdin.tty? ? Menu.new.add : Maf.flow(*AgentArgs.convert(@args, "--agent"))
|
|
97
101
|
def run_remove = Maf.flow(*AgentArgs.convert(@args, "--remove"))
|
|
98
|
-
def run_update
|
|
102
|
+
def run_update
|
|
103
|
+
Maf.flow(*@args)
|
|
104
|
+
Untrack.warn_tracked(SetupAgent::Project.root)
|
|
105
|
+
end
|
|
99
106
|
def run_roles = Maf.flow("--list-roles")
|
|
100
107
|
|
|
101
108
|
def run_role
|
|
@@ -108,13 +115,15 @@ module Maf
|
|
|
108
115
|
def run_prepare = Prepare.new(@args).run
|
|
109
116
|
def run_team = TeamCommand.new(@args, SetupAgent::Project.root).run
|
|
110
117
|
def run_worker
|
|
118
|
+
return WorkerList.new(SetupAgent::Project.root).run if @args.first == "list"
|
|
119
|
+
|
|
111
120
|
spec = @args[1] || abort(worker_usage)
|
|
112
121
|
limits = RoleLimits.parse(@args)
|
|
113
122
|
WorkerControl.new(SetupAgent::Project.root, spec, force: @args.include?("--force"), limits: limits)
|
|
114
123
|
.run(@args[0].to_s)
|
|
115
124
|
end
|
|
116
125
|
|
|
117
|
-
def worker_usage = "usage: maf worker #{WorkerControl::ACTIONS.join("|")} ROLE[_WORKER] [--force] " \
|
|
126
|
+
def worker_usage = "usage: maf worker list | #{WorkerControl::ACTIONS.join("|")} ROLE[_WORKER] [--force] " \
|
|
118
127
|
"[--max-context N] [--max-session-runs N] [--cache-window S]"
|
|
119
128
|
def run_retire = Retire.new(SetupAgent::Project.root, @args.first || abort("usage: maf retire ROLE[_WORKER]")).run
|
|
120
129
|
def run_uninstall = Uninstall::Runner.new(["--project", Dir.pwd, *@args]).run
|
|
@@ -132,7 +141,8 @@ module Maf
|
|
|
132
141
|
end
|
|
133
142
|
|
|
134
143
|
def agent_line(agent)
|
|
135
|
-
|
|
144
|
+
effort = agent["effort"] && "effort=#{agent["effort"]}"
|
|
145
|
+
"#{agent["harness"]}:#{agent["role"]}".ljust(32) + [agent["model"], effort].compact.join(" ")
|
|
136
146
|
end
|
|
137
147
|
end
|
|
138
148
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Flow
|
|
5
|
+
# EffortReport warns about an effort level that the harness does not list.
|
|
6
|
+
module EffortReport
|
|
7
|
+
def self.print(results)
|
|
8
|
+
list = results.select { |r| r[:effort] && !harness(r).known_effort?(r[:effort], r[:model]) }
|
|
9
|
+
return if list.empty?
|
|
10
|
+
|
|
11
|
+
puts "", "WARNING: the harness does not list these effort levels. Check the level for a typo:",
|
|
12
|
+
list.map { |r| " #{name(r)} -> #{r[:effort]}" },
|
|
13
|
+
" Fix one with: maf add HARNESS:ROLE --effort ROLE=LEVEL"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.harness(result) = Harness.fetch(result[:agent][:harness])
|
|
17
|
+
def self.name(result) = "#{result[:agent][:harness]}:#{result[:agent][:role]}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/maf/flow/generator.rb
CHANGED
|
@@ -26,6 +26,7 @@ module Maf
|
|
|
26
26
|
Validator.project!(@options)
|
|
27
27
|
@catalog = RoleCatalog.new(@options.project)
|
|
28
28
|
@roles = @catalog.roles
|
|
29
|
+
@options.role_efforts = @roles.transform_values { |data| data["effort"] }
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def generate_all
|
|
@@ -51,7 +52,7 @@ module Maf
|
|
|
51
52
|
|
|
52
53
|
def print_role(key, data)
|
|
53
54
|
puts "", " #{key} (#{@catalog.source(key)})", " #{data.fetch("description")}",
|
|
54
|
-
" model hint: #{data.fetch("model_hint")}"
|
|
55
|
+
" model hint: #{data.fetch("model_hint")}", *(" effort: #{data["effort"]}" if data["effort"])
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def finish_without_agents
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Flow
|
|
5
|
+
# HermesEnvSetup prints the step that lets Hermes give the flow variables to
|
|
6
|
+
# the commands of an agent. The flow never edits the Hermes config.
|
|
7
|
+
class HermesEnvSetup
|
|
8
|
+
KEY = "terminal.client_env_passthrough"
|
|
9
|
+
NAMES = %w[COORD_DIR COORD_ROLE COORD_WORKER COORD_DISPATCHED COORD_SLOT COORD_SESSION_PID TASKRC
|
|
10
|
+
MAF_BIN].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(config_yaml = File.join(Dir.home, ".hermes", "config.yaml"))
|
|
13
|
+
@config_yaml = config_yaml
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns true when the user has a step left.
|
|
17
|
+
def install
|
|
18
|
+
names = listed
|
|
19
|
+
return puts(" hermes env ready: #{KEY}") || false if (NAMES - names).empty?
|
|
20
|
+
|
|
21
|
+
print_step(names) || true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def command(names) = "hermes config set #{KEY} '#{JSON.generate((names + NAMES).uniq)}'"
|
|
25
|
+
|
|
26
|
+
def listed
|
|
27
|
+
list = config.dig("terminal", "client_env_passthrough")
|
|
28
|
+
list.is_a?(Array) ? list.map(&:to_s) : []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def print_step(names)
|
|
34
|
+
puts "", " Hermes env: the commands of a Hermes agent get no COORD_* variables yet.",
|
|
35
|
+
" Do this step one time. It keeps the names that the list has now.", "", " #{command(names)}", ""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def config
|
|
39
|
+
data = YAML.safe_load_file(@config_yaml)
|
|
40
|
+
data.is_a?(Hash) ? data : {}
|
|
41
|
+
rescue SystemCallError, Psych::Exception
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/maf/flow/manifest.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Maf
|
|
|
34
34
|
# a role without the role definitions.
|
|
35
35
|
def entry(agent)
|
|
36
36
|
{ harness: agent[:harness], role: agent[:role], model: @options.model_for(agent),
|
|
37
|
-
can_edit: @roles.fetch(agent[:role]).fetch("can_edit") }
|
|
37
|
+
can_edit: @roles.fetch(agent[:role]).fetch("can_edit") }.merge({ effort: @options.effort_for(agent) }.compact)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Other tools own other keys (maf team: "team"; users: "base_branch").
|
data/lib/maf/flow/models.rb
CHANGED
|
@@ -21,6 +21,17 @@ module Maf
|
|
|
21
21
|
|
|
22
22
|
def self.known?(harness, model) = Harness[harness]&.known_model?(model, self.for(harness)) != false
|
|
23
23
|
def self.default(harness) = Harness[harness]&.default_model
|
|
24
|
+
# Without a model, the levels of the default model of the harness.
|
|
25
|
+
def self.efforts(harness, model) = Harness[harness]&.efforts(model || default(harness))
|
|
26
|
+
|
|
27
|
+
# The level of the role if the harness lists it. A model that runs only
|
|
28
|
+
# with a level gets the lowest level instead.
|
|
29
|
+
def self.default_effort(harness, level, model)
|
|
30
|
+
levels = efforts(harness, model).to_a
|
|
31
|
+
return level if levels.include?(level)
|
|
32
|
+
|
|
33
|
+
levels.first if Harness[harness]&.effort_required?(model)
|
|
34
|
+
end
|
|
24
35
|
end
|
|
25
36
|
end
|
|
26
37
|
end
|
data/lib/maf/flow/options.rb
CHANGED
|
@@ -4,8 +4,8 @@ module Maf
|
|
|
4
4
|
module Flow
|
|
5
5
|
# Options parses the command line and holds the settings of one run.
|
|
6
6
|
class Options
|
|
7
|
-
attr_accessor :project, :agents
|
|
8
|
-
attr_reader :removed, :models, :hermes_dir, :list_roles
|
|
7
|
+
attr_accessor :project, :agents, :role_efforts
|
|
8
|
+
attr_reader :removed, :models, :efforts, :hermes_dir, :list_roles
|
|
9
9
|
|
|
10
10
|
# Each flag: the OptionParser arguments, and a block that runs in the
|
|
11
11
|
# Options instance with the flag value.
|
|
@@ -14,6 +14,7 @@ module Maf
|
|
|
14
14
|
[["--agent SPEC", "HARNESS:ROLE[:MODEL]"], ->(v) { @agents << parse_agent(v) }],
|
|
15
15
|
[["--remove SPEC", "HARNESS:ROLE"], ->(v) { @removed << parse_agent(v) }],
|
|
16
16
|
[["--model PAIR", "ROLE=MODEL"], ->(v) { @models.store(*v.split("=", 2)) }],
|
|
17
|
+
[["--effort PAIR", "ROLE=LEVEL"], ->(v) { @efforts.store(*v.split("=", 2)) }],
|
|
17
18
|
[["--hermes-dir DIR"], ->(v) { @hermes_dir = File.expand_path(v) }],
|
|
18
19
|
[["--check"], ->(_) { @check = true }],
|
|
19
20
|
[["--force"], ->(_) { @force = true }],
|
|
@@ -22,7 +23,8 @@ module Maf
|
|
|
22
23
|
].freeze
|
|
23
24
|
|
|
24
25
|
def initialize(argv)
|
|
25
|
-
@argv, @agents, @removed, @models = argv, [], [], {}
|
|
26
|
+
@argv, @agents, @removed, @models, @efforts = argv, [], [], {}, {}
|
|
27
|
+
@role_efforts = {}
|
|
26
28
|
@check = @force = @list_roles = false
|
|
27
29
|
@bootstrap = true
|
|
28
30
|
end
|
|
@@ -48,8 +50,32 @@ module Maf
|
|
|
48
50
|
agent[:model] || @models[agent[:role]] || agent[:saved_model] || Harness[agent[:harness]]&.default_model
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
# An empty --effort ROLE= clears the level: the harness default applies.
|
|
54
|
+
# A saved level belongs to the saved model, so a new model gets the role default.
|
|
55
|
+
def effort_for(agent)
|
|
56
|
+
flag = effort_flag(agent)
|
|
57
|
+
return (flag unless flag.empty?) if flag
|
|
58
|
+
|
|
59
|
+
(agent[:saved_effort] unless model_changed?(agent)) ||
|
|
60
|
+
Models.default_effort(agent[:harness], @role_efforts[agent[:role]], model_for(agent))
|
|
61
|
+
end
|
|
62
|
+
|
|
51
63
|
private
|
|
52
64
|
|
|
65
|
+
# --effort ROLE=LEVEL applies to the agents of this run. Without one of
|
|
66
|
+
# the role, it applies to the saved agents of the role.
|
|
67
|
+
def effort_flag(agent)
|
|
68
|
+
role = agent[:role]
|
|
69
|
+
@efforts[role] if run_agent?(agent) || @agents.none? { |a| a[:role] == role && run_agent?(a) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def run_agent?(agent) = agent.key?(:model)
|
|
73
|
+
|
|
74
|
+
def model_changed?(agent)
|
|
75
|
+
chosen = agent[:model] || @models[agent[:role]]
|
|
76
|
+
chosen && chosen != agent[:saved_model]
|
|
77
|
+
end
|
|
78
|
+
|
|
53
79
|
def parser
|
|
54
80
|
OptionParser.new do |o|
|
|
55
81
|
o.banner = "Usage: maf add|remove|update [HARNESS:ROLE ...] [options]"
|
data/lib/maf/flow/prompt_text.rb
CHANGED
|
@@ -99,9 +99,12 @@ module Maf
|
|
|
99
99
|
- Before you continue a claimed task after a pause, run `coord show <id>`. If the status is not pending,
|
|
100
100
|
or the worker is not you, stop work on that task. `coord reap` releases the claims of stalled workers.
|
|
101
101
|
- Finish the whole task. Report done only when each acceptance criterion passes.
|
|
102
|
-
- If you cannot finish, do the parts you can
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
- If you cannot finish, do the parts you can and commit them on the task branch.
|
|
103
|
+
Then block the task: `coord block <id> --kind <kind> "<blocker and missing parts>"`.
|
|
104
|
+
Kinds: needs_input (a decision of the user), dependency (other work must land first),
|
|
105
|
+
capability (a missing tool, access, or model). coord tells the role that can remove the blocker.
|
|
106
|
+
A blocked task leaves `coord next`, so no other worker hits the blocker.
|
|
107
|
+
Stop. Do not retry a failing approach.
|
|
105
108
|
%{no_task_instruction}
|
|
106
109
|
- Record durable knowledge in the shared vault or #{DECISIONS}.
|
|
107
110
|
- Never write ad-hoc verification scripts. The test suite is the verification.
|
data/lib/maf/flow/report.rb
CHANGED
|
@@ -12,20 +12,21 @@ module Maf
|
|
|
12
12
|
|
|
13
13
|
def print(results)
|
|
14
14
|
puts "", "Generated role files:", results.map { |result| generated_line(result) }
|
|
15
|
-
%i[print_missing_models print_unknown_models print_unembeddable
|
|
16
|
-
|
|
15
|
+
%i[print_missing_models print_unknown_models print_unembeddable].each { |name| send(name, results) }
|
|
16
|
+
EffortReport.print(results)
|
|
17
|
+
print_sessions(results)
|
|
17
18
|
print_hermes_reminder
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
private
|
|
21
22
|
|
|
22
|
-
# The
|
|
23
|
+
# The Hermes steps print at install time, far above the last lines the user
|
|
23
24
|
# reads. Repeat the state here so the flow is not started with a dead hook.
|
|
24
25
|
def print_hermes_reminder
|
|
25
26
|
return unless @hermes_pending
|
|
26
27
|
|
|
27
|
-
puts "The Hermes
|
|
28
|
-
"Then run `maf update` to confirm
|
|
28
|
+
puts "The Hermes setup is not done yet. Finish the steps above.",
|
|
29
|
+
"Then run `maf update` to confirm that Hermes is ready."
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def generated_line(result)
|
|
@@ -6,7 +6,7 @@ module Maf
|
|
|
6
6
|
# role in .maf/roles.yml adds a role or replaces a built-in role.
|
|
7
7
|
class RoleCatalog
|
|
8
8
|
FILE = ".maf/roles.yml"
|
|
9
|
-
DEFAULTS = { "model_hint" => "", "can_edit" => true }.freeze
|
|
9
|
+
DEFAULTS = { "model_hint" => "", "can_edit" => true, "effort" => nil }.freeze
|
|
10
10
|
|
|
11
11
|
def initialize(project)
|
|
12
12
|
@project = project
|
data/lib/maf/flow/role_files.rb
CHANGED
|
@@ -20,14 +20,14 @@ module Maf
|
|
|
20
20
|
|
|
21
21
|
def generate_agent(agent)
|
|
22
22
|
harness = Harness.fetch(agent[:harness])
|
|
23
|
-
|
|
23
|
+
choice = { model: @options.model_for(agent), effort: @options.effort_for(agent) }
|
|
24
24
|
dest = harness.role_destination(@options.project, agent[:role], @options.settings)
|
|
25
|
-
content = render(harness, agent[:role], @roles.fetch(agent[:role]),
|
|
26
|
-
{ agent: agent, dest: dest, status: write(dest, content),
|
|
25
|
+
content = render(harness, agent[:role], @roles.fetch(agent[:role]), choice)
|
|
26
|
+
{ agent: agent, dest: dest, status: write(dest, content), **choice }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def render(harness, role, data,
|
|
30
|
-
harness.render_role(**template_values(role, data), prompt: @prompts.build(harness, role, data),
|
|
29
|
+
def render(harness, role, data, choice)
|
|
30
|
+
harness.render_role(**template_values(role, data), prompt: @prompts.build(harness, role, data), **choice)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def template_values(role, data)
|
data/lib/maf/flow/roster.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Maf
|
|
4
4
|
module Flow
|
|
5
5
|
# Roster merges the agents saved in .maf/config.json with the --agent and
|
|
6
|
-
# --remove specs of this run. A saved model ranks below --model
|
|
6
|
+
# --remove specs of this run. A saved model ranks below --model, a saved
|
|
7
|
+
# effort below --effort.
|
|
7
8
|
class Roster
|
|
8
9
|
def initialize(project)
|
|
9
10
|
path = File.join(project, ".maf/config.json")
|
|
@@ -11,7 +12,7 @@ module Maf
|
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def merge(added, removed)
|
|
14
|
-
kept = saved.map { |a| added.find { |b| same?(a, b) }&.merge(saved_model
|
|
15
|
+
kept = saved.map { |a| added.find { |b| same?(a, b) }&.merge(a.slice(:saved_model, :saved_effort)) || a }
|
|
15
16
|
agents = kept + added.reject { |b| kept.any? { |a| same?(a, b) } }
|
|
16
17
|
agents.reject { |a| removed.any? { |b| same?(a, b) } }
|
|
17
18
|
end
|
|
@@ -19,7 +20,9 @@ module Maf
|
|
|
19
20
|
private
|
|
20
21
|
|
|
21
22
|
def saved
|
|
22
|
-
@saved.map
|
|
23
|
+
@saved.map do |a|
|
|
24
|
+
{ harness: a["harness"], role: a["role"], saved_model: a["model"], saved_effort: a["effort"] }
|
|
25
|
+
end
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def same?(one, other) = one[:harness] == other[:harness] && one[:role] == other[:role]
|
data/lib/maf/flow.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# maf add, maf remove, maf update, and maf roles run Flow::Generator.
|
|
6
6
|
# Options: --project DIR [--agent HARNESS:ROLE ...] [--remove HARNESS:ROLE ...]
|
|
7
|
-
# [--model ROLE=MODEL] [--hermes-dir DIR] [--check] [--force] [--no-bootstrap]
|
|
7
|
+
# [--model ROLE=MODEL] [--effort ROLE=LEVEL] [--hermes-dir DIR] [--check] [--force] [--no-bootstrap]
|
|
8
8
|
# [--list-roles]
|
|
9
9
|
#
|
|
10
10
|
# HARNESS is a harness of the registry: one file in lib/maf/shared/harnesses/.
|
|
@@ -52,6 +52,7 @@ require_relative "flow/options"
|
|
|
52
52
|
require_relative "flow/prompt_builder"
|
|
53
53
|
require_relative "flow/hook_files"
|
|
54
54
|
require_relative "flow/hermes_hook_setup"
|
|
55
|
+
require_relative "flow/hermes_env_setup"
|
|
55
56
|
require_relative "flow/codex_hooks"
|
|
56
57
|
require_relative "flow/legacy_codex_hook"
|
|
57
58
|
require_relative "flow/hook_installer"
|
|
@@ -61,6 +62,7 @@ require_relative "flow/mcp_installer"
|
|
|
61
62
|
require_relative "flow/role_files"
|
|
62
63
|
require_relative "flow/manifest"
|
|
63
64
|
require_relative "flow/models"
|
|
65
|
+
require_relative "flow/effort_report"
|
|
64
66
|
require_relative "flow/report"
|
|
65
67
|
require_relative "flow/validator"
|
|
66
68
|
require_relative "flow/harness_linker"
|
data/lib/maf/menu.rb
CHANGED
|
@@ -21,14 +21,14 @@ module Maf
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Only the installed harnesses are choices. Each role gets its own model.
|
|
24
|
+
# Only the installed harnesses are choices. Each role gets its own model and effort.
|
|
25
25
|
def add
|
|
26
26
|
harness = @prompt.choose("Harness", Flow::Models.installed) or return
|
|
27
27
|
roles = @prompt.choose_many("Roles", Maf.role_names)
|
|
28
28
|
return if roles.empty?
|
|
29
29
|
|
|
30
30
|
models = show_models(harness)
|
|
31
|
-
Maf.flow(*roles.flat_map { |role|
|
|
31
|
+
Maf.flow(*roles.flat_map { |role| agent_args(harness, role, models) })
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
private
|
|
@@ -56,21 +56,18 @@ module Maf
|
|
|
56
56
|
@prompt.say("error: #{e.message}")
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def
|
|
59
|
+
def agent_args(harness, role, models)
|
|
60
60
|
model = ask_model(harness, role, models).to_s
|
|
61
|
-
|
|
61
|
+
effort = ask_effort(harness, role, model.empty? ? nil : model).to_s
|
|
62
|
+
flags = effort.empty? ? [] : ["--effort", "#{role}=#{effort}"]
|
|
63
|
+
["--agent", [harness, role, model].reject(&:empty?).join(":"), *flags]
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# every dispatched run fail.
|
|
66
|
+
# A model name that the harness does not list needs a confirmation: a typo
|
|
67
|
+
# makes every dispatched run fail.
|
|
67
68
|
def ask_model(harness, role, models)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return models[number - 1] if number&.between?(1, models.size)
|
|
71
|
-
return answer if answer.empty? || Flow::Models.known?(harness, answer)
|
|
72
|
-
|
|
73
|
-
@prompt.confirm?("#{harness} does not list #{answer}. Use it anyway?") ? answer : ask_model(harness, role, models)
|
|
69
|
+
question = "Model for #{role} (number or name, Enter = #{default_label(harness)}):"
|
|
70
|
+
ask_listed(harness, question, models) { |name| Flow::Models.known?(harness, name) }
|
|
74
71
|
end
|
|
75
72
|
|
|
76
73
|
def default_label(harness) = Flow::Models.default(harness) || "the harness default"
|
|
@@ -78,10 +75,47 @@ module Maf
|
|
|
78
75
|
def show_models(harness)
|
|
79
76
|
models = Flow::Models.for(harness).to_a
|
|
80
77
|
@prompt.say(models.empty? ? "#{harness} lists no models. Type a name." : "Models of #{harness}:")
|
|
81
|
-
models.each_with_index { |model, i| @prompt.say(" #{i + 1}) #{model}") }
|
|
78
|
+
models.each_with_index { |model, i| @prompt.say(" #{i + 1}) #{model}#{levels_label(harness, model)}") }
|
|
82
79
|
models
|
|
83
80
|
end
|
|
84
81
|
|
|
82
|
+
def levels_label(harness, model)
|
|
83
|
+
levels = Flow::Models.efforts(harness, model).to_a
|
|
84
|
+
levels.empty? ? "" : " (effort: #{levels.join(", ")})"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# An empty list: the model takes no level, so maf does not ask.
|
|
88
|
+
# nil: the harness lists no levels, so every level counts as known.
|
|
89
|
+
def ask_effort(harness, role, model)
|
|
90
|
+
levels = Flow::Models.efforts(harness, model)
|
|
91
|
+
return if levels == []
|
|
92
|
+
|
|
93
|
+
levels.to_a.each_with_index { |level, i| @prompt.say(" #{i + 1}) #{level}") }
|
|
94
|
+
question = "Effort for #{role} (number or name, Enter = #{effort_label(harness, role, model)}):"
|
|
95
|
+
ask_listed(harness, question, levels.to_a) { |name| levels.nil? || levels.include?(name) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def effort_label(harness, role, model)
|
|
99
|
+
Flow::Models.default_effort(harness, Maf.role_effort(role), model) || "the harness default"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The answer is a number of the list, a name, or Enter for the default.
|
|
103
|
+
def ask_listed(harness, question, list, &known)
|
|
104
|
+
answer = @prompt.ask(question).to_s
|
|
105
|
+
picked = pick(answer, list, &known)
|
|
106
|
+
return picked if picked
|
|
107
|
+
return answer if @prompt.confirm?("#{harness} does not list #{answer}. Use it anyway?")
|
|
108
|
+
|
|
109
|
+
ask_listed(harness, question, list, &known)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def pick(answer, list)
|
|
113
|
+
number = Integer(answer, exception: false)
|
|
114
|
+
return list[number - 1] if number&.between?(1, list.size)
|
|
115
|
+
|
|
116
|
+
answer if answer.empty? || yield(answer)
|
|
117
|
+
end
|
|
118
|
+
|
|
85
119
|
def remove
|
|
86
120
|
spec = @prompt.choose("Agent", Maf.agent_specs) or return
|
|
87
121
|
Maf.flow("--remove", spec) if @prompt.confirm?("Remove #{spec}?")
|
data/lib/maf/setup_agent/args.rb
CHANGED
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
module Maf
|
|
4
4
|
module SetupAgent
|
|
5
5
|
# Args reads the positional arguments (HARNESS ROLE[_WORKER] [model:M]),
|
|
6
|
-
# then the flags. --dispatch
|
|
7
|
-
# other flags go to the dispatcher and need --dispatch.
|
|
6
|
+
# then the flags. --dispatch, --model M, and --effort L are setup_agent's own
|
|
7
|
+
# flags; all other flags go to the dispatcher and need --dispatch.
|
|
8
8
|
class Args
|
|
9
|
-
Parsed = Struct.new(:harness, :role, :worker_id, :worker, :model, :
|
|
10
|
-
keyword_init: true)
|
|
9
|
+
Parsed = Struct.new(:harness, :role, :worker_id, :worker, :model, :effort, :dispatch, :detach,
|
|
10
|
+
:dispatcher_args, keyword_init: true)
|
|
11
11
|
|
|
12
12
|
def self.parse(argv)
|
|
13
13
|
dispatch, detach, positional, flags = split(argv)
|
|
14
|
-
model =
|
|
14
|
+
model, effort = %w[--model --effort].map { |flag| take_value(flags, flag) }
|
|
15
|
+
check_flags(flags, dispatch, detach)
|
|
16
|
+
Parsed.new(**identity(positional, model, dispatch), effort: effort, dispatch: dispatch, detach: detach,
|
|
17
|
+
dispatcher_args: flags)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.check_flags(flags, dispatch, detach)
|
|
15
21
|
reject_stray(flags, dispatch)
|
|
16
22
|
abort "setup_agent: --detach works only with --dispatch" if detach && !dispatch
|
|
17
|
-
Parsed.new(**identity(positional, model, dispatch), dispatch: dispatch, detach: detach, dispatcher_args: flags)
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def self.split(argv)
|
|
@@ -31,10 +36,10 @@ module Maf
|
|
|
31
36
|
model: model || model_arg&.sub(/\Amodel:/, "") }
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
def self.
|
|
35
|
-
index = flags.index(
|
|
39
|
+
def self.take_value(flags, flag)
|
|
40
|
+
index = flags.index(flag)
|
|
36
41
|
value = index && flags.slice!(index, 2)[1]
|
|
37
|
-
abort "setup_agent:
|
|
42
|
+
abort "setup_agent: #{flag} needs a value" if index && (value.nil? || value.start_with?("--"))
|
|
38
43
|
value
|
|
39
44
|
end
|
|
40
45
|
|
|
@@ -50,7 +55,8 @@ module Maf
|
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
def self.abort_usage
|
|
53
|
-
abort "usage: maf start HARNESS ROLE[_WORKER] [model:MODEL] [--model MODEL] [--
|
|
58
|
+
abort "usage: maf start HARNESS ROLE[_WORKER] [model:MODEL] [--model MODEL] [--effort LEVEL] " \
|
|
59
|
+
"[--dispatch [FLAGS...]]"
|
|
54
60
|
end
|
|
55
61
|
end
|
|
56
62
|
end
|
|
@@ -9,11 +9,11 @@ module Maf
|
|
|
9
9
|
module Dispatch
|
|
10
10
|
DISPATCHER = ".maf/bin/dispatcher"
|
|
11
11
|
|
|
12
|
-
def self.launch(args
|
|
12
|
+
def self.launch(args)
|
|
13
13
|
dispatcher = File.join(Project.root, DISPATCHER)
|
|
14
14
|
require_dispatcher!(dispatcher)
|
|
15
15
|
cmd = [RbConfig.ruby, dispatcher, args.role, "--harness", args.harness]
|
|
16
|
-
cmd +=
|
|
16
|
+
cmd += { "--model" => args.model, "--effort" => args.effort }.compact.flatten
|
|
17
17
|
cmd += args.dispatcher_args
|
|
18
18
|
args.detach ? detach(cmd, args.worker) : Launcher.exec_or_die(cmd)
|
|
19
19
|
end
|