18 KiB
Integration Guide
Instructions for adding the jj + Claude Code workflow to an existing or new project. Written for agentic devs (Claude, Codex, etc.) to follow.
Workflow Layers
This template ships two independent layers; adopt them together (recommended) or separately.
- Session-commit tooling —
cmd/,scripts/,.claude/settings.json,.claude/commands/. Thejj-commitddaemon + hook scripts +/repo-cleanupslash command give every Claude Code session its own isolated commit stack via debounced per-session commits. Documented in the "Building the Daemon", "New Project", and "Existing Project" sections below. - Spec-driven workflow —
AGENTS.md,CLAUDE.md,dev/,.claude/skills/. Portable cross-agent conventions: YYWWD-prefixed plan directories, the/create-prd→/create-design→/generate-tasks→/process-task-listchain, peer-reviewer CLI skills (codex,copilot,gemini,kiro), multi-agent workspace guidance, optional upstream-leak pre-push guard. Documented in the "Spec-Driven Workflow" section at the bottom of this file.
The layers are orthogonal. You can adopt only the session-commit tooling (existing users pre-dating the workflow layer), only the spec-driven workflow (projects that manage commits manually), or both.
Prerequisites
- jj, jq, Claude Code
- Go 1.22+ (optional, for the commit daemon)
Building the Daemon (Optional)
The commit daemon provides debounced commits during editing. Without it, the hook falls back to committing at session end only.
cd /path/to/jj-template
go install ./cmd/jj-commitd/
# Binary installed to $GOPATH/bin/jj-commitd (ensure it's in PATH)
The hook script auto-starts the daemon on session-start if the binary is found.
New Project
# 1. Create your project and init jj
mkdir my-project && cd my-project
jj git init
# 2. Copy the template files
cp -r /path/to/jj-template/scripts ./scripts
cp -r /path/to/jj-template/.claude ./.claude
# Note: settings.json is team-shared (committed to repo)
chmod +x scripts/jj-hook.sh scripts/test-jj-hooks.sh
# 3. Commit the tooling
jj commit -m "chore: add jj + claude code workflow"
jj bookmark set main -r '@-'
# 4. Verify
scripts/test-jj-hooks.sh
Existing Project
Step 1: Add Scripts
Copy scripts/jj-hook.sh, scripts/_lib.sh, and scripts/test-jj-hooks.sh into your project's scripts/ directory (create it if needed).
mkdir -p scripts
cp /path/to/jj-template/scripts/_lib.sh scripts/
cp /path/to/jj-template/scripts/jj-hook.sh scripts/
cp /path/to/jj-template/scripts/test-jj-hooks.sh scripts/
chmod +x scripts/jj-hook.sh scripts/test-jj-hooks.sh
If your project already has a scripts/_lib.sh, merge in the REPO_ROOT line:
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
That's the only variable jj-hook.sh requires from _lib.sh.
Step 2: Add Claude Code Hooks
Copy .claude/settings.json into your project, or merge the hooks into your existing settings:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./scripts/jj-hook.sh session-start",
"timeout": 15
}
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./scripts/jj-hook.sh session-end",
"timeout": 15
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "./scripts/jj-hook.sh post-edit",
"timeout": 10
}
]
}
]
}
}
Note: settings.json is committed to the repo and shared across the team. Use .claude/settings.local.json for personal overrides (it's gitignored by default).
Step 3: Add Repo Cleanup Command (Optional)
Copy .claude/commands/repo-cleanup.md to get the /repo-cleanup slash command:
mkdir -p .claude/commands
cp /path/to/jj-template/.claude/commands/repo-cleanup.md .claude/commands/
Customize the theme categories in that file to match your project structure.
Step 4: Verify
# Run the test suite
scripts/test-jj-hooks.sh
# Start a Claude session and check status
./scripts/jj-hook.sh status
# View daemon logs (if daemon is running)
tail -f /tmp/jj-commitd-$(printf '%s' "$PWD" | shasum -a 256 | cut -c1-12).log
Converting a Git Repo to jj
If your project uses git and you want to adopt jj:
cd my-project
jj git init --colocate # colocate keeps .git alongside .jj
jj bookmark set main -r '@-'
Colocated mode lets you use both git and jj commands. The hook script works with both colocated and standalone jj repos.
Customization
Different Script Location
If your scripts live somewhere other than scripts/, update the paths in .claude/settings.json:
"command": "./tools/jj-hook.sh session-start"
And update the _lib.sh source line in jj-hook.sh:
source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
Existing _lib.sh
The hook only needs REPO_ROOT from _lib.sh. If your _lib.sh already sets REPO_ROOT, no changes needed — just ensure the double-source guard pattern:
[[ -n "${_LIB_LOADED:-}" ]] && return 0
_LIB_LOADED=1
Commit Message Format
The default format is wip(claude:<session_id>): <file summary>. To change it, edit the jj commit -m lines in jj-hook.sh (search for wip(claude:).
Main Branch Name
The squash-wip command assumes a main bookmark. If you use a different name (e.g., trunk, master), update the jj log -r 'main' references in jj-hook.sh.
Tuning
Environment variables (set in your shell profile or .claude/settings.local.json hook commands):
| Variable | Default | Description |
|---|---|---|
JJ_HOOK_DEBOUNCE_SEC |
3 |
Seconds to wait before batching edits into a commit (max 30) |
JJ_HOOK_STALE_MIN |
30 |
Minutes before an idle session is reaped by the daemon |
Example in hook command:
"command": "JJ_HOOK_DEBOUNCE_SEC=5 ./scripts/jj-hook.sh post-edit"
Spec-Driven Workflow
The second template layer: portable cross-agent conventions that sit on top of whatever commit infrastructure you use. Works with or without the jj-commitd daemon above; works with or without jj (though the skills document jj-first commands).
Step 1: Copy the portable files
From the template root into your project:
cp /path/to/template-jj/AGENTS.md .
cp /path/to/template-jj/CLAUDE.md .
cp -r /path/to/template-jj/dev .
mkdir -p .claude && cp -r /path/to/template-jj/.claude/skills .claude/
chmod +x dev/tools/install-hooks.sh dev/tools/pre-push-guard.sh
The template's CLAUDE.md contains a pointer to AGENTS.md, Claude-specific sub-agent guidance, and a "Template-maintenance" section with jj-commitd internals. The first two travel cleanly; drop the "Template-maintenance" section once copied — those notes apply to the template's own repo, not your project.
Step 2: Replace placeholder tokens
The portable files use placeholder tokens so you can slot your project's specifics in. Find them:
rg -n '<[A-Z_]+>' AGENTS.md CLAUDE.md dev/ .claude/skills/
Replace each across the files (find-and-replace, a scripted sed, or ad-hoc as team members encounter them):
| Token | Example values |
|---|---|
<PROJECT_NAME> |
my-app, acme-core |
<PRIMARY_LANGUAGE> |
Zig, Rust, Python, Go, TypeScript |
<TEST_COMMAND> |
pytest, npm test, go test ./..., cargo test |
<SOURCE_ROOT> |
src/, lib/, pkg/ |
<UPSTREAM_BLOCKED_PATTERN> |
upstream-org/repo (for forks), or leave unset for greenfield projects |
The <YYWWD> and <feature-slug> tokens inside plan-directory paths are runtime-generated by the skills — leave those as-is.
Step 3: Install the optional upstream-leak pre-push guard
If this repo is a fork and dev/ artifacts should never land on the upstream:
export UPSTREAM_BLOCKED_PATTERN='upstream-org/upstream-repo' # add to .envrc or shell profile
./dev/tools/install-hooks.sh
The guard at .git/hooks/pre-push (symlinked to dev/tools/pre-push-guard.sh) enumerates every path touched by commits in the push range and refuses pushes touching dev/ paths to any remote whose URL contains the pattern. Greenfield projects with no upstream-leak concern can skip this step — the guard is a no-op when UPSTREAM_BLOCKED_PATTERN is unset.
Step 4: Choose a parallel-agent convention
If multiple agents will run concurrently in this repo, pick the isolation level you need:
Option A — jj workspace add (preferred, true isolation)
Each agent gets its own working directory while sharing the same .jj/ object store. Branch switches in one workspace never flip another's tree.
# From the main clone:
jj workspace add --name planner ../<PROJECT_NAME>-planner
jj workspace add --name impl ../<PROJECT_NAME>-impl
# Agent sessions each start in their own directory:
# planner: cd ../<PROJECT_NAME>-planner
# impl: cd ../<PROJECT_NAME>-impl
# Inspect or remove workspaces:
jj workspace list
jj workspace forget planner
See dev/README.md → "Primary isolation: jj workspaces" for the full convention (plans commit vs. feature branch discipline, handoff squash, etc.).
Option B — env vars (same working directory)
When a single working directory is unavoidable, each agent exports its role and target bookmark. The spec-driven skills' §0 Pre-flight step checks for mismatches before any write:
export AGENT_ROLE=planner
export AGENT_BOOKMARK=feature/<YYWWD>-<feature-slug>
Coming soon — JJ_AGENT_FEATURE
A future JJ_AGENT_FEATURE env var will label commits and bookmarks with a human-readable feature name (e.g. wip(auth:abc12345), bookmark feat/auth) so multi-agent history is readable at a glance without decoding session UUIDs. Track docs/jj-commitd.md for availability.
Solo projects can skip this step — the convention matters only when two or more agents share a repo.
Step 5: Verify
# Confirm the eight skills are discoverable by Claude Code:
ls .claude/skills/
# Should list: codex copilot create-design create-prd gemini generate-tasks kiro process-task-list
# Confirm dev/ layout:
ls dev/
# Should list: QUICKSTART.md README.md notes plans research tools
# If you installed the upstream guard, verify the symlink:
ls -la .git/hooks/pre-push
# Should show a symlink to ../../dev/tools/pre-push-guard.sh
# Claude Code should now recognize /create-prd, /create-design, /generate-tasks, /process-task-list,
# and the per-tool skills (codex, copilot, gemini, kiro) as slash commands.
Step 6: Drive your first feature
From a Claude Code session, type /create-prd and describe the feature. The skill chain will walk you through PRD → (optional design) → tasks → implementation, with codex peer-reviews between stages. See dev/QUICKSTART.md for the short version.
Mixing with other agent tools
The .claude/skills/ pack is readable documentation even for tools that don't natively load skills:
- GitHub Copilot CLI reads
AGENTS.mdautomatically. The skills are referenced as docs it can fetch via@.claude/skills/codex/SKILL.mdetc. - Codex CLI, Gemini CLI, Kiro CLI don't auto-load skill packs but respect
@-file references in prompts. - Cursor / other IDEs typically read
AGENTS.mdor.cursorrules; point those atAGENTS.md.
If you use multiple agents, per-tool session logs (codex-sessions.md, copilot-sessions.md, gemini-sessions.md, kiro-sessions.md) keep the review lineage unambiguous for each feature.
Context-Window Token Awareness (planctl)
Claude Code agents using planctl can receive threshold-based recommendations about their own context-window fill, so they can commit and start a fresh session before running out of tokens mid-task. The signal surfaces in every planctl lint invocation when session data is available.
What the output looks like
When context fill reaches 70%, 85%, or 95% of the limit, planctl appends a context: line after the plan summary:
26174-planctl-context-tokens: clean (12 tasks, 6 requirements, 0 design sections)
context: 142 k / 200 k tokens (71%) — plan to wrap up this session soon.
Band behavior:
| Fill | Band | Severity | Suffix |
|---|---|---|---|
| < 70% | Normal | — | silent (no context: line) |
| 70–84% | Info | info |
plan to wrap up this session soon. |
| 85–94% | Warn | warn |
commit current work and start a new session after this task. |
| ≥ 95% | Error | error |
stop new work; commit and close out immediately. |
Token counts are expressed in thousands rounded to the nearest integer (143 k, 200 k); counts below 1000 display as < 1 k.
JSON output
With --format=json, the summary object gains a peer context_window key:
{
"summary": { "plans": 1, "errors": 0, "warnings": 0 },
"context_window": {
"tokens_used": 142000,
"tokens_limit": 200000,
"pct": 71,
"severity": "info",
"recommendation": "plan to wrap up this session soon."
}
}
In multi-plan output (e.g. a repo-root planctl lint that walks dev/plans/**), the context: line appears once after the aggregate N plans linted, … summary — context is session-level, not plan-level.
When context is omitted
- No session data: no env vars, no PID-keyed session file found → output is identical to the baseline (graceful degradation).
- Normal band (fill < 70%): no
context:line emitted, to keep healthy sessions quiet. - Unknown limit: when
CLAUDE_CODE_MAX_CONTEXT_TOKENSis unset,planctlemits a raw count (context: 143 k tokens (limit unknown)) and, in JSON, only thetokens_usedfield insidecontext_window.
context_window never shifts the summary's errors / warnings counts — it's advisory output, not a lint diagnostic.
Environment variables
| Variable | Source | Purpose |
|---|---|---|
CLAUDE_SESSION_ID |
Hook (see below) or Claude Code | Primary session UUID |
CLAUDE_CODE_MAX_CONTEXT_TOKENS |
Claude Code | Context-window limit (200000 for Opus 4.7) |
CLAUDE_PROJECT_DIR |
Claude Code | Project slug derivation for transcript lookup |
If CLAUDE_CODE_MAX_CONTEXT_TOKENS is not set, planctl emits the raw count without a percentage or recommendation. Hooks are optional: with no hook and no env var, planctl degrades silently — same output as any other CLI tool.
Hook integration (optional but recommended)
scripts/jj-hook.sh already ships the integration. If you copied the template, you have it. The hook adds two behaviors:
session-start case (template ships this — excerpt):
FULL_SID=$(printf '%s\n' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
if [[ "$FULL_SID" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
# Primary: propagate session ID to all subprocesses via env
[ -n "${CLAUDE_ENV_FILE:-}" ] && \
echo "export CLAUDE_SESSION_ID=${FULL_SID}" >> "$CLAUDE_ENV_FILE"
# Secondary: PID-keyed fallback for processes that lost env
START_TICKS=$(planctl_start_ticks "$PPID")
mkdir -p /tmp/planctl-sessions
TMPF=$(mktemp /tmp/planctl-sessions/.tmp.XXXXXX)
printf '%s\n%s\n' "$FULL_SID" "$START_TICKS" > "$TMPF"
mv -f "$TMPF" "/tmp/planctl-sessions/${PPID}"
fi
session-end case cleans up the PID-keyed file:
rm -f "/tmp/planctl-sessions/${PPID}"
planctl_start_ticks helper — platform-specific process-start-time readback used by the PID-reuse defense in cmd/planctl/context.go:
planctl_start_ticks() {
local pid="$1"
if [ -f "/proc/$pid/stat" ]; then
# Linux: field 22 of /proc/<pid>/stat is starttime in clock ticks since boot.
# `comm` can contain ')' and spaces — strip up to and including the LAST ')'.
local tail
tail=$(sed 's/.*)//' "/proc/$pid/stat" 2>/dev/null) || { echo 0; return; }
echo "$tail" | awk '{print $20}' | grep -E '^[0-9]+$' || echo 0
else
# macOS: ps lstart → epoch seconds.
ps -o lstart= -p "$pid" 2>/dev/null | xargs -I{} date -j -f '%a %b %d %T %Y' '{}' '+%s' 2>/dev/null || echo 0
fi
}
The Linux branch writes clock ticks since boot and the Go resolver (cmd/planctl/proc_linux.go) reads the same unit from /proc/<pid>/stat — units match within a platform. macOS uses epoch seconds on both sides via kinfo_proc.kp_proc.p_starttime. No cross-platform comparison is ever made.
Troubleshooting
planctloutput never shows acontext:line: confirm you're running inside a Claude Code session.echo $CLAUDE_SESSION_IDshould print a UUID. If not, check that theSessionStarthook fires and your shell sources$CLAUDE_ENV_FILE.- Wrong percentage:
CLAUDE_CODE_MAX_CONTEXT_TOKENSmay be mis-set. Without it, you'll see(limit unknown)instead of a percentage. - Multiple concurrent sessions: each agent's
planctlresolves its own session via ancestor-PID walk, backed by/tmp/planctl-sessions/<PPID>files. Liveness + start-stamp checks prevent cross-session contamination even across PID reuse. - No hook installed:
planctlworks without the hook — if neitherCLAUDE_SESSION_IDis set nor a PID-keyed file is found during the 8-hop ancestor walk, the context line is silently omitted.