---
name: dld-reindex
description: Resolve decision ID collisions between a local branch and the base branch (and open PRs). Renames colliding local decisions with git mv, rewrites cross-references and annotations, and commits the renames on top of the branch. Squashes the branch only when the user asks.
compatibility: Requires Node.js 20+ and git. Open-PR scanning additionally needs the `gh` CLI authenticated against a GitHub remote — the skill falls back gracefully when unavailable.
metadata:
dld-kit-version: "1.0.0-rc.4"
---
# /dld-reindex — Resolve Decision ID Collisions
You are helping the developer untangle decision ID collisions. Two or more developers can draft `DL-NNN` decisions in parallel; once one of them lands on the base branch (or appears in an open PR), the others must rename their local copies to the next free ID. This skill does that mechanically.
**By default the skill adds a commit and does not rewrite history.** The renames go into one new commit on top of the branch (or a few, for a large reindex). A merge only compares end states, so once the branch tip has the record at its new path, `git merge ` and merging the PR on GitHub (merge commit or squash merge) don't conflict on the records. A normal push is enough.
**Squash mode, only when the user asks for it.** A rebase replays every branch commit, and the commit that added the colliding path (e.g. `decisions/records/DL-205.md`) hits an add/add conflict, whatever later commits do. If the user wants to rebase (or their repo uses GitHub's "Rebase and merge"), they can ask for a squash: the branch's commits since the merge-base become one reindex commit, and a pushed branch then needs `git push --force-with-lease`. Use squash mode only when the user's request says so (e.g. `/dld-reindex squash`, "squash it", "I want to rebase"). Never pick it on your own.
## Interaction style
Use the `AskUserQuestion` tool when asking about pushing and, in squash mode, for consent to rewrite history. Everything else is deterministic.
**Do not redirect any command output to `/tmp` files.** The commands in this skill emit only what you need to act on; piping to `/tmp/*.txt`, `tee`-ing into scratch files, or stashing stderr separately is unnecessary and creates clutter outside the repo. If a command's output is too long to read in one go, narrow it (`| tail -N`, `| head -N`, or pass a more specific flag) rather than persisting it.
## Commands
The commands below run the `dld` CLI bundled with the dld-common skill, and need Node.js 20+. `` stands for the absolute path of this skill's directory. If `/../dld-common/scripts/dld.mjs` does not exist, stop and tell the user to install the dld-common skill: `npx skills add jimutt/dld-kit --skill dld-common`.
This skill uses: `resolve-base`, `plan-renames`, `rename-decision`, `find-stale-mentions`, `commit-reindex`, `regenerate-index`.
The steps below use shell variables (`BASE`, `PLAN`, `GROUP`). If your shell doesn't keep variables between commands, set them again in each command, or write the values out (e.g. pipe the plan lines with `printf '%s\t%s\t%s\n' ...`). `find-stale-mentions` and `commit-reindex` fail on an empty plan, so a lost variable shows up as an error.
## Prerequisites
1. Check that `dld.config.yaml` exists at the repo root. If not, tell the user to run `/dld-init` first and stop.
2. Verify the working tree is clean (`git status --porcelain` empty). If not, ask the user to commit or stash first, then stop.
3. Fetch the latest base state:
```bash
git fetch origin
```
## Step 1: Resolve the base ref
```bash
BASE=$(node "/../dld-common/scripts/dld.mjs" resolve-base)
```
`resolve-base` prefers the branch's upstream when it tracks a *different* branch (the typical "feature → main" setup). It falls back to `origin/main` if the upstream is unset OR if the upstream tracks the same branch name as the current branch (i.e. it's just the remote copy of this same branch, not a useful collision base).
The user may pass an explicit base when invoking the skill (e.g. `/dld-reindex origin/develop`) — honor it if present. The word `squash` is not a base: it selects squash mode (`/dld-reindex squash origin/develop` does both).
## Step 2: Plan the renames
```bash
PLAN=$(node "/../dld-common/scripts/dld.mjs" plan-renames --base "$BASE")
echo "$PLAN"
```
Output is tab-separated, one rename per line:
```
\t\t
```
If the output is empty, exit with:
> No ID collisions with `$BASE` or open PRs.
`plan-renames` may print a stderr note like `[dld-reindex] open PRs not scanned: gh CLI not installed`. **Always surface this to the user** so they know the renamed IDs were chosen against base-branch state only and may still collide with an open PR.
**Check that each rename is a real collision.** For each plan line, compare the local record with the base's record of the same ID: `git show "$BASE:"`, or, when the base keeps it in another namespace directory, find it with `git ls-tree -r --name-only "$BASE" | grep '/DL-205\.md$'`. If they are the same decision (same title and substance, give or take later edits), the branch got it from a PR that has since merged into the base, typically a decisions PR this branch was stacked on that was squash- or rebase-merged. Don't rename it. Stop and tell the user to merge the base into the branch first (`git merge $BASE`; if INDEX.md conflicts, `node "/../dld-common/scripts/dld.mjs" regenerate-index` and `git add` it), then run `/dld-reindex` again.
A record this branch got from an open PR it is stacked on (an implementation branch cut from a decisions PR) is not a collision with that PR: it is the same decision. This needs the PR's head commit locally, which the `git fetch origin` above provides; a PR from a fork whose head isn't fetched still counts as a collision.
The underlying helpers (`find-collisions`, `list-taken-ids`) remain available for debugging, but the SKILL flow always goes through `plan-renames`.
## Step 3: Choose how to commit
**Default mode (no squash requested).** Don't ask for consent; the renames are new commits the user can review or revert. Decide how many commits to make:
- **One commit** for the whole plan. This is the normal case.
- **Several commits** when the reindex is large: many renames, or renames that rewrite many files (lots of annotations, amendments, supersedes or references between the renamed decisions). Each commit holds one decision, or a group of related ones (e.g. a decision and the local decisions that amend or supersede it). Say in a line which groups you chose.
**Squash mode (the user asked for it).** Show the rename plan and the implication, then use `AskUserQuestion`:
> Squashing rewrites branch history. I will squash the N commits since `` into a single reindex commit. The original commit subjects will be preserved in the new commit body. If the branch has already been pushed, finishing will require `git push --force-with-lease`. How should I proceed?
Options:
- **Rewrite and force-push** — agent applies renames, squashes, commits, and runs `git push --force-with-lease`.
- **Rewrite only** — agent applies renames, squashes, and commits. User pushes when ready.
- **Cancel** — abort with no changes.
If the user cancels, exit without touching anything.
## Step 4: Apply renames
Run steps 4 to 6 once for the whole plan, or, in default mode, once per group when you split the work. Put the lines you are working on in `GROUP`, in the plan's format. For a single commit or a squash, that's the whole plan; for a group, pick its lines by old ID:
```bash
GROUP="$PLAN" # whole plan
GROUP=$(printf '%s\n' "$PLAN" | grep -E $'\t(DL-205|DL-207)\t') # one group
```
For each line in the plan (or group), call:
```bash
node "/../dld-common/scripts/dld.mjs" rename-decision --old DL-OLD --new DL-NEW --path --base "$BASE"
```
`rename-decision` does all of:
- `git mv` the file from `DL-OLD.md` to `DL-NEW.md`.
- Patches the `id:` frontmatter field in the renamed file.
- Rewrites `DL-OLD` mentions inside the renamed file's body.
- Rewrites `DL-OLD` mentions inside OTHER locally-added/modified decision files (frontmatter `supersedes` / `amends` / `references` and body). Scoped to the local change set vs the base ref.
- Rewrites `` `@decision` ``(DL-OLD) annotations to `` `@decision` ``(DL-NEW) in non-decision files that are part of the local change set.
The substitution is digit-aware: renaming `DL-100` will not accidentally rewrite `DL-1000`.
**Note on plain-text DL-NNN mentions in code:** In non-decision files (source code, READMEs, etc.) `rename-decision`'s rewrite is **scoped to `` `@decision` ``(DL-NNN) annotations only**. Bare `DL-NNN` references in comments, log strings, or test fixtures are left untouched here to avoid false-positive matches against unrelated identifiers. Step 5 handles them.
## Step 5: Review plain-text DL-OLD mentions
After the renames are applied (and before committing), find any remaining bare `DL-OLD` references in non-decision changed files:
```bash
echo "$GROUP" | node "/../dld-common/scripts/dld.mjs" find-stale-mentions --base "$BASE"
```
Output is tab-separated, one match per line: `\t\t\t\t`. Empty output means nothing to review.
For each match:
1. Read the surrounding context in the file.
2. Decide whether the reference makes sense as `DL-OLD` or should become `DL-NEW`. A code comment like `// Span-driven batching (DL-207) groups resolutions sharing the same turn context (DL-202)` after a `DL-207 → DL-213` rename almost certainly wants `DL-213` (it's prose alongside an annotation). A test fixture string that's checking historical data may need to stay as `DL-OLD`.
3. If the line should change, use the `Edit` tool to update that specific occurrence — don't do a blanket find-and-replace.
4. If the line should stay, leave it.
Surface the list of matches to the user with your verdicts before you finish, so they can sanity-check the judgment calls.
## Step 6: Commit
**Do not use `git add -A` or `git commit -a` anywhere in this flow.** Use only `commit-reindex` to commit.
**Default mode.** Regenerate INDEX.md so it lists the new IDs, then commit:
```bash
node "/../dld-common/scripts/dld.mjs" regenerate-index
echo "$GROUP" | node "/../dld-common/scripts/dld.mjs" commit-reindex --base "$BASE"
```
`commit-reindex` checks that every renamed file exists, stages the plan's old and new paths plus every tracked file changed since HEAD (the renames, the rewritten references and annotations, your step 5 edits, INDEX.md), and commits on top of HEAD. Untracked files are not staged. The message lists the group's renames. If the commit fails (e.g. a hook rejects it), the index is restored and HEAD has not moved.
If you split the work, go back to step 4 for the next group.
**Squash mode.** Pipe the whole rename plan into `commit-reindex --squash`:
```bash
echo "$PLAN" | node "/../dld-common/scripts/dld.mjs" commit-reindex --base "$BASE" --squash
```
This:
1. Computes the merge-base with `$BASE`.
2. Mixed-resets HEAD to the merge-base (working tree is preserved — it already holds the post-rename state from step 4).
3. Restores `INDEX.md` in the working tree to its merge-base state so the working tree is consistent with what we're about to commit. **INDEX.md is intentionally excluded from the reindex commit** — including it would cause a content conflict during rebase whenever the base branch also modified INDEX.md (git's 3-way merge fails to align both sides' top-of-file inserts even when row content overlaps). INDEX.md gets regenerated post-rebase instead.
4. Stages **only** an explicit path list derived from the original branch diff and the rename plan — the old paths (for deletions), the new paths (for additions), every other file the branch touched. Untracked unrelated paths (e.g. `.claude/worktrees`, scratch files, in-progress edits to unrelated files) are deliberately NOT swept in.
5. Commits with a templated message that lists the renames in the subject and preserves the original branch commits' subjects in the body.
## Step 7: Push
**Default mode.** Use `AskUserQuestion` to ask whether to push now (options: **Push** / **Don't push**). If yes:
```bash
if git rev-parse --verify --quiet "@{upstream}" >/dev/null; then
git push
else
git push -u origin HEAD
fi
```
**Squash mode.** If step 3's answer was "Rewrite and force-push":
```bash
if git rev-parse --verify --quiet "@{upstream}" >/dev/null; then
git push --force-with-lease
else
git push -u origin HEAD
fi
```
`--force-with-lease` is mandatory over `--force` here — it refuses to push if the remote moved since the last fetch, which protects against overwriting concurrent collaborator pushes.
## Step 8: Report
Print:
- The renames table.
- The stderr note from step 2 if `gh` was skipped.
- The commits created (default mode), or the number of commits squashed (squash mode).
- The next steps.
Default mode:
> 1. Bring in the base when you need it: `git merge $BASE`, or merge the PR as usual.
> 2. If INDEX.md conflicts (both sides added rows), run `node "/../dld-common/scripts/dld.mjs" regenerate-index`, then `git add` INDEX.md and finish the merge.
> 3. Don't rebase this branch onto `$BASE`: its earlier commits still add the old paths and will conflict. To rebase anyway, before you merge the base, drop the reindex commits (`git reset --keep HEAD~N`, N = the number of reindex commits) and run `/dld-reindex squash`.
Squash mode:
> 1. `git rebase $BASE`
> 2. `node "/../dld-common/scripts/dld.mjs" regenerate-index` (to repopulate INDEX.md with the renamed locals — the reindex commit intentionally leaves INDEX.md alone to keep the rebase conflict-free; INDEX.md is missing the renamed rows until you regenerate)
> 3. Commit the INDEX.md update
The skill never rebases or merges — that is always the user's call.
## Out of scope
- **Already-conflicted rebases.** If the user is mid-rebase with conflicts, tell them to `git rebase --abort` first and re-run this skill.
- **Rewriting each commit in place.** Neither mode rewrites the branch commit by commit (a cherry-pick walk); the edge cases (commits modifying an already-renamed file, merge commits, partial reruns) make it much more complex than adding a commit or squashing.
- **Cross-namespace ID reconciliation** in namespaced projects. IDs are assumed globally unique across namespaces, matching `next-id`.