@snipcodeit/mgw 0.2.2 → 0.4.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 +55 -5
- package/bin/mgw-install.cjs +121 -24
- package/commands/board/configure.md +205 -0
- package/commands/board/create.md +688 -0
- package/commands/board/show.md +221 -0
- package/commands/board/sync.md +461 -0
- package/commands/board/views.md +253 -0
- package/commands/board.md +23 -1543
- package/commands/context.md +183 -0
- package/commands/handoff.md +169 -0
- package/commands/issue.md +62 -0
- package/commands/milestone.md +42 -43
- package/commands/project.md +19 -0
- package/commands/review.md +222 -42
- package/commands/run/execute.md +820 -0
- package/commands/run/pr-create.md +324 -0
- package/commands/run/triage.md +510 -0
- package/commands/run/worktree.md +95 -0
- package/commands/run.md +23 -1547
- package/commands/sync.md +69 -0
- package/commands/workflows/gsd.md +1 -13
- package/dist/bin/mgw.cjs +107 -15
- package/dist/{index-BiwU0uWA.cjs → index-B-_JvYpz.cjs} +885 -69
- package/dist/lib/index.cjs +653 -155
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# MGW — My GSD Workflow
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@snipcodeit/mgw)
|
|
4
|
+
[](https://github.com/snipcodeit/mgw/actions/workflows/ci.yml)
|
|
4
5
|
[](https://www.npmjs.com/package/@snipcodeit/mgw)
|
|
5
6
|
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
[](https://nodejs.org)
|
|
@@ -341,18 +342,23 @@ bin/
|
|
|
341
342
|
lib/
|
|
342
343
|
index.cjs Barrel export
|
|
343
344
|
claude.cjs Claude Code invocation helpers
|
|
344
|
-
|
|
345
|
+
errors.cjs Typed error hierarchy (MgwError, GitHubApiError, TimeoutError, etc.)
|
|
346
|
+
github.cjs Async GitHub CLI wrappers with retry/timeout (issues, PRs, milestones, Projects v2)
|
|
345
347
|
gsd.cjs GSD integration
|
|
346
348
|
gsd-adapter.cjs GSD route adapter (maps triage results to GSD spawn args)
|
|
347
|
-
|
|
349
|
+
logger.cjs Structured JSON-lines execution logging (.mgw/logs/)
|
|
350
|
+
pipeline.cjs Pipeline stage constants, valid transitions, and transition hooks
|
|
351
|
+
state.cjs .mgw/ state management, cross-refs validation, dependency parsing
|
|
348
352
|
output.cjs Logging and formatting
|
|
349
|
-
|
|
353
|
+
progress.cjs Milestone progress display
|
|
354
|
+
retry.cjs Retry/backoff logic with failure classification
|
|
350
355
|
templates.cjs Template system
|
|
351
356
|
template-loader.cjs Output validation (JSON Schema) + parseRoadmap()
|
|
352
357
|
commands/ Slash command source files (deployed to ~/.claude/commands/mgw/ at install time)
|
|
353
358
|
ask.md Contextual question routing during milestone execution
|
|
354
359
|
assign.md Claim/reassign issues; resolves GitHub noreply co-author tag
|
|
355
|
-
board.md GitHub Projects v2 board
|
|
360
|
+
board.md GitHub Projects v2 board dispatcher
|
|
361
|
+
board/ Board subcommands (create, show, configure, views, sync)
|
|
356
362
|
help.md Command reference display
|
|
357
363
|
init.md One-time repo bootstrap (state, templates, labels)
|
|
358
364
|
project.md State-aware project init (Vision Cycle, alignment, drift, extend)
|
|
@@ -361,7 +367,8 @@ commands/ Slash command source files (deployed to ~/.claude/com
|
|
|
361
367
|
next.md Next unblocked issue picker (surfaces failed issues as advisory)
|
|
362
368
|
review.md Comment review and classification since last triage
|
|
363
369
|
roadmap.md Milestone roadmap table; optional GitHub due-date setter and Discussion post
|
|
364
|
-
run.md Autonomous pipeline orchestrator (
|
|
370
|
+
run.md Autonomous pipeline orchestrator (dispatches to run/ stages)
|
|
371
|
+
run/ Pipeline stage files (triage, worktree, execute, pr-create)
|
|
365
372
|
milestone.md Milestone execution with dependency ordering and failed-issue recovery
|
|
366
373
|
update.md Structured GitHub comment templates
|
|
367
374
|
pr.md PR creation from GSD artifacts with phase context + plan traceability
|
|
@@ -381,6 +388,49 @@ templates/
|
|
|
381
388
|
|
|
382
389
|
For a detailed walkthrough of the directory structure, slash command anatomy, and CLI architecture, see the [Architecture Guide](docs/ARCHITECTURE.md#directory-structure).
|
|
383
390
|
|
|
391
|
+
## Post-install Behavior
|
|
392
|
+
|
|
393
|
+
When you `npm install` MGW (globally or locally), the `postinstall` script (`bin/mgw-install.cjs`) copies all slash command `.md` files from `commands/` to `~/.claude/commands/mgw/`. This makes `/mgw:*` commands available in Claude Code.
|
|
394
|
+
|
|
395
|
+
To skip this behavior (e.g., in CI or Docker):
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
npm install -g @snipcodeit/mgw --ignore-scripts
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
To re-run manually:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
node ./bin/mgw-install.cjs
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## CLI Commands
|
|
408
|
+
|
|
409
|
+
In addition to slash commands (used inside Claude Code), MGW provides standalone CLI commands:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
mgw issues # Browse GitHub issues
|
|
413
|
+
mgw sync # Reconcile .mgw/ state with GitHub
|
|
414
|
+
mgw link 42 43 # Cross-reference two issues
|
|
415
|
+
mgw log # View execution logs
|
|
416
|
+
mgw log --since 7d --metrics # Aggregated metrics for the last 7 days
|
|
417
|
+
mgw metrics # Pipeline metrics dashboard
|
|
418
|
+
mgw metrics --since 30d # Metrics over the last 30 days
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The `log` and `metrics` commands read from structured JSON-lines logs in `.mgw/logs/` that are written automatically during command execution.
|
|
422
|
+
|
|
423
|
+
## Development
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
git clone https://github.com/snipcodeit/mgw.git
|
|
427
|
+
cd mgw
|
|
428
|
+
npm install
|
|
429
|
+
npm test # 365 tests across 71 suites (Node.js built-in test runner)
|
|
430
|
+
npm run lint # ESLint
|
|
431
|
+
npm run build # pkgroll → dist/
|
|
432
|
+
```
|
|
433
|
+
|
|
384
434
|
## Documentation
|
|
385
435
|
|
|
386
436
|
| Document | Description |
|
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);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: board:configure
|
|
3
|
+
description: Update board field options by comparing current state against canonical schema
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<step name="subcommand_configure">
|
|
7
|
+
**Execute 'configure' subcommand:**
|
|
8
|
+
|
|
9
|
+
Only run if `$SUBCOMMAND = "configure"`.
|
|
10
|
+
|
|
11
|
+
Reads current field options from GitHub and compares to the canonical schema in
|
|
12
|
+
docs/BOARD-SCHEMA.md / .mgw/board-schema.json. Adds any missing options.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
if [ "$SUBCOMMAND" = "configure" ]; then
|
|
16
|
+
if [ "$BOARD_CONFIGURED" = "false" ]; then
|
|
17
|
+
echo "No board configured. Run /mgw:board create first."
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
22
|
+
echo " MGW ► BOARD CONFIGURE: ${PROJECT_NAME}"
|
|
23
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Board: #${BOARD_NUMBER} — ${BOARD_URL}"
|
|
26
|
+
echo ""
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Fetch current field state from GitHub:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
FIELDS_STATE=$(gh api graphql -f query='
|
|
33
|
+
query($owner: String!, $number: Int!) {
|
|
34
|
+
user(login: $owner) {
|
|
35
|
+
projectV2(number: $number) {
|
|
36
|
+
fields(first: 20) {
|
|
37
|
+
nodes {
|
|
38
|
+
... on ProjectV2SingleSelectField {
|
|
39
|
+
id
|
|
40
|
+
name
|
|
41
|
+
options { id name color description }
|
|
42
|
+
}
|
|
43
|
+
... on ProjectV2Field {
|
|
44
|
+
id
|
|
45
|
+
name
|
|
46
|
+
dataType
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
|
|
54
|
+
|
|
55
|
+
# Try org if user fails
|
|
56
|
+
if ! echo "$FIELDS_STATE" | python3 -c "import json,sys; d=json.load(sys.stdin); _ = d['data']['user']['projectV2']" 2>/dev/null; then
|
|
57
|
+
FIELDS_STATE=$(gh api graphql -f query='
|
|
58
|
+
query($owner: String!, $number: Int!) {
|
|
59
|
+
organization(login: $owner) {
|
|
60
|
+
projectV2(number: $number) {
|
|
61
|
+
fields(first: 20) {
|
|
62
|
+
nodes {
|
|
63
|
+
... on ProjectV2SingleSelectField {
|
|
64
|
+
id
|
|
65
|
+
name
|
|
66
|
+
options { id name color description }
|
|
67
|
+
}
|
|
68
|
+
... on ProjectV2Field {
|
|
69
|
+
id
|
|
70
|
+
name
|
|
71
|
+
dataType
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
' -f owner="$OWNER" -F number="$BOARD_NUMBER" 2>/dev/null)
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
echo "Current fields on board:"
|
|
82
|
+
echo "$FIELDS_STATE" | python3 -c "
|
|
83
|
+
import json,sys
|
|
84
|
+
d = json.load(sys.stdin)
|
|
85
|
+
data = d.get('data', {})
|
|
86
|
+
proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
|
|
87
|
+
nodes = proj.get('fields', {}).get('nodes', [])
|
|
88
|
+
for node in nodes:
|
|
89
|
+
name = node.get('name', 'unknown')
|
|
90
|
+
nid = node.get('id', 'unknown')
|
|
91
|
+
opts = node.get('options')
|
|
92
|
+
if opts is not None:
|
|
93
|
+
print(f' {name} (SINGLE_SELECT, {len(opts)} options): {nid}')
|
|
94
|
+
for opt in opts:
|
|
95
|
+
print(f' - {opt[\"name\"]} ({opt[\"color\"]}) [{opt[\"id\"]}]')
|
|
96
|
+
else:
|
|
97
|
+
dtype = node.get('dataType', 'TEXT')
|
|
98
|
+
print(f' {name} ({dtype}): {nid}')
|
|
99
|
+
" 2>/dev/null || echo " (could not fetch field details)"
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Compare with canonical schema and identify missing options:**
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Canonical Status options from BOARD-SCHEMA.md
|
|
108
|
+
CANONICAL_STATUS_OPTIONS='["New","Triaged","Needs Info","Needs Security Review","Discussing","Approved","Planning","Executing","Verifying","PR Created","Done","Failed","Blocked"]'
|
|
109
|
+
|
|
110
|
+
# Get current Status option names
|
|
111
|
+
CURRENT_STATUS_OPTIONS=$(echo "$FIELDS_STATE" | python3 -c "
|
|
112
|
+
import json,sys
|
|
113
|
+
d = json.load(sys.stdin)
|
|
114
|
+
data = d.get('data', {})
|
|
115
|
+
proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
|
|
116
|
+
nodes = proj.get('fields', {}).get('nodes', [])
|
|
117
|
+
for node in nodes:
|
|
118
|
+
if node.get('name') == 'Status' and 'options' in node:
|
|
119
|
+
print(json.dumps([o['name'] for o in node['options']]))
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
print('[]')
|
|
122
|
+
" 2>/dev/null || echo "[]")
|
|
123
|
+
|
|
124
|
+
MISSING_STATUS=$(python3 -c "
|
|
125
|
+
import json
|
|
126
|
+
canonical = json.loads('${CANONICAL_STATUS_OPTIONS}')
|
|
127
|
+
current = json.loads('''${CURRENT_STATUS_OPTIONS}''')
|
|
128
|
+
missing = [o for o in canonical if o not in current]
|
|
129
|
+
if missing:
|
|
130
|
+
print('Missing Status options: ' + ', '.join(missing))
|
|
131
|
+
else:
|
|
132
|
+
print('Status field: all options present')
|
|
133
|
+
" 2>/dev/null)
|
|
134
|
+
|
|
135
|
+
echo "Schema comparison:"
|
|
136
|
+
echo " ${MISSING_STATUS}"
|
|
137
|
+
|
|
138
|
+
# Canonical GSD Route options
|
|
139
|
+
CANONICAL_GSD_OPTIONS='["quick","quick --full","plan-phase","new-milestone"]'
|
|
140
|
+
|
|
141
|
+
CURRENT_GSD_OPTIONS=$(echo "$FIELDS_STATE" | python3 -c "
|
|
142
|
+
import json,sys
|
|
143
|
+
d = json.load(sys.stdin)
|
|
144
|
+
data = d.get('data', {})
|
|
145
|
+
proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
|
|
146
|
+
nodes = proj.get('fields', {}).get('nodes', [])
|
|
147
|
+
for node in nodes:
|
|
148
|
+
if node.get('name') == 'GSD Route' and 'options' in node:
|
|
149
|
+
print(json.dumps([o['name'] for o in node['options']]))
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
print('[]')
|
|
152
|
+
" 2>/dev/null || echo "[]")
|
|
153
|
+
|
|
154
|
+
MISSING_GSD=$(python3 -c "
|
|
155
|
+
import json
|
|
156
|
+
canonical = json.loads('${CANONICAL_GSD_OPTIONS}')
|
|
157
|
+
current = json.loads('''${CURRENT_GSD_OPTIONS}''')
|
|
158
|
+
missing = [o for o in canonical if o not in current]
|
|
159
|
+
if missing:
|
|
160
|
+
print('Missing GSD Route options: ' + ', '.join(missing))
|
|
161
|
+
else:
|
|
162
|
+
print('GSD Route field: all options present')
|
|
163
|
+
" 2>/dev/null)
|
|
164
|
+
|
|
165
|
+
echo " ${MISSING_GSD}"
|
|
166
|
+
echo ""
|
|
167
|
+
|
|
168
|
+
# Check for missing text fields
|
|
169
|
+
CURRENT_FIELD_NAMES=$(echo "$FIELDS_STATE" | python3 -c "
|
|
170
|
+
import json,sys
|
|
171
|
+
d = json.load(sys.stdin)
|
|
172
|
+
data = d.get('data', {})
|
|
173
|
+
proj = (data.get('user') or data.get('organization', {})).get('projectV2', {})
|
|
174
|
+
nodes = proj.get('fields', {}).get('nodes', [])
|
|
175
|
+
print(json.dumps([n.get('name') for n in nodes]))
|
|
176
|
+
" 2>/dev/null || echo "[]")
|
|
177
|
+
|
|
178
|
+
REQUIRED_TEXT_FIELDS='["AI Agent State","Milestone","Phase"]'
|
|
179
|
+
MISSING_TEXT=$(python3 -c "
|
|
180
|
+
import json
|
|
181
|
+
required = json.loads('${REQUIRED_TEXT_FIELDS}')
|
|
182
|
+
current = json.loads('''${CURRENT_FIELD_NAMES}''')
|
|
183
|
+
missing = [f for f in required if f not in current]
|
|
184
|
+
if missing:
|
|
185
|
+
print('Missing text fields: ' + ', '.join(missing))
|
|
186
|
+
else:
|
|
187
|
+
print('Text fields: all present')
|
|
188
|
+
" 2>/dev/null)
|
|
189
|
+
|
|
190
|
+
echo " ${MISSING_TEXT}"
|
|
191
|
+
echo ""
|
|
192
|
+
|
|
193
|
+
# Report: no automated field addition (GitHub Projects v2 API does not support
|
|
194
|
+
# updating existing single-select field options — must delete and recreate)
|
|
195
|
+
echo "Note: GitHub Projects v2 GraphQL does not support adding options to an"
|
|
196
|
+
echo "existing single-select field. To add new pipeline stages:"
|
|
197
|
+
echo " 1. Delete the existing Status field on the board UI"
|
|
198
|
+
echo " 2. Run /mgw:board create (idempotency check will be skipped for fields)"
|
|
199
|
+
echo " Or: manually add options via GitHub Projects UI at ${BOARD_URL}"
|
|
200
|
+
echo ""
|
|
201
|
+
echo "For missing text fields, run /mgw:board create (it will create missing fields)."
|
|
202
|
+
|
|
203
|
+
fi # end configure subcommand
|
|
204
|
+
```
|
|
205
|
+
</step>
|