# jj-template Reusable [jj](https://martinvonz.github.io/jj/) + [Claude Code](https://docs.anthropic.com/en/docs/claude-code) workflow template. Provides automatic per-session snapshot management so every Claude Code session's work is committed and isolated — even with concurrent agents. ## What It Does - **Session start**: snapshots any pre-existing uncommitted work before the agent starts editing - **Post-edit**: tracks every file Claude edits (via Edit/Write/NotebookEdit tools) per-session - **Session end**: commits tracked files with a descriptive `wip(claude:):` message, then commits any untracked leftovers - **Concurrent safety**: multiple agent sessions on the same repo get isolated, file-scoped commits - **Bookmarks**: each session's commit gets a `wip/claude-{session}` bookmark for easy reference - **Base tracking**: records the starting revision per session (visible in `status`) - **Orphan reaping**: dead sessions (crashed CLI) are cleaned up automatically via PID liveness checks and 30m timeout (configurable via `JJ_HOOK_STALE_MIN`) - **Legacy cleanup**: daemon sweeps orphaned `/tmp/jj-claude-*-files` from pre-daemon hook versions on startup - **Conflict detection**: warns when multiple sessions edit the same file - **Squash**: `squash-wip` consolidates accumulated wip commits into one for cleaner history - **Auto-squash**: set `JJ_HOOK_AUTO_SQUASH=1` to squash automatically at session end - **Repo cleanup**: `/repo-cleanup` Claude command organizes wip commits into themed conventional commits ## Commit Daemon (`jj-commitd`) A Go daemon that batches file edits into debounced commits via a Unix socket. - **How it works**: `session-start` launches the daemon; `post-edit` sends file paths over a socket; a debounce timer (default 3s) batches edits into a single `jj commit`; `session-end` flushes pending commits and shuts down. - **Install**: `go install forgejo.zerova.net/sid/template-jj/cmd/jj-commitd@latest`. - **Config**: `JJ_HOOK_DEBOUNCE_SEC` (default `3`, max `30`), `JJ_HOOK_STALE_MIN` (default `30`), `JJ_HOOK_AUTO_SQUASH=1` to squash at session end. - **Session inventory**: on `session-start`, returns other active sessions and their tracked files. - **Logging**: daemon log at `/tmp/jj-commitd-.log`. - **Fallback**: without the binary on `PATH`, the hook falls back to file-tracking-only mode that commits at session end. - **Full reference**: [`docs/jj-commitd.md`](docs/jj-commitd.md). ## planctl Lint tool for spec-driven plan directories (`dev/plans/-/`). Validates traceability tags, cross-references between `prd.md` / `design.md` / `tasks.md`, file-presence at close-out, and EARS-keyword conformance on PRD acceptance criteria. Read-only by design — v1 ships `planctl lint` and nothing else. - **Install**: `go install forgejo.zerova.net/sid/template-jj/cmd/planctl@latest`. - **Usage**: `planctl lint [plan-dir]` — pass a path, or invoke from anywhere inside a plan dir or a repo containing `dev/plans/`. - **Output**: one line per diagnostic in the form `:: [] : `; a clean plan prints one summary line. `--format=json` emits JSONL for tooling. - **Flags**: `--strict` (warnings → exit 1), `--no-ears` (skip EARS check), `--format={text,json}`, `--color={auto,always,never}` (no-op in v1). - **Classification codes**: `tag-syntax`, `tag-unclosed`, `orphan-requirement`, `orphan-design`, `uncovered-requirement`, `uncovered-design`, `missing-prd`, `missing-closeout-file`, `ears-violation`. These are grep-stable public API. - **Full reference**: [`docs/planctl.md`](docs/planctl.md). - **Spec**: [`dev/plans/26172-planctl/`](dev/plans/26172-planctl/) — PRD, design, task list, and codex-review log. ## Documentation - [`docs/building.md`](docs/building.md) — build both binaries from source, reproducible builds, cross-compile, CI matrix. - [`docs/planctl.md`](docs/planctl.md) — full `planctl` command reference. - [`docs/jj-commitd.md`](docs/jj-commitd.md) — full `jj-commitd` daemon reference. - [`INTEGRATION.md`](INTEGRATION.md) — integrate this template into an existing or new project. - [`AGENTS.md`](AGENTS.md) — cross-agent working conventions. ## Prerequisites - [jj](https://martinvonz.github.io/jj/latest/install/) (Jujutsu VCS) - [jq](https://jqlang.github.io/jq/) (JSON processor) - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI - [Go](https://go.dev/dl/) 1.22+ (optional, for building the commit daemon) ## Quick Start See [INTEGRATION.md](INTEGRATION.md) for step-by-step instructions to add this to an existing or new project. ## Files ``` cmd/jj-commitd/ main.go # Commit daemon (Go) scripts/ _lib.sh # Shared utilities (REPO_ROOT resolution) jj-hook.sh # Main hook script (all 6 actions) test-jj-hooks.sh # 26-test integration suite .claude/ settings.json # Hook configuration for Claude Code (team-shared) commands/ repo-cleanup.md # /repo-cleanup slash command ``` ## Manual Commands ```bash # Check active sessions for this repo ./scripts/jj-hook.sh status # Squash all wip commits between main and @ into one ./scripts/jj-hook.sh squash-wip # Force-reap all non-alive sessions ./scripts/jj-hook.sh reap ``` ## How It Works ### Tracking Files Each active session gets a tracking file at `/tmp/jj-claude-{REPO_ID}-{SESSION_ID}-files`. The `REPO_ID` is a `cksum` of the repo root path, preventing cross-repo contamination. Companion files record the parent process (`-pid`) and starting revision (`-base`) for liveness checking and cleanup context. ### Commit Flow With daemon (default when `jj-commitd` binary is available): ``` session-start → snapshot pre-existing work → launch jj-commitd daemon (or connect to existing) → record base revision, reap stale sessions post-edit (per file) → send file path to daemon via Unix socket → daemon resets debounce timer (3s default) → on timer expiry: commit all batched files session-end → flush pending commits (daemon commits immediately) → set wip/claude-{session} bookmark → commit untracked leftovers (if sole session) → auto-squash (if JJ_HOOK_AUTO_SQUASH=1) → clean up tracking + PID + base files ``` Without daemon (fallback): ``` session-start → snapshot pre-existing work → record base revision, initialize tracking file → reap stale/orphaned sessions post-edit (per file) → append relative path to tracking file (dedup) session-end → commit tracked files (file-scoped) → set wip/claude-{session} bookmark → commit untracked leftovers (if sole session) → auto-squash (if JJ_HOOK_AUTO_SQUASH=1) → clean up tracking + PID + base files ``` ### Concurrency When multiple Claude sessions edit the same repo simultaneously: - Each session tracks its own files independently - Session A ending only commits files A touched - Untracked changes (Bash-created files) are only committed when the last session ends - If both sessions touch the same file, the first to end commits it; the second skips it (no diff remaining) ## Tests ```bash scripts/test-jj-hooks.sh ``` Runs 34 integration tests in temporary jj repos. Requires `jj` and `jq`. Portable across macOS and Linux.