template-jj/INTEGRATION.md
sid e913ce1880
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
merge: integrate planctl v2 task-cmds + context-window token awareness
2026-04-26 09:42:58 -06:00

442 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) |
| 7084% | Info | `info` | `plan to wrap up this session soon.` |
| 8594% | 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.