Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .githooks/jj-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

# forge jj push gate. jj runs no git hooks, so the pre-push gate is relocated here.
# Wired by `make install` as the repo-local jj `push` alias (absolute path so it
# resolves from any working directory):
# jj config set --repo aliases.push '["util","exec","--","bash","<repo>/.githooks/jj-push"]'
# Runs the same pre-push gate as git's pre-push hook, then hands off to jj git push.
# Canonical source: https://github.com/N4M3Z/forge-cli/blob/main/templates/init/.githooks/jj-push

# cd to the repo root so prek finds .pre-commit-config.yaml regardless of cwd.
cd "$(jj workspace root 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || dirname "$0")"

if command -v prek >/dev/null 2>&1; then
prek run --all-files --stage pre-push
elif command -v forge >/dev/null 2>&1; then
forge validate
fi

exec jj git push "$@"
36 changes: 36 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail

# forge module pre-push hook — runs the pre-push-staged gates (gitleaks, semgrep).
# Canonical source: https://github.com/N4M3Z/forge-cli/blob/main/templates/init/.githooks/pre-push

SCRIPT_URL="https://raw.githubusercontent.com/N4M3Z/forge-cli/main/scripts/validate.sh"
SCRIPT_SHA="55998b60a7e972c09c9bcc9767d675b95e011e58309c69849b44fec994909d0b"

if command -v prek >/dev/null 2>&1; then
prek run --all-files --stage pre-push
elif command -v forge >/dev/null 2>&1; then
forge validate
else
echo ""
echo " ⚠ Neither prek nor forge found — using validate.sh fallback"
echo " Install: cargo install --locked forge-cli or cargo binstall prek"
echo ""

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

curl --max-time 30 -sfL "$SCRIPT_URL" -o "$tmpdir/validate.sh" 2>/dev/null \
|| { echo "validate.sh unreachable — install forge-cli or check connectivity"; exit 1; }

actual=$(sha256sum "$tmpdir/validate.sh" 2>/dev/null || shasum -a 256 "$tmpdir/validate.sh")
actual=${actual%% *}

if [ "$actual" != "$SCRIPT_SHA" ]; then
echo "validate.sh hash mismatch (expected $SCRIPT_SHA, got $actual)"
echo " update SCRIPT_SHA after verifying the new script"
exit 1
fi

bash "$tmpdir/validate.sh" .
fi
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ config.yaml
# forge assemble output
build/

# Parallel feature work
.worktrees/

# Entire session data (runtime, not source)
.entire/metadata/
.entire/logs/
.entire/tmp/
.entire/settings.local.json
.entire/redactors/local/

# Provider directories (generated by make install)
.claude/
.gemini/
Expand Down
5 changes: 5 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
[extend]
useDefault = true

[allowlist]
paths = [
"evals/baselines/.*",
# DefensiveProgramming teaches secure coding and contains example secrets by design.
"skills/DefensiveProgramming/.*",
]
86 changes: 86 additions & 0 deletions skills/CmuxToolkit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: CmuxToolkit
version: 0.1.0
description: "Drive cmux from its Unix-socket CLI: open a new split, pane, or tab and run a command in it by sending text and keys, build declarative layouts, render a diff in the native viewer, and resolve window / workspace / pane / surface handles. USE WHEN opening tuicr or any command in a new cmux pane, splitting the current pane from the CLI, sending commands or keys to another surface, watching logs in a side pane, showing a diff in cmux, or scripting cmux layouts. For app config, persistence, and hooks see the cmux tldr."
sources:
- https://raw.githubusercontent.com/manaflow-ai/cmux/main/docs/cli-contract.md
- https://cmux.com/docs/api
- https://github.com/manaflow-ai/cmux
---

# CmuxToolkit

cmux is controlled through a Unix-socket CLI that addresses a tree of windows > workspaces > panes > surfaces. The headline capability for an agent: **open new panes and drive them by sending terminal commands**, so a review TUI, a log tail, a dev server, or any command runs in its own split beside your work. App setup, config, persistence, and Claude Code hooks live in the cmux tldr.

The binary is `cmux`, installed and symlinked into `~/.local/bin` by the provisioning module.

## Open a pane and run a command in it

`new-split` and `new-pane` create an empty terminal but do NOT accept a command, so it is two steps: spawn the pane, capture its surface ref, then `send` the command line. `send` types text into the surface; only `\n` (or `\r`) presses Enter, so without it the command sits unrun.

```sh
surf=$(cmux new-split right --focus true | grep -oE 'surface:[0-9]+')
cmux send --surface "$surf" "cd /path/to/repo && tuicr -r 'main@origin..my-bookmark'\n"
```

Spawn as many panes as you like and send each its own commands, building a live layout (review here, logs there, a shell over there). Send more lines anytime to the same ref; interrupt or submit with `send-key`.

| Action | Command |
| ---------------------------------------- | --------------------------------------------------------------- |
| Split the current pane (returns the ref) | `cmux new-split <direction> [--focus true]` |
| New terminal or browser pane | `cmux new-pane [--type terminal/browser] [--direction <dir>]` |
| New tab that runs a command immediately | `cmux new-workspace --cwd <dir> --command "<cmd>" [--name <t>]` |
| Run a command in a surface | `cmux send --surface <ref> "<cmd>\n"` |
| Send one key (submit, interrupt) | `cmux send-key --surface <ref> <key>` (`enter`, `ctrl+c`) |

Escape sequences for `send`: `\n` / `\r` = Enter, `\t` = Tab. Only `new-workspace` accepts `--command` directly; `new-split` / `new-pane` need the `send` follow-up. A new split inherits the caller's cwd, so prefix the sent command with `cd <dir> &&`. `send-panel` / `send-key-panel` are the panel-targeted variants.

## Targeting surfaces

Every pane is a surface. cmux injects three env vars per pane: `CMUX_SURFACE_ID` (this pane), `CMUX_WORKSPACE_ID` (this tab), `CMUX_SOCKET_PATH` (the control socket). Targets accept a UUID, a short ref (`surface:30`, `workspace:9`, `tab:2`), or an index; output prints refs by default. With no `--surface`, a command acts on the caller's own pane, so always pass the ref captured from `new-split` when driving a different one.

| Command | Effect |
| ---------------------- | ------------------------------------------------------------ |
| `cmux tree --all` | Print the full window / workspace / pane / surface tree |
| `cmux identify --json` | Report the caller's own window / workspace / surface context |
| `cmux list-panes` | List panes in a workspace |
| `cmux top` | Per-surface process and resource usage |
| `cmux move-surface` | Move a surface to another pane, workspace, window, or index |
| `cmux split-off` | Move a surface into a new split without stealing focus |
| `cmux notify` | Post a notification to a workspace or surface |
| `cmux events` | Stream cmux events as newline-delimited JSON (reconnectable) |

## Declarative layouts

For a fixed multi-pane arrangement, pass `--layout` (inline JSON, same schema as `cmux.json` `commands[]`) instead of `--command`. Surfaces are leaves with `type` (`terminal` / `browser`) and optional `command`, `cwd`, `url`, `env`; splits nest via `direction`, `split` (0.0-1.0 ratio), and `children`:

```sh
cmux new-workspace --name Review --layout '{"direction":"horizontal","split":0.5,"children":[
{"pane":{"surfaces":[{"type":"terminal","command":"tuicr -w","cwd":"/repo"}]}},
{"pane":{"surfaces":[{"type":"terminal","command":"jj log"}]}}]}'
```

Structural templates reference no versions or APIs, so they survive cmux updates. The same recipes live in `cmux.json` `commands[]`, invokable from the Command Palette (see the cmux tldr).

## Native diff viewer

`cmux diff` renders a unified diff in a read-only browser split. It reads piped stdin, so any diff source works, and also has git-native sources:

```sh
jj diff -r @- --git | cmux diff --title "diff" --layout split --focus true
git diff | cmux diff
cmux diff --branch --base main --cwd /repo # or --unstaged / --staged / --last-turn
```

Options: `--source <unstaged|staged|branch|last-turn>` (or the matching flags), `--base <ref>`, `--cwd/--repo <path>`, `--layout <split|unified>`, `--font-size <pt>`, `--title`, `--focus`. It requires the cmux browser enabled (`cmux enable-browser`), failing `browser_disabled` otherwise. For an interactive, annotatable review, prefer a TUI (`tuicr` / `revdiff`) in a `new-split` pane.

## Gotchas

| Symptom | Cause / Fix |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `Failed to connect to socket ... not permitted` | The agent command sandbox blocks the cmux Unix socket. Run cmux CLI commands with the sandbox lifted. |
| `cmux --help` omits the pane / window verbs | It is truncated. Use `cmux docs api` and the cli-contract for the authoritative command list. |
| `cmux diff` fails `browser_disabled` | The browser is off. `cmux enable-browser` first, or run a TUI in a `new-split` pane instead. |
| `send` lands in the wrong pane | The default target is the caller's own surface. Pass `--surface <ref>` captured from `new-split`. |
| The sent command runs in the wrong directory | Splits inherit the caller's cwd. Prefix with `cd <dir> &&`, or use `new-workspace --cwd`. |
| Text was sent but nothing ran | Missing the trailing `\n`. `send` types text; only `\n` / `\r` submits it. |
54 changes: 54 additions & 0 deletions skills/JujutsuToolkit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: JujutsuToolkit
version: 0.1.0
description: "Jujutsu (jj) as a colocated VCS front-end over git for AI-agent work: colocate strategy, git-hook-tool coexistence (Entire, gitleaks, safety-net), parallel agents via jj workspaces, op-log recovery, provisioning. USE WHEN colocating a repo with jj git init --colocate, deciding whether jj coexists with Entire / gitleaks / safety-net or other git-hook tooling, isolating parallel agents with jj workspace, recovering jj state with jj undo or op restore, or debugging jj-and-git interaction. For commit and push discipline see VersionControl's Jujutsu companion; for commands see the jj tldr."
sources:
- https://docs.jj-vcs.dev/latest/
- https://docs.jj-vcs.dev/latest/git-compatibility/
- https://docs.jj-vcs.dev/latest/config/
- https://steveklabnik.github.io/jujutsu-tutorial/
---

# JujutsuToolkit

Jujutsu (jj) run colocated over an existing git repo, the version-control front-end for multi-agent dev work. The working copy is an auto-snapshotted commit, so there is no index for concurrent sessions to collide on, and signing is deferred to push. Decision and rationale: `docs/decisions/ARCH-0032` (forge-provision). Command reference: `docs/tldrs/jujutsu.md`. Commit and push discipline: forge-core VersionControl's `Jujutsu.md` companion. This skill covers colocation, tool coexistence, parallel agents, and recovery.

## Colocate strategy

`jj git init --colocate` puts `.jj/` beside `.git/` and syncs them on every jj command. That is the load-bearing choice: git tooling keeps working (gh, gitui, revdiff, tuicr, the IDE, Claude Code repo detection) while jj drives the model. A non-colocated repo hides git inside `.jj/repo/store/git` and blinds all of it, so always colocate.

The one rule: drive mutations through jj; treat raw git as read-only. Colocated git sits in detached HEAD (jj parks it), and interleaving jj and git writes is the only way to produce conflicting refs. Reads (`git log`, `git status`, `git diff`, blame, the review TUIs) are always safe. Before a rare mutating git command, run `git switch <branch>` first.

`.jj/` is local-only and never pushed; the remote sees ordinary git. Colocation is reversible: move `.jj/` aside (`rm -rf` is guarded by the dangerous-command hook) and the git repo is untouched.

## Compatibility with git-hook tools

jj runs no git hooks. Any tool whose behavior rides on git hooks is bypassed for jj-driven commits. This is not damage, an uncalled hook is inert, but it moves where the work happens.

- **Entire.** Its session capture runs off Claude Code hooks (Task / TodoWrite / session events), which are VCS-agnostic and keep working under jj. Only its git hooks (`prepare-commit-msg` / `commit-msg` trailer injection, `post-commit` condense, `post-rewrite` remap, `pre-push` log shipping) are skipped. Under a squash-merge plus `push_sessions: false` workflow that linkage is already moot. jj support in Entire's hook layer is upstream's to add; do not work around it here. See `docs/decisions/ARCH-0028`.
- **gitleaks and safety-net.** Both of VersionControl's commit-time secret gates ride git hooks (gitleaks via `.githooks/pre-commit`; safety-net intercepts `git commit`, which jj never calls), so both bypass under jj. forge modules relocate the gate automatically: `make install` wires a repo-local jj `push` alias to `.githooks/jj-push`, which runs `prek run --all-files --stage pre-push` then `jj git push`. Push with `jj push`; details in VersionControl's `Jujutsu.md`.

General rule: under jj, a git-hook-based check becomes a pre-push check (run by the `jj push` alias) or a CI check. For strict isolation across parallel jj workspaces, `jj-hp` (the `jj-hooks` crate) runs the same hooks in an ephemeral worktree and is the escalation path.

## Parallel agents

Multiple agents editing one working copy still collide at the file level; jj only removes the index collision. Give each agent its own working copy sharing one store: `jj workspace add ../repo-agent-1` creates an isolated directory and `@` backed by the same history and op log. This is the jj-native replacement for the git-worktree lifecycle (no manual create / rebase / remove / delete-branch). Companions: `jj workspace list`, `jj workspace forget`.

## Recovery

Every jj operation is recorded in the op log. `jj undo` reverses the last one; `jj op log` browses the history; `jj op restore <id>` rolls the whole repo back to any prior state, including a botched agent edit or rebase. `revsets.immutable-heads` (default `trunk() | tags() | untracked_remote_bookmarks()`) blocks rewriting published history, so an agent cannot accidentally rewrite trunk.

## Provisioning

Installed via the Brewfile (`brew "jj"`, binary `jj`) and configured globally by `scripts/configure/jujutsu.sh` (forge-provision), which reads identity and the signing key from `git config` and sets `signing.behavior=drop` plus `git.sign-on-push=true`. Verify with `jj config list --user`.

## Common pitfalls

| Symptom | Cause / Fix |
|---|---|
| A jj or git mutation ran in the wrong repo | The harness resets cwd to the project root each call. Use absolute paths, `jj -R <path>`, `git -C <path>`, and verify `pwd` before any mutating command. |
| `gh` / IDE shows detached HEAD | Normal when colocated. `git switch <branch>` only before a mutating git command. |
| A `git commit` appears to do nothing | jj owns mutations; the change is already in `@`. Commit through jj, not git. |
| A file watcher (Vite, test runner) thrashes | It is watching `.jj/`. Exclude `.jj/` from the watcher. |
| Large file refused on snapshot | `snapshot.max-new-file-size` defaults to 1MiB; raise it per-repo. No Git LFS support. |
| Entire stopped tagging commits | Expected: jj skips git hooks. Capture (Claude Code hooks) is unaffected; the linkage trailer is moot under squash-merge. |
Loading