* 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.
146 lines
9.6 KiB
Markdown
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.
|