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 / bench (push) Has been cancelled
442 lines
18 KiB
Markdown
442 lines
18 KiB
Markdown
# 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.
|
||
|
||
1. **Session-commit tooling** — `cmd/`, `scripts/`, `.claude/settings.json`, `.claude/commands/`. The `jj-commitd` daemon + hook scripts + `/repo-cleanup` slash 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.
|
||
2. **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-list` chain, 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](https://martinvonz.github.io/jj/latest/install/), [jq](https://jqlang.github.io/jq/), [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
||
- [Go](https://go.dev/dl/) 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.
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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).
|
||
|
||
```bash
|
||
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:
|
||
```bash
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```json
|
||
"command": "./tools/jj-hook.sh session-start"
|
||
```
|
||
|
||
And update the `_lib.sh` source line in `jj-hook.sh`:
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
[[ -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:
|
||
```json
|
||
"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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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.
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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.md` automatically. The skills are referenced as docs it can fetch via `@.claude/skills/codex/SKILL.md` etc.
|
||
- **Codex CLI**, **Gemini CLI**, **Kiro CLI** don't auto-load skill packs but respect `@`-file references in prompts.
|
||
- **Cursor / other IDEs** typically read `AGENTS.md` or `.cursorrules`; point those at `AGENTS.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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"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_TOKENS` is unset, `planctl` emits a raw count (`context: 143 k tokens (limit unknown)`) and, in JSON, only the `tokens_used` field inside `context_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):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```bash
|
||
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
|
||
|
||
- **`planctl` output never shows a `context:` line**: confirm you're running inside a Claude Code session. `echo $CLAUDE_SESSION_ID` should print a UUID. If not, check that the `SessionStart` hook fires and your shell sources `$CLAUDE_ENV_FILE`.
|
||
- **Wrong percentage**: `CLAUDE_CODE_MAX_CONTEXT_TOKENS` may be mis-set. Without it, you'll see `(limit unknown)` instead of a percentage.
|
||
- **Multiple concurrent sessions**: each agent's `planctl` resolves 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**: `planctl` works without the hook — if neither `CLAUDE_SESSION_ID` is set nor a PID-keyed file is found during the 8-hop ancestor walk, the context line is silently omitted.
|