diff --git a/commands/project.md b/commands/project.md
index 35755ca..e25bd06 100644
--- a/commands/project.md
+++ b/commands/project.md
@@ -574,6 +574,228 @@ 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_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
+
+ 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
+```
+
+
**Write .mgw/project.json with project state**
@@ -607,6 +829,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'],
@@ -615,7 +839,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({
@@ -658,7 +883,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
```
@@ -666,6 +902,11 @@ 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.
@@ -770,7 +1011,12 @@ 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