A starter kit for agentic systems on iii

ghola

You clone it. You teach it. It remembers.

iii is the framework and the ecosystem doing the real work here: the turn loop, the tools, worktrees, the forge client, durable queues, the console. You can build on it a piece at a time, adding each worker when you need it, and for a distributed system that is often the right call. ghola is the other route: one clone that has already wired a harness and a factory together, plus the handful of things that make running it easier.

627 tests · M8, the whole lifecycle runs · one clone · needs iii 0.23.0, Python 3.11+, git, gh · MIT
What the kit wires together

A harness constrains one turn. A factory runs many. You need both, and they are not the same job.

Assembling these yourself on iii is a real option, and nothing below is hidden from you. What a kit saves is the wiring and the order to do it in: which workers, on which ports, in what sequence. Which of them has to be up before the next one writes anything.

A harness with no factory gives you a well-behaved agent and no way to get work through it. A factory with no harness runs unattended and cannot tell you what it was allowed to do. That split is also how the improve lane sorts its proposals.

The harness: one turn, constrained

Six phases, each with its own model, thinking level, turn cap and tool grant. Four callbacks around the turn. The prompts it is actually asked, in prompts/. The ladder deciding what a rule refuses, in the rule's own words.

The factory: many turns, to a diff

A stage graph where every transition is a durable queue message, so a crash resumes rather than restarts. A worktree per job with a claim that stops two racing. A delivery gate over the finished diff, then a pull request nothing can merge for you.

flowchart TB
    subgraph f["THE FACTORY: many turns, to a diff"]
        direction LR
        s(["a spec"]) --> g["the stage graph
durable between stages"] g --> gate["the delivery gate
over the finished diff"] gate --> pr(["a pull request"]) end subgraph h["THE HARNESS: one turn, constrained"] direction LR brief["the brief
and the prompt"] --> grant["the grant
what this phase may call"] grant --> cb["four callbacks
around the turn"] cb --> lad["the ladder
refuses in the rule's own words"] end g -. "starts a turn" .-> brief lad -. "an outcome" .-> g classDef fac fill:#23150C,stroke:#6E2E13,color:#E7C982 classDef har fill:#1A100A,stroke:#A4470F,color:#E8A33D classDef out fill:#3A2112,stroke:#E7C982,color:#E7C982 class g,gate fac class brief,grant,cb,lad har class s,pr out
the factory decides what happens next; the harness decides what is allowed to happen at all
What it is

iii is the product. ghola is a starter kit for it.

iii ships the turn loop, the tools, git worktrees, the GitHub client, the approval gate, durable queues, and the console. That is where the engineering is, and it is worth reading on its own terms at iii.dev. The rule here is that if a worker does it, ghola does not, which is why there is so little of ghola to read.

Two workers are ghola's own, because nothing else carries these ideas: the constraint and capability ladder, and the append-only record. Both ship inside the repository, so one clone is the whole thing.

Neither registers a tool any phase is granted. They serve the factory, the callbacks and you, never a model mid-turn, so a phase's tools still all belong to a stock iii worker.

Each also lives on its own, at ladder and audit-log, which is where each becomes a worker other projects install. Point LADDER or AUDITLOG at a checkout and it serves instead of the copy, because the seam is the function id rather than an import. A starter kit whose best ideas are locked inside it is a worse starter kit.

The whole lifecycle runs

A spec becomes a plan, a diff, a proof, a review, a commit through your repository's own hook, and a pull request. Comment on it and the job reworks.

The forge is a setting

GitHub and local both ship, and a third is a file in forges/. Local needs no account and no token: the request for review is a file in the repository.

Configuration, not a fork

The flow of work, the development process, how much a person watches. Three files and a prompts directory, and make config prints every effective value with where it came from.

It reads its own record

The improve lane reads the audit log and the job records, not anyone's memory of the week. It drops any proposal it cannot trace to evidence, rather than repairing it.

Extension is two mechanisms

Drop a Python file in actions/, guards/ or predicates/ and ghola finds it by filename. Or name any function id on the bus and write it in whatever language you like.

Once you clone it, it is yours

No upgrade path, on purpose rather than because I ran out of time. Nothing here will migrate your configuration, because a starter kit that owned it would not be yours.

Three steps

It works before you configure anything. Step two is optional, and step three is the one that costs money.

ghola runs on built-in defaults with an empty settings/, so a fresh clone does the whole lifecycle without a decision from you. The shortest honest path points it at a scratch repository with no forge at all, and the first pull request then costs one turn and no GitHub account.

make config is what makes that safe rather than mysterious. It prints every effective value with the source that produced it, so a default is never a magic number you go hunting for in the code.

1. clone the repo          git clone https://github.com/tacoda/ghola.git && make setup
2. add config and scripts  edit settings/, drop files in actions/
3. tell it to do work      make submit SPEC=specs/x.md REPO=../repo
# repos.local.toml, and no account, no token, no slug
[repos."/Users/you/code/scratch"]
forge = "local"
base  = "main"
flowchart LR
    spec(["a spec"]) --> plan --> run --> prove --> review --> commit
    commit -- "your hook refused" --> run
    commit -- "your hook accepted" --> pr(["a pull request"])
    pr --> you(["you"])
    you -- "a comment" --> run
    you -- "a merge" --> landed(["landed"])

    classDef edge fill:#1A100A,stroke:#A4470F,color:#E8A33D
    classDef stage fill:#23150C,stroke:#6E2E13,color:#E7C982
    classDef gate fill:#3A2112,stroke:#E7C982,color:#E7C982
    class spec,pr,landed edge
    class plan,run,prove,review,commit stage
    class you gate
    
the whole lifecycle, and the one step that is always yours

make doctor checks your tools, the harness pin, your key, and your gh login. Look there first when a job fails and nothing says why.

Changing it

Config for a value, a script for a judgment. Take the cheapest one that can say the thing.

Every decision has a home, a built-in default, and a way to override it. Most of what you want is a key in a YAML file. The rest is a Python file dropped into a named directory, found by its filename, with no registration step and no import to add.

a keyone value in settings/*.yaml, overriding one default
a blocka whole phase, stage or contract at once
a scriptdrop it in actions/, guards/, parsers/, predicates/ or forges/
a function idany worker on the bus, in whatever language you like
# settings/phases.yaml — one key, and the rest of the phase keeps its defaults
phases:
  review:
    thinking_level: high
    max_turns: 30
# predicates/no_secrets.py — no ghola imports, so you can run it directly
def check(path: str, content: str, context: dict) -> list[dict]:
    return [{"line": n, "why": "a credential in source"}
            for n, line in enumerate(content.splitlines(), 1)
            if PATTERN.search(line)]

A name that resolves to nothing is an error rather than a no-op. make pipeline reports it before a job has paid for a worktree and a plan. What does not belong here is your repository's own conventions: those go in its CLAUDE.md, versioned with the code they describe.

The idea

A constraint has a rung: the mechanism that carries it.

Writing a rule down is rung zero, and rung zero enforces nothing. Each rung above it puts the same rule further out of reach of what it constrains. The interesting question about a rule is not whether the agent agrees with it, but which mechanism still stops it on a bad day.

0 prosestated in the rules, and nothing enforces it
1 tool grantthe phase was never handed the tool
2 hookthe repository's own hook refuses
3 in-turna callback refuses the call before the target runs
4 stage gatethe delivery gate, over the finished diff
5 CIout of reach of both the agent and the factory

Capability climbs the other way

A constraint withholds, and gets stronger as it moves out of the agent's reach. A capability grants, and gets stronger as it reaches further. Both are primitives of the same shape, and both are served by the same worker.

0 prosedescribed somewhere, and whoever reads it has it
1 projectthis repository, and nothing else
2 teamevery repository the team owns
3 orgeverybody
4 toolevery agent, everywhere, by name

The two ladders join at rung 1, the grant, which is the sum of the capabilities that reached a phase minus everything a constraint took back. ladder::list answers both sides at once, and the ladder worker serves the whole promote, demote, add and remove lifecycle, not ghola.

iii trigger ladder::list
iii trigger ladder::move --json '{"id":"no-secrets","move":"carry","at":"delivery"}'
Oversight

A dial, not a switch.

"Dark factory" is a useful phrase and a bad setting. Nobody wants a system where no human sees anything, and nobody wants to approve every read either. The level is a dial, and a stage may override it, because run and review want different answers.

levela person answersrefuses without asking
manualevery callnothing
attendedevery writereads run
supervisedonly what a rule marks askthe ladder, deterministically
darknothingthe ladder, and ask degrades to refuse

An ask never becomes an allow, at any level. An unattended factory reading "ask" as "yes" has answered a question nobody put. The default is supervised.

What it looks like

One terminal and one console. There is no third surface.

This is the part a kit is actually for. The workers underneath would run without any of it. What ghola adds is the management: one place to ask what is configured, what is up, what a job did, and what it cost.

make is that whole operator surface, and every target is four lines of shell you can read. The $ column marks the ones that send a paid turn, because a command that spends money should say so before you run it.

askand it answers
make doctorwhat is missing, before you spend a turn finding out
make configevery effective setting, and where each value came from
make pipelinethe stage graph as it will run, and what is wrong with it
make statuswhat is up, and which provider is serving the swappable pair
make jobsevery job, newest first
make auditthe append-only record: does it still verify, and what does it count
make improveread what went wrong, propose what would have helped
make help, listing the targets grouped as getting started, running it, doing work, improving it and checking it, with a dollar sign marking every target that sends a paid turn
make help

While a job is up, the picture worth having open is the iii console. Each phase is one traced session, named for the job and the phase. The span count beside it is every tool call that phase made.

The iii console, chat on the left and a live trace panel on the right, listing one traced session per phase for each job
the iii console, on port 3133
Before you adopt this

What ghola does not do.

Every tool's documentation says what it does. I put the limitations page first in the reading order instead. Deciding against this cheaply and early costs you less than finding these out in week two. None of it is a promise to fix, and some of it is on purpose.

  • Nothing merges itself. No configuration removes the pull request. The gate is not a setting, so there is nothing here to turn off.
  • No upgrade path. Clone it and it is yours. Want a later change? Read the diff and take the parts you want.
  • No CLI. make is the whole operator surface. Every target is four lines of shell, and you can read all of them.
  • No HTTP surface of its own. The iii console is the interface. A dashboard of ours would be a second thing to maintain and a worse trace view.
  • The harness pin is load-bearing. On 1.8.7 the engine honors a pre-trigger hook's deny. On 1.8.1 it ignored that deny and ran the call anyway, so a ladder mounted there looks wired and enforces nothing.