@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 +1 -0
- package/bin/mgw-install.cjs +121 -24
- package/commands/board/create.md +192 -0
- package/commands/board/sync.md +44 -0
- package/commands/board/views.md +23 -0
- package/commands/context.md +183 -0
- package/commands/handoff.md +169 -0
- package/commands/issue.md +62 -0
- package/commands/milestone.md +37 -5
- package/commands/project.md +19 -0
- package/commands/run/execute.md +604 -27
- package/commands/run/pr-create.md +88 -0
- package/commands/run/triage.md +199 -2
- package/commands/run/worktree.md +41 -0
- package/commands/sync.md +69 -0
- package/commands/workflows/gsd.md +71 -0
- package/commands/workflows/state.md +340 -1
- package/dist/bin/mgw.cjs +14 -11
- package/dist/{index-s7v-ifd0.cjs → index-CHrVAIMY.cjs} +414 -23
- package/dist/lib/index.cjs +997 -20
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@snipcodeit/mgw)
|
|
4
4
|
[](https://github.com/snipcodeit/mgw/actions/workflows/ci.yml)
|
|
5
|
+
[](./coverage/lcov-report/index.html)
|
|
5
6
|
[](https://www.npmjs.com/package/@snipcodeit/mgw)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[](https://nodejs.org)
|
package/bin/mgw-install.cjs
CHANGED
|
@@ -2,45 +2,84 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* bin/mgw-install.cjs —
|
|
5
|
+
* bin/mgw-install.cjs — Multi-CLI-aware idempotent slash command installer
|
|
6
6
|
*
|
|
7
|
-
* Runs automatically via npm postinstall.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
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
|
-
//
|
|
28
|
-
const
|
|
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
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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);
|
package/commands/board/create.md
CHANGED
|
@@ -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)"
|
package/commands/board/sync.md
CHANGED
|
@@ -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
|
package/commands/board/views.md
CHANGED
|
@@ -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>
|