A local-first software factory. One Go binary that runs repeatable, phased coding-agent workflows against Git repositories: you declare a workflow once, and jig executes it with structure — phases, typed hand-offs, gates that verify claims, bounded repair loops, an enforced write boundary, and a durable record of everything that happened.
The problem it solves: a coding agent invoked as one opaque prompt is done when it stops talking. jig makes "done" a property of the work — every phase passed, the acceptance predicate held, and the change was published with remote proof — and makes the whole run inspectable while it happens.
Local-first by design: loopback HTTP, one trusted operator, SQLite, no
service to sign up for. Delivery is GitHub-coupled in v1 — publish means a
branch and a pull request via gh.
definition (YAML) → run (frozen by value) → job per repository → attempt
│
phase → phase → phase, each gated ────────┘
From source (Go 1.26+, git, and one agent CLI):
git clone https://github.com/StructuPath/jig
cd jig
just build # or: go build -o bin/jig ./cmd/jigNode is never required: the UI is committed and embedded, and the SQLite
driver is pure Go — which is also why one command cross-compiles every
supported platform (just release).
From a release (no Go toolchain needed) — pick your platform, check it
against the published checksums, and put jig on your PATH:
VERSION=v0.2.0
OS=$(uname -s | tr '[:upper:]' '[:lower:]') # darwin or linux
ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
BASE=https://github.com/StructuPath/jig/releases/download/$VERSION
curl -fsSLO "$BASE/jig_${VERSION}_${OS}_${ARCH}.tar.gz"
curl -fsSLO "$BASE/SHA256SUMS"
sha256sum -c SHA256SUMS --ignore-missing # macOS: shasum -a 256 -c
tar -xzf "jig_${VERSION}_${OS}_${ARCH}.tar.gz"
./jig versionVerify the checksum before you run the binary, not after — that line is the only thing standing between a tampered download and an executable you are about to trust with your repositories.
You also need at least one agent CLI, authenticated:
| Runtime | --runtime |
Auth | Roster model: values |
|---|---|---|---|
| Claude Code | claude-code (default) |
claude logged in |
haiku, sonnet, opus, … |
| Codex | codex |
codex login |
Codex model ids, e.g. gpt-5.6-sol |
Model aliases (haiku, sonnet, opus) resolve inside the CLI to the
newest model it knows in that tier, so keep the CLI current: jig run and
jig worker warn at startup when Claude Code is older than 2.1.280, the first
release that serves Opus 5.5.
One process runs one runtime: the engine holds a single adapter per attempt,
so --runtime is an operator choice, not a per-role one. The model: value
in a definition's roster goes straight to that CLI, which is why the stock
definitions (written against Claude Code) need their models renamed before
they run on Codex.
jig run --def examples/definitions/smoke.yaml . "what is this repository?"That is the install check: one genuinely read-only agent phase that validates
and passes on any repository, with no server, no worktree, and no publish. It
prints the attempt's trace path and its verdict. docs/quickstart.md walks
the whole path from here to a browsable accepted run.
jig run — the direct harness. Loads a definition file, freezes a run
against the local repository at its current HEAD, executes the chain
in-process, prints the trace path. No standing server, no publish. This is
the day-one loop and the fastest way to iterate on a definition.
jig serve + jig worker — the server path. serve is the control
plane: HTTP API, embedded UI, lease sweeper, and the admission loop (cron
schedules and GitHub polling) over one SQLite database. worker claims
queued work, materializes an isolated worktree per attempt, runs the chain,
and publishes accepted work as a branch and pull request.
jig def and jig trigger — authoring and admission. Validate a
definition offline, save it, invoke it, and stand up the schedules and GitHub
polls that invoke it unattended. Every read command takes --json.
jig report — measurement. Jobs, accepts, publish outcomes, CI repair,
re-runs, and spend over a time window, read from the control plane's ledger.
A definition is data, not a script. Five stock ones ship in
examples/definitions/:
| Definition | What it demonstrates |
|---|---|
smoke.yaml |
the install check: one read-only phase, repo-independent |
scout.yaml |
read-only recon: two agent phases and the hand-off between them |
two-phase.yaml |
an agent phase that writes, verified by a code phase |
plan-build-test.yaml |
a code-phase repair edge: a red suite routes back to the builder |
simple-sdlc.yaml |
an agent-phase repair edge (review → revise → re-review), a conditional retest, and per-phase commit messages |
factory.yaml |
the software factory: plan → build → commit → test → a panel of specialist reviewers looping the builder until they approve, then a deterministic risk gate that holds high-risk work for a person |
The shape:
name: my-workflow
roster: # one entry per agent role
builder:
model: opus
effort: medium # optional: low | medium | high | xhigh | max
budget_usd: 8 # optional: cap on what one send may spend
system_prompt: | # or system_prompt_path: <repo-relative file>
You are a builder …
user_prompt: |
Task: {{prompt}} # the invocation's prompt, frozen at admission
env: [PATH] # env allowlist — the ONLY variables it receives
writes: ["src/**"] # write allowlist; [] is read-only, omitted is unrestricted
phases:
- name: build
kind: agent # agent | code
owner: builder
gates: # claim verifiers, run after the phase
- {name: artifacts_exist}
- {name: diff_matches_claims}
- name: test
kind: code
command: "go test ./..."
on_fail: {run: build, then: rerun-self, budget: 2, exhausted: fail-job}
- name: retest
kind: code
if: revised # skipped unless a previous envelope set it truthy
command: "go test ./..."
- name: classify-risk
kind: code
reports_fields: true # the last output line is a JSON object of envelope fields
command: "scripts/risk.sh"
- name: review-risk
kind: agent
owner: reviewer
if: "risk == high" # a comparison guard, same language as on_fail.when
acceptance: [all_phases_passed, diff_matches_claims]
publish:
hold_when: "risk == high" # accepted but held for a person; publish retry releases it
ci: {wait: true, timeout: 30m} # accepted only once CI on the PR head is greenRules worth knowing before you write one:
- Envelopes are the only inter-phase contract. Every agent phase ends
with one JSON object carrying at least
status(successorfail). Extra fields —approved,changed_files,revised— are what gates,if:guards, and repair predicates read. Code phases get an adapter envelope built from their exit status, so a failing test suite enters the repair loop through the same door as a failing agent report. - A verdict is stated, never inferred. Under
verdict_consistenta reviewer must writeapprovedas a JSON boolean; omitting it is its own gate failure, not a silent rejection. The distinction matters becauseon_fail: {when: "approved == false"}is how a review routes work back to a builder, and a verdict nobody stated must not decide that either way. - Gates verify claims, they do not judge quality. The registry is
artifacts_exist,files_non_empty,diff_matches_claims,verdict_consistent, andtests_pass(command). Repo-specific verification is a code phase, not a new gate. - Repair loops must be declared and bounded.
on_failis the only loop construct; a cycle or a missing budget is rejected at save time, not discovered at 2 a.m. - Effort is per role, and optional.
effortgoes to Claude Code as--effort, and to Codex asmodel_reasoning_effort(Codex tops out atxhigh, somaxruns there). It sets how much a role thinks and verifies, and thinking is billed as output. A useful split:mediumfor a builder working to a plan,highfor a reviewer hunting the edge cases the build missed. Omit it to keep the CLI's default for the model, and omit it forhaiku, which does not take an effort level. - Spend is capped per role, and optional.
budget_usdis the most one send by that role may cost; Claude Code enforces it as--max-budget-usd. Codex has no such flag, and a cap it cannot enforce fails the send rather than run uncapped — the same posture as atoolsallowlist there. - Code phases can report. A code phase with
reports_fields: trueprints a JSON object as its last output line, and those fields join the envelope view thatif:guards andpublish.hold_whenread. It is how a deterministic script — a risk classifier scoring paths and diff size — gets a say in routing without a model in the loop. The adapter's own fields (status,passed,exit_code, …) are reserved, and a reported field is protected: a later agent envelope cannot overwrite it. Code phases and gates receiveJIG_BASE_SHA, the commit the run was pinned to, so a script can diff the whole change without guessing a base from history. - Guards compare as well as test.
if: revisedruns a phase when a previous envelope set the field truthy;if: "risk == high"runs it when the comparison holds, in the same<field> ==|!= <literal>language ason_fail.when. - Publish can be held.
publish: {hold_when: "<predicate>"}is judged after acceptance passes. When it holds, the attempt endsaccepted_unpublishedwith the hold recorded, nothing is pushed, and the worktree and branch are retained — the publish-only retry is the human sign-off that releases it. Low-risk work never waits. Write the predicate to fail closed —risk != lowholds work whose risk was never reported, whererisk == highwould ship it. The sign-off is only as strong as access to the control plane: jig has one trusted operator and no authentication (see Loopback only), so the operator's retry is the approval, with no separate approver identity. - Publish can wait for CI.
publish: {ci: {wait: true, timeout: 30m}}adds a fourth publish step after proof: the worker polls the check runs and commit statuses on the branch's head throughgh api, and the job isacceptedonly when every one is green — the control plane enforces this from the frozen definition, not the worker's word. A red check fails fast; checks still pending at the timeout (default 30m, 1m–6h) fail too; a head with no checks at all passes after two minutes, since that repository runs no CI. Any of these ends the jobaccepted_unpublishedwith the branch and pull request in place and the red checks named in the result. Fix the branch — push to it yourself — and the publish-only retry judges CI on whatever its head is by then. Neutral and skipped checks count as green. The wait holds a worker slot, and a cancel during it ends the job the same way. Omitciand publish does not wait, exactly as before. - Red CI can be repaired inside the attempt.
publish: {ci: {wait: true, on_fail: {run: build, budget: 2}}}gives red CI back to therunagent phase instead of ending the job. A round hands it the failing checks and the last 16 KiB of each failed GitHub Actions job's log (at most four logs), framed as data, never instructions; then every phase afterrunruns again, gates and repair edges included, and acceptance andhold_whenare judged anew. Only a fix that passes all of that, is not held, and actually changed something is pushed, non-force, to the same pull request, and CI is awaited again. There is no way to skip phases:runmust be an agent phase and not the last one. Rounds share the attempt's send budget and wall-clock ceiling, run in a freshly wiped ephemeral HOME, and are fenced by the lease again right before each push. jig never builds on someone else's commit: if CI is red on a head it did not push, no round runs. When the budget runs out, or a round fails, is held, is cancelled, or changes nothing, the job endsaccepted_unpublishedexactly as red CI did before, with every round's heads, changed paths, and outcome in the result. The publish-only retry judges CI but never repairs — a continuation lives only in the process that ran the chain. Budget is 1–3. A fix that weakens a check instead of the code is the risk to design for:factory.yamlscores edits to CI or lint configuration, and test files that lose more lines than they gain, as not low, so such a "fix" is held for a person. - A read-only review panel can run in parallel.
parallel: [review-correctness, review-security, review-maintainability]runs those phases at once, as one step of the chain. It is opt-in and narrow: one group per definition, two or more consecutive agent phases, each with its own role, every rolewrites: [], and no member'sif:guard reading a field a sibling reports. Each member runs in its own ephemeral HOME and session and is handed the envelope from BEFORE the group, never a sibling's; results merge in declared order, and the next phase gets the last member's envelope. Any worktree change while the group runs — including one a crashed member left — rolls back and aborts the attempt. Rejections resolve after every member finishes: the first member in declared order that rejected with budget left dispatches its repair target and charges only its own budget, and then the whole group runs again, so earlier approvals are re-judged. The group runs at most 1 + the sum of its members' budgets times.examples/definitions/factory-parallel.yamlis the stock factory with its panel grouped;factory.yamlitself stays sequential until the parallel panel has been watched on real work. - Validation happens before anything runs.
jig def validate <file>is the same check the store applies at save time, offline.
jig def validate examples/definitions/simple-sdlc.yaml
jig def create examples/definitions/simple-sdlc.yaml
jig def list --json
jig def invoke --instructions "add a --version flag" --repo github.com/you/repo <definition-id>jig serve # terminal 1
jig worker --runtime claude-code # terminal 2The worker registers (advertising its capacity, its runtime's probed version
and capabilities, and the names — never values — of its environment
variables), reconciles its worktrees against the control-plane ledger, and
then claims. Each attempt gets its own worktree at the run's pinned base SHA
on branch jig/<job-id>/<attempt>, and accepted work is published there:
push, find-or-create the pull request by head ref, then verify the remote
ref as proof.
Two things a worker prints at startup deserve reading: orphan worktrees (reported, never deleted) and stray publish branches — attempt-scoped branches on a remote that no fenced push record explains, which is the visible residue of a zombie attempt that pushed after its lease expired.
Once it is running, the UI's Fleet view (/fleet, or GET /api/workers)
is where you check on it: which workers are registered, which agent runtime
each one owns, how many attempt slots are busy, and whether the control plane
still counts a worker live. That last one is the usual answer to a queue that
will not drain — a worker whose heartbeat lapsed is skipped by the claim
transaction, and the view reports that transaction's own verdict rather than
a second opinion.
Ctrl-C is orderly: it stops new claims, cancels in-flight attempts, records
their terminal state, and destroys their ephemeral scratch.
jig report # the last seven days
jig report --since 24h
jig report --since 2026-09-01T00:00:00Z --until 2026-09-15T00:00:00Z
jig report --json # the GET /api/report object, verbatimjig report reads the control plane's ledger over a window and changes
nothing. A job is in the window when it is terminal and was last updated in
[since, until). --since takes a duration back from now (7d, 24h,
90m) or an RFC3339 timestamp; --until takes a timestamp and defaults to
now. The prose form prints one short table per section:
| Section | What it counts |
|---|---|
| Jobs | terminal jobs by state |
| Accepts | clean accepts (green CI on a head jig pushed, or a definition that does not wait for CI), person-fixed accepts (green CI on a head jig did not push), and accepts the ledger cannot classify |
| Publish | publish outcomes of accepted work, the held rate, and failure codes |
| CI repair | attempts that waited for CI, first-pass greens, repair entry and success rates, rounds, stop codes, and the ci_timeout and retried-still-red rates |
| Re-runs | flaky-check re-runs, and passes that came only after one |
| Spend | total agent spend, spend per clean accept, and each outcome's share |
A rate with nothing to divide by prints n/a, never 0%.
Two limits are printed with every report. jig does not watch CI after an accept, so there is no post-merge CI rate. And spend is a floor: a send killed before it returned a result has no cost to record (the report counts these as unmetered sends), and spend recorded by jig v0.2.0 or earlier omits phases that did not pass.
These are the things that will surprise you if nobody says them out loud.
jig invokes Claude Code with --permission-mode bypassPermissions and Codex
with --dangerously-bypass-approvals-and-sandbox. There is no interactive
approver in an unattended run, and a CLI that stops to ask is a CLI that
hangs until the watchdog kills it.
jig's own write boundary is the containment layer, not the CLI's prompts.
Before every agent phase jig fingerprints the worktree's change-set; after
it, it compares. Anything the role's writes allowlist does not permit is
rolled back and the attempt aborts — never retried. Reverts count as
modifications, renames are decomposed into literal paths, and gitignored
files, the .git directory, and hook paths are all watched, because a
planted hook would execute during jig's own later git commands.
What follows from that: the writes allowlist is the security control you
actually configure. writes: ["**"] (the stock builders' default, so the
examples work anywhere) is the weakest boundary jig can enforce. Narrow it.
And network egress is not restricted in v1 — a per-role tools allowlist is
the control there.
Every attempt's subprocesses get a jig-created HOME and XDG directory tree
containing no operator credentials: ~/.ssh, ~/.config/gh, and
~/.gitconfig are unreachable, and anything an agent writes "to its home"
dies with the attempt. The environment is composed from the role's env
allowlist alone — never your shell's environment.
That containment is also why the CLIs cannot log in on their own, so jig seeds each one's auth material into that HOME:
| Runtime | Seeded from | Into the ephemeral HOME |
|---|---|---|
| Claude Code | on macOS the keychain item Claude Code-credentials (where the CLI keeps its live login), falling back to ~/.claude/.credentials.json; elsewhere that file; plus ~/.claude.json onboarding state |
~/.claude/.credentials.json, ~/.claude.json |
| Codex | $CODEX_HOME/auth.json (default ~/.codex/auth.json), plus config.toml when present |
$HOME/.codex/auth.json, $HOME/.codex/sessions/ |
Two consequences:
--no-seed-authturns seeding off, for a CLI that authenticates through its environment instead (put the variable on the role'senvallowlist — e.g.ANTHROPIC_API_KEY).- Do not put
CODEX_HOMEon a role'senvallowlist. Codex resolves its thread rollouts under$CODEX_HOME/sessions, and the location must stay stable across the sends of one attempt or an id-keyed resume finds no rollout. Allowlisting it points the agent back at your real credential directory and at sessions that outlive the attempt.
Every command uses the same four, so jig is scriptable:
| Code | Meaning |
|---|---|
0 |
accepted — the work passed and the verdict is yes |
1 |
infrastructure failure — jig could not deliver a verdict |
2 |
usage or definition-validation error |
3 |
rejected — jig worked and the answer was no (including a cancelled run) |
1 and 3 are deliberately distinct. "The agent's work was rejected" and
"jig broke" require different responses, and conflating them makes every
wrapper script wrong.
jig serve binds 127.0.0.1 and refuses a non-loopback address without
--allow-non-loopback. There is no authentication and no TLS: the control
plane assumes one trusted operator on one machine. State-changing routes also
reject requests whose Origin is not the server's own, which is what stops a
web page you happen to be visiting from driving your control plane through
your browser.
just check # format, vet, package boundary, definitions, tests, build
just test-race # the concurrency suites under the race detector
just release # cross-compiled binaries into dist/The plan this repository was built from is
docs/plans/2026-08-05-001-feat-jig-software-factory-plan.md. It carries the
requirements (R1–R21) and key technical decisions (KTD1–KTD12) that the code
comments cite by name.
To trial the factory on a real repository and judge it by the numbers, follow
docs/dogfooding.md: preflight, a spend-ceiling check
before each batch, and a results table filled from jig report.
MIT — see LICENSE.