SLOPSHOPPER

tool-fold

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

newrows
v0.1.0no licenseupdated 2026-10-09zainokta/zstack/mods/tool-fold
A shopper browsing a rack in a slop shop
README

zstack

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.

Start

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>".

Hosts

hosthow the manager is loadeddefault team (config [hosts.<host>.roles])
claudeplugin agent zstack:managerClaude only: scouts Haiku, workers Sonnet, reviewers and verifier Opus
codexmanager prompt as developer_instructions, workspace-write sandbox + networkCodex only: scouts/workers gpt-6-luna, planner/debugger/reviewers/verifier gpt-6-sol
omp--append-system-promptomp 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-promptpi only, chosen in zstack setup --host pi (suggested: luna workers, sol planner/reviewers via openai-codex)
opencodeinline zstack-manager agent via OPENCODE_CONFIG_CONTENTopencode 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.

One-time setup for pi, omp, opencode

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.

How it flows

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

Build loop (squad, team and swarm)

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])

Agents talking over the bus

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

Team sizes

sizewhenagents
solotrivial, one obvious edit0 (manager does it)
pairone focused change1 worker + 1 reviewer
squad2–4 independent units≤4 workers, 2 reviewers, 1 tester, scouts, verifier
team5–12 units or several repos≤12 workers, 1 reviewer per 3 workers, 1 tester per 4 workers, planner, verifier, scribe
swarm13+ 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".

What's inside

pathwhat
agents/manager, scout, planner, worker, reviewer, tester, debugger, operator, verifier, scribe (Claude Code plugin agents; zstack brief reuses them for other harnesses)
skills/zstackthe manager playbook: intent → size → flow → spawn → monitor → gates → report
skills/zstack-bushow agents talk: message kinds, handoffs, evidence format
skills/zstack-jevthe exact Jev questions: size, route, loop, finding, approve, scope, complete
skills/zstack-reviewreview rubric and personas (code, architecture, security, contract, scope, infra)
skills/zstack-verifyverification ladder: static → unit → e2e → run → browser → parity/bench → live
skills/zstack-debugroot-cause loop used after two failed fixes
skills/zstack-gitgit policy (default: no commit, current branch, no worktree)
rules/house.mdyour standing rules, injected into every agent brief
config/zstack.tomlcaps, per-host role → model routing, setup suggestions
bin/zstackstdlib-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

CLI cheat sheet

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.

Optional install

./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.

Mods

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>).

modwhat it doesthe pain it targets
intent-gateJev 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-guardBlocks 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-radiusHolds 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-vaultSwaps 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-breakerAfter the same failure twice (or the same error pasted again) tells the model to stop patching and diagnose."still same ahh" loops
evidence-checkFlags a reply that claims "tests pass / fixed / verified" when no test or check ran that turn."you said it pass?"
ship-stateBand above the prompt: branch, ↑↓ vs upstream, staged/modified/untracked, last test result."already pushed to main?"
rules-injectorAdds 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-meterStatus line with context %, tokens and rate-limit usage.cost and rate limits
turn-done-alertToast when a long turn ends or when Claude is waiting on you; a heartbeat while a turn runs long."is it stuck?"
tool-foldClick ▸ 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.

Source 2 files
hooks/register.tsx 127 lines
1import { 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}
127
types/index.d.ts 8 lines
1export type FoldOpen = boolean
2
3declare module 'claude-code' {
4  interface PluginState {
5    'tool-fold': { open: StateFamily<FoldOpen> }
6  }
7}
8