jj: document the restore, divergent, and squash traps
Each trap cost real work and reported success while doing it. `jj
restore FILE` reset a file to its parent to back out one sed and took
unrelated uncommitted work with it. `jj squash` with only `--into`
moved nothing because `@` had advanced past the commit holding the
changes, yet still printed "Rebased 3 descendant commits" and gave the
destination a new commit ID, so the no-op read as success. Concurrent
workspaces produce the same "my edits vanished" symptom by a third
route, so a new Divergent Changes section covers recovery — including
that `abcd/0` offsets are ordered by recency and shift under a
concurrent rewrite. jj-workspaces gains a red flag for rewriting a
revision another workspace has checked out and routes to that section
rather than repeating the recovery steps.
Also moves the "never authoritative, run --help" note to the top, where
it is read before the rest of the skill rather than after, and cuts a
paragraph that only restated the commit/describe/new table.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/ai/skills/jj-workspaces/SKILL.md b/ai/skills/jj-workspaces/SKILL.md
index e1ad1e2e..99fadbb0 100644
--- a/ai/skills/jj-workspaces/SKILL.md
+++ b/ai/skills/jj-workspaces/SKILL.md
@@ -124,6 +124,10 @@ Use Task tool to dispatch in parallel. **Start agents in the workspace directory
- `jj workspace` commands
- `jj git push`
- `jj rebase`
+- Rewriting a commit another workspace has checked out (`describe`,
+ `squash`, `abandon`, `restore --into`). Check `jj log -r
+ 'working_copies()'` before touching any revision other than `@`. Two
+ workspaces rewriting the same commit is what produces divergent changes.
- Any work outside assigned workspace
### Phase 4: Review (Coordinator)
@@ -177,6 +181,7 @@ jj workspace update-stale
| Empty WC commits after rebase | After rebase + forget, run `jj abandon 'empty() & description(exact:"") & @-'` to drop the workspace's leftover empty WC |
| Forgetting cleanup | Remove directories after forgetting workspace |
| jj not seeing file changes | Run `jj` or `jj status` periodically—jj only snapshots on command execution |
+| Files written in one workspace vanish | Concurrent rewrite left the change divergent. See the `jj` skill's Divergent Changes section—recover, don't rewrite |
## Red Flags
diff --git a/ai/skills/jj/SKILL.md b/ai/skills/jj/SKILL.md
index e8ff52ae..7bde60d8 100644
--- a/ai/skills/jj/SKILL.md
+++ b/ai/skills/jj/SKILL.md
@@ -5,7 +5,8 @@ description: Use when running any jj (Jujutsu) version control command — commi
# Jujutsu (jj) Version Control
-Use `jj --help` for authoritative command reference and current flags.
+Never treat this skill as authoritative. Run `jj <command> --help` to verify
+behavior, flags, and current options.
## Core Workflow
@@ -25,10 +26,6 @@ they move `@`:
| `jj new <rev>` | no | yes, to a new empty commit | start fresh work on top of `<rev>` |
| `jj commit -m` | yes, on `@` | yes, to a new empty commit | finish `@` and move on — `describe @` + `new` in one step |
-So `jj commit` is just `jj describe` followed by `jj new`. Reach for
-`describe` when you only want to fix the current message without moving
-on; `new` when you want a fresh commit without touching messages.
-
**Prefer `jj commit` over `jj squash`.** Create new commits by default —
even for refactors, cleanups, or small follow-ups. Only squash when the
prior commit specifically needs fixing up (e.g., correcting a bug it
@@ -93,6 +90,34 @@ jj restore SOME_FILE # restore file from parent
jj restore --from kn --into kn FILE # restore file in a specific revision
```
+**`jj restore FILE` is not "undo my last edit."** It resets the whole
+file to the parent revision, wiping *every* uncommitted change in it —
+not just the one you meant to back out. Backing out a temporary sed
+this way also destroyed unrelated in-progress work in the same file.
+Before restoring, run `jj diff FILE` to see everything that would be
+lost; to back out one surgical experiment, revert it with the same
+tool that made it (another sed/edit) instead of `jj restore`.
+
+## Divergent Changes
+
+A change is divergent when it has more than one visible commit — most often
+because two workspaces rewrote it concurrently. jj labels these
+`(divergent)` in the log, and the edits from one side appear to have
+vanished. Don't rewrite the missing files; recover them:
+
+```bash
+jj log -r 'divergent()' # find them
+jj diff -r <commit> --name-only # see what each holds
+jj restore --from <commit> <paths> # recover
+```
+
+A bare change ID errors once it's divergent, so use the change offsets jj
+suggests (`abcd/0`, `abcd/1`) to inspect them. Offsets are assigned by
+recency, with the most recent commit at `/0`, so re-read `jj log` rather
+than reusing an offset from earlier in a session — a concurrent rewrite in
+another workspace shifts them all down by one. For `jj abandon`, pass the
+short commit ID, as jj's own hint recommends.
+
## Common Pitfalls
**Avoid revset functions with parentheses.** Claude Code's shell
@@ -156,6 +181,18 @@ jj log --cwd /path/to/repo # WRONG — no such flag; fails silently
**Put `-m` before `--` or fileset args.** jj parses everything after `--` as
fileset, so `-m` placed after `--` becomes a parse error.
+**`jj squash` takes its source from `@` unless you pass `--from`.** Naming
+only `--into <rev>` and a fileset moves that fileset out of `@`, not out of
+the commit you were reading about. When `@` has moved since you last ran
+`jj log` — the human committing in another terminal is enough — the squash
+moves nothing, yet still prints "Rebased N descendant commits" and gives the
+destination a new commit ID, so a no-op looks like it worked. Name the source
+and re-check `jj st` immediately before any rewrite:
+
+```bash
+jj squash --from <rev> --into <rev> -u FILE # explicit source
+```
+
**`jj squash` opens `$EDITOR` to merge descriptions and hangs in
non-interactive shells.** When source and destination both have
descriptions, jj launches the editor to combine them — Claude Code's
@@ -170,8 +207,3 @@ jj squash -m 'new message' # inline replacement
The same applies to `jj squash --from X --into Y`. If you actually want
to merge the two descriptions, do it from a real terminal.
-
-## When to Use jj --help
-
-Never rely on this skill as authoritative. Always run `jj <command> --help`
-to verify behavior, flags, and current options.