maf 0.1.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 +174 -0
- data/README.md +29 -11
- data/assets/agents-contract.md +2 -0
- data/assets/analyst +29 -51
- data/assets/coord +168 -41
- data/assets/dashboard +1 -5
- data/assets/dispatcher +127 -298
- data/assets/harness-hooks/context-watch.rb +5 -4
- data/assets/lima/maf.yaml.erb +61 -0
- data/install.md +6 -1
- data/lib/maf/bootstrap/claude_settings.rb +17 -9
- data/lib/maf/bootstrap/script_planner.rb +1 -1
- data/lib/maf/bootstrap.rb +4 -0
- data/lib/maf/cli.rb +22 -9
- data/lib/maf/flow/agent_links.rb +2 -2
- data/lib/maf/flow/bootstrapper.rb +5 -5
- data/lib/maf/flow/codex_hooks.rb +12 -1
- data/lib/maf/flow/effort_report.rb +20 -0
- data/lib/maf/flow/generator.rb +3 -2
- data/lib/maf/flow/harness_linker.rb +2 -2
- data/lib/maf/flow/hermes_env_setup.rb +46 -0
- data/lib/maf/flow/hook_files.rb +6 -0
- data/lib/maf/flow/hook_installer.rb +6 -20
- data/lib/maf/flow/manifest.rb +3 -3
- data/lib/maf/flow/mcp_config.rb +8 -22
- data/lib/maf/flow/mcp_installer.rb +3 -3
- data/lib/maf/flow/models.rb +14 -38
- data/lib/maf/flow/options.rb +33 -5
- data/lib/maf/flow/prompt_builder.rb +7 -14
- data/lib/maf/flow/prompt_text.rb +13 -14
- data/lib/maf/flow/report.rb +8 -9
- data/lib/maf/flow/role_catalog.rb +1 -1
- data/lib/maf/flow/role_files.rb +7 -22
- data/lib/maf/flow/roster.rb +6 -3
- data/lib/maf/flow/validator.rb +2 -2
- data/lib/maf/flow.rb +7 -21
- data/lib/maf/menu.rb +49 -15
- data/lib/maf/migrate/role_files.rb +3 -1
- data/lib/maf/retire.rb +42 -6
- data/lib/maf/setup_agent/args.rb +16 -10
- data/lib/maf/setup_agent/dispatch.rb +3 -3
- data/lib/maf/setup_agent/launcher.rb +16 -65
- data/lib/maf/setup_agent/manifest.rb +9 -3
- data/lib/maf/setup_agent/pty_relay.rb +75 -0
- data/lib/maf/setup_agent/role_file.rb +1 -1
- data/lib/maf/setup_agent/runtime_hooks.rb +1 -12
- data/lib/maf/setup_agent.rb +11 -13
- data/lib/maf/shared/harness/base.rb +57 -0
- data/lib/maf/shared/harness/defaults.rb +147 -0
- data/lib/maf/shared/harness/output.rb +55 -0
- data/lib/maf/shared/harness/traits.rb +35 -0
- data/lib/maf/shared/harness/vm_credential.rb +36 -0
- data/lib/maf/shared/harness.rb +12 -0
- data/lib/maf/shared/harnesses/claude.rb +137 -0
- data/lib/maf/shared/harnesses/codex.rb +202 -0
- data/lib/maf/shared/harnesses/cursor.rb +120 -0
- data/lib/maf/shared/harnesses/hermes.rb +122 -0
- data/lib/maf/shared/harnesses/opencode.rb +192 -0
- data/lib/maf/shared/processes.rb +20 -0
- data/lib/maf/shared/roles.rb +4 -0
- data/lib/maf/shared/sandbox/bwrap.rb +128 -0
- data/lib/maf/shared/sandbox/harness_files.rb +50 -0
- data/lib/maf/shared/sandbox/links.rb +28 -0
- data/lib/maf/shared/sandbox/paths.rb +124 -0
- data/lib/maf/shared/sandbox/references.rb +64 -0
- data/lib/maf/shared/sandbox/seatbelt.rb +86 -0
- data/lib/maf/shared/sandbox.rb +84 -0
- data/lib/maf/team.rb +5 -4
- data/lib/maf/team_command.rb +5 -3
- data/lib/maf/uninstall/manifest.rb +10 -8
- data/lib/maf/uninstall/mcp_entries.rb +4 -5
- data/lib/maf/uninstall/notes.rb +13 -2
- data/lib/maf/uninstall/role_files.rb +11 -10
- data/lib/maf/uninstall/runner.rb +4 -3
- data/lib/maf/uninstall/scripts.rb +1 -1
- data/lib/maf/uninstall/vm_files.rb +16 -0
- data/lib/maf/uninstall.rb +20 -5
- data/lib/maf/untrack.rb +12 -25
- data/lib/maf/version.rb +1 -1
- data/lib/maf/vm/login.rb +52 -0
- data/lib/maf/vm/options.rb +41 -0
- data/lib/maf/vm/template.rb +44 -0
- data/lib/maf/vm.rb +94 -0
- data/lib/maf/worker_control.rb +16 -8
- data/lib/maf/worker_list.rb +83 -0
- data/lib/maf/workers.rb +27 -0
- data/templates/roles.yml +14 -0
- metadata +33 -11
- data/lib/maf/setup_agent/hermes_launcher.rb +0 -34
- data/lib/maf/setup_agent/hermes_skill.rb +0 -26
- data/templates/claude.md.erb +0 -16
- data/templates/codex.md.erb +0 -7
- data/templates/hermes.md.erb +0 -12
- data/templates/opencode.md.erb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d92abf8bbcca7ba5044ac6185534e883a55a1387e05460ac4142e1d89f3d13e
|
|
4
|
+
data.tar.gz: 25eaa46e4ed438fef70a08841137fc87c7ede35dfe9c09ff4d053a351e11e9ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96324cff6b379cb0ec71cbe0d8a15d127a950ddfb7e3997012f4f9b6c4759508a34668fc06a7702d04030ebf65875335c05a6a5836f7959422cbaf107d6abe92
|
|
7
|
+
data.tar.gz: 9faa6836a004d143d9bcdfc7852d4479b2f9b43f40d09efcab44c32343607b9bcf73949c7e62235b772f817d37845e0e2a5955deefab1c576477a962798e8aa2
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,180 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-10-21
|
|
6
|
+
|
|
7
|
+
- Fix: each dispatched Hermes run failed. Hermes 0.22 refuses `--run-budget` with `--format
|
|
8
|
+
stream-json`. maf no longer passes `--run-budget`. The `--timeout` of the dispatcher still
|
|
9
|
+
stops a run.
|
|
10
|
+
- Fix: in a Hermes run, `coord` did not get `COORD_WORKER` and the other flow variables, so
|
|
11
|
+
`coord done` refused to close the task. Hermes runs each session in a shared gateway and
|
|
12
|
+
passes only the names in `terminal.client_env_passthrough`. `maf add` and `maf update` print
|
|
13
|
+
the `hermes config set` command that adds the flow names to that list.
|
|
14
|
+
- `maf worker stop|start|restart|status ROLE` finds the worker when the role has one worker,
|
|
15
|
+
for example `tester-bot`.
|
|
16
|
+
- `maf worker list` names the stopped workers also when no worker runs.
|
|
17
|
+
- A worker does not start a team of its own. `.maf/claude/settings.json` sets
|
|
18
|
+
`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0`, so a subagent that Claude names in a `maf start`
|
|
19
|
+
session does not start as a teammate. `maf update` adds the setting.
|
|
20
|
+
- Hermes: each role gets a toolset list. A role that edits no longer gets `kanban`,
|
|
21
|
+
`delegation`, `cronjob`, `computer_use`, and the other toolsets outside the list.
|
|
22
|
+
- Fix: a Hermes role with `can_edit: false` did not get the graph MCP server, because its
|
|
23
|
+
toolset list did not name `mcp-graphify-<project>`.
|
|
24
|
+
- The dispatcher blocks a task after 3 runs in a row that leave it claimed and not done, and
|
|
25
|
+
escalates it to the project manager. Before, it resumed such a task each hour without end.
|
|
26
|
+
New commands: `coord block ID [--kind K] TEXT` holds a task out of `coord next`, and
|
|
27
|
+
`coord unblock ID` returns it. `coord status` and `coord board` show blocked tasks.
|
|
28
|
+
- A worker that cannot finish a task blocks it with a kind: `needs_input`, `dependency`, or
|
|
29
|
+
`capability`. coord tells the architect (dependency) or the project manager (the other kinds).
|
|
30
|
+
Before, the worker kept the claim, and the dispatcher resumed the task each hour.
|
|
31
|
+
- `maf worker list` shows each running worker: its role, harness, mode (session or dispatch),
|
|
32
|
+
pid, and start time. A worker that runs twice, for example as a session and as a
|
|
33
|
+
dispatcher, gets a warning. The last line names the registered workers that do not run.
|
|
34
|
+
- Effort levels per role (#44): `maf add --effort ROLE=LEVEL`, `maf start` and `maf prepare
|
|
35
|
+
--effort LEVEL`, and `dispatcher --effort LEVEL`. maf passes the level to the harness in its
|
|
36
|
+
own form: `--effort` (Claude Code), `model_reasoning_effort` (Codex), `--reasoning` (Hermes),
|
|
37
|
+
`--variant` (opencode), and the model name for Cursor (`gpt-5.3-codex` + `high` =
|
|
38
|
+
`gpt-5.3-codex-high`). Each role has a default level in `roles.yml` (`effort:`), used when the
|
|
39
|
+
harness lists it. After `maf update`, the claude, codex, hermes, and cursor agents get the
|
|
40
|
+
default of their role. The interactive mode shows the levels of each model and asks for one.
|
|
41
|
+
- Fix: Codex 0.163 ran the commands of a worker in its shared background server. The commands
|
|
42
|
+
got the environment of the shell that started the server, not of the worker: no `.maf/bin` on
|
|
43
|
+
PATH, no `COORD_ROLE` and `COORD_WORKER`. maf now starts Codex with `--no-daemon`.
|
|
44
|
+
- Fix: after a message run, the dispatcher counted the unclaimed tasks as dispatched and backed
|
|
45
|
+
off: 2 minutes, then 4, then more. A message run must not claim a task, so the task waited
|
|
46
|
+
without a reason. The task now runs in the next cycle.
|
|
47
|
+
|
|
48
|
+
## [0.2.0] - 2026-10-14
|
|
49
|
+
|
|
50
|
+
- The sandbox also protects the flow config in `.maf/claude/` and `.maf/mcp/`, and each script
|
|
51
|
+
that it names. `maf start` and the dispatcher give these files to the harness on the host.
|
|
52
|
+
- Fix: `maf start` of a Codex worker in the sandbox stopped with `Errno::EPERM` on the protected
|
|
53
|
+
`.codex/hooks.json`. maf now writes the file only on a change.
|
|
54
|
+
- The sandbox also protects `.maf/coordination/taskdata/hooks/`: Taskwarrior runs each script
|
|
55
|
+
in it, also on the host.
|
|
56
|
+
- Fix: on Linux, a protected file in `/tmp` (for example a hook script in a harness config)
|
|
57
|
+
made bwrap bind the host `/tmp`, with the sockets of host services. bwrap now binds a parent
|
|
58
|
+
folder only in an allowed folder.
|
|
59
|
+
- Fix: `coord worktree` in the sandbox stopped with an error on the protected `.maf/env.sh` of a
|
|
60
|
+
worktree. coord now writes the file only on a change, and it shows a warning if it cannot.
|
|
61
|
+
- Fix: `maf retire` did not stop a dispatcher of an older maf during a run longer than one
|
|
62
|
+
hour. maf now also finds the dispatcher from its command in the worktree.
|
|
63
|
+
- `maf vm create` installs the maf version of the host, not the newest maf.
|
|
64
|
+
- The sandbox also protects `.maf/coordination/harness-hooks` of the main checkout.
|
|
65
|
+
- On Linux, maf no longer creates an empty `~/.codex/packages` or a `{}` `~/.cursor/cli-config.json`.
|
|
66
|
+
- Fix: `maf team` showed a running dispatcher of an older maf as stopped.
|
|
67
|
+
- maf does not take `bwrap` from a folder of the `write` list.
|
|
68
|
+
- Fix: a broken `.maf/sandbox.json` turned the sandbox off. maf now stops with an error.
|
|
69
|
+
- Fix: maf ran `sandbox-exec` and `bwrap` from `PATH`. maf now runs `/usr/bin/sandbox-exec`,
|
|
70
|
+
and it does not take `bwrap` from the project or a temporary folder.
|
|
71
|
+
- On Linux, each sandboxed run gets a private `/dev/shm`.
|
|
72
|
+
- The analyst command starts in a new session without a terminal.
|
|
73
|
+
- Fix: an empty start time in `workers.json` made a running dispatcher look stopped.
|
|
74
|
+
- The sandbox also protects a hook script that `$CLAUDE_PROJECT_DIR` names, the scripts that
|
|
75
|
+
the harness config of a worktree names, and `config` and `hooks/` of each submodule.
|
|
76
|
+
- Fix: `maf retire` stopped for good when another process had the pid of an old dispatcher.
|
|
77
|
+
For a record without a start time, maf now uses the heartbeat.
|
|
78
|
+
- In a worktree, maf checks the Codex hook trust of the main checkout, because Codex uses
|
|
79
|
+
the hooks of the main checkout there. The warning tells you to trust the hooks on the host.
|
|
80
|
+
- Fix: on Linux, a `TMPDIR` in `/run/user` (or a `write` path in or above a masked folder)
|
|
81
|
+
opened the session bus again. bwrap now skips such a path.
|
|
82
|
+
- Fix: `maf retire` and `maf worker restart` went on when maf could not check the pid of a
|
|
83
|
+
worker from an older maf. They now stop and show the `kill` command.
|
|
84
|
+
- Fix: in the dispatcher, a failed start of a run could run the exit handlers of the
|
|
85
|
+
dispatcher in the child process.
|
|
86
|
+
- Fix: `printf task | maf start` (input without a final newline) waited forever in the sandbox.
|
|
87
|
+
- Fix: on macOS, a run could take the temporary folder of another project from `TMPDIR`.
|
|
88
|
+
- `maf vm login` says "this computer" instead of "this Mac", because it also runs on Linux.
|
|
89
|
+
- Fix: the dashboard ran the analyst command of `.maf/config.json` on the host. The agent
|
|
90
|
+
can change that file, so the command now runs in the sandbox.
|
|
91
|
+
- The sandbox also protects `.maf/coordination/harness-hooks` in each worktree, and
|
|
92
|
+
`.git/config.worktree` of the project and of each worktree.
|
|
93
|
+
- Fix: a relative path in the `write` list of `.maf/sandbox.json` started at the current
|
|
94
|
+
folder. It now starts at the project root.
|
|
95
|
+
- Fix: on Linux, a home folder with its own overlay made maf think that it already ran in the
|
|
96
|
+
sandbox, so it did not wrap the agent. The sandbox now also sets `MAF_BWRAP=1`.
|
|
97
|
+
- Fix: `echo task | maf start` with the sandbox on waited forever. The terminal relay now
|
|
98
|
+
sends ^D when the input ends.
|
|
99
|
+
- Fix: `maf vm` stopped with an error for a VM without mounts.
|
|
100
|
+
- Fix: `maf start` could start a second dispatcher for a worker whose presence record has no
|
|
101
|
+
start time. Such a record now counts as live while its heartbeat is recent. `maf stop` then
|
|
102
|
+
shows the `kill` command instead of a signal to an unknown pid.
|
|
103
|
+
- Fix: in the sandbox, `maf stop` of an interactive worker stopped only the terminal relay.
|
|
104
|
+
The relay now passes TERM, HUP, and INT to the harness, and waits for it.
|
|
105
|
+
- On Linux, maf puts back a protected symlink (for example `~/.claude/CLAUDE.md` into your
|
|
106
|
+
dotfiles) that the agent changed during a run, and shows a warning.
|
|
107
|
+
- The sandbox also protects the harness config in each worktree.
|
|
108
|
+
- Fix: on Linux, a worker that a lead starts failed when the home folder has a space in its path.
|
|
109
|
+
- `docs/sandbox.md` has a new section: what the sandbox protects against.
|
|
110
|
+
- The sandbox uses an allowlist in the folder of each harness: the agent can write only
|
|
111
|
+
the state paths, for example `~/.claude/projects` and `~/.codex/sessions`. Each other
|
|
112
|
+
path is protected, also a new name in an update of the harness. On Linux, each existing
|
|
113
|
+
path that is not state is bound read-only.
|
|
114
|
+
- Fix: the dispatcher ran the verify command of `.maf/config.json` on the host. The agent
|
|
115
|
+
can change that file, so the command now runs in the sandbox.
|
|
116
|
+
- Fix: a dispatched run could push a command into the terminal of the dispatcher (TIOCSTI).
|
|
117
|
+
Each run now starts in a new session without a terminal.
|
|
118
|
+
- Fix: the dispatcher ran `coord` from the worktree, where the agent can change it. It now
|
|
119
|
+
runs `coord` of the main checkout.
|
|
120
|
+
- Fix: a sandboxed run of a worker with `--command` stopped with `NoMethodError`.
|
|
121
|
+
- The sandbox protects the config of each harness in the project (`.mcp.json`,
|
|
122
|
+
`.claude/settings.json`, `.codex/config.toml`, `.opencode/`, and more), and `.maf/bin`,
|
|
123
|
+
`.maf/lib`, and `.maf/env.sh` in each worktree.
|
|
124
|
+
- The sandbox no longer opens `~/.cache` and `~/Library/Caches`. On macOS, `XDG_CACHE_HOME`
|
|
125
|
+
is a private folder of the project.
|
|
126
|
+
- The sandbox also protects a file that a config names with a relative path, and a named file of
|
|
127
|
+
1 MB or more. In a Markdown file, maf follows only the imports, so the text of a skill does not
|
|
128
|
+
protect the files of the project.
|
|
129
|
+
- `maf vm create` finds the agents of the project also in a subfolder or in a worktree.
|
|
130
|
+
- Fix: an agent in the sandbox could push a command into the terminal of an interactive
|
|
131
|
+
`maf start` (TIOCSTI). The shell ran the command outside the sandbox after the harness stopped.
|
|
132
|
+
With the sandbox on, the harness now gets its own terminal.
|
|
133
|
+
- The sandbox also protects each file that the config of a harness names: hook scripts,
|
|
134
|
+
status line scripts, and imports in `CLAUDE.md`. It also protects `~/.claude/rules`, and
|
|
135
|
+
`prompts`, `packages`, and `AGENTS.override.md` in `~/.codex`.
|
|
136
|
+
- On Linux, maf creates each missing config file of a harness before each run, so bwrap can protect it.
|
|
137
|
+
- `maf uninstall` removes `.maf/vm/` and tells you if the VM of the project still exists.
|
|
138
|
+
- `maf vm create` checks the checksum of the Taskwarrior download, and accepts only sizes above 0.
|
|
139
|
+
- The sandbox also protects `.maf/coordination/taskrc`, `.maf/coordination/worktree-env.rb`,
|
|
140
|
+
`.git/commondir`, the git links of each worktree, and the config, hooks, and plugins of each harness.
|
|
141
|
+
If a protected path is a symlink, the link is protected too. On Linux, maf creates the missing
|
|
142
|
+
protected folders before each run.
|
|
143
|
+
- On macOS, the sandbox blocks LaunchServices (`open`) and Apple Events: they started apps outside the sandbox.
|
|
144
|
+
- `maf vm` with a missing or unknown action, or an unknown flag, shows the usage.
|
|
145
|
+
- A VM boot no longer installs the harness CLIs again. `maf vm create` makes a smaller VM by default:
|
|
146
|
+
4 GiB of memory and a 20 GiB disk.
|
|
147
|
+
- Fix: `maf retire` could stop an unrelated process that got the pid of an ended dispatcher.
|
|
148
|
+
maf now signals a pid only if its start time matches the recorded one. `maf start --detach`
|
|
149
|
+
records the start time. `maf team`, `maf worker`, and the dashboard use the same check.
|
|
150
|
+
- The sandbox policy moved to `.maf/sandbox.json`. The agent cannot change it, the flow scripts,
|
|
151
|
+
the message hooks, the git hooks, or `.git/config`. On macOS, each run gets a private `TMPDIR`,
|
|
152
|
+
and the agent can connect only to the sockets of its own folders.
|
|
153
|
+
- `maf vm shell` loads `.maf/env.sh`, so `coord` is on PATH.
|
|
154
|
+
- The sandbox blocks host service sockets (tmux, Docker, systemd --user, D-Bus, SSH agent).
|
|
155
|
+
On Linux, `/tmp` is private to the run, and bwrap keeps the Claude Code state file.
|
|
156
|
+
- `maf vm`: the VM name holds a hash of the project path, and each command checks that the VM
|
|
157
|
+
mounts the project. The VM runs Ubuntu 26.04 and needs Lima 2.2.0 or later. The copied logins are always mode 600.
|
|
158
|
+
- Sandbox mode `bwrap` needs bubblewrap 0.10 or later. maf stops with an older bubblewrap.
|
|
159
|
+
- Linux support: sandbox mode `bwrap` (bubblewrap) and mode `auto` (seatbelt on macOS, bwrap on Linux).
|
|
160
|
+
`maf vm` also runs on a Linux host with QEMU, and copies the claude login from `~/.claude/.credentials.json`.
|
|
161
|
+
- New command `maf vm`: run the whole team in a Lima VM on macOS. `maf vm create` makes a VM that
|
|
162
|
+
sees only the project folder, at the same path as on the host. It installs Ruby, git, tmux,
|
|
163
|
+
Taskwarrior 3, maf, and the CLI of each harness of the project. Then it asks for each harness
|
|
164
|
+
if it can copy the login from this Mac into the VM (`--copy-logins`, `--no-copy-logins`, `maf vm login`).
|
|
165
|
+
- New sandbox for the agents on macOS: `{"mode": "seatbelt"}` in `.maf/sandbox.json`
|
|
166
|
+
or `MAF_SANDBOX=seatbelt`. `maf start` and the dispatcher run the harness in `sandbox-exec`.
|
|
167
|
+
The agent can write only to the project, the board, the temporary folders, and the folders of its harness.
|
|
168
|
+
- Each harness is one file in `lib/maf/shared/harnesses/`. To add a harness, add one
|
|
169
|
+
file, one test file, and one row in the Harnesses table of the user manual.
|
|
170
|
+
`test/harnesses/contract_test.rb` checks every harness file.
|
|
171
|
+
- The dispatcher has no default harness. Give `--harness NAME` or `--command TEMPLATE`.
|
|
172
|
+
`maf start` gives the harness, so `maf start` does not change.
|
|
173
|
+
- A dispatched run gets `OPENCODE_CONFIG` only on the `opencode` harness.
|
|
174
|
+
- `maf untrack` untracks only the role links in `.cursor/agents/`, not the whole folder.
|
|
175
|
+
- New harness `cursor` (the Cursor CLI, `agent`): `maf add cursor:ROLE`, `maf start cursor ROLE`,
|
|
176
|
+
and `dispatcher ROLE --harness cursor`. Role files go to `.cursor/agents/` as Cursor subagents.
|
|
177
|
+
Cursor has no wake-up hook yet, so its agents wait with `coord next --wait`.
|
|
178
|
+
|
|
5
179
|
## [0.1.0] - 2026-10-08
|
|
6
180
|
|
|
7
181
|
- First release as a gem: `gem install maf`.
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# multi_agent_flow
|
|
2
2
|
|
|
3
3
|
Coordination layer for running several AI coding agents (opencode, Claude Code,
|
|
4
|
-
Hermes, Codex) in parallel on one repository, each in its own terminal.
|
|
4
|
+
Hermes, Codex, Cursor) in parallel on one repository, each in its own terminal.
|
|
5
5
|
|
|
6
6
|
Three pillars:
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ The `maf` gem holds the flow. Install these tools yourself:
|
|
|
25
25
|
| Ruby 3.0+ | runs `maf`, `coord`, and the other scripts | macOS: `brew install mise && mise install ruby`. Linux: your package manager or [mise](https://mise.jdx.dev) |
|
|
26
26
|
| git | worktrees, hooks, the local exclude file | macOS: `xcode-select --install` or `brew install git`. Linux: `apt install git` |
|
|
27
27
|
| Taskwarrior | the task board | macOS: `brew install task`. Linux: `apt install taskwarrior` |
|
|
28
|
-
| One or more harnesses | the agents | [Claude Code](https://docs.claude.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [opencode](https://opencode.ai), [Hermes](https://github.com/NousResearch/hermes-agent) |
|
|
28
|
+
| One or more harnesses | the agents | [Claude Code](https://docs.claude.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [opencode](https://opencode.ai), [Cursor CLI](https://cursor.com/cli), [Hermes](https://github.com/NousResearch/hermes-agent) |
|
|
29
29
|
| graphify (optional) | the shared knowledge graph | `uv tool install graphifyy` |
|
|
30
30
|
|
|
31
31
|
Install the gem:
|
|
@@ -83,7 +83,7 @@ lives in `graphify-out/` at the project root, where graphify looks by default.
|
|
|
83
83
|
lib/maf/shared/ code that the scripts in bin/ load (maf update replaces it)
|
|
84
84
|
coordination/ task board, inbox, locks, presence, sessions, hooks, logs
|
|
85
85
|
worktrees/ one git worktree per worker
|
|
86
|
-
agents/ role files: claude/, opencode/, codex/
|
|
86
|
+
agents/ role files: claude/, opencode/, codex/, cursor/
|
|
87
87
|
claude/ settings.json: the Claude Code hooks (maf start passes --settings)
|
|
88
88
|
mcp/ the graphify MCP server for Claude Code and opencode
|
|
89
89
|
config.json the agents and settings of the project
|
|
@@ -113,7 +113,7 @@ Local files that a harness or git reads outside `.maf/`:
|
|
|
113
113
|
|---|---|
|
|
114
114
|
| `.git/hooks/*` | Git reads them there. |
|
|
115
115
|
| `.git/info/exclude` | Keeps the flow out of git in this clone. |
|
|
116
|
-
| `.claude/agents/`, `.opencode/agents/`, `.codex/prompts/` | Symlinks into `.maf/agents/<harness>/`. |
|
|
116
|
+
| `.claude/agents/`, `.opencode/agents/`, `.codex/prompts/`, `.cursor/agents/` | Symlinks into `.maf/agents/<harness>/`. |
|
|
117
117
|
| `.opencode/plugins/board-watch.js` | opencode reads it there. |
|
|
118
118
|
| `.codex/hooks.json` | Codex reads it there. Excluded when the project does not track it. |
|
|
119
119
|
| `~/.hermes/skills/<project>-<role>/SKILL.md` | Hermes reads it there. |
|
|
@@ -145,6 +145,8 @@ section of [USER_MANUAL.md](USER_MANUAL.md).
|
|
|
145
145
|
| **[GETTING_STARTED.md](GETTING_STARTED.md)** | First-time user | Concepts, prerequisites, manual install, basic workflow |
|
|
146
146
|
| **[USER_MANUAL.md](USER_MANUAL.md)** | Setting up a real team | Full install (maf), all harnesses, dispatcher, monitoring |
|
|
147
147
|
| **[install.md](install.md)** | An AI coding agent | Interactive wizard: asks the user for harnesses/roles, runs `maf add` |
|
|
148
|
+
| **[docs/sandbox.md](docs/sandbox.md)** | Running agents without approvals | Sandbox mode (macOS and Linux), the protected files, the limits, and the Lima VM |
|
|
149
|
+
| **[docs/releasing.md](docs/releasing.md)** | Maintainer | Release a new gem version: version bump, tag, the release workflow, fixes |
|
|
148
150
|
| **[docs/out-of-scope.md](docs/out-of-scope.md)** | Contributor | Requests that the project rejects on purpose, with the reason |
|
|
149
151
|
| **[SKILL.md](SKILL.md)** | Agent skill loader | Self-contained portable skill (frontmatter + full API reference) |
|
|
150
152
|
|
|
@@ -162,7 +164,7 @@ multi_agent_flow/
|
|
|
162
164
|
GETTING_STARTED.md # first-time walkthrough (concepts + manual setup)
|
|
163
165
|
USER_MANUAL.md # full team setup reference
|
|
164
166
|
maf.gemspec # the gem: files, version, the webrick dependency
|
|
165
|
-
Gemfile # development gems: rake, minitest, rubocop
|
|
167
|
+
Gemfile # development gems: rake, minitest, rubocop, mutant
|
|
166
168
|
CHANGELOG.md # changes per version
|
|
167
169
|
exe/
|
|
168
170
|
maf # the maf command line tool (gem install maf puts it on PATH)
|
|
@@ -185,22 +187,21 @@ multi_agent_flow/
|
|
|
185
187
|
team_command.rb # maf team: shows the team, or sets its budget
|
|
186
188
|
retire.rb # maf retire: removes one worker and archives its state
|
|
187
189
|
worker_control.rb # maf worker: stops, starts, or restarts one worker
|
|
190
|
+
worker_list.rb # maf worker list: shows the sessions and dispatchers that run
|
|
188
191
|
flow/role_catalog.rb # merges .maf/roles.yml over the built-in roles
|
|
189
192
|
flow/workflow.rb # reads .maf/workflow.md for the architect prompt
|
|
190
193
|
flow/mcp_config.rb # writes the graphify MCP server into .maf/mcp/
|
|
191
194
|
untrack.rb # maf untrack: removes an older install from git
|
|
192
195
|
local_exclude.rb # the flow block in .git/info/exclude
|
|
193
196
|
shared/ # stdlib-only code that the maf CLI and the scripts share (installed as .maf/lib/maf/shared/)
|
|
197
|
+
shared/harness/ # the harness registry, the default of each harness method, shared modules
|
|
198
|
+
shared/harnesses/ # one file per harness: commands, role file template, quirks
|
|
194
199
|
scripts/
|
|
195
200
|
check.rb # repo consistency check (UDA sync, markers, script signatures)
|
|
196
201
|
templates/
|
|
197
202
|
roles.yml # role definitions + model hints
|
|
198
203
|
role-stub.yml.erb # stub that maf role add writes
|
|
199
204
|
workflows/ # default workflows: simple, plan-review, tdd, panel
|
|
200
|
-
opencode.md.erb # role file templates per harness
|
|
201
|
-
claude.md.erb
|
|
202
|
-
codex.md.erb
|
|
203
|
-
hermes.md.erb
|
|
204
205
|
assets/
|
|
205
206
|
coord # coordination CLI (Ruby)
|
|
206
207
|
dispatcher # polls task board + inbox, starts one-shot agents (Ruby)
|
|
@@ -219,6 +220,7 @@ multi_agent_flow/
|
|
|
219
220
|
maf_test.rb # tests for the maf command
|
|
220
221
|
dashboard_test.rb # tests for the dashboard data
|
|
221
222
|
dispatcher_test.rb # tests for the dispatcher
|
|
223
|
+
harnesses/ # one test file per harness, and the contract test for all harnesses
|
|
222
224
|
uninstaller_test.rb # tests for uninstall.rb
|
|
223
225
|
migrate_test.rb # tests for migrate.rb
|
|
224
226
|
roles_workflow_test.rb # tests for project roles and the workflow
|
|
@@ -230,6 +232,7 @@ multi_agent_flow/
|
|
|
230
232
|
hook_session_test.rb # tests for session isolation
|
|
231
233
|
hook_config_test.rb # tests for project hooks
|
|
232
234
|
worker_control_test.rb # tests for maf worker
|
|
235
|
+
worker_list_test.rb # tests for maf worker list
|
|
233
236
|
untrack_test.rb # tests for maf untrack
|
|
234
237
|
analyst_test.rb # tests for the analyst
|
|
235
238
|
shared_test.rb # tests for lib/maf/shared/
|
|
@@ -316,8 +319,8 @@ bundle exec rake build # writes pkg/maf-VERSION.gem
|
|
|
316
319
|
bundle exec rake install # builds and installs the gem
|
|
317
320
|
```
|
|
318
321
|
|
|
319
|
-
To release,
|
|
320
|
-
|
|
322
|
+
To release, bump `Maf::VERSION`, merge, then tag `main` with `vX.Y.Z` and push the tag.
|
|
323
|
+
See **[docs/releasing.md](docs/releasing.md)** for the full steps, the one-time setup, and the fixes for a failed release.
|
|
321
324
|
|
|
322
325
|
RuboCop (pinned in the `Gemfile`) uses `.rubocop.yml`. It sets the
|
|
323
326
|
size rules: a class at most 100 lines, a method at most 5 lines and 4
|
|
@@ -335,6 +338,20 @@ them when you run a test file directly, so a test never writes to that board:
|
|
|
335
338
|
env -u TASKRC -u COORD_DIR -u COORD_ROLE -u COORD_WORKER ruby test/coord_test.rb
|
|
336
339
|
```
|
|
337
340
|
|
|
341
|
+
`rake mutant` runs the mutation tests with [mutant](https://github.com/mbj/mutant).
|
|
342
|
+
Mutant changes the code in `lib/` and runs the tests again. A change that no test
|
|
343
|
+
finds is an alive mutation: add a test for it. Mutant needs Ruby 3.3 or later.
|
|
344
|
+
It is free for open source projects (`usage: opensource` in `.mutant.yml`).
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
bundle exec rake mutant # the subjects in .mutant.yml
|
|
348
|
+
bundle exec rake mutant SUBJECT='Maf::Budget*' # other subjects
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Mutant runs only the test classes that name their code with `cover`. To add a
|
|
352
|
+
subject, require `test/mutant_cover.rb` in the test file, add `cover "Maf::X*"` to
|
|
353
|
+
the test class, and add `Maf::X*` to `matcher.subjects` in `.mutant.yml`.
|
|
354
|
+
|
|
338
355
|
```sh
|
|
339
356
|
ruby test/coord_test.rb # covers the coord CLI
|
|
340
357
|
ruby test/installer_test.rb # covers bootstrap.rb, flow.rb, setup_agent.rb
|
|
@@ -352,6 +369,7 @@ ruby test/dispatcher_test.rb # covers the dispatcher
|
|
|
352
369
|
ruby test/board_watch_test.rb # covers the board watcher
|
|
353
370
|
ruby test/context_watch_test.rb # covers the context-watch hook
|
|
354
371
|
ruby test/worker_control_test.rb # covers maf worker
|
|
372
|
+
ruby test/worker_list_test.rb # covers maf worker list
|
|
355
373
|
ruby test/untrack_test.rb # covers maf untrack
|
|
356
374
|
ruby test/analyst_test.rb # covers the analyst
|
|
357
375
|
ruby test/shared_test.rb # covers lib/maf/shared/
|
data/assets/agents-contract.md
CHANGED
|
@@ -31,6 +31,8 @@ coord claim ID | start-task ID | done ID # take a task, check out its branch
|
|
|
31
31
|
coord annotate ID TEXT # add a note to a task
|
|
32
32
|
coord lesson ID dead_end|corrected TEXT # an approach that failed, or the right way: the graph keeps it
|
|
33
33
|
coord escalate [--task ID] TEXT # a problem you cannot fix: the project manager asks the user
|
|
34
|
+
coord block ID --kind K TEXT # you cannot finish: K is needs_input, dependency, or capability
|
|
35
|
+
coord unblock ID # lead roles: the blocker is gone; the task returns to coord next
|
|
34
36
|
coord msg --from A [--task ID] [--fyi] TO TEXT # message a role or a worker; --task also notes the task
|
|
35
37
|
# --fyi: no run starts; the next run of TO reads it
|
|
36
38
|
coord broadcast --from A [--to workers|leads|all] TEXT
|
data/assets/analyst
CHANGED
|
@@ -20,9 +20,16 @@
|
|
|
20
20
|
require "json"
|
|
21
21
|
require "fileutils"
|
|
22
22
|
require "open3"
|
|
23
|
+
require "rbconfig"
|
|
23
24
|
require "optparse"
|
|
24
25
|
require "tmpdir"
|
|
25
26
|
require "time"
|
|
27
|
+
begin
|
|
28
|
+
require_relative "../lib/maf/shared/harness"
|
|
29
|
+
require_relative "../lib/maf/shared/sandbox"
|
|
30
|
+
rescue LoadError
|
|
31
|
+
abort "analyst: the maf shared library is missing. Run maf update."
|
|
32
|
+
end
|
|
26
33
|
|
|
27
34
|
module Analyst
|
|
28
35
|
HISTORY = 20
|
|
@@ -71,57 +78,14 @@ module Analyst
|
|
|
71
78
|
Call.new(kind || command.sub(SETUP, "").split.first.to_s, text, size.to_i)
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
# Transcripts reads the tool calls of one session from the files of the
|
|
81
|
+
# Transcripts reads the tool calls of one session from the files of the
|
|
82
|
+
# harness (Harness#transcript_calls).
|
|
75
83
|
module Transcripts
|
|
76
84
|
def self.calls(harness, id)
|
|
77
85
|
return [] if id.to_s.empty?
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.lines(path) = path ? File.foreach(path).filter_map { |line| JSON.parse(line) rescue nil } : []
|
|
84
|
-
|
|
85
|
-
# Claude Code: a tool_use block, then a tool_result block with the same ID.
|
|
86
|
-
def self.claude(id)
|
|
87
|
-
events = lines(Dir.glob(File.join(Dir.home, ".claude", "projects", "*", "#{id}.jsonl")).first)
|
|
88
|
-
blocks = events.flat_map { |event| Array(event.dig("message", "content")).select { |block| block.is_a?(Hash) } }
|
|
89
|
-
sizes = blocks.select { |block| block["type"] == "tool_result" }
|
|
90
|
-
.to_h { |block| [block["tool_use_id"], block["content"].to_s.size] }
|
|
91
|
-
blocks.select { |block| block["type"] == "tool_use" }
|
|
92
|
-
.map { |block| Analyst.call(block["name"], block.dig("input", "command"), sizes[block["id"]]) }
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Codex: a custom_tool_call or function_call, then its output with the same call_id.
|
|
96
|
-
def self.codex(id)
|
|
97
|
-
events = lines(Dir.glob(File.join(Dir.home, ".codex", "sessions", "**", "rollout-*#{id}.jsonl")).first)
|
|
98
|
-
payloads = events.map { |event| event["payload"] || {} }
|
|
99
|
-
sizes = payloads.select { |p| p["type"].to_s.end_with?("_output") }
|
|
100
|
-
.to_h { |p| [p["call_id"], p["output"].to_s.size] }
|
|
101
|
-
payloads.select { |p| %w[custom_tool_call function_call].include?(p["type"]) }
|
|
102
|
-
.map { |p| Analyst.call(p["name"], codex_command(p), sizes[p["call_id"]]) }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def self.codex_command(payload)
|
|
106
|
-
text = (payload["input"] || payload["arguments"]).to_s
|
|
107
|
-
text[/cmd["']?\s*:\s*["']((?:[^"'\\]|\\.)*)/, 1] || text
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# opencode keeps its sessions in a SQLite database. The sqlite3 CLI reads it.
|
|
111
|
-
def self.opencode(id)
|
|
112
|
-
db = File.join(Dir.home, ".local", "share", "opencode", "opencode.db")
|
|
113
|
-
return [] unless File.exist?(db) && id.match?(/\A[\w-]+\z/)
|
|
114
|
-
|
|
115
|
-
out, status = Open3.capture2("sqlite3", "-readonly", "-json", db, opencode_query(id))
|
|
116
|
-
status.success? ? JSON.parse(out.empty? ? "[]" : out).map { |row| Analyst.call(*row.values) } : []
|
|
117
|
-
rescue SystemCallError, JSON::ParserError
|
|
118
|
-
[]
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def self.opencode_query(id)
|
|
122
|
-
"select json_extract(data,'$.tool') tool, json_extract(data,'$.state.input.command') command, " \
|
|
123
|
-
"length(json_extract(data,'$.state.output')) size from part where session_id = '#{id}' " \
|
|
124
|
-
"and json_extract(data,'$.type') = 'tool'"
|
|
87
|
+
calls = Maf::Shared::Harness[harness]&.transcript_calls(id) || []
|
|
88
|
+
calls.map { |name, command, size| Analyst.call(name, command, size) }
|
|
125
89
|
end
|
|
126
90
|
end
|
|
127
91
|
|
|
@@ -169,8 +133,10 @@ module Analyst
|
|
|
169
133
|
|
|
170
134
|
# Model sends the prompt to the analyst command and reads the hints.
|
|
171
135
|
module Model
|
|
172
|
-
|
|
173
|
-
|
|
136
|
+
NEW_SESSION = [RbConfig.ruby, "-e", "Process.setsid; exec(*ARGV)"].freeze
|
|
137
|
+
|
|
138
|
+
def self.ask(command, prompt, coord)
|
|
139
|
+
out, err, status = run(command, prompt, coord)
|
|
174
140
|
raise "#{command.first} failed: #{(err.empty? ? out : err).strip[0, 200]}" unless status.success?
|
|
175
141
|
|
|
176
142
|
reply = JSON.parse(out) rescue nil
|
|
@@ -178,6 +144,18 @@ module Analyst
|
|
|
178
144
|
[hints(reply["result"] || out), reply["total_cost_usd"]]
|
|
179
145
|
end
|
|
180
146
|
|
|
147
|
+
def self.run(command, prompt, coord)
|
|
148
|
+
sandbox = Maf::Shared::Sandbox
|
|
149
|
+
wrapped = [*NEW_SESSION, *sandbox.wrap(command, harness(command), coord_dir: coord)]
|
|
150
|
+
run = -> { Open3.capture3(MODEL_ENV, *wrapped, stdin_data: prompt, chdir: Dir.tmpdir) }
|
|
151
|
+
sandbox.keeping_links(harness(command), coord_dir: coord, &run)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.harness(command)
|
|
155
|
+
name = File.basename(command.first.to_s)
|
|
156
|
+
Maf::Shared::Harness.all.find { |harness| harness.command == name } || Maf::Shared::Harness::Base.new
|
|
157
|
+
end
|
|
158
|
+
|
|
181
159
|
def self.hints(text)
|
|
182
160
|
data = JSON.parse(text.to_s[/\{\s*"hints".*\}/m].to_s)
|
|
183
161
|
Array(data["hints"]).first(3).filter_map { |hint| clean(hint) }
|
|
@@ -214,9 +192,9 @@ module Analyst
|
|
|
214
192
|
return puts(prompt) if @print
|
|
215
193
|
|
|
216
194
|
write("running" => true)
|
|
217
|
-
hints, cost = Model.ask(command, prompt)
|
|
195
|
+
hints, cost = Model.ask(command, prompt, @coord)
|
|
218
196
|
write("hints" => hints, "cost_usd" => cost)
|
|
219
|
-
rescue StandardError => e
|
|
197
|
+
rescue StandardError, SystemExit => e
|
|
220
198
|
write("error" => e.message)
|
|
221
199
|
end
|
|
222
200
|
|