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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +174 -0
  3. data/README.md +29 -11
  4. data/assets/agents-contract.md +2 -0
  5. data/assets/analyst +29 -51
  6. data/assets/coord +168 -41
  7. data/assets/dashboard +1 -5
  8. data/assets/dispatcher +127 -298
  9. data/assets/harness-hooks/context-watch.rb +5 -4
  10. data/assets/lima/maf.yaml.erb +61 -0
  11. data/install.md +6 -1
  12. data/lib/maf/bootstrap/claude_settings.rb +17 -9
  13. data/lib/maf/bootstrap/script_planner.rb +1 -1
  14. data/lib/maf/bootstrap.rb +4 -0
  15. data/lib/maf/cli.rb +22 -9
  16. data/lib/maf/flow/agent_links.rb +2 -2
  17. data/lib/maf/flow/bootstrapper.rb +5 -5
  18. data/lib/maf/flow/codex_hooks.rb +12 -1
  19. data/lib/maf/flow/effort_report.rb +20 -0
  20. data/lib/maf/flow/generator.rb +3 -2
  21. data/lib/maf/flow/harness_linker.rb +2 -2
  22. data/lib/maf/flow/hermes_env_setup.rb +46 -0
  23. data/lib/maf/flow/hook_files.rb +6 -0
  24. data/lib/maf/flow/hook_installer.rb +6 -20
  25. data/lib/maf/flow/manifest.rb +3 -3
  26. data/lib/maf/flow/mcp_config.rb +8 -22
  27. data/lib/maf/flow/mcp_installer.rb +3 -3
  28. data/lib/maf/flow/models.rb +14 -38
  29. data/lib/maf/flow/options.rb +33 -5
  30. data/lib/maf/flow/prompt_builder.rb +7 -14
  31. data/lib/maf/flow/prompt_text.rb +13 -14
  32. data/lib/maf/flow/report.rb +8 -9
  33. data/lib/maf/flow/role_catalog.rb +1 -1
  34. data/lib/maf/flow/role_files.rb +7 -22
  35. data/lib/maf/flow/roster.rb +6 -3
  36. data/lib/maf/flow/validator.rb +2 -2
  37. data/lib/maf/flow.rb +7 -21
  38. data/lib/maf/menu.rb +49 -15
  39. data/lib/maf/migrate/role_files.rb +3 -1
  40. data/lib/maf/retire.rb +42 -6
  41. data/lib/maf/setup_agent/args.rb +16 -10
  42. data/lib/maf/setup_agent/dispatch.rb +3 -3
  43. data/lib/maf/setup_agent/launcher.rb +16 -65
  44. data/lib/maf/setup_agent/manifest.rb +9 -3
  45. data/lib/maf/setup_agent/pty_relay.rb +75 -0
  46. data/lib/maf/setup_agent/role_file.rb +1 -1
  47. data/lib/maf/setup_agent/runtime_hooks.rb +1 -12
  48. data/lib/maf/setup_agent.rb +11 -13
  49. data/lib/maf/shared/harness/base.rb +57 -0
  50. data/lib/maf/shared/harness/defaults.rb +147 -0
  51. data/lib/maf/shared/harness/output.rb +55 -0
  52. data/lib/maf/shared/harness/traits.rb +35 -0
  53. data/lib/maf/shared/harness/vm_credential.rb +36 -0
  54. data/lib/maf/shared/harness.rb +12 -0
  55. data/lib/maf/shared/harnesses/claude.rb +137 -0
  56. data/lib/maf/shared/harnesses/codex.rb +202 -0
  57. data/lib/maf/shared/harnesses/cursor.rb +120 -0
  58. data/lib/maf/shared/harnesses/hermes.rb +122 -0
  59. data/lib/maf/shared/harnesses/opencode.rb +192 -0
  60. data/lib/maf/shared/processes.rb +20 -0
  61. data/lib/maf/shared/roles.rb +4 -0
  62. data/lib/maf/shared/sandbox/bwrap.rb +128 -0
  63. data/lib/maf/shared/sandbox/harness_files.rb +50 -0
  64. data/lib/maf/shared/sandbox/links.rb +28 -0
  65. data/lib/maf/shared/sandbox/paths.rb +124 -0
  66. data/lib/maf/shared/sandbox/references.rb +64 -0
  67. data/lib/maf/shared/sandbox/seatbelt.rb +86 -0
  68. data/lib/maf/shared/sandbox.rb +84 -0
  69. data/lib/maf/team.rb +5 -4
  70. data/lib/maf/team_command.rb +5 -3
  71. data/lib/maf/uninstall/manifest.rb +10 -8
  72. data/lib/maf/uninstall/mcp_entries.rb +4 -5
  73. data/lib/maf/uninstall/notes.rb +13 -2
  74. data/lib/maf/uninstall/role_files.rb +11 -10
  75. data/lib/maf/uninstall/runner.rb +4 -3
  76. data/lib/maf/uninstall/scripts.rb +1 -1
  77. data/lib/maf/uninstall/vm_files.rb +16 -0
  78. data/lib/maf/uninstall.rb +20 -5
  79. data/lib/maf/untrack.rb +12 -25
  80. data/lib/maf/version.rb +1 -1
  81. data/lib/maf/vm/login.rb +52 -0
  82. data/lib/maf/vm/options.rb +41 -0
  83. data/lib/maf/vm/template.rb +44 -0
  84. data/lib/maf/vm.rb +94 -0
  85. data/lib/maf/worker_control.rb +16 -8
  86. data/lib/maf/worker_list.rb +83 -0
  87. data/lib/maf/workers.rb +27 -0
  88. data/templates/roles.yml +14 -0
  89. metadata +33 -11
  90. data/lib/maf/setup_agent/hermes_launcher.rb +0 -34
  91. data/lib/maf/setup_agent/hermes_skill.rb +0 -26
  92. data/templates/claude.md.erb +0 -16
  93. data/templates/codex.md.erb +0 -7
  94. data/templates/hermes.md.erb +0 -12
  95. data/templates/opencode.md.erb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cdbedeaf3c68e4b2aa40939c8f006f846d0d044531f2c6d53e01cddf4d9e45d
4
- data.tar.gz: e90a24b66c78fd7cc493e493f722b25a5803430967b2cae0de46202befaf12b2
3
+ metadata.gz: 1d92abf8bbcca7ba5044ac6185534e883a55a1387e05460ac4142e1d89f3d13e
4
+ data.tar.gz: 25eaa46e4ed438fef70a08841137fc87c7ede35dfe9c09ff4d053a351e11e9ef
5
5
  SHA512:
6
- metadata.gz: ee79aa5c928c7be62ed39cdcf2904f7905b6e33189c31f19b7badd522cd0600431d6b39faebf72c7f60caf951ddbb577efc6d2a1fc167145f4f2503665ffe256
7
- data.tar.gz: 0aa9875967605035e251dab51a6ad097dc80c428cda40a8db8abfad99d0cb5381d92da1c5b7e9dc5b4daa5d7263e670655150100a40f70fb0ec9369b35f868fe
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, change `Maf::VERSION` in `lib/maf/version.rb` and add a
320
- `CHANGELOG.md` entry.
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/
@@ -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 harness.
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
- { "claude" => -> { claude(id) }, "codex" => -> { codex(id) }, "opencode" => -> { opencode(id) } }
80
- .fetch(harness, -> { [] }).call
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
- def self.ask(command, prompt)
173
- out, err, status = Open3.capture3(MODEL_ENV, *command, stdin_data: prompt, chdir: Dir.tmpdir)
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