Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ You also need at least one agent CLI, authenticated:
| 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
Expand Down Expand Up @@ -117,14 +122,17 @@ A definition is data, not a script. Five stock ones ship in
| `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:

```yaml
name: my-workflow
roster: # one entry per agent role
builder:
model: sonnet
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: |
Expand All @@ -146,7 +154,17 @@ phases:
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
```

Rules worth knowing before you write one:
Expand All @@ -169,6 +187,40 @@ Rules worth knowing before you write one:
- **Repair loops must be declared and bounded.** `on_fail` is 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.** `effort` goes to Claude Code as
`--effort`, and to Codex as `model_reasoning_effort` (Codex tops out at
`xhigh`, so `max` runs there). It sets how much a role thinks and verifies,
and thinking is billed as output. A useful split: `medium` for a builder
working to a plan, `high` for a reviewer hunting the edge cases the build
missed. Omit it to keep the CLI's default for the model, and omit it for
`haiku`, which does not take an effort level.
- **Spend is capped per role, and optional.** `budget_usd` is 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 a `tools` allowlist there.
- **Code phases can report.** A code phase with `reports_fields: true`
prints a JSON object as its last output line, and those fields join the
envelope view that `if:` guards and `publish.hold_when` read. 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 receive `JIG_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: revised` runs 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 as
`on_fail.when`.
- **Publish can be held.** `publish: {hold_when: "<predicate>"}` is judged
after acceptance passes. When it holds, the attempt ends
`accepted_unpublished` with 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 != low` holds work whose risk was never reported,
where `risk == high` would 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.
- **Validation happens before anything runs.** `jig def validate <file>`
is the same check the store applies at save time, offline.

Expand Down
3 changes: 3 additions & 0 deletions cmd/jig/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ func runCommand(ctx context.Context, args []string, stdout, stderr io.Writer) in
fmt.Fprintf(stderr, "jig run: %v\n", err)
return exitInfraFailed
}
if selected.Warning != "" {
fmt.Fprintf(stderr, "jig run: warning: %s\n", selected.Warning)
}

result, err := controlplane.DirectRun(ctx, controlplane.DirectRunConfig{
DataDir: *dataDir,
Expand Down
11 changes: 10 additions & 1 deletion cmd/jig/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type selectedRuntime struct {
Runtime runtime.Runtime
Capability protocol.RuntimeCapability
Seeder engine.HomeSeeder
// Warning is an operator-facing note from the probe (an out-of-date CLI),
// or "". It never blocks: the probe already proved the CLI runs.
Warning string
// Scripted reports the test hook: the enginetest fake needs no auth
// material, so seeding is skipped for it entirely.
Scripted bool
Expand Down Expand Up @@ -97,7 +100,13 @@ func selectRuntime(ctx context.Context, name string, seedAuth bool) (selectedRun
if !seedAuth {
seeder = nil
}
return selectedRuntime{Runtime: adapter, Capability: capability, Seeder: seeder}, nil
warning := ""
if name == runtimeClaudeCode {
warning = claudecode.VersionWarning(capability.Version)
}
return selectedRuntime{
Runtime: adapter, Capability: capability, Seeder: seeder, Warning: warning,
}, nil
}

// seedClaudeAuth is the darwin-aware Claude Code seeder (KTD11): the minimum
Expand Down
3 changes: 3 additions & 0 deletions cmd/jig/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ func workerCommand(ctx context.Context, args []string, stdout, stderr io.Writer)
fmt.Fprintf(stderr, "jig worker: %v\n", err)
return exitInfraFailed
}
if selected.Warning != "" {
fmt.Fprintf(stderr, "jig worker: warning: %s\n", selected.Warning)
}

// Every control-plane write rides a context the interrupt does not
// cancel: an interrupted attempt still has to report its terminal state,
Expand Down
7 changes: 6 additions & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Common first-run failures:
environment variable (and add that variable to the role's `env` allowlist).
- *“seed codex auth: … is unreadable”* — run `codex login`, then
`jig run --runtime codex …`. Also change the roster's `model:` values: the
stock definitions name Claude Code models (`haiku`, `sonnet`), and the value
stock definitions name Claude Code models (`haiku`, `sonnet`, `opus`), and the value
is passed to whichever CLI you selected.

Everything the run did is in the trace file, one JSON event per line:
Expand Down Expand Up @@ -188,6 +188,11 @@ lands the job in `accepted_unpublished` — a distinct state with a
publish-only retry, not a failure: the phases passed and the work is on the
branch `jig/<job-id>/<attempt>`.

The same state is where a definition's `publish.hold_when` parks work on
purpose: `factory.yaml` scores every change for risk and holds high-risk
work with `publish: held` in its result. Read the risk reviewer's briefing in
the trace, then the publish retry is your sign-off.

`Ctrl-C` stops the worker in order: no new claims, in-flight attempts
cancelled, terminal states recorded, ephemeral scratch destroyed.

Expand Down
Loading
Loading