Autonomous Orchestration
jig includes a built-in daemon that supervises workers, monitors their PRs, and intervenes when things go wrong — no cron jobs, no bash scripts, no manual babysitting.
This page explains how it all works: spawn, the event system, the daemon tick loop, nudges, PR lifecycle monitoring, and how jig ps --watch ties it together.
The big picture
You write a ticket
→ jig spawn launches an agent in a tmux window
→ Agent works autonomously (commits, pushes, opens PR)
→ Daemon watches activity via events
→ If idle/stuck: nudge the agent via tmux
→ If PR has issues: nudge about CI/conflicts/reviews
→ If max nudges exceeded: notify you
→ If PR merged/closed: clean up automatically
You stay in the loop through jig ps --watch, which shows a live dashboard of all workers, their state, and PR health.
Spawn: launching autonomous workers
jig spawn creates a git worktree, opens a tmux window, and starts an agent session:
# Spawn with free-text context
jig spawn feature-auth --context "Implement JWT authentication"
# Spawn from an issue file
jig spawn feature-auth --issue features/jwt-auth
# Spawn in auto mode (fully autonomous, no human interaction)
jig spawn feature-auth --issue features/jwt-auth --auto
Auto mode
The agent is always launched with --dangerously-skip-permissions and a structured preamble that tells the agent:
- It’s autonomous — don’t ask for confirmation, don’t enter plan mode
- Its goal is to complete the task and create a draft PR
- A daemon is watching — if it goes idle for ~5 minutes, it’ll get nudged
- If it’s truly stuck, it should explain what’s blocking it
This preamble is a Handlebars template (spawn-preamble) that can be customized per-repo.
What spawn registers
When you spawn a worker, jig:
- Creates the worktree and branch
- Records the worker in
.jig/state.json(local orchestrator state) - Emits a
Spawnevent to the worker’s event log - Opens a tmux window in a
jig-<repo>session - Sends the agent command to the window
The event system
Every worker has a JSONL event log at ~/.config/jig/state/events/<repo>-<worker>/events.jsonl. Events are appended by git hooks (post-commit, post-merge) and by the daemon itself.
Event types
| Event | Source | Meaning |
|---|---|---|
Spawn |
jig spawn |
Worker created |
ToolUseStart / ToolUseEnd |
Claude hooks | Agent is actively using tools |
Commit |
post-commit hook | Code committed |
Push |
post-commit hook | Code pushed |
PrOpened |
Daemon discovery | PR found for the branch |
Notification |
Agent | Agent hit an interactive prompt |
Stop |
Agent exit | Agent session ended |
Nudge |
Daemon | Nudge delivered |
Terminal |
Daemon | Worker cleaned up |
State derivation
Worker state is derived by replaying the event stream — there’s no mutable state database. The reducer walks events in order and produces a WorkerState:
| Status | Meaning |
|---|---|
initializing |
Worktree created, on-create hook still running |
spawned |
Created, no tool use activity yet |
running |
Tool use events flowing |
idle |
Agent exited, at shell prompt |
waiting |
Agent hit an interactive prompt |
stalled |
No events for 5+ minutes (configurable) |
review |
Non-draft PR opened, waiting on human review |
draft |
Draft PR opened, agent still working |
approved |
PR approved |
merged |
PR merged (terminal) |
failed |
Error or killed (terminal) |
archived |
Cleaned up (terminal) |
The silence check is key: if no events arrive for silence_threshold_seconds (default 300s), and the worker isn’t terminal or in review, it transitions to stalled. This is what triggers idle nudges.
The daemon tick loop
The daemon runs a periodic loop (default every 30 seconds). Each tick:
- Sync repos —
git fetchthe base branch for each registered repo - Discover workers — Scan
~/.config/jig/state/events/for active event logs - For each worker:
- Read the event log and derive current state
- Discover PRs if not already known (via
gh pr list) - Compare against previous state (from
~/.config/jig/workers.json) - Dispatch actions based on state transitions
- Check PR lifecycle (CI, conflicts, reviews, commits)
- Execute actions (nudge, notify, cleanup)
- Save state — Write updated
workers.json
Actor architecture
The tick thread stays responsive by offloading all blocking I/O to background actor threads:
| Actor | Purpose |
|---|---|
| sync | git fetch for registered repos |
| github | PR status, CI checks, review comments |
| issue | Poll for spawnable issues |
| spawn | Create worktrees and launch agents |
| prune | Remove worktrees for merged/closed PRs |
| nudge | Deliver nudge messages via tmux |
Each actor uses bounded flume channels for non-blocking communication. The nudge actor is critical — it prevents tmux send-keys from blocking the tick thread when a pane can’t accept input.
All tmux subprocess calls have a 5-second timeout. If a tmux command hangs, the child process is killed and reaped, returning a TimedOut error.
Running the daemon
The daemon runs in two ways:
jig ps --watch— Runs the tick loop inline, displaying a live table with keypress-driven log toggle. Best for active supervision.jig daemon— Runs headless. Good for persistent background supervision.
Both share the same run_with() entrypoint — ps --watch just adds the display layer and keypress handling.
Nudges: intervening when agents get stuck
Nudges are messages sent to agents via tmux send-keys. The daemon classifies what kind of nudge is needed, renders a template with contextual information on the tick thread, and dispatches delivery to the nudge actor — a background thread with its own TmuxClient. This ensures that slow or hanging tmux calls never block the tick loop.
The daemon also checks that the pane is actually running a command before nudging. It filters out shell prompts (bash, zsh, fish, sh), tmux itself, and version-like strings (2.1.72) that tmux can report as the pane command.
Nudge types
| Type | Trigger | What it does |
|---|---|---|
| idle | Worker is stalled or idle, no PR |
Asks for a status update. If there are uncommitted changes, pushes toward committing and opening a PR. |
| stuck | Worker is waiting (interactive prompt) |
Sends an auto-approve keystroke to dismiss the prompt, then a message. |
| ci | CI failing on open PR | Lists the failing checks, tells the agent to fix and push. |
| conflict | Merge conflicts on open PR | Tells the agent to rebase and resolve. |
| review | Unresolved review comments on PR | Tells the agent to address feedback. |
| bad-commits | Non-conventional commits on PR | Lists the bad commits, tells the agent to reword them. |
Escalation
Each nudge type has an independent counter. After max_nudges (default 3) of the same type, the daemon stops nudging and fires a Notify action instead — alerting you that the worker needs human attention.
The nudge templates are Handlebars and can be overridden per-repo by placing custom templates in your repo’s template directory.
Example: idle nudge
STATUS CHECK: You've been idle for a while (nudge 2/3).
You have uncommitted changes but no PR yet. What's blocking you?
1. If ready: commit (conventional format), push, create PR, update issue, call /review
2. If stuck: explain what you need help with
3. If complete but confused: finish the PR
Example: CI nudge
CI is failing on your PR (nudge 1/3).
Fix these issues:
- lint: cargo clippy found 3 warnings
- test: 2 tests failing in auth module
STEPS:
1. Fix the failing checks
2. Commit using conventional commits: fix(ci): fix linting errors
3. Push to your branch: git push
4. Verify CI passes
5. Call /review when green
PR lifecycle monitoring
When a worker has an open PR, the daemon runs four checks every tick:
CI status
Queries GitHub for check run results. If any required check is failing, fires a ci nudge with the failure details.
Merge conflicts
Checks the PR’s mergeable state. If the PR has conflicts with the base branch, fires a conflict nudge.
Review comments
Checks for unresolved review comments or changes-requested reviews. If found, fires a review nudge.
Commit format
Validates that all commits follow conventional commit format (type(scope): description). Non-conforming commits trigger a bad-commits nudge.
PR state transitions
The daemon also handles terminal PR states:
- Merged — If
github.auto_cleanup_mergedis true (default), kills the tmux window and emits aTerminalevent. Sends a notification. - Closed without merge — Sends a notification. If
github.auto_cleanup_closedis true, also cleans up.
Auto-pruning
When a PR is merged or closed, the daemon automatically prunes the associated worker:
- Triggers — PR merged or PR closed (depending on config)
- What gets cleaned up — The git worktree, event logs, and worker state are all removed
- Safe on uncommitted changes — Pruning will not remove a worktree with uncommitted changes; it skips the worker and logs a warning
- Recovery scan — On startup, the daemon scans for PRs that were merged or closed while the daemon was offline and prunes their stale workers
PR discovery
Workers don’t need to tell jig about their PR. The daemon proactively checks GitHub for PRs matching the worker’s branch name. When found, it emits a PrOpened event and the worker transitions to review status.
The watch display
jig ps --watch shows a live table updated every tick:
jig ps --watch — 3 workers (every 2s)
WORKER STATE COMMITS PR HEALTH ISSUE
● feature-auth running 3 #42 ok jwt-auth
● fix-pagination stalled 1* #43 ci fix-page
○ add-tests idle - - - add-tests
[l]ogs [q]uit
Column meanings:
| Column | Description |
|---|---|
| WORKER | Worker name with tmux indicator: ● running, ○ exited |
| STATE | Derived worker status from the event stream |
| COMMITS | Commits ahead of base branch (* = uncommitted changes) |
| PR | PR number if one exists |
| HEALTH | PR check results: ok (all green), problem names in red, - if no PR |
| ISSUE | Linked issue reference |
The HEALTH column gives you at-a-glance visibility into what’s wrong with each worker’s PR, independent of whether nudges have fired.
Log view
Press l in watch mode to switch to the log view. This shows timestamped daemon activity — which nudges fired, PR check results, errors — so you can see what the daemon is actually doing:
jig ps --watch — logs (every 2s)
[14:32:05] tick: 3 workers, 1 action, 1 nudge, 0 errors
[14:32:05] myrepo/feature-auth PR: ok
[14:32:05] myrepo/fix-pagination PR: ci, conflicts
[14:32:35] tick: 3 workers, 0 actions, 0 nudges, 0 errors
[14:32:35] myrepo/feature-auth PR: ok
[14:32:35] myrepo/fix-pagination PR: ok
[t]able [q]uit
Press t or l again to switch back to the table. Press q to quit cleanly.
Configuration
Global config (~/.config/jig/config.toml)
[health]
silence_threshold_seconds = 300 # 5 minutes before "stalled"
max_nudges = 3 # per nudge type before escalation
[github]
auto_cleanup_merged = true # kill workers when PR merges
auto_cleanup_closed = false # kill workers when PR closed without merge
[notify]
exec = "notify-send 'jig' '$MESSAGE'" # shell command for notifications
# webhook = "https://hooks.slack.com/..." # or a webhook URL
# events = ["worker.done"] # filter which events trigger
Per-repo config (jig.toml)
Repos can override health and nudge settings, and configure auto-spawn:
[health]
silence_threshold_seconds = 600 # slower repos get more patience
[health.nudge.idle]
max = 5
cooldown_seconds = 600
[health.nudge.ci]
max = 2
cooldown_seconds = 180
[spawn]
max_concurrent_workers = 3
auto_spawn_interval = 120
[issues]
provider = "file"
auto_spawn_labels = ["auto"]
Per-type nudge config (idle, stuck, ci, conflict, review, bad_commits) can set max and cooldown_seconds independently. Resolution: per-type repo → repo defaults → global → hardcoded defaults.
State files
| Path | Purpose |
|---|---|
~/.config/jig/config.toml |
Global daemon configuration |
~/.config/jig/workers.json |
Last-known state of all workers (for diff-based dispatch) |
~/.config/jig/state/events/<repo>-<worker>/events.jsonl |
Per-worker event stream |
~/.config/jig/notifications.jsonl |
Notification log |
Customization
Custom nudge templates
Override any built-in template by placing a file in your repo’s .jig/templates/ directory:
.jig/templates/
├── nudge-idle.hbs
├── nudge-ci.hbs
└── spawn-preamble.hbs
Available template variables vary by nudge type but always include nudge_count, max_nudges, and is_final_nudge.
Notification hooks
The [notify] config supports shell commands and webhooks. The command receives a JSON payload on stdin with event details:
[notify]
exec = "jq -r '.reason' | terminal-notifier -title 'jig'"
Tuning thresholds
- Lower
silence_threshold_secondsif your agents are fast and you want quicker intervention - Raise
max_nudgesif agents often recover on their own after a few tries - Set
auto_cleanup_closed = trueif you want aggressive cleanup of abandoned work