Merge workflow: id alignment before merge
When two feature branches each allocate EPIC-04 (or FEAT-04-02, FIX-04-02-01, IMP-04-02-01) without knowing about each other, merging the second branch into dev produces an id collision. Without a fix, the artifact graph becomes inconsistent: two epics share an id, two features point at the same parent, the backlog shows duplicates.
DIA solves this with three cooperating components, all installed once via tools/install-git-hooks.sh.
The three components
| Component | File | Role |
|---|---|---|
| Renumber engine | tools/renumber-for-merge.py | Reads target backlog, computes mapping, applies renames + reference updates |
| Wrapper | scripts/merge-to-dev.sh | Renumbers source branch, commits chore(renumber), then merges |
| Safety net | tools/git-hooks/pre-merge-commit | Blocks direct git merge if duplicate ids land in the working tree |
Installation
In the target project, run once:
bash <DIA-repo>/tools/install-git-hooks.shThis installs both hooks (pre-commit and pre-merge-commit) under .git/hooks/ and copies the renumber engine to .git/hooks-data/renumber-for-merge.py.
Canonical path: through the wrapper
bash scripts/merge-to-dev.sh <source-branch> [<target-branch>]
# Default target: developThe wrapper:
- Snapshots
<target>to<target>-backup(lightweight branch for one-step rollback). - Switches to the source branch.
- Runs
tools/renumber-for-merge.py --target <target> --check-only. - If collisions exist: applies the renumber and auto-commits
chore(renumber): align ids with <target> before merge. - Switches to the target branch.
- Runs
git merge --no-ff <source-branch>.
The renumber commit lands on the source branch, so the audit trail of "what changed" is clean: one commit shows exactly which ids were shifted before the merge.
Rollback if needed:
git checkout <target>
git reset --hard <target>-backupDirect merge: what happens then?
git checkout develop
git merge --no-ff feature/fooThe pre-merge-commit hook scans the working tree (post-merge) for two artifact files carrying the same id. On hit it exits 1, the merge commit is not created, and the user sees:
Merge blocked: id collisions between the merging branch and dev.
Use the canonical merge path so ids are renumbered on the source
branch before the merge:
git merge --abort
git checkout <source-branch>
bash scripts/merge-to-dev.sh <source-branch> develop
If you really know what you are doing: bypass with --no-verify.git merge --no-verify skips the hook for legitimate edge cases (hotfix that must keep its id), but should not be the default.
What gets renumbered
The renumber maps old-id -> new-id and rewrites everywhere the old id appears:
- File names in
_devprocess/requirements/{epics,features,fixes,improvements}/ - File names of the corresponding Item-BAs in
_devprocess/analysis/BA-{EPIC,FEAT,IMP,FIX}-*.md - Frontmatter fields:
id,epic,feature,ba-ref,depends-on,feature-refs,adr-refs,supersedes,superseded-by,target-id,parent-feat - Body refs in every
*.mdunder_devprocess/ src/ARCHITECTURE.map(wayfinder)FIXME(stub):markers in the source tree (per graph-invariants E-14)
Not renumbered:
- ADR and PLAN ids (they have their own numbering, semantically decoupled from the EPIC/FEAT graph)
_devprocess/context/HANDOFFS.md(append-only audit trail)
Mapping rule
For each class, the next free id is computed from the target branch's BACKLOG.md (read via git show <target>:_devprocess/context/BACKLOG.md):
- EPIC:
max(target.EPIC) + 1 - FEAT: epic-local;
eefrom the EPIC mapping,ffis the next free counter inside the new epic - IMP / FIX: feature-local;
eeandfffrom the FEAT mapping,nnis the next free counter inside the new feature
The order of operations is deterministic: EPIC first, FEAT next (propagating the EPIC remap), IMP and FIX last (propagating the FEAT remap). Every source id has exactly one target id.
Modes of tools/renumber-for-merge.py
| Mode | Purpose |
|---|---|
--check-only | Exit 1 if collisions exist, else 0. No output. Used by hooks. |
--list-conflicts | Prints the mapping as JSON. No file changes. |
--dry-run | Computes mapping, prints plan, writes nothing. |
--check-tree-duplicates | Scans the working tree for two files with the same id. The pre-merge-commit hook uses this mode. |
--source-ref <ref> | Read source ids from a git ref instead of the working tree. Combine only with the read-only modes above. |
| (default) | Computes mapping, applies renames + updates. |
Edge cases
Same id on both sides, identical file. No collision, no rename. Common when both branches share a common upstream commit that introduced the artifact.
BACKLOG.md three-way conflict. A normal markdown merge conflict, orthogonal to the id collision. Resolve manually after the renumber. Tip: keep the source branch up to date with git rebase <target> before running the wrapper, so BACKLOG.md is not in conflict at merge time.
Source branch without _devprocess/. Script prints "No _devprocess/ found; nothing to renumber" and exits 0. Hook lets the merge through.
Multiple parallel feature branches. Each merge handles only the branch being merged. Other branches stay untouched and may need their own renumber when they merge later. /dia-migration Phase 8 and /reverse-engineering Phase 6.5 list all parallel branches with collision status, so the maintainer can plan ahead.
Test the workflow
Manual end-to-end verification: tools/test-merge-hook.md on GitHub. Five scenarios against a temp repo under /tmp:
- Direct merge is blocked by the hook
- Wrapper renumbers and merges through cleanly
- Idempotency (second run is no-op)
- FEAT and FIX renaming including body refs
- Mode self-test (
--list-conflicts,--check-only,--dry-run,--check-tree-duplicates)
Bypass rules
git merge --no-verifyskips thepre-merge-commithook. Acceptable for: a deliberate hotfix that must keep its id (for example, an external tracker references the id literally).- There is no environment-variable override for the script. The explicit hook bypass is the only escalation path.
Known stumbling blocks
pre-merge-commitonly fires for actual merge commits, not for fast-forward merges. If the source branch is directly ahead of the target tip, no hook runs. Force a real merge commit withgit merge --no-ffor usescripts/merge-to-dev.sh.MERGE_HEADis not reliably present at thepre-merge-commitpoint (depends on merge strategy and git version). The hook uses--check-tree-duplicatesinstead, which sees two files with the same id directly in the working tree.- macOS bash 3.x works, but
set -einteractions insideif !constructs can be subtle. To debug a hook problem: runbash -x .git/hooks/pre-merge-commitafter agit merge --no-committo inspect the trace.
Read the source
Tool source on GitHub:
tools/renumber-for-merge.pytools/git-hooks/pre-merge-commitscripts/merge-to-dev.shtools/install-git-hooks.sh
Skill integrations: