maf 0.1.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 +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +411 -0
- data/assets/agents-contract.md +80 -0
- data/assets/analyst +240 -0
- data/assets/coord +2936 -0
- data/assets/dashboard +553 -0
- data/assets/dashboard.html +341 -0
- data/assets/dispatcher +1687 -0
- data/assets/doc-graph-refresh +286 -0
- data/assets/env.sh +6 -0
- data/assets/git-hooks/post-commit +7 -0
- data/assets/git-hooks/post-merge +7 -0
- data/assets/git-hooks/pre-commit +32 -0
- data/assets/harness-hooks/board-watch-opencode.js +87 -0
- data/assets/harness-hooks/board-watch.rb +286 -0
- data/assets/harness-hooks/context-watch.rb +268 -0
- data/assets/harness-hooks/next-task-hermes.sh +48 -0
- data/assets/harness-hooks/next-task.rb +97 -0
- data/assets/harness-hooks/session-guard.rb +128 -0
- data/assets/taskrc.append +11 -0
- data/assets/vault +224 -0
- data/assets/worktree-env.example.rb +26 -0
- data/exe/maf +14 -0
- data/install.md +326 -0
- data/lib/maf/bootstrap/claude_settings.rb +55 -0
- data/lib/maf/bootstrap/dependencies.rb +37 -0
- data/lib/maf/bootstrap/git_hook_planner.rb +68 -0
- data/lib/maf/bootstrap/global_taskrc_warning.rb +33 -0
- data/lib/maf/bootstrap/graph_home.rb +62 -0
- data/lib/maf/bootstrap/hook_merger.rb +53 -0
- data/lib/maf/bootstrap/installer.rb +66 -0
- data/lib/maf/bootstrap/layout_planner.rb +18 -0
- data/lib/maf/bootstrap/marked_block.rb +44 -0
- data/lib/maf/bootstrap/memory_branch.rb +77 -0
- data/lib/maf/bootstrap/options.rb +34 -0
- data/lib/maf/bootstrap/project.rb +77 -0
- data/lib/maf/bootstrap/script_planner.rb +81 -0
- data/lib/maf/bootstrap/text_planner.rb +42 -0
- data/lib/maf/bootstrap/vault_starter.rb +41 -0
- data/lib/maf/bootstrap/writer.rb +69 -0
- data/lib/maf/bootstrap.rb +162 -0
- data/lib/maf/budget.rb +59 -0
- data/lib/maf/cli.rb +135 -0
- data/lib/maf/env_exclude.rb +23 -0
- data/lib/maf/flow/agent_links.rb +79 -0
- data/lib/maf/flow/bootstrapper.rb +36 -0
- data/lib/maf/flow/codex_hooks.rb +50 -0
- data/lib/maf/flow/generator.rb +63 -0
- data/lib/maf/flow/harness_linker.rb +37 -0
- data/lib/maf/flow/hermes_hook.rb +48 -0
- data/lib/maf/flow/hermes_hook_setup.rb +69 -0
- data/lib/maf/flow/hook_files.rb +16 -0
- data/lib/maf/flow/hook_installer.rb +33 -0
- data/lib/maf/flow/legacy_codex_hook.rb +71 -0
- data/lib/maf/flow/manifest.rb +51 -0
- data/lib/maf/flow/mcp_config.rb +72 -0
- data/lib/maf/flow/mcp_installer.rb +45 -0
- data/lib/maf/flow/models.rb +61 -0
- data/lib/maf/flow/options.rb +65 -0
- data/lib/maf/flow/prompt_builder.rb +85 -0
- data/lib/maf/flow/prompt_text.rb +263 -0
- data/lib/maf/flow/report.rb +89 -0
- data/lib/maf/flow/role_catalog.rb +40 -0
- data/lib/maf/flow/role_files.rb +72 -0
- data/lib/maf/flow/role_stub.rb +38 -0
- data/lib/maf/flow/roster.rb +28 -0
- data/lib/maf/flow/validator.rb +38 -0
- data/lib/maf/flow/workflow.rb +28 -0
- data/lib/maf/flow.rb +84 -0
- data/lib/maf/local_exclude.rb +53 -0
- data/lib/maf/menu.rb +101 -0
- data/lib/maf/migrate/moves.rb +44 -0
- data/lib/maf/migrate/rewrites.rb +53 -0
- data/lib/maf/migrate/role_files.rb +35 -0
- data/lib/maf/migrate/runner.rb +66 -0
- data/lib/maf/migrate/worktrees.rb +65 -0
- data/lib/maf/migrate.rb +62 -0
- data/lib/maf/prompt.rb +40 -0
- data/lib/maf/retire.rb +116 -0
- data/lib/maf/role_limits.rb +49 -0
- data/lib/maf/setup_agent/args.rb +57 -0
- data/lib/maf/setup_agent/dispatch.rb +44 -0
- data/lib/maf/setup_agent/hermes_launcher.rb +34 -0
- data/lib/maf/setup_agent/hermes_skill.rb +26 -0
- data/lib/maf/setup_agent/launcher.rb +85 -0
- data/lib/maf/setup_agent/manifest.rb +35 -0
- data/lib/maf/setup_agent/project.rb +9 -0
- data/lib/maf/setup_agent/role_file.rb +30 -0
- data/lib/maf/setup_agent/runtime_hooks.rb +37 -0
- data/lib/maf/setup_agent/worktree.rb +50 -0
- data/lib/maf/setup_agent.rb +111 -0
- data/lib/maf/shared/git_exclude.rb +33 -0
- data/lib/maf/shared/git_identity.rb +41 -0
- data/lib/maf/shared/peak_rate.rb +20 -0
- data/lib/maf/shared/processes.rb +31 -0
- data/lib/maf/shared/project.rb +34 -0
- data/lib/maf/shared/roles.rb +19 -0
- data/lib/maf/team.rb +114 -0
- data/lib/maf/team_command.rb +73 -0
- data/lib/maf/uninstall/claude_settings.rb +40 -0
- data/lib/maf/uninstall/codex_hooks.rb +18 -0
- data/lib/maf/uninstall/commit_guard.rb +16 -0
- data/lib/maf/uninstall/coordination.rb +15 -0
- data/lib/maf/uninstall/doc_graph_hooks.rb +38 -0
- data/lib/maf/uninstall/git.rb +13 -0
- data/lib/maf/uninstall/local_files.rb +33 -0
- data/lib/maf/uninstall/manifest.rb +29 -0
- data/lib/maf/uninstall/marked_files.rb +37 -0
- data/lib/maf/uninstall/mcp_entries.rb +43 -0
- data/lib/maf/uninstall/notes.rb +31 -0
- data/lib/maf/uninstall/owned.rb +12 -0
- data/lib/maf/uninstall/role_files.rb +51 -0
- data/lib/maf/uninstall/runner.rb +67 -0
- data/lib/maf/uninstall/scripts.rb +35 -0
- data/lib/maf/uninstall/vault_watcher.rb +21 -0
- data/lib/maf/uninstall/worktrees.rb +30 -0
- data/lib/maf/uninstall.rb +59 -0
- data/lib/maf/untrack.rb +90 -0
- data/lib/maf/version.rb +5 -0
- data/lib/maf/worker_archive.rb +63 -0
- data/lib/maf/worker_control.rb +137 -0
- data/lib/maf/workers.rb +37 -0
- data/lib/maf.rb +5 -0
- data/templates/claude.md.erb +16 -0
- data/templates/codex.md.erb +7 -0
- data/templates/hermes.md.erb +12 -0
- data/templates/opencode.md.erb +24 -0
- data/templates/role-stub.yml.erb +15 -0
- data/templates/roles.yml +289 -0
- data/templates/workflows/panel.md +20 -0
- data/templates/workflows/plan-review.md +9 -0
- data/templates/workflows/simple.md +4 -0
- data/templates/workflows/tdd.md +8 -0
- metadata +193 -0
data/install.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Multi-agent flow - installation instruction
|
|
2
|
+
|
|
3
|
+
This file is an instruction for an AI coding agent. Read the whole file. Then do
|
|
4
|
+
the steps in order.
|
|
5
|
+
|
|
6
|
+
You set up a shared coordination layer for multiple coding agents in one project.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Step 1 - Install the maf command
|
|
11
|
+
|
|
12
|
+
Check the tools. `maf` and the `coord` tool are Ruby scripts.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
ruby -v
|
|
16
|
+
git --version
|
|
17
|
+
task --version
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If Ruby is older than 3.0, stop. Tell the user to install Ruby 3.0 or later.
|
|
21
|
+
If `task` is missing, install it. On macOS, run `brew install task`.
|
|
22
|
+
On Linux, run `sudo apt-get install taskwarrior`.
|
|
23
|
+
|
|
24
|
+
Install the maf gem:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
gem install maf
|
|
28
|
+
maf version
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If `maf version` works already, do not install the gem again.
|
|
32
|
+
If `maf version` fails after the install, add the gem bin folder to `PATH`.
|
|
33
|
+
`gem env` shows the folder under EXECUTABLE DIRECTORY.
|
|
34
|
+
|
|
35
|
+
The flow folder holds the templates. Call that folder `FLOW`:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
export FLOW="$(dirname "$(dirname "$(gem which maf)")")"
|
|
39
|
+
ls "$FLOW/templates"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If maf runs from a clone of the repository, `FLOW` is the clone folder.
|
|
43
|
+
|
|
44
|
+
## Step 2 - Ask the user for the project folder
|
|
45
|
+
|
|
46
|
+
Ask: "Which project folder should use the multi-agent flow?"
|
|
47
|
+
|
|
48
|
+
Use the answer as `PROJECT`. The folder must exist and must be a git repository.
|
|
49
|
+
Run all `maf` commands below in `PROJECT`:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
cd "$PROJECT"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Step 3 - Ask the user for harnesses and roles
|
|
56
|
+
|
|
57
|
+
Ask: "Which agent harnesses do you want to use? For example: opencode, Claude
|
|
58
|
+
Code, Codex, Hermes."
|
|
59
|
+
|
|
60
|
+
Show the available roles. Run:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
maf roles
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Tell the user about the `project-manager` role: the user talks to it, it sends
|
|
67
|
+
goals to the architect (`coord goal add`), and the architect reports back to it.
|
|
68
|
+
Recommend it whenever the architect would otherwise take requests directly
|
|
69
|
+
from the user. Ask the user to map roles to harnesses. Example answer:
|
|
70
|
+
|
|
71
|
+
- claude -> project-manager
|
|
72
|
+
- claude -> architect
|
|
73
|
+
- opencode -> backend-developer
|
|
74
|
+
- opencode -> frontend-developer
|
|
75
|
+
- codex -> reviewer
|
|
76
|
+
- hermes -> tester
|
|
77
|
+
|
|
78
|
+
You may run more than one role in one harness. Open one session per role.
|
|
79
|
+
|
|
80
|
+
## Step 3b - Ask the user for the number of workers
|
|
81
|
+
|
|
82
|
+
Ask: "How many workers do you want for each worker role?"
|
|
83
|
+
|
|
84
|
+
A worker role is a role other than `project-manager` and `architect`.
|
|
85
|
+
Default: one worker per role.
|
|
86
|
+
Remember the answer. Step 7 uses it for the `maf start` commands.
|
|
87
|
+
|
|
88
|
+
## Step 3c - Ask the user for custom roles
|
|
89
|
+
|
|
90
|
+
Ask: "Do you need a role that is not in the list? Describe it in one sentence."
|
|
91
|
+
|
|
92
|
+
If the user needs no custom role, skip this step.
|
|
93
|
+
If the user needs a custom role, do these steps for each role:
|
|
94
|
+
|
|
95
|
+
1. Run `maf role add NAME`. The command writes a stub into `.maf/roles.yml`.
|
|
96
|
+
2. Replace each `TODO` line in the stub. Use the four duty parts: focus,
|
|
97
|
+
checks, done condition, and avoid. Write them in Simplified Technical English.
|
|
98
|
+
3. Use `NAME` as a role in Step 5.
|
|
99
|
+
|
|
100
|
+
A role in `.maf/roles.yml` with the name of a built-in role replaces the built-in role.
|
|
101
|
+
`maf roles` shows the source of each role.
|
|
102
|
+
|
|
103
|
+
## Step 3d - Ask the user for the workflow
|
|
104
|
+
|
|
105
|
+
The workflow tells the architect in which order to create tasks.
|
|
106
|
+
Ask: "Which workflow do you want? Choose one, or describe your own in your own words."
|
|
107
|
+
|
|
108
|
+
| Name | Stages |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `simple` | Implement, review, merge. |
|
|
111
|
+
| `plan-review` | Plan, review the plan, implement, review, merge. |
|
|
112
|
+
| `tdd` | Plan, review the plan, write specs, implement, review, merge. |
|
|
113
|
+
| `panel` | Plan, three reviewers attack the plan, implement, review, three reviewers attack the goal diff, merge. |
|
|
114
|
+
|
|
115
|
+
If the user chooses `panel`, add the roles `reviewer`, `skeptic`, and `auditor` in Step 5.
|
|
116
|
+
The three roles review the same plan and the same goal diff. Each role checks a different area.
|
|
117
|
+
|
|
118
|
+
If the user chooses a name, copy the file `FLOW/templates/workflows/<name>.md`
|
|
119
|
+
to `.maf/workflow.md`.
|
|
120
|
+
If the user describes a workflow, write the description to `.maf/workflow.md` as
|
|
121
|
+
stage instructions. Use Simplified Technical English. Write one instruction per
|
|
122
|
+
sentence. Write "Stage N." at the start of each stage. State the gate that ends
|
|
123
|
+
each stage. Do not create tasks for a stage in advance: the architect creates
|
|
124
|
+
the tasks of the next stage after the gate passes.
|
|
125
|
+
If the user wants no workflow, create no `.maf/workflow.md`.
|
|
126
|
+
|
|
127
|
+
Only the architect reads `.maf/workflow.md`. Other roles do not see it.
|
|
128
|
+
Run `maf update` after each later change of the file.
|
|
129
|
+
Step 5 reads `.maf/roles.yml` and `.maf/workflow.md`, so write both before Step 5.
|
|
130
|
+
|
|
131
|
+
## Step 4 - Ask the user for a model per role
|
|
132
|
+
|
|
133
|
+
Do not choose models yourself. Ask the user.
|
|
134
|
+
|
|
135
|
+
Show the model hint from step 3 for each role. Explain that the hint is only a
|
|
136
|
+
recommendation. The user knows what runs on the machine.
|
|
137
|
+
|
|
138
|
+
The user may skip a model. If skipped, the harness default applies.
|
|
139
|
+
|
|
140
|
+
If the workflow is `panel`, propose a different model family for each of
|
|
141
|
+
`reviewer`, `skeptic`, and `auditor`. Example: Claude, GPT, and Gemini.
|
|
142
|
+
Different models miss different defects. The user makes the final choice.
|
|
143
|
+
|
|
144
|
+
## Step 5 - Add the agents
|
|
145
|
+
|
|
146
|
+
Give one `HARNESS:ROLE` argument per role. Add one `--model ROLE=MODEL` flag
|
|
147
|
+
per chosen model.
|
|
148
|
+
|
|
149
|
+
Preview first. This writes nothing.
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
maf add claude:project-manager claude:architect \
|
|
153
|
+
opencode:backend-developer opencode:frontend-developer \
|
|
154
|
+
codex:reviewer hermes:tester \
|
|
155
|
+
--model project-manager=claude-opus-5-5 \
|
|
156
|
+
--model architect=claude-opus-5-5 \
|
|
157
|
+
--check
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then run the same command without `--check`.
|
|
161
|
+
|
|
162
|
+
`maf add` does these things:
|
|
163
|
+
|
|
164
|
+
1. Sets up the coordination layer.
|
|
165
|
+
2. Writes a role file for each role, in the format of its harness.
|
|
166
|
+
3. Writes a manifest at `$PROJECT/.maf/config.json`.
|
|
167
|
+
4. Writes the graphify MCP server into `.maf/mcp/` for Claude Code and opencode.
|
|
168
|
+
`maf start` passes the file to the harness. The project's `.mcp.json` and `opencode.json` stay as they are.
|
|
169
|
+
For Codex and Hermes, `maf add` prints a command. Run it to add the server.
|
|
170
|
+
To turn the server off, set `"mcp": false` in `.maf/config.json`.
|
|
171
|
+
|
|
172
|
+
`maf add` is idempotent. It skips files that are already correct.
|
|
173
|
+
|
|
174
|
+
## Step 5b - Turn on the Hermes hook (Hermes roles only)
|
|
175
|
+
|
|
176
|
+
Skip this step when no role uses the Hermes harness.
|
|
177
|
+
|
|
178
|
+
`maf add` installs the hook script at `~/.hermes/agent-hooks/next-task.sh`.
|
|
179
|
+
Hermes runs it when a session ends. The hook resumes the session when the role
|
|
180
|
+
has unclaimed tasks.
|
|
181
|
+
|
|
182
|
+
The hook stays inactive until the Hermes config declares it and the user
|
|
183
|
+
approves it. `maf add` prints the commands. Run them in order.
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
hermes config set hooks.on_session_end '[{"command":"<script path>","timeout":30}]'
|
|
187
|
+
hermes chat --oneshot --accept-hooks -q ok
|
|
188
|
+
hermes hooks doctor
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Do not edit `~/.hermes/config.yaml` by hand. The file holds comments and markers
|
|
192
|
+
that a rewrite destroys.
|
|
193
|
+
|
|
194
|
+
The first command replaces the whole `on_session_end` list. If the list is not
|
|
195
|
+
empty, read it first with `hermes config get hooks.on_session_end`. Then set the
|
|
196
|
+
list with the existing entries plus the new entry.
|
|
197
|
+
|
|
198
|
+
The second command approves the hook one time. Hermes stores the consent for this
|
|
199
|
+
version of the script. A new script version needs a new approval.
|
|
200
|
+
|
|
201
|
+
Confirm that every check from `hermes hooks doctor` passes. Then continue.
|
|
202
|
+
|
|
203
|
+
## Step 5c - Set the Gemini key for the doc-graph refresh
|
|
204
|
+
|
|
205
|
+
`maf add` appends a flow block to the `post-commit` and `post-merge` git hooks.
|
|
206
|
+
A markdown commit or merge starts `.maf/bin/doc-graph-refresh` detached.
|
|
207
|
+
The script runs `graphify extract . --backend gemini` and re-exports
|
|
208
|
+
`graphify-out/obsidian/`. It needs `GEMINI_API_KEY`:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
export GEMINI_API_KEY=<key>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The hook starts no LLM call without the key. It logs a skip in
|
|
215
|
+
`.maf/coordination/doc-graph.log`. The hook never fails a commit.
|
|
216
|
+
|
|
217
|
+
## Step 6 - Verify
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
cd "$PROJECT"
|
|
221
|
+
source .maf/env.sh
|
|
222
|
+
coord init
|
|
223
|
+
coord status
|
|
224
|
+
maf agents
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`source .maf/env.sh` puts `.maf/bin` on `PATH`, so the shell finds `coord`.
|
|
228
|
+
|
|
229
|
+
Check that each role file exists:
|
|
230
|
+
|
|
231
|
+
- opencode: `.opencode/agents/<role>.md`
|
|
232
|
+
- Claude Code: `.claude/agents/<role>.md`
|
|
233
|
+
- Codex: `.codex/prompts/<role>.md`
|
|
234
|
+
- Hermes: `~/.hermes/skills/<project-name>-<role>/SKILL.md` (namespaced by
|
|
235
|
+
project; Hermes skills are global, so this keeps two projects with the
|
|
236
|
+
same role from overwriting each other's skill)
|
|
237
|
+
|
|
238
|
+
## Step 6b - Do not commit the flow
|
|
239
|
+
|
|
240
|
+
maf is a tool, not a part of the project. It lists `.maf/` and its links in
|
|
241
|
+
`.git/info/exclude`, so `git status` shows no maf file. Do not commit them.
|
|
242
|
+
The project needs at least one commit, because each worker worktree starts from a commit.
|
|
243
|
+
|
|
244
|
+
If `git ls-files .maf` lists files, an older maf version committed them. Run
|
|
245
|
+
`maf untrack`, review `git status`, and ask the user to commit the result.
|
|
246
|
+
|
|
247
|
+
## Step 7 - Report to the user
|
|
248
|
+
|
|
249
|
+
Report the generated files. Then print one `maf start` command for each worker.
|
|
250
|
+
Use the number of workers from Step 3b. A worker role with N workers gets the
|
|
251
|
+
names `ROLE_1` to `ROLE_N`. A role with one worker may use the bare role name.
|
|
252
|
+
Then give the user these instructions.
|
|
253
|
+
|
|
254
|
+
> For each claude, opencode, or codex agent, open a terminal in the project
|
|
255
|
+
> folder and run:
|
|
256
|
+
>
|
|
257
|
+
> maf start HARNESS ROLE[_WORKER] [model:PROVIDER/MODEL]
|
|
258
|
+
>
|
|
259
|
+
> Example, for this setup:
|
|
260
|
+
>
|
|
261
|
+
> maf start claude project-manager
|
|
262
|
+
> maf start claude architect
|
|
263
|
+
> maf start opencode backend-developer_1
|
|
264
|
+
> maf start opencode frontend-developer_1
|
|
265
|
+
> maf start codex reviewer
|
|
266
|
+
>
|
|
267
|
+
> To run an agent unattended, add `--dispatch`. The agent then starts only
|
|
268
|
+
> when there is work, and it exits when the work is done:
|
|
269
|
+
>
|
|
270
|
+
> maf start hermes tester --dispatch
|
|
271
|
+
>
|
|
272
|
+
> The architect never talks to the user. Run it with `--dispatch` unless the
|
|
273
|
+
> user wants to watch it: `maf start claude architect --dispatch`.
|
|
274
|
+
>
|
|
275
|
+
> This creates (or reuses) a worktree at `.maf/worktrees/<role>-<worker_id>`,
|
|
276
|
+
> sets `COORD_ROLE` and `COORD_WORKER`, and launches the harness there with
|
|
277
|
+
> its role loaded. To run several instances of one role, add a worker suffix:
|
|
278
|
+
> `backend-developer_1`, `backend-developer_2`. Claims are atomic, so they
|
|
279
|
+
> will not collide.
|
|
280
|
+
>
|
|
281
|
+
> Hermes loads the role as a skill (`--skills <project>-<role>`); the skill
|
|
282
|
+
> file at `~/.hermes/skills/<project>-<role>/SKILL.md` must have been generated
|
|
283
|
+
> by `maf add` first.
|
|
284
|
+
>
|
|
285
|
+
> Talk to the project manager session, not the architect. For example: "Build a
|
|
286
|
+
> task tracker app." The project manager sends the goal to the architect. The
|
|
287
|
+
> architect creates tasks. The workers pick them up. The architect reports back
|
|
288
|
+
> to the project manager, and the project manager reports back to you. If no
|
|
289
|
+
> `project-manager` role was set up, talk to the architect session directly
|
|
290
|
+
> instead.
|
|
291
|
+
|
|
292
|
+
## Domain documentation
|
|
293
|
+
|
|
294
|
+
Do not create `GLOSSARY.md` at install time. The project has no terms yet.
|
|
295
|
+
The project manager creates the first draft when the first term resolves.
|
|
296
|
+
The architect owns `GLOSSARY.md`. A worker that can edit files commits it on the goal branch.
|
|
297
|
+
Tell the user this in the report of Step 7.
|
|
298
|
+
Do not add a `GLOSSARY-MAP.md` unless the project has more than one bounded context.
|
|
299
|
+
ADRs use the decisions folder: `.agent/decisions/` if it exists, else `docs/decisions/`.
|
|
300
|
+
|
|
301
|
+
## Notes
|
|
302
|
+
|
|
303
|
+
- One writer per path. The task scope defines the paths. `coord add`/`conflicts`
|
|
304
|
+
warns on overlap; roles with `can_edit: false` also get a restricted tool grant
|
|
305
|
+
where the harness supports one (Claude Code, opencode, Hermes), and the git
|
|
306
|
+
`pre-commit` guard refuses their commits.
|
|
307
|
+
- Every generated role file requires Simplified Technical English in
|
|
308
|
+
`coord msg`, `coord annotate`, and task titles: one instruction per
|
|
309
|
+
sentence, active voice, named subject, no idioms.
|
|
310
|
+
- Take the `ollama` lock before a local model generation:
|
|
311
|
+
`coord with-lock ollama -- <command>`.
|
|
312
|
+
- Give each agent its own worktree so file changes never collide:
|
|
313
|
+
`coord worktree <role>` creates `.maf/worktrees/<role>-<worker_id>` (inside the
|
|
314
|
+
project, excluded from git) on branch `worker/<role>-<worker_id>`. In that worktree
|
|
315
|
+
run `source .maf/env.sh` first; it points `COORD_DIR` and `TASKRC` at the
|
|
316
|
+
main project, so every worktree shares one .maf/coordination/ dir and one task
|
|
317
|
+
board. `maf start` does all of this for you.
|
|
318
|
+
- Claude Code does not auto-load `.claude/agents/<role>.md` into an interactive
|
|
319
|
+
session (that file is a subagent definition, used via its Task tool, not the
|
|
320
|
+
session's own persona). `maf start claude <role>` works around this by
|
|
321
|
+
passing an initial prompt that tells the session to read and follow it.
|
|
322
|
+
- The user can add agents later with `maf add HARNESS:ROLE`.
|
|
323
|
+
The user can remove agents with `maf remove HARNESS:ROLE`.
|
|
324
|
+
`maf add` keeps the current agents.
|
|
325
|
+
- Codex and Hermes have no subagent files. Codex gets custom prompts. Hermes gets
|
|
326
|
+
skills, loaded via `--skills <project>-<role>`. Both work the same way in this flow.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Bootstrap
|
|
5
|
+
# ClaudeSettings plans and applies the Claude Code hooks in the settings
|
|
6
|
+
# file of the flow, .maf/claude/settings.json. `maf start` passes the file
|
|
7
|
+
# with --settings. Claude Code runs these hooks next to the project's own.
|
|
8
|
+
class ClaudeSettings
|
|
9
|
+
def initialize(project)
|
|
10
|
+
@project = project
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def plan
|
|
14
|
+
file = @project.path(CLAUDE_SETTINGS)
|
|
15
|
+
[@project.action(status(file), file, "#{file} (next-task + board-watch hooks)", :claude_stop_hook)]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure(file)
|
|
19
|
+
settings = read(file)
|
|
20
|
+
missing = missing_hooks(settings)
|
|
21
|
+
missing.each { |event, hook| add_hook(settings, event, hook) }
|
|
22
|
+
write(file, settings) unless missing.empty?
|
|
23
|
+
!missing.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def status(file)
|
|
29
|
+
File.exist?(file) && missing_hooks(read(file)).empty? ? :skip : :configure_claude_hook
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def read(file)
|
|
33
|
+
File.exist?(file) ? (JSON.parse(File.read(file)) rescue {}) : {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def missing_hooks(settings)
|
|
37
|
+
CLAUDE_HOOKS.reject { |event, hook| hook_entry?(settings, event, hook["command"]) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def hook_entry?(settings, event, command)
|
|
41
|
+
(settings.dig("hooks", event) || []).any? { |entry| entry.dig("hooks", 0, "command") == command }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add_hook(settings, event, hook)
|
|
45
|
+
settings["hooks"] ||= {}
|
|
46
|
+
(settings["hooks"][event] ||= []) << { "matcher" => "", "hooks" => [hook] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def write(file, data)
|
|
50
|
+
FileUtils.mkdir_p(File.dirname(file))
|
|
51
|
+
File.write(file, JSON.pretty_generate(data))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Bootstrap
|
|
5
|
+
# Dependencies reports required and optional tools. It installs the
|
|
6
|
+
# required tools only when the options ask for it.
|
|
7
|
+
class Dependencies
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def report
|
|
13
|
+
REQUIRED_DEPS.each { |bin, meta| report_required(bin, meta) }
|
|
14
|
+
OPTIONAL_DEPS.each { |bin, hint| report_optional(bin, hint) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def report_required(bin, meta)
|
|
20
|
+
return Bootstrap.say("#{bin}: present") if Bootstrap.which(bin)
|
|
21
|
+
return install(bin, meta) if @options.install_deps && !@options.check
|
|
22
|
+
|
|
23
|
+
Bootstrap.say("#{bin}: MISSING (required - #{meta[:why]}). Re-run with --install-deps or install it yourself.")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def install(bin, meta)
|
|
27
|
+
Bootstrap.say("#{bin}: missing -> installing (#{meta[:why]})")
|
|
28
|
+
meta[:install].call || abort("bootstrap: failed to install #{bin}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def report_optional(bin, hint)
|
|
32
|
+
state = Bootstrap.which(bin) ? "present" : "missing (optional - #{hint})"
|
|
33
|
+
Bootstrap.say("#{bin}: #{state}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Bootstrap
|
|
5
|
+
# GitHookPlanner plans the git hooks: the commit guard, and the flow block
|
|
6
|
+
# in the post-commit and post-merge hooks.
|
|
7
|
+
class GitHookPlanner
|
|
8
|
+
COMMIT_GUARD = "git-hooks/pre-commit"
|
|
9
|
+
# Hook event name -> the asset source symbol that holds the block.
|
|
10
|
+
DOC_GRAPH_HOOKS = { "post-commit" => :post_commit, "post-merge" => :post_merge }.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(project)
|
|
13
|
+
@project = project
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The commit guard blocks commits by roles with can_edit false. A foreign
|
|
17
|
+
# pre-commit hook stays, even with --force: it may run the project checks.
|
|
18
|
+
def commit_guard
|
|
19
|
+
dest = hook_path("pre-commit")
|
|
20
|
+
return [] unless dest
|
|
21
|
+
|
|
22
|
+
status = commit_guard_status(dest)
|
|
23
|
+
[@project.action(status, dest, @project.refuse_label(status, dest, FOREIGN_GUARD), COMMIT_GUARD)]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Append the flow block to the post-commit and post-merge hooks. A foreign
|
|
27
|
+
# hook (graphify installs one) stays; the flow owns only its block.
|
|
28
|
+
def doc_graph_hooks
|
|
29
|
+
DOC_GRAPH_HOOKS.map { |name, source| hook(name, source) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
FOREIGN_GUARD = "a foreign pre-commit hook; the commit guard is off"
|
|
35
|
+
|
|
36
|
+
def commit_guard_status(dest)
|
|
37
|
+
return :create unless File.exist?(dest)
|
|
38
|
+
return :refuse unless @project.ours?(dest, COMMIT_GUARD_SIGNATURE)
|
|
39
|
+
|
|
40
|
+
@project.changed_script?(dest, COMMIT_GUARD) ? :update : :skip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def hook(name, source)
|
|
44
|
+
dest = hook_path(name)
|
|
45
|
+
return @project.action(:skip, "git hook #{name} (no git repository)") unless dest
|
|
46
|
+
|
|
47
|
+
@project.action(hook_status(dest, source), dest, dest, source)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def hook_status(dest, source) = File.exist?(dest) && block_current?(dest, source) ? :skip : :merge_hook
|
|
51
|
+
|
|
52
|
+
def block_current?(dest, source)
|
|
53
|
+
MarkedBlock.new(File.read(dest)).current?(@project.append_content(source))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def hook_path(name)
|
|
57
|
+
hooks = git_hooks_dir
|
|
58
|
+
hooks && File.join(hooks, name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def git_hooks_dir
|
|
62
|
+
cmd = ["git", "-C", @project.target, "rev-parse", "--git-path", "hooks"]
|
|
63
|
+
path = IO.popen(cmd, err: File::NULL, &:read).strip
|
|
64
|
+
$?.success? && !path.empty? ? File.expand_path(path, @project.target) : nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Bootstrap
|
|
5
|
+
# GlobalTaskrcWarning warns when an older install left the UDA block in the
|
|
6
|
+
# user's global ~/.taskrc. Earlier versions of this installer shared one
|
|
7
|
+
# Taskwarrior database across every project. The warning stops old tasks
|
|
8
|
+
# from staying stranded there without notice.
|
|
9
|
+
class GlobalTaskrcWarning
|
|
10
|
+
def initialize(project)
|
|
11
|
+
@project = project
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
return unless File.exist?(global) && File.read(global).include?(MARKER)
|
|
16
|
+
return if same_file?(global, @project.taskrc_path)
|
|
17
|
+
|
|
18
|
+
target = @project.target
|
|
19
|
+
puts format(MIGRATION_NOTE, taskrc: global, project: target, project_name: File.basename(target))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def global
|
|
25
|
+
ENV.fetch("TASKRC", File.join(Dir.home, ".taskrc"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def same_file?(first, second)
|
|
29
|
+
File.exist?(first) && File.exist?(second) && File.identical?(first, second)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Bootstrap
|
|
5
|
+
# GraphHome moves the graph of an older install from .maf/graphify-out/ to
|
|
6
|
+
# graphify-out/ at the project root, and the vault from .maf/obsidian/ to
|
|
7
|
+
# graphify-out/obsidian/. The vault watcher writes the graph, so it stops
|
|
8
|
+
# first. VaultStarter starts it again after the move.
|
|
9
|
+
class GraphHome
|
|
10
|
+
OLD_GRAPH = File.join(MAF_DIR, "graphify-out")
|
|
11
|
+
OLD_VAULT = File.join(MAF_DIR, "obsidian")
|
|
12
|
+
VAULT = File.join(GRAPH_DIR, "obsidian")
|
|
13
|
+
|
|
14
|
+
def initialize(project) = @project = project
|
|
15
|
+
|
|
16
|
+
def move
|
|
17
|
+
return unless Dir.exist?(path(OLD_GRAPH)) || Dir.exist?(path(OLD_VAULT))
|
|
18
|
+
|
|
19
|
+
stop_watcher
|
|
20
|
+
graph_moves = movable?(OLD_GRAPH, GRAPH_DIR)
|
|
21
|
+
relocate(OLD_GRAPH, GRAPH_DIR) if graph_moves
|
|
22
|
+
move_vault(graph_moves)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# A graph at the root is newer than the old one, or belongs to the user. It stays.
|
|
28
|
+
def movable?(old, new) = Dir.exist?(path(old)) && !kept?(old, new)
|
|
29
|
+
|
|
30
|
+
# The moved graph folder can hold a stray vault that an agent exported.
|
|
31
|
+
# The old scripts deleted it before each export, so the real vault replaces it.
|
|
32
|
+
def move_vault(graph_moved)
|
|
33
|
+
return unless Dir.exist?(path(OLD_VAULT))
|
|
34
|
+
|
|
35
|
+
FileUtils.rm_rf(path(VAULT)) if graph_moved
|
|
36
|
+
relocate(OLD_VAULT, VAULT) unless kept?(OLD_VAULT, VAULT)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# True when NEW exists. The old folder then stays where it is.
|
|
40
|
+
def kept?(old, new)
|
|
41
|
+
return false unless File.exist?(path(new))
|
|
42
|
+
|
|
43
|
+
Bootstrap.say("keep #{old} (#{new} exists)")
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def relocate(old, new)
|
|
48
|
+
FileUtils.mkdir_p(File.dirname(path(new)))
|
|
49
|
+
FileUtils.mv(path(old), path(new))
|
|
50
|
+
Bootstrap.say("done move #{old} -> #{new}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stop_watcher
|
|
54
|
+
script = @project.bin_path("vault")
|
|
55
|
+
pid = path(MAF_DIR, "coordination", "vault.pid")
|
|
56
|
+
system(script, "stop", chdir: @project.target, out: File::NULL) if File.exist?(pid) && File.exist?(script)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def path(*parts) = @project.path(*parts)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maf
|
|
4
|
+
module Bootstrap
|
|
5
|
+
# HookMerger puts the flow block at the top of a git hook. The rest of the
|
|
6
|
+
# hook stays. A hook in another language than sh stays as it is.
|
|
7
|
+
class HookMerger
|
|
8
|
+
SH_SHEBANG = %r{\A#!\s*(?:/usr/bin/env\s+)?(?:\S*/)?(?:ba)?sh(?:\s|\z)}
|
|
9
|
+
|
|
10
|
+
def initialize(project)
|
|
11
|
+
@project = project
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns true when the hook changed.
|
|
15
|
+
def merge(file, source)
|
|
16
|
+
return skip_foreign_hook(file) if foreign_interpreter?(file)
|
|
17
|
+
|
|
18
|
+
body = File.exist?(file) ? MarkedBlock.new(File.read(file)).remove : "#!/bin/sh\n"
|
|
19
|
+
write_hook(file, prepend_block(body, @project.append_content(source)))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def foreign_interpreter?(file)
|
|
25
|
+
return false unless File.exist?(file)
|
|
26
|
+
|
|
27
|
+
first = File.open(file, &:gets).to_s
|
|
28
|
+
first.start_with?("#!") && !first.match?(SH_SHEBANG)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def skip_foreign_hook(file)
|
|
32
|
+
Bootstrap.say("skip #{file}: foreign hook with a non-sh shebang, doc-graph refresh is off")
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepend_block(body, block)
|
|
37
|
+
shebang, rest = split_shebang(body)
|
|
38
|
+
"#{shebang}#{block.chomp}\n#{rest.lstrip}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def split_shebang(body)
|
|
42
|
+
first, *rest = body.lines
|
|
43
|
+
first&.start_with?("#!") ? [first, rest.join] : ["", body]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def write_hook(file, text)
|
|
47
|
+
File.write(file, text)
|
|
48
|
+
FileUtils.chmod("+x", file)
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|