@snipcodeit/mgw 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@snipcodeit/mgw)](https://www.npmjs.com/package/@snipcodeit/mgw)
4
4
  [![CI](https://github.com/snipcodeit/mgw/actions/workflows/ci.yml/badge.svg)](https://github.com/snipcodeit/mgw/actions/workflows/ci.yml)
5
+ [![Coverage](https://img.shields.io/badge/coverage-70%25-brightgreen)](./coverage/lcov-report/index.html)
5
6
  [![npm downloads](https://img.shields.io/npm/dm/@snipcodeit/mgw)](https://www.npmjs.com/package/@snipcodeit/mgw)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
  [![node](https://img.shields.io/node/v/@snipcodeit/mgw)](https://nodejs.org)
@@ -2,45 +2,84 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * bin/mgw-install.cjs — Idempotent slash command installer
5
+ * bin/mgw-install.cjs — Multi-CLI-aware idempotent slash command installer
6
6
  *
7
- * Runs automatically via npm postinstall. Copies the commands/ source tree
8
- * into ~/.claude/commands/mgw/ so Claude Code slash commands are available
9
- * without any manual copy step.
7
+ * Runs automatically via npm postinstall. Detects the active AI CLI
8
+ * (claude, gemini, or opencode in priority order) and copies the
9
+ * commands/ source tree into the correct provider-specific commands
10
+ * directory so slash commands are available without any manual copy step.
10
11
  *
11
12
  * Behavior:
12
- * - If ~/.claude/ does not exist: prints a skip message and exits 0 (non-fatal)
13
- * - If ~/.claude/ exists: creates ~/.claude/commands/mgw/ and recursively
14
- * copies commands/ into it (overwriting existing files idempotent)
15
- * - Exits 0 in both cases
13
+ * - Auto-detects first available CLI binary (claude > gemini > opencode)
14
+ * - --provider=<id> flag overrides auto-detection
15
+ * - If no AI CLI is found on PATH: prints skip message and exits 0 (non-fatal)
16
+ * - If provider's base dir does not exist: prints skip message and exits 0
17
+ * - If previously installed provider differs: removes old install dir first
18
+ * - Idempotent: running twice for the same provider is safe
19
+ * - Tracks installed provider in ~/.mgw-install-state.json
16
20
  *
17
- * Dependencies: Node.js built-ins only (path, fs, os)
21
+ * Dependencies: Node.js built-ins only (path, fs, os, child_process)
18
22
  */
19
23
 
20
24
  const path = require('path');
21
25
  const fs = require('fs');
22
26
  const os = require('os');
27
+ const { execSync } = require('child_process');
23
28
 
24
29
  // Source: commands/ directory relative to this script (bin/ → ../commands/)
25
30
  const sourceDir = path.join(__dirname, '..', 'commands');
26
31
 
27
- // Target: ~/.claude/commands/mgw/
28
- const claudeDir = path.join(os.homedir(), '.claude');
29
- const targetDir = path.join(claudeDir, 'commands', 'mgw');
32
+ // State file for tracking installed provider across runs
33
+ const statePath = path.join(os.homedir(), '.mgw-install-state.json');
30
34
 
31
- // Guard: if ~/.claude/ does not exist, skip silently with a clear message
32
- if (!fs.existsSync(claudeDir)) {
33
- console.log(
34
- 'mgw: ~/.claude/ not found — skipping slash command install ' +
35
- '(run `node ./bin/mgw-install.cjs` after installing Claude Code)'
36
- );
37
- process.exit(0);
35
+ // Provider target directories where each CLI expects slash commands
36
+ const PROVIDER_TARGETS = {
37
+ claude: path.join(os.homedir(), '.claude', 'commands', 'mgw'),
38
+ gemini: path.join(os.homedir(), '.gemini', 'commands', 'mgw'),
39
+ opencode: path.join(os.homedir(), '.opencode', 'commands', 'mgw'),
40
+ };
41
+
42
+ // Valid provider IDs (order is also detection priority)
43
+ const VALID_PROVIDERS = ['claude', 'gemini', 'opencode'];
44
+
45
+ /**
46
+ * Parse --provider flag from process.argv.
47
+ * Handles both --provider=claude and --provider claude forms.
48
+ * @returns {string|null} provider ID string or null if not specified
49
+ */
50
+ function parseProviderFlag() {
51
+ const args = process.argv.slice(2);
52
+ for (let i = 0; i < args.length; i++) {
53
+ if (args[i].startsWith('--provider=')) {
54
+ return args[i].split('=')[1] || null;
55
+ }
56
+ if (args[i] === '--provider' && i + 1 < args.length) {
57
+ return args[i + 1];
58
+ }
59
+ }
60
+ return null;
38
61
  }
39
62
 
40
- // Guard: ensure commands/ source exists in this package
41
- if (!fs.existsSync(sourceDir)) {
42
- console.log('mgw: commands/ source not found skipping slash command install');
43
- process.exit(0);
63
+ /**
64
+ * Detect the active AI CLI by trying binaries in priority order.
65
+ * @param {string|null} forcedProvider - If set, skip detection and return this value.
66
+ * @returns {string|null} Provider ID of the first found binary, or null if none found.
67
+ */
68
+ function detectProvider(forcedProvider) {
69
+ if (forcedProvider) {
70
+ return forcedProvider;
71
+ }
72
+
73
+ for (const id of VALID_PROVIDERS) {
74
+ try {
75
+ execSync(id + ' --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
76
+ return id;
77
+ } catch (_) {
78
+ // Binary not found or not working — try next
79
+ }
80
+ }
81
+
82
+ return null;
44
83
  }
45
84
 
46
85
  /**
@@ -76,6 +115,64 @@ function copyDirRecursive(src, dest) {
76
115
  return count;
77
116
  }
78
117
 
118
+ // --- Main ---
119
+
120
+ // Guard: ensure commands/ source exists in this package
121
+ if (!fs.existsSync(sourceDir)) {
122
+ console.log('mgw: commands/ source not found — skipping slash command install');
123
+ process.exit(0);
124
+ }
125
+
126
+ // Parse and validate --provider flag
127
+ const flagProvider = parseProviderFlag();
128
+ if (flagProvider !== null && !VALID_PROVIDERS.includes(flagProvider)) {
129
+ console.error(
130
+ 'mgw: unknown provider "' + flagProvider + '" — valid options: ' + VALID_PROVIDERS.join(', ')
131
+ );
132
+ process.exit(1);
133
+ }
134
+
135
+ // Detect or use forced provider
136
+ const detectedProvider = detectProvider(flagProvider);
137
+
138
+ if (detectedProvider === null) {
139
+ console.log('mgw: no AI CLI found (claude/gemini/opencode) — skipping slash command install');
140
+ process.exit(0);
141
+ }
142
+
143
+ // Parent dir guard: provider's base config directory must already exist
144
+ // (i.e. the user has run the CLI at least once to initialize it).
145
+ // This MUST run before old-dir cleanup — otherwise switching to a provider
146
+ // whose home dir doesn't exist would delete old commands and install nothing.
147
+ const targetDir = PROVIDER_TARGETS[detectedProvider];
148
+ const providerHomeDir = path.join(os.homedir(), '.' + detectedProvider);
149
+
150
+ if (!fs.existsSync(providerHomeDir)) {
151
+ console.log(
152
+ 'mgw: ~/.' + detectedProvider + '/ not found — skipping slash command install ' +
153
+ '(run the CLI once to initialize it)'
154
+ );
155
+ process.exit(0);
156
+ }
157
+
158
+ // Old-dir cleanup: if previously installed provider differs, remove old install.
159
+ // Safe to run here — we've already confirmed the new provider's home dir exists.
160
+ if (fs.existsSync(statePath)) {
161
+ try {
162
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
163
+ if (state.provider && state.provider !== detectedProvider && PROVIDER_TARGETS[state.provider]) {
164
+ const oldDir = PROVIDER_TARGETS[state.provider];
165
+ fs.rmSync(oldDir, { recursive: true, force: true });
166
+ }
167
+ } catch (_) {
168
+ // Corrupt or unreadable state file — ignore and continue
169
+ }
170
+ }
171
+
79
172
  // Perform the install
80
173
  const fileCount = copyDirRecursive(sourceDir, targetDir);
81
- console.log(`mgw: installed ${fileCount} slash commands to ${targetDir}`);
174
+
175
+ // Write state file
176
+ fs.writeFileSync(statePath, JSON.stringify({ provider: detectedProvider }, null, 2));
177
+
178
+ console.log('mgw: detected ' + detectedProvider + ' CLI — installed ' + fileCount + ' slash commands to ' + targetDir);
@@ -399,6 +399,141 @@ print(json.dumps(result))
399
399
  else
400
400
  echo " WARNING: GSD Route field creation failed"
401
401
  fi
402
+
403
+ # Field 6: Plan Summary (TEXT)
404
+ PLAN_SUMMARY_RESULT=$(gh api graphql -f query='
405
+ mutation($projectId: ID!) {
406
+ createProjectV2Field(input: {
407
+ projectId: $projectId
408
+ dataType: TEXT
409
+ name: "Plan Summary"
410
+ }) {
411
+ projectV2Field {
412
+ ... on ProjectV2Field {
413
+ id
414
+ name
415
+ }
416
+ }
417
+ }
418
+ }
419
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
420
+
421
+ PLAN_SUMMARY_FIELD_ID=$(echo "$PLAN_SUMMARY_RESULT" | python3 -c "
422
+ import json,sys
423
+ d = json.load(sys.stdin)
424
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
425
+ " 2>/dev/null)
426
+
427
+ if [ -n "$PLAN_SUMMARY_FIELD_ID" ]; then
428
+ echo " Plan Summary field created: ${PLAN_SUMMARY_FIELD_ID}"
429
+ else
430
+ echo " WARNING: Plan Summary field creation failed"
431
+ fi
432
+
433
+ # Field 7: Priority (SINGLE_SELECT)
434
+ PRIORITY_RESULT=$(gh api graphql -f query='
435
+ mutation($projectId: ID!) {
436
+ createProjectV2Field(input: {
437
+ projectId: $projectId
438
+ dataType: SINGLE_SELECT
439
+ name: "Priority"
440
+ singleSelectOptions: [
441
+ { name: "P0", color: RED, description: "Critical — blocking other work" }
442
+ { name: "P1", color: ORANGE, description: "High — needed this milestone" }
443
+ { name: "P2", color: YELLOW, description: "Medium — important but not urgent" }
444
+ { name: "P3", color: GRAY, description: "Low — nice to have" }
445
+ ]
446
+ }) {
447
+ projectV2Field {
448
+ ... on ProjectV2SingleSelectField {
449
+ id
450
+ name
451
+ options { id name }
452
+ }
453
+ }
454
+ }
455
+ }
456
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
457
+
458
+ PRIORITY_FIELD_ID=$(echo "$PRIORITY_RESULT" | python3 -c "
459
+ import json,sys
460
+ d = json.load(sys.stdin)
461
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
462
+ " 2>/dev/null)
463
+
464
+ PRIORITY_OPTIONS=$(echo "$PRIORITY_RESULT" | python3 -c "
465
+ import json,sys
466
+ d = json.load(sys.stdin)
467
+ options = d['data']['createProjectV2Field']['projectV2Field']['options']
468
+ name_to_id = {o['name']: o['id'] for o in options}
469
+ print(json.dumps(name_to_id))
470
+ " 2>/dev/null || echo "{}")
471
+
472
+ if [ -n "$PRIORITY_FIELD_ID" ]; then
473
+ echo " Priority field created: ${PRIORITY_FIELD_ID}"
474
+ else
475
+ echo " WARNING: Priority field creation failed"
476
+ fi
477
+
478
+ # Field 8: Start Date (DATE)
479
+ START_DATE_RESULT=$(gh api graphql -f query='
480
+ mutation($projectId: ID!) {
481
+ createProjectV2Field(input: {
482
+ projectId: $projectId
483
+ dataType: DATE
484
+ name: "Start Date"
485
+ }) {
486
+ projectV2Field {
487
+ ... on ProjectV2Field {
488
+ id
489
+ name
490
+ }
491
+ }
492
+ }
493
+ }
494
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
495
+
496
+ START_DATE_FIELD_ID=$(echo "$START_DATE_RESULT" | python3 -c "
497
+ import json,sys
498
+ d = json.load(sys.stdin)
499
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
500
+ " 2>/dev/null)
501
+
502
+ if [ -n "$START_DATE_FIELD_ID" ]; then
503
+ echo " Start Date field created: ${START_DATE_FIELD_ID}"
504
+ else
505
+ echo " WARNING: Start Date field creation failed"
506
+ fi
507
+
508
+ # Field 9: Target Date (DATE)
509
+ TARGET_DATE_RESULT=$(gh api graphql -f query='
510
+ mutation($projectId: ID!) {
511
+ createProjectV2Field(input: {
512
+ projectId: $projectId
513
+ dataType: DATE
514
+ name: "Target Date"
515
+ }) {
516
+ projectV2Field {
517
+ ... on ProjectV2Field {
518
+ id
519
+ name
520
+ }
521
+ }
522
+ }
523
+ }
524
+ ' -f projectId="$NEW_PROJECT_ID" 2>&1)
525
+
526
+ TARGET_DATE_FIELD_ID=$(echo "$TARGET_DATE_RESULT" | python3 -c "
527
+ import json,sys
528
+ d = json.load(sys.stdin)
529
+ print(d['data']['createProjectV2Field']['projectV2Field']['id'])
530
+ " 2>/dev/null)
531
+
532
+ if [ -n "$TARGET_DATE_FIELD_ID" ]; then
533
+ echo " Target Date field created: ${TARGET_DATE_FIELD_ID}"
534
+ else
535
+ echo " WARNING: Target Date field creation failed"
536
+ fi
402
537
  ```
403
538
 
404
539
  **Update project.json with board metadata:**
@@ -456,6 +591,36 @@ if '${GSD_ROUTE_FIELD_ID}':
456
591
  'options': gsd_route_options
457
592
  }
458
593
 
594
+ if '${PLAN_SUMMARY_FIELD_ID}':
595
+ fields['plan_summary'] = {
596
+ 'field_id': '${PLAN_SUMMARY_FIELD_ID}',
597
+ 'field_name': 'Plan Summary',
598
+ 'type': 'TEXT'
599
+ }
600
+
601
+ priority_options = json.loads('''${PRIORITY_OPTIONS}''') if '${PRIORITY_OPTIONS}' != '{}' else {}
602
+ if '${PRIORITY_FIELD_ID}':
603
+ fields['priority'] = {
604
+ 'field_id': '${PRIORITY_FIELD_ID}',
605
+ 'field_name': 'Priority',
606
+ 'type': 'SINGLE_SELECT',
607
+ 'options': priority_options
608
+ }
609
+
610
+ if '${START_DATE_FIELD_ID}':
611
+ fields['start_date'] = {
612
+ 'field_id': '${START_DATE_FIELD_ID}',
613
+ 'field_name': 'Start Date',
614
+ 'type': 'DATE'
615
+ }
616
+
617
+ if '${TARGET_DATE_FIELD_ID}':
618
+ fields['target_date'] = {
619
+ 'field_id': '${TARGET_DATE_FIELD_ID}',
620
+ 'field_name': 'Target Date',
621
+ 'type': 'DATE'
622
+ }
623
+
459
624
  # Update project_board section
460
625
  project['project']['project_board'] = {
461
626
  'number': int('${NEW_PROJECT_NUMBER}') if '${NEW_PROJECT_NUMBER}'.isdigit() else None,
@@ -484,9 +649,36 @@ PYEOF
484
649
  echo " milestone ${MILESTONE_FIELD_ID:-FAILED} (TEXT)"
485
650
  echo " phase ${PHASE_FIELD_ID:-FAILED} (TEXT)"
486
651
  echo " gsd_route ${GSD_ROUTE_FIELD_ID:-FAILED} (SINGLE_SELECT, 4 options)"
652
+ echo " plan_summary ${PLAN_SUMMARY_FIELD_ID:-FAILED} (TEXT)"
653
+ echo " priority ${PRIORITY_FIELD_ID:-FAILED} (SINGLE_SELECT, 4 options)"
654
+ echo " start_date ${START_DATE_FIELD_ID:-FAILED} (DATE)"
655
+ echo " target_date ${TARGET_DATE_FIELD_ID:-FAILED} (DATE)"
487
656
  echo ""
488
657
  echo "Field IDs stored in .mgw/project.json"
489
658
  echo ""
659
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
660
+ echo " VIEW SETUP GUIDE"
661
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
662
+ echo ""
663
+ echo "Open your project board and create these views:"
664
+ echo ""
665
+ echo "1. Pipeline (Board)"
666
+ echo " - Layout: Board | Column field: Status"
667
+ echo " - Visible fields: Phase, GSD Route, Milestone, Priority"
668
+ echo ""
669
+ echo "2. Sprint Table (Table)"
670
+ echo " - Layout: Table | Group by: Milestone"
671
+ echo " - Columns: Status, Phase, Priority, Plan Summary, Assignees"
672
+ echo " - Sort by: Priority (ascending)"
673
+ echo ""
674
+ echo "3. Roadmap (Roadmap)"
675
+ echo " - Layout: Roadmap | Date field: Start Date -> Target Date"
676
+ echo " - Group by: Milestone"
677
+ echo ""
678
+ echo "4. My Work (Table)"
679
+ echo " - Layout: Table | Filter: assignee:@me"
680
+ echo " - Columns: Status, Phase, Plan Summary, Priority"
681
+ echo ""
490
682
  echo "Next:"
491
683
  echo " /mgw:board show Display board state"
492
684
  echo " /mgw:run 73 Sync issues onto board items (#73)"
@@ -385,6 +385,50 @@ print(stage_map.get(label, ''))
385
385
  fi
386
386
  fi
387
387
 
388
+ # Update Plan Summary field from latest plan comment (if field exists)
389
+ PLAN_SUMMARY_FIELD_ID=$(echo "$FIELDS_JSON" | python3 -c "
390
+ import json,sys
391
+ fields = json.load(sys.stdin)
392
+ print(fields.get('plan_summary', {}).get('field_id', ''))
393
+ " 2>/dev/null)
394
+
395
+ if [ -n "$PLAN_SUMMARY_FIELD_ID" ] && [ -n "$BOARD_ITEM_ID" ]; then
396
+ PLAN_SUMMARY=$(node -e "
397
+ const ic = require('./lib/issue-context.cjs');
398
+ ic.findLatestComment(${ISSUE_NUMBER}, 'plan')
399
+ .then(plan => {
400
+ if (plan) {
401
+ const body = plan.body.replace(/<!--[\\s\\S]*?-->\\n?/, '').trim();
402
+ const summary = body.split('\\n').slice(0, 3).join(' ').substring(0, 200);
403
+ process.stdout.write(summary);
404
+ }
405
+ })
406
+ .catch(() => {});
407
+ " 2>/dev/null || echo "")
408
+
409
+ if [ -n "$PLAN_SUMMARY" ]; then
410
+ gh api graphql -f query='
411
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
412
+ updateProjectV2ItemFieldValue(input: {
413
+ projectId: $projectId
414
+ itemId: $itemId
415
+ fieldId: $fieldId
416
+ value: { text: $text }
417
+ }) { projectV2Item { id } }
418
+ }
419
+ ' -f projectId="$BOARD_NODE_ID" \
420
+ -f itemId="$BOARD_ITEM_ID" \
421
+ -f fieldId="$PLAN_SUMMARY_FIELD_ID" \
422
+ -f text="$PLAN_SUMMARY" 2>/dev/null || true
423
+
424
+ if [ -n "$CHANGED_FIELDS" ]; then
425
+ CHANGED_FIELDS="${CHANGED_FIELDS}, Plan Summary (updated)"
426
+ else
427
+ CHANGED_FIELDS="Plan Summary (updated)"
428
+ fi
429
+ fi
430
+ fi
431
+
388
432
  if [ -z "$CHANGED_FIELDS" ]; then
389
433
  CHANGED_FIELDS="no changes"
390
434
  fi
@@ -27,6 +27,29 @@ if [ "$SUBCOMMAND" = "views" ]; then
27
27
  echo " kanban Create Board layout view (swimlanes by Status)"
28
28
  echo " table Create Table layout view (flat list with all fields)"
29
29
  echo " roadmap Create Roadmap layout view (timeline grouped by Milestone)"
30
+ echo ""
31
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
32
+ echo " VIEW SETUP GUIDE"
33
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
34
+ echo ""
35
+ echo "Open your project board and create these views:"
36
+ echo ""
37
+ echo "1. Pipeline (Board)"
38
+ echo " - Layout: Board | Column field: Status"
39
+ echo " - Visible fields: Phase, GSD Route, Milestone, Priority"
40
+ echo ""
41
+ echo "2. Sprint Table (Table)"
42
+ echo " - Layout: Table | Group by: Milestone"
43
+ echo " - Columns: Status, Phase, Priority, Plan Summary, Assignees"
44
+ echo " - Sort by: Priority (ascending)"
45
+ echo ""
46
+ echo "3. Roadmap (Roadmap)"
47
+ echo " - Layout: Roadmap | Date field: Start Date -> Target Date"
48
+ echo " - Group by: Milestone"
49
+ echo ""
50
+ echo "4. My Work (Table)"
51
+ echo " - Layout: Table | Filter: assignee:@me"
52
+ echo " - Columns: Status, Phase, Plan Summary, Priority"
30
53
  exit 1
31
54
  fi
32
55
 
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: mgw:context
3
+ description: Show assembled context for an issue — what GSD agents see before planning/execution
4
+ argument-hint: "<issue-number>"
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ ---
9
+
10
+ <objective>
11
+ Display the full assembled context for a GitHub issue, showing exactly what a GSD agent
12
+ would receive as input before planning or execution. Useful for debugging context gaps,
13
+ verifying that prior phase summaries are available, and checking what a second developer's
14
+ agent will see.
15
+
16
+ This command is read-only. It does not modify any state.
17
+ </objective>
18
+
19
+ <process>
20
+
21
+ <step name="validate_input">
22
+ **Validate issue number provided:**
23
+
24
+ Parse $ARGUMENTS for a numeric issue number. If missing:
25
+ ```
26
+ AskUserQuestion(
27
+ header: "Issue Number Required",
28
+ question: "Which issue number do you want context for?",
29
+ followUp: "Enter the GitHub issue number (e.g., 42)"
30
+ )
31
+ ```
32
+ </step>
33
+
34
+ <step name="assemble_and_display">
35
+ **Assemble full context from GitHub and display formatted output:**
36
+
37
+ ```bash
38
+ REPO_ROOT=$(git rev-parse --show-toplevel)
39
+
40
+ node -e "
41
+ const ic = require('${REPO_ROOT}/lib/issue-context.cjs');
42
+
43
+ (async () => {
44
+ // Assemble issue context
45
+ const ctx = await ic.assembleIssueContext(${ISSUE_NUMBER});
46
+ if (!ctx || !ctx.issue) {
47
+ console.log('No context found for issue #${ISSUE_NUMBER}.');
48
+ process.exit(0);
49
+ }
50
+
51
+ const issue = ctx.issue;
52
+ const milestone = issue.milestone || {};
53
+ const labels = (issue.labels || []).map(l => typeof l === 'object' ? l.name : l);
54
+
55
+ // Header
56
+ console.log('');
57
+ console.log('Issue #' + issue.number + ': ' + issue.title);
58
+ if (milestone.title) {
59
+ console.log('Milestone: ' + milestone.title);
60
+ }
61
+ console.log('Status: ' + (issue.state || 'unknown'));
62
+ if (labels.length > 0) {
63
+ console.log('Labels: ' + labels.join(', '));
64
+ }
65
+
66
+ // Vision
67
+ const vision = await ic.fetchProjectVision();
68
+ if (vision) {
69
+ console.log('');
70
+ console.log('-- Vision --');
71
+ console.log(vision.slice(0, 2000));
72
+ }
73
+
74
+ // Prior phases (from milestone siblings)
75
+ if (ctx.milestoneContext && ctx.milestoneContext.length > 0) {
76
+ console.log('');
77
+ console.log('-- Prior Phases --');
78
+ for (const s of ctx.milestoneContext) {
79
+ if (s.issueNumber === issue.number) continue;
80
+ console.log('Phase #' + s.issueNumber + ': ' + s.title);
81
+ if (s.summary) {
82
+ // Indent summary lines
83
+ const lines = s.summary.split('\\n').slice(0, 5);
84
+ for (const line of lines) {
85
+ console.log(' ' + line);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // Phase goal (from issue body)
92
+ if (issue.body) {
93
+ console.log('');
94
+ console.log('-- Phase Goal --');
95
+ // Extract acceptance criteria or goal sections from body
96
+ const body = issue.body;
97
+ const goalMatch = body.match(/##?\\s*(?:Goal|Description|Overview)\\s*\\n([\\s\\S]*?)(?=\\n##|$)/i);
98
+ if (goalMatch) {
99
+ console.log(goalMatch[1].trim().slice(0, 1000));
100
+ } else {
101
+ // Show first meaningful paragraph
102
+ const paras = body.split('\\n\\n').filter(p => p.trim() && !p.startsWith('<!--'));
103
+ if (paras.length > 0) {
104
+ console.log(paras[0].trim().slice(0, 1000));
105
+ }
106
+ }
107
+
108
+ const acMatch = body.match(/##?\\s*Acceptance\\s*Criteria\\s*\\n([\\s\\S]*?)(?=\\n##|$)/i);
109
+ if (acMatch) {
110
+ console.log('');
111
+ console.log('-- Acceptance Criteria --');
112
+ console.log(acMatch[1].trim().slice(0, 1000));
113
+ }
114
+ }
115
+
116
+ // Dependencies (from labels)
117
+ const blockedBy = labels.filter(l => l.startsWith('blocked-by:'));
118
+ const blocks = labels.filter(l => l.startsWith('blocks:'));
119
+ if (blockedBy.length > 0 || blocks.length > 0) {
120
+ console.log('');
121
+ console.log('-- Dependencies --');
122
+ if (blockedBy.length > 0) {
123
+ console.log('Blocked by: ' + blockedBy.map(l => l.replace('blocked-by:', '')).join(', '));
124
+ }
125
+ if (blocks.length > 0) {
126
+ console.log('Blocks: ' + blocks.map(l => l.replace('blocks:', '')).join(', '));
127
+ }
128
+ }
129
+
130
+ // Plan (if posted)
131
+ if (ctx.planComment) {
132
+ console.log('');
133
+ console.log('-- Plan (from structured comment) --');
134
+ const planBody = ctx.planComment.body.replace(/<!--[\\s\\S]*?-->\\n?/, '').trim();
135
+ console.log(planBody.slice(0, 2000));
136
+ }
137
+
138
+ // Summary (if posted)
139
+ if (ctx.summaryComment) {
140
+ console.log('');
141
+ console.log('-- Summary (from structured comment) --');
142
+ const summaryBody = ctx.summaryComment.body.replace(/<!--[\\s\\S]*?-->\\n?/, '').trim();
143
+ console.log(summaryBody.slice(0, 1000));
144
+ }
145
+
146
+ console.log('');
147
+ })().catch(e => {
148
+ console.error('Error assembling context: ' + e.message);
149
+ process.exit(1);
150
+ });
151
+ " 2>/dev/null
152
+ ```
153
+ </step>
154
+
155
+ <step name="display">
156
+ **Display context report with banner:**
157
+
158
+ ```
159
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
160
+ MGW ► CONTEXT FOR #${ISSUE_NUMBER}
161
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+
163
+ ${FORMATTED_OUTPUT}
164
+
165
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
166
+ Tip: This is what GSD agents see before planning.
167
+ → /mgw:run ${ISSUE_NUMBER} — Execute this issue
168
+ → /mgw:status — Project dashboard
169
+ ```
170
+ </step>
171
+
172
+ </process>
173
+
174
+ <success_criteria>
175
+ - [ ] Issue number validated
176
+ - [ ] Issue context assembled from GitHub comments via assembleIssueContext()
177
+ - [ ] Vision fetched via fetchProjectVision() (GitHub Project README first, local fallback)
178
+ - [ ] Output formatted with clear section headers (Vision, Prior Phases, Phase Goal, etc.)
179
+ - [ ] Acceptance criteria extracted from issue body when present
180
+ - [ ] Dependencies shown from labels
181
+ - [ ] Plan and summary comments displayed when available
182
+ - [ ] No state modified
183
+ </success_criteria>