Autonomous Orchestration

v0.5

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:

  1. Creates the worktree and branch
  2. Records the worker in .jig/state.json (local orchestrator state)
  3. Emits a Spawn event to the worker’s event log
  4. Opens a tmux window in a jig-<repo> session
  5. 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:

  1. Sync reposgit fetch the base branch for each registered repo
  2. Discover workers — Scan ~/.config/jig/state/events/ for active event logs
  3. 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)
  4. 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_merged is true (default), kills the tmux window and emits a Terminal event. Sends a notification.
  • Closed without merge — Sends a notification. If github.auto_cleanup_closed is 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_seconds if your agents are fast and you want quicker intervention
  • Raise max_nudges if agents often recover on their own after a few tries
  • Set auto_cleanup_closed = true if you want aggressive cleanup of abandoned work