diff --git a/PULL_REQUEST_DRAFT.md b/PULL_REQUEST_DRAFT.md new file mode 100644 index 0000000000..cccdae43b1 --- /dev/null +++ b/PULL_REQUEST_DRAFT.md @@ -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=]` + +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 :...` — 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=]` | 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. diff --git a/commands/gsd/sync.md b/commands/gsd/sync.md new file mode 100644 index 0000000000..3b5c219474 --- /dev/null +++ b/commands/gsd/sync.md @@ -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=] +allowed-tools: + - Read + - Write + - Edit + - Bash +--- + + +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. + + + +Arguments: $ARGUMENTS +- --dry-run: show all planned changes but make none +- --target=: base branch to sync against (default: main) + + + + +## Step 1: Parse arguments and safety checks + +Parse --dry-run and --target= 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 -- .planning/phases/ +git ls-tree --name-only -- .planning/quick/ +git show :.planning/ROADMAP.md +git show :.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//`. + 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//" ".planning/phases//"` + +2. Rename the directory: + - Run: `git mv ".planning/phases/" ".planning/phases/"` + +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//`. + 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//" ".planning/quick//"` + 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/" ".planning/quick/"` + +## 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 ` from main. +``` + +Do NOT commit. Do NOT push. Leave that to the user. + + + + +- 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) + + + +- 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 +