catherd-cli 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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/catalog/catalog.json +112 -0
  4. package/catalog/models-dev.json +56453 -0
  5. package/package.json +52 -0
  6. package/plugin/.claude-plugin/plugin.json +13 -0
  7. package/plugin/.mcp.json +8 -0
  8. package/plugin/commands/catherd-setup.md +8 -0
  9. package/plugin/commands/catherd.md +10 -0
  10. package/plugin/skills/catherd/SKILL.md +301 -0
  11. package/plugin/skills/catherd-setup/SKILL.md +65 -0
  12. package/src/cli.ts +28 -0
  13. package/src/core/codex-events.ts +49 -0
  14. package/src/core/codex.ts +166 -0
  15. package/src/core/harness.ts +89 -0
  16. package/src/core/lanes.ts +67 -0
  17. package/src/core/lock.ts +111 -0
  18. package/src/core/opencode.ts +207 -0
  19. package/src/core/reconcile.ts +26 -0
  20. package/src/core/reply.ts +54 -0
  21. package/src/core/runstore.ts +163 -0
  22. package/src/core/spawn.ts +89 -0
  23. package/src/core/status.ts +193 -0
  24. package/src/files.ts +19 -0
  25. package/src/mcp/commands.ts +7 -0
  26. package/src/mcp/dispatch.ts +154 -0
  27. package/src/mcp/lane-tools.ts +200 -0
  28. package/src/mcp/out.ts +6 -0
  29. package/src/mcp/preflight.ts +69 -0
  30. package/src/mcp/run-tools.ts +121 -0
  31. package/src/mcp/runs.ts +118 -0
  32. package/src/mcp/server.ts +26 -0
  33. package/src/mcp/setup-tools.ts +175 -0
  34. package/src/paths.ts +20 -0
  35. package/src/profile/agents.ts +118 -0
  36. package/src/profile/profile.ts +228 -0
  37. package/src/profile/role-prompts.ts +129 -0
  38. package/src/routing/catalog.ts +227 -0
  39. package/src/routing/commands.ts +22 -0
  40. package/src/routing/jev.ts +181 -0
  41. package/src/routing/route.ts +155 -0
  42. package/src/routing/select.ts +90 -0
  43. package/src/tui/backends.ts +57 -0
  44. package/src/tui/commands.ts +51 -0
  45. package/src/tui/editor.tsx +221 -0
  46. package/src/tui/init.tsx +221 -0
  47. package/src/tui/matrix-model.ts +211 -0
  48. package/src/tui/matrix.tsx +350 -0
  49. package/src/tui/profiles.ts +38 -0
  50. package/src/tui/theme.ts +140 -0
  51. package/src/tui/ui.tsx +104 -0
  52. package/src/tui/watch-model.ts +83 -0
  53. package/src/tui/watch.tsx +258 -0
  54. package/src/types.ts +184 -0
  55. package/src/version.ts +7 -0
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "catherd-cli",
3
+ "version": "0.1.0",
4
+ "description": "Herds coding agents: autopilot builds from your own Claude Code session.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/47vigen/catherd.git"
9
+ },
10
+ "bin": {
11
+ "catherd": "src/cli.ts"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "catalog",
16
+ "plugin"
17
+ ],
18
+ "type": "module",
19
+ "scripts": {
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "oxlint --deny-warnings src test",
22
+ "format": "oxfmt",
23
+ "format:check": "oxfmt --check",
24
+ "snapshot:models-dev": "bun scripts/snapshot-models-dev.ts",
25
+ "version-packages": "changeset version && bun scripts/stamp-plugin-version.mjs"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.30.1",
29
+ "@opentui/core": "^0.5.12",
30
+ "@opentui/react": "^0.5.12",
31
+ "citty": "^0.2.2",
32
+ "microdiff": "^1.6.0",
33
+ "ofetch": "^1.5.1",
34
+ "react": "^19.3.0",
35
+ "zod": "^4.6.5"
36
+ },
37
+ "devDependencies": {
38
+ "@changesets/cli": "^3.0.3",
39
+ "@commitlint/cli": "^21.2.3",
40
+ "@commitlint/config-conventional": "^21.2.3",
41
+ "@types/bun": "^1.4.2",
42
+ "@types/react": "^19.3.0",
43
+ "lefthook": "^2.1.14",
44
+ "oxfmt": "^0.70.0",
45
+ "oxlint": "^1.85.0",
46
+ "string-width": "^8.3.0",
47
+ "typescript": "^7.0.2"
48
+ },
49
+ "engines": {
50
+ "bun": ">=1.4"
51
+ }
52
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "catherd",
3
+ "version": "0.1.0",
4
+ "description": "Herds coding agents: Claude plans and verifies, Codex and opencode workers write the code, and Jev picks the model and effort for each piece.",
5
+ "author": {
6
+ "name": "47vigen",
7
+ "url": "https://github.com/47vigen"
8
+ },
9
+ "homepage": "https://github.com/47vigen/catherd",
10
+ "repository": "https://github.com/47vigen/catherd",
11
+ "license": "MIT",
12
+ "keywords": ["orchestration", "codex", "opencode", "subagents", "mcp"]
13
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "catherd": {
4
+ "command": "bunx",
5
+ "args": ["catherd-cli@0.1.0", "mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Tune your catherd profile in conversation — models and efforts per role, cost or speed, harness isolation
3
+ argument-hint: "[what you want to change]"
4
+ ---
5
+
6
+ Use the `catherd-setup` skill for this, and follow it exactly.
7
+
8
+ Where the user wants to start, if anywhere: $ARGUMENTS
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Orchestrate a task with catherd — Claude plans and verifies, Codex and opencode workers write the code
3
+ argument-hint: "<task, or nothing to resume the latest run>"
4
+ ---
5
+
6
+ Use the `catherd` skill for this, and follow it exactly.
7
+
8
+ Task: $ARGUMENTS
9
+
10
+ If the task is empty, resume: call `status()`, tell the user which run it names, and continue that run from its state.md.
@@ -0,0 +1,301 @@
1
+ ---
2
+ name: catherd
3
+ description: Use when the user asks for a task or a project to be orchestrated with catherd — /catherd, "orchestrate this with catherd", "run this on catherd" — or when resuming a paused catherd run. Not for a plain request that merely mentions an architect, a verifier, a review or a plan.
4
+ ---
5
+
6
+ # catherd
7
+
8
+ ## Overview
9
+
10
+ You are the orchestrator of an autopilot build. The goals, in order:
11
+
12
+ 1. the work finishes without the user;
13
+ 2. it finishes fast;
14
+ 3. it stays cheap;
15
+ 4. it holds its quality.
16
+
17
+ Your job:
18
+
19
+ - plan the milestones;
20
+ - dispatch the roles;
21
+ - read their one-line results;
22
+ - report.
23
+
24
+ The catherd server keeps `state.md` true: it rewrites it on every dispatch, climb and landing.
25
+
26
+ **You never edit product files.** Every line of code, tests or docs comes from a role, including a one-line fix.
27
+
28
+ **You run in the user's own session, on purpose.** The main thread is the user's own model in Claude Code, with their plugins, hooks and memory beside it: the environment they decide in every day. Never propose a stripped launcher or a headless relay to save tokens.
29
+
30
+ **Each role runs in its vendor's own harness with the user's customizations — never isolate it.** Codex and opencode roles keep the user's config, hooks, MCP servers, skills and `AGENTS.md`. The one exception is the user's own choice, the profile's `harness.<name>.isolated`, and `dispatch` applies it for you.
31
+
32
+ ## Tools
33
+
34
+ The catherd MCP tools ship with this plugin. They appear as `mcp__plugin_catherd_catherd__<name>`. If they are deferred, load them all with one ToolSearch call at the start, together with `PushNotification`.
35
+
36
+ | Tool | Use |
37
+ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | `run_start(repo, title, a_lines)` | Once per project. Returns `run`, the id every other call takes, and `dir`, the run folder `R` |
39
+ | `route(run, lane_file?, role?)` | A lane's rung, decided by Jev from its lane file; or, without a lane file, a role's rung from the profile. Returns `rung`, `ladder`, `backend`, and `agent` for a Claude rung |
40
+ | `preflight(run)` | Runs each lane's fast check once, on the base tree, behind the machine-wide lock. Call it once every lane file exists, before dispatching any of them |
41
+ | `dispatch(run, role, name, brief, rung, thread?, lane?, next?)` | Runs one Codex or opencode role and returns its `record` and `hints` |
42
+ | `climb(run, lane, reason, evidence?)` | The lane's next rung, or `top: true` |
43
+ | `ask(run, question, state)` | Jev's `finding` or `same-defect` answer |
44
+ | `land(run, milestone, what, commit, evidence, next, learned?)` | A landed milestone's ledger row, and `state.md`. `learned` appends to this repo's `knowledge.md` |
45
+ | `read_knowledge(repo)` | What past runs of this repo learned. The dossier brief reads it |
46
+ | `write_run_file(run, path, content)`, `read_run_file(run, path)` | Files in `R`. Never your own Write or Read tools there: `R` is outside the repo |
47
+ | `result(run, name)` | A role's capped reply and its record |
48
+ | `status(run?)` | One screen: the `state.md` tail, live roles with their age, totals, Jev fallbacks, the harness cost, the run's budget spend |
49
+ | `set_next(run, next)` | The next step, when you pause or the plan changes |
50
+ | `runs_summary(filter)` | Time, tokens, refusals and climbs per role and rung, and the harness cost, for the report |
51
+ | `profile_get()` | The active profile: enabled roles, and the moments to push |
52
+
53
+ ## Roles
54
+
55
+ | Role | How to run it | Job |
56
+ | ----------- | ------------------------------------------------------------------------------- | ------------------------------------------------------- |
57
+ | architect | `Agent(subagent_type: <agent>)`, the agent from `route(run, role: "architect")` | Decisions, milestones, lane files. No code. |
58
+ | verifier | `Agent(subagent_type: <agent>)`, from `route(run, role: "verifier")` | Independent PASS/FAIL of a milestone, by running it |
59
+ | worker | `dispatch(run, "worker", "worker-<lane>", brief, rung, lane: "<lane>")` | One lane: its code and its tests |
60
+ | reviewer | `dispatch(run, "reviewer", "reviewer-<m>", brief, rung)` | Review of a milestone's diff |
61
+ | ui-reviewer | `dispatch(run, "ui-reviewer", "ui-<m>", brief, rung)` | Screenshots of the changed screens with `agent-browser` |
62
+ | artist | `dispatch(run, "artist", "artist-<n>", brief, rung)` | Images a screen needs, with Codex's built-in image tool |
63
+ | writer | `dispatch(run, "writer", "writer-<m>", brief, rung)` | README, docs, changelog, MR body |
64
+ | researcher | `dispatch(run, "researcher", "researcher-<n>", brief, rung)` | The dossier, or one factual question about the code |
65
+
66
+ - **Every rung comes from `route`,** never from you. The table shows the usual backend; the profile decides. When `route` returns `backend: "claude"`, run that role with `Agent(subagent_type: <agent>)`, with the brief and the run id as its prompt; otherwise `dispatch` it. `dispatch` refuses a Claude rung and names the agent.
67
+ - **A role the profile disables** (`profile_get`; every role but the worker can be off) is skipped, and the report says so.
68
+ - **Sandbox:** catherd sets it per role. Worker, artist and writer write in the repo only; reviewer, researcher and architect read; verifier and UI reviewer get full access for Docker, a browser, gate logs and screenshots.
69
+ - **Cheap rungs hide broken tools.** The lowest Track A rung stays silent about a broken tool about a third of the time, so every lane on it is checked by your fast check, not by its own word.
70
+ - **Claude agents:**
71
+ - catherd generates them from the profile, and Claude Code registers them at session start.
72
+ - Pass no `model` to those Agent calls. Never let `Plan`, `general-purpose` or `Explore` stand in.
73
+ - `Agent type … not found` means the profile changed after this session started: tell the user to open a new session.
74
+ - If the model needs a newer Claude Code, stop and tell the user to upgrade Claude Code. Never pass a `model` to get past it.
75
+ - **The record:** `R/roles/<name>.out` is the latest reply; earlier rounds are kept as `<name>.r<k>.*`. `R/runs.jsonl` holds one record per run (role, rung, status, thread, seconds, tokens). It is the token and time record: never keep your own.
76
+
77
+ ## Effort: the ladder, decided by Jev
78
+
79
+ A lane starts on the lowest rung that can do it, and climbs one rung when it shows it cannot. The ladders come from the profile and the catalog. With the default profile:
80
+
81
+ | Track | Lanes | Rungs, low to high |
82
+ | ----- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
83
+ | A | `copy` or `build`: an existing pattern, a clear fast check | `gpt-6-luna#high` → `gpt-6-sol#medium` → `gpt-6-sol#high` → `gpt-6-sol#xhigh` |
84
+ | B | `logic` or `hard`, or `terminal` work: state, concurrency, ops, unclear cause | `gpt-6-sol#medium` → `gpt-6-sol#high` → `gpt-6-sol#xhigh` |
85
+
86
+ **Jev picks the start.** Jev (TypeSafe) is a decision model: it answers a fixed question with calibrated confidence in about three seconds, for a fraction of a cent. On low confidence, or with no Jev, the tools return the profile's default (`source: "default"`), so the run never waits on it. Every call is logged to `R/jev.jsonl`.
87
+
88
+ | When | Call | On the answer |
89
+ | --------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------- |
90
+ | Each lane, before its first dispatch | `route(run, "lanes/Mx.Ly.md")` | Dispatch it at `rung` |
91
+ | A review finding that may be the plan's fault | `ask(run, "finding", { lane_file: "lanes/Mx.Ly.md", finding: "<the line>" })` | `design` → the architect. Else → the worker |
92
+ | A finding back after its fix round | `ask(run, "same-defect", { before: "<old line>", after: "<new line>" })` | `yes` → climb one rung |
93
+
94
+ **Climb one rung** with `climb(run, lane, reason, evidence)`, then dispatch the lane at the new rung on a fresh thread whose brief is the lane file plus the path of the failing evidence, when:
95
+
96
+ - your fast check fails twice on that lane (`check-failed-twice`);
97
+ - the lane gets a BLOCKER (`blocker`), or Jev calls a returning finding the same defect (`same-defect`);
98
+ - the reply says `STATUS: refused` or `blocked`, or the run exits 0 while the lane's owned files are unchanged: that is a refusal, whatever the reply says (`refused`, `blocked`, `unchanged`). `dispatch` flags each of these in its `hints` as `climb: <reason>`.
99
+
100
+ A failure on the top rung (`top: true`) goes to the architect when Jev calls it design, else to the report as open.
101
+
102
+ - Jev decides which model does the work. It never decides that the work is done: only a check, the reviewer or the verifier does.
103
+ - `R/routes.jsonl` records each lane's rung and every climb with its reason.
104
+ - Never put a secret or a key into an `ask` state. Keep the state short and in English.
105
+
106
+ ## Threads
107
+
108
+ A resumed thread replays its whole history on every tool call. In the first real run, one worker thread cost 20–25M input tokens per resume, and threads were 90% of the Codex spend.
109
+
110
+ - **Resume a thread only for its own fix round:** the reviewer's findings on that piece, or the verifier's FAIL, at the same rung. That is `dispatch(…, thread: <record.thread>)`, with the fix as the brief.
111
+ - **A new piece of work gets a fresh thread,** even for the same worker role: a new bug, a cleanup slice, a docs pass, a re-run, a climb to the next rung. Its brief carries what it needs, from its lane file and the ledger.
112
+ - A `thread-heavy` hint means that thread is spent. Its next piece starts fresh.
113
+
114
+ ## Your own context
115
+
116
+ Your context is re-read on every turn, and it is the run's most expensive token. In the first real run you were 68% of the Claude spend: 269 turns at about 220k tokens each.
117
+
118
+ - **One message per transition.** Independent tool calls (a `land`, the next lanes' dispatches, a `route`) go in the same message.
119
+ - **Read little, and read it narrowly.**
120
+ - From the architect, read its short reply. The plan lives in `plan.md` and the lane files; read a section with `read_run_file` only when a decision needs it.
121
+ - From a role, read its record and its capped reply.
122
+ - Never read a log, a diff, a test file or source.
123
+ - **Brief by path.** A brief names the files a role must read (its lane file, a prior reply, a finding). Never paste their contents into your own context to copy them over.
124
+ - **Screenshots are paths.** The UI reviewer's findings are text, each with its screenshot path. Open one yourself only when you must decide on a finding the text leaves unclear.
125
+
126
+ ## Waiting
127
+
128
+ Call `dispatch` from your main thread, never from a subagent: a subagent's MCP call never backgrounds, so it would hold that subagent for the whole run. Launch every independent role in the same message. Each dispatch backgrounds by itself after two minutes, and its result arrives as a notification that wakes you. Then end your turn with one status line. Never `sleep` and never poll.
129
+
130
+ **When the user asks where it stands,** call `status(run)` once and answer from it.
131
+
132
+ **Push a notification** (`PushNotification`) only at the moments the profile's `notify` lists (`profile_get`), one line each:
133
+
134
+ - `milestone`: a milestone landed: its name, its commit, the time it took;
135
+ - `finish`: the run finished: verdict and total time;
136
+ - `blocked`: the run is blocked on a decision only the user can make.
137
+
138
+ Nothing else pushes: a phone that buzzes for progress teaches the user to ignore it.
139
+
140
+ ## The run folder
141
+
142
+ `run_start` creates `R` under catherd's data directory, outside the repo. It outlives the session. `R` holds:
143
+
144
+ - **`plan.md`:** the architect's decisions, milestones and lanes. It changes only by an architect delta.
145
+ - **`lanes/Mx.Ly.md`:** one file per lane, the only plan a worker reads. Its second line, `Owns: <paths>`, is what `dispatch` uses to refuse a lane that shares a file with a running one, and to spot a refusal.
146
+ - **`ledger.md`:** one row per landed piece, appended by `land`. Never rewritten.
147
+ - **`state.md`:** rewritten by the server at every dispatch, climb and landing, so a fresh session resumes from it alone: HEAD, the dirty files and their owners, each running role with its brief, thread and rung, the last check, and the next step on the last line.
148
+ - **`runs.jsonl`, `jev.jsonl`, `routes.jsonl`, `harness.jsonl`:** the record.
149
+ - **`roles/`** (briefs and replies) and **`shots/`** (screenshots).
150
+ - **`<data>/<repo-slug>/knowledge.md`** (one level up, per repo, not per run): what past runs learned. `land`'s `learned` appends to it; `read_knowledge` reads it.
151
+
152
+ **Pause** (on the user's word, or a usage limit): dispatch nothing new, and bring down only this run's stack. Leave the tree as it is, call `set_next(run, "paused: <why>; resume with <step>")`, then stop. On a usage limit, `dispatch` has already written the pause.
153
+
154
+ **Resume:** `status()` names the run, and `status(run)` shows it. Check HEAD and the dirty files against its `state.md`. Continue each role on its own thread with `dispatch(…, thread, brief: "<where it stopped>")`.
155
+
156
+ ## The sequence
157
+
158
+ **Once per project:**
159
+
160
+ 1. **A-lines.** Write the request as numbered, observable lines `A1…An`. Ask the user every open product question now; after this point, run without them. Then `run_start(repo, title, a_lines)`.
161
+ 2. **Dossier.** One researcher, `researcher-dossier`, maps the code the A-lines touch. Its brief asks for, with `file:line` everywhere:
162
+ - what past runs of this repo learned (`read_knowledge(repo)`), so it maps only what changed since then;
163
+ - the files and folders involved, one line each on what they hold;
164
+ - the existing patterns the work should copy, by path;
165
+ - the build, test and run commands, with how long the full suite takes;
166
+ - the symbols the change will call or alter, with their signatures;
167
+ - the repo's rules (`CLAUDE.md`, `AGENTS.md`, conventions) that bind this work;
168
+ - risks: shared files, generated code, slow or flaky tests.
169
+
170
+ It may run to 200 lines; the 15-line cap does not apply to it. Its reply is `R/roles/researcher-dossier.out`. A Claude researcher writes it to `R/dossier.md` with `write_run_file` instead.
171
+
172
+ 3. **Architect,** once, with the A-lines, the run id and the dossier's path. It reads the dossier first, writes `plan.md` and every `lanes/Mx.Ly.md` itself with `write_run_file`, and replies with a short list of milestones and lanes. Each lane names its owned files and its **fast check** (targeted tests, seconds to a minute or two); each milestone names its **full check** (the whole suite).
173
+ - A full check slower than about five minutes is a problem to solve, not to live with. The architect makes speeding it up (parallel tests, a shared fixture) an early lane.
174
+ - **No dossier and no architect** for a polish or fix run (a list of known defects or tweaks to code that exists) or a single mechanical task. You write the lane files yourself with `write_run_file`, straight from the A-lines: one lane per cluster of defects that share files, with owned files found by `grep -n`, and the three header lines the architect writes (`# Mx.Ly — …`, `Owns: …`, `Fast check: …`).
175
+ 4. **Route and preflight.** `route(run, "lanes/Mx.Ly.md")` for every lane, all in one message. Then, once every lane file exists, `preflight(run)` once, before dispatching any lane: a lane whose fast check cannot even start blocks the run with its output until fixed.
176
+
177
+ **Per milestone:**
178
+
179
+ 5. **Lanes.** Launch every lane of the milestone in one message, each at its rung: workers, the artist, and a researcher if needed. A worker's brief points at its lane file, and `dispatch` gets its `lane`. A worker runs its own fast check until it passes.
180
+ - When a worker returns, check its STATUS line, its `changedOwned` and its `hints`, then run its fast check yourself once. A fail goes back to the same thread with the failing output's path; a second fail climbs a rung.
181
+ 6. **writer,** when the milestone changes docs. It starts once the workers are done.
182
+ 7. **reviewer,** once, over the whole milestone diff on a frozen tree.
183
+ - **UI pass,** when the milestone touched a screen. List the changed files (`git diff --name-only <milestone base>`), map them to the screens that render them, and brief the UI reviewer on those screens only. You start the app first.
184
+ 8. **One fix round.** Send each lane's findings, verbatim, to its own worker thread, at its rung. A BLOCKER climbs a rung instead, on a fresh thread. All lanes go at once. Then resume the same reviewer thread, and it re-checks only the BLOCKER and BUG lines.
185
+ - Before routing a finding that questions the plan, `ask(run, "finding", …)`. `design` goes to the architect (`SendMessage` to the same agent), and its delta rewrites the lane files.
186
+ - A finding that comes back: `ask(run, "same-defect", …)`. `yes` gets one climb and one re-check of that line. Anything still open goes to the report, not into another round.
187
+ 9. **verifier,** with the milestone's A-lines and the **full check**, on a frozen tree. A full check that starts while a role still edits proves nothing, and it has to run again. Never give it a worker's reply.
188
+ - On FAIL, the owning worker fixes it, and you `SendMessage` the same verifier to re-check.
189
+ - A second FAIL on the same line goes to the architect.
190
+ - A third one: pause, report and push.
191
+ 10. **Land.** Commit the milestone path-scoped, then `land(run, milestone, what, commit, evidence, next, learned)`, passing `learned` when the milestone taught the next run something worth knowing (a slow suite, a flaky test, a pattern to copy); push if the profile's `notify` has `milestone`, and move to the next milestone.
192
+ - Commit only while no role is writing. A pre-commit hook may stash unstaged files, and a role's edits vanish under it.
193
+ - The next milestone's lanes can start in the same message as the `land`.
194
+
195
+ **Finish:** run the project's final gate if it has one.
196
+
197
+ - The verifier is the gate owner, run in the **foreground** (`run_in_background: false`). A background subagent dies with the session, and the gate is lost.
198
+ - Beside it, in the background:
199
+ - any independent review (a spec or milestone review by a fresh verifier);
200
+ - the writer's MR body;
201
+ - one **whole-app UI pass**, for what a change broke on a screen nobody touched.
202
+ - A gate FAIL goes to a fresh worker thread as one piece. The gate then re-runs from the start on the new SHA.
203
+
204
+ Then report to the user, in their language, and push if `notify` has `finish`:
205
+
206
+ - the verdict per milestone;
207
+ - the commits;
208
+ - one line per role run, with its rung;
209
+ - the time and token sums per backend and per rung, and the climbs with their reasons (`runs_summary({ run })`), the Claude subagent runs, and the Jev decisions with how many fell back (`status(run)`);
210
+ - the harness line from `runs_summary`: what the user's Codex and opencode customizations cost per run, so they can judge the isolation toggle (`/catherd-setup` offers it);
211
+ - the run's budget spend (`status(run)`), if the profile set one;
212
+ - what was left open, and why.
213
+
214
+ Roles never commit. You commit, following the repo's rules.
215
+
216
+ ## Brief for a role
217
+
218
+ The brief is the `brief` text you pass to `dispatch` (catherd writes it to `R/roles/<name>.md`, or `<name>.fix.md` for a fix), or the prompt of a Claude role's Agent call, with the run id. It has these parts, in order:
219
+
220
+ 1. The role and the goal, in one line.
221
+ 2. The A-lines this run must meet.
222
+ 3. The files it owns, and the files it must not touch. If the repo has a `CLAUDE.md`, add "Read CLAUDE.md first": Codex loads only `AGENTS.md`.
223
+ 4. For a worker:
224
+ - "Read `<R>/lanes/Mx.Ly.md`": decisions, signatures, data shapes;
225
+ - its fast check, to run until it passes;
226
+ - "Run the full suite only if this brief says so", and "Wrap any full build or full test suite in `npx -y catherd lock -- <command>`": other lanes share the machine.
227
+ 5. For a reviewer: the A-lines and the changed files, with new files read in full. It reports every finding as `BLOCKER|BUG|NIT file:line — problem — fix`, covering:
228
+ - unmet A-lines and edge cases;
229
+ - code or abstractions nobody needs;
230
+ - comments that restate code;
231
+ - tests that assert nothing.
232
+
233
+ With no finding, the reply is `CLEAN`.
234
+
235
+ 6. For a UI reviewer:
236
+ - the URL and the changed screens, by route;
237
+ - the viewports and themes;
238
+ - `agent-browser screenshot <R>/shots/<name>.png` at viewport size, not full page; it opens each PNG with its image tool;
239
+ - every finding is `BLOCKER|BUG|NIT <screen> — problem — fix — <R>/shots/<name>.png`, always with the path.
240
+ 7. For an artist, per image:
241
+ - the screen, where the image sits, its shape;
242
+ - the mood and scene in words (subject, materials, light, colour): taste, not measurements;
243
+ - the final `.webp` path in the repo's assets folder. The worker's brief names the same path, so both lanes run at once.
244
+
245
+ Its rules:
246
+ - use the built-in image tool, and copy its untouched output from `${CODEX_HOME:-~/.codex}/generated_images/`; the record's `images` lists those paths;
247
+ - the only processing is `cwebp -q 85 in.png -o out.webp`;
248
+ - never paint, patch or upscale;
249
+ - never redraw a real logo, and never present a generated person as a real customer;
250
+ - add one provenance line per image to a `README.md` beside it.
251
+
252
+ The tool tops out around 1536×1024.
253
+
254
+ 8. "Do not commit." Then the reply shape: **at most 15 lines**. Results, file:line, and evidence as the log path, not the log. The last line is `STATUS: complete|partial|blocked|refused — <one line why>`.
255
+
256
+ ## Reading results
257
+
258
+ - Read the record `dispatch` returns, its `hints`, and the reply (`result(run, name)`), nothing else. Read `roles/<name>.jsonl` or `.err` with `read_run_file` only when `status` is `failed`. Never read a diff or a log yourself: that is the reviewer's and verifier's job, and your context is the run's most expensive token.
259
+ - Exit 0 means the model finished, not that it is right. The STATUS line is the role's claim; `changedOwned` and your fast check are the facts.
260
+ - `failed` comes only from a real turn failure or an exit with no reply; a reconnect mid-run does not count. Read the reply before you retry.
261
+ - `cli-too-old`: tell the user the upgrade command in the record's `error`. `limit`: a usage limit is the user's to fix: pause, report and push that turn.
262
+
263
+ ## Red flags
264
+
265
+ | You notice | Do instead |
266
+ | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
267
+ | A lane waits on another lane that shares none of its files | Launch it now, in the same message |
268
+ | A worker or fix loop runs the full suite to check one change | Its fast check. The full check is the verifier's, once per milestone |
269
+ | A reviewer or verifier runs after each lane | Once per milestone, over the whole milestone |
270
+ | A third review round | One fix round, one climb for a returning defect, one re-check. The rest goes to the report |
271
+ | You open a diff, a log or a source file to judge the work | Send a reviewer, or the verifier |
272
+ | You open every screenshot | Read the findings. Open one path only to settle an unclear finding |
273
+ | The UI pass covers the whole app mid-run | Changed screens only. The whole app once, at finish |
274
+ | You write `state.md`, the ledger or a brief file with your own tools | The server writes `state.md`, `land` writes the ledger, `dispatch` writes the brief |
275
+ | A new bug, cleanup, re-run or climb sent with `thread` | A fresh thread. Resume only for that piece's own findings at the same rung |
276
+ | One transition spread over several turns (a land, routes, dispatches) | One message, with every independent call in it |
277
+ | A plan, log or test file read whole into your context | `read_run_file` for the one section, or `grep -n` then those lines. Or name the path in a brief |
278
+ | You copy the architect's plan into `plan.md` yourself | The architect writes `plan.md` and the lane files |
279
+ | A dossier or an architect for a polish or fix run | Write the lane files from the A-lines yourself |
280
+ | A lane file without an `Owns:` line | Add it: `dispatch` refuses the lane without one |
281
+ | You pick a rung by feel | `route`. Its default covers the case where Jev is unsure or down |
282
+ | Jev's answer taken as proof a lane is done | Jev picks who works. Checks, the reviewer and the verifier decide done |
283
+ | A reply from the lowest Track A rung trusted without your fast check | Run it. Cheap rungs hide broken tools |
284
+ | A reviewer, or a `logic`/`hard` lane, on a rung you chose yourself | The rung `route` returned |
285
+ | Exit 0 with the owned files unchanged, treated as done | A refusal: climb with reason `unchanged` |
286
+ | The gate verifier dispatched in the background | Foreground. Reviews, the MR body and the whole-app UI pass go in the background beside it |
287
+ | A full check started while a worker still edits | Wait for a frozen tree. That run proves nothing |
288
+ | Lanes dispatched before `preflight` ran | Run `preflight(run)` first; a lane whose check cannot even start wastes a dispatch |
289
+ | An architect planning without reading what past runs learned | `read_knowledge(repo)` first, from the dossier brief |
290
+ | A landed milestone that taught something, landed without `learned` | Pass it: the next run's architect reads `knowledge.md` |
291
+ | You update catherd, this plugin or the profile while a run is in flight | After the run. A role mid-flight must see one version |
292
+ | You stop to ask the user something mid-run | Decide within the A-lines and note it for the report. Only a product question outside them pauses the run, with a push |
293
+ | A push for progress that is not a landed milestone, the finish or a block | No push. `status(run)` answers when the user asks |
294
+ | Your own decision changes behavior that already exists and no A-line asked for it (e.g. re-numbering `list` to match a new command) | Pick the option that keeps existing behavior, and fit the new code to it |
295
+ | `Agent(subagent_type: "Plan", model: "opus")` for the architect | The `agent` that `route(run, role: "architect")` returned, with no model |
296
+ | `dispatch` called from a subagent | The main thread. A subagent's MCP call never backgrounds |
297
+ | `codex exec` or `opencode run` called by hand | Always `dispatch`: it records the run, keeps `state.md` true and guards the lanes |
298
+ | You isolate a role's harness yourself, or tell a role to ignore the user's config | Never. Only the profile's `harness.<name>.isolated`, which the user sets |
299
+ | The architect's plan contains function bodies | Ask for decisions and signatures. The worker writes the code |
300
+ | "It's one line, I'll fix it myself" | Send it to the worker's thread |
301
+ | The artist's image was resized, retouched or patched | Regenerate it from the artist's thread |
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: catherd-setup
3
+ description: Use when the user wants to tune catherd — which models and efforts each role may use, cost versus speed, which subscriptions to lean on, harness isolation — /catherd-setup, "set up catherd", "tune my catherd profile". Not for running a task; that is the catherd skill.
4
+ ---
5
+
6
+ # catherd setup
7
+
8
+ You tune the user's catherd profile in conversation. A profile says, per role, which models and efforts it may run on; whether routing favours cost or speed; whether each vendor harness runs with the user's customizations or isolated; how many heavy commands run at once; and when to push.
9
+
10
+ **You never edit a file by hand.** `profile_set` is the only writer, so this conversation and the TUI (`bunx catherd-cli`) cannot drift apart.
11
+
12
+ ## Rules
13
+
14
+ - **One question at a time.** Each question carries your recommended answer and one line on why, so the user can simply say yes.
15
+ - **The user steers.** When they reverse a proposal, take the reversal and never re-argue it.
16
+ - **Talk in their terms:** minutes, their subscriptions, how often a rung climbed on their own runs, tokens per run. Benchmark names only when they ask.
17
+ - **Every number you quote comes from a tool result in this conversation.** When there is no data yet, say so instead of estimating.
18
+
19
+ ## 1. Learn the goals
20
+
21
+ Ask these, one at a time, each with its recommended answer:
22
+
23
+ 1. **Their order of speed, cost and quality.** Recommend cost first, the default `objective`: catherd climbs a rung when a cheap one cannot do the work, so the lanes that need speed get it anyway, and the reviewer and the verifier hold quality either way.
24
+ 2. **The subscriptions they hold:** a ChatGPT plan (Codex), a Claude plan, OpenRouter or other opencode credit. Recommend leaning on subscriptions before metered spend, and on Claude last among the workers, since it spends the same quota as this conversation.
25
+ 3. **The kind of work they orchestrate:** front-end screens, back-end services, terminal and ops work, docs. Recommend from their own runs when `runs_summary` has any.
26
+
27
+ ## 2. Read the facts before proposing
28
+
29
+ In one message, call:
30
+
31
+ - `catalog_query({ role: "<role>" })` for each role you will discuss: what their installed backends can run, with capabilities and scores. A model whose `installed` is false is not offered;
32
+ - `runs_summary({})`: how each rung has done on their own runs (runs, refusals, climbs, time) and the harness cost line;
33
+ - `profile_get()`: where they stand now.
34
+
35
+ ## 3. Propose one decision at a time
36
+
37
+ Go through these in order, and skip any the user does not care about:
38
+
39
+ 1. `objective`;
40
+ 2. the worker's ladder: its enabled models and efforts, and its `defaultRung`;
41
+ 3. the reviewer and the UI reviewer;
42
+ 4. the architect and the verifier, which spend Claude quota;
43
+ 5. the writer, the researcher and the artist;
44
+ 6. harness isolation, per harness (below);
45
+ 7. `lock.heavy` and `notify`.
46
+
47
+ Each proposal has three parts: the change, a worked example from their facts, and the tradeoff in their terms. For instance: "Luna high on build lanes: about 5 min slower than Sol medium, no Claude quota, climbs on 1 in 5 of your runs so far."
48
+
49
+ - Offer only the models `catalog_query` lists as capable for the role.
50
+ - An unscored model can be enabled only once it has a "treat like <scored model#effort>". That lives in the catalog override, which the TUI edits: send the user to `bunx catherd-cli` for it, then come back.
51
+
52
+ **Harness isolation.** For each harness they use (`codex`, `opencode`), offer `harness.<name>.isolated` with its harness line from `runs_summary` and this tradeoff: "native keeps your hooks, skills and AGENTS.md; isolated saves ~N tokens per run, but the role loses them." Recommend native. When they have no isolated runs yet, the number is the median first-turn input of their native runs: say that isolation would save some part of it, not all of it. When there are no runs at all, say there is no number yet, and recommend native until there is.
53
+
54
+ ## 4. Write it
55
+
56
+ - Call `profile_set({ patch })` with only what changed; pass `name` only to edit a profile other than the active one. It validates before it writes:
57
+ - `saved: false` comes with `errors`. Explain each in their terms, fix the patch, and propose again.
58
+ - `saved: true` comes with the `diff`. Read it back to the user, one line per change.
59
+ - Then call `profile_validate()`. It checks that every enabled role has a capable model, that the worker's ladder is non-empty for every kind, and that no unscored model is enabled without its "treat like".
60
+
61
+ ## 5. Say what applies when
62
+
63
+ - Codex and opencode changes, isolation included, apply at the next dispatch, even in a run already under way.
64
+ - A Claude agent listed in `new_session_needed_for` applies from the next Claude Code session: Claude Code registers agent files when a session starts. Other Claude changes apply now.
65
+ - Editing a profile that is not the active one writes its agent files but links none; they apply once it becomes active.
package/src/cli.ts ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { defineCommand, runMain } from "citty";
5
+ import { lockCommand } from "./core/lock.ts";
6
+ import { mcpCommand } from "./mcp/commands.ts";
7
+ import { catalogCommand } from "./routing/commands.ts";
8
+ import { editorRun, initCommand, watchCommand } from "./tui/commands.ts";
9
+
10
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
11
+ version: string;
12
+ };
13
+
14
+ export const main = defineCommand({
15
+ meta: { name: "catherd", version: pkg.version, description: "Herds coding agents." },
16
+ subCommands: {
17
+ lock: lockCommand,
18
+ catalog: catalogCommand,
19
+ mcp: mcpCommand,
20
+ init: initCommand,
21
+ watch: watchCommand,
22
+ },
23
+ run: editorRun,
24
+ });
25
+
26
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
27
+ void runMain(main);
28
+ }
@@ -0,0 +1,49 @@
1
+ import type { Tokens } from "../types.ts";
2
+
3
+ export interface CodexParse {
4
+ thread: string | null;
5
+ tokens: Tokens;
6
+ turnFailed: boolean;
7
+ failure: string | null;
8
+ limit: boolean;
9
+ cliTooOld: boolean;
10
+ lastEvent: string | null;
11
+ }
12
+
13
+ const LIMIT = /usage limit|rate limit reached|quota exceeded|try again later/i;
14
+ const TOO_OLD = /not supported when using Codex with a ChatGPT account/i;
15
+
16
+ export function parseCodexEvents(lines: string[]): CodexParse {
17
+ const p: CodexParse = {
18
+ thread: null,
19
+ tokens: { input: 0, cached: 0, output: 0 },
20
+ turnFailed: false,
21
+ failure: null,
22
+ limit: false,
23
+ cliTooOld: false,
24
+ lastEvent: null,
25
+ };
26
+ for (const line of lines) {
27
+ if (!line.trim().startsWith("{")) continue;
28
+ let e: Record<string, any>;
29
+ try {
30
+ e = JSON.parse(line);
31
+ } catch {
32
+ continue;
33
+ }
34
+ p.lastEvent = e.item?.type ? `${e.type}/${e.item.type}` : e.type;
35
+ const msg: string = e.message ?? e.error?.message ?? e.item?.message ?? "";
36
+ if (TOO_OLD.test(msg)) p.cliTooOld = true;
37
+ if (e.type === "thread.started") p.thread = e.thread_id ?? null;
38
+ else if (e.type === "turn.completed" && e.usage) {
39
+ p.tokens.input += e.usage.input_tokens ?? 0;
40
+ p.tokens.cached += e.usage.cached_input_tokens ?? 0;
41
+ p.tokens.output += e.usage.output_tokens ?? 0;
42
+ } else if (e.type === "turn.failed") {
43
+ p.turnFailed = true;
44
+ p.failure = msg || "turn failed";
45
+ if (LIMIT.test(msg)) p.limit = true;
46
+ }
47
+ }
48
+ return p;
49
+ }