Monitoring
Monitor all your workers from the terminal with jig ps. A single background daemon (jig daemon start) does the supervising — monitoring workers, nudging stuck agents, tracking PR health — and jig ps -gw is the live dashboard onto it.
With no daemon running, jig ps still works: it drives one in-process, exactly as it always did.
The dashboard
jig ps -gw
This starts the global live watch display — all workers across all repos, updating in real time:
jig ps --watch — 4 workers (every 2s · daemon pid 72706)
WORKER STATE COMMITS PR HEALTH ISSUE
● jwt-auth running 2 - - ENG-123
● pagination running 0 - - ENG-124
● test-coverage draft 3 #42 ci ENG-125
● error-pages review 5 #43 ok ENG-126
[l]ogs [q]uit
The header says where the frames come from: daemon pid N when a daemon is
answering, hosting the daemon when this view started one because none was
running.
Columns
| Column | Description |
|---|---|
| WORKER | Name with a mux status dot: ● running, ○ exited, ✗ not found — colored red/green/yellow on herdr when it also reports the agent as blocked/working/idle |
| 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, problem names in red, - if no PR, or ? gh when the check itself failed — see jig daemon logs for why |
| ISSUE | Linked issue reference |
At a glance
- Which agents are active —
runningmeans tool use is flowing - Who’s stuck —
stalledmeans silence for 5+ minutes, the daemon will nudge - Draft vs review —
draftmeans agent is still working;reviewmeans ready for human review - PR health —
cimeans checks failing,conflictsmeans merge conflicts - Progress — Commit count shows how far along each worker is
Log view
Press l in watch mode to see daemon activity:
[14:32:05] tick: 3 workers, 1 action, 1 nudge, 0 errors
[14:32:05] myrepo/jwt-auth PR: ok
[14:32:05] myrepo/test-coverage PR: ci, conflicts
[14:32:35] tick: 3 workers, 0 actions, 0 nudges, 0 errors
Press t or l again to switch back. Press q to quit.
The daemon
Every 30 seconds, the daemon fetches repos, scans event logs to derive worker state, discovers PRs via GitHub, and dispatches actions (nudges, notifications, cleanup).
The daemon uses background actor threads for blocking I/O: syncing repos, querying GitHub, polling for spawnable issues, creating worktrees, pruning merged workers, and delivering nudges through the configured mux backend.
Running it
jig daemon start # run it in the foreground (ctrl-c to stop)
jig daemon stop # ask the running one to shut down
jig daemon status # is it alive, ticking, and unstuck?
jig daemon start --once runs a single tick and exits — useful in a cron job
or a smoke test. It waits up to 30s for the monitor pass to finish so the tick
is complete; --timeout <SECONDS> raises that when a pass has many repos to
poll over a slow network.
There is one daemon per user, always global — it watches every tracked
repo. A second jig daemon start fails with daemon already running (pid N)
rather than starting a rival that would fight over the same worktrees.
It binds a unix socket at $XDG_RUNTIME_DIR/jig/daemon.sock (falling back to
~/.config/jig/state/daemon.sock) and claims daemon.pid beside it. Both are
removed on a clean exit; after a crash the next start finds them stale and
takes them over, so there is nothing to clean up by hand.
jig ps and jig ps -gw are clients of that socket. They render what the
daemon reports instead of running a tick loop of their own, so you can have as
many dashboards open as you like. When no daemon is listening, jig ps -gw
starts one inline for the life of the view (and, in global mode, takes the
socket so jig daemon status can see it) — which is how jig worked before the
daemon had a socket.
Checking on the daemon
jig daemon status asks the daemon over the socket — an answer is the proof
of life. Useful when workers seem to have stopped being nudged or spawned:
✓ daemon running pid 72706 · up 3h12m · v0.5.2
→ last tick 1s ago (every 2s)
→ log ~/.config/jig/state/logs/20260910T195104Z.log
→ jig-monitor last finished 1s ago
→ jig-sync last finished 1m40s ago
→ jig-spawn busy 45s (last finished 2m ago)
It reports one of:
- running — answering and ticking on schedule
- stalled — answering, but the tick loop has stopped (the listener runs on its own thread, so a wedged tick still gets a reply)
- not running — nothing is listening on the socket
It also flags any actor that has been busy for over 10 minutes (e.g. a git fetch hung on auth), since the tick loop keeps running while a wedged actor silently skips its work. The command exits non-zero unless the daemon is healthy.
jig daemon logs prints the daemon’s own log; -f follows it (and moves to the new log when the daemon restarts), -n sets how many lines, --path prints the file path.
Running it in the background
jig daemon start runs in the foreground. To have the OS keep it alive:
sudo jig daemon install # Linux: a system service that runs as you
jig daemon install # macOS: a launchd agent
jig daemon status
jig daemon uninstall # when you want it gone
It runs as you, starts automatically, and is restarted if it dies.
Why Linux needs sudo
The daemon runs your hooks, so it needs your permissions — your docker group, your SSH agent, your PATH. On Linux that means a system unit with User=<you>: systemd looks your groups up when it starts the service, so the daemon has all of them.
A systemd --user service cannot do that. The user manager is started by init with your user and primary group only, and it has no privilege to add the rest — so a user service never sees docker, no matter how long ago you joined the group. That shows up as a hook failing with:
auto-spawn failed: hook failed: permission denied while trying to connect to
the docker API at unix:///var/run/docker.sock
If sudo isn’t available, jig daemon install --user installs a user service anyway and warns about exactly this. It’s fine when your hooks only need your own files.
What the service inherits
Installing it for yourself, the service keeps your PATH, and your XDG_CONFIG_HOME and SSH_AUTH_SOCK when set — so the daemon finds the same tools, reads the same config, and can use the same SSH agent for git fetch as the shell you installed from.
Installing it for someone else — --as, below — none of that applies, because your environment is root’s, not theirs. The service gets a PATH rooted at their home, and anything further you say with --env:
sudo jig daemon install --as bot \
--env SSH_AUTH_SOCK=/run/user/1001/gnupg/S.gpg-agent.ssh
Running it for a service account
On a server the person with sudo usually isn’t the person the daemon runs as. Name them:
sudo jig daemon install --as bot
Everything then comes from bot: their groups, their home, their runtime directory for the socket. Without --as the daemon runs as whoever invoked sudo, which is right on your own machine and wrong on a server.
With Ansible that’s the whole setup:
- name: Install the jig daemon
ansible.builtin.command: /usr/local/bin/jig daemon install --as bot
args:
creates: /etc/systemd/system/jig-daemon.service
become: true
Only the daemon and jig ps --watch write log files (~/.config/jig/state/logs/). Every other command prints warnings straight to stderr; set RUST_LOG=info (or debug) to see more.
Nudges
When agents get stuck, the daemon intervenes by sending keystrokes through the mux backend (tmux or herdr). Each nudge type has an independent counter and escalates after max_nudges (default 3) to a notification instead.
Nudge types
| Type | Trigger | Action |
|---|---|---|
| idle | Worker stalled or idle, no PR | Asks for status update, pushes toward committing |
| stuck | Worker waiting (interactive prompt) | Sends auto-approve keystroke, then message |
| ci | CI failing on open PR | Lists failing checks, tells agent to fix |
| conflict | Merge conflicts on PR | Tells agent to rebase and resolve |
| review | Unresolved review comments | Tells agent to address feedback |
| bad-commits | Non-conventional commits | Lists bad commits, tells agent to reword |
Escalation
After max_nudges of the same type, the daemon stops nudging and fires a notification — alerting you that the worker needs human attention. This prevents infinite loops where an agent keeps failing at the same thing.
Draft-only nudging
Nudges only fire for draft PRs. Once a PR is marked ready for review, the daemon backs off — the human is in control. Health problems still appear in the HEALTH column for visibility.
Auto-cleanup
When a PR is merged or closed, the daemon automatically:
- Kills the worker’s window (tmux window or herdr tab)
- Removes the worktree and event logs
- Emits a
Terminalevent
Pruning skips worktrees with uncommitted changes (logs a warning). On startup, the daemon scans for PRs merged/closed while it was offline and prunes stale workers.
Configure cleanup behavior:
[github]
auto_cleanup_merged = true # default: kill workers when PR merges
auto_cleanup_closed = false # kill workers when PR closed without merge
Configuration
Health thresholds
# ~/.config/jig/config.toml (global) or jig.toml (per-repo)
[health]
silence_threshold_seconds = 300 # 5 minutes before "stalled"
max_nudges = 3 # per nudge type before escalation
Per-type nudge config
# jig.toml
[health.nudge.idle]
max = 5
cooldown_seconds = 600
[health.nudge.ci]
max = 2
cooldown_seconds = 180
Available types: idle, stuck, ci, conflict, review, bad_commits. Resolution: per-type repo → repo defaults → global → hardcoded defaults.
Custom nudge templates
Override any built-in template by placing files in .jig/templates/:
.jig/templates/
├── nudge-idle.hbs
├── nudge-ci.hbs
└── spawn-preamble.hbs
Templates use Handlebars and always receive nudge_count, max_nudges, and is_final_nudge.
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 and the daemon.
| Event | Source | Meaning |
|---|---|---|
Spawn |
jig spawn |
Worker created |
ToolUseStart / ToolUseEnd |
Agent hooks | Tool use activity |
Commit |
post-commit hook | Code committed |
Push |
post-commit hook | Code pushed |
PrOpened |
Daemon | PR discovered for branch |
Notification |
Agent | Hit an interactive prompt |
Stop |
Agent exit | Session ended |
Nudge |
Daemon | Nudge delivered |
Terminal |
Daemon | Worker cleaned up |
Worker state is derived by replaying the event stream — there’s no mutable state database.
Quick reference
jig ps # Status snapshot
jig ps -w # Watch mode (current repo)
jig ps -gw # Global watch — all repos, live dashboard
jig daemon start # Run the daemon (one per user, watches every repo)
jig daemon stop # Stop the running daemon
jig daemon status # Is the daemon alive and ticking?
jig daemon logs -f # Follow the daemon's log