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

146 lines
9.6 KiB
Markdown

# 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
```bash
go install forgejo.zerova.net/sid/template-jj/cmd/jj-commitd@latest
```
See [`docs/building.md`](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`](../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:
```bash
# 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
```bash
# 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 down** — `scripts/jj-hook.sh status` to inspect active sessions; send the `shutdown` event directly (see "Manual protocol poke" above) to force exit.
## See also
- [`INTEGRATION.md`](../INTEGRATION.md) — installing hooks in a new or existing project.
- [`scripts/jj-hook.sh`](../scripts/jj-hook.sh) — the bash client that talks to the daemon.
- [`docs/planctl.md`](planctl.md) — the other binary in this repo.
- [`docs/building.md`](building.md) — build from source.