diff --git a/.claude/commands/mgw/assign.md b/.claude/commands/mgw/assign.md new file mode 100644 index 0000000..6d35472 --- /dev/null +++ b/.claude/commands/mgw/assign.md @@ -0,0 +1,333 @@ +--- +name: mgw:assign +description: Claim an issue for a user — assigns via GitHub and updates board + state +argument-hint: " [username]" +allowed-tools: + - Bash + - Read + - Write + - Edit +--- + + +Claim a GitHub issue for yourself or another team member. Three operations in one call: + +1. **GitHub assignment** — `gh issue edit --add-assignee` to set the issue assignee +2. **State update** — write assignee to `.mgw/active/.json` (creates minimal entry + if not yet triaged) +3. **Board confirmation** — if a board is configured, emit the board URL so the team + can verify the assignment is reflected on the board item + +Usage: +- `mgw:assign 42` — assign issue #42 to yourself (@me) +- `mgw:assign 42 alice` — assign issue #42 to @alice + +GitHub Projects v2 automatically syncs issue assignees to board items, so no direct +GraphQL mutation is needed for the board Assignees field. + +Follows delegation boundary: only state and GitHub operations — no application code reads. + + + +@~/.claude/commands/mgw/workflows/state.md +@~/.claude/commands/mgw/workflows/github.md +@~/.claude/commands/mgw/workflows/board-sync.md + + + +Arguments: $ARGUMENTS + +State: .mgw/active/ (issue state — created if missing) +Board: .mgw/project.json (if configured — read for board URL only) + + + + + +**Parse $ARGUMENTS into issue number and optional username:** + +```bash +ISSUE_NUMBER=$(echo "$ARGUMENTS" | awk '{print $1}') +USERNAME=$(echo "$ARGUMENTS" | awk '{print $2}') + +# Validate issue number +if [ -z "$ISSUE_NUMBER" ]; then + echo "Usage: /mgw:assign [username]" + echo "" + echo " mgw:assign 42 — assign #42 to yourself" + echo " mgw:assign 42 alice — assign #42 to @alice" + exit 1 +fi + +if ! echo "$ISSUE_NUMBER" | grep -qE '^[0-9]+$'; then + echo "ERROR: Issue number must be numeric. Got: '${ISSUE_NUMBER}'" + exit 1 +fi +``` + + + +**Initialize .mgw/ and load existing state (from state.md):** + +```bash +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -z "$REPO_ROOT" ]; then + echo "ERROR: Not a git repository." + exit 1 +fi + +MGW_DIR="${REPO_ROOT}/.mgw" + +# Ensure directory structure +mkdir -p "${MGW_DIR}/active" "${MGW_DIR}/completed" + +# Ensure gitignore entries +for ENTRY in ".mgw/" ".worktrees/"; do + if ! grep -qF "${ENTRY}" "${REPO_ROOT}/.gitignore" 2>/dev/null; then + echo "${ENTRY}" >> "${REPO_ROOT}/.gitignore" + fi +done + +# Initialize cross-refs if missing +if [ ! -f "${MGW_DIR}/cross-refs.json" ]; then + echo '{"links":[]}' > "${MGW_DIR}/cross-refs.json" +fi + +# Find state file for this issue +STATE_FILE=$(ls "${MGW_DIR}/active/${ISSUE_NUMBER}-"*.json 2>/dev/null | head -1) +STATE_EXISTS=$( [ -n "$STATE_FILE" ] && echo "true" || echo "false" ) +``` + + + +**Resolve the assignee username:** + +```bash +# If no username provided, use the authenticated user +if [ -z "$USERNAME" ]; then + RESOLVED_USER=$(gh api user -q .login 2>/dev/null) + if [ -z "$RESOLVED_USER" ]; then + echo "ERROR: Cannot resolve current GitHub user. Check your gh auth status." + exit 1 + fi +else + RESOLVED_USER="$USERNAME" + # Validate user exists on GitHub + USER_EXISTS=$(gh api "users/${RESOLVED_USER}" -q .login 2>/dev/null) + if [ -z "$USER_EXISTS" ]; then + echo "ERROR: GitHub user '${RESOLVED_USER}' not found." + exit 1 + fi +fi + +echo "MGW: Assigning #${ISSUE_NUMBER} to @${RESOLVED_USER}..." +``` + + + +**Build the Co-Authored-By tag for the assignee:** + +GitHub assigns every account a noreply email: `{id}+{login}@users.noreply.github.com`. +This gets stored in the active state file so GSD commits for this issue use the right tag. +Falls back to the project-level default in `project.json` if resolution fails. + +```bash +# Fetch assignee's GitHub ID and display name +ASSIGNEE_DATA=$(gh api "users/${RESOLVED_USER}" --jq '{id: .id, name: .name}' 2>/dev/null) +ASSIGNEE_ID=$(echo "$ASSIGNEE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" 2>/dev/null) +ASSIGNEE_NAME=$(echo "$ASSIGNEE_DATA" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['name'] or d.get('login',''))" 2>/dev/null) + +if [ -n "$ASSIGNEE_ID" ]; then + COAUTHOR_TAG="${ASSIGNEE_NAME} <${ASSIGNEE_ID}+${RESOLVED_USER}@users.noreply.github.com>" +else + # Fall back to project-level default + COAUTHOR_TAG=$(python3 -c " +import json +try: + p = json.load(open('${MGW_DIR}/project.json')) + print(p.get('project', {}).get('coauthor', '')) +except: + print('') +" 2>/dev/null || echo "") +fi + +echo "MGW: Co-author tag: ${COAUTHOR_TAG}" +``` + + + +**Fetch issue metadata from GitHub:** + +```bash +ISSUE_DATA=$(gh issue view "$ISSUE_NUMBER" --json number,title,url,labels,assignees,state 2>/dev/null) +if [ -z "$ISSUE_DATA" ]; then + echo "ERROR: Issue #${ISSUE_NUMBER} not found in this repo." + exit 1 +fi + +ISSUE_TITLE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])" 2>/dev/null) +ISSUE_URL=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])" 2>/dev/null) +ISSUE_STATE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['state'])" 2>/dev/null) + +# Check if user is already assigned (idempotent) +ALREADY_ASSIGNED=$(echo "$ISSUE_DATA" | python3 -c " +import json,sys +d = json.load(sys.stdin) +assignees = [a['login'] for a in d.get('assignees', [])] +print('true' if '${RESOLVED_USER}' in assignees else 'false') +" 2>/dev/null) +``` + + + +**Assign the issue on GitHub:** + +```bash +if [ "$ALREADY_ASSIGNED" = "true" ]; then + echo "MGW: @${RESOLVED_USER} is already assigned to #${ISSUE_NUMBER} — confirming state." +else + if ! gh issue edit "$ISSUE_NUMBER" --add-assignee "$RESOLVED_USER" 2>/dev/null; then + echo "ERROR: Failed to assign @${RESOLVED_USER} to #${ISSUE_NUMBER}." + echo " Check that the user has access to this repo." + exit 1 + fi + echo "MGW: Assigned @${RESOLVED_USER} to #${ISSUE_NUMBER}." +fi +``` + + + +**Write assignee to .mgw/active/ state (create minimal entry if needed):** + +```bash +TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null \ + || date -u +"%Y-%m-%dT%H:%M:%S.000Z") + +if [ "$STATE_EXISTS" = "true" ]; then + # Update existing state file: set issue.assignee and coauthor fields + python3 -c " +import json +with open('${STATE_FILE}') as f: + state = json.load(f) +state['issue']['assignee'] = '${RESOLVED_USER}' +state['coauthor'] = '${COAUTHOR_TAG}' +state['updated_at'] = '${TIMESTAMP}' +with open('${STATE_FILE}', 'w') as f: + json.dump(state, f, indent=2) +print('updated') +" 2>/dev/null + +else + # No state file — generate slug and create minimal entry + SLUG=\$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs generate-slug "${ISSUE_TITLE}" --raw 2>/dev/null | cut -c1-40 \ + || echo "issue-${ISSUE_NUMBER}") + + NEW_STATE_FILE="${MGW_DIR}/active/${ISSUE_NUMBER}-${SLUG}.json" + + python3 -c " +import json +state = { + 'issue': { + 'number': ${ISSUE_NUMBER}, + 'title': '${ISSUE_TITLE}', + 'url': '${ISSUE_URL}', + 'labels': [], + 'assignee': '${RESOLVED_USER}' + }, + 'coauthor': '${COAUTHOR_TAG}', + 'triage': { + 'scope': { 'size': 'unknown', 'file_count': 0, 'files': [], 'systems': [] }, + 'validity': 'pending', + 'security_risk': 'unknown', + 'security_notes': '', + 'conflicts': [], + 'last_comment_count': 0, + 'last_comment_at': None, + 'gate_result': { 'status': 'pending', 'blockers': [], 'warnings': [], 'missing_fields': [] } + }, + 'gsd_route': None, + 'gsd_artifacts': { 'type': None, 'path': None }, + 'pipeline_stage': 'new', + 'comments_posted': [], + 'linked_pr': None, + 'linked_issues': [], + 'linked_branches': [], + 'created_at': '${TIMESTAMP}', + 'updated_at': '${TIMESTAMP}' +} +with open('${NEW_STATE_FILE}', 'w') as f: + json.dump(state, f, indent=2) +print('created') +" 2>/dev/null + + STATE_FILE="$NEW_STATE_FILE" + echo "MGW: Created minimal state entry at ${STATE_FILE}" +fi +``` + + + +**Check if board is configured and emit board URL:** + +GitHub Projects v2 automatically syncs issue assignees to board items. No direct +GraphQL mutation is needed — the board will reflect the new assignee when refreshed. + +```bash +BOARD_URL=$(python3 -c " +import json, sys, os +try: + p = json.load(open('${MGW_DIR}/project.json')) + board = p.get('project', {}).get('project_board', {}) + print(board.get('url', '')) +except: + print('') +" 2>/dev/null || echo "") + +BOARD_ITEM_ID=$(python3 -c " +import json, sys +try: + p = json.load(open('${MGW_DIR}/project.json')) + for m in p.get('milestones', []): + for i in m.get('issues', []): + if i.get('github_number') == ${ISSUE_NUMBER}: + print(i.get('board_item_id', '')) + sys.exit(0) + print('') +except: + print('') +" 2>/dev/null || echo "") + +BOARD_CONFIGURED=$( [ -n "$BOARD_URL" ] && echo "true" || echo "false" ) +``` + + + +**Emit assignment confirmation:** + +```bash +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " MGW ► ISSUE ASSIGNED" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo " Issue : #${ISSUE_NUMBER} — ${ISSUE_TITLE}" +echo " URL : ${ISSUE_URL}" +echo " Assignee: @${RESOLVED_USER}" +echo " State : ${ISSUE_STATE}" +if [ "$BOARD_CONFIGURED" = "true" ]; then + echo " Board : ${BOARD_URL}" + if [ -n "$BOARD_ITEM_ID" ]; then + echo " (board item updated automatically by GitHub)" + else + echo " (issue not yet added to board — run /mgw:board show)" + fi +fi +echo "" +if [ "$ALREADY_ASSIGNED" = "true" ]; then + echo " Note: @${RESOLVED_USER} was already the assignee — state confirmed." +fi +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +``` + + + diff --git a/.claude/commands/mgw/issue.md b/.claude/commands/mgw/issue.md index 91fab81..5bf5f9c 100644 --- a/.claude/commands/mgw/issue.md +++ b/.claude/commands/mgw/issue.md @@ -191,8 +191,14 @@ Return a structured report: - Active overlaps: [list or 'none'] ### Recommended GSD Route -- Route: gsd:quick | gsd:quick --full | gsd:new-milestone +- Route: gsd:quick | gsd:quick --full | gsd:new-milestone | gsd:diagnose-issues - Reasoning: [why this route] + +Route selection guide: +- gsd:quick -- Small, well-defined task (1-3 files, clear fix) +- gsd:quick --full -- Medium task needing plan verification (3-8 files, some complexity) +- gsd:new-milestone -- Large feature or multi-system change (9+ files or new system) +- gsd:diagnose-issues -- Bug report where root cause is unclear; symptoms are known but the fix is not obvious; needs investigation before planning ", subagent_type="general-purpose", diff --git a/.claude/commands/mgw/link.md b/.claude/commands/mgw/link.md index 040400f..eb81d74 100644 --- a/.claude/commands/mgw/link.md +++ b/.claude/commands/mgw/link.md @@ -19,6 +19,8 @@ Reference formats: - Issue: 42 or #42 or issue:42 - PR: pr:15 or pr:#15 - Branch: branch:fix/auth-42 +- GitHub Milestone: milestone:N +- GSD Milestone: gsd-milestone:name @@ -39,6 +41,8 @@ Normalize reference formats: - Bare number or #N → "issue:N" - pr:N or pr:#N → "pr:N" - branch:name → "branch:name" +- milestone:N → "milestone:N" (GitHub milestone by number) +- gsd-milestone:name → "gsd-milestone:name" (GSD milestone by id/name) If fewer than 2 refs provided: ``` @@ -68,6 +72,7 @@ Determine link type: - issue + pr → "implements" - issue + branch → "tracks" - pr + branch → "tracks" +- milestone + gsd-milestone → "maps-to" (maps GitHub milestone to GSD milestone) Check for duplicate (same a+b pair exists). If duplicate, report and skip. diff --git a/.claude/commands/mgw/milestone.md b/.claude/commands/mgw/milestone.md index 1a06241..7e8195b 100644 --- a/.claude/commands/mgw/milestone.md +++ b/.claude/commands/mgw/milestone.md @@ -67,7 +67,17 @@ if [ -z "$MILESTONE_NUM" ]; then echo "No project initialized. Run /mgw:project first." exit 1 fi - MILESTONE_NUM=$(python3 -c "import json; print(json.load(open('${MGW_DIR}/project.json'))['current_milestone'])") + # Resolve active milestone index (0-based) and convert to 1-indexed milestone number + ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +console.log(resolveActiveMilestoneIndex(state)); +") + if [ "$ACTIVE_IDX" -lt 0 ]; then + echo "No active milestone set. Run /mgw:project to initialize or set active_gsd_milestone." + exit 1 + fi + MILESTONE_NUM=$((ACTIVE_IDX + 1)) fi ``` @@ -250,6 +260,72 @@ fi ``` + +**Post milestone-start announcement to GitHub Discussions (or first-issue comment fallback):** + +Runs once before the execute loop. Skipped if --dry-run is set. Failure is non-blocking — a warning is logged and execution continues. + +```bash +if [ "$DRY_RUN" = true ]; then + echo "MGW: Skipping milestone-start announcement (dry-run mode)" +else + # Gather board URL from project.json if present (non-blocking) + BOARD_URL=$(echo "$PROJECT_JSON" | python3 -c " +import json,sys +p = json.load(sys.stdin) +m = p['milestones'][${MILESTONE_NUM} - 1] +print(m.get('board_url', '')) +" 2>/dev/null || echo "") + + # Build issues JSON array with assignee and gsd_route per issue + ISSUES_PAYLOAD=$(echo "$ISSUES_JSON" | python3 -c " +import json,sys +issues = json.load(sys.stdin) +result = [] +for i in issues: + result.append({ + 'number': i.get('github_number', 0), + 'title': i.get('title', '')[:60], + 'assignee': i.get('assignee') or None, + 'gsdRoute': i.get('gsd_route', 'plan-phase') + }) +print(json.dumps(result)) +" 2>/dev/null || echo "[]") + + # Get first issue number for fallback comment (non-blocking) + FIRST_ISSUE_NUM=$(echo "$ISSUES_JSON" | python3 -c " +import json,sys +issues = json.load(sys.stdin) +print(issues[0]['github_number'] if issues else '') +" 2>/dev/null || echo "") + + REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "") + + REPO="$REPO" \ + MILESTONE_NAME="$MILESTONE_NAME" \ + BOARD_URL="$BOARD_URL" \ + ISSUES_PAYLOAD="$ISSUES_PAYLOAD" \ + FIRST_ISSUE_NUM="$FIRST_ISSUE_NUM" \ + node -e " +const { postMilestoneStartAnnouncement } = require('./lib/index.cjs'); +const result = postMilestoneStartAnnouncement({ + repo: process.env.REPO, + milestoneName: process.env.MILESTONE_NAME, + boardUrl: process.env.BOARD_URL || undefined, + issues: JSON.parse(process.env.ISSUES_PAYLOAD || '[]'), + firstIssueNumber: process.env.FIRST_ISSUE_NUM ? parseInt(process.env.FIRST_ISSUE_NUM) : undefined +}); +if (result.posted) { + const detail = result.url ? ': ' + result.url : ''; + console.log('MGW: Milestone-start announcement posted via ' + result.method + detail); +} else { + console.log('MGW: Milestone-start announcement skipped (Discussions unavailable, no fallback)'); +} +" 2>/dev/null || echo "MGW: Announcement step failed (non-blocking) — continuing" +fi +``` + + **If --dry-run flag: display execution plan and exit:** @@ -316,17 +392,15 @@ if [ "$IN_PROGRESS_COUNT" -gt 0 ]; then fi # Reset pipeline_stage to 'new' (will be re-run from scratch) - python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -milestone = project['milestones'][project['current_milestone'] - 1] -for issue in milestone['issues']: - if issue['github_number'] == ${ISSUE_NUM}: - issue['pipeline_stage'] = 'new' - break -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) + node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +if (idx < 0) { console.error('No active milestone'); process.exit(1); } +const milestone = state.milestones[idx]; +const issue = (milestone.issues || []).find(i => i.github_number === ${ISSUE_NUM}); +if (issue) { issue.pipeline_stage = 'new'; } +writeProjectState(state); " done fi @@ -570,17 +644,15 @@ COMMENTEOF # Update project.json checkpoint (MLST-05) STAGE=$([ -n "$PR_NUMBER" ] && echo "done" || echo "failed") - python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -milestone = project['milestones'][project['current_milestone'] - 1] -for issue in milestone['issues']: - if issue['github_number'] == ${ISSUE_NUMBER}: - issue['pipeline_stage'] = '${STAGE}' - break -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) + node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +if (idx < 0) { console.error('No active milestone'); process.exit(1); } +const milestone = state.milestones[idx]; +const issue = (milestone.issues || []).find(i => i.github_number === ${ISSUE_NUMBER}); +if (issue) { issue.pipeline_stage = '${STAGE}'; } +writeProjectState(state); " ISSUES_RUN=$((ISSUES_RUN + 1)) @@ -668,19 +740,140 @@ gh release create "$RELEASE_TAG" --draft \ --notes "$RELEASE_BODY" 2>/dev/null ``` -3. Advance current_milestone in project.json: +3. Finalize GSD milestone state (archive phases, clean up): +```bash +# Only run if .planning/phases exists (GSD was used for this milestone) +if [ -d ".planning/phases" ]; then + EXECUTOR_MODEL=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs resolve-model gsd-executor --raw) + Task( + prompt=" + +- ./CLAUDE.md (Project instructions -- if exists, follow all guidelines) +- .planning/ROADMAP.md (Current roadmap to archive) +- .planning/REQUIREMENTS.md (Requirements to archive) + + +Complete the GSD milestone. Follow the complete-milestone workflow: +@~/.claude/get-shit-done/workflows/complete-milestone.md + +This archives the milestone's ROADMAP and REQUIREMENTS to .planning/milestones/, +cleans up ROADMAP.md for the next milestone, and tags the release in git. + +Milestone: ${MILESTONE_NAME} +", + subagent_type="gsd-executor", + model="${EXECUTOR_MODEL}", + description="Complete GSD milestone: ${MILESTONE_NAME}" + ) +fi +``` + +4. Advance active milestone pointer in project.json: ```bash -python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -project['current_milestone'] += 1 -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) +node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const currentIdx = resolveActiveMilestoneIndex(state); +const nextMilestone = (state.milestones || [])[currentIdx + 1]; +if (nextMilestone) { + // New schema: point active_gsd_milestone at the next milestone's gsd_milestone_id + state.active_gsd_milestone = nextMilestone.gsd_milestone_id || null; + // Backward compat: if next milestone has no gsd_milestone_id, fall back to legacy integer + if (!state.active_gsd_milestone) { + state.current_milestone = currentIdx + 2; // next 1-indexed + } +} else { + // All milestones complete — clear the active pointer + state.active_gsd_milestone = null; + state.current_milestone = currentIdx + 2; // past end, signals completion +} +writeProjectState(state); " ``` -4. Display completion banner: +5. Milestone mapping verification: + +After advancing to the next milestone, check its GSD linkage: + +```bash +NEXT_MILESTONE_CHECK=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +const activeIdx = resolveActiveMilestoneIndex(state); + +if (activeIdx < 0 || activeIdx >= state.milestones.length) { + console.log('none'); + process.exit(0); +} + +const nextMilestone = state.milestones[activeIdx]; +if (!nextMilestone) { + console.log('none'); + process.exit(0); +} + +const gsdId = nextMilestone.gsd_milestone_id; +const name = nextMilestone.name; + +if (!gsdId) { + console.log('unlinked:' + name); +} else { + console.log('linked:' + name + ':' + gsdId); +} +") + +case "$NEXT_MILESTONE_CHECK" in + none) + echo "All milestones complete — project is done!" + ;; + unlinked:*) + NEXT_NAME=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f2-) + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Next milestone '${NEXT_NAME}' has no GSD milestone linked." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Before running /mgw:milestone for the next milestone:" + echo " 1) Run /gsd:new-milestone to create GSD state for '${NEXT_NAME}'" + echo " 2) Run /mgw:project extend to link the new GSD milestone" + echo "" + ;; + linked:*) + NEXT_NAME=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f2) + GSD_ID=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f3) + # Verify ROADMAP.md matches expected GSD milestone + ROADMAP_CHECK=$(python3 -c " +import os, sys +if not os.path.exists('.planning/ROADMAP.md'): + print('no_roadmap') + sys.exit() +with open('.planning/ROADMAP.md') as f: + content = f.read() +if '${GSD_ID}' in content: + print('match') +else: + print('mismatch') +" 2>/dev/null || echo "no_roadmap") + + case "$ROADMAP_CHECK" in + match) + echo "Next milestone '${NEXT_NAME}' (GSD: ${GSD_ID}) — ROADMAP.md is ready." + ;; + mismatch) + echo "Next milestone '${NEXT_NAME}' links to GSD milestone '${GSD_ID}'" + echo " but .planning/ROADMAP.md does not contain that milestone ID." + echo " Run /gsd:new-milestone to update ROADMAP.md before proceeding." + ;; + no_roadmap) + echo "NOTE: Next milestone '${NEXT_NAME}' (GSD: ${GSD_ID}) linked." + echo " No .planning/ROADMAP.md found — run /gsd:new-milestone when ready." + ;; + esac + ;; +esac +``` + +6. Display completion banner: ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ MGW ► MILESTONE ${MILESTONE_NUM} COMPLETE ✓ @@ -709,7 +902,7 @@ Draft release created: ${RELEASE_TAG} ─────────────────────────────────────────────────────────────── ``` -5. Check if next milestone exists and offer auto-advance (only if no failures in current). +7. Check if next milestone exists and offer auto-advance (only if no failures in current). **If some issues failed:** @@ -732,7 +925,7 @@ Milestone NOT closed. Resolve failures and re-run: /mgw:milestone ${MILESTONE_NUM} ``` -6. Post final results table as GitHub comment on the first issue in the milestone: +8. Post final results table as GitHub comment on the first issue in the milestone: ```bash gh issue comment ${FIRST_ISSUE_NUMBER} --body "$FINAL_RESULTS_COMMENT" ``` diff --git a/.claude/commands/mgw/next.md b/.claude/commands/mgw/next.md index 2b3d76d..6cf9fe4 100644 --- a/.claude/commands/mgw/next.md +++ b/.claude/commands/mgw/next.md @@ -38,14 +38,20 @@ if [ ! -f "${MGW_DIR}/project.json" ]; then fi PROJECT_JSON=$(cat "${MGW_DIR}/project.json") -CURRENT_MILESTONE=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['current_milestone'])") + +# Resolve active milestone index using state resolution (supports both schema versions) +ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +console.log(resolveActiveMilestoneIndex(state)); +") # Get milestone data MILESTONE_DATA=$(echo "$PROJECT_JSON" | python3 -c " import json,sys p = json.load(sys.stdin) -idx = p['current_milestone'] - 1 -if idx >= len(p['milestones']): +idx = ${ACTIVE_IDX} +if idx < 0 or idx >= len(p['milestones']): print(json.dumps({'error': 'No more milestones'})) sys.exit(0) m = p['milestones'][idx] diff --git a/.claude/commands/mgw/pr.md b/.claude/commands/mgw/pr.md index 1482693..82607eb 100644 --- a/.claude/commands/mgw/pr.md +++ b/.claude/commands/mgw/pr.md @@ -61,6 +61,10 @@ SUMMARY_DATA=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs summary-extract "$ # Also read raw artifacts for full context SUMMARY_RAW=$(cat ${gsd_artifacts_path}/*SUMMARY* 2>/dev/null) VERIFICATION=$(cat ${gsd_artifacts_path}/*VERIFICATION* 2>/dev/null) +# Read PLAN.md content for Phase Context section (may be long — pass as-is, agent wraps in details tag) +PLAN_CONTENT=$(cat ${gsd_artifacts_path}/*PLAN* 2>/dev/null) +PLAN_PATH=$(ls ${gsd_artifacts_path}/*PLAN* 2>/dev/null | head -1 || echo "") +SUMMARY_PATH=$(ls ${gsd_artifacts_path}/*SUMMARY* 2>/dev/null | head -1 || echo "") # Progress table for details section PROGRESS_TABLE=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs progress table --raw 2>/dev/null || echo "") @@ -148,6 +152,12 @@ ${SUMMARY_DATA} ${SUMMARY_RAW} + +${PLAN_CONTENT} +Plan path: ${PLAN_PATH} +Summary path: ${SUMMARY_PATH} + + ${VERIFICATION} @@ -176,6 +186,14 @@ SECTION 1 — PR body: ${if_linked: 'Closes #${ISSUE_NUMBER}'} +## Phase Context +- **Issue:** #${ISSUE_NUMBER} — ${issue_title} (omit if standalone) +- **Phase:** ${PHASE_INFO} (omit if no phase mapping) +- **GSD Route:** ${gsd_route} (omit if unknown) +- **PLAN.md:** ${path_to_plan_file} (omit if not found) +- **SUMMARY.md:** ${path_to_summary_file} (omit if not found) +(Omit this section entirely if issue is standalone and no phase context exists) + ${if MILESTONE_TITLE non-empty: ## Milestone Context - **Milestone:** ${MILESTONE_TITLE} @@ -190,6 +208,21 @@ ${if MILESTONE_TITLE non-empty: ## Cross-References ${cross_ref_list_or_omit_if_none} +${if PLAN_CONTENT non-empty: +## Plan +
+Expand to see the execution plan + +${PLAN_CONTENT} + +
+} + +${if VERIFICATION non-empty: +## Verification +${VERIFICATION} +} + ${if PROGRESS_TABLE non-empty:
GSD Progress diff --git a/.claude/commands/mgw/project.md b/.claude/commands/mgw/project.md index 2939336..5302ad4 100644 --- a/.claude/commands/mgw/project.md +++ b/.claude/commands/mgw/project.md @@ -17,9 +17,10 @@ issues scaffolded from AI-generated project-specific content, dependencies label state persisted. The developer never leaves Claude Code and never does project management manually. -MGW does NOT write to .planning/ — that directory is owned by GSD. If a project needs -a ROADMAP.md or other GSD files, run the appropriate GSD command (e.g., /gsd:new-milestone) -after project initialization. +MGW does NOT write to .planning/ directly — that directory is owned by GSD. For Fresh +projects, MGW spawns a gsd:new-project Task agent (spawn_gsd_new_project step) which creates +.planning/PROJECT.md and .planning/ROADMAP.md as part of the vision cycle. For non-Fresh +projects with existing GSD state, .planning/ is already populated before this command runs. This command creates structure only. It does NOT trigger execution. Run /mgw:milestone to begin executing the first milestone. @@ -44,15 +45,6 @@ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) If not a git repo → error: "Not a git repository. Run from a repo root." If no GitHub remote → error: "No GitHub remote found. MGW requires a GitHub repo." -**Check for existing project initialization:** - -```bash -if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then - echo "Project already initialized. Run /mgw:milestone to continue." - exit 0 -fi -``` - **Initialize .mgw/ state (from state.md validate_and_load):** ```bash @@ -71,9 +63,865 @@ fi ``` + +**Detect existing project state from five signal sources:** + +Check five signals to determine what already exists for this project: + +```bash +# Signal checks +P=false # .planning/PROJECT.md exists +R=false # .planning/ROADMAP.md exists +S=false # .planning/STATE.md exists +M=false # .mgw/project.json exists +G=0 # GitHub milestone count + +[ -f "${REPO_ROOT}/.planning/PROJECT.md" ] && P=true +[ -f "${REPO_ROOT}/.planning/ROADMAP.md" ] && R=true +[ -f "${REPO_ROOT}/.planning/STATE.md" ] && S=true +[ -f "${REPO_ROOT}/.mgw/project.json" ] && M=true + +G=$(gh api "repos/${REPO}/milestones" --jq 'length' 2>/dev/null || echo 0) +``` + +**Classify into STATE_CLASS:** + +| State | P | R | S | M | G | Meaning | +|---|---|---|---|---|---|---| +| Fresh | false | false | false | false | 0 | Clean slate — no GSD, no MGW | +| GSD-Only | true | false | false | false | 0 | PROJECT.md present but no roadmap yet | +| GSD-Mid-Exec | true | true | true | false | 0 | GSD in progress, MGW not yet linked | +| Aligned | true | — | — | true | >0 | Both MGW + GitHub consistent with each other | +| Diverged | — | — | — | true | >0 | MGW + GitHub present but inconsistent | +| Extend | true | — | — | true | >0 | All milestones in project.json are done | + +```bash +# Classification logic +STATE_CLASS="Fresh" +EXTEND_MODE=false + +if [ "$M" = "true" ] && [ "$G" -gt 0 ]; then + # Check if all milestones are complete (Extend detection) + 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 + STATE_CLASS="Extend" + 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))") + else + # M=true, G>0, not all done — check consistency (Aligned vs Diverged) + GH_MILESTONE_COUNT=$G + LOCAL_MILESTONE_COUNT=$(python3 -c "import json; print(len(json.load(open('${REPO_ROOT}/.mgw/project.json')).get('milestones', [])))") + + # Consistency: milestone counts match and names overlap + CONSISTENCY_OK=$(python3 -c " +import json, subprocess, sys +local = json.load(open('${REPO_ROOT}/.mgw/project.json')) +local_names = set(m['name'] for m in local.get('milestones', [])) +local_count = len(local_names) +gh_count = ${GH_MILESTONE_COUNT} + +# Count mismatch is a drift signal (allow off-by-one for in-flight) +if abs(local_count - gh_count) > 1: + print('false') + sys.exit(0) + +# Name overlap check: at least 50% of local milestone names found on GitHub +result = subprocess.run( + ['gh', 'api', 'repos/${REPO}/milestones', '--jq', '[.[].title]'], + capture_output=True, text=True +) +try: + gh_names = set(json.loads(result.stdout)) + overlap = len(local_names & gh_names) + print('true' if overlap >= max(1, local_count // 2) else 'false') +except Exception: + print('false') +") + + if [ "$CONSISTENCY_OK" = "true" ]; then + STATE_CLASS="Aligned" + else + STATE_CLASS="Diverged" + fi + fi +elif [ "$M" = "false" ] && [ "$G" -eq 0 ]; then + # No MGW state, no GitHub milestones — GSD signals determine class + if [ "$P" = "true" ] && [ "$R" = "true" ] && [ "$S" = "true" ]; then + STATE_CLASS="GSD-Mid-Exec" + elif [ "$P" = "true" ] && [ "$R" = "true" ]; then + STATE_CLASS="GSD-Mid-Exec" + elif [ "$P" = "true" ]; then + STATE_CLASS="GSD-Only" + else + STATE_CLASS="Fresh" + fi +fi + +echo "State detected: ${STATE_CLASS} (P=${P} R=${R} S=${S} M=${M} G=${G})" +``` + +**Route by STATE_CLASS:** + +```bash +case "$STATE_CLASS" in + "Fresh") + # Proceed to gather_inputs (standard flow) + ;; + + "GSD-Only"|"GSD-Mid-Exec") + # GSD artifacts exist but MGW not initialized — delegate to align_from_gsd + # (proceed to align_from_gsd step) + ;; + + "Aligned") + # MGW + GitHub consistent — display status and offer extend mode + TOTAL_ISSUES=$(python3 -c " +import json +p = json.load(open('${REPO_ROOT}/.mgw/project.json')) +print(sum(len(m.get('issues', [])) for m in p.get('milestones', []))) +") + echo "" + echo "Project already initialized and aligned with GitHub." + echo " Milestones: ${LOCAL_MILESTONE_COUNT} local / ${GH_MILESTONE_COUNT} on GitHub" + echo " Issues: ${TOTAL_ISSUES} tracked in project.json" + echo "" + echo "What would you like to do?" + echo "" + echo " 1) Continue with /mgw:milestone (execute next milestone)" + echo " 2) Add new milestones to this project (extend mode)" + echo " 3) View full status (/mgw:status)" + echo "" + read -p "Choose [1/2/3]: " ALIGNED_CHOICE + case "$ALIGNED_CHOICE" in + 2) + echo "" + echo "Entering extend mode — new milestones will be added to the existing project." + EXTEND_MODE=true + EXISTING_MILESTONE_COUNT=${LOCAL_MILESTONE_COUNT} + EXISTING_PHASE_COUNT=$(python3 -c " +import json +p = json.load(open('${REPO_ROOT}/.mgw/project.json')) +print(sum(len(m.get('phases', [])) for m in p.get('milestones', []))) +") + echo "Phase numbering will continue from phase ${EXISTING_PHASE_COUNT}." + # Fall through to gather_inputs — do NOT exit + ;; + 3) + echo "" + echo "Run /mgw:status to view the full project status dashboard." + exit 0 + ;; + *) + echo "" + echo "Run /mgw:milestone to execute the next milestone." + exit 0 + ;; + esac + ;; + + "Diverged") + # MGW + GitHub inconsistent — delegate to reconcile_drift + # (proceed to reconcile_drift step) + ;; + + "Extend") + # All milestones done — entering extend mode + echo "All ${EXISTING_MILESTONE_COUNT} milestones complete. Entering extend mode." + echo "Phase numbering will continue from phase ${EXISTING_PHASE_COUNT}." + # Proceed to gather_inputs in extend mode (EXTEND_MODE=true already set) + ;; +esac +``` + + + +**Align MGW state from existing GSD artifacts (STATE_CLASS = GSD-Only or GSD-Mid-Exec):** + +Spawn alignment-analyzer agent: + +Task( + description="Analyze GSD state for alignment", + subagent_type="general-purpose", + prompt=" + +- ./CLAUDE.md +- .planning/PROJECT.md (if exists) +- .planning/ROADMAP.md (if exists) +- .planning/MILESTONES.md (if exists) +- .planning/STATE.md (if exists) + + +Analyze existing GSD project state and produce an alignment report. + +Read each file that exists. Extract: +- Project name and description from PROJECT.md (H1 heading, description paragraph) +- Active milestone: from ROADMAP.md header or STATE.md current milestone name +- Archived milestones: from MILESTONES.md — list each milestone with name and phase count +- Phases per milestone: from ROADMAP.md sections (### Phase N:) and MILESTONES.md + +For each milestone found: +- name: milestone name string +- source: 'ROADMAP' (if from current ROADMAP.md) or 'MILESTONES' (if archived) +- state: 'active' (ROADMAP source), 'completed' (archived in MILESTONES.md), 'planned' (referenced but not yet created) +- phases: array of { number, name, status } objects + + +Write JSON to .mgw/alignment-report.json: +{ + \"project_name\": \"extracted from PROJECT.md\", + \"project_description\": \"extracted from PROJECT.md\", + \"milestones\": [ + { + \"name\": \"milestone name\", + \"source\": \"ROADMAP|MILESTONES\", + \"state\": \"active|completed|planned\", + \"phases\": [{ \"number\": N, \"name\": \"...\", \"status\": \"...\" }] + } + ], + \"active_milestone\": \"name of currently active milestone or null\", + \"total_phases\": N, + \"total_issues_estimated\": N +} + +" +) + +After agent completes: +1. Read .mgw/alignment-report.json +2. Display alignment summary to user: + - Project: {project_name} + - Milestones found: {count} ({active_milestone} active, N completed) + - Phases: {total_phases} total, ~{total_issues_estimated} issues estimated +3. Ask: "Import this GSD state into MGW? This will create GitHub milestones and issues, and build project.json. (Y/N)" +4. If Y: proceed to step milestone_mapper +5. If N: exit with message "Run /mgw:project again when ready to import." + + + +**Map GSD milestones to GitHub milestones:** + +Read .mgw/alignment-report.json produced by the alignment-analyzer agent. + +```bash +ALIGNMENT=$(python3 -c " +import json +with open('.mgw/alignment-report.json') as f: + data = json.load(f) +print(json.dumps(data)) +") +``` + +For each milestone in the alignment report: +1. Check if a GitHub milestone with a matching title already exists: + ```bash + gh api repos/${REPO}/milestones --jq '.[].title' + ``` +2. If not found: create it: + ```bash + gh api repos/${REPO}/milestones -X POST \ + -f title="${MILESTONE_NAME}" \ + -f description="Imported from GSD: ${MILESTONE_SOURCE}" \ + -f state="open" + ``` + Capture the returned `number` as GITHUB_MILESTONE_NUMBER. +3. If found: use the existing milestone's number. +4. For each phase in the milestone: create GitHub issues (one per phase, title = phase name, body includes phase goals and gsd_route). Use the same issue creation pattern as the existing `create_issues` step. +5. Add project.json entry for this milestone using the new schema fields: + ```json + { + "github_number": GITHUB_MILESTONE_NUMBER, + "name": milestone_name, + "gsd_milestone_id": null, + "gsd_state": "active|completed based on alignment report state", + "roadmap_archived_at": null + } + ``` +6. Add maps-to cross-ref entry: + ```bash + # Append to .mgw/cross-refs.json + TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ") + # Add entry: { "a": "milestone:${GITHUB_NUMBER}", "b": "gsd-milestone:${GSD_ID}", "type": "maps-to", "created": "${TIMESTAMP}" } + ``` + +After all milestones are mapped: +- Write updated project.json with all milestone entries and new schema fields +- Set active_gsd_milestone to the name of the 'active' milestone from alignment report +- Display mapping summary: + ``` + Mapped N GSD milestones → GitHub milestones: + ✓ "Milestone Name" → #N (created/existing) + ... + cross-refs.json updated with N maps-to entries + ``` +- Proceed to create_project_board step (existing step — reused for new project) + + + +**Reconcile diverged state (STATE_CLASS = Diverged):** + +Spawn drift-analyzer agent: + +Task( + description="Analyze project state drift", + subagent_type="general-purpose", + prompt=" + +- ./CLAUDE.md +- .mgw/project.json + + +Compare .mgw/project.json with live GitHub state. + +1. Read project.json: parse milestones array, get repo name from project.repo +2. Query GitHub milestones: + gh api repos/{REPO}/milestones --jq '.[] | {number, title, state, open_issues, closed_issues}' +3. For each milestone in project.json: + - Does a GitHub milestone with matching title exist? (fuzzy: case-insensitive, strip emoji) + - If no match: flag as missing_github + - If match: compare issue count (open + closed GitHub vs issues array length) +4. For each GitHub milestone NOT matched to project.json entry: flag as missing_local +5. For issues: check pipeline_stage vs GitHub issue state + - GitHub closed + local not 'done' or 'pr-created': flag as stage_mismatch + + +Write JSON to .mgw/drift-report.json: +{ + \"mismatches\": [ + {\"type\": \"missing_github\", \"milestone_name\": \"...\", \"local_issue_count\": N, \"action\": \"create_github_milestone\"}, + {\"type\": \"missing_local\", \"github_number\": N, \"github_title\": \"...\", \"action\": \"import_to_project_json\"}, + {\"type\": \"count_mismatch\", \"milestone_name\": \"...\", \"local\": N, \"github\": M, \"action\": \"review_manually\"}, + {\"type\": \"stage_mismatch\", \"issue\": N, \"local_stage\": \"...\", \"github_state\": \"closed\", \"action\": \"update_local_stage\"} + ], + \"summary\": \"N mismatches found across M milestones\" +} + +" +) + +After agent completes: +1. Read .mgw/drift-report.json +2. Display mismatches as a table: + + | Type | Detail | Suggested Action | + |------|--------|-----------------| + | missing_github | Milestone: {name} ({N} local issues) | Create GitHub milestone | + | missing_local | GitHub #N: {title} | Import to project.json | + | count_mismatch | {name}: local={N}, github={M} | Review manually | + | stage_mismatch | Issue #{N}: local={stage}, github=closed | Update local stage to done | + +3. If no mismatches: echo "No drift detected — state is consistent. Reclassifying as Aligned." and proceed to report alignment status. +4. If mismatches: Ask "Apply auto-fixes? Options: (A)ll / (S)elective / (N)one" + - All: apply each action (create missing milestones, update stages in project.json) + - Selective: present each fix individually, Y/N per item + - None: exit with "Drift noted. Run /mgw:sync to reconcile later." +5. After applying fixes: write updated project.json and display summary. + + + +**Intake: capture the raw project idea (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Display to user: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + MGW ► VISION CYCLE — Let's Build Your Project +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Tell me about the project you want to build. Don't worry +about being complete or precise — just describe the idea, +the problem you're solving, and who it's for. +``` + +Capture freeform user input as RAW_IDEA. + +```bash +TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ") +``` + +Save to `.mgw/vision-draft.md`: +```markdown +--- +current_stage: intake +rounds_completed: 0 +soft_cap_reached: false +--- + +# Vision Draft + +## Intake +**Raw Idea:** {RAW_IDEA} +**Captured:** {TIMESTAMP} +``` + +Proceed to vision_research step. + + + +**Domain Expansion: spawn vision-researcher agent (silent)** + +If STATE_CLASS != Fresh: skip this step. + +Spawn vision-researcher Task agent: + +Task( + description="Research project domain and platform requirements", + subagent_type="general-purpose", + prompt=" +You are a domain research agent for a new software project. + +Raw idea from user: +{RAW_IDEA} + +Research this project idea and produce a domain analysis. Write your output to .mgw/vision-research.json. + +Your analysis must include: + +1. **domain_analysis**: What does this domain actually require to succeed? + - Core capabilities users expect + - Table stakes vs differentiators + - Common failure modes in this domain + +2. **platform_requirements**: Specific technical/integration needs + - APIs, third-party services the domain typically needs + - Compliance or regulatory considerations + - Platform targets (mobile, web, desktop, API-only) + +3. **competitive_landscape**: What similar solutions exist? + - 2-3 examples with their key approaches + - Gaps in existing solutions that this could fill + +4. **risk_factors**: Common failure modes for this type of project + - Technical risks + - Business/adoption risks + - Scope creep patterns in this domain + +5. **suggested_questions**: 6-10 targeted questions to ask the user + - Prioritized by most impactful for scoping + - Each question should clarify a decision that affects architecture or milestone structure + - Format: [{\"question\": \"...\", \"why_it_matters\": \"...\"}, ...] + +Output format — write to .mgw/vision-research.json: +{ + \"domain_analysis\": {\"core_capabilities\": [...], \"differentiators\": [...], \"failure_modes\": [...]}, + \"platform_requirements\": [...], + \"competitive_landscape\": [{\"name\": \"...\", \"approach\": \"...\"}], + \"risk_factors\": [...], + \"suggested_questions\": [{\"question\": \"...\", \"why_it_matters\": \"...\"}] +} +" +) + +After agent completes: +- Read .mgw/vision-research.json +- Append research summary to .mgw/vision-draft.md: + ```markdown + ## Domain Research (silent) + - Domain: {domain from analysis} + - Key platform requirements: {top 3} + - Risks identified: {count} + - Questions generated: {count} + ``` +- Update vision-draft.md frontmatter: current_stage: questioning +- Proceed to vision_questioning step. + + + +**Structured Questioning Loop (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Read .mgw/vision-research.json to get suggested_questions. +Read .mgw/vision-draft.md to get current state. + +Initialize loop: +```bash +ROUND=0 +SOFT_CAP=8 +HARD_CAP=15 +SOFT_CAP_REACHED=false +``` + +**Questioning loop:** + +Each round: + +1. Load questions remaining from .mgw/vision-research.json suggested_questions (dequeue used ones). + Also allow orchestrator to generate follow-up questions based on previous answers. + +2. Present 2-4 questions to user (never more than 4 per round): + ``` + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vision Cycle — Round {N} of {SOFT_CAP} + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + 1) {question_1} + 2) {question_2} + 3) {question_3} + + (Answer all, some, or type 'done' to proceed to synthesis) + ``` + +3. Capture user answers as ANSWERS_ROUND_N. + +4. Append round to .mgw/vision-draft.md: + ```markdown + ## Round {N} — {TIMESTAMP} + **Questions asked:** + 1. {q1} + 2. {q2} + + **Answers:** + {ANSWERS_ROUND_N} + + **Key decisions extracted:** + - {decision_1} + - {decision_2} + ``` + (Key decisions: orchestrator extracts 1-3 concrete decisions from answers inline — no agent spawn needed) + +5. Increment ROUND. + Update .mgw/vision-draft.md frontmatter: rounds_completed={ROUND} + +6. **Soft cap check** (after round {SOFT_CAP}): + If ROUND >= SOFT_CAP and !SOFT_CAP_REACHED: + Set SOFT_CAP_REACHED=true + Update vision-draft.md frontmatter: soft_cap_reached=true + Display: + ``` + ───────────────────────────────────── + We've covered {ROUND} rounds of questions. + + Options: + D) Dig deeper — continue questioning (up to {HARD_CAP} rounds total) + S) Synthesize — proceed to Vision Brief generation + ───────────────────────────────────── + ``` + If user chooses S: exit loop and proceed to vision_synthesis + If user chooses D: continue loop + +7. **Hard cap** (ROUND >= HARD_CAP): automatically exit loop with notice: + ``` + Reached {HARD_CAP}-round limit. Proceeding to synthesis. + ``` + +8. **User 'done'**: if user types 'done' as answer: exit loop immediately. + +After loop exits: +- Update vision-draft.md frontmatter: current_stage: synthesizing +- Display: "Questioning complete ({ROUND} rounds). Generating Vision Brief..." +- Proceed to vision_synthesis step. + + + +**Vision Synthesis: spawn vision-synthesizer agent and review loop (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Display: "Generating Vision Brief from {rounds_completed} rounds of input..." + +**Synthesizer spawn:** + +Task( + description="Synthesize Vision Brief from research and questioning", + subagent_type="general-purpose", + prompt=" +You are the vision-synthesizer agent for a software project planning cycle. + +Read these files: +- .mgw/vision-draft.md — all rounds of user questions and answers, raw idea +- .mgw/vision-research.json — domain research, platform requirements, risks + +Synthesize a comprehensive Vision Brief. Write it to .mgw/vision-brief.json using this schema (templates/vision-brief-schema.json): + +{ + \"project_identity\": { \"name\": \"...\", \"tagline\": \"...\", \"domain\": \"...\" }, + \"target_users\": [{ \"persona\": \"...\", \"needs\": [...], \"pain_points\": [...] }], + \"core_value_proposition\": \"1-2 sentences: who, what, why different\", + \"feature_categories\": { + \"must_have\": [{ \"name\": \"...\", \"description\": \"...\", \"rationale\": \"why non-negotiable\" }], + \"should_have\": [{ \"name\": \"...\", \"description\": \"...\" }], + \"could_have\": [{ \"name\": \"...\", \"description\": \"...\" }], + \"wont_have\": [{ \"name\": \"...\", \"reason\": \"explicit out-of-scope reasoning\" }] + }, + \"technical_constraints\": [...], + \"success_metrics\": [...], + \"estimated_scope\": { \"milestones\": N, \"phases\": N, \"complexity\": \"small|medium|large|enterprise\" }, + \"recommended_milestone_structure\": [{ \"name\": \"...\", \"focus\": \"...\", \"deliverables\": [...] }] +} + +Be specific and concrete. Use the user's actual answers from vision-draft.md. Do NOT pad with generic content. +" +) + +After synthesizer completes: +1. Read .mgw/vision-brief.json +2. Display the Vision Brief to user in structured format: + ``` + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vision Brief: {project_identity.name} + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Tagline: {tagline} + Domain: {domain} + + Target Users: + • {persona_1}: {needs summary} + • {persona_2}: ... + + Core Value: {core_value_proposition} + + Must-Have Features ({count}): + • {feature_1}: {rationale} + • ... + + Won't Have ({count}): {list} + + Estimated Scope: {complexity} — {milestones} milestones, ~{phases} phases + + Recommended Milestones: + 1. {name}: {focus} + 2. ... + ``` + +3. Present review options: + ``` + ───────────────────────────────────────── + Review Options: + A) Accept — proceed to condensing and project creation + R) Revise — tell me what to change, regenerate + D) Dig deeper on: [specify area] + ───────────────────────────────────────── + ``` + +4. If Accept: proceed to vision_condense step +5. If Revise: capture correction, spawn vision-synthesizer again with correction appended to vision-draft.md, loop back to step 2 +6. If Dig deeper: append "Deeper exploration of {area}" to vision-draft.md, spawn vision-synthesizer again + + + +**Vision Condense: produce gsd:new-project handoff document (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Display: "Condensing Vision Brief into project handoff..." + +Task( + description="Condense Vision Brief into gsd:new-project handoff", + subagent_type="general-purpose", + prompt=" +You are the vision-condenser agent. Your job is to produce a handoff document +that will be passed as context to a gsd:new-project spawn. + +Read .mgw/vision-brief.json. + +Produce a structured handoff document at .mgw/vision-handoff.md that: + +1. Opens with a context block that gsd:new-project can use directly to produce PROJECT.md: + - Project name, tagline, domain + - Target users and their core needs + - Core value proposition + - Must-have feature list with rationale + - Won't-have list (explicit out-of-scope) + - Technical constraints + - Success metrics + +2. Includes recommended milestone structure as a numbered list: + - Each milestone: name, focus area, key deliverables + +3. Closes with an instruction for gsd:new-project: + 'Use the above as the full project context when creating PROJECT.md. + The project name, scope, users, and milestones above reflect decisions + made through {rounds_completed} rounds of collaborative planning. + Do not hallucinate scope beyond what is specified.' + +Format as clean markdown. This document becomes the prompt prefix for gsd:new-project. +" +) + +After condenser completes: +1. Verify .mgw/vision-handoff.md exists and has content +2. Display: "Vision Brief condensed. Ready to initialize project structure." +3. Update .mgw/vision-draft.md frontmatter: current_stage: spawning +4. Proceed to spawn_gsd_new_project step. + + + +**Spawn gsd:new-project with Vision Brief context (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Read .mgw/vision-handoff.md: +```bash +HANDOFF_CONTENT=$(cat .mgw/vision-handoff.md) +``` + +Display: "Spawning gsd:new-project with full vision context..." + +Spawn gsd:new-project as a Task agent, passing the handoff document as context prefix: + +Task( + description="Initialize GSD project from Vision Brief", + subagent_type="general-purpose", + prompt=" +${HANDOFF_CONTENT} + +--- + +You are now running gsd:new-project. Using the Vision Brief above as your full project context, create: + +1. .planning/PROJECT.md — Complete project definition following GSD format: + - Project name and one-line description from vision brief + - Vision and goals aligned with the value proposition + - Target users from the personas + - Core requirements mapping to the must-have features + - Non-goals matching the wont-have list + - Success criteria from success_metrics + - Technical constraints listed explicitly + +2. .planning/ROADMAP.md — First milestone plan following GSD format: + - Use the first milestone from recommended_milestone_structure + - Break it into 3-8 phases + - Each phase has: number, name, goal, requirements, success criteria + - Phase numbering starts at 1 + - Include a progress table at the top + +Write both files. Do not create additional files. Do not deviate from the Vision Brief scope. +" +) + +After agent completes: +1. Verify .planning/PROJECT.md exists: + ```bash + if [ ! -f .planning/PROJECT.md ]; then + echo "ERROR: gsd:new-project did not create .planning/PROJECT.md" + echo "Check the agent output and retry, or create PROJECT.md manually." + exit 1 + fi + ``` + +2. Verify .planning/ROADMAP.md exists: + ```bash + if [ ! -f .planning/ROADMAP.md ]; then + echo "ERROR: gsd:new-project did not create .planning/ROADMAP.md" + echo "Check the agent output and retry, or create ROADMAP.md manually." + exit 1 + fi + ``` + +3. Display success: + ``` + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD Project Initialized + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + .planning/PROJECT.md created + .planning/ROADMAP.md created (first milestone phases ready) + + Vision cycle: {rounds_completed} rounds -> Vision Brief -> PROJECT.md + ``` + +4. Update .mgw/vision-draft.md frontmatter: current_stage: complete + +4b. Synthesize alignment-report.json for milestone_mapper: + +The Fresh path skips `align_from_gsd`, so `.mgw/alignment-report.json` does not exist yet. +Synthesize it from the freshly created ROADMAP.md and PROJECT.md so `milestone_mapper` has +consistent input regardless of which path was taken. + +```bash +python3 << 'PYEOF' +import json, re, os + +repo_root = os.environ.get("REPO_ROOT", ".") + +# --- Parse PROJECT.md for name and description --- +project_path = os.path.join(repo_root, ".planning", "PROJECT.md") +with open(project_path, "r") as f: + project_text = f.read() + +# Extract H1 heading as project name +name_match = re.search(r"^#\s+(.+)$", project_text, re.MULTILINE) +project_name = name_match.group(1).strip() if name_match else "Untitled Project" + +# Extract first paragraph after H1 as description +desc_match = re.search(r"^#\s+.+\n+(.+?)(?:\n\n|\n#)", project_text, re.MULTILINE | re.DOTALL) +project_description = desc_match.group(1).strip() if desc_match else "" + +# --- Parse ROADMAP.md for phases --- +roadmap_path = os.path.join(repo_root, ".planning", "ROADMAP.md") +with open(roadmap_path, "r") as f: + roadmap_text = f.read() + +# Extract milestone name from first heading after any frontmatter +roadmap_body = re.sub(r"^---\n.*?\n---\n?", "", roadmap_text, flags=re.DOTALL) +milestone_heading = re.search(r"^#{1,2}\s+(.+)$", roadmap_body, re.MULTILINE) +milestone_name = milestone_heading.group(1).strip() if milestone_heading else "Milestone 1" + +# Extract phases (### Phase N: Name or ## Phase N: Name) +phase_pattern = re.compile(r"^#{2,3}\s+Phase\s+(\d+)[:\s]+(.+)$", re.MULTILINE) +phases = [] +for m in phase_pattern.finditer(roadmap_text): + phases.append({ + "number": int(m.group(1)), + "name": m.group(2).strip(), + "status": "pending" + }) + +if not phases: + phases = [{"number": 1, "name": milestone_name, "status": "pending"}] + +# Estimate ~2 issues per phase as a rough default +total_issues_estimated = len(phases) * 2 + +report = { + "project_name": project_name, + "project_description": project_description, + "milestones": [ + { + "name": milestone_name, + "source": "ROADMAP", + "state": "active", + "phases": phases + } + ], + "active_milestone": milestone_name, + "total_phases": len(phases), + "total_issues_estimated": total_issues_estimated +} + +output_path = os.path.join(repo_root, ".mgw", "alignment-report.json") +os.makedirs(os.path.dirname(output_path), exist_ok=True) +with open(output_path, "w") as f: + json.dump(report, f, indent=2) + +print(f"Synthesized alignment-report.json: {len(phases)} phases, milestone='{milestone_name}'") +PYEOF +``` + +5. Proceed to milestone_mapper step: + The ROADMAP.md now exists, so PATH A (HAS_ROADMAP=true) logic applies. + Call the milestone_mapper step to read ROADMAP.md and create GitHub milestones/issues. + (Note: at this point STATE_CLASS was Fresh but now GSD files exist — the milestone_mapper + step was designed for the GSD-Only path but works identically here. Proceed to it directly.) + + **Gather project inputs conversationally:** +If STATE_CLASS = Fresh: skip this step (handled by vision_intake through spawn_gsd_new_project above — proceed directly to milestone_mapper). + Ask the following questions in sequence: **Question 1:** "What are you building?" @@ -268,7 +1116,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 @@ -495,29 +1348,280 @@ 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 " Reusing existing project board: #${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 [ -n "$PROJECT_NUMBER" ]; then - echo " Created project board: #${PROJECT_NUMBER} — ${PROJECT_URL}" +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 "") - # Add all issues to the board + 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 +``` + +Store `PROJECT_NUMBER` and `PROJECT_URL` for inclusion in project.json and the summary report. + + + +**Sync newly created issues onto the board as items with field values (non-blocking):** + +This step runs after `create_project_board` (both init and extend modes). It adds each +newly created issue as a board item and sets Milestone, Phase, and GSD Route field values. +Board item IDs are collected here and stored in project.json (as `board_item_id` per issue). + +If no board is configured (PROJECT_NUMBER is empty) or the board has no custom fields +configured (node_id or fields missing from project.json), skip silently. + +Non-blocking: any GraphQL error is logged as a WARNING and does not halt the pipeline. + +```bash +# Load board field metadata from project.json +BOARD_NODE_ID=$(python3 -c " +import json +try: + p = json.load(open('${MGW_DIR}/project.json')) + print(p.get('project', {}).get('project_board', {}).get('node_id', '')) +except: + print('') +" 2>/dev/null || echo "") + +BOARD_FIELDS_JSON=$(python3 -c " +import json +try: + p = json.load(open('${MGW_DIR}/project.json')) + fields = p.get('project', {}).get('project_board', {}).get('fields', {}) + print(json.dumps(fields)) +except: + print('{}') +" 2>/dev/null || echo "{}") + +# Resolve field IDs from stored metadata +MILESTONE_FIELD_ID=$(echo "$BOARD_FIELDS_JSON" | python3 -c " +import json,sys +fields = json.load(sys.stdin) +print(fields.get('milestone', {}).get('field_id', '')) +" 2>/dev/null || echo "") + +PHASE_FIELD_ID=$(echo "$BOARD_FIELDS_JSON" | python3 -c " +import json,sys +fields = json.load(sys.stdin) +print(fields.get('phase', {}).get('field_id', '')) +" 2>/dev/null || echo "") + +GSD_ROUTE_FIELD_ID=$(echo "$BOARD_FIELDS_JSON" | python3 -c " +import json,sys +fields = json.load(sys.stdin) +print(fields.get('gsd_route', {}).get('field_id', '')) +" 2>/dev/null || echo "") + +GSD_ROUTE_OPTIONS=$(echo "$BOARD_FIELDS_JSON" | python3 -c " +import json,sys +fields = json.load(sys.stdin) +print(json.dumps(fields.get('gsd_route', {}).get('options', {}))) +" 2>/dev/null || echo "{}") + +# Determine if sync is possible +BOARD_SYNC_ENABLED=false +if [ -n "$PROJECT_NUMBER" ] && [ -n "$BOARD_NODE_ID" ]; then + BOARD_SYNC_ENABLED=true + echo "" + echo "Syncing ${TOTAL_ISSUES_CREATED} issues onto board #${PROJECT_NUMBER}..." +elif [ -n "$PROJECT_NUMBER" ] && [ -z "$BOARD_NODE_ID" ]; then + echo "" + echo "NOTE: Board #${PROJECT_NUMBER} exists but custom fields not configured." + echo " Run /mgw:board create to set up fields, then board sync will be available." +fi + +# ISSUE_RECORD format: "milestone_index:issue_number:title:phase_num:phase_name:gsd_route:depends_on" +# ITEM_ID_MAP accumulates: "issue_number:item_id" for project.json storage +ITEM_ID_MAP=() +BOARD_SYNC_WARNINGS=() + +if [ "$BOARD_SYNC_ENABLED" = "true" ]; then 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 + ISSUE_PHASE_NUM=$(echo "$RECORD" | cut -d':' -f4) + ISSUE_PHASE_NAME=$(echo "$RECORD" | cut -d':' -f5) + ISSUE_GSD_ROUTE=$(echo "$RECORD" | cut -d':' -f6) + ISSUE_MILESTONE_IDX=$(echo "$RECORD" | cut -d':' -f1) + + # Get milestone name for this issue + ISSUE_MILESTONE_NAME=$(python3 -c " +import json +try: + d = json.load(open('/tmp/mgw-template.json')) + print(d['milestones'][${ISSUE_MILESTONE_IDX}]['name']) +except: + print('') +" 2>/dev/null || echo "") + + # Resolve GitHub issue node ID (needed for addProjectV2ItemById) + ISSUE_NODE_ID=$(gh api graphql -f query=' + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $number) { id } + } + } + ' -f owner="$OWNER" -f repo="$REPO_NAME" -F number="${ISSUE_NUM}" \ + --jq '.data.repository.issue.id' 2>/dev/null || echo "") + + if [ -z "$ISSUE_NODE_ID" ]; then + BOARD_SYNC_WARNINGS+=("WARNING: Could not resolve node ID for issue #${ISSUE_NUM} — skipping board sync for this issue") + continue + fi + + # Add issue to board + ADD_RESULT=$(gh api graphql -f query=' + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: { + projectId: $projectId + contentId: $contentId + }) { + item { id } + } + } + ' -f projectId="$BOARD_NODE_ID" -f contentId="$ISSUE_NODE_ID" 2>/dev/null) + + ITEM_ID=$(echo "$ADD_RESULT" | python3 -c " +import json,sys +try: + d = json.load(sys.stdin) + print(d['data']['addProjectV2ItemById']['item']['id']) +except: + print('') +" 2>/dev/null || echo "") + + if [ -z "$ITEM_ID" ]; then + BOARD_SYNC_WARNINGS+=("WARNING: Failed to add issue #${ISSUE_NUM} to board") + continue + fi + + echo " Added #${ISSUE_NUM} to board (item: ${ITEM_ID})" + ITEM_ID_MAP+=("${ISSUE_NUM}:${ITEM_ID}") + + # Set Milestone field (TEXT) + if [ -n "$MILESTONE_FIELD_ID" ] && [ -n "$ISSUE_MILESTONE_NAME" ]; then + gh api graphql -f query=' + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { text: $value } + }) { projectV2Item { id } } + } + ' -f projectId="$BOARD_NODE_ID" -f itemId="$ITEM_ID" \ + -f fieldId="$MILESTONE_FIELD_ID" -f value="$ISSUE_MILESTONE_NAME" \ + 2>/dev/null || BOARD_SYNC_WARNINGS+=("WARNING: Failed to set Milestone field on board item for #${ISSUE_NUM}") + fi + + # Set Phase field (TEXT) — "Phase N: Phase Name" + if [ -n "$PHASE_FIELD_ID" ]; then + PHASE_DISPLAY="Phase ${ISSUE_PHASE_NUM}: ${ISSUE_PHASE_NAME}" + gh api graphql -f query=' + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { text: $value } + }) { projectV2Item { id } } + } + ' -f projectId="$BOARD_NODE_ID" -f itemId="$ITEM_ID" \ + -f fieldId="$PHASE_FIELD_ID" -f value="$PHASE_DISPLAY" \ + 2>/dev/null || BOARD_SYNC_WARNINGS+=("WARNING: Failed to set Phase field on board item for #${ISSUE_NUM}") + fi + + # Set GSD Route field (SINGLE_SELECT) — look up option ID from stored map + if [ -n "$GSD_ROUTE_FIELD_ID" ]; then + # Map template gsd_route to board option key (e.g. "plan-phase" → "gsd:plan-phase") + # GSD_ROUTE_OPTIONS stores keys like "gsd:quick", "gsd:plan-phase", etc. + ROUTE_OPTION_ID=$(echo "$GSD_ROUTE_OPTIONS" | python3 -c " +import json,sys +opts = json.load(sys.stdin) +# Try exact match on gsd: prefix first, then plain match +route = '${ISSUE_GSD_ROUTE}' +for key, val in opts.items(): + if key == 'gsd:' + route or key == route: + print(val) + sys.exit(0) +# Fallback: plain match on the route name without prefix +for key, val in opts.items(): + if key.endswith(':' + route) or key == route: + print(val) + sys.exit(0) +print('') +" 2>/dev/null || echo "") + + if [ -n "$ROUTE_OPTION_ID" ]; then + gh api graphql -f query=' + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { singleSelectOptionId: $optionId } + }) { projectV2Item { id } } + } + ' -f projectId="$BOARD_NODE_ID" -f itemId="$ITEM_ID" \ + -f fieldId="$GSD_ROUTE_FIELD_ID" -f optionId="$ROUTE_OPTION_ID" \ + 2>/dev/null || BOARD_SYNC_WARNINGS+=("WARNING: Failed to set GSD Route field on board item for #${ISSUE_NUM}") + fi + fi done - echo " Added ${TOTAL_ISSUES_CREATED} issues to project board" -else - echo " WARNING: Failed to create project board: ${PROJECT_RESP}" - PROJECT_NUMBER="" - PROJECT_URL="" + + if [ ${#BOARD_SYNC_WARNINGS[@]} -gt 0 ]; then + echo "" + echo "Board sync warnings:" + for W in "${BOARD_SYNC_WARNINGS[@]}"; do + echo " $W" + done + fi + + BOARD_SYNC_COUNT=$((${#ITEM_ID_MAP[@]})) + echo " Board sync complete: ${BOARD_SYNC_COUNT}/${TOTAL_ISSUES_CREATED} issues synced" fi ``` - -Store `PROJECT_NUMBER` and `PROJECT_URL` for inclusion in project.json and the summary report. @@ -553,6 +1657,8 @@ for m_idx, milestone in enumerate(template_data['milestones']): # Find github number from SLUG_TO_NUMBER slug = slugify(issue['title'])[:40] gh_num = SLUG_TO_NUMBER_MAP.get(slug) + # Look up board_item_id from ITEM_ID_MAP if available + item_id = ITEM_ID_MAP_DICT.get(gh_num, None) issues_out.append({ "github_number": gh_num, "title": issue['title'], @@ -561,7 +1667,8 @@ for m_idx, milestone in enumerate(template_data['milestones']): "gsd_route": phase.get('gsd_route', 'plan-phase'), "labels": issue.get('labels', []), "depends_on_slugs": issue.get('depends_on', []), - "pipeline_stage": "new" + "pipeline_stage": "new", + "board_item_id": item_id }) milestones_out.append({ @@ -604,7 +1711,18 @@ import json, sys # Read template data from the validated generated file template_data = json.load(open('/tmp/mgw-template.json')) -# ... (construct from available bash variables) + +# Build ITEM_ID_MAP_DICT from bash ITEM_ID_MAP array ("issue_num:item_id" entries) +# This dict maps github_number (int) -> board_item_id (str) +ITEM_ID_MAP_DICT = {} +for entry in [x for x in '''${ITEM_ID_MAP[*]}'''.split() if ':' in x]: + parts = entry.split(':', 1) + try: + ITEM_ID_MAP_DICT[int(parts[0])] = parts[1] + except (ValueError, IndexError): + pass + +# ... (construct from available bash variables — see pseudocode above) PYEOF ``` @@ -612,13 +1730,78 @@ The simplest implementation: build the JSON structure incrementally during the issue/milestone creation steps (maintaining bash arrays), then assemble them into a python3 dictionary and write with `json.dumps(indent=2)` at this step. +The `ITEM_ID_MAP` bash array (populated in `sync_milestone_to_board`) contains entries +in `"issue_number:board_item_id"` format. Decode it into `ITEM_ID_MAP_DICT` (as shown +above) and use it when building each issue record so `board_item_id` is stored. +If board sync was skipped (ITEM_ID_MAP is empty), `board_item_id` is null for all issues. + 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. + +**Extend mode: verify new milestone GSD linkage** + +After writing the updated project.json in extend mode, report the GSD linkage status for each newly added milestone: + +```bash +if [ "$EXTEND_MODE" = true ]; then + echo "" + echo "New milestone linkage status:" + for MILESTONE in "${NEW_MILESTONES[@]}"; do + MILE_NAME=$(echo "$MILESTONE" | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])" 2>/dev/null || echo "unknown") + echo " o '${MILE_NAME}' — no GSD milestone linked yet" + echo " -> Run /gsd:new-milestone after completing the previous milestone to link" + done + echo "" +fi +``` **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} @@ -673,7 +1856,13 @@ Warnings: - [ ] Slug-to-number mapping built during Pass 1b - [ ] Dependency labels applied (Pass 2) — blocked-by:#N on dependent issues - [ ] cross-refs.json updated with dependency entries -- [ ] .mgw/project.json written with full project state +- [ ] Board sync: if board configured (PROJECT_NUMBER + BOARD_NODE_ID in project.json), each new issue added as board item +- [ ] Board sync: Milestone, Phase, and GSD Route fields set on each board item where field IDs are available +- [ ] Board sync: board_item_id stored per issue in project.json (null if board sync skipped or failed) +- [ ] Board sync: non-blocking — GraphQL errors logged as warnings, pipeline continues +- [ ] Board sync: skipped silently if board not configured or custom fields not set up +- [ ] .mgw/project.json written with full project state (including board_item_id per issue) - [ ] 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/.claude/commands/mgw/run.md b/.claude/commands/mgw/run.md index 7dcde4f..c515dc2 100644 --- a/.claude/commands/mgw/run.md +++ b/.claude/commands/mgw/run.md @@ -38,7 +38,6 @@ Checkpoints requiring user input: @~/.claude/commands/mgw/workflows/github.md @~/.claude/commands/mgw/workflows/gsd.md @~/.claude/commands/mgw/workflows/validation.md -@~/.claude/commands/mgw/workflows/board-sync.md @@ -58,110 +57,6 @@ REPO_ROOT=$(git rev-parse --show-toplevel) DEFAULT=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name) ``` -Define the board sync utilities (non-blocking — see board-sync.md for full reference): -```bash -update_board_status() { - local ISSUE_NUMBER="$1" - local NEW_STAGE="$2" - if [ -z "$ISSUE_NUMBER" ] || [ -z "$NEW_STAGE" ]; then return 0; fi - BOARD_NODE_ID=$(python3 -c " -import json,sys,os -try: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - print(p.get('project',{}).get('project_board',{}).get('node_id','')) -except: print('') -" 2>/dev/null || echo "") - if [ -z "$BOARD_NODE_ID" ]; then return 0; fi - ITEM_ID=$(python3 -c " -import json,sys -try: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - for m in p.get('milestones',[]): - for i in m.get('issues',[]): - if i.get('github_number')==${ISSUE_NUMBER}: - print(i.get('board_item_id','')); sys.exit(0) - print('') -except: print('') -" 2>/dev/null || echo "") - if [ -z "$ITEM_ID" ]; then return 0; fi - FIELD_ID=$(python3 -c " -import json,sys,os -try: - s='${REPO_ROOT}/.mgw/board-schema.json' - if os.path.exists(s): - print(json.load(open(s)).get('fields',{}).get('status',{}).get('field_id','')) - else: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - print(p.get('project',{}).get('project_board',{}).get('fields',{}).get('status',{}).get('field_id','')) -except: print('') -" 2>/dev/null || echo "") - if [ -z "$FIELD_ID" ]; then return 0; fi - OPTION_ID=$(python3 -c " -import json,sys,os -try: - stage='${NEW_STAGE}' - s='${REPO_ROOT}/.mgw/board-schema.json' - if os.path.exists(s): - print(json.load(open(s)).get('fields',{}).get('status',{}).get('options',{}).get(stage,'')) - else: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - print(p.get('project',{}).get('project_board',{}).get('fields',{}).get('status',{}).get('options',{}).get(stage,'')) -except: print('') -" 2>/dev/null || echo "") - if [ -z "$OPTION_ID" ]; then return 0; fi - gh api graphql -f query=' - mutation($projectId:ID!,$itemId:ID!,$fieldId:ID!,$optionId:String!){ - updateProjectV2ItemFieldValue(input:{projectId:$projectId,itemId:$itemId,fieldId:$fieldId,value:{singleSelectOptionId:$optionId}}){projectV2Item{id}} - } - ' -f projectId="$BOARD_NODE_ID" -f itemId="$ITEM_ID" \ - -f fieldId="$FIELD_ID" -f optionId="$OPTION_ID" 2>/dev/null || true -} - -update_board_agent_state() { - local ISSUE_NUMBER="$1" - local STATE_TEXT="$2" - if [ -z "$ISSUE_NUMBER" ]; then return 0; fi - BOARD_NODE_ID=$(python3 -c " -import json,sys,os -try: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - print(p.get('project',{}).get('project_board',{}).get('node_id','')) -except: print('') -" 2>/dev/null || echo "") - if [ -z "$BOARD_NODE_ID" ]; then return 0; fi - ITEM_ID=$(python3 -c " -import json,sys -try: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - for m in p.get('milestones',[]): - for i in m.get('issues',[]): - if i.get('github_number')==${ISSUE_NUMBER}: - print(i.get('board_item_id','')); sys.exit(0) - print('') -except: print('') -" 2>/dev/null || echo "") - if [ -z "$ITEM_ID" ]; then return 0; fi - FIELD_ID=$(python3 -c " -import json,sys,os -try: - s='${REPO_ROOT}/.mgw/board-schema.json' - if os.path.exists(s): - print(json.load(open(s)).get('fields',{}).get('ai_agent_state',{}).get('field_id','')) - else: - p=json.load(open('${REPO_ROOT}/.mgw/project.json')) - print(p.get('project',{}).get('project_board',{}).get('fields',{}).get('ai_agent_state',{}).get('field_id','')) -except: print('') -" 2>/dev/null || echo "") - if [ -z "$FIELD_ID" ]; then return 0; fi - gh api graphql -f query=' - mutation($projectId:ID!,$itemId:ID!,$fieldId:ID!,$text:String!){ - updateProjectV2ItemFieldValue(input:{projectId:$projectId,itemId:$itemId,fieldId:$fieldId,value:{text:$text}}){projectV2Item{id}} - } - ' -f projectId="$BOARD_NODE_ID" -f itemId="$ITEM_ID" \ - -f fieldId="$FIELD_ID" -f text="$STATE_TEXT" 2>/dev/null || true -} -``` - Parse $ARGUMENTS for issue number. If missing: ``` AskUserQuestion( @@ -211,6 +106,187 @@ If state file exists → load it. Check pipeline_stage: Log warning: "MGW: WARNING — Acknowledging security risk for #${ISSUE_NUMBER}. Proceeding with --security-ack." Update state: pipeline_stage = "triaged", add override_log entry. Continue pipeline. + +**Cross-milestone detection (runs after loading issue state):** + +Check if this issue belongs to a non-active GSD milestone: + +```bash +CROSS_MILESTONE_WARN=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +if (!state) { console.log('none'); process.exit(0); } + +const activeGsdId = state.active_gsd_milestone; + +// Find this issue's milestone in project.json +const issueNum = ${ISSUE_NUMBER}; +let issueMilestone = null; +for (const m of (state.milestones || [])) { + if ((m.issues || []).some(i => i.github_number === issueNum)) { + issueMilestone = m; + break; + } +} + +if (!issueMilestone) { console.log('none'); process.exit(0); } + +const issueGsdId = issueMilestone.gsd_milestone_id; + +// No active_gsd_milestone set (legacy schema): no warning +if (!activeGsdId) { console.log('none'); process.exit(0); } + +// Issue is in the active milestone: no warning +if (issueGsdId === activeGsdId) { console.log('none'); process.exit(0); } + +// Issue is in a different milestone +const gsdRoute = '${GSD_ROUTE}'; +if (gsdRoute === 'quick' || gsdRoute === 'gsd:quick') { + console.log('isolation:' + issueMilestone.name + ':' + (issueGsdId || 'unlinked')); +} else { + console.log('warn:' + issueMilestone.name + ':' + (issueGsdId || 'unlinked') + ':' + activeGsdId); +} +") + +case "$CROSS_MILESTONE_WARN" in + none) + # No cross-milestone issue — proceed normally + ;; + isolation:*) + MILESTONE_NAME=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f2) + GSD_ID=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f3) + + # Re-validate route against live GitHub labels (project.json may be stale from triage time) + LIVE_LABELS=$(gh issue view ${ISSUE_NUMBER} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "") + QUICK_CONFIRMED=false + if echo "$LIVE_LABELS" | grep -qiE "gsd-route:quick|gsd:quick|quick"; then + QUICK_CONFIRMED=true + fi + + if [ "$QUICK_CONFIRMED" = "true" ]; then + echo "" + echo "NOTE: Issue #${ISSUE_NUMBER} belongs to milestone '${MILESTONE_NAME}' (GSD: ${GSD_ID})" + echo " Confirmed gsd:quick via live labels — running in isolation." + echo "" + else + # Route mismatch: project.json says quick but labels don't confirm it + echo "" + echo "⚠️ Route mismatch for cross-milestone issue #${ISSUE_NUMBER}:" + echo " project.json route: quick (set at triage time)" + echo " Live GitHub labels: ${LIVE_LABELS:-none}" + echo " Labels do not confirm gsd:quick — treating as plan-phase (requires milestone context)." + echo "" + echo "Options:" + echo " 1) Switch active milestone to '${GSD_ID}' and continue" + echo " 2) Re-triage this issue (/mgw:issue ${ISSUE_NUMBER}) to update its route" + echo " 3) Abort" + echo "" + read -p "Choice [1/2/3]: " ROUTE_MISMATCH_CHOICE + case "$ROUTE_MISMATCH_CHOICE" in + 1) + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '${GSD_ID}'; +writeProjectState(state); +console.log('Switched active_gsd_milestone to: ${GSD_ID}'); +" + # Validate ROADMAP.md matches (same check as option 1 in warn case) + ROADMAP_VALID=$(python3 -c " +import os +if not os.path.exists('.planning/ROADMAP.md'): + print('missing') +else: + with open('.planning/ROADMAP.md') as f: + content = f.read() + print('match' if '${GSD_ID}' in content else 'mismatch') +" 2>/dev/null || echo "missing") + if [ "$ROADMAP_VALID" != "match" ]; then + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f4)'; +writeProjectState(state); +" 2>/dev/null || true + echo "Switch rolled back — ROADMAP.md does not match '${GSD_ID}'." + echo "Run /gsd:new-milestone to update ROADMAP.md first." + exit 0 + fi + ;; + 2) + echo "Re-triage with: /mgw:issue ${ISSUE_NUMBER}" + exit 0 + ;; + *) + echo "Aborted." + exit 0 + ;; + esac + fi + ;; + warn:*) + ISSUE_MILESTONE=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f2) + ISSUE_GSD=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f3) + ACTIVE_GSD=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f4) + echo "" + echo "⚠️ Cross-milestone issue detected:" + echo " Issue #${ISSUE_NUMBER} belongs to: '${ISSUE_MILESTONE}' (GSD: ${ISSUE_GSD})" + echo " Active GSD milestone: ${ACTIVE_GSD}" + echo "" + echo "This issue requires plan-phase work that depends on ROADMAP.md context." + echo "Running it against the wrong active milestone may produce incorrect plans." + echo "" + echo "Options:" + echo " 1) Switch active milestone to '${ISSUE_GSD}' and continue" + echo " 2) Continue anyway (not recommended)" + echo " 3) Abort — run /gsd:new-milestone to set up the correct milestone first" + echo "" + read -p "Choice [1/2/3]: " MILESTONE_CHOICE + case "$MILESTONE_CHOICE" in + 1) + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '${ISSUE_GSD}'; +writeProjectState(state); +console.log('Switched active_gsd_milestone to: ${ISSUE_GSD}'); +" + # Validate ROADMAP.md matches the new active milestone + ROADMAP_VALID=$(python3 -c " +import os +if not os.path.exists('.planning/ROADMAP.md'): + print('missing') +else: + with open('.planning/ROADMAP.md') as f: + content = f.read() + print('match' if '${ISSUE_GSD}' in content else 'mismatch') +" 2>/dev/null || echo "missing") + if [ "$ROADMAP_VALID" = "match" ]; then + echo "Active milestone updated. ROADMAP.md confirmed for '${ISSUE_GSD}'." + else + # Roll back — ROADMAP.md doesn't match + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '${ACTIVE_GSD}'; +writeProjectState(state); +" 2>/dev/null || true + echo "Switch rolled back — ROADMAP.md does not match '${ISSUE_GSD}'." + echo "Run /gsd:new-milestone to update ROADMAP.md first." + exit 0 + fi + ;; + 2) + echo "Proceeding with cross-milestone issue (may affect plan quality)." + ;; + *) + echo "Aborted. Run /gsd:new-milestone then /mgw:project to align milestones." + exit 0 + ;; + esac + ;; +esac +``` @@ -353,7 +429,7 @@ Return ONLY valid JSON: |---------------|--------| | **informational** | Log: "MGW: ${NEW_COUNT} new comment(s) reviewed — informational, continuing." Update `triage.last_comment_count` in state file. Continue pipeline. | | **material** | Log: "MGW: Material comment(s) detected — scope may have changed." Update state: add new_requirements to triage context. Update `triage.last_comment_count`. Re-read issue body for updated requirements. Continue with enriched context (pass new_requirements to planner). Check for security keywords in material comments (see below). | -| **blocking** | Log: "MGW: Blocking comment detected — pipeline paused." Update state: `pipeline_stage = "blocked"`. Apply mgw:blocked label. Call `update_board_status $ISSUE_NUMBER "blocked"` (non-blocking). Post comment on issue: `> **MGW** . \`pipeline-blocked\` . Blocked by stakeholder comment. Reason: ${blocking_reason}`. Stop pipeline execution. | +| **blocking** | Log: "MGW: Blocking comment detected — pipeline paused." Update state: `pipeline_stage = "blocked"`. Apply mgw:blocked label. Post comment on issue: `> **MGW** . \`pipeline-blocked\` . Blocked by stakeholder comment. Reason: ${blocking_reason}`. Stop pipeline execution. | **Security keyword check for material comments:** ```bash @@ -394,14 +470,19 @@ TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --r # Load milestone/phase context from project.json if available MILESTONE_CONTEXT="" if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then - MILESTONE_CONTEXT=$(python3 -c " -import json -p = json.load(open('${REPO_ROOT}/.mgw/project.json')) -for m in p['milestones']: - for i in m.get('issues', []): - if i.get('github_number') == ${ISSUE_NUMBER}: - print(f\"Milestone: {m['name']} | Phase {i['phase_number']}: {i['phase_name']}\") - break + MILESTONE_CONTEXT=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +if (!state) process.exit(0); +// Search all milestones for the issue (not just active) to handle cross-milestone lookups +for (const m of (state.milestones || [])) { + for (const i of (m.issues || [])) { + if (i.github_number === ${ISSUE_NUMBER}) { + console.log('Milestone: ' + m.name + ' | Phase ' + i.phase_number + ': ' + i.phase_name); + process.exit(0); + } + } +} " 2>/dev/null || echo "") fi ``` @@ -444,9 +525,6 @@ Log comment in state file (at `${REPO_ROOT}/.mgw/active/`). Only run this step if gsd_route is "gsd:quick" or "gsd:quick --full". Update pipeline_stage to "executing" in state file (at `${REPO_ROOT}/.mgw/active/`). -```bash -update_board_status $ISSUE_NUMBER "executing" # non-blocking board sync -``` Determine flags: - "gsd:quick" → $QUICK_FLAGS = "" @@ -481,9 +559,6 @@ mkdir -p "$QUICK_DIR" ``` 3. **Spawn planner (task agent):** -```bash -update_board_agent_state $ISSUE_NUMBER "Planning" # non-blocking agent state -``` ``` Task( prompt=" @@ -586,9 +661,6 @@ If issues found and iteration < 2: spawn planner revision, then re-check. If iteration >= 2: offer force proceed or abort. 7. **Spawn executor (task agent):** -```bash -update_board_agent_state $ISSUE_NUMBER "Executing" # non-blocking agent state -``` ``` Task( prompt=" @@ -620,9 +692,6 @@ VERIFY_RESULT=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs verify-summary "$ Parse JSON result. Use `passed` field for go/no-go. Checks summary existence, files created, and commits. 9. **(If --full) Spawn verifier:** -```bash -update_board_agent_state $ISSUE_NUMBER "Verifying" # non-blocking agent state -``` ``` Task( prompt=" @@ -656,9 +725,6 @@ node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "docs(quick-${next_num}): ``` Update state (at `${REPO_ROOT}/.mgw/active/`): gsd_artifacts.path = $QUICK_DIR, pipeline_stage = "verifying". -```bash -update_board_status $ISSUE_NUMBER "verifying" # non-blocking board sync -``` @@ -696,7 +762,6 @@ Set pipeline_stage to "discussing" and apply "mgw:discussing" label: ```bash gh issue edit ${ISSUE_NUMBER} --remove-label "mgw:in-progress" 2>/dev/null gh issue edit ${ISSUE_NUMBER} --add-label "mgw:discussing" 2>/dev/null -update_board_status $ISSUE_NUMBER "discussing" # non-blocking board sync ``` Present to user: @@ -744,9 +809,6 @@ If proceed: apply "mgw:approved" label and continue. ``` Update pipeline_stage to "planning" (at `${REPO_ROOT}/.mgw/active/`). - ```bash - update_board_status $ISSUE_NUMBER "planning" # non-blocking board sync - ``` 2. **If resuming with pipeline_stage = "planning" and ROADMAP.md exists:** Discover phases from ROADMAP and run the full per-phase GSD lifecycle: @@ -785,9 +847,6 @@ If proceed: apply "mgw:approved" label and continue. ``` **b. Spawn planner agent (gsd:plan-phase):** - ```bash - update_board_agent_state $ISSUE_NUMBER "Planning phase ${PHASE_NUMBER}" # non-blocking agent state - ``` ``` Task( prompt=" @@ -834,7 +893,6 @@ If proceed: apply "mgw:approved" label and continue. ```bash EXEC_INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs init execute-phase "${PHASE_NUMBER}") # Parse EXEC_INIT JSON for: executor_model, verifier_model, phase_dir, plans, incomplete_plans, plan_count - update_board_agent_state $ISSUE_NUMBER "Executing phase ${PHASE_NUMBER}" # non-blocking agent state ``` ``` Task( @@ -867,9 +925,6 @@ If proceed: apply "mgw:approved" label and continue. ``` **e. Spawn verifier agent (gsd:verify-phase):** - ```bash - update_board_agent_state $ISSUE_NUMBER "Verifying phase ${PHASE_NUMBER}" # non-blocking agent state - ``` ``` Task( prompt=" @@ -916,9 +971,6 @@ COMMENTEOF ``` After ALL phases complete → update pipeline_stage to "verifying" (at `${REPO_ROOT}/.mgw/active/`). - ```bash - update_board_status $ISSUE_NUMBER "verifying" # non-blocking board sync - ``` @@ -956,9 +1008,6 @@ gh issue comment ${ISSUE_NUMBER} --body "$EXEC_BODY" 2>/dev/null || true ``` Update pipeline_stage to "pr-pending" (at `${REPO_ROOT}/.mgw/active/`). -```bash -update_board_status $ISSUE_NUMBER "pr-created" # non-blocking board sync (pr-pending maps to pr-created on board) -``` @@ -1129,12 +1178,6 @@ Update state (at `${REPO_ROOT}/.mgw/active/`): - linked_pr = PR number - pipeline_stage = "pr-created" -```bash -update_board_status $ISSUE_NUMBER "pr-created" # non-blocking board sync -update_board_agent_state $ISSUE_NUMBER "" # clear agent state after PR creation (non-blocking) -sync_pr_to_board $ISSUE_NUMBER $PR_NUMBER # non-blocking — add PR as board item -``` - Add cross-ref (at `${REPO_ROOT}/.mgw/cross-refs.json`): issue → PR. @@ -1195,9 +1238,6 @@ gh issue comment ${ISSUE_NUMBER} --body "$PR_READY_BODY" 2>/dev/null || true ``` Update pipeline_stage to "done" (at `${REPO_ROOT}/.mgw/active/`). -```bash -update_board_status $ISSUE_NUMBER "done" # non-blocking board sync -``` Report to user: ``` @@ -1238,10 +1278,5 @@ Next: - [ ] Worktree cleaned up, user returned to main workspace - [ ] mgw:in-progress label removed at completion - [ ] State file updated through all pipeline stages -- [ ] Board Status field synced at each pipeline_stage transition (non-blocking) -- [ ] AI Agent State field set before each GSD agent spawn (non-blocking) -- [ ] AI Agent State field cleared after PR creation (non-blocking) -- [ ] PR added to board as board item after creation (non-blocking) -- [ ] Board sync failures never block pipeline execution - [ ] User prompted to run /mgw:sync after merge diff --git a/.claude/commands/mgw/status.md b/.claude/commands/mgw/status.md index 8213ad9..4e612d3 100644 --- a/.claude/commands/mgw/status.md +++ b/.claude/commands/mgw/status.md @@ -1,7 +1,7 @@ --- name: mgw:status description: Project status dashboard — milestone progress, issue pipeline stages, open PRs -argument-hint: "[milestone_number] [--json]" +argument-hint: "[milestone_number] [--json] [--board]" allowed-tools: - Bash - Read @@ -34,10 +34,12 @@ Repo detected via: gh repo view --json nameWithOwner -q .nameWithOwner ```bash MILESTONE_NUM="" JSON_OUTPUT=false +OPEN_BOARD=false for ARG in $ARGUMENTS; do case "$ARG" in --json) JSON_OUTPUT=true ;; + --board) OPEN_BOARD=true ;; [0-9]*) MILESTONE_NUM="$ARG" ;; esac done @@ -52,6 +54,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel) MGW_DIR="${REPO_ROOT}/.mgw" REPO_NAME=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || basename "$REPO_ROOT") +BOARD_URL="" if [ ! -f "${MGW_DIR}/project.json" ]; then # No project.json — fall back to GitHub-only mode FALLBACK_MODE=true @@ -102,8 +105,18 @@ Exit after display. ```bash PROJECT_JSON=$(cat "${MGW_DIR}/project.json") -# Get current milestone pointer -CURRENT_MILESTONE=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['current_milestone'])") +# Resolve active milestone index (0-based) via state resolution (supports both schema versions) +ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +const milestone = state.milestones ? state.milestones[idx] : null; +const gsdId = state.active_gsd_milestone || ('legacy:' + state.current_milestone); +console.log(JSON.stringify({ idx, gsd_id: gsdId, name: milestone ? milestone.name : 'unknown' })); +") +CURRENT_MILESTONE_IDX=$(echo "$ACTIVE_IDX" | python3 -c "import json,sys; print(json.load(sys.stdin)['idx'])") +# Convert 0-based index to 1-indexed milestone number for display and compatibility +CURRENT_MILESTONE=$((CURRENT_MILESTONE_IDX + 1)) TOTAL_MILESTONES=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['milestones']))") # Use specified milestone or current @@ -124,9 +137,38 @@ print(json.dumps(m)) MILESTONE_NAME=$(echo "$MILESTONE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])") ISSUES_JSON=$(echo "$MILESTONE_DATA" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['issues']))") TOTAL_ISSUES=$(echo "$ISSUES_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))") + +# Extract board URL from project.json (top-level board_url or nested board.url) +BOARD_URL=$(echo "$PROJECT_JSON" | python3 -c " +import json, sys +p = json.load(sys.stdin) +# Check top-level board_url first, then board.url (nested) +url = p.get('board_url') or (p.get('board') or {}).get('url', '') +print(url or '') +" 2>/dev/null || echo "") ``` + +**Handle --board flag — open board in browser and exit early:** + +```bash +if [ "$OPEN_BOARD" = true ]; then + if [ -z "$BOARD_URL" ]; then + echo "No board configured in project.json. Run /mgw:board create first." >&2 + exit 1 + fi + echo "Opening board: ${BOARD_URL}" + xdg-open "${BOARD_URL}" 2>/dev/null \ + || open "${BOARD_URL}" 2>/dev/null \ + || echo "Could not open browser. Board URL: ${BOARD_URL}" + exit 0 +fi +``` + +This step exits early — do not continue to the dashboard display. + + **Compute pipeline stage counts and progress:** @@ -172,6 +214,71 @@ print(json.dumps({ ``` + +**Compute milestone health metrics — velocity, done count, blocked count:** + +```bash +HEALTH_DATA=$(echo "$ISSUES_JSON" | python3 -c " +import json, sys, os, glob + +issues = json.load(sys.stdin) +repo_root = os.environ.get('REPO_ROOT', os.getcwd()) +mgw_dir = os.path.join(repo_root, '.mgw') + +done_stages = {'done', 'pr-created'} +blocked_stages = {'blocked'} + +done_count = 0 +blocked_count = 0 +done_timestamps = [] + +for issue in issues: + stage = issue.get('pipeline_stage', 'new') + num = issue.get('github_number', 0) + + if stage in done_stages: + done_count += 1 + # Use .mgw/active/ or .mgw/completed/ file mtime as done timestamp proxy + for subdir in ['active', 'completed']: + pattern = os.path.join(mgw_dir, subdir, str(num) + '-*.json') + matches = glob.glob(pattern) + if matches: + try: + done_timestamps.append(os.path.getmtime(matches[0])) + except Exception: + pass + break + elif stage in blocked_stages: + blocked_count += 1 + +# Compute velocity (issues completed per day) +if done_count == 0: + velocity_str = '0/day' +elif len(done_timestamps) >= 2: + span_days = (max(done_timestamps) - min(done_timestamps)) / 86400.0 + if span_days >= 0.1: + velocity_str = '{:.1f}/day'.format(done_count / span_days) + else: + velocity_str = str(done_count) + ' (same day)' +elif done_count == 1: + velocity_str = '1 (single)' +else: + velocity_str = str(done_count) + '/day' + +import json as json2 +print(json2.dumps({ + 'done_count': done_count, + 'blocked_count': blocked_count, + 'velocity': velocity_str +})) +") + +HEALTH_DONE=$(echo "$HEALTH_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['done_count'])" 2>/dev/null || echo "0") +HEALTH_BLOCKED=$(echo "$HEALTH_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['blocked_count'])" 2>/dev/null || echo "0") +HEALTH_VELOCITY=$(echo "$HEALTH_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['velocity'])" 2>/dev/null || echo "N/A") +``` + + **Build per-issue status lines:** @@ -243,6 +350,13 @@ fi **Display the status dashboard:** +```bash +# Print board URL prominently at top if configured +if [ -n "$BOARD_URL" ]; then + echo " Board: ${BOARD_URL}" +fi +``` + ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ MGW > PROJECT STATUS: ${REPO_NAME} @@ -256,16 +370,51 @@ Progress: ${bar} ${pct}% #37 ⏳ new /mgw:status dashboard #38 🔒 blocked contextual routing (blocked by #37) +Milestone Health: + Completed: ${HEALTH_DONE}/${TOTAL_ISSUES} + Velocity: ${HEALTH_VELOCITY} + Blocked: ${HEALTH_BLOCKED} + Open PRs: #40 ← #36 comment-aware pipeline (review requested) Next Milestone: ${next_name} (${next_done}/${next_total} done) ``` +Full display example with board configured: +``` + Board: https://github.com/orgs/snipcodeit/projects/1 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + MGW > PROJECT STATUS: snipcodeit/mgw +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Current Milestone: v2 — Team Collaboration (3/6 done) +Progress: ████████░░░░░░░░ 50% + + #80 ✅ done Add mgw:assign command + #81 ✅ done Post board link to Discussions + #82 ✅ done Add mgw:board sync + #83 🔄 executing Add milestone health report + #84 ⏳ new Create mgw:roadmap command + #85 ⏳ new Add growth analytics + +Milestone Health: + Completed: 3/6 + Velocity: 2.1/day + Blocked: 0 + +Open PRs: + (none matched to this milestone) + +Next Milestone: v3 — Analytics & Extensions (0/5 done) +``` + Rendering rules: +- Print board URL line (` Board: ${BOARD_URL}`) only when BOARD_URL is non-empty - Use stage icons from the issue table - Right-align issue numbers - Truncate titles to 50 chars +- Milestone Health section always appears in project mode (after issue table, before Open PRs) - If no open PRs matched to milestone, show "No open PRs for this milestone." - If no next milestone, show "No more milestones planned." - If `TARGET_MILESTONE != CURRENT_MILESTONE`, add "(viewing milestone ${TARGET_MILESTONE})" to header @@ -282,13 +431,21 @@ import json result = { 'repo': '${REPO_NAME}', - 'current_milestone': ${CURRENT_MILESTONE}, + 'board_url': '${BOARD_URL}', + 'current_milestone': ${CURRENT_MILESTONE}, # 1-indexed (legacy compat) + 'active_milestone_idx': ${CURRENT_MILESTONE_IDX}, # 0-based resolved index 'viewing_milestone': ${TARGET_MILESTONE}, 'milestone': { 'name': '${MILESTONE_NAME}', 'total_issues': ${TOTAL_ISSUES}, 'done': done_count, 'progress_pct': pct, + 'health': { + 'done': int('${HEALTH_DONE}' or '0'), + 'total': ${TOTAL_ISSUES}, + 'blocked': int('${HEALTH_BLOCKED}' or '0'), + 'velocity': '${HEALTH_VELOCITY}' + }, 'issues': issues_with_stages }, 'open_prs': matched_prs, @@ -305,32 +462,40 @@ The JSON structure: ```json { "repo": "owner/repo", + "board_url": "https://github.com/orgs/snipcodeit/projects/1", "current_milestone": 2, + "active_milestone_idx": 1, "viewing_milestone": 2, "milestone": { - "name": "v1 — Pipeline Intelligence", - "total_issues": 4, - "done": 2, + "name": "v2 — Team Collaboration & Lifecycle Orchestration", + "total_issues": 6, + "done": 3, "progress_pct": 50, + "health": { + "done": 3, + "total": 6, + "blocked": 0, + "velocity": "2.1/day" + }, "issues": [ { - "number": 35, - "title": "refactor: remove .planning/ writes", + "number": 80, + "title": "Add mgw:assign command", "pipeline_stage": "done", - "labels": ["refactor"] + "labels": ["enhancement"] } ] }, "open_prs": [ { - "number": 40, - "title": "comment-aware pipeline", - "linked_issue": 36, - "review_status": "review_requested" + "number": 95, + "title": "Add mgw:assign command", + "linked_issue": 80, + "review_status": "approved" } ], "next_milestone": { - "name": "v1 — NPM Publishing & Distribution", + "name": "v3 — Analytics & Extensions", "total_issues": 5, "done": 0 } @@ -351,4 +516,11 @@ The JSON structure: - [ ] Milestone number argument selects non-current milestone - [ ] Read-only: no state modifications, no GitHub writes - [ ] No agent spawns, no side effects +- [ ] Board URL displayed before header when board_url is set in project.json +- [ ] --board flag opens board URL via xdg-open (open on macOS fallback) and exits 0 +- [ ] --board flag exits 1 with helpful error when no board configured +- [ ] Milestone Health section shows Completed N/total, Velocity, and Blocked count +- [ ] Velocity computed from .mgw/active/ and .mgw/completed/ file mtimes +- [ ] --json output includes board_url and milestone.health object +- [ ] Board URL line omitted when board_url is not set in project.json diff --git a/.claude/commands/mgw/sync.md b/.claude/commands/mgw/sync.md index df86c66..1825bdc 100644 --- a/.claude/commands/mgw/sync.md +++ b/.claude/commands/mgw/sync.md @@ -68,6 +68,74 @@ else fi ``` +**GSD milestone consistency check (maps-to links):** + +Read all maps-to links from .mgw/cross-refs.json: + +```bash +MAPS_TO_LINKS=$(python3 -c " +import json +with open('.mgw/cross-refs.json') as f: + data = json.load(f) +links = data.get('links', []) +maps_to = [l for l in links if l.get('type') == 'maps-to'] +print(json.dumps(maps_to)) +") +``` + +For each maps-to link (format: { "a": "milestone:N", "b": "gsd-milestone:id", "type": "maps-to" }): +1. Extract the GitHub milestone number from "a" (parse "milestone:N") +2. Extract the GSD milestone ID from "b" (parse "gsd-milestone:id") +3. Check if this GSD milestone ID appears in either: + - .planning/ROADMAP.md header (active milestone) + - .planning/MILESTONES.md (archived milestones) +4. If found in neither: flag as inconsistent + +```bash +# Check each maps-to link +echo "$MAPS_TO_LINKS" | python3 -c " +import json, sys, os + +links = json.load(sys.stdin) +inconsistent = [] + +for link in links: + a = link.get('a', '') + b = link.get('b', '') + + if not a.startswith('milestone:') or not b.startswith('gsd-milestone:'): + continue + + github_num = a.split(':')[1] + gsd_id = b.split(':', 1)[1] + + found = False + + # Check ROADMAP.md + if os.path.exists('.planning/ROADMAP.md'): + with open('.planning/ROADMAP.md') as f: + content = f.read() + if gsd_id in content: + found = True + + # Check MILESTONES.md + if not found and os.path.exists('.planning/MILESTONES.md'): + with open('.planning/MILESTONES.md') as f: + content = f.read() + if gsd_id in content: + found = True + + if not found: + inconsistent.append({'github_milestone': github_num, 'gsd_id': gsd_id}) + +for i in inconsistent: + print(f\"WARN: GitHub milestone #{i['github_milestone']} maps to GSD milestone '{i['gsd_id']}' which was not found in .planning/\") + +if not inconsistent: + print('GSD milestone links: all consistent') +" +``` + Classify each issue into: - **Completed:** Issue closed AND (PR merged OR no PR expected) - **Stale:** PR merged but issue still open (auto-close missed) @@ -203,6 +271,7 @@ ${HEALTH ? 'GSD Health: ' + HEALTH.status : ''} ${details_for_each_non_active_item} ${comment_drift_details ? 'Unreviewed comments:\n' + comment_drift_details : ''} +${gsd_milestone_consistency ? 'GSD Milestone Links:\n' + gsd_milestone_consistency : ''} ``` @@ -212,10 +281,11 @@ ${comment_drift_details ? 'Unreviewed comments:\n' + comment_drift_details : ''} - [ ] All .mgw/active/ files scanned - [ ] GitHub state checked for each issue, PR, branch - [ ] Comment delta checked for each active issue +- [ ] GSD milestone consistency checked for all maps-to links - [ ] Completed items moved to .mgw/completed/ - [ ] Lingering worktrees cleaned up for completed items - [ ] Branch deletion offered for completed items -- [ ] Stale/orphaned/drift items flagged (including comment drift) +- [ ] Stale/orphaned/drift items flagged (including comment drift and milestone inconsistencies) - [ ] Board reconciliation run — all PR cross-refs checked against board (non-blocking) - [ ] Summary presented diff --git a/.claude/commands/mgw/workflows/gsd.md b/.claude/commands/mgw/workflows/gsd.md index 763d30b..7047404 100644 --- a/.claude/commands/mgw/workflows/gsd.md +++ b/.claude/commands/mgw/workflows/gsd.md @@ -196,6 +196,64 @@ ROADMAP_ANALYSIS=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs roadmap analyz v{X.Y}-REQUIREMENTS.md (archived requirements) ``` +## GSD Debug Pipeline Pattern + +Used by `/mgw:run` when triage recommends `gsd:diagnose-issues`. This route investigates +a bug's root cause before planning a fix. It is a two-step process: diagnose, then fix. + +The GSD debug workflow is `diagnose-issues.md` -- it spawns parallel debug agents per +symptom/gap, each investigating autonomously, returning root causes. + +```bash +# 1. Gather symptoms from the issue body +# Extract: what's broken, error messages, reproduction steps, expected vs actual + +# 2. Create debug directory +mkdir -p .planning/debug + +# 3. Spawn diagnosis agent +Task( + prompt=" + + - ./CLAUDE.md (Project instructions -- if exists, follow all guidelines) + - .planning/STATE.md (if exists) + + + Diagnose the root cause of this bug. + + + Title: ${issue_title} + Body: ${issue_body} + + + + Read the GSD diagnose-issues workflow for your process: + @~/.claude/get-shit-done/workflows/diagnose-issues.md + + Create a debug session file at .planning/debug/${slug}.md + Investigate the codebase to find the root cause. + Return: root cause, evidence, files involved, suggested fix direction. + + ", + subagent_type="general-purpose", + description="Diagnose bug: ${issue_title}" +) + +# 4. After diagnosis, route to quick fix +# Read the debug session file for root cause +# If root cause found: spawn gsd:quick executor with the root cause as context +# If inconclusive: report to user, suggest manual investigation +``` + +**Artifacts created:** +``` +.planning/debug/ + {slug}.md (debug session with root cause) +``` + +After diagnosis, the pipeline continues to the quick execution flow (task 3 in the +existing Quick Pipeline Pattern) with the root cause informing the plan. + ## Utility Patterns GSD tools used by MGW for common operations. diff --git a/.claude/commands/mgw/workflows/state.md b/.claude/commands/mgw/workflows/state.md index 9b0bc88..df4aede 100644 --- a/.claude/commands/mgw/workflows/state.md +++ b/.claude/commands/mgw/workflows/state.md @@ -42,7 +42,17 @@ if [ ! -f "${MGW_DIR}/cross-refs.json" ]; then echo '{"links":[]}' > "${MGW_DIR}/cross-refs.json" fi -# 4. Run staleness check (see Staleness Detection below) +# 4. Migrate project.json schema (idempotent — adds new fields without overwriting) +# This ensures all commands see gsd_milestone_id, gsd_state, active_gsd_milestone, etc. +# Non-blocking: if migration fails for any reason, continue silently. +if [ -f "${MGW_DIR}/project.json" ]; then + node -e " +const { migrateProjectState } = require('./lib/state.cjs'); +try { migrateProjectState(); } catch(e) { /* non-blocking */ } +" 2>/dev/null || true +fi + +# 5. Run staleness check (see Staleness Detection below) # Only if active issues exist — skip for commands that don't need it (e.g., init, help) if ls "${MGW_DIR}/active/"*.json 1>/dev/null 2>&1; then check_staleness "${MGW_DIR}" @@ -216,7 +226,7 @@ File: `.mgw/active/-.json` }, "gsd_route": null, "gsd_artifacts": { "type": null, "path": null }, - "pipeline_stage": "new|triaged|needs-info|needs-security-review|discussing|approved|planning|executing|verifying|pr-created|done|failed|blocked", + "pipeline_stage": "new|triaged|needs-info|needs-security-review|discussing|approved|planning|diagnosing|executing|verifying|pr-created|done|failed|blocked", "comments_posted": [], "linked_pr": null, "linked_issues": [], @@ -237,11 +247,14 @@ needs-security-review --> triaged (re-triage after security ack) triaged --> discussing (new-milestone route, large scope) triaged --> approved (discussion complete, ready for execution) triaged --> planning (direct route, skip discussion) +triaged --> diagnosing (gsd:diagnose-issues route) discussing --> approved (stakeholder approval) approved --> planning planning --> executing +diagnosing --> planning (root cause found, proceeding to fix) +diagnosing --> blocked (investigation inconclusive) executing --> verifying verifying --> pr-created pr-created --> done @@ -301,53 +314,88 @@ if [ -z "$PROJECT_JSON" ]; then exit 1 fi -CURRENT_MILESTONE=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['current_milestone'])") +# Resolve active milestone index — supports both new schema (active_gsd_milestone string) +# and legacy schema (current_milestone 1-indexed integer). +ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +console.log(resolveActiveMilestoneIndex(state)); +") +CURRENT_MILESTONE=$((ACTIVE_IDX + 1)) # 1-indexed for display/legacy compat ``` ### Read Milestone Issues ```bash -MILESTONE_ISSUES=$(echo "$PROJECT_JSON" | python3 -c " -import json,sys -p = json.load(sys.stdin) -m = p['milestones'][p['current_milestone'] - 1] -print(json.dumps(m['issues'], indent=2)) +MILESTONE_ISSUES=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +if (idx < 0) { console.error('No active milestone'); process.exit(1); } +console.log(JSON.stringify(state.milestones[idx].issues || [], null, 2)); ") ``` ### Update Issue Pipeline Stage Used after each `/mgw:run` completion to checkpoint progress. ```bash -python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -milestone = project['milestones'][project['current_milestone'] - 1] -for issue in milestone['issues']: - if issue['github_number'] == ${ISSUE_NUMBER}: - issue['pipeline_stage'] = '${NEW_STAGE}' - break -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) +node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +if (idx < 0) { console.error('No active milestone'); process.exit(1); } +const issue = (state.milestones[idx].issues || []).find(i => i.github_number === ${ISSUE_NUMBER}); +if (issue) { issue.pipeline_stage = '${NEW_STAGE}'; } +writeProjectState(state); " ``` -Valid stages: `new`, `triaged`, `planning`, `executing`, `verifying`, `pr-created`, `done`, `failed`, `blocked`. +Valid stages: `new`, `triaged`, `planning`, `diagnosing`, `executing`, `verifying`, `pr-created`, `done`, `failed`, `blocked`. ### Advance Current Milestone Used after milestone completion to move pointer to next milestone. ```bash -python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -project['current_milestone'] += 1 -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) +node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const currentIdx = resolveActiveMilestoneIndex(state); +const next = (state.milestones || [])[currentIdx + 1]; +if (next) { + state.active_gsd_milestone = next.gsd_milestone_id || null; + if (!state.active_gsd_milestone) { + state.current_milestone = currentIdx + 2; // legacy fallback + } +} else { + state.active_gsd_milestone = null; + state.current_milestone = currentIdx + 2; // past end, signals completion +} +writeProjectState(state); " ``` Only advance if ALL issues in current milestone completed successfully. +### Phase Map Usage + +The `phase_map` in project.json maps GSD phase numbers to their metadata. This is the +bridge between MGW's issue tracking and GSD's phase-based execution: + +```json +{ + "phase_map": { + "1": {"milestone_index": 0, "gsd_route": "plan-phase", "name": "Core Data Models"}, + "2": {"milestone_index": 0, "gsd_route": "plan-phase", "name": "API Endpoints"}, + "3": {"milestone_index": 1, "gsd_route": "plan-phase", "name": "Frontend Components"} + } +} +``` + +Each issue in project.json has a `phase_number` field that indexes into this map. +When `/mgw:run` picks up an issue, it reads the `phase_number` to determine which +GSD phase directory (`.planning/phases/{NN}-{slug}/`) to operate in. + +Issues created outside of `/mgw:project` (e.g., manually filed bugs) will not have +a `phase_number`. In this case, `/mgw:run` falls back to the quick pipeline. + ## Consumers | Pattern | Referenced By | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4871d80 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# MGW -- My GSD Workflow + +## What This Is + +MGW is a GitHub-native issue-to-PR automation system for Claude Code. It orchestrates GSD (Get Shit Done) agents to triage issues, plan work, execute code changes, and create pull requests. + +## Architecture + +MGW is an orchestration layer. It NEVER touches application code directly. + +``` +GitHub (issues, PRs, milestones, labels) + ^ + | reads/writes metadata +MGW (orchestration layer -- .mgw/ state, pipeline stages, agent spawning) + | + | spawns agents, passes context + v +GSD (execution layer -- .planning/ state, PLAN.md, code changes, SUMMARY.md) + | + v +Target Codebase +``` + +### The Delegation Boundary + +MGW orchestrates. MGW never codes. See `workflows/validation.md` for the full rule. + +**MGW may do directly:** +- Read/write `.mgw/` state files +- Read/write GitHub metadata via `gh` CLI +- Spawn `Task()` agents +- Manage git worktrees and branches +- Display output to users + +**MGW must NEVER do directly:** +- Read application source code +- Write application source code +- Make implementation decisions +- Analyze code for scope or security (spawn an agent for this) + +### Vision Collaboration Cycle (Fresh Projects) + +When `mgw:project` detects a Fresh state (no existing GSD or GitHub state), it runs a 6-stage vision cycle before creating any GitHub structure: + +1. **Intake** — freeform project description from user +2. **Domain Expansion** — `vision-researcher` Task agent produces `.mgw/vision-research.json` +3. **Structured Questioning** — 3-8 rounds (soft cap), 15 max (hard cap); decisions appended to `.mgw/vision-draft.md` +4. **Vision Synthesis** — `vision-synthesizer` Task agent produces `.mgw/vision-brief.json` (schema: `templates/vision-brief-schema.json`) +5. **Review** — user accepts or requests revisions (loops back to synthesis) +6. **Condense** — `vision-condenser` Task agent produces `.mgw/vision-handoff.md` for `gsd:new-project` spawn + +Context strategy: rolling summary only. Agents receive Vision Brief + latest delta, never full transcript. + +### Key Directories + +| Directory | Owner | Purpose | +|-----------|-------|---------| +| `.mgw/` | MGW | Pipeline state, cross-refs, project.json | +| `.mgw/vision-*.json` | Vision Brief artifacts (runtime, gitignored): `vision-research.json`, `vision-brief.json`, `vision-handoff.md`, `vision-draft.md`, `alignment-report.json`, `drift-report.json` | +| `.planning/` | GSD | ROADMAP.md, STATE.md, config.json, phase plans | +| `commands/` | MGW | Slash command definitions (mirrored to .claude/commands/mgw/) | +| `workflows/` | MGW | Shared workflow patterns referenced by commands | +| `lib/` | MGW | Node.js utilities (template-loader, github, state, etc.) | +| `templates/` | MGW | JSON schema for project templates | + +### Coding Conventions + +- Commands are markdown files with XML structure (``, ``, ``) +- All bash in commands is pseudocode -- it shows the pattern, not runnable scripts +- Every `Task()` spawn MUST include the CLAUDE.md injection block (see `workflows/gsd.md`) +- Model names are NEVER hardcoded -- resolve via `gsd-tools.cjs resolve-model` +- State files use JSON format +- Slug generation uses `gsd-tools.cjs generate-slug` with 40-char truncation +- Timestamps use `gsd-tools.cjs current-timestamp` + +### Command Surface + +| Command | Purpose | Modifies State? | +|---------|---------|-----------------| +| `project` | Initialize project -- create GitHub milestones/issues from ROADMAP.md | Yes (.mgw/project.json) | +| `run` | Autonomous pipeline -- triage through execution to PR | Yes (.mgw/active/) | +| `issue` | Deep-triage a single issue | Yes (.mgw/active/) | +| `milestone` | Execute all issues in a milestone | Yes (.mgw/project.json) | +| `board` | Create/configure/sync GitHub Projects v2 board | Yes (.mgw/project.json) | +| `assign` | Claim/reassign issues | No | +| `ask` | Classify a question/observation | No | +| `init` | Bootstrap .mgw/ directory | Yes (.mgw/) | +| `next` | Find next unblocked issue | No | +| `pr` | Create PR from GSD artifacts | Yes (.mgw/active/) | +| `review` | Classify new comments | No | +| `status` | Project status dashboard | No | +| `sync` | Reconcile .mgw/ with GitHub | Yes (.mgw/) | +| `update` | Post structured status comment | No | +| `link` | Cross-reference issues/PRs/branches | Yes (.mgw/cross-refs.json) | +| `help` | Show commands | No | + +### Testing + +There are currently no automated tests. When adding new lib/ functions, verify they work by running them with `node` directly. For command changes, test by running the command against the MGW repo itself or a test repo. + +### GSD Integration Points + +- `gsd-tools.cjs` provides: slug generation, timestamps, model resolution, roadmap analysis, init contexts, commit utility, progress display, summary extraction, health checks +- GSD workflows live at `~/.claude/get-shit-done/workflows/` +- GSD agents are typed: `gsd-planner`, `gsd-executor`, `gsd-verifier`, `gsd-plan-checker`, `general-purpose` +- The GSD debug/diagnosis workflow is `diagnose-issues.md` (spawns parallel debug agents per UAT gap) diff --git a/commands/assign.md b/commands/assign.md index 33e73d0..6d35472 100644 --- a/commands/assign.md +++ b/commands/assign.md @@ -124,6 +124,37 @@ echo "MGW: Assigning #${ISSUE_NUMBER} to @${RESOLVED_USER}..." ``` + +**Build the Co-Authored-By tag for the assignee:** + +GitHub assigns every account a noreply email: `{id}+{login}@users.noreply.github.com`. +This gets stored in the active state file so GSD commits for this issue use the right tag. +Falls back to the project-level default in `project.json` if resolution fails. + +```bash +# Fetch assignee's GitHub ID and display name +ASSIGNEE_DATA=$(gh api "users/${RESOLVED_USER}" --jq '{id: .id, name: .name}' 2>/dev/null) +ASSIGNEE_ID=$(echo "$ASSIGNEE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" 2>/dev/null) +ASSIGNEE_NAME=$(echo "$ASSIGNEE_DATA" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['name'] or d.get('login',''))" 2>/dev/null) + +if [ -n "$ASSIGNEE_ID" ]; then + COAUTHOR_TAG="${ASSIGNEE_NAME} <${ASSIGNEE_ID}+${RESOLVED_USER}@users.noreply.github.com>" +else + # Fall back to project-level default + COAUTHOR_TAG=$(python3 -c " +import json +try: + p = json.load(open('${MGW_DIR}/project.json')) + print(p.get('project', {}).get('coauthor', '')) +except: + print('') +" 2>/dev/null || echo "") +fi + +echo "MGW: Co-author tag: ${COAUTHOR_TAG}" +``` + + **Fetch issue metadata from GitHub:** @@ -173,12 +204,13 @@ TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --r || date -u +"%Y-%m-%dT%H:%M:%S.000Z") if [ "$STATE_EXISTS" = "true" ]; then - # Update existing state file: set issue.assignee field + # Update existing state file: set issue.assignee and coauthor fields python3 -c " import json with open('${STATE_FILE}') as f: state = json.load(f) state['issue']['assignee'] = '${RESOLVED_USER}' +state['coauthor'] = '${COAUTHOR_TAG}' state['updated_at'] = '${TIMESTAMP}' with open('${STATE_FILE}', 'w') as f: json.dump(state, f, indent=2) @@ -202,6 +234,7 @@ state = { 'labels': [], 'assignee': '${RESOLVED_USER}' }, + 'coauthor': '${COAUTHOR_TAG}', 'triage': { 'scope': { 'size': 'unknown', 'file_count': 0, 'files': [], 'systems': [] }, 'validity': 'pending', diff --git a/commands/link.md b/commands/link.md index 040400f..eb81d74 100644 --- a/commands/link.md +++ b/commands/link.md @@ -19,6 +19,8 @@ Reference formats: - Issue: 42 or #42 or issue:42 - PR: pr:15 or pr:#15 - Branch: branch:fix/auth-42 +- GitHub Milestone: milestone:N +- GSD Milestone: gsd-milestone:name @@ -39,6 +41,8 @@ Normalize reference formats: - Bare number or #N → "issue:N" - pr:N or pr:#N → "pr:N" - branch:name → "branch:name" +- milestone:N → "milestone:N" (GitHub milestone by number) +- gsd-milestone:name → "gsd-milestone:name" (GSD milestone by id/name) If fewer than 2 refs provided: ``` @@ -68,6 +72,7 @@ Determine link type: - issue + pr → "implements" - issue + branch → "tracks" - pr + branch → "tracks" +- milestone + gsd-milestone → "maps-to" (maps GitHub milestone to GSD milestone) Check for duplicate (same a+b pair exists). If duplicate, report and skip. diff --git a/commands/milestone.md b/commands/milestone.md index 1331db4..7e8195b 100644 --- a/commands/milestone.md +++ b/commands/milestone.md @@ -67,7 +67,17 @@ if [ -z "$MILESTONE_NUM" ]; then echo "No project initialized. Run /mgw:project first." exit 1 fi - MILESTONE_NUM=$(python3 -c "import json; print(json.load(open('${MGW_DIR}/project.json'))['current_milestone'])") + # Resolve active milestone index (0-based) and convert to 1-indexed milestone number + ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +console.log(resolveActiveMilestoneIndex(state)); +") + if [ "$ACTIVE_IDX" -lt 0 ]; then + echo "No active milestone set. Run /mgw:project to initialize or set active_gsd_milestone." + exit 1 + fi + MILESTONE_NUM=$((ACTIVE_IDX + 1)) fi ``` @@ -382,17 +392,15 @@ if [ "$IN_PROGRESS_COUNT" -gt 0 ]; then fi # Reset pipeline_stage to 'new' (will be re-run from scratch) - python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -milestone = project['milestones'][project['current_milestone'] - 1] -for issue in milestone['issues']: - if issue['github_number'] == ${ISSUE_NUM}: - issue['pipeline_stage'] = 'new' - break -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) + node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +if (idx < 0) { console.error('No active milestone'); process.exit(1); } +const milestone = state.milestones[idx]; +const issue = (milestone.issues || []).find(i => i.github_number === ${ISSUE_NUM}); +if (issue) { issue.pipeline_stage = 'new'; } +writeProjectState(state); " done fi @@ -636,17 +644,15 @@ COMMENTEOF # Update project.json checkpoint (MLST-05) STAGE=$([ -n "$PR_NUMBER" ] && echo "done" || echo "failed") - python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -milestone = project['milestones'][project['current_milestone'] - 1] -for issue in milestone['issues']: - if issue['github_number'] == ${ISSUE_NUMBER}: - issue['pipeline_stage'] = '${STAGE}' - break -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) + node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +if (idx < 0) { console.error('No active milestone'); process.exit(1); } +const milestone = state.milestones[idx]; +const issue = (milestone.issues || []).find(i => i.github_number === ${ISSUE_NUMBER}); +if (issue) { issue.pipeline_stage = '${STAGE}'; } +writeProjectState(state); " ISSUES_RUN=$((ISSUES_RUN + 1)) @@ -762,19 +768,112 @@ Milestone: ${MILESTONE_NAME} fi ``` -4. Advance current_milestone in project.json: +4. Advance active milestone pointer in project.json: ```bash -python3 -c " -import json -with open('${MGW_DIR}/project.json') as f: - project = json.load(f) -project['current_milestone'] += 1 -with open('${MGW_DIR}/project.json', 'w') as f: - json.dump(project, f, indent=2) +node -e " +const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +const currentIdx = resolveActiveMilestoneIndex(state); +const nextMilestone = (state.milestones || [])[currentIdx + 1]; +if (nextMilestone) { + // New schema: point active_gsd_milestone at the next milestone's gsd_milestone_id + state.active_gsd_milestone = nextMilestone.gsd_milestone_id || null; + // Backward compat: if next milestone has no gsd_milestone_id, fall back to legacy integer + if (!state.active_gsd_milestone) { + state.current_milestone = currentIdx + 2; // next 1-indexed + } +} else { + // All milestones complete — clear the active pointer + state.active_gsd_milestone = null; + state.current_milestone = currentIdx + 2; // past end, signals completion +} +writeProjectState(state); " ``` -5. Display completion banner: +5. Milestone mapping verification: + +After advancing to the next milestone, check its GSD linkage: + +```bash +NEXT_MILESTONE_CHECK=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +const activeIdx = resolveActiveMilestoneIndex(state); + +if (activeIdx < 0 || activeIdx >= state.milestones.length) { + console.log('none'); + process.exit(0); +} + +const nextMilestone = state.milestones[activeIdx]; +if (!nextMilestone) { + console.log('none'); + process.exit(0); +} + +const gsdId = nextMilestone.gsd_milestone_id; +const name = nextMilestone.name; + +if (!gsdId) { + console.log('unlinked:' + name); +} else { + console.log('linked:' + name + ':' + gsdId); +} +") + +case "$NEXT_MILESTONE_CHECK" in + none) + echo "All milestones complete — project is done!" + ;; + unlinked:*) + NEXT_NAME=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f2-) + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Next milestone '${NEXT_NAME}' has no GSD milestone linked." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Before running /mgw:milestone for the next milestone:" + echo " 1) Run /gsd:new-milestone to create GSD state for '${NEXT_NAME}'" + echo " 2) Run /mgw:project extend to link the new GSD milestone" + echo "" + ;; + linked:*) + NEXT_NAME=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f2) + GSD_ID=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f3) + # Verify ROADMAP.md matches expected GSD milestone + ROADMAP_CHECK=$(python3 -c " +import os, sys +if not os.path.exists('.planning/ROADMAP.md'): + print('no_roadmap') + sys.exit() +with open('.planning/ROADMAP.md') as f: + content = f.read() +if '${GSD_ID}' in content: + print('match') +else: + print('mismatch') +" 2>/dev/null || echo "no_roadmap") + + case "$ROADMAP_CHECK" in + match) + echo "Next milestone '${NEXT_NAME}' (GSD: ${GSD_ID}) — ROADMAP.md is ready." + ;; + mismatch) + echo "Next milestone '${NEXT_NAME}' links to GSD milestone '${GSD_ID}'" + echo " but .planning/ROADMAP.md does not contain that milestone ID." + echo " Run /gsd:new-milestone to update ROADMAP.md before proceeding." + ;; + no_roadmap) + echo "NOTE: Next milestone '${NEXT_NAME}' (GSD: ${GSD_ID}) linked." + echo " No .planning/ROADMAP.md found — run /gsd:new-milestone when ready." + ;; + esac + ;; +esac +``` + +6. Display completion banner: ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ MGW ► MILESTONE ${MILESTONE_NUM} COMPLETE ✓ @@ -803,7 +902,7 @@ Draft release created: ${RELEASE_TAG} ─────────────────────────────────────────────────────────────── ``` -6. Check if next milestone exists and offer auto-advance (only if no failures in current). +7. Check if next milestone exists and offer auto-advance (only if no failures in current). **If some issues failed:** @@ -826,7 +925,7 @@ Milestone NOT closed. Resolve failures and re-run: /mgw:milestone ${MILESTONE_NUM} ``` -7. Post final results table as GitHub comment on the first issue in the milestone: +8. Post final results table as GitHub comment on the first issue in the milestone: ```bash gh issue comment ${FIRST_ISSUE_NUMBER} --body "$FINAL_RESULTS_COMMENT" ``` diff --git a/commands/next.md b/commands/next.md index 2b3d76d..6cf9fe4 100644 --- a/commands/next.md +++ b/commands/next.md @@ -38,14 +38,20 @@ if [ ! -f "${MGW_DIR}/project.json" ]; then fi PROJECT_JSON=$(cat "${MGW_DIR}/project.json") -CURRENT_MILESTONE=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['current_milestone'])") + +# Resolve active milestone index using state resolution (supports both schema versions) +ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +console.log(resolveActiveMilestoneIndex(state)); +") # Get milestone data MILESTONE_DATA=$(echo "$PROJECT_JSON" | python3 -c " import json,sys p = json.load(sys.stdin) -idx = p['current_milestone'] - 1 -if idx >= len(p['milestones']): +idx = ${ACTIVE_IDX} +if idx < 0 or idx >= len(p['milestones']): print(json.dumps({'error': 'No more milestones'})) sys.exit(0) m = p['milestones'][idx] diff --git a/commands/project.md b/commands/project.md index e25bd06..5302ad4 100644 --- a/commands/project.md +++ b/commands/project.md @@ -17,9 +17,10 @@ issues scaffolded from AI-generated project-specific content, dependencies label state persisted. The developer never leaves Claude Code and never does project management manually. -MGW does NOT write to .planning/ — that directory is owned by GSD. If a project needs -a ROADMAP.md or other GSD files, run the appropriate GSD command (e.g., /gsd:new-milestone) -after project initialization. +MGW does NOT write to .planning/ directly — that directory is owned by GSD. For Fresh +projects, MGW spawns a gsd:new-project Task agent (spawn_gsd_new_project step) which creates +.planning/PROJECT.md and .planning/ROADMAP.md as part of the vision cycle. For non-Fresh +projects with existing GSD state, .planning/ is already populated before this command runs. This command creates structure only. It does NOT trigger execution. Run /mgw:milestone to begin executing the first milestone. @@ -44,11 +45,63 @@ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) If not a git repo → error: "Not a git repository. Run from a repo root." If no GitHub remote → error: "No GitHub remote found. MGW requires a GitHub repo." -**Check for existing project initialization:** +**Initialize .mgw/ state (from state.md validate_and_load):** + +```bash +MGW_DIR="${REPO_ROOT}/.mgw" +mkdir -p "${MGW_DIR}/active" "${MGW_DIR}/completed" + +for ENTRY in ".mgw/" ".worktrees/"; do + if ! grep -q "^${ENTRY}$" "${REPO_ROOT}/.gitignore" 2>/dev/null; then + echo "${ENTRY}" >> "${REPO_ROOT}/.gitignore" + fi +done + +if [ ! -f "${MGW_DIR}/cross-refs.json" ]; then + echo '{"links":[]}' > "${MGW_DIR}/cross-refs.json" +fi +``` + + + +**Detect existing project state from five signal sources:** + +Check five signals to determine what already exists for this project: ```bash -if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then - # Check if all milestones are complete +# Signal checks +P=false # .planning/PROJECT.md exists +R=false # .planning/ROADMAP.md exists +S=false # .planning/STATE.md exists +M=false # .mgw/project.json exists +G=0 # GitHub milestone count + +[ -f "${REPO_ROOT}/.planning/PROJECT.md" ] && P=true +[ -f "${REPO_ROOT}/.planning/ROADMAP.md" ] && R=true +[ -f "${REPO_ROOT}/.planning/STATE.md" ] && S=true +[ -f "${REPO_ROOT}/.mgw/project.json" ] && M=true + +G=$(gh api "repos/${REPO}/milestones" --jq 'length' 2>/dev/null || echo 0) +``` + +**Classify into STATE_CLASS:** + +| State | P | R | S | M | G | Meaning | +|---|---|---|---|---|---|---| +| Fresh | false | false | false | false | 0 | Clean slate — no GSD, no MGW | +| GSD-Only | true | false | false | false | 0 | PROJECT.md present but no roadmap yet | +| GSD-Mid-Exec | true | true | true | false | 0 | GSD in progress, MGW not yet linked | +| Aligned | true | — | — | true | >0 | Both MGW + GitHub consistent with each other | +| Diverged | — | — | — | true | >0 | MGW + GitHub present but inconsistent | +| Extend | true | — | — | true | >0 | All milestones in project.json are done | + +```bash +# Classification logic +STATE_CLASS="Fresh" +EXTEND_MODE=false + +if [ "$M" = "true" ] && [ "$G" -gt 0 ]; then + # Check if all milestones are complete (Extend detection) ALL_COMPLETE=$(python3 -c " import json p = json.load(open('${REPO_ROOT}/.mgw/project.json')) @@ -61,39 +114,814 @@ print('true' if all_done else 'false') ") if [ "$ALL_COMPLETE" = "true" ]; then + STATE_CLASS="Extend" 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 + # M=true, G>0, not all done — check consistency (Aligned vs Diverged) + GH_MILESTONE_COUNT=$G + LOCAL_MILESTONE_COUNT=$(python3 -c "import json; print(len(json.load(open('${REPO_ROOT}/.mgw/project.json')).get('milestones', [])))") + + # Consistency: milestone counts match and names overlap + CONSISTENCY_OK=$(python3 -c " +import json, subprocess, sys +local = json.load(open('${REPO_ROOT}/.mgw/project.json')) +local_names = set(m['name'] for m in local.get('milestones', [])) +local_count = len(local_names) +gh_count = ${GH_MILESTONE_COUNT} + +# Count mismatch is a drift signal (allow off-by-one for in-flight) +if abs(local_count - gh_count) > 1: + print('false') + sys.exit(0) + +# Name overlap check: at least 50% of local milestone names found on GitHub +result = subprocess.run( + ['gh', 'api', 'repos/${REPO}/milestones', '--jq', '[.[].title]'], + capture_output=True, text=True +) +try: + gh_names = set(json.loads(result.stdout)) + overlap = len(local_names & gh_names) + print('true' if overlap >= max(1, local_count // 2) else 'false') +except Exception: + print('false') +") + + if [ "$CONSISTENCY_OK" = "true" ]; then + STATE_CLASS="Aligned" + else + STATE_CLASS="Diverged" + fi + fi +elif [ "$M" = "false" ] && [ "$G" -eq 0 ]; then + # No MGW state, no GitHub milestones — GSD signals determine class + if [ "$P" = "true" ] && [ "$R" = "true" ] && [ "$S" = "true" ]; then + STATE_CLASS="GSD-Mid-Exec" + elif [ "$P" = "true" ] && [ "$R" = "true" ]; then + STATE_CLASS="GSD-Mid-Exec" + elif [ "$P" = "true" ]; then + STATE_CLASS="GSD-Only" + else + STATE_CLASS="Fresh" fi fi + +echo "State detected: ${STATE_CLASS} (P=${P} R=${R} S=${S} M=${M} G=${G})" ``` -**Initialize .mgw/ state (from state.md validate_and_load):** +**Route by STATE_CLASS:** ```bash -MGW_DIR="${REPO_ROOT}/.mgw" -mkdir -p "${MGW_DIR}/active" "${MGW_DIR}/completed" +case "$STATE_CLASS" in + "Fresh") + # Proceed to gather_inputs (standard flow) + ;; + + "GSD-Only"|"GSD-Mid-Exec") + # GSD artifacts exist but MGW not initialized — delegate to align_from_gsd + # (proceed to align_from_gsd step) + ;; + + "Aligned") + # MGW + GitHub consistent — display status and offer extend mode + TOTAL_ISSUES=$(python3 -c " +import json +p = json.load(open('${REPO_ROOT}/.mgw/project.json')) +print(sum(len(m.get('issues', [])) for m in p.get('milestones', []))) +") + echo "" + echo "Project already initialized and aligned with GitHub." + echo " Milestones: ${LOCAL_MILESTONE_COUNT} local / ${GH_MILESTONE_COUNT} on GitHub" + echo " Issues: ${TOTAL_ISSUES} tracked in project.json" + echo "" + echo "What would you like to do?" + echo "" + echo " 1) Continue with /mgw:milestone (execute next milestone)" + echo " 2) Add new milestones to this project (extend mode)" + echo " 3) View full status (/mgw:status)" + echo "" + read -p "Choose [1/2/3]: " ALIGNED_CHOICE + case "$ALIGNED_CHOICE" in + 2) + echo "" + echo "Entering extend mode — new milestones will be added to the existing project." + EXTEND_MODE=true + EXISTING_MILESTONE_COUNT=${LOCAL_MILESTONE_COUNT} + EXISTING_PHASE_COUNT=$(python3 -c " +import json +p = json.load(open('${REPO_ROOT}/.mgw/project.json')) +print(sum(len(m.get('phases', [])) for m in p.get('milestones', []))) +") + echo "Phase numbering will continue from phase ${EXISTING_PHASE_COUNT}." + # Fall through to gather_inputs — do NOT exit + ;; + 3) + echo "" + echo "Run /mgw:status to view the full project status dashboard." + exit 0 + ;; + *) + echo "" + echo "Run /mgw:milestone to execute the next milestone." + exit 0 + ;; + esac + ;; + + "Diverged") + # MGW + GitHub inconsistent — delegate to reconcile_drift + # (proceed to reconcile_drift step) + ;; + + "Extend") + # All milestones done — entering extend mode + echo "All ${EXISTING_MILESTONE_COUNT} milestones complete. Entering extend mode." + echo "Phase numbering will continue from phase ${EXISTING_PHASE_COUNT}." + # Proceed to gather_inputs in extend mode (EXTEND_MODE=true already set) + ;; +esac +``` + -for ENTRY in ".mgw/" ".worktrees/"; do - if ! grep -q "^${ENTRY}$" "${REPO_ROOT}/.gitignore" 2>/dev/null; then - echo "${ENTRY}" >> "${REPO_ROOT}/.gitignore" - fi -done + +**Align MGW state from existing GSD artifacts (STATE_CLASS = GSD-Only or GSD-Mid-Exec):** + +Spawn alignment-analyzer agent: + +Task( + description="Analyze GSD state for alignment", + subagent_type="general-purpose", + prompt=" + +- ./CLAUDE.md +- .planning/PROJECT.md (if exists) +- .planning/ROADMAP.md (if exists) +- .planning/MILESTONES.md (if exists) +- .planning/STATE.md (if exists) + + +Analyze existing GSD project state and produce an alignment report. + +Read each file that exists. Extract: +- Project name and description from PROJECT.md (H1 heading, description paragraph) +- Active milestone: from ROADMAP.md header or STATE.md current milestone name +- Archived milestones: from MILESTONES.md — list each milestone with name and phase count +- Phases per milestone: from ROADMAP.md sections (### Phase N:) and MILESTONES.md + +For each milestone found: +- name: milestone name string +- source: 'ROADMAP' (if from current ROADMAP.md) or 'MILESTONES' (if archived) +- state: 'active' (ROADMAP source), 'completed' (archived in MILESTONES.md), 'planned' (referenced but not yet created) +- phases: array of { number, name, status } objects + + +Write JSON to .mgw/alignment-report.json: +{ + \"project_name\": \"extracted from PROJECT.md\", + \"project_description\": \"extracted from PROJECT.md\", + \"milestones\": [ + { + \"name\": \"milestone name\", + \"source\": \"ROADMAP|MILESTONES\", + \"state\": \"active|completed|planned\", + \"phases\": [{ \"number\": N, \"name\": \"...\", \"status\": \"...\" }] + } + ], + \"active_milestone\": \"name of currently active milestone or null\", + \"total_phases\": N, + \"total_issues_estimated\": N +} + +" +) + +After agent completes: +1. Read .mgw/alignment-report.json +2. Display alignment summary to user: + - Project: {project_name} + - Milestones found: {count} ({active_milestone} active, N completed) + - Phases: {total_phases} total, ~{total_issues_estimated} issues estimated +3. Ask: "Import this GSD state into MGW? This will create GitHub milestones and issues, and build project.json. (Y/N)" +4. If Y: proceed to step milestone_mapper +5. If N: exit with message "Run /mgw:project again when ready to import." + -if [ ! -f "${MGW_DIR}/cross-refs.json" ]; then - echo '{"links":[]}' > "${MGW_DIR}/cross-refs.json" -fi + +**Map GSD milestones to GitHub milestones:** + +Read .mgw/alignment-report.json produced by the alignment-analyzer agent. + +```bash +ALIGNMENT=$(python3 -c " +import json +with open('.mgw/alignment-report.json') as f: + data = json.load(f) +print(json.dumps(data)) +") ``` + +For each milestone in the alignment report: +1. Check if a GitHub milestone with a matching title already exists: + ```bash + gh api repos/${REPO}/milestones --jq '.[].title' + ``` +2. If not found: create it: + ```bash + gh api repos/${REPO}/milestones -X POST \ + -f title="${MILESTONE_NAME}" \ + -f description="Imported from GSD: ${MILESTONE_SOURCE}" \ + -f state="open" + ``` + Capture the returned `number` as GITHUB_MILESTONE_NUMBER. +3. If found: use the existing milestone's number. +4. For each phase in the milestone: create GitHub issues (one per phase, title = phase name, body includes phase goals and gsd_route). Use the same issue creation pattern as the existing `create_issues` step. +5. Add project.json entry for this milestone using the new schema fields: + ```json + { + "github_number": GITHUB_MILESTONE_NUMBER, + "name": milestone_name, + "gsd_milestone_id": null, + "gsd_state": "active|completed based on alignment report state", + "roadmap_archived_at": null + } + ``` +6. Add maps-to cross-ref entry: + ```bash + # Append to .mgw/cross-refs.json + TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ") + # Add entry: { "a": "milestone:${GITHUB_NUMBER}", "b": "gsd-milestone:${GSD_ID}", "type": "maps-to", "created": "${TIMESTAMP}" } + ``` + +After all milestones are mapped: +- Write updated project.json with all milestone entries and new schema fields +- Set active_gsd_milestone to the name of the 'active' milestone from alignment report +- Display mapping summary: + ``` + Mapped N GSD milestones → GitHub milestones: + ✓ "Milestone Name" → #N (created/existing) + ... + cross-refs.json updated with N maps-to entries + ``` +- Proceed to create_project_board step (existing step — reused for new project) + + + +**Reconcile diverged state (STATE_CLASS = Diverged):** + +Spawn drift-analyzer agent: + +Task( + description="Analyze project state drift", + subagent_type="general-purpose", + prompt=" + +- ./CLAUDE.md +- .mgw/project.json + + +Compare .mgw/project.json with live GitHub state. + +1. Read project.json: parse milestones array, get repo name from project.repo +2. Query GitHub milestones: + gh api repos/{REPO}/milestones --jq '.[] | {number, title, state, open_issues, closed_issues}' +3. For each milestone in project.json: + - Does a GitHub milestone with matching title exist? (fuzzy: case-insensitive, strip emoji) + - If no match: flag as missing_github + - If match: compare issue count (open + closed GitHub vs issues array length) +4. For each GitHub milestone NOT matched to project.json entry: flag as missing_local +5. For issues: check pipeline_stage vs GitHub issue state + - GitHub closed + local not 'done' or 'pr-created': flag as stage_mismatch + + +Write JSON to .mgw/drift-report.json: +{ + \"mismatches\": [ + {\"type\": \"missing_github\", \"milestone_name\": \"...\", \"local_issue_count\": N, \"action\": \"create_github_milestone\"}, + {\"type\": \"missing_local\", \"github_number\": N, \"github_title\": \"...\", \"action\": \"import_to_project_json\"}, + {\"type\": \"count_mismatch\", \"milestone_name\": \"...\", \"local\": N, \"github\": M, \"action\": \"review_manually\"}, + {\"type\": \"stage_mismatch\", \"issue\": N, \"local_stage\": \"...\", \"github_state\": \"closed\", \"action\": \"update_local_stage\"} + ], + \"summary\": \"N mismatches found across M milestones\" +} + +" +) + +After agent completes: +1. Read .mgw/drift-report.json +2. Display mismatches as a table: + + | Type | Detail | Suggested Action | + |------|--------|-----------------| + | missing_github | Milestone: {name} ({N} local issues) | Create GitHub milestone | + | missing_local | GitHub #N: {title} | Import to project.json | + | count_mismatch | {name}: local={N}, github={M} | Review manually | + | stage_mismatch | Issue #{N}: local={stage}, github=closed | Update local stage to done | + +3. If no mismatches: echo "No drift detected — state is consistent. Reclassifying as Aligned." and proceed to report alignment status. +4. If mismatches: Ask "Apply auto-fixes? Options: (A)ll / (S)elective / (N)one" + - All: apply each action (create missing milestones, update stages in project.json) + - Selective: present each fix individually, Y/N per item + - None: exit with "Drift noted. Run /mgw:sync to reconcile later." +5. After applying fixes: write updated project.json and display summary. + + + +**Intake: capture the raw project idea (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Display to user: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + MGW ► VISION CYCLE — Let's Build Your Project +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Tell me about the project you want to build. Don't worry +about being complete or precise — just describe the idea, +the problem you're solving, and who it's for. +``` + +Capture freeform user input as RAW_IDEA. + +```bash +TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ") +``` + +Save to `.mgw/vision-draft.md`: +```markdown +--- +current_stage: intake +rounds_completed: 0 +soft_cap_reached: false +--- + +# Vision Draft + +## Intake +**Raw Idea:** {RAW_IDEA} +**Captured:** {TIMESTAMP} +``` + +Proceed to vision_research step. + + + +**Domain Expansion: spawn vision-researcher agent (silent)** + +If STATE_CLASS != Fresh: skip this step. + +Spawn vision-researcher Task agent: + +Task( + description="Research project domain and platform requirements", + subagent_type="general-purpose", + prompt=" +You are a domain research agent for a new software project. + +Raw idea from user: +{RAW_IDEA} + +Research this project idea and produce a domain analysis. Write your output to .mgw/vision-research.json. + +Your analysis must include: + +1. **domain_analysis**: What does this domain actually require to succeed? + - Core capabilities users expect + - Table stakes vs differentiators + - Common failure modes in this domain + +2. **platform_requirements**: Specific technical/integration needs + - APIs, third-party services the domain typically needs + - Compliance or regulatory considerations + - Platform targets (mobile, web, desktop, API-only) + +3. **competitive_landscape**: What similar solutions exist? + - 2-3 examples with their key approaches + - Gaps in existing solutions that this could fill + +4. **risk_factors**: Common failure modes for this type of project + - Technical risks + - Business/adoption risks + - Scope creep patterns in this domain + +5. **suggested_questions**: 6-10 targeted questions to ask the user + - Prioritized by most impactful for scoping + - Each question should clarify a decision that affects architecture or milestone structure + - Format: [{\"question\": \"...\", \"why_it_matters\": \"...\"}, ...] + +Output format — write to .mgw/vision-research.json: +{ + \"domain_analysis\": {\"core_capabilities\": [...], \"differentiators\": [...], \"failure_modes\": [...]}, + \"platform_requirements\": [...], + \"competitive_landscape\": [{\"name\": \"...\", \"approach\": \"...\"}], + \"risk_factors\": [...], + \"suggested_questions\": [{\"question\": \"...\", \"why_it_matters\": \"...\"}] +} +" +) + +After agent completes: +- Read .mgw/vision-research.json +- Append research summary to .mgw/vision-draft.md: + ```markdown + ## Domain Research (silent) + - Domain: {domain from analysis} + - Key platform requirements: {top 3} + - Risks identified: {count} + - Questions generated: {count} + ``` +- Update vision-draft.md frontmatter: current_stage: questioning +- Proceed to vision_questioning step. + + + +**Structured Questioning Loop (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Read .mgw/vision-research.json to get suggested_questions. +Read .mgw/vision-draft.md to get current state. + +Initialize loop: +```bash +ROUND=0 +SOFT_CAP=8 +HARD_CAP=15 +SOFT_CAP_REACHED=false +``` + +**Questioning loop:** + +Each round: + +1. Load questions remaining from .mgw/vision-research.json suggested_questions (dequeue used ones). + Also allow orchestrator to generate follow-up questions based on previous answers. + +2. Present 2-4 questions to user (never more than 4 per round): + ``` + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vision Cycle — Round {N} of {SOFT_CAP} + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + 1) {question_1} + 2) {question_2} + 3) {question_3} + + (Answer all, some, or type 'done' to proceed to synthesis) + ``` + +3. Capture user answers as ANSWERS_ROUND_N. + +4. Append round to .mgw/vision-draft.md: + ```markdown + ## Round {N} — {TIMESTAMP} + **Questions asked:** + 1. {q1} + 2. {q2} + + **Answers:** + {ANSWERS_ROUND_N} + + **Key decisions extracted:** + - {decision_1} + - {decision_2} + ``` + (Key decisions: orchestrator extracts 1-3 concrete decisions from answers inline — no agent spawn needed) + +5. Increment ROUND. + Update .mgw/vision-draft.md frontmatter: rounds_completed={ROUND} + +6. **Soft cap check** (after round {SOFT_CAP}): + If ROUND >= SOFT_CAP and !SOFT_CAP_REACHED: + Set SOFT_CAP_REACHED=true + Update vision-draft.md frontmatter: soft_cap_reached=true + Display: + ``` + ───────────────────────────────────── + We've covered {ROUND} rounds of questions. + + Options: + D) Dig deeper — continue questioning (up to {HARD_CAP} rounds total) + S) Synthesize — proceed to Vision Brief generation + ───────────────────────────────────── + ``` + If user chooses S: exit loop and proceed to vision_synthesis + If user chooses D: continue loop + +7. **Hard cap** (ROUND >= HARD_CAP): automatically exit loop with notice: + ``` + Reached {HARD_CAP}-round limit. Proceeding to synthesis. + ``` + +8. **User 'done'**: if user types 'done' as answer: exit loop immediately. + +After loop exits: +- Update vision-draft.md frontmatter: current_stage: synthesizing +- Display: "Questioning complete ({ROUND} rounds). Generating Vision Brief..." +- Proceed to vision_synthesis step. + + + +**Vision Synthesis: spawn vision-synthesizer agent and review loop (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Display: "Generating Vision Brief from {rounds_completed} rounds of input..." + +**Synthesizer spawn:** + +Task( + description="Synthesize Vision Brief from research and questioning", + subagent_type="general-purpose", + prompt=" +You are the vision-synthesizer agent for a software project planning cycle. + +Read these files: +- .mgw/vision-draft.md — all rounds of user questions and answers, raw idea +- .mgw/vision-research.json — domain research, platform requirements, risks + +Synthesize a comprehensive Vision Brief. Write it to .mgw/vision-brief.json using this schema (templates/vision-brief-schema.json): + +{ + \"project_identity\": { \"name\": \"...\", \"tagline\": \"...\", \"domain\": \"...\" }, + \"target_users\": [{ \"persona\": \"...\", \"needs\": [...], \"pain_points\": [...] }], + \"core_value_proposition\": \"1-2 sentences: who, what, why different\", + \"feature_categories\": { + \"must_have\": [{ \"name\": \"...\", \"description\": \"...\", \"rationale\": \"why non-negotiable\" }], + \"should_have\": [{ \"name\": \"...\", \"description\": \"...\" }], + \"could_have\": [{ \"name\": \"...\", \"description\": \"...\" }], + \"wont_have\": [{ \"name\": \"...\", \"reason\": \"explicit out-of-scope reasoning\" }] + }, + \"technical_constraints\": [...], + \"success_metrics\": [...], + \"estimated_scope\": { \"milestones\": N, \"phases\": N, \"complexity\": \"small|medium|large|enterprise\" }, + \"recommended_milestone_structure\": [{ \"name\": \"...\", \"focus\": \"...\", \"deliverables\": [...] }] +} + +Be specific and concrete. Use the user's actual answers from vision-draft.md. Do NOT pad with generic content. +" +) + +After synthesizer completes: +1. Read .mgw/vision-brief.json +2. Display the Vision Brief to user in structured format: + ``` + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vision Brief: {project_identity.name} + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Tagline: {tagline} + Domain: {domain} + + Target Users: + • {persona_1}: {needs summary} + • {persona_2}: ... + + Core Value: {core_value_proposition} + + Must-Have Features ({count}): + • {feature_1}: {rationale} + • ... + + Won't Have ({count}): {list} + + Estimated Scope: {complexity} — {milestones} milestones, ~{phases} phases + + Recommended Milestones: + 1. {name}: {focus} + 2. ... + ``` + +3. Present review options: + ``` + ───────────────────────────────────────── + Review Options: + A) Accept — proceed to condensing and project creation + R) Revise — tell me what to change, regenerate + D) Dig deeper on: [specify area] + ───────────────────────────────────────── + ``` + +4. If Accept: proceed to vision_condense step +5. If Revise: capture correction, spawn vision-synthesizer again with correction appended to vision-draft.md, loop back to step 2 +6. If Dig deeper: append "Deeper exploration of {area}" to vision-draft.md, spawn vision-synthesizer again + + + +**Vision Condense: produce gsd:new-project handoff document (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Display: "Condensing Vision Brief into project handoff..." + +Task( + description="Condense Vision Brief into gsd:new-project handoff", + subagent_type="general-purpose", + prompt=" +You are the vision-condenser agent. Your job is to produce a handoff document +that will be passed as context to a gsd:new-project spawn. + +Read .mgw/vision-brief.json. + +Produce a structured handoff document at .mgw/vision-handoff.md that: + +1. Opens with a context block that gsd:new-project can use directly to produce PROJECT.md: + - Project name, tagline, domain + - Target users and their core needs + - Core value proposition + - Must-have feature list with rationale + - Won't-have list (explicit out-of-scope) + - Technical constraints + - Success metrics + +2. Includes recommended milestone structure as a numbered list: + - Each milestone: name, focus area, key deliverables + +3. Closes with an instruction for gsd:new-project: + 'Use the above as the full project context when creating PROJECT.md. + The project name, scope, users, and milestones above reflect decisions + made through {rounds_completed} rounds of collaborative planning. + Do not hallucinate scope beyond what is specified.' + +Format as clean markdown. This document becomes the prompt prefix for gsd:new-project. +" +) + +After condenser completes: +1. Verify .mgw/vision-handoff.md exists and has content +2. Display: "Vision Brief condensed. Ready to initialize project structure." +3. Update .mgw/vision-draft.md frontmatter: current_stage: spawning +4. Proceed to spawn_gsd_new_project step. + + + +**Spawn gsd:new-project with Vision Brief context (Fresh path only)** + +If STATE_CLASS != Fresh: skip this step. + +Read .mgw/vision-handoff.md: +```bash +HANDOFF_CONTENT=$(cat .mgw/vision-handoff.md) +``` + +Display: "Spawning gsd:new-project with full vision context..." + +Spawn gsd:new-project as a Task agent, passing the handoff document as context prefix: + +Task( + description="Initialize GSD project from Vision Brief", + subagent_type="general-purpose", + prompt=" +${HANDOFF_CONTENT} + +--- + +You are now running gsd:new-project. Using the Vision Brief above as your full project context, create: + +1. .planning/PROJECT.md — Complete project definition following GSD format: + - Project name and one-line description from vision brief + - Vision and goals aligned with the value proposition + - Target users from the personas + - Core requirements mapping to the must-have features + - Non-goals matching the wont-have list + - Success criteria from success_metrics + - Technical constraints listed explicitly + +2. .planning/ROADMAP.md — First milestone plan following GSD format: + - Use the first milestone from recommended_milestone_structure + - Break it into 3-8 phases + - Each phase has: number, name, goal, requirements, success criteria + - Phase numbering starts at 1 + - Include a progress table at the top + +Write both files. Do not create additional files. Do not deviate from the Vision Brief scope. +" +) + +After agent completes: +1. Verify .planning/PROJECT.md exists: + ```bash + if [ ! -f .planning/PROJECT.md ]; then + echo "ERROR: gsd:new-project did not create .planning/PROJECT.md" + echo "Check the agent output and retry, or create PROJECT.md manually." + exit 1 + fi + ``` + +2. Verify .planning/ROADMAP.md exists: + ```bash + if [ ! -f .planning/ROADMAP.md ]; then + echo "ERROR: gsd:new-project did not create .planning/ROADMAP.md" + echo "Check the agent output and retry, or create ROADMAP.md manually." + exit 1 + fi + ``` + +3. Display success: + ``` + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD Project Initialized + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + .planning/PROJECT.md created + .planning/ROADMAP.md created (first milestone phases ready) + + Vision cycle: {rounds_completed} rounds -> Vision Brief -> PROJECT.md + ``` + +4. Update .mgw/vision-draft.md frontmatter: current_stage: complete + +4b. Synthesize alignment-report.json for milestone_mapper: + +The Fresh path skips `align_from_gsd`, so `.mgw/alignment-report.json` does not exist yet. +Synthesize it from the freshly created ROADMAP.md and PROJECT.md so `milestone_mapper` has +consistent input regardless of which path was taken. + +```bash +python3 << 'PYEOF' +import json, re, os + +repo_root = os.environ.get("REPO_ROOT", ".") + +# --- Parse PROJECT.md for name and description --- +project_path = os.path.join(repo_root, ".planning", "PROJECT.md") +with open(project_path, "r") as f: + project_text = f.read() + +# Extract H1 heading as project name +name_match = re.search(r"^#\s+(.+)$", project_text, re.MULTILINE) +project_name = name_match.group(1).strip() if name_match else "Untitled Project" + +# Extract first paragraph after H1 as description +desc_match = re.search(r"^#\s+.+\n+(.+?)(?:\n\n|\n#)", project_text, re.MULTILINE | re.DOTALL) +project_description = desc_match.group(1).strip() if desc_match else "" + +# --- Parse ROADMAP.md for phases --- +roadmap_path = os.path.join(repo_root, ".planning", "ROADMAP.md") +with open(roadmap_path, "r") as f: + roadmap_text = f.read() + +# Extract milestone name from first heading after any frontmatter +roadmap_body = re.sub(r"^---\n.*?\n---\n?", "", roadmap_text, flags=re.DOTALL) +milestone_heading = re.search(r"^#{1,2}\s+(.+)$", roadmap_body, re.MULTILINE) +milestone_name = milestone_heading.group(1).strip() if milestone_heading else "Milestone 1" + +# Extract phases (### Phase N: Name or ## Phase N: Name) +phase_pattern = re.compile(r"^#{2,3}\s+Phase\s+(\d+)[:\s]+(.+)$", re.MULTILINE) +phases = [] +for m in phase_pattern.finditer(roadmap_text): + phases.append({ + "number": int(m.group(1)), + "name": m.group(2).strip(), + "status": "pending" + }) + +if not phases: + phases = [{"number": 1, "name": milestone_name, "status": "pending"}] + +# Estimate ~2 issues per phase as a rough default +total_issues_estimated = len(phases) * 2 + +report = { + "project_name": project_name, + "project_description": project_description, + "milestones": [ + { + "name": milestone_name, + "source": "ROADMAP", + "state": "active", + "phases": phases + } + ], + "active_milestone": milestone_name, + "total_phases": len(phases), + "total_issues_estimated": total_issues_estimated +} + +output_path = os.path.join(repo_root, ".mgw", "alignment-report.json") +os.makedirs(os.path.dirname(output_path), exist_ok=True) +with open(output_path, "w") as f: + json.dump(report, f, indent=2) + +print(f"Synthesized alignment-report.json: {len(phases)} phases, milestone='{milestone_name}'") +PYEOF +``` + +5. Proceed to milestone_mapper step: + The ROADMAP.md now exists, so PATH A (HAS_ROADMAP=true) logic applies. + Call the milestone_mapper step to read ROADMAP.md and create GitHub milestones/issues. + (Note: at this point STATE_CLASS was Fresh but now GSD files exist — the milestone_mapper + step was designed for the GSD-Only path but works identically here. Proceed to it directly.) **Gather project inputs conversationally:** +If STATE_CLASS = Fresh: skip this step (handled by vision_intake through spawn_gsd_new_project above — proceed directly to milestone_mapper). + Ask the following questions in sequence: **Question 1:** "What are you building?" @@ -936,6 +1764,23 @@ the newly created milestones/phases (matching the existing project.json schema). so indices remain globally unique. When `EXTEND_MODE` is false, the existing write logic (full project.json from scratch) is unchanged. + +**Extend mode: verify new milestone GSD linkage** + +After writing the updated project.json in extend mode, report the GSD linkage status for each newly added milestone: + +```bash +if [ "$EXTEND_MODE" = true ]; then + echo "" + echo "New milestone linkage status:" + for MILESTONE in "${NEW_MILESTONES[@]}"; do + MILE_NAME=$(echo "$MILESTONE" | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])" 2>/dev/null || echo "unknown") + echo " o '${MILE_NAME}' — no GSD milestone linked yet" + echo " -> Run /gsd:new-milestone after completing the previous milestone to link" + done + echo "" +fi +``` diff --git a/commands/run.md b/commands/run.md index 7a6a780..c515dc2 100644 --- a/commands/run.md +++ b/commands/run.md @@ -106,6 +106,187 @@ If state file exists → load it. Check pipeline_stage: Log warning: "MGW: WARNING — Acknowledging security risk for #${ISSUE_NUMBER}. Proceeding with --security-ack." Update state: pipeline_stage = "triaged", add override_log entry. Continue pipeline. + +**Cross-milestone detection (runs after loading issue state):** + +Check if this issue belongs to a non-active GSD milestone: + +```bash +CROSS_MILESTONE_WARN=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +if (!state) { console.log('none'); process.exit(0); } + +const activeGsdId = state.active_gsd_milestone; + +// Find this issue's milestone in project.json +const issueNum = ${ISSUE_NUMBER}; +let issueMilestone = null; +for (const m of (state.milestones || [])) { + if ((m.issues || []).some(i => i.github_number === issueNum)) { + issueMilestone = m; + break; + } +} + +if (!issueMilestone) { console.log('none'); process.exit(0); } + +const issueGsdId = issueMilestone.gsd_milestone_id; + +// No active_gsd_milestone set (legacy schema): no warning +if (!activeGsdId) { console.log('none'); process.exit(0); } + +// Issue is in the active milestone: no warning +if (issueGsdId === activeGsdId) { console.log('none'); process.exit(0); } + +// Issue is in a different milestone +const gsdRoute = '${GSD_ROUTE}'; +if (gsdRoute === 'quick' || gsdRoute === 'gsd:quick') { + console.log('isolation:' + issueMilestone.name + ':' + (issueGsdId || 'unlinked')); +} else { + console.log('warn:' + issueMilestone.name + ':' + (issueGsdId || 'unlinked') + ':' + activeGsdId); +} +") + +case "$CROSS_MILESTONE_WARN" in + none) + # No cross-milestone issue — proceed normally + ;; + isolation:*) + MILESTONE_NAME=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f2) + GSD_ID=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f3) + + # Re-validate route against live GitHub labels (project.json may be stale from triage time) + LIVE_LABELS=$(gh issue view ${ISSUE_NUMBER} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "") + QUICK_CONFIRMED=false + if echo "$LIVE_LABELS" | grep -qiE "gsd-route:quick|gsd:quick|quick"; then + QUICK_CONFIRMED=true + fi + + if [ "$QUICK_CONFIRMED" = "true" ]; then + echo "" + echo "NOTE: Issue #${ISSUE_NUMBER} belongs to milestone '${MILESTONE_NAME}' (GSD: ${GSD_ID})" + echo " Confirmed gsd:quick via live labels — running in isolation." + echo "" + else + # Route mismatch: project.json says quick but labels don't confirm it + echo "" + echo "⚠️ Route mismatch for cross-milestone issue #${ISSUE_NUMBER}:" + echo " project.json route: quick (set at triage time)" + echo " Live GitHub labels: ${LIVE_LABELS:-none}" + echo " Labels do not confirm gsd:quick — treating as plan-phase (requires milestone context)." + echo "" + echo "Options:" + echo " 1) Switch active milestone to '${GSD_ID}' and continue" + echo " 2) Re-triage this issue (/mgw:issue ${ISSUE_NUMBER}) to update its route" + echo " 3) Abort" + echo "" + read -p "Choice [1/2/3]: " ROUTE_MISMATCH_CHOICE + case "$ROUTE_MISMATCH_CHOICE" in + 1) + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '${GSD_ID}'; +writeProjectState(state); +console.log('Switched active_gsd_milestone to: ${GSD_ID}'); +" + # Validate ROADMAP.md matches (same check as option 1 in warn case) + ROADMAP_VALID=$(python3 -c " +import os +if not os.path.exists('.planning/ROADMAP.md'): + print('missing') +else: + with open('.planning/ROADMAP.md') as f: + content = f.read() + print('match' if '${GSD_ID}' in content else 'mismatch') +" 2>/dev/null || echo "missing") + if [ "$ROADMAP_VALID" != "match" ]; then + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f4)'; +writeProjectState(state); +" 2>/dev/null || true + echo "Switch rolled back — ROADMAP.md does not match '${GSD_ID}'." + echo "Run /gsd:new-milestone to update ROADMAP.md first." + exit 0 + fi + ;; + 2) + echo "Re-triage with: /mgw:issue ${ISSUE_NUMBER}" + exit 0 + ;; + *) + echo "Aborted." + exit 0 + ;; + esac + fi + ;; + warn:*) + ISSUE_MILESTONE=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f2) + ISSUE_GSD=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f3) + ACTIVE_GSD=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f4) + echo "" + echo "⚠️ Cross-milestone issue detected:" + echo " Issue #${ISSUE_NUMBER} belongs to: '${ISSUE_MILESTONE}' (GSD: ${ISSUE_GSD})" + echo " Active GSD milestone: ${ACTIVE_GSD}" + echo "" + echo "This issue requires plan-phase work that depends on ROADMAP.md context." + echo "Running it against the wrong active milestone may produce incorrect plans." + echo "" + echo "Options:" + echo " 1) Switch active milestone to '${ISSUE_GSD}' and continue" + echo " 2) Continue anyway (not recommended)" + echo " 3) Abort — run /gsd:new-milestone to set up the correct milestone first" + echo "" + read -p "Choice [1/2/3]: " MILESTONE_CHOICE + case "$MILESTONE_CHOICE" in + 1) + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '${ISSUE_GSD}'; +writeProjectState(state); +console.log('Switched active_gsd_milestone to: ${ISSUE_GSD}'); +" + # Validate ROADMAP.md matches the new active milestone + ROADMAP_VALID=$(python3 -c " +import os +if not os.path.exists('.planning/ROADMAP.md'): + print('missing') +else: + with open('.planning/ROADMAP.md') as f: + content = f.read() + print('match' if '${ISSUE_GSD}' in content else 'mismatch') +" 2>/dev/null || echo "missing") + if [ "$ROADMAP_VALID" = "match" ]; then + echo "Active milestone updated. ROADMAP.md confirmed for '${ISSUE_GSD}'." + else + # Roll back — ROADMAP.md doesn't match + node -e " +const { loadProjectState, writeProjectState } = require('./lib/state.cjs'); +const state = loadProjectState(); +state.active_gsd_milestone = '${ACTIVE_GSD}'; +writeProjectState(state); +" 2>/dev/null || true + echo "Switch rolled back — ROADMAP.md does not match '${ISSUE_GSD}'." + echo "Run /gsd:new-milestone to update ROADMAP.md first." + exit 0 + fi + ;; + 2) + echo "Proceeding with cross-milestone issue (may affect plan quality)." + ;; + *) + echo "Aborted. Run /gsd:new-milestone then /mgw:project to align milestones." + exit 0 + ;; + esac + ;; +esac +``` @@ -289,14 +470,19 @@ TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --r # Load milestone/phase context from project.json if available MILESTONE_CONTEXT="" if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then - MILESTONE_CONTEXT=$(python3 -c " -import json -p = json.load(open('${REPO_ROOT}/.mgw/project.json')) -for m in p['milestones']: - for i in m.get('issues', []): - if i.get('github_number') == ${ISSUE_NUMBER}: - print(f\"Milestone: {m['name']} | Phase {i['phase_number']}: {i['phase_name']}\") - break + MILESTONE_CONTEXT=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +if (!state) process.exit(0); +// Search all milestones for the issue (not just active) to handle cross-milestone lookups +for (const m of (state.milestones || [])) { + for (const i of (m.issues || [])) { + if (i.github_number === ${ISSUE_NUMBER}) { + console.log('Milestone: ' + m.name + ' | Phase ' + i.phase_number + ': ' + i.phase_name); + process.exit(0); + } + } +} " 2>/dev/null || echo "") fi ``` diff --git a/commands/status.md b/commands/status.md index 59f787b..4e612d3 100644 --- a/commands/status.md +++ b/commands/status.md @@ -105,8 +105,18 @@ Exit after display. ```bash PROJECT_JSON=$(cat "${MGW_DIR}/project.json") -# Get current milestone pointer -CURRENT_MILESTONE=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['current_milestone'])") +# Resolve active milestone index (0-based) via state resolution (supports both schema versions) +ACTIVE_IDX=$(node -e " +const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs'); +const state = loadProjectState(); +const idx = resolveActiveMilestoneIndex(state); +const milestone = state.milestones ? state.milestones[idx] : null; +const gsdId = state.active_gsd_milestone || ('legacy:' + state.current_milestone); +console.log(JSON.stringify({ idx, gsd_id: gsdId, name: milestone ? milestone.name : 'unknown' })); +") +CURRENT_MILESTONE_IDX=$(echo "$ACTIVE_IDX" | python3 -c "import json,sys; print(json.load(sys.stdin)['idx'])") +# Convert 0-based index to 1-indexed milestone number for display and compatibility +CURRENT_MILESTONE=$((CURRENT_MILESTONE_IDX + 1)) TOTAL_MILESTONES=$(echo "$PROJECT_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['milestones']))") # Use specified milestone or current @@ -422,7 +432,8 @@ import json result = { 'repo': '${REPO_NAME}', 'board_url': '${BOARD_URL}', - 'current_milestone': ${CURRENT_MILESTONE}, + 'current_milestone': ${CURRENT_MILESTONE}, # 1-indexed (legacy compat) + 'active_milestone_idx': ${CURRENT_MILESTONE_IDX}, # 0-based resolved index 'viewing_milestone': ${TARGET_MILESTONE}, 'milestone': { 'name': '${MILESTONE_NAME}', @@ -453,6 +464,7 @@ The JSON structure: "repo": "owner/repo", "board_url": "https://github.com/orgs/snipcodeit/projects/1", "current_milestone": 2, + "active_milestone_idx": 1, "viewing_milestone": 2, "milestone": { "name": "v2 — Team Collaboration & Lifecycle Orchestration", diff --git a/commands/sync.md b/commands/sync.md index 5919117..056f034 100644 --- a/commands/sync.md +++ b/commands/sync.md @@ -67,6 +67,74 @@ else fi ``` +**GSD milestone consistency check (maps-to links):** + +Read all maps-to links from .mgw/cross-refs.json: + +```bash +MAPS_TO_LINKS=$(python3 -c " +import json +with open('.mgw/cross-refs.json') as f: + data = json.load(f) +links = data.get('links', []) +maps_to = [l for l in links if l.get('type') == 'maps-to'] +print(json.dumps(maps_to)) +") +``` + +For each maps-to link (format: { "a": "milestone:N", "b": "gsd-milestone:id", "type": "maps-to" }): +1. Extract the GitHub milestone number from "a" (parse "milestone:N") +2. Extract the GSD milestone ID from "b" (parse "gsd-milestone:id") +3. Check if this GSD milestone ID appears in either: + - .planning/ROADMAP.md header (active milestone) + - .planning/MILESTONES.md (archived milestones) +4. If found in neither: flag as inconsistent + +```bash +# Check each maps-to link +echo "$MAPS_TO_LINKS" | python3 -c " +import json, sys, os + +links = json.load(sys.stdin) +inconsistent = [] + +for link in links: + a = link.get('a', '') + b = link.get('b', '') + + if not a.startswith('milestone:') or not b.startswith('gsd-milestone:'): + continue + + github_num = a.split(':')[1] + gsd_id = b.split(':', 1)[1] + + found = False + + # Check ROADMAP.md + if os.path.exists('.planning/ROADMAP.md'): + with open('.planning/ROADMAP.md') as f: + content = f.read() + if gsd_id in content: + found = True + + # Check MILESTONES.md + if not found and os.path.exists('.planning/MILESTONES.md'): + with open('.planning/MILESTONES.md') as f: + content = f.read() + if gsd_id in content: + found = True + + if not found: + inconsistent.append({'github_milestone': github_num, 'gsd_id': gsd_id}) + +for i in inconsistent: + print(f\"WARN: GitHub milestone #{i['github_milestone']} maps to GSD milestone '{i['gsd_id']}' which was not found in .planning/\") + +if not inconsistent: + print('GSD milestone links: all consistent') +" +``` + Classify each issue into: - **Completed:** Issue closed AND (PR merged OR no PR expected) - **Stale:** PR merged but issue still open (auto-close missed) @@ -155,6 +223,8 @@ ${HEALTH ? 'GSD Health: ' + HEALTH.status : ''} ${details_for_each_non_active_item} ${comment_drift_details ? 'Unreviewed comments:\n' + comment_drift_details : ''} +${gsd_milestone_consistency ? 'GSD Milestone Links:\n' + gsd_milestone_consistency : ''} + ``` @@ -164,9 +234,10 @@ ${comment_drift_details ? 'Unreviewed comments:\n' + comment_drift_details : ''} - [ ] All .mgw/active/ files scanned - [ ] GitHub state checked for each issue, PR, branch - [ ] Comment delta checked for each active issue +- [ ] GSD milestone consistency checked for all maps-to links - [ ] Completed items moved to .mgw/completed/ - [ ] Lingering worktrees cleaned up for completed items - [ ] Branch deletion offered for completed items -- [ ] Stale/orphaned/drift items flagged (including comment drift) +- [ ] Stale/orphaned/drift items flagged (including comment drift and milestone inconsistencies) - [ ] Summary presented diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 597271f..a25d444 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -159,6 +159,16 @@ The full rule with a review checklist is defined in `workflows/validation.md`. E ## How an Issue Becomes a PR +### ROADMAP.md Integration + +`/mgw:project` now reads `.planning/ROADMAP.md` when it exists. The recommended setup flow is: + +1. `gsd:new-project` or `gsd:new-milestone` -- creates `.planning/ROADMAP.md` with structured phases +2. `/mgw:project` -- reads the ROADMAP.md and creates GitHub milestones and issues from it +3. `/mgw:milestone` -- executes the first milestone + +When no ROADMAP.md exists, `/mgw:project` falls back to AI-driven generation from a project description. + This is the end-to-end data flow for a single issue processed through `/mgw:run`: ``` @@ -201,6 +211,15 @@ GitHub Issue #42 | f. gsd-tools verify artifacts | +------------------------------------------+ | + +--[diagnose-issues route]-----------------+ + | a. Create .planning/debug/ directory | + | b. Spawn diagnosis agent (general-purpose)| + | - Reads codebase, finds root cause | + | - Creates .planning/debug/{slug}.md | + | c. If root cause found: route to quick | + | d. If inconclusive: report to user | + +------------------------------------------+ + | +--[milestone route]-----------------------+ | a. gsd-tools init new-milestone | | b. Spawn roadmapper agent | @@ -241,7 +260,7 @@ Issue #42 auto-closes on merge MGW provides composable commands. The full pipeline is `/mgw:run`, but each stage can be invoked independently: ``` -/mgw:project -----> Scaffold milestones + issues from a project description +/mgw:project -----> Read GSD ROADMAP.md to create GitHub milestones and issues (or generate from AI description as fallback) Creates: GitHub milestones, issues, labels, .mgw/project.json /mgw:issue N -----> Deep triage of a single issue @@ -774,6 +793,17 @@ MGW also runs non-blocking post-execution checks via `gsd-tools verify artifacts The PR agent is a `general-purpose` type (no code execution). It reads the artifacts as text and composes the PR body. It never reads application source code -- it only works from GSD's structured output. +### Debug Artifacts + +When the `gsd:diagnose-issues` route is used, the following artifact is created before execution: + +``` +.planning/debug/ + {slug}.md (debug session: root cause, evidence, files involved, fix direction) +``` + +MGW may reference `.planning/debug/{slug}.md` when building context for the subsequent quick-fix execution agent. The debug artifact is not included in the PR body directly, but informs the planner agent that follows. + --- *This document describes MGW v0.1.0. For usage instructions, see the [README](../README.md). For contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md).* diff --git a/lib/state.cjs b/lib/state.cjs index 43d47f5..8ca7182 100644 --- a/lib/state.cjs +++ b/lib/state.cjs @@ -103,11 +103,12 @@ function loadActiveIssue(number) { * 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 + * @param {number} newCurrentMilestone - 1-indexed milestone pointer for first new milestone (legacy) + * @param {string|null} [activeGsdMilestone] - Optional new-schema active milestone ID (gsd_milestone_id string) * @returns {object} The merged project state * @throws {Error} If no existing project state found */ -function mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone) { +function mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone, activeGsdMilestone) { const existing = loadProjectState(); if (!existing) { throw new Error('No existing project state found. Cannot merge without a project.json.'); @@ -119,8 +120,15 @@ function mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone) { // 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; + // New schema: if activeGsdMilestone is provided, set it and skip the legacy field + if (activeGsdMilestone !== undefined && activeGsdMilestone !== null) { + existing.active_gsd_milestone = activeGsdMilestone; + } else { + // Legacy schema: set current_milestone (1-indexed) when active_gsd_milestone is not in use + if (!existing.active_gsd_milestone) { + existing.current_milestone = newCurrentMilestone; + } + } // Write the merged state back to disk writeProjectState(existing); @@ -128,6 +136,73 @@ function mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone) { return existing; } +/** + * Migrate an existing project.json to the new multi-milestone schema. + * Adds default values for new fields (active_gsd_milestone, gsd_milestone_id, + * gsd_state, roadmap_archived_at) without overwriting existing values. + * @returns {object|null} The (possibly updated) project state, or null if no state exists + */ +function migrateProjectState() { + const existing = loadProjectState(); + if (!existing) return null; + + let changed = false; + + // Add active_gsd_milestone if missing (replaces current_milestone in new schema) + if (!existing.hasOwnProperty('active_gsd_milestone')) { + existing.active_gsd_milestone = null; + changed = true; + } + + // Add new fields to each milestone if missing + for (const m of (existing.milestones || [])) { + if (!m.hasOwnProperty('gsd_milestone_id')) { + m.gsd_milestone_id = null; + changed = true; + } + if (!m.hasOwnProperty('gsd_state')) { + m.gsd_state = null; + changed = true; + } + if (!m.hasOwnProperty('roadmap_archived_at')) { + m.roadmap_archived_at = null; + changed = true; + } + } + + if (changed) { + writeProjectState(existing); + } + + return existing; +} + +/** + * Resolve the 0-based index of the active milestone from project state. + * Supports both the new schema (active_gsd_milestone string ID) and the + * legacy schema (current_milestone 1-indexed integer). + * @param {object|null} state - Project state object (from loadProjectState) + * @returns {number} 0-based index into state.milestones, or -1 if not found/unset + */ +function resolveActiveMilestoneIndex(state) { + if (!state) return -1; + + // New schema: active_gsd_milestone is a string ID matching gsd_milestone_id + if (state.active_gsd_milestone) { + const idx = (state.milestones || []).findIndex( + m => m.gsd_milestone_id === state.active_gsd_milestone + ); + return idx; // -1 if not found + } + + // Legacy schema: current_milestone is 1-indexed integer + if (typeof state.current_milestone === 'number') { + return state.current_milestone - 1; // convert to 0-based + } + + return -1; +} + module.exports = { getMgwDir, getActiveDir, @@ -135,5 +210,7 @@ module.exports = { loadProjectState, writeProjectState, loadActiveIssue, - mergeProjectState + mergeProjectState, + migrateProjectState, + resolveActiveMilestoneIndex }; diff --git a/lib/template-loader.cjs b/lib/template-loader.cjs index cee9c22..646f82c 100644 --- a/lib/template-loader.cjs +++ b/lib/template-loader.cjs @@ -15,7 +15,9 @@ const VALID_GSD_ROUTES = [ 'verify-phase', 'new-project', 'new-milestone', - 'complete-milestone' + 'complete-milestone', + 'debug', + 'diagnose-issues' ]; /** @@ -185,6 +187,131 @@ function validate(output) { }; } +/** + * Parse a GSD ROADMAP.md file into structured phase data. + * + * Extracts phases from the `### Phase N: Name` sections. Each phase section + * may contain **Goal:**, **Requirements:**, and **Success Criteria:** fields. + * Also parses the ## Progress table for per-phase status. + * + * @param {string} content - String content of a ROADMAP.md file + * @returns {{phases: Array, total_phases: number, completed_phases: number, error?: string}} + */ +function parseRoadmap(content) { + if (!content || typeof content !== 'string') { + return { phases: [], total_phases: 0, completed_phases: 0, error: 'No content provided' }; + } + + // ── Parse the progress table for status per phase ───────────────────────── + // The table has columns: # | Phase | Status | Plans | Date + // or may use checkbox rows like: - [ ] Phase N: Name + const statusMap = {}; + + // Look for a markdown table under ## Progress + const progressTableMatch = content.match(/##\s+Progress[\s\S]*?\n((?:\|[^\n]+\|\n)+)/); + if (progressTableMatch) { + const tableRows = progressTableMatch[1].split('\n').filter(r => r.trim().startsWith('|')); + for (const row of tableRows) { + // Skip header / separator rows + if (row.includes('---') || row.toLowerCase().includes('phase') && row.toLowerCase().includes('status')) continue; + const cells = row.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 3) { + // cells[0] = #, cells[1] = Phase name, cells[2] = Status + const phaseNum = parseInt(cells[0], 10); + if (!isNaN(phaseNum)) { + statusMap[phaseNum] = cells[2] || 'Not Started'; + } + } + } + } + + // Also look for checkbox-style rows: - [ ] or - [x] + const checkboxRe = /- \[([ xX])\]\s+Phase\s+(\d+)[:\s]/gm; + let cbMatch; + while ((cbMatch = checkboxRe.exec(content)) !== null) { + const checked = cbMatch[1].trim().toLowerCase() === 'x'; + const num = parseInt(cbMatch[2], 10); + if (!isNaN(num)) { + statusMap[num] = checked ? 'Complete' : (statusMap[num] || 'Not Started'); + } + } + + // ── Parse phase sections ────────────────────────────────────────────────── + // Format: ### Phase N: Name + const phaseSectionRe = /###\s+Phase\s+(\d+)[:\s]+([^\n]+)/g; + const phases = []; + let match; + + while ((match = phaseSectionRe.exec(content)) !== null) { + const phaseNumber = parseInt(match[1], 10); + const phaseName = match[2].trim(); + const sectionStart = match.index + match[0].length; + + // Find end of this section (next ### heading or end of string) + const nextSectionMatch = /\n###\s+/g; + nextSectionMatch.lastIndex = sectionStart; + const nextMatch = nextSectionMatch.exec(content); + const sectionEnd = nextMatch ? nextMatch.index : content.length; + const sectionText = content.slice(sectionStart, sectionEnd); + + // Extract Goal + const goalMatch = sectionText.match(/\*\*Goal[:\*]*\*?\*?[:\s]+([^\n]+)/); + const goal = goalMatch ? goalMatch[1].trim().replace(/\*+$/, '') : ''; + + // Extract Requirements (comma-separated REQ-IDs after **Requirements:**) + const reqMatch = sectionText.match(/\*\*Requirements?[:\*]*\*?\*?[:\s]+([\s\S]*?)(?=\n\*\*|\n###|$)/); + let requirements = []; + if (reqMatch) { + const reqText = reqMatch[1].trim(); + // Split on commas or newlines, filter for REQ-style IDs or any non-empty token + requirements = reqText + .split(/[,\n]+/) + .map(r => r.trim().replace(/^[-*\s]+/, '').trim()) + .filter(r => r.length > 0); + } + + // Extract Success Criteria (numbered list items under **Success Criteria:**) + const scMatch = sectionText.match(/\*\*Success Criteria[:\*]*\*?\*?[:\s]+([\s\S]*?)(?=\n\*\*|\n###|$)/); + let success_criteria = []; + if (scMatch) { + const scText = scMatch[1]; + success_criteria = scText + .split('\n') + .map(line => line.trim().replace(/^[\d]+\.\s*/, '').replace(/^[-*]\s*/, '').trim()) + .filter(line => line.length > 0); + } + + // Determine status from parsed table or default + const status = statusMap[phaseNumber] || 'Not Started'; + + phases.push({ + number: phaseNumber, + name: phaseName, + goal, + requirements, + success_criteria, + status + }); + } + + if (phases.length === 0) { + return { phases: [], total_phases: 0, completed_phases: 0, error: 'No phases found in ROADMAP.md' }; + } + + // Sort phases by number + phases.sort((a, b) => a.number - b.number); + + const completedPhases = phases.filter(p => + p.status === 'Complete' || p.status === 'complete' || p.status === 'Done' + ).length; + + return { + phases, + total_phases: phases.length, + completed_phases: completedPhases + }; +} + // ── CLI Mode ──────────────────────────────────────────────────────────────── if (require.main === module) { @@ -193,12 +320,16 @@ if (require.main === module) { if (!command || command === '--help' || command === '-h') { console.log(`Usage: - node template-loader.cjs validate (reads JSON from stdin) - node template-loader.cjs schema (prints templates/schema.json to stdout) + node template-loader.cjs validate (reads JSON from stdin) + node template-loader.cjs schema (prints templates/schema.json to stdout) + node template-loader.cjs parse-roadmap (reads ROADMAP.md from stdin, outputs JSON) Commands: - validate Validate an AI-generated template from stdin - schema Print the templates/schema.json schema to stdout + validate Validate an AI-generated template from stdin + schema Print the templates/schema.json schema to stdout + parse-roadmap Parse a GSD ROADMAP.md from stdin into structured JSON + +Valid GSD routes: ${VALID_GSD_ROUTES.join(', ')} The validate command accepts any JSON object with: - type: any descriptive string (game, mobile-app, api-service, data-pipeline, etc.) @@ -237,13 +368,34 @@ The validate command accepts any JSON object with: process.exit(1); } + } else if (command === 'parse-roadmap') { + // Read ROADMAP.md content from stdin + let input = ''; + process.stdin.setEncoding('utf-8'); + process.stdin.on('data', chunk => { input += chunk; }); + process.stdin.on('end', () => { + try { + const result = parseRoadmap(input); + console.log(JSON.stringify(result, null, 2)); + process.exit(result.error ? 1 : 0); + } catch (err) { + console.error(JSON.stringify({ + phases: [], + total_phases: 0, + completed_phases: 0, + error: `Failed to parse ROADMAP.md: ${err.message}` + }, null, 2)); + process.exit(1); + } + }); + } else { console.error(JSON.stringify({ success: false, - errors: [{ field: 'command', error: `Unknown command: ${command}`, suggestion: 'Valid commands: validate, schema' }] + errors: [{ field: 'command', error: `Unknown command: ${command}`, suggestion: 'Valid commands: validate, schema, parse-roadmap' }] }, null, 2)); process.exit(1); } } -module.exports = { validate, getSchema, VALID_GSD_ROUTES }; +module.exports = { validate, getSchema, parseRoadmap, VALID_GSD_ROUTES }; diff --git a/templates/vision-brief-schema.json b/templates/vision-brief-schema.json new file mode 100644 index 0000000..34b8934 --- /dev/null +++ b/templates/vision-brief-schema.json @@ -0,0 +1,98 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Vision Brief", + "description": "Structured output from the vision-synthesizer agent. Represents the full project vision after the collaboration cycle.", + "type": "object", + "required": ["project_identity", "target_users", "core_value_proposition", "feature_categories", "success_metrics", "estimated_scope", "recommended_milestone_structure"], + "properties": { + "project_identity": { + "type": "object", + "required": ["name", "tagline", "domain"], + "properties": { + "name": { "type": "string", "description": "Short project name" }, + "tagline": { "type": "string", "description": "One-line value statement" }, + "domain": { "type": "string", "description": "Industry/domain (e.g. 'local services', 'developer tooling', 'healthcare')" } + } + }, + "target_users": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["persona", "needs", "pain_points"], + "properties": { + "persona": { "type": "string" }, + "needs": { "type": "array", "items": { "type": "string" } }, + "pain_points": { "type": "array", "items": { "type": "string" } } + } + } + }, + "core_value_proposition": { + "type": "string", + "description": "1-2 sentences: who it's for, what it does, why it's different" + }, + "feature_categories": { + "type": "object", + "required": ["must_have", "should_have", "could_have", "wont_have"], + "properties": { + "must_have": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "description", "rationale"], + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "rationale": { "type": "string", "description": "Why this is non-negotiable" } + } + } + }, + "should_have": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" } } } }, + "could_have": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" } } } }, + "wont_have": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "reason"], + "properties": { + "name": { "type": "string" }, + "reason": { "type": "string", "description": "Explicit out-of-scope reasoning" } + } + } + } + } + }, + "technical_constraints": { + "type": "array", + "items": { "type": "string" }, + "description": "Hard constraints: tech stack requirements, platform targets, compliance needs" + }, + "success_metrics": { + "type": "array", + "items": { "type": "string" }, + "description": "Measurable definition of done for the full project" + }, + "estimated_scope": { + "type": "object", + "required": ["milestones", "phases", "complexity"], + "properties": { + "milestones": { "type": "integer", "minimum": 1 }, + "phases": { "type": "integer", "minimum": 1 }, + "complexity": { "type": "string", "enum": ["small", "medium", "large", "enterprise"] } + } + }, + "recommended_milestone_structure": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["name", "focus", "deliverables"], + "properties": { + "name": { "type": "string", "description": "Milestone name (e.g. 'v1 — Foundation')" }, + "focus": { "type": "string", "description": "What this milestone achieves" }, + "deliverables": { "type": "array", "items": { "type": "string" } } + } + } + } + } +}