SLOPSHOPPER

makarasty

Run one job from one Claude Code chat, workers as background subagents (separate chats on opt-in): a shared task queue, reports written to files, a frame-count…

newguardprocess
v1.7.0MITupdated 2026-10-19makarasty/makarasty-claude
A shopper browsing a rack in a slop shop
README

makarasty

Two Claude Code plugins from one marketplace.

  • makarasty runs one job from one chat first (several sessions at once only on opt-in): a test sweep of a live app, a fix run over its backlog, a refactor across a hundred files, a research sweep, a design pass. You plan once and answer up to four questions. By default the run stays in one chat: the coordinator works the queue and spawns a fresh subagent per task, which reports by writing files (docs/ONECHAT.md). Separate worker chats, one chip each, are opt-in. Between what a run finds and what it changes there is a gate: no fix without a reproduction that failed first.
  • makarasty-tools is twelve everyday commands that have nothing to do with fleets: commit under your own name, ship a branch, review a diff with evidence, hand a chat off before its context runs out, get a phone message when a chat finishes, tune the machine for a game, find what is worth updating, clean up after chats, and a few more.

Install one or both. They are versioned separately.

Why it exists

An agent that cannot see what it is inspecting still reports findings, with full confidence, and they look exactly like real ones. A browser pane that stopped compositing is the sharpest case: it still navigates, still returns DOM, and every visual observation through it is false. A green suite that skipped your file and a docs page that never loaded produce the same confident nothing.

So every path through this plugin measures before it trusts. A worker counts frames before it looks at a page, a finding without evidence is refused by a script rather than by a request in prose, and a run that had a blind worker says so by name instead of reporting its area as clean.

The second reason is the one most multi-agent setups learn late: a session runs only while something invokes it. In one measured run, three of six workers ended a turn right after claiming their next task and sat dead for about three hours each, holding claims nobody else could take. The protocol here is built so that this can't happen silently. See The other way a run dies.

Install

/plugin marketplace add makarasty/makarasty-claude
/plugin install makarasty@makarasty
/plugin install makarasty-tools@makarasty

Restart Claude Code afterwards; plugins load at session start. To install from a local checkout instead, pass its path to marketplace add.

Requirements

  • Claude Code with function hooks (tested on 2.1.293 and 2.1.294): the fleet's hooks are one in-engine gate, so a chat outside a fleet runs no process for them [M40]. The fleet's browser checks and opt-in worker chips use the desktop app's Code tab (the built-in browser pane and the task chips); a fleet with no browser tasks runs anywhere Claude Code does.
  • A POSIX shell. On Windows that is Git Bash, which Claude Code already needs. The self-test passes under bash and dash (FLEET_SELFTEST_SH=dash node scripts/fleet-selftest.mjs runs it under dash).
  • Node.js 20.11 or newer, for the hooks and for fleet.sh itself: its commands run in scripts/fleet.mjs, where a call that took seconds of shell forks on Git Bash takes a tenth of one [M37]. The plugin ships that JavaScript compiled; nothing is installed or built on your machine.
  • Nothing else. rg, sg, jq and similar tools are offered by fleet-init and never required.

Check that it works on your machine

node scripts/fleet-selftest.mjs

From an install rather than a checkout, the plugin's path is installPath for makarasty in ~/.claude/plugins/installed_plugins.json. The self-test runs the whole protocol (claims, lanes, the schema gate, the clocks, the markers, the landing check) against temporary directories in about 75 seconds on a 16-thread Windows machine, with no sessions, no browser and no tokens. It should end with N passed, 0 failed. --jobs N (or FLEET_SELFTEST_JOBS) sets how many parts run at once, 3 by default, and the run prints the free RAM and the live node.exe count before it starts. To run one part, use node --test scripts/selftest/<part>.test.mjs.

Changing the code

The scripts and hooks are written in TypeScript under src/ (and tools/src/ for the tools plugin) and compiled to the .mjs files beside them, which are committed and are what runs. The two browser probes are the exception: src/probes/*.ts compiles to scripts/*.js. Edit the .mts or .ts, never the .mjs or the probe's .js:

npm install --prefix src
npm run build --prefix src

The first installs the pinned compiler and Node's types into src/node_modules, once. The self-test then also type-checks the sources and fails when a committed .mjs is not what its source compiles to.

Quick start

In the project you want to work on:

/makarasty:fleet-init

It finds the app's origin and services, sets up a login path an agent can use on its own, writes FLEET.md, and tells you how many workers this machine will carry. Then:

/makarasty:fleet-plan <what you want done>

The planner interviews you (at most four questions, asked at once), splits the job into a queue, and runs it in this chat. Only if you chose separate chats does it offer one chip per worker for you to click. When the run lands, /makarasty:fleet-collect <run-id> merges their findings into one ranked backlog.

docs/WALKTHROUGH.md is a first fleet in fifteen minutes, with two workers and no browser.

Fleet commands

CommandStarted byWhat it does
/makarasty:fleetyouLists the other commands and when to use each
/makarasty:fleet-inityou or ClaudePrepares a project: origin, services, agent login, FLEET.md, machine sizing
/makarasty:fleet-plan <mission> [fast]you or Claude, when askedInterviews you into a plan, splits it into a queue or briefs, runs it in one chat; one chip per worker only on opt-in
/makarasty:fleet-run <brief or run dir>the worker (an opt-in chat, after you click its chip)Runs one brief, or works a queue until it is drained
/makarasty:fleet-loginyou or ClaudeOpens and logs in to the project's local app (or at an origin you pass it), with credentials from the runbook, never from the chat
/makarasty:fleet-wait <run-id> [n]you or ClaudeWaits without spending model turns, then collects
/makarasty:fleet-collect <run-id>you or ClaudeMerges, enforces the evidence contract, dedupes, ranks
/makarasty:fleet-analyze <run-id>you or ClaudeHow a run went, from its transcripts: where task time went, one-tool turns, re-reads, cache misses, tokens and an estimated cost per worker and task, the slowest tasks, and what the numbers say to change
/makarasty:fleet-pause <run-id> [off] [reason]you or ClaudeReally stops every worker (a hook refuses their edits, browser and shell calls after a 30 s grace; they commit, stop their subagents and wait), or lifts the pause. "Pause the fleet" in plain words fires it
/makarasty:fleet-resume <run-id>you or ClaudeAfter a crash: reopens the workers whose context survived, respawns the rest
/makarasty:fleet-design <screens> [fast]youCaptures the app's screens as artboards on disk and assembles them into a Claude Design canvas
/makarasty:fleet-redesign <screens and direction> [fast]youProposes redesigns beside the captured screens, loading and error states included
/makarasty:fleet-call <who and what about> [fast]youDigs the facts for a vendor call out of the project, with evidence, and builds a bilingual page to read aloud; live <run-id> answers beside you during the call

fleet, fleet-design, fleet-redesign and fleet-call carry disable-model-invocation: true: they start paid work and wait on your clicks, so only you can start them. fleet-pause is the other one an agent can start. fleet-plan can also be started by Claude when you ask it to launch a fleet or hand a run to a new chat; in one chat it starts spawning subagents after Round 0, and offers chips only on opt-in, so it costs nothing until you answer Round 0. fleet-run is the exception on purpose. The worker's own model invokes it in the new session, and the flag would stop every worker the moment it tried to begin.

Agents

  • fleet-coder does one code task in the shared checkout, touching only the paths its task owns:, and commits nothing. One fresh subagent per task.
  • fleet-reviewer reviews one task's diff and edits nothing.
  • fleet-scenario walks a multi-step browser scenario and returns bounded JSON. Screenshots and DOM reads stay in its context; about eighty tokens come back to the parent.
  • fleet-profiler measures load, interaction and stability, with the spread of each reading and the machine load beside it.
  • fleet-triage merges and ranks a run's findings, on Haiku.
  • fleet-design-eye reviews one screen for design defects: geometry probes first, a zoomed screenshot of each candidate second, and every finding carries the rectangles behind it.

Tools commands

CommandWhat it does
/makarasty-tools:commitCommits this chat's files under your own name, short message, no AI trailer. Commits by path, so a file another chat staged stays out
`/makarasty-tools:ship [branches\all] [mine] [tag <name>]`Merges branches in, splits the tree into your own commits, leaves files a parallel chat is still editing, pushes, tags with notes
`/makarasty-tools:review [--fix\--loop]`Reports only findings that name an input that breaks the code, each one checked by a pass that tries to disprove it. --loop fixes and re-reviews until a round finds nothing
`/makarasty-tools:handoff [focus] \from <chat>`Writes a handoff file and offers the next chat as a chip that can read or ask this one; from takes over another chat's work after checking its claims
/makarasty-tools:hold [codeword]Collects dictated bugs one line each without acting, then on the codeword fixes them grouped by shared cause
/makarasty-tools:explain [topic]What happened, why, what was done, what is left, whether it can ship and what was not checked, in plain words
`/makarasty-tools:unslop [on\off\text]`Turns humanised replies on or off, or rewrites a given text without assistant tics
/makarasty-tools:say <what to say>Turns what you mean into simple English to say on a call or send to a vendor, with a gist in your language beside each line
/makarasty-tools:notify [what you are waiting for]Sends one message to your phone when this chat, another chat or a fleet run finishes: Telegram, Discord, ntfy or a webhook
/makarasty-tools:game <game>Reads the hardware, disks and the game's install, proposes changes with sources, has a red team try to disprove each, and reports what survived. Applies nothing without a yes per item
/makarasty-tools:cleanup [area]Finds processes ended chats left running, runaway logs, stale worktrees and caches, has a red team defend each, ends processes on a yes and gives the commands for files. "почисти за собой" stops what this chat started
/makarasty-tools:update [area]Finds what is out of date (apps, runtimes, Claude Code and plugins, other agents, drivers), ranks it, has a red team challenge each pick, and updates only on a yes

All twelve can be started by you or by Claude, and most trigger on plain phrasing in any language: "commit as me", "закоммить от меня", "ping me when the tests are done".

Context hook. Once a chat's context passes 700k tokens, and again every 150k above that, the chat is told to offer a handoff in one line (a chat that coordinates or works a fleet run gets none: the run's own marks watch it, below). MAKARASTY_HANDOFF_AT and MAKARASTY_HANDOFF_STEP move the levels; MAKARASTY_HANDOFF_AT=0 turns it off.

Model router. A subagent that Opus or Fable spawns without naming a model runs on Sonnet when its description starts with [exec] and on Haiku with [mech]; an Explore agent runs on Haiku. fleet-coder is routed by tag like general-purpose; fleet-reviewer never is (a tag on it is ignored), because it is the check that makes a tag safe. Untagged work keeps the parent's model, and a named model is never changed. /router shows what it changed; MAKARASTY_ROUTER=off turns it off. Why it waits for a tag: docs/MODELS.md, "The router".

Git guard. A shell call that would throw away uncommitted work (git stash, reset --hard, clean -f, checkout -- <path>, restore without --staged, a forced switch) is refused with the reason and a safe way round it, because several chats often share one working tree. Commits, plain switches, stash list and clean -n run. It is a function hook, so no shell call pays a process for it; MAKARASTY_GIT_GUARD=off turns it off.

Reads through tools. One line in the system prompt sends file reads and searches to Read, Grep and Glob instead of cat, grep or sed through the shell: 0.02-0.07 s a call against 0.1-1 s [M40]. MAKARASTY_READS=off turns it off.

Relaunch instead of a chain of handoffs. A queue worker on 1.5.14 or later that nears the context limit (worker_relaunch_k in calibration.json, 700K as shipped) is replaced by the coordinator without asking: it finishes its task and leaves at its next claim, and you click the replacement chip. A fleet coordinator, or a brief worker or an older one, that nears its mark (coordinator_handoff_k and worker_relaunch_k, both 700K, with a second ask at 800K; auto-compaction starts a little past 900K) makes the coordinator ask you once, with a notification you can see from another chat. On "Relaunch now" it pauses the run, writes the state, hands the open work of the over-full workers back (a worker's uncommitted work is committed for it as wip: handed back), and offers one coordinator chip and the fresh worker chips in one go. Click the coordinator first, then the workers; the run stays paused until the new coordinator resumes it. When only workers are over, "Replace workers NN only" offers just their chips and the coordinator carries on.

Notifications. Say "ping me when it's done" in the chat doing the work. It arms a marker for that session, and a hook sends one message when the turn ends, with the last 500 characters of the reply under the verdict. A chat that ends on a question is reported as waiting for you; one that leaves a background job running is not finished and keeps its marker; one that dies on a rate limit says so. Setup is one command in your terminal: a wizard asks for the channel, takes one value (a bot token, a webhook URL or an ntfy topic), and saves it to ~/.claude/makarasty/notify.json only after a test message reaches your phone. The secret never passes through a chat. While no chat is armed the hooks cost one ls per event and start no Node process.

How a fleet works

Two shapes

Assigned (opt-in separate chats only; one chat always files a queue): the planner writes one brief per worker, and the run ends when the briefs do.

Pull: the planner writes a queue, workers claim tasks when free, and the planner keeps adding tasks while they run. A claim is a directory, because mkdir fails atomically on one that exists; with eight concurrent claimers, exactly one won. Use pull when the job is bigger than the plan, which is most of the time. Details in docs/PULL.md.

Lanes

A fleet queues for whatever the machine has exactly one of. Each task declares a lane: pane for the browser, verify for the test suite and typechecker, repo for work that only reads and writes files. The lanes are capped separately. The pane lane is 1 in one chat; with opt-in chats it is capped by your display, ten at most. The repo lane is capped by the machine. Capping file work at the width of a monitor is how a run ends up eight browsers wide and two files wide.

That one field is what makes the plugin more than a browser tool. A run with no pane tasks is a refactor, a migration or a research sweep, and the gate changes its evidence instead of disappearing: a reproduction that fails before a fix and passes after, a verbatim quote with its locator from each source, a test count.

A pane worker spends about 20 of its 23 minutes waiting on one scenario subagent, so it claims one repo task and works it during the wait. Details in docs/LANES.md.

Mission kinds

Each task declares its kind, which sets the working style and the axis the job splits along.

KindSplits byIsolation
verifyscreen ownershipnone
fixfile cluster, re-verifying each entry before repairingworktree
investigatehypothesisworktree when instrumenting
implementseamworktree
researchsourcenone
designone screen, in three waves: recon, primitives, screensworktree
critiqueone screen; the evidence is a rectangle or a ratio, never a screenshot alonenone
canvasone screen per artboard: recon, primitives sheet, screens, compare, assemblenone
redesignone screen; proposals beside the captured ones, directions sketched firstnone
callsource: the facts with their evidence first, then the page you read aloudnone

Splitting along the wrong axis is what makes a fleet run worthless. Two workers on one slice cost twice as much and then agree with each other, which reads as corroboration and isn't.

The last four kinds form a loop: critique what runs, capture it as a canvas, propose beside it, then implement the approved artboards with the design kind. See docs/DESIGN.md and docs/MISSIONS.md.

The pane gate

A browser pane that is not on screen stops compositing, and nothing tells you:

SymptomCause
screenshot times out after 5spane not displayed
requestAnimationFrame never firesnothing is scheduled without compositing
transitions frozen at their start valuetransitionend never fires
virtualized rows read as empty textthey need layout that never runs
in-page requests hang to their timeoutmeasured: an axios POST sat 180s while curl answered in 4s
preview_start returns navOk with the right titlenavigation and titles survive; only frames don't

So every pane worker counts frames first:

new Promise(res => { let f = 0; requestAnimationFrame(function t(){ f++; requestAnimationFrame(t); }); setTimeout(() => res(f), 1000); })

Ten or more is live, read twice a second apart; under ten is blind. From ten to fifty-nine the pane is live on a busy machine: the worker carries on, reports the number, and asks you nothing. A blind worker asks you to open its pane and measures again, because the reading is the proof, not your reply. A worker that stays blind writes .blocked and no findings, and collection lists blocked workers by name. Two panes in separate sessions were measured live at the same time, with the second chat unfocused: what matters is that the pane is displayed, not which chat has focus. More in docs/BROWSER.md.

The other way a run dies

A blind pane is loud once you know the symptom. This one is silent. When a turn ends with no subagent running and no background command pending, the session stops, and nothing in a fleet types into a worker's chat to restart it. Its last message usually says what it was about to do next.

Measured on 2026-08-27: three of six workers ended a turn right after claiming their next task and sat dead for 169, 171 and 176 minutes. The planner slept through it, because its watch reported new files and there were none. One cross-session status check brought all three back within seconds.

So a worker claims and starts work in the same turn, arms sleep 120; echo wake in the background when it has to stop anywhere else, and the planner's watch reports silence as well as progress. A Stop hook catches the case of a fresh claim whose heartbeat never moved. Full rules in docs/PROTOCOL.md.

The gate between a finding and a fix

About 15 findings in 100 were refuted once somebody tried to fix them. So a fix task is refused by finish unless a reproduction ran through fleet-gate.mjs prove, failing before the change and passing after. A cause several findings share is ruled on once, before any of them is patched. An edit that drops a name something outside the repository reads (a route, a config key) is blocked by a hook until a question or a recorded decision names it. See docs/GATE.md.

What it may delete

A fleet runs unattended across a dozen sessions, so what it may remove is a short closed list, not each worker's judgement:

  1. Its own scratch, under .fleet/<run-id>/.
  2. The worktrees its own workers created, under .claude/worktrees/, and only through fleet.sh clean.

Nothing else. There is no git reset --hard, no git clean, no git checkout -- anywhere in the plugin, and no recursive force-delete of a path it did not create.

fleet.sh clean is a dry run unless given --remove. It touches only worktrees this run registered, keeps any tree with uncommitted changes or with commits neither merged nor pushed, and deletes a branch only with git branch -d, which refuses unmerged work. Every deletion passes a path gate: absolute, no .. or . segments, at least four levels below the root, inside .claude/worktrees/ with something after it, and never a directory that contains the shell's working directory.

One step here is not obvious, and it was measured [M32]: a worktree usually has a node_modules junction into the main checkout, and git worktree remove follows that junction and deletes what it points at. Eight runs out of eight on Windows, at the top level and nested, --force included, exit code 0 every time. So clean unlinks every junction and symlink inside a worktree before anything recursive touches it. The same applies to the harness's own ExitWorktree and to any hand-written rm -rf. See docs/WORKTREES.md and docs/SAFETY.md.

Real runs, and what they cost

A selection of real runs, not all of them. They come from two different projects, a large private TypeScript web app and the public Kotlin plugin Essentials, and cover different scenarios: browser sweeps of a running app, audits, fix runs, refactors and performance measurement. Each one is written up in docs/RUNS.md with how it was set up, what it produced, what went wrong, and the cost.

RunWorkersWall clockResultWorker cost
First eight-worker browser sweep8 pane104 min94 findings, 3 blockers; all 8 panes opened blind and the gate caught every one~$140
Pull run, 34 tasks6 pane4 h 57 min254 findings; three workers sat dead for about 3 h eachnot separable
Audit, then fix14, then 8132 + 80 min246 findings, then 38 fixed and merged, 3 refuted~$560 + ~$430
Eighty tasks across three lanes134 h 6 min80 tasks; third verify round green at about 37,000 tests~$1,600
Read-only sweep, then a fix run12, then 4~90 min active, then 3 h 51 min1,500 findings, then 175 tasks worked, 93 fixed~$740 + ~$1,110
Refactor, then shrink9, then 1071 + 107 min96 fixed, 32 refuted; then about 27,000 lines removed (−17.6% against a −25% target)~$480 + ~$740

| Essentials, audit to green | 4-10 per run | ~50 h calendar | 436 findings; 251 tests with 20 failing to 383 with 0, ten green runs of ten | ~$1,400, plus ~

Source 1 files
hooks/gate.ts 49 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// The fleet's hooks, run only in a chat a fleet registered. A command hook is a process per tool call, and
4// every chat with the plugin installed paid it: bash plus node, 0.1-0.2 s idle, about 1 s on a box a fleet
5// was loading [M40]. Here the question "is this chat in a fleet?" is one file check inside the engine, and
6// only a fleet chat reaches the same .mjs hooks with the same stdin and exit-code contract as before.
7
8// The command hooks' two matchers in one literal, so the engine filters before the module runs.
9const WATCHED = /^(Bash|PowerShell|Monitor|Agent|Task|Skill|Edit|Write|MultiEdit|NotebookEdit)$|^mcp__(.*[Bb]rowser|claude-in-chrome)__/
10const EDITS = /^(Edit|Write|MultiEdit|NotebookEdit)$/
11
12type Ran = { exitCode: number; stderr: string }
13
14// registerChip (lib.mts) writes this for every coordinator and worker, under the same configDir().
15async function inFleet($: EngineInterface, sid: string): Promise<boolean> {
16  if (!sid) return false
17  const cfg = (await $.env.get('CLAUDE_CONFIG_DIR'))
18    || `${(await $.env.get('USERPROFILE')) || (await $.env.get('HOME')) || ''}/.claude`
19  return $.fs.exists(`${cfg.replace(/\\/g, '/').replace(/([^:/])\/+$/, '$1')}/makarasty/fleet-sessions/${sid}`)
20}
21
22// A hook that cannot run is not a reason to block work: the command hooks this replaces failed open too.
23async function hook($: EngineInterface, file: string, payload: object, timeoutMs: number): Promise<Ran | null> {
24  try {
25    return await $.process.run(['node', `${$.plugin.root}/hooks/${file}`], { stdin: JSON.stringify(payload), timeoutMs })
26  } catch { return null }
27}
28
29export const register: Register = on => {
30  on('tool.call', { tool: WATCHED }, async ($, e, next) => {
31    const sid = await $.session.id()
32    if (!(await inFleet($, sid))) return next(e)
33    const { tool, tool_use_id: _id, agentId, consent: _c, ...input } = e as Record<string, unknown> & { tool: string; agentId?: string }
34    const edit = EDITS.test(tool)
35    const ran = await hook($, edit ? 'fleet-contract.mjs' : 'fleet-memory.mjs', {
36      hook_event_name: 'PreToolUse', session_id: sid, cwd: await $.session.cwd(),
37      tool_name: tool, tool_input: input, ...(agentId ? { agent_id: agentId } : {}),
38    }, edit ? 5000 : 6000)
39    return ran?.exitCode === 2 ? { deny: ran.stderr.trim() || 'refused by the fleet' } : next(e)
40  }).catch(($, e, next) => next(e))
41
42  on('classic.Stop', async ($, e, next) => {
43    const done = await next(e)
44    if (!(await inFleet($, e.session_id))) return done
45    const ran = await hook($, 'fleet-guard.mjs', e, 5000)
46    return ran?.exitCode === 2 ? { ...done, block: ran.stderr.trim() || 'refused by the fleet' } : done
47  }).catch(($, e, next) => next(e))
48}
49