SLOPSHOPPER

harness-ops

Agents (builder, tester, researcher, research-auditor, reviewer, architect, suite-auditor), skills (lead, research, harness), guard hooks, and a status band…

newbandcommandtoastprocesstimer
★ 3v0.1.14MITupdated 2026-10-22Marius-Juston/agent-harness/plugins/harness-ops
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · harness-ops
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /ops ⎿ harness-ops: No status: run `uv run dev setup` in the repository root. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

agent-harness

A harness for repositories that many Claude Code agents work on at once, on one or several machines: one lead session per machine plans and integrates, worker subagents each take one GitHub issue in their own worktree and hand back a gated pull request. It is one source of truth in three parts, so projects that use it do not drift apart:

PartWhatHow a project gets it
devkit/The uv run dev command: the gate (dev check), the board on GitHub issues, symbol navigation, PR and integrate, research checks (cite-check, lit), setupa pinned dependency (git tag)
plugins/harness-ops/Claude Code plugin: agents (builder, tester, researcher, research-auditor, reviewer, architect, suite-auditor), skills (lead, research, harness), guard hooks, a status band with /opsthe marketplace in this repository, pinned to the same tag
template/Copier template: settings, CI, pre-commit, .gitattributes, CLAUDE.md and docs skeletons, budgetscopier copy, then copier update (three-way)

The design and its evidence: docs/research/agent-harness.md. The protocol agents follow: docs/specs/multi-agent.md.

<img src="docs/assets/demo-new-project.svg" width="100%" alt="A terminal walkthrough in six scenes: copier copy creates my-project; uv run dev setup prints ok for each step; the repository goes on GitHub and uv run dev board new files issue #1 with a DONE WHEN; claude starts and the /goal prompt is pasted; the lead dispatches harness-ops:builder, which opens PR #2, harness-ops:reviewer approves it and uv run dev integrate re-gates and merges it; gh issue list shows #1 closed."> The commands are the quickstart below; an existing project and an update have their own, further down.</sub></p>

Quickstart

uvx copier copy gh:Marius-Juston/agent-harness my-project
cd my-project
uv run dev setup      # git init, uv sync, generated docs, git config, prek hooks, the Claude Code plugins

Then commit, create the GitHub repository (gh repo create), run uv run dev board init once, and file work with uv run dev board new (each item gets a measurable --done). Start claude and paste the /goal prompt from the new project's docs/SESSION.md, or type continue for one pass that stops at the first decision. The session is the lead: it gives each startable issue to a worker in its own worktree, and every PR is re-gated and merged only by uv run dev integrate. It stops when the board needs you.

Prerequisites: git, uv, gh (authenticated), jq, Node.js 20+ and Claude Code.

The plugin alone, in any project:

/plugin marketplace add Marius-Juston/agent-harness
/plugin install harness-ops@agent-harness

Its agents and skills are namespaced: harness-ops:builder, /harness-ops:lead. Most hooks call uv run dev, so the plugin is meant for projects made from the template.

Adding it to an existing project

<img src="docs/assets/demo-existing-project.svg" width="100%" alt="A terminal walkthrough in five scenes: in an existing app with a clean git tree, copier copy asks to overwrite pyproject.toml and .gitignore and skips CLAUDE.md and README.md; git diff shows the replaced version and dependencies, which are put back with an editor and uv add; @.devkit/HARNESS.md is appended to CLAUDE.md; uv run dev setup, a commit and uv run dev check pass.">

cd my-app                                          # a clean git tree, so git shows what the template changes
uvx copier copy gh:Marius-Juston/agent-harness .   # yes to overwrite pyproject.toml and .gitignore
git diff                                           # put back your [project] fields (`uv add` your dependencies) and ignores
echo '@.devkit/HARNESS.md' >> CLAUDE.md            # your CLAUDE.md and README.md are kept: import the agent rules
uv run dev setup && git add -A && git commit -m "Adopt agent-harness" && uv run dev check

Commit before the first uv run dev check: its test-weakening check reads the template's pytest settings as an edit until they are on the branch. The template's lint and test settings now cover your code, so the first gate may find things to fix; fix them or set the rules in pyproject.toml, never by weakening a check. Then continue as in the quickstart: uv run dev board init, uv run dev board new, claude.

Research API keys

uv run dev lit (the shared literature search) works without keys, but OpenAlex's and Semantic Scholar's shared pools throttle parallel researchers hard and degrade discovery. Get both free keys and store them in ~/.config/api-keys.env (mode 600, outside any repository) with read -rsp, so they are never echoed, kept in shell history or pasted into a Claude chat. Do not export them in your shell profile: an exported key sits in every agent's environment. dev lit reads the file itself, and the plugin's Bash guard and the project's Read deny rule keep agents from reading it. The steps: docs/SETUP.md, which every project made from the template carries too; uv run dev doctor reports each key as found or missing.

Results from Semantic Scholar come from the Semantic Scholar Academic Graph; work that publishes them cites Kinney et al., The Semantic Scholar Open Data Platform (2023), as the API license asks.

Configuration

What differs between projects lives in the project: [tool.devkit] in its pyproject.toml (the keys and defaults are in devkit/config.py), word budgets and test floors in .devkit/ratchets.toml, the area-label map in .github/labeler.yml, and its own CLAUDE.md. Everything generic is changed here, released as a tag, and pulled into a project with uvx copier update --data harness_ref=<tag>, which also moves the devkit and plugin pins.

Updating a project

<img src="docs/assets/demo-update.svg" width="100%" alt="A terminal walkthrough in four scenes: copier check-update finds v0.1.13; on a new branch copier update changes five files and git diff shows the devkit, source and plugin pins moving from v0.1.12 to v0.1.13; uv sync, dev docs and dev check pass; uv run dev setup moves the plugin to the new pin and the branch is pushed.">

uvx copier check-update   # is a newer release out? (`uv run dev doctor` warns too, for the template and plugins)
uvx copier update --data harness_ref=<tag>   # three-way: your edits are kept; the flag moves the pins
uv sync && uv run dev docs && uv run dev check
uv run dev setup          # moves the installed plugin to the new pin

copier update never touches a project's CLAUDE.md or README.md (_skip_if_exists); the generic agent rules are the template-owned .devkit/HARNESS.md (source), which CLAUDE.md @-imports. A project made before v0.1.5 replaces the generic sections of its CLAUDE.md with the line @.devkit/HARNESS.md once, by hand, after its first such update.

Working on this repository

It runs its own harness on itself ([tool.devkit] in pyproject.toml): uv sync, then uv run dev check. CI also renders the template into a fresh project and gates it (scripts/render_check.sh), and runs claude plugin validate --strict. A test scans every file for strings that must not be public (tests/test_scrub.py). From a clone to a pull request: CONTRIBUTING.md; vulnerabilities: SECURITY.md.

License

MIT, see LICENSE.

Source 2 files
hooks/register.tsx 81 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { OpsStatus } from '../types'
5
6// One source: the repository's own `dev status --json` (devkit/workflow.py). The band, /ops and agents read it.
7const status = atom({ plugin: 'harness-ops', key: 'status' } as const, null)
8const isHidden = atom({ plugin: 'harness-ops', key: 'isHidden' } as const, false)
9
10const POLL_MS = 20_000
11const LOW_MEM_GB = 4
12
13export function line(s: OpsStatus): string {
14  const agents = s.worktrees
15    .filter(w => w.branch !== 'main')
16    .map(w => `${w.branch.replace(/^worktree-/, '')}+${w.ahead}${w.dirty ? '*' : ''}`)
17  const quota = s.quota_pct == null ? 'quota ?' : `quota ${s.quota_pct}%/${s.quota_cap}%`
18  const gate = s.gate.startsWith('running') ? 'gate running' : 'gate idle'
19  return `${gate} · agents ${agents.join(' ') || 'none'} · ${s.mem_gb} GB free · ${quota}`
20}
21
22let root = ''
23let warned = ''
24
25async function refresh($: EngineInterface) {
26  if (!root) return
27  try {
28    const ran = await $.process.run([`${root}/.venv/bin/dev`, 'status', '--json'], { cwd: root, timeoutMs: 20_000 })
29    if (ran.exitCode !== 0) return
30    const next: OpsStatus = JSON.parse(ran.stdout)
31    const alarm =
32      next.mem_gb < LOW_MEM_GB
33        ? `Low memory: ${next.mem_gb} GB free (hold heavy agents)`
34        : next.quota_pct != null && next.quota_pct >= next.quota_cap
35          ? `Quota at ${next.quota_pct}% (cap ${next.quota_cap}%): stop spawning, commit and hand off`
36          : ''
37    if (alarm && alarm !== warned) $.ui.toast(alarm)
38    warned = alarm
39    await update($, status, () => next)
40  } catch {
41    // Not a devkit project or no venv yet (`uv run dev setup`): show nothing.
42  }
43}
44
45export const register: Register = on => {
46  on('session.start', async ($, e, next) => {
47    const started = await next(e)
48    const cwd = await $.session.cwd()
49    const probe = await $.process.run(['test', '-x', `${cwd}/.venv/bin/dev`]).catch(() => null)
50    if (probe?.exitCode === 0) {
51      root = cwd
52      await $.command.register({ name: 'ops', description: 'Gate, agents, memory and quota (no Claude turn); /ops hide|show' })
53      await refresh($)
54      $.clock.every(POLL_MS, () => refresh($))
55    }
56    return started
57  })
58
59  on('command.run', { command: 'ops' }, async ($, e) => {
60    const arg = e.args.trim()
61    if (arg === 'hide' || arg === 'show') {
62      await update($, isHidden, () => arg === 'hide')
63      return { text: `Ops band ${arg === 'hide' ? 'hidden' : 'shown'}.` }
64    }
65    await refresh($)
66    const s = await read($, status)
67    return { text: s ? `${line(s)}\n${s.quota}` : 'No status: run `uv run dev setup` in the repository root.' }
68  })
69
70  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
71    const s = await read($, status)
72    if (!root || e.props.hasSurvey || s === null || (await read($, isHidden))) return next(e)
73    const { Box, Text } = $.ui.resolve(e)
74    return (
75      <Box>
76        <Text dimColor>{line(s)}</Text>
77      </Box>
78    )
79  })
80}
81
types/index.d.ts 16 lines
1export type Worktree = { path: string; branch: string; ahead: number; dirty: number; last: string }
2export type OpsStatus = {
3  gate: string
4  mem_gb: number
5  worktrees: Worktree[]
6  quota: string
7  quota_pct: number | null
8  quota_cap: number
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'harness-ops': { status: OpsStatus | null; isHidden: boolean }
14  }
15}
16