From 46a1d598d230793872ec5496b9f5073a52e3e6b5 Mon Sep 17 00:00:00 2001 From: Martin Zeman Date: Wed, 17 Jun 2026 00:39:51 +0200 Subject: [PATCH] feat: JujutsuToolkit + CmuxToolkit skills, secret gate under jj and git --- .githooks/jj-push | 20 ++++++++ .githooks/pre-push | 36 ++++++++++++++ .gitignore | 10 ++++ .gitleaks.toml | 5 ++ skills/CmuxToolkit/SKILL.md | 86 ++++++++++++++++++++++++++++++++++ skills/JujutsuToolkit/SKILL.md | 54 +++++++++++++++++++++ 6 files changed, 211 insertions(+) create mode 100755 .githooks/jj-push create mode 100755 .githooks/pre-push create mode 100644 skills/CmuxToolkit/SKILL.md create mode 100644 skills/JujutsuToolkit/SKILL.md diff --git a/.githooks/jj-push b/.githooks/jj-push new file mode 100755 index 0000000..fa57cbc --- /dev/null +++ b/.githooks/jj-push @@ -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","/.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 "$@" diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..9e2ca59 --- /dev/null +++ b/.githooks/pre-push @@ -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 diff --git a/.gitignore b/.gitignore index 2e87984..8fa8a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/.gitleaks.toml b/.gitleaks.toml index c925804..8018da6 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,4 +1,9 @@ +[extend] +useDefault = true + [allowlist] paths = [ "evals/baselines/.*", + # DefensiveProgramming teaches secure coding and contains example secrets by design. + "skills/DefensiveProgramming/.*", ] diff --git a/skills/CmuxToolkit/SKILL.md b/skills/CmuxToolkit/SKILL.md new file mode 100644 index 0000000..610b4a7 --- /dev/null +++ b/skills/CmuxToolkit/SKILL.md @@ -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 [--focus true]` | +| New terminal or browser pane | `cmux new-pane [--type terminal/browser] [--direction ]` | +| New tab that runs a command immediately | `cmux new-workspace --cwd --command "" [--name ]` | +| Run a command in a surface | `cmux send --surface "\n"` | +| Send one key (submit, interrupt) | `cmux send-key --surface ` (`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 &&`. `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 ` (or the matching flags), `--base `, `--cwd/--repo `, `--layout `, `--font-size `, `--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 ` captured from `new-split`. | +| The sent command runs in the wrong directory | Splits inherit the caller's cwd. Prefix with `cd &&`, or use `new-workspace --cwd`. | +| Text was sent but nothing ran | Missing the trailing `\n`. `send` types text; only `\n` / `\r` submits it. | diff --git a/skills/JujutsuToolkit/SKILL.md b/skills/JujutsuToolkit/SKILL.md new file mode 100644 index 0000000..0a1d488 --- /dev/null +++ b/skills/JujutsuToolkit/SKILL.md @@ -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 ` 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 ` 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 `, `git -C `, and verify `pwd` before any mutating command. | +| `gh` / IDE shows detached HEAD | Normal when colocated. `git switch ` 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. |