Skip to content
Open
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
125 changes: 125 additions & 0 deletions PULL_REQUEST_DRAFT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# PR Draft: feat: add /gsd:sync for clean parallel-branch merges

> **Instructions for submitting:**
> 1. Fork `glittercowboy/get-shit-done` on GitHub (if you haven't already)
> 2. Push this branch to your fork
> 3. Open a PR from your fork's branch to `glittercowboy/get-shit-done:main`
> 4. Copy the text below the line as the PR body
> 5. Delete this file before committing (or add it to .gitignore)
>
> **Related issues to reference:** #64 (worktrees, closed), #707 (quick branching, open)
>
> **README note:** See bottom of this file for suggested README.md addition.

---

## feat: add `/gsd:sync` for clean parallel-branch merges

### Problem

GSD's `.planning` indexes are computed at runtime by scanning the filesystem — phase numbers from `ROADMAP.md`, quick task numbers from the `quick/` directory. There's no central counter. This works perfectly for linear, single-branch development, but creates real friction when development isn't linear:

- Solo developers running multiple agents in parallel on different branches
- Git worktrees (one per feature, one per environment, etc.)
- Any workflow where two branches independently run `/gsd:add-phase` or `/gsd:quick`

Both branches compute the same "next" number from the same base, producing index collisions:

```
main at phase 17, quick task 3

branch-A: /gsd:add-phase → creates phases/18-feature-a/
branch-B: /gsd:add-phase → creates phases/18-feature-b/ ← same number

branch-A merges first. Now branch-B needs to merge.
Result: ROADMAP.md conflict, ambiguous directories, manual renaming across
phases/, PLAN files, SUMMARY files, ROADMAP.md, STATE.md.
```

This was discussed in #64 and is a recurring pain point as GSD usage grows beyond strictly sequential single-context workflows. Modern AI-assisted development — including GSD's own parallel wave execution — naturally produces parallel workstreams.

### Solution

`/gsd:sync [--dry-run] [--target=<branch>]`

A pre-merge preparation command that resolves index collisions **before** they become merge conflicts, using git to read the target branch's state without switching to it.

**How it works:**

1. Reads target branch's `.planning/` state via `git ls-tree` / `git show <target>:...` — no branch switch required
2. Identifies branch-local phases and quicks (present locally, absent on target)
3. Detects which ones have conflicting index numbers
4. **Absorbs target's ROADMAP.md phase entries into the local ROADMAP** — so git sees them as already present on both sides, eliminating the ROADMAP.md merge conflict entirely
5. Renumbers conflicting branch-local phases and quicks to start after the target's highest index
6. Renames directories and all internal files (`git mv`, preserving history)
7. Updates `ROADMAP.md` and `STATE.md` references to match

**Result:** merge (not rebase) produces zero `.planning` conflicts.

**Example:**

```
# On feature-branch, after branch-A has already merged phase 18:

/gsd:sync

GSD Sync Plan
=============
Target branch : main (max phase: 18, max quick: 4)
Current branch: feature-branch

PHASES
Absorb from main: Phase 18: feature-a → inserted into local ROADMAP.md
Rename: phases/18-feature-b/ → phases/19-feature-b/
18-01-PLAN.md → 19-01-PLAN.md
18-01-SUMMARY.md → 19-01-SUMMARY.md

QUICKS
Rename: quick/4-fix-x/ → quick/5-fix-x/

Proceed? (y/n): y

✓ Absorbed 1 ROADMAP entry from main
✓ Renamed 1 phase: 18 → 19
✓ Renamed 1 quick: 4 → 5
✓ Updated ROADMAP.md, STATE.md

Ready to merge. No .planning conflicts expected.
```

**`--dry-run`** shows the full plan with zero changes — useful before opening a PR or during code review.

### Why not change the naming scheme?

An alternative approach is slug-only phase directories (dropping numeric prefixes entirely). That's a cleaner long-term architecture but a breaking change requiring migration of all existing projects. `/gsd:sync` is additive — zero impact on users who work linearly, opt-in for those who don't.

### Files changed

- `commands/gsd/sync.md` — new command

### Testing notes

Tested against a project with:
- Two branches both adding phases from the same base
- Quick task collisions
- Target branch with ROADMAP entries the local branch hadn't seen

---

## README.md — suggested addition

Add to the **Utilities** table (after `/gsd:health`):

```markdown
| `/gsd:sync [--dry-run] [--target=<branch>]` | Resolve .planning index conflicts before merging parallel branches |
```

Optionally, a short note in the **Git Branching** configuration section:

```markdown
> **Working in parallel?** If you use git worktrees or run multiple agents on
> different branches simultaneously, run `/gsd:sync` before merging to resolve
> any `.planning` index collisions automatically.
```

This surfaces the feature to users who configure `branching_strategy` — the most likely audience.
250 changes: 250 additions & 0 deletions commands/gsd/sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
---
name: gsd:sync
description: Sync .planning indexes with target branch so the merge is clean — no rebase needed
argument-hint: [--dry-run] [--target=<branch>]
allowed-tools:
- Read
- Write
- Edit
- Bash
---

<objective>
Prepare the current branch for a clean merge into a target branch (default: main) by resolving
.planning index collisions before they become merge conflicts.

Specifically:
1. Detect phase/quick index numbers that exist on both branches (collision)
2. Absorb phase entries from the target's ROADMAP.md that aren't in the local one (pre-empts ROADMAP.md merge conflicts)
3. Renumber branch-local phases and quicks to start after the target's highest index
4. Rename all affected directories and files to match

After running this, merging produces zero .planning conflicts.
</objective>

<execution_context>
Arguments: $ARGUMENTS
- --dry-run: show all planned changes but make none
- --target=<branch>: base branch to sync against (default: main)
</execution_context>

<process>

## Step 1: Parse arguments and safety checks

Parse --dry-run and --target=<branch> from $ARGUMENTS. Default target to `main`.

Run `git branch --show-current` to get current branch name.
If current branch equals target branch: output error and stop.
"Already on [target]. Switch to your feature branch first."

Run `git status --short` and note any uncommitted changes. If dirty, warn the user but continue.

## Step 2: Read target branch state

Run these commands to read the target branch without switching to it:

```bash
git ls-tree --name-only <target> -- .planning/phases/
git ls-tree --name-only <target> -- .planning/quick/
git show <target>:.planning/ROADMAP.md
git show <target>:.planning/STATE.md
```

If .planning/ doesn't exist on target yet, treat all as empty.

Extract:
- **target_phases**: list of phase directory names (e.g. `["01-foundation", "17-auth", "18-feature-a"]`)
- **target_quicks**: list of quick directory names (e.g. `["1-fix-sidebar", "3-update-config"]`)
- **target_roadmap**: full ROADMAP.md text from target
- **target_max_phase**: highest integer phase number in target_phases (ignore decimal parts — `17.1` counts as `17`)
- **target_max_quick**: highest integer quick number in target_quicks

## Step 3: Read local branch state

List `.planning/phases/` and `.planning/quick/` directories.
Read `.planning/ROADMAP.md` and `.planning/STATE.md`.

Extract:
- **local_phases**: list of phase directory names
- **local_quicks**: list of quick directory names

## Step 4: Identify what needs to change

**Branch-local phases** = local_phases entries whose directory name does NOT appear in target_phases.
**Branch-local quicks** = local_quicks entries whose directory name does NOT appear in target_quicks.
**Target-only phases** = target_phases entries whose directory name does NOT appear in local_phases.
These are phases the target has that the current branch doesn't know about yet — their ROADMAP
entries need to be absorbed locally so git doesn't conflict on them.

For each branch-local phase, extract its numeric prefix using pattern `/^(\d+(?:\.\d+)*)-/`.

**Conflicting phases** = branch-local phases whose numeric prefix matches any numeric prefix in target_phases.
**Conflicting quicks** = branch-local quicks whose numeric prefix matches any numeric prefix in target_quicks.

If there are no conflicting phases, no conflicting quicks, and no target-only phases to absorb:
Output: "Nothing to sync — no index conflicts with [target]."
Exit successfully.

## Step 5: Compute new indexes

For conflicting branch-local phases:
- Compute `local_max_non_conflicting` = highest integer phase number among non-conflicting branch-local phases (0 if none)
- Compute `next_available = max(target_max_phase, local_max_non_conflicting) + 1`
- Sort conflicting phases by current phase number ascending (numerically, not lexicographically)
- Assign new integers starting at `next_available`, incrementing by 1
- Non-conflicting branch-local phases are left at their current number
- Example: target_max = 18, local non-conflicting max = 19, conflicting local phases are 18 and 20
→ next_available = max(18, 19) + 1 = 20 ← would collide with existing 20, so bump again
→ actually: next_available = max(18, 19) + 1 = 20, but 20 is also a conflicting phase being renamed,
so assign in order: 18 → 20, 20 → 21
- In short: compute the full rename map first, then verify no assigned number collides with any
remaining local phase not in the rename map. If it does, increment further.

For conflicting branch-local quicks:
- Same logic: `next_available = max(target_max_quick, local_max_non_conflicting_quick) + 1`

Build a rename map: `{ old_name: new_name }` for both phases and quicks.

## Step 6: Present plan and confirm

Print the full plan before touching anything:

```
GSD Sync Plan
=============
Target branch : main
Current branch: feature/my-work
Max phase on main : 18
Max quick on main : 4

PHASES
Absorb from main (ROADMAP only, no dir copy):
Phase 18: feature-a ← will be inserted into local ROADMAP.md

Rename (directory + files inside):
phases/18-feature-b/ → phases/19-feature-b/
18-01-PLAN.md → 19-01-PLAN.md
18-01-SUMMARY.md → 19-01-SUMMARY.md
18-VERIFICATION.md → 19-VERIFICATION.md

QUICKS
Rename (directory + files inside):
quick/4-fix-something/ → quick/5-fix-something/
4-PLAN.md → 5-PLAN.md
4-SUMMARY.md → 5-SUMMARY.md

ROADMAP.md
Insert "### Phase 18: feature-a" section (absorbed from main)
Rename "### Phase 18: feature-b" → "### Phase 19: feature-b"

STATE.md
Update phase number references 18 → 19
```

If --dry-run: print plan and stop. Do not touch anything.

Otherwise: ask user to confirm before proceeding.

## Step 7: Absorb target-only ROADMAP entries

**Do this before renaming** so the inserted entries get the correct final numbers.

For each target-only phase (exists on target, not locally):
- Extract the full phase section from target_roadmap. A phase section runs from its
`### Phase N:` heading up to (but not including) the next `### Phase` heading.
- Insert the extracted section into local ROADMAP.md at the correct sorted position
(ordered by phase number, before any branch-local phases being renumbered).

This means the local branch already contains the target's new phase entries before the merge.
Git will see them as already present → no conflict on those lines.

## Step 8: Rename phase directories and files

For each phase in the rename map (old → new):

1. Rename files inside the directory first. List all files in `.planning/phases/<old_name>/`.
The old file prefix is the full numeric part of the directory name followed by a hyphen
(e.g. directory `18-slug` → prefix `18-`, directory `18.1-slug` → prefix `18.1-`).
The new file prefix is the new integer phase number followed by a hyphen (e.g. `19-`).
For each file whose name starts with the old file prefix:
- Compute new filename by replacing the old prefix with the new prefix
- Run: `git mv ".planning/phases/<old_name>/<old_file>" ".planning/phases/<old_name>/<new_file>"`

2. Rename the directory:
- Run: `git mv ".planning/phases/<old_name>" ".planning/phases/<new_name>"`

Use `git mv` for all renames so git tracks them as renames rather than delete+add.

## Step 9: Rename quick directories and files

For each quick in the rename map (old → new):

1. Rename files inside first. List files in `.planning/quick/<old_name>/`.
For each file whose name starts with the old quick number (e.g. `4-`):
- Replace old prefix with new (e.g. `4-` → `5-`)
- Run: `git mv ".planning/quick/<old_name>/<old_file>" ".planning/quick/<old_name>/<new_file>"`
Note: quick files may also be named `PLAN.md` / `SUMMARY.md` without a numeric prefix — leave those unchanged.

2. Rename the directory:
- Run: `git mv ".planning/quick/<old_name>" ".planning/quick/<new_name>"`

## Step 10: Update ROADMAP.md

For each phase rename, update all references in local ROADMAP.md:
- Phase heading: `### Phase 18:` → `### Phase 19:`
- Checkbox entries: `**Phase 18:**` → `**Phase 19:**`
- Dependency references: `Depends on: Phase 18` → `Depends on: Phase 19`
- Table rows: `| 18.` → `| 19.` (be precise to avoid collateral changes)

Write the updated file.

## Step 11: Update STATE.md

Read `.planning/STATE.md`. For each phase rename:
- Update `**Current Phase:** 18` → `**Current Phase:** 19` (if applicable)
- Update phase references in progress tables

Write the updated file.

## Step 12: Report

Print what was done:

```
GSD Sync Complete
=================
✓ Absorbed 1 ROADMAP entry from main (Phase 18: feature-a)
✓ Renamed 1 phase: 18-feature-b → 19-feature-b (3 files)
✓ Renamed 1 quick: 4-fix-something → 5-fix-something (2 files)
✓ Updated ROADMAP.md
✓ Updated STATE.md

Ready to merge into main. No .planning conflicts expected.
Suggested next step: open your PR or run `git merge <this-branch>` from main.
```

Do NOT commit. Do NOT push. Leave that to the user.

</process>

<success_criteria>
- No branch-local phase or quick index overlaps with target branch after sync
- Target branch's ROADMAP.md phase entries are absorbed into local ROADMAP.md
- All renamed directories have their internal files renamed to match (PLAN, SUMMARY, VERIFICATION, UAT, RESEARCH, CONTEXT)
- ROADMAP.md and STATE.md reflect the new numbers
- --dry-run shows the full plan with zero filesystem changes
- User confirmed before any changes were made (unless --dry-run)
- git mv used for all renames (preserves history)
</success_criteria>

<critical_rules>
- NEVER rename phases that exist on both branches — only branch-local ones
- NEVER copy phase directories from target — absorb ROADMAP entries only
- ALWAYS absorb target ROADMAP entries BEFORE renaming local phases (order matters for numbering)
- Use `git mv` for all renames, not plain `mv`
- Do NOT commit or push — leave staging and committing to the user
- Do NOT touch any files outside .planning/
- --dry-run must make ZERO filesystem changes
- If a branch-local phase has no numeric conflict with target, leave it at its current number
</critical_rules>