Some checks failed
planctl / build-test (1.22, macos-latest) (push) Has been cancelled
planctl / build-test (1.22, ubuntu-latest) (push) Has been cancelled
planctl / build-test (1.22, windows-latest) (push) Has been cancelled
planctl / build-test (stable, macos-latest) (push) Has been cancelled
planctl / build-test (stable, ubuntu-latest) (push) Has been cancelled
planctl / build-test (stable, windows-latest) (push) Has been cancelled
planctl / build-test (1.22, macos-latest) (pull_request) Has been cancelled
planctl / build-test (1.22, ubuntu-latest) (pull_request) Has been cancelled
planctl / build-test (1.22, windows-latest) (pull_request) Has been cancelled
planctl / build-test (stable, macos-latest) (pull_request) Has been cancelled
planctl / build-test (stable, ubuntu-latest) (pull_request) Has been cancelled
planctl / build-test (stable, windows-latest) (pull_request) Has been cancelled
planctl / bench (push) Has been cancelled
planctl / bench (pull_request) Has been cancelled
- Add tasks.go: TaskRecord, buildTaskRecords, findTaskByID, atomicRewriteTaskLine, evalPlanStatus - Add runNext, runList, runComplete, runStatus subcommands in main.go - Add emitNext, emitList, emitComplete, emitStatus formatters in emit.go - Rewrite CLI dispatch; update printUsage to list all five subcommands - Add 28 golden-file fixture dirs under testdata/v2/ - Add mutation tests for complete (success, already-done, triggers-lint, dry-run) - Add per-subcommand --help tests and unit tests for all emit functions - Fix: warnings-only status shows PASS not FAIL per R4.2 - Fix: case-insensitive T prefix for complete <task-ref> per R3.1 - Fix: dry-run JSON computes hypothetical lint diagnostics per R3.8 - Fix: dry-run JSON always exits 0 per R3.8 - Add multi-plan status aggregate: Summary line (text) + summary object (JSON) per R4.6 Tasks 2.0-6.0 from dev/plans/26174-planctl-task-cmds/prd.md
169 lines
11 KiB
Markdown
169 lines
11 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). |
|
|
| `JJ_AGENT_FEATURE` | — | — | **(Planned, not yet implemented.)** Human-readable feature name for commits and bookmarks. When set, commits become `wip(<feature>:<session_id>)` and the session-end bookmark becomes `feat/<feature>` instead of `wip/claude-<session_id>`. |
|
|
|
|
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.
|
|
|
|
## Session inventory consumption
|
|
|
|
On `session-start`, the daemon responds with a JSON `sessions` array listing every *other* currently-active session and its tracked files:
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"sessions": [
|
|
{"id": "abc12345", "pid": 1234, "files": ["src/auth.go", "cmd/main.go"], "age": "2m34s"}
|
|
]
|
|
}
|
|
```
|
|
|
|
**What agents should do with this:**
|
|
|
|
1. **Log it at startup.** Even if no overlap exists, the inventory tells you who else is active. This is useful context for the user.
|
|
2. **Check for file overlap.** Before editing a file, verify it doesn't appear in another session's `files` list. If it does, flag it to the user: *"Session abc12345 (pid 1234, active 2m34s) is also tracking `src/auth.go` — proceed anyway?"*
|
|
3. **Empty inventory is the common case.** Solo sessions (the most common scenario) return an empty array. Log nothing and proceed.
|
|
4. **Stale entries self-clean.** The daemon reaps sessions with dead PIDs and sessions older than `JJ_HOOK_STALE_MIN` (default 30 min). Don't rely on the inventory being perfectly current across a long session — re-read by sending `session-start` again if you need a fresh snapshot. A fresh session restart is always cleaner.
|
|
|
|
The hook script (`scripts/jj-hook.sh`) logs the inventory to the daemon log on start but does not yet surface it to the agent automatically. In a future version this will be piped into the agent's startup context.
|
|
|
|
## 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.
|