Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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)_
158 changes: 138 additions & 20 deletions commands/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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
```
</step>

<step name="generate_template">
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -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.
</step>

<step name="report">
**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}
Expand Down Expand Up @@ -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
</success_criteria>
42 changes: 42 additions & 0 deletions docs/USER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Loading