@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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # MGW — My GSD Workflow
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@snipcodeit/mgw)](https://www.npmjs.com/package/@snipcodeit/mgw)
4
+ [![CI](https://github.com/snipcodeit/mgw/actions/workflows/ci.yml/badge.svg)](https://github.com/snipcodeit/mgw/actions/workflows/ci.yml)
4
5
  [![npm downloads](https://img.shields.io/npm/dm/@snipcodeit/mgw)](https://www.npmjs.com/package/@snipcodeit/mgw)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
  [![node](https://img.shields.io/node/v/@snipcodeit/mgw)](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
- github.cjs GitHub CLI wrappers (issues, PRs, milestones, Projects v2)
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
- state.cjs .mgw/ state management (migrateProjectState, resolveActiveMilestoneIndex)
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
- retry.cjs Retry logic for GitHub API calls
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 management
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 (cross-milestone enforcement)
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 |
@@ -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);
@@ -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>