diff --git a/.planning/quick/1-issue-55-mgw-project-should-support-exte/1-VERIFICATION.md b/.planning/quick/1-issue-55-mgw-project-should-support-exte/1-VERIFICATION.md
new file mode 100644
index 0000000..0a9103d
--- /dev/null
+++ b/.planning/quick/1-issue-55-mgw-project-should-support-exte/1-VERIFICATION.md
@@ -0,0 +1,104 @@
+---
+phase: 1-issue-55-mgw-project-should-support-exte
+verified: 2026-02-27T08:00:00Z
+status: passed
+score: 7/7 must-haves verified
+re_verification: false
+---
+
+# Quick Task 1: MGW Project Extend Support — Verification Report
+
+**Task Goal:** Issue #55: mgw:project should support extending completed projects — when all milestones are complete, /mgw:project detects this and offers to extend the project with new milestones instead of blocking. Must preserve existing project.json data, append new milestones, set current_milestone, reuse existing board, continue phase numbering.
+
+**Verified:** 2026-02-27T08:00:00Z
+**Status:** PASSED
+**Re-verification:** No — initial verification
+
+---
+
+## Goal Achievement
+
+### Observable Truths
+
+| # | Truth | Status | Evidence |
+|---|-------|--------|----------|
+| 1 | verify_repo detects all-milestones-complete state | VERIFIED | `commands/project.md` lines 52-73: python3 snippet sets `ALL_COMPLETE=true` when `current > len(milestones) and len(milestones) > 0`; sets `EXTEND_MODE=true` |
+| 2 | mergeProjectState function exists and is exported in lib/state.cjs | VERIFIED | `lib/state.cjs` lines 110-129: function implemented; line 138: exported in `module.exports` |
+| 3 | Phase numbering continues from existing count (not reset to 1) | VERIFIED | `commands/project.md` lines 265-269: `if EXTEND_MODE=true: GLOBAL_PHASE_NUM=$EXISTING_PHASE_COUNT` |
+| 4 | GitHub Projects board is reused when it already exists | VERIFIED | `commands/project.md` lines 496-521: reads `project.project_board.number` from project.json, calls `gh project item-add` with existing board number |
+| 5 | write_project_json uses merge in extend mode (not overwrite) | VERIFIED | `commands/project.md` lines 645-670: calls `mergeProjectState` via Node when `EXTEND_MODE=true`; standard write path unchanged when false |
+| 6 | Non-regression: incomplete milestones still exits with "already initialized" | VERIFIED | `commands/project.md` lines 69-71: `else` branch of ALL_COMPLETE check prints "Project already initialized. Run /mgw:milestone to continue." and `exit 0` |
+| 7 | USER-GUIDE.md documents the extend workflow | VERIFIED | Three locations: line 341 (command reference), line 613 (workflow walkthrough), line 1334 (FAQ) — all substantive, not placeholders |
+
+**Score:** 7/7 truths verified
+
+---
+
+## Required Artifacts
+
+| Artifact | Expected | Status | Details |
+|----------|----------|--------|---------|
+| `commands/project.md` | Extend flow with all-milestones-complete detection, EXTEND_MODE propagation, phase continuity, board reuse, merge-based write, extended report | VERIFIED | EXTEND_MODE: 9 occurrences; mergeProjectState: 4; EXISTING_PHASE_COUNT: 4; ALL_COMPLETE/all_done: 4; "PROJECT EXTENDED": 1; "Reusing existing project board": 1; "Project already initialized": 1 |
+| `lib/state.cjs` | mergeProjectState function exported with 3-arg signature | VERIFIED | Function at lines 110-129; exported at line 138; `node` confirms all 7 exports present, arity=3 |
+| `docs/USER-GUIDE.md` | "Extending a Completed Project" section plus command reference and FAQ entries | VERIFIED | "Extending a Completed Project": 1 occurrence; "extend mode": 4 occurrences; "add more milestones after completing": 1 occurrence |
+
+---
+
+## Key Link Verification
+
+| From | To | Via | Status | Details |
+|------|----|-----|--------|---------|
+| `commands/project.md` | `lib/state.cjs` | `mergeProjectState` call | WIRED | Line 656: `const { mergeProjectState } = require('${REPO_ROOT}/lib/state.cjs')` with actual call at line 660 |
+| `commands/project.md verify_repo` | `commands/project.md gather_inputs` and downstream | `EXTEND_MODE=true` flag propagation | WIRED | EXTEND_MODE set in verify_repo (line 64), checked at gather_inputs (line 135), create_issues (line 265), create_project_board (line 496), write_project_json (line 647) |
+| `commands/project.md create_project_board` | `.mgw/project.json project.project_board` | existing board number check | WIRED | Lines 498-505: reads `p.get('project', {}).get('project_board', {})` and extracts `number`/`url`; only creates new board if `PROJECT_NUMBER` is empty |
+
+---
+
+## Commit Verification
+
+All three commits from SUMMARY.md confirmed present in git log:
+
+| Hash | Description |
+|------|-------------|
+| `5ac1df9` | feat(quick-1): add mergeProjectState to lib/state.cjs |
+| `2fce691` | feat(quick-1): add extend flow to commands/project.md |
+| `42a8f46` | docs(quick-1): document extend flow in USER-GUIDE.md |
+
+---
+
+## Anti-Patterns Found
+
+No anti-patterns found:
+- No TODO/FIXME/placeholder comments in modified files
+- No empty implementations — mergeProjectState has full logic (load, concat, Object.assign, set, write, return)
+- No stub returns — board-reuse path has real `gh project item-add` calls
+- Non-extend path is unchanged (verified by "Project already initialized" grep)
+
+One implementation note (not a blocker): In `mergeProjectState`, `Object.assign({}, newPhaseMap, existing.phase_map)` places `existing.phase_map` last so existing keys win over new ones. This correctly implements "new keys only, no overwrites of existing phase numbers" as specified in the plan.
+
+---
+
+## Human Verification Required
+
+### 1. End-to-end extend flow
+
+**Test:** On a real repo with a completed project (current_milestone > len(milestones)), run `/mgw:project`, describe new work, observe output.
+**Expected:** MGW prints "All N milestones complete. Entering extend mode.", asks for new milestone description, creates GitHub milestones and issues, appends to project.json without losing existing data, reuses the existing project board number.
+**Why human:** Requires a live GitHub repo with completed MGW state and an active Claude session running the command.
+
+### 2. Board reuse when project_board.number is absent
+
+**Test:** On a project where `.mgw/project.json` has no `project_board` key (e.g., board creation previously failed), run `/mgw:project` with all milestones complete.
+**Expected:** Falls through to create a new board (`EXTEND_MODE_BOARD=false` branch).
+**Why human:** Requires specific project.json state to exercise the fallback path.
+
+---
+
+## Gaps Summary
+
+No gaps. All 7 must-haves are verified against the actual codebase. The implementation matches the plan specification exactly — verified via file content inspection, grep counts, and Node.js module loading checks.
+
+---
+
+_Verified: 2026-02-27T08:00:00Z_
+_Verifier: Claude (gsd-verifier)_
diff --git a/commands/project.md b/commands/project.md
index ba5d156..0dae1ad 100644
--- a/commands/project.md
+++ b/commands/project.md
@@ -48,8 +48,28 @@ If no GitHub remote → error: "No GitHub remote found. MGW requires a GitHub re
```bash
if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then
- echo "Project already initialized. Run /mgw:milestone to continue."
- exit 0
+ # Check if all milestones are complete
+ ALL_COMPLETE=$(python3 -c "
+import json
+p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
+milestones = p.get('milestones', [])
+current = p.get('current_milestone', 1)
+# All complete when current_milestone exceeds array length
+# (milestone.md increments current_milestone after completing each)
+all_done = current > len(milestones) and len(milestones) > 0
+print('true' if all_done else 'false')
+")
+
+ if [ "$ALL_COMPLETE" = "true" ]; then
+ EXTEND_MODE=true
+ EXISTING_MILESTONE_COUNT=$(python3 -c "import json; print(len(json.load(open('${REPO_ROOT}/.mgw/project.json'))['milestones']))")
+ EXISTING_PHASE_COUNT=$(python3 -c "import json; print(max((int(k) for k in json.load(open('${REPO_ROOT}/.mgw/project.json')).get('phase_map',{}).keys()), default=0))")
+ echo "All ${EXISTING_MILESTONE_COUNT} milestones complete. Entering extend mode."
+ echo "Phase numbering will continue from phase ${EXISTING_PHASE_COUNT}."
+ else
+ echo "Project already initialized. Run /mgw:milestone to continue."
+ exit 0
+ fi
fi
```
@@ -106,6 +126,26 @@ fi
# Prefix: default v1
PREFIX="v1"
```
+
+**In extend mode, load existing metadata and ask for new milestones:**
+
+When `EXTEND_MODE=true`, skip the questions above and instead:
+
+```bash
+if [ "$EXTEND_MODE" = true ]; then
+ # Load existing project metadata — name, repo, stack, prefix are already known
+ PROJECT_NAME=$(python3 -c "import json; print(json.load(open('${REPO_ROOT}/.mgw/project.json'))['project']['name'])")
+ STACK=$(python3 -c "import json; print(json.load(open('${REPO_ROOT}/.mgw/project.json'))['project'].get('stack','unknown'))")
+ PREFIX=$(python3 -c "import json; print(json.load(open('${REPO_ROOT}/.mgw/project.json'))['project'].get('prefix','v1'))")
+ EXISTING_MILESTONE_NAMES=$(python3 -c "import json; p=json.load(open('${REPO_ROOT}/.mgw/project.json')); print(', '.join(m['name'] for m in p['milestones']))")
+
+ # Ask only for the new work — different question for extend mode
+ # Ask: "What new milestones should we add to ${PROJECT_NAME}?"
+ # Capture as EXTENSION_DESCRIPTION
+
+ DESCRIPTION="Extension of existing project. Existing milestones: ${EXISTING_MILESTONE_NAMES}. New work: ${EXTENSION_DESCRIPTION}"
+fi
+```
@@ -221,7 +261,12 @@ TOTAL_ISSUES_CREATED=0
FAILED_SLUGS=()
# Global phase counter across milestones
-GLOBAL_PHASE_NUM=0
+# In extend mode, continue numbering from last existing phase
+if [ "$EXTEND_MODE" = true ]; then
+ GLOBAL_PHASE_NUM=$EXISTING_PHASE_COUNT
+else
+ GLOBAL_PHASE_NUM=0
+fi
for MILESTONE_INDEX in $(seq 0 $((MILESTONE_COUNT - 1))); do
# Get this milestone's GitHub number from MILESTONE_MAP
@@ -448,25 +493,54 @@ fi
```bash
OWNER=$(echo "$REPO" | cut -d'/' -f1)
-# Create project board
-PROJECT_RESP=$(gh project create --owner "$OWNER" --title "${PROJECT_NAME} Roadmap" --format json 2>&1)
-PROJECT_NUMBER=$(echo "$PROJECT_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['number'])" 2>/dev/null || echo "")
-PROJECT_URL=$(echo "$PROJECT_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])" 2>/dev/null || echo "")
+if [ "$EXTEND_MODE" = true ]; then
+ # Reuse existing project board — load number and URL from project.json
+ EXISTING_BOARD=$(python3 -c "
+import json
+p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
+board = p.get('project', {}).get('project_board', {})
+print(json.dumps(board))
+")
+ PROJECT_NUMBER=$(echo "$EXISTING_BOARD" | python3 -c "import json,sys; print(json.load(sys.stdin).get('number',''))")
+ PROJECT_URL=$(echo "$EXISTING_BOARD" | python3 -c "import json,sys; print(json.load(sys.stdin).get('url',''))")
-if [ -n "$PROJECT_NUMBER" ]; then
- echo " Created project board: #${PROJECT_NUMBER} — ${PROJECT_URL}"
+ if [ -n "$PROJECT_NUMBER" ]; then
+ echo " Reusing existing project board: #${PROJECT_NUMBER} — ${PROJECT_URL}"
- # Add all issues to the board
- for RECORD in "${ISSUE_RECORDS[@]}"; do
- ISSUE_NUM=$(echo "$RECORD" | cut -d':' -f2)
- ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUM}"
- gh project item-add "$PROJECT_NUMBER" --owner "$OWNER" --url "$ISSUE_URL" 2>/dev/null || true
- done
- echo " Added ${TOTAL_ISSUES_CREATED} issues to project board"
-else
- echo " WARNING: Failed to create project board: ${PROJECT_RESP}"
- PROJECT_NUMBER=""
- PROJECT_URL=""
+ # Add only NEW issues to the existing board
+ for RECORD in "${ISSUE_RECORDS[@]}"; do
+ ISSUE_NUM=$(echo "$RECORD" | cut -d':' -f2)
+ ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUM}"
+ gh project item-add "$PROJECT_NUMBER" --owner "$OWNER" --url "$ISSUE_URL" 2>/dev/null || true
+ done
+ echo " Added ${TOTAL_ISSUES_CREATED} new issues to existing project board"
+ else
+ # Board not found — fall through to create a new one
+ EXTEND_MODE_BOARD=false
+ fi
+fi
+
+if [ "$EXTEND_MODE" != true ] || [ "$EXTEND_MODE_BOARD" = false ]; then
+ # Create a new project board (standard flow or extend fallback)
+ PROJECT_RESP=$(gh project create --owner "$OWNER" --title "${PROJECT_NAME} Roadmap" --format json 2>&1)
+ PROJECT_NUMBER=$(echo "$PROJECT_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['number'])" 2>/dev/null || echo "")
+ PROJECT_URL=$(echo "$PROJECT_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])" 2>/dev/null || echo "")
+
+ if [ -n "$PROJECT_NUMBER" ]; then
+ echo " Created project board: #${PROJECT_NUMBER} — ${PROJECT_URL}"
+
+ # Add all issues to the board
+ for RECORD in "${ISSUE_RECORDS[@]}"; do
+ ISSUE_NUM=$(echo "$RECORD" | cut -d':' -f2)
+ ISSUE_URL="https://github.com/${REPO}/issues/${ISSUE_NUM}"
+ gh project item-add "$PROJECT_NUMBER" --owner "$OWNER" --url "$ISSUE_URL" 2>/dev/null || true
+ done
+ echo " Added ${TOTAL_ISSUES_CREATED} issues to project board"
+ else
+ echo " WARNING: Failed to create project board: ${PROJECT_RESP}"
+ PROJECT_NUMBER=""
+ PROJECT_URL=""
+ fi
fi
```
@@ -567,11 +641,54 @@ a python3 dictionary and write with `json.dumps(indent=2)` at this step.
Note: use `GENERATED_TYPE` (read from `/tmp/mgw-template.json`) for the `template` field in project.json,
not a hardcoded template name.
+
+**In extend mode, use mergeProjectState instead of full write:**
+
+When `EXTEND_MODE=true`, do NOT write a full project.json. Instead, build only the new milestones
+and phase_map entries (with `template_milestone_index` offset by `EXISTING_MILESTONE_COUNT`), then call:
+
+```bash
+# Compute the current_milestone pointer for the first new milestone (1-indexed)
+NEW_CURRENT_MILESTONE=$((EXISTING_MILESTONE_COUNT + 1))
+
+# Call mergeProjectState via Node — appends without overwriting existing data
+node -e "
+const { mergeProjectState } = require('${REPO_ROOT}/lib/state.cjs');
+const newMilestones = JSON.parse(process.argv[1]);
+const newPhaseMap = JSON.parse(process.argv[2]);
+const newCurrentMilestone = parseInt(process.argv[3]);
+const merged = mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone);
+console.log('project.json updated: ' + merged.milestones.length + ' total milestones');
+" "$NEW_MILESTONES_JSON" "$NEW_PHASE_MAP_JSON" "$NEW_CURRENT_MILESTONE"
+```
+
+Where `NEW_MILESTONES_JSON` and `NEW_PHASE_MAP_JSON` are JSON-encoded strings built from only
+the newly created milestones/phases (matching the existing project.json schema). The
+`template_milestone_index` for each new milestone should be offset by `EXISTING_MILESTONE_COUNT`
+so indices remain globally unique.
+
+When `EXTEND_MODE` is false, the existing write logic (full project.json from scratch) is unchanged.
**Display post-init summary:**
+In extend mode, show the extended banner:
+
+```
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ MGW ► PROJECT EXTENDED — {PROJECT_NAME}
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Extended with {NEW_MILESTONE_COUNT} new milestones (total: {TOTAL_MILESTONES})
+Phase numbering: continued from {EXISTING_PHASE_COUNT} (new phases: {EXISTING_PHASE_COUNT+1}–{NEW_MAX_PHASE})
+Board: reused #{PROJECT_NUMBER}
+
+(remaining output follows the same format as project init for the new milestones/issues)
+```
+
+In standard (non-extend) mode, show the original init banner:
+
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MGW ► PROJECT INIT — {PROJECT_NAME}
@@ -629,4 +746,5 @@ Warnings:
- [ ] .mgw/project.json written with full project state
- [ ] Post-init summary displayed
- [ ] Command does NOT trigger execution (PROJ-05)
+- [ ] Extend mode: all milestones complete detected, new milestones appended, existing data preserved
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index f8ca1d0..54a1440 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -338,6 +338,8 @@ Scaffold an entire project from a description. Creates milestones, issues, depen
This is an interactive command. MGW asks "What are you building?" and generates project-specific milestones, phases, and issues based on your description. It does not ask you to pick a template type -- the AI infers the project structure from your description.
+If all milestones in the project are already complete, `/mgw:project` enters **extend mode**: it asks what new milestones to add, appends them to the existing project.json, reuses the GitHub Projects board, and continues phase numbering from the last phase. Existing data is fully preserved.
+
What gets created:
- GitHub milestones with descriptions
- Issues assigned to milestones with phase labels
@@ -608,6 +610,34 @@ Starting a brand new project from scratch:
/mgw:sync
```
+### Extending a Completed Project
+
+When all milestones are complete and you want to add more work:
+
+```
+# Run project again -- MGW detects all milestones are done
+/mgw:project
+# MGW shows: "All N milestones complete. Entering extend mode."
+# Asks: "What new milestones should we add?"
+# You describe the new work.
+
+# What happens:
+# - New milestones and issues are appended (existing ones preserved)
+# - Phase numbering continues from where it left off
+# - current_milestone is set to the first new milestone
+# - Existing project board is reused (new issues added to it)
+# - cross-refs.json is preserved and extended with new dependency entries
+
+# Then execute the new milestones
+/mgw:milestone
+```
+
+What is preserved during extension:
+- All completed milestone data and pipeline stages
+- The GitHub Projects v2 board (new issues added, old ones remain)
+- cross-refs.json entries for all existing links
+- project.json `project` metadata (name, description, repo, etc.)
+
### Existing Issues
Working with a repo that already has GitHub issues:
@@ -1301,6 +1331,18 @@ MGW uses `gsd-tools.cjs generate-slug` for consistent slug generation.
Not currently. MGW is designed for interactive use with Claude Code. CI integration is on the roadmap.
+### How do I add more milestones after completing all of them?
+
+Run `/mgw:project` again. When all milestones are complete, it automatically enters extend mode:
+
+```
+/mgw:project
+# "All milestones complete. Entering extend mode."
+# Describe the new work.
+```
+
+New milestones are appended. Existing data (completed milestones, board, cross-refs) is preserved.
+
### How do I completely reset MGW state?
```bash
diff --git a/lib/state.cjs b/lib/state.cjs
index 19c24ab..43d47f5 100644
--- a/lib/state.cjs
+++ b/lib/state.cjs
@@ -97,11 +97,43 @@ function loadActiveIssue(number) {
}
}
+/**
+ * Merge new milestones into existing project state.
+ * Appends milestones and phase_map entries, sets current_milestone.
+ * Preserves all existing data (completed milestones, project config, board).
+ * @param {Array} newMilestones - New milestone objects to append
+ * @param {object} newPhaseMap - New phase_map entries (keyed by phase number string)
+ * @param {number} newCurrentMilestone - 1-indexed milestone pointer for first new milestone
+ * @returns {object} The merged project state
+ * @throws {Error} If no existing project state found
+ */
+function mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone) {
+ const existing = loadProjectState();
+ if (!existing) {
+ throw new Error('No existing project state found. Cannot merge without a project.json.');
+ }
+
+ // Append new milestones to existing milestones array
+ existing.milestones = (existing.milestones || []).concat(newMilestones);
+
+ // Merge new phase_map entries — new keys only, no overwrites of existing phase numbers
+ existing.phase_map = Object.assign({}, newPhaseMap, existing.phase_map);
+
+ // Set current_milestone to point at the first new milestone (1-indexed)
+ existing.current_milestone = newCurrentMilestone;
+
+ // Write the merged state back to disk
+ writeProjectState(existing);
+
+ return existing;
+}
+
module.exports = {
getMgwDir,
getActiveDir,
getCompletedDir,
loadProjectState,
writeProjectState,
- loadActiveIssue
+ loadActiveIssue,
+ mergeProjectState
};