Click a tool call's ▸ to expand its full command, file, diff or prompt (and full Bash output); click ▾ to collapse.

A manager-led agent team built from how you actually work in Claude Code, Codex, opencode, pi and omp.
You talk to one manager. It classifies your message, sizes the work with Jev, and spawns up to 100 agents (at most 8 at once by default) across harnesses. Those agents message each other to build, review, debug and test. The manager reports only after cross-model review, real checks and a final independent verification.
docs/DESIGN.md: architecture, caps, sizing table, bus protocol, and how zen-workflow, herdr, the Codex lanes and claude-review fit in.cd ~/Project/<repo>
zstack # manager in Claude Code (claude --plugin-dir ~/Project/zstack --agent zstack:manager)
zstack --host codex # manager in Codex
zstack --host omp # manager in omp
zstack --host pi # manager in pi
zstack --host opencode # manager in opencode
zstack --host codex -- -m gpt-6-sol # anything after -- goes to the harness
zstack --host omp --dry-run # print the launch command instead
Then talk normally: "wdyt about…", "create the plan", "review the current changes", "fix X, then commit and push", "retrigger until it works", "status?", "remember: never commit docs".
Headless: claude -p --plugin-dir ~/Project/zstack --agent zstack:manager --permission-mode auto "<request>".
| host | how the manager is loaded | default team (config [hosts.<host>.roles]) |
|---|---|---|
| claude | plugin agent zstack:manager | Claude only: scouts Haiku, workers Sonnet, reviewers and verifier Opus |
| codex | manager prompt as developer_instructions, workspace-write sandbox + network | Codex only: scouts/workers gpt-6-luna, planner/debugger/reviewers/verifier gpt-6-sol |
| omp | --append-system-prompt | omp only, models chosen once in zstack setup --host omp (suggested: DeepSeek v4.1 flash workers, GLM 5.3 flash planner/reviewers, luna scouts, all via Command Code) |
| pi | --append-system-prompt | pi only, chosen in zstack setup --host pi (suggested: luna workers, sol planner/reviewers via openai-codex) |
| opencode | inline zstack-manager agent via OPENCODE_CONFIG_CONTENT | opencode only, chosen in zstack setup --host opencode (suggested: DeepSeek v4.1 flash workers, v4 pro planner, gpt-5.6-luna reviewers via opencode-go) |
Outside Claude Code the manager spawns every agent through zstack spawn (there is no Agent tool). Codex runs each shell command in a sandbox that kills its child processes, so for --host codex the launcher also starts a small dispatcher outside the sandbox: zstack spawn queues the agent and the dispatcher starts it. Agents never leave the host harness, so dropping one subscription never breaks another host; review stays cross-model inside it (Sonnet → Opus, luna → sol). zstack agent add --harness <other> is refused without --allow-cross.
These harnesses route to many providers, so zstack asks once which model each tier uses: fast (scouts, scribe), work (workers, testers, operators), strong (planner, debugger) and review (reviewers, verifier). The first zstack --host <host> asks; you can also run it directly:
zstack setup --host omp # interactive; `?` lists the harness's models, Enter takes the suggestion
zstack setup --host pi --defaults # take the suggestions
zstack setup --host opencode --work opencode-go/deepseek-v4.1-flash --review opencode-go/gpt-5.6-luna --fast … --strong …
The choice is saved in ~/.local/state/zstack/zstack.toml and checked against the harness's own model list.
The manager first works out what kind of message you sent. Only the execute path changes code.
flowchart TD
U([You]) --> M[Manager]
M --> C{"Classify intent<br/>(Jev)"}
C -->|question| A["Answer<br/>(scouts if wide)"]
C -->|plan| P["Scouts → planner<br/>plan + numbered questions"]
C -->|review| R["Reviewer personas<br/>code · arch · security · contract"]
C -->|ops| O["Operator loop<br/>trigger → poll → logs → fix → rerun"]
C -->|execute| I["Intake<br/>git status, files, independent units"]
I --> S{"zstack size<br/>(Jev picks the tier)"}
S -->|"risky, ambiguous,<br/>team or swarm"| G["Plan gate<br/>you answer 1.A 2.yes<br/>or Jev decides for you"]
S -->|small and clear| B[[Build loop]]
G --> B
O -->|"deploy / delete / console"| GATE[/"Stop at gate, ask you"/]
A --> REP([Report to you])
P --> REP
R --> REP
O --> REP
B --> REP
flowchart TD
T["Task graph<br/>owned paths · deps · acceptance"] --> W1[worker-1]
T --> W2[worker-2]
T --> W3["worker-n<br/>(≤ max_parallel at once)"]
W1 -->|review-request| RV
W2 -->|review-request| RV
W3 -->|review-request| RV
RV["Reviewers<br/>different model, same harness, read-only"] -->|"findings → same worker"| FIX[Owner fixes]
FIX -->|fix-done| RV
FIX -->|"same property fails twice"| DBG["Debugger → stronger model → you"]
RV -->|approved| TS["Testers<br/>unit → e2e → run → browser → parity"]
TS -->|"test-result: fail"| FIX
TS -->|all tasks done with evidence| VF["Verifier<br/>re-runs checks · scope<br/>Jev: answers / backed / scoped"]
VF -->|FAIL| FIX
VF -->|PASS| DOC["Scribe (only if docs needed)"]
DOC --> GIT["Git: only what you allowed"]
GIT --> REP([Report])
sequenceDiagram
participant M as Manager
participant W as worker-1
participant R as reviewer-1 (Opus)
participant T as tester-1
M->>W: task t1 (brief: paths, acceptance, house rules)
W->>W: implement + checks
W->>R: review-request t1
R->>W: findings F1, F2
W->>R: fix-done (new check output)
R->>W: approved
R-->>M: approved (cc)
W->>T: test-request t1
T->>W: test-result PASS
T-->>M: test-result (cc)
W->>M: done + evidence
| size | when | agents |
|---|---|---|
| solo | trivial, one obvious edit | 0 (manager does it) |
| pair | one focused change | 1 worker + 1 reviewer |
| squad | 2–4 independent units | ≤4 workers, 2 reviewers, 1 tester, scouts, verifier |
| team | 5–12 units or several repos | ≤12 workers, 1 reviewer per 3 workers, 1 tester per 4 workers, planner, verifier, scribe |
| swarm | 13+ units | ≤60 workers, scaled to fit under the 100 cap (10 kept in reserve) |
Jev picks the size. To force one, say "use a team", "use 8 agents" or "no subagents".
| path | what |
|---|---|
agents/ | manager, scout, planner, worker, reviewer, tester, debugger, operator, verifier, scribe (Claude Code plugin agents; zstack brief reuses them for other harnesses) |
skills/zstack | the manager playbook: intent → size → flow → spawn → monitor → gates → report |
skills/zstack-bus | how agents talk: message kinds, handoffs, evidence format |
skills/zstack-jev | the exact Jev questions: size, route, loop, finding, approve, scope, complete |
skills/zstack-review | review rubric and personas (code, architecture, security, contract, scope, infra) |
skills/zstack-verify | verification ladder: static → unit → e2e → run → browser → parity/bench → live |
skills/zstack-debug | root-cause loop used after two failed fixes |
skills/zstack-git | git policy (default: no commit, current branch, no worktree) |
rules/house.md | your standing rules, injected into every agent brief |
config/zstack.toml | caps, per-host role → model routing, setup suggestions |
bin/zstack | stdlib-only Python CLI: run ledger, task graph, message bus, caps, Jev sizing, briefs, cross-harness spawn |
tests/ | python3 -m unittest discover tests |
mods/ | Claude Code mods (below); .claude-plugin/marketplace.json lists zstack and every mod |
zstack init --goal "split league service" --repo . [--git commit-push] [--worktree per-writer] [--deploy authorized]
echo '{"request":"…","units":6}' | zstack size # Jev → intent, tier, flow, counts per role
zstack agent add --role worker --task t1 [--harness codex --model gpt-6-luna]
zstack brief worker-1 # brief for an in-session subagent
zstack spawn reviewer-1 # headless agent on its harness; posts `done` when it exits
zstack msg send --to worker-1 --kind findings --ref t1 --body "F1 …"
zstack --as worker-1 msg wait --kind findings,approved
zstack msg log --kind blocker,question --tail 5
zstack task add --title … --paths internal/league --deps t1 --accept "…"
zstack task set t1 --state done --evidence "go test ./league/... ok"
zstack status | zstack report | zstack close
zstack rule add "never put code in cmd/" --project # remembered for every future brief
State lives in ~/.local/state/zstack/ (override with ZSTACK_HOME), never in your repo.
./install.sh puts zstack on your PATH and links the skills into ~/.agents/skills (Codex, pi and omp read skills from there). ./install.sh --codex-agents also generates ~/.codex/agents/zstack-*.toml. The script never edits existing files. For Claude Code, zstack loads the plugin with --plugin-dir, so nothing needs installing.
Claude Code mods (plugins of function hooks) aimed at the frustrations that come up most in the history. Each is its own plugin under mods/, with tests (claude plugin test mods/<name>).
| mod | what it does | the pain it targets |
|---|---|---|
intent-gate | Jev classifies each prompt as question / plan / review / execute / ops; on non-execute turns it refuses Edit/Write and git writes with a reason. /intent execute or /intent off overrides. Fails open when Jev is down. | "why you implement? I only want the docs" |
git-guard | Blocks staging docs/plans, git add -A, force push, rebase and amend unless your prompt asked; strips co-author lines. | committed docs, co-author lines, surprise rebases |
stack-blast-radius | Holds terraform apply, pulumi up, cloud deletes/deploys and broad SQL writes for a Proceed/Cancel pane with a dry-run preview. | over-broad deletes, risky infra |
secret-vault | Swaps pasted JWTs, cookies, DSNs, passwords and keys for ⟨secret:N⟩ placeholders before the model sees them, puts real values back only inside commands, masks them in output. /vault list. | live secrets in transcripts |
loop-breaker | After the same failure twice (or the same error pasted again) tells the model to stop patching and diagnose. | "still same ahh" loops |
evidence-check | Flags a reply that claims "tests pass / fixed / verified" when no test or check ran that turn. | "you said it pass?" |
ship-state | Band above the prompt: branch, ↑↓ vs upstream, staged/modified/untracked, last test result. | "already pushed to main?" |
rules-injector | Adds your zstack rule list (house + global + project) to every session's system prompt; /rules add …. | forgotten standing rules |
zstack-pane | /zs opens a live pane of the current zstack run: agents, tasks, blockers, and a steer box. | supervising runs |
usage-meter | Status line with context %, tokens and rate-limit usage. | cost and rate limits |
turn-done-alert | Toast when a long turn ends or when Claude is waiting on you; a heartbeat while a turn runs long. | "is it stuck?" |
tool-fold | Click ▸ on a tool call to see its full command, file, diff or prompt (and full Bash output); ▾ folds it. | truncated tool rows |
Try one in a single session:
claude --plugin-dir ~/Project/zstack/mods/git-guard
Install for every session:
claude plugin marketplace add zainokta/zstack # or a local path: ~/Project/zstack
claude plugin install intent-gate@zstack --scope user
Guards (intent-gate, git-guard, stack-blast-radius) read command text, so a script or $(…) can get past them. Keep permission rules for hard blocks.
hooks/register.tsx 127 lines1import { atom, memberOf, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// One open/closed flag per tool call. ToolUse and ToolResult rows share it:
5// both are drawn with the call's tool_use_id as their requestId.
6const open = atom({ plugin: 'tool-fold', key: 'open' } as const, false)
7
8// Inputs at or under this size already fit in the engine's own one-line row.
9const SMALL_JSON = 160
10
11type Fold = { source: string; language?: string; path?: string; format?: 'diff' }
12
13const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)
14
15function hunk(before: string, after: string): string {
16 const a = before.split('\n')
17 const b = after.split('\n')
18 return [`@@ -1,${a.length} +1,${b.length} @@`, ...a.map(l => `-${l}`), ...b.map(l => `+${l}`)].join('\n')
19}
20
21// What a call's row unfolds into: the code the model actually sent.
22export function foldOf(tool: string, input: unknown): Fold | undefined {
23 if (typeof input !== 'object' || input === null) {
24 return undefined
25 }
26 const i = input as Record<string, unknown>
27 const path = str(i.file_path) ?? str(i.notebook_path)
28
29 switch (tool) {
30 case 'Bash': {
31 const command = str(i.command)
32 return command ? { source: command, language: 'bash' } : undefined
33 }
34 case 'Write': {
35 const content = str(i.content)
36 return content !== undefined ? { source: content, path } : undefined
37 }
38 case 'Edit': {
39 const before = str(i.old_string)
40 const after = str(i.new_string)
41 return before !== undefined && after !== undefined
42 ? { source: hunk(before, after), format: 'diff', path }
43 : undefined
44 }
45 case 'MultiEdit': {
46 const edits = Array.isArray(i.edits) ? (i.edits as Record<string, unknown>[]) : []
47 const hunks = edits.flatMap(e => {
48 const before = str(e.old_string)
49 const after = str(e.new_string)
50 return before !== undefined && after !== undefined ? [hunk(before, after)] : []
51 })
52 return hunks.length ? { source: hunks.join('\n'), format: 'diff', path } : undefined
53 }
54 case 'NotebookEdit': {
55 const source = str(i.new_source)
56 return source !== undefined ? { source, language: 'python' } : undefined
57 }
58 case 'Agent':
59 case 'Task': {
60 const prompt = str(i.prompt)
61 return prompt ? { source: prompt, language: 'markdown' } : undefined
62 }
63 }
64
65 const json = JSON.stringify(input, null, 2)
66 return json.length > SMALL_JSON ? { source: json, language: 'json' } : undefined
67}
68
69// The full result text for tools whose engine row only shows a preview.
70export function outputOf(tool: string, output: unknown): string | undefined {
71 if (tool !== 'Bash' || typeof output !== 'object' || output === null) {
72 return undefined
73 }
74 const o = output as Record<string, unknown>
75 const parts = [str(o.stdout), str(o.stderr)].filter((p): p is string => !!p && p.trim() !== '')
76 return parts.length ? parts.join('\n') : undefined
77}
78
79export const register: Register = on => {
80 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
81 const row = await next(e)
82 const fold = foldOf(e.props.tool, e.props.input)
83 const { Box, Button, Code } = $.ui.resolve(e)
84
85 if (!fold || !Button || !Code) {
86 return row
87 }
88
89 const member = memberOf(open, e)
90 const isOpen = await read($, member)
91
92 return (
93 <Box flexDirection="column">
94 <Box flexDirection="row">
95 <Button
96 key="fold"
97 plain
98 label={isOpen ? '▾' : '▸'}
99 onPress={() => update($, member, was => !was)}
100 />
101 <Box marginLeft={1}>{row}</Box>
102 </Box>
103 {isOpen ? (
104 <Box key="code" paddingLeft={2}>
105 <Code {...fold} />
106 </Box>
107 ) : null}
108 </Box>
109 )
110 })
111
112 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
113 const full = e.props.isErrored ? undefined : outputOf(e.props.tool, e.props.output)
114 const { Box, Code } = $.ui.resolve(e)
115
116 if (full === undefined || !Code || !(await read($, memberOf(open, e)))) {
117 return next(e)
118 }
119
120 return (
121 <Box key="output" paddingLeft={2}>
122 <Code source={full} />
123 </Box>
124 )
125 })
126}
127types/index.d.ts 8 lines1export type FoldOpen = boolean
2
3declare module 'claude-code' {
4 interface PluginState {
5 'tool-fold': { open: StateFamily<FoldOpen> }
6 }
7}
8