template-jj/docs/jj-commitd.md
Sid 7fa52772b3 docs: add reference docs for both binaries + consolidated build guide
* docs/building.md — how to build jj-commitd and planctl from source,
  requirements, `go install` recipes, reproducible-build flags
  (`-trimpath -ldflags="-buildid="` per planctl PRD R7.3), test +
  benchmark commands, cross-compile, CI matrix link.
* docs/planctl.md — full command reference beyond --help: plan-dir
  discovery cases A-D, all 9 classification codes with severity and
  meaning, tag-grammar spec (including paren-aware annotations + the
  tag-unclosed recovery semantics), the five EARS regexes and the
  bold-prefix exemption, JSON schema, worked examples for pre-commit
  / CI / multi-plan / JSON consumers.
* docs/jj-commitd.md — architecture diagram, socket event protocol
  (session-start, post-edit, session-end, shutdown, status) with
  field tables, all env vars (REPO_ROOT, JJ_HOOK_DEBOUNCE_SEC,
  JJ_HOOK_STALE_MIN, JJ_HOOK_AUTO_SQUASH), baked-in timing constants,
  socket / log paths, fallback mode, troubleshooting.
* README.md — expanded the jj-commitd bullets and linked to each
  reference from a new "Documentation" section; kept top-level
  summaries terse.
2026-04-21 21:38:01 -06:00

9.6 KiB

jj-commitd — command reference

Debounced commit daemon for Claude Code sessions. Per-session, file-scoped commits with ~3 s debouncing so rapid successive edits batch into one jj commit instead of racing.

Optional. If the binary isn't on PATH, the hook script falls back to file-tracking-only mode that commits at session end.

Install

go install forgejo.zerova.net/sid/template-jj/cmd/jj-commitd@latest

See docs/building.md for building from source.

Architecture

Claude Code hooks (bash)    Unix socket           Go daemon            jj CLI
┌─────────────────────┐  ┌────────────────────┐  ┌──────────────┐  ┌────────┐
│ session-start       │→ │                    │→ │              │→ │ jj     │
│ post-edit (edit N)  │→ │ /tmp/jj-commitd-   │  │ jj-commitd   │→ │ commit │
│ post-edit (edit N+1)│→ │   <repo_id>.sock   │  │ (this binary)│  │        │
│ session-end         │→ │                    │→ │              │→ │ jj ... │
└─────────────────────┘  └────────────────────┘  └──────────────┘  └────────┘
  • The bash hook client (scripts/jj-hook.sh) is installed into Claude Code's .claude/settings.json as an event hook.
  • On the first session-start the hook auto-spawns the daemon in the background.
  • Events travel over the Unix socket as one JSON object per line.
  • The daemon serialises all jj invocations through a single mutex so concurrent sessions never race.
  • When the last session ends and an idle timeout passes (30 min), the daemon exits and cleans up its socket.

Usage

The binary is normally not invoked directly — scripts/jj-hook.sh manages its lifecycle. For debugging / manual testing:

# Start manually from a repo root (daemon blocks in foreground):
REPO_ROOT=$(pwd) jj-commitd

# Ask an existing daemon to log its status (writes to the daemon's log file):
scripts/jj-hook.sh status

# Gracefully shut down a running daemon:
scripts/jj-hook.sh session-end   # (or send a shutdown event directly — see below)

There is no --help, no --version, and no subcommand dispatch. The daemon reads all configuration from environment variables.

Events

The daemon consumes one JSON object per line over its Unix socket. All events are one-shot request/response; the daemon closes the connection after replying.

Event Fields Semantics
session-start session_id, repo_root, pid Register a session. Response includes the inventory of other active sessions + their tracked files.
post-edit session_id, file (absolute path), repo_root Track a file edit. Resets the session's debounce timer; the next idle window triggers jj commit.
session-end session_id Flush any pending commit, record the session as ended. Daemon exits once all sessions end + idle out.
shutdown Immediate graceful shutdown. Used by cleanup scripts.
status Snapshot current session state to the log file. Returns {"ok": true}.

Unknown events are rejected with {"ok": false, "error": "unknown event"}.

Example — manual protocol poke

# Send a status request to an existing daemon:
echo '{"event":"status"}' \
  | nc -U /tmp/jj-commitd-$(echo -n $(pwd) | shasum -a 256 | head -c 16).sock

(The repo_id is a SHA-256 prefix of the absolute repo root — see repoID() in cmd/jj-commitd/main.go.)

Configuration (environment variables)

Variable Default Max Effect
REPO_ROOT $(pwd) Repo root the daemon manages. Auto-detected from cwd if unset.
JJ_HOOK_DEBOUNCE_SEC 3 30 Debounce window in seconds. Edits within this window batch into one commit.
JJ_HOOK_STALE_MIN 30 Minutes before a session with a dead PID is reaped.
JJ_HOOK_AUTO_SQUASH 0 When 1, session-end triggers the squash-wip flow (squashes the session's WIP commits into one).

Invalid or zero values for the numeric vars fall back to defaults.

Paths

All daemon state is per-repo, keyed by a SHA-256 prefix of $REPO_ROOT:

  • Socket: /tmp/jj-commitd-<repo_id>.sock
  • Log: /tmp/jj-commitd-<repo_id>.log (rotated when it exceeds 512 KB)

Both clean up automatically when the daemon exits. Stale sockets from a crashed daemon are detected via a DialTimeout probe on startup and unlinked.

Timing constants

All baked in at cmd/jj-commitd/main.go:46-53; not user-configurable beyond the env vars above.

Constant Value Purpose
defaultDebounce 3 s Default batching window for post-edit events.
quiescenceWindow 500 ms Must see no new edits for this long before a commit fires.
maxDebounce 30 s Cap on JJ_HOOK_DEBOUNCE_SEC.
idleShutdown 30 m Time after the last session ends before the daemon self-exits.
defaultStaleSessionAge 30 m Default stale-session reap age (overridable via JJ_HOOK_STALE_MIN).
maxLogSize 512 KB Log rotation threshold.

Session lifecycle

  1. session-start registers a new session with an empty Files map and StartedAt = now. The response carries an inventory of other active sessions — a Claude Code agent can inspect this to avoid stepping on another session's tracked files.
  2. post-edit appends the edited file to the session's dedup set and resets the debounce timer. When the timer fires and the session has been idle for quiescenceWindow, the daemon:
    • Runs jj diff --summary -- <tracked files> to confirm there are real changes (avoids empty commits from Claude re-reading an unchanged file).
    • Runs jj commit -m "wip(claude:<session>): <file-list>" -- <tracked files> under a global jjLock so concurrent sessions can't race.
  3. session-end flushes any pending commit and marks the session ended. If JJ_HOOK_AUTO_SQUASH=1, the hook then invokes the squash-wip flow.
  4. When all sessions have ended AND no new traffic arrives for idleShutdown (30 minutes), the daemon exits cleanly.

Conflict detection

When two sessions touch the same file, the daemon logs a warning (grep /tmp/jj-commitd-<repo_id>.log for conflict) and attributes the edit to whichever session's timer fires first. Per-session commit messages keep the history readable even when multiple agents interleave.

Legacy cleanup

On startup the daemon sweeps and removes any /tmp/jj-claude-*-files tracking files left over from pre-daemon hook versions. Safe to run alongside old installations — it won't disrupt an in-flight older hook since the files it cleans are ephemeral per-session markers.

Fallback mode

If jj-commitd is not on PATH when scripts/jj-hook.sh session-start runs, the hook falls back to file-tracking-only mode:

  • Every post-edit appends to /tmp/jj-claude-<session>-files.
  • On session-end, one big jj commit flushes all tracked files at once.
  • No debouncing, no per-session isolation beyond the file list itself.

Functional, but loses the debounce and live-status benefits of the daemon.

Troubleshooting

  • "connection refused" from the hook — stale socket. The daemon detects these on startup, but if you see it mid-session, kill any leftover jj-commitd processes (pkill jj-commitd) and retry; the next session-start will relaunch.
  • Edits not committing — check /tmp/jj-commitd-<repo_id>.log. Look for commit error or jj exec lines; the jj CLI's stderr is captured there.
  • Disappearing edits across sessions — grep the log for the file path; you're likely hitting a cross-session conflict.
  • Daemon won't shut downscripts/jj-hook.sh status to inspect active sessions; send the shutdown event directly (see "Manual protocol poke" above) to force exit.

See also