@snipcodeit/mgw 0.2.2 → 0.3.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.
@@ -84,17 +84,21 @@ function requireState () {
84
84
  return existing;
85
85
  }
86
86
  function migrateProjectState() {
87
+ const warnings = [];
87
88
  const existing = loadProjectState();
88
- if (!existing) return null;
89
+ if (!existing) return { state: null, warnings: [] };
89
90
  let changed = false;
90
91
  if (!existing.hasOwnProperty("active_gsd_milestone")) {
91
92
  existing.active_gsd_milestone = null;
92
93
  changed = true;
94
+ warnings.push("migration: added active_gsd_milestone field");
93
95
  }
94
96
  for (const m of existing.milestones || []) {
97
+ const mLabel = m.title || m.gsd_milestone_id || "unnamed";
95
98
  if (!m.hasOwnProperty("gsd_milestone_id")) {
96
99
  m.gsd_milestone_id = null;
97
100
  changed = true;
101
+ warnings.push(`migration: added gsd_milestone_id to milestone "${mLabel}"`);
98
102
  }
99
103
  if (!m.hasOwnProperty("gsd_state")) {
100
104
  m.gsd_state = null;
@@ -113,8 +117,9 @@ function requireState () {
113
117
  let entries;
114
118
  try {
115
119
  entries = fs.readdirSync(activeDir);
116
- } catch {
120
+ } catch (err) {
117
121
  entries = [];
122
+ warnings.push(`migration: could not read active dir: ${err.message}`);
118
123
  }
119
124
  for (const file of entries) {
120
125
  if (!file.endsWith(".json")) continue;
@@ -122,7 +127,8 @@ function requireState () {
122
127
  let issueState;
123
128
  try {
124
129
  issueState = JSON.parse(fs.readFileSync(filePath, "utf-8"));
125
- } catch {
130
+ } catch (err) {
131
+ warnings.push(`migration: skipping unreadable ${file}: ${err.message}`);
126
132
  continue;
127
133
  }
128
134
  let issueChanged = false;
@@ -137,12 +143,13 @@ function requireState () {
137
143
  if (issueChanged) {
138
144
  try {
139
145
  fs.writeFileSync(filePath, JSON.stringify(issueState, null, 2), "utf-8");
140
- } catch {
146
+ } catch (err) {
147
+ warnings.push(`migration: failed to write ${file}: ${err.message}`);
141
148
  }
142
149
  }
143
150
  }
144
151
  }
145
- return existing;
152
+ return { state: existing, warnings };
146
153
  }
147
154
  function resolveActiveMilestoneIndex(state) {
148
155
  if (!state) return -1;
@@ -157,6 +164,159 @@ function requireState () {
157
164
  }
158
165
  return -1;
159
166
  }
167
+ const VALID_LINK_TYPES = /* @__PURE__ */ new Set(["related", "implements", "tracks", "maps-to", "blocked-by"]);
168
+ function loadCrossRefs() {
169
+ const filePath = path.join(getMgwDir(), "cross-refs.json");
170
+ const warnings = [];
171
+ if (!fs.existsSync(filePath)) {
172
+ return { links: [], warnings: [] };
173
+ }
174
+ let raw;
175
+ try {
176
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
177
+ } catch (err) {
178
+ return { links: [], warnings: [`cross-refs.json parse error: ${err.message}`] };
179
+ }
180
+ if (!raw || !Array.isArray(raw.links)) {
181
+ return { links: [], warnings: ["cross-refs.json missing links array"] };
182
+ }
183
+ const validLinks = [];
184
+ for (let i = 0; i < raw.links.length; i++) {
185
+ const link = raw.links[i];
186
+ const issues = [];
187
+ if (!link || typeof link !== "object") {
188
+ warnings.push(`cross-refs link[${i}]: not an object, skipping`);
189
+ continue;
190
+ }
191
+ if (typeof link.a !== "string" || !link.a) {
192
+ issues.push('missing "a"');
193
+ }
194
+ if (typeof link.b !== "string" || !link.b) {
195
+ issues.push('missing "b"');
196
+ }
197
+ if (link.type && !VALID_LINK_TYPES.has(link.type)) {
198
+ issues.push(`unknown type "${link.type}"`);
199
+ }
200
+ if (issues.length > 0) {
201
+ warnings.push(`cross-refs link[${i}]: ${issues.join(", ")}, skipping`);
202
+ continue;
203
+ }
204
+ validLinks.push(link);
205
+ }
206
+ return { links: validLinks, warnings };
207
+ }
208
+ function parseDependencies(body) {
209
+ if (!body || typeof body !== "string") return [];
210
+ const deps = /* @__PURE__ */ new Set();
211
+ const linePattern = /(?:depends[\s-]*on|blocked[\s-]*by)[:\s]+([^\n]+)/gi;
212
+ let match;
213
+ while ((match = linePattern.exec(body)) !== null) {
214
+ const refs = match[1];
215
+ const numPattern = /#(\d+)/g;
216
+ let numMatch;
217
+ while ((numMatch = numPattern.exec(refs)) !== null) {
218
+ deps.add(parseInt(numMatch[1], 10));
219
+ }
220
+ }
221
+ return Array.from(deps).sort((a, b) => a - b);
222
+ }
223
+ function storeDependencies(issueNumber, dependsOn) {
224
+ if (!dependsOn || dependsOn.length === 0) return { added: 0, existing: 0 };
225
+ const crossRefsPath = path.join(getMgwDir(), "cross-refs.json");
226
+ let crossRefs = { links: [] };
227
+ if (fs.existsSync(crossRefsPath)) {
228
+ try {
229
+ crossRefs = JSON.parse(fs.readFileSync(crossRefsPath, "utf-8"));
230
+ if (!Array.isArray(crossRefs.links)) crossRefs.links = [];
231
+ } catch {
232
+ crossRefs = { links: [] };
233
+ }
234
+ }
235
+ let added = 0;
236
+ let existing = 0;
237
+ const issueRef = `#${issueNumber}`;
238
+ for (const dep of dependsOn) {
239
+ const depRef = `#${dep}`;
240
+ const alreadyExists = crossRefs.links.some(
241
+ (l) => l.a === issueRef && l.b === depRef && l.type === "blocked-by"
242
+ );
243
+ if (alreadyExists) {
244
+ existing++;
245
+ continue;
246
+ }
247
+ crossRefs.links.push({
248
+ a: issueRef,
249
+ b: depRef,
250
+ type: "blocked-by",
251
+ created: (/* @__PURE__ */ new Date()).toISOString()
252
+ });
253
+ added++;
254
+ }
255
+ if (added > 0) {
256
+ const mgwDir = getMgwDir();
257
+ if (!fs.existsSync(mgwDir)) {
258
+ fs.mkdirSync(mgwDir, { recursive: true });
259
+ }
260
+ fs.writeFileSync(crossRefsPath, JSON.stringify(crossRefs, null, 2), "utf-8");
261
+ }
262
+ return { added, existing };
263
+ }
264
+ function topologicalSort(issues, links) {
265
+ if (!issues || issues.length === 0) return [];
266
+ if (!links || links.length === 0) return [...issues];
267
+ const deps = /* @__PURE__ */ new Map();
268
+ const issueSet = new Set(issues.map((i) => i.number));
269
+ for (const issue of issues) {
270
+ deps.set(issue.number, /* @__PURE__ */ new Set());
271
+ }
272
+ for (const link of links) {
273
+ if (link.type !== "blocked-by") continue;
274
+ const aMatch = String(link.a).match(/#(\d+)/);
275
+ const bMatch = String(link.b).match(/#(\d+)/);
276
+ if (!aMatch || !bMatch) continue;
277
+ const a = parseInt(aMatch[1], 10);
278
+ const b = parseInt(bMatch[1], 10);
279
+ if (issueSet.has(a) && issueSet.has(b) && deps.has(a)) {
280
+ deps.get(a).add(b);
281
+ }
282
+ }
283
+ const inDegree = /* @__PURE__ */ new Map();
284
+ for (const issue of issues) {
285
+ inDegree.set(issue.number, deps.get(issue.number).size);
286
+ }
287
+ const queue = [];
288
+ for (const [num, degree] of inDegree) {
289
+ if (degree === 0) queue.push(num);
290
+ }
291
+ const issueMap = new Map(issues.map((i) => [i.number, i]));
292
+ const sorted = [];
293
+ while (queue.length > 0) {
294
+ queue.sort((a, b) => {
295
+ const idxA = issues.findIndex((i) => i.number === a);
296
+ const idxB = issues.findIndex((i) => i.number === b);
297
+ return idxA - idxB;
298
+ });
299
+ const num = queue.shift();
300
+ sorted.push(issueMap.get(num));
301
+ for (const [dependent, depSet] of deps) {
302
+ if (depSet.has(num)) {
303
+ depSet.delete(num);
304
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
305
+ if (inDegree.get(dependent) === 0) {
306
+ queue.push(dependent);
307
+ }
308
+ }
309
+ }
310
+ }
311
+ if (sorted.length < issues.length) {
312
+ for (const issue of issues) {
313
+ if (!sorted.includes(issue)) {
314
+ sorted.push(issue);
315
+ }
316
+ }
317
+ }
318
+ return sorted;
319
+ }
160
320
  state = {
161
321
  getMgwDir,
162
322
  getActiveDir,
@@ -166,11 +326,260 @@ function requireState () {
166
326
  loadActiveIssue,
167
327
  mergeProjectState,
168
328
  migrateProjectState,
169
- resolveActiveMilestoneIndex
329
+ resolveActiveMilestoneIndex,
330
+ loadCrossRefs,
331
+ VALID_LINK_TYPES,
332
+ parseDependencies,
333
+ storeDependencies,
334
+ topologicalSort
170
335
  };
171
336
  return state;
172
337
  }
173
338
 
339
+ var errors;
340
+ var hasRequiredErrors;
341
+
342
+ function requireErrors () {
343
+ if (hasRequiredErrors) return errors;
344
+ hasRequiredErrors = 1;
345
+ class MgwError extends Error {
346
+ /**
347
+ * @param {string} message
348
+ * @param {object} [opts]
349
+ * @param {string} [opts.code] - Machine-readable error code
350
+ * @param {string} [opts.stage] - Pipeline stage where error occurred
351
+ * @param {number} [opts.issueNumber] - Related GitHub issue number
352
+ * @param {Error} [opts.cause] - Original error
353
+ */
354
+ constructor(message, opts) {
355
+ super(message);
356
+ this.name = "MgwError";
357
+ const o = opts || {};
358
+ this.code = o.code || "MGW_ERROR";
359
+ this.stage = o.stage || null;
360
+ this.issueNumber = o.issueNumber || null;
361
+ if (o.cause) this.cause = o.cause;
362
+ }
363
+ }
364
+ class GitHubApiError extends MgwError {
365
+ /**
366
+ * @param {string} message
367
+ * @param {object} [opts]
368
+ * @param {number} [opts.status] - HTTP status code
369
+ * @param {string} [opts.endpoint] - API endpoint or gh subcommand
370
+ */
371
+ constructor(message, opts) {
372
+ const o = opts || {};
373
+ super(message, { code: "GITHUB_API_ERROR", ...o });
374
+ this.name = "GitHubApiError";
375
+ this.status = o.status || null;
376
+ this.endpoint = o.endpoint || null;
377
+ }
378
+ }
379
+ class GsdToolError extends MgwError {
380
+ /**
381
+ * @param {string} message
382
+ * @param {object} [opts]
383
+ * @param {string} [opts.command] - GSD subcommand that failed
384
+ */
385
+ constructor(message, opts) {
386
+ const o = opts || {};
387
+ super(message, { code: "GSD_TOOL_ERROR", ...o });
388
+ this.name = "GsdToolError";
389
+ this.command = o.command || null;
390
+ }
391
+ }
392
+ class StateError extends MgwError {
393
+ /**
394
+ * @param {string} message
395
+ * @param {object} [opts]
396
+ * @param {string} [opts.filePath] - State file path
397
+ */
398
+ constructor(message, opts) {
399
+ const o = opts || {};
400
+ super(message, { code: "STATE_ERROR", ...o });
401
+ this.name = "StateError";
402
+ this.filePath = o.filePath || null;
403
+ }
404
+ }
405
+ class TimeoutError extends MgwError {
406
+ /**
407
+ * @param {string} message
408
+ * @param {object} [opts]
409
+ * @param {number} [opts.timeoutMs] - Timeout duration in milliseconds
410
+ * @param {string} [opts.operation] - Description of the timed-out operation
411
+ */
412
+ constructor(message, opts) {
413
+ const o = opts || {};
414
+ super(message, { code: "TIMEOUT_ERROR", ...o });
415
+ this.name = "TimeoutError";
416
+ this.timeoutMs = o.timeoutMs || null;
417
+ this.operation = o.operation || null;
418
+ }
419
+ }
420
+ class ClaudeNotAvailableError extends MgwError {
421
+ /**
422
+ * @param {string} message
423
+ * @param {object} [opts]
424
+ * @param {'not-installed'|'not-authenticated'|'check-failed'} [opts.reason]
425
+ */
426
+ constructor(message, opts) {
427
+ const o = opts || {};
428
+ super(message, { code: "CLAUDE_NOT_AVAILABLE", ...o });
429
+ this.name = "ClaudeNotAvailableError";
430
+ this.reason = o.reason || null;
431
+ }
432
+ }
433
+ errors = {
434
+ MgwError,
435
+ GitHubApiError,
436
+ GsdToolError,
437
+ StateError,
438
+ TimeoutError,
439
+ ClaudeNotAvailableError
440
+ };
441
+ return errors;
442
+ }
443
+
444
+ var retry;
445
+ var hasRequiredRetry;
446
+
447
+ function requireRetry () {
448
+ if (hasRequiredRetry) return retry;
449
+ hasRequiredRetry = 1;
450
+ const MAX_RETRIES = 3;
451
+ const BACKOFF_BASE_MS = 5e3;
452
+ const BACKOFF_MAX_MS = 3e5;
453
+ const TRANSIENT_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
454
+ const TRANSIENT_MESSAGE_PATTERNS = [
455
+ "network timeout",
456
+ "econnreset",
457
+ "econnrefused",
458
+ "etimedout",
459
+ "socket hang up",
460
+ "worktree lock",
461
+ "model overload",
462
+ "rate limit",
463
+ "too many requests",
464
+ "service unavailable",
465
+ "bad gateway",
466
+ "gateway timeout"
467
+ ];
468
+ const NEEDS_INFO_MESSAGE_PATTERNS = [
469
+ "ambiguous",
470
+ "missing required field",
471
+ "contradictory requirements",
472
+ "issue body"
473
+ ];
474
+ function classifyFailure(error) {
475
+ if (!error || typeof error !== "object") {
476
+ return { class: "permanent", reason: "no error object provided" };
477
+ }
478
+ const status = error.status;
479
+ const message = (error.message || "").toLowerCase();
480
+ const code = (error.code || "").toLowerCase();
481
+ if (typeof status === "number") {
482
+ if (status === 429) {
483
+ return { class: "transient", reason: "rate limit (HTTP 429)" };
484
+ }
485
+ if (TRANSIENT_STATUS_CODES.has(status)) {
486
+ return { class: "transient", reason: `server error (HTTP ${status})` };
487
+ }
488
+ if (status === 403) {
489
+ return { class: "permanent", reason: "forbidden (HTTP 403 \u2014 non-rate-limit)" };
490
+ }
491
+ if (status >= 400 && status < 500) {
492
+ return { class: "permanent", reason: `client error (HTTP ${status})` };
493
+ }
494
+ }
495
+ if (code) {
496
+ const networkCodes = /* @__PURE__ */ new Set([
497
+ "econnreset",
498
+ "econnrefused",
499
+ "etimedout",
500
+ "enotfound",
501
+ "epipe"
502
+ ]);
503
+ if (networkCodes.has(code)) {
504
+ return { class: "transient", reason: `network error (${code.toUpperCase()})` };
505
+ }
506
+ if (code === "enoent") {
507
+ return { class: "permanent", reason: "file not found (ENOENT) \u2014 GSD tools may be missing" };
508
+ }
509
+ }
510
+ for (const pattern of TRANSIENT_MESSAGE_PATTERNS) {
511
+ if (message.includes(pattern)) {
512
+ return { class: "transient", reason: `transient condition detected: "${pattern}"` };
513
+ }
514
+ }
515
+ for (const pattern of NEEDS_INFO_MESSAGE_PATTERNS) {
516
+ if (message.includes(pattern)) {
517
+ return { class: "needs-info", reason: `issue requires clarification: "${pattern}"` };
518
+ }
519
+ }
520
+ return {
521
+ class: "permanent",
522
+ reason: "unknown error \u2014 classified as permanent to prevent runaway retries"
523
+ };
524
+ }
525
+ function canRetry(issueState) {
526
+ if (!issueState || typeof issueState !== "object") return false;
527
+ if (issueState.dead_letter === true) return false;
528
+ const count = typeof issueState.retry_count === "number" ? issueState.retry_count : 0;
529
+ return count < MAX_RETRIES;
530
+ }
531
+ function incrementRetry(issueState) {
532
+ const current = typeof issueState.retry_count === "number" ? issueState.retry_count : 0;
533
+ return Object.assign({}, issueState, { retry_count: current + 1 });
534
+ }
535
+ function resetRetryState(issueState) {
536
+ return Object.assign({}, issueState, {
537
+ retry_count: 0,
538
+ last_failure_class: null,
539
+ dead_letter: false
540
+ });
541
+ }
542
+ function getBackoffMs(retryCount) {
543
+ const count = Math.max(0, Math.floor(retryCount));
544
+ const base = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * Math.pow(2, count));
545
+ return Math.floor(Math.random() * (base + 1));
546
+ }
547
+ async function withRetry(fn, opts) {
548
+ const o = opts || {};
549
+ const maxAttempts = (typeof o.maxRetries === "number" ? o.maxRetries : MAX_RETRIES) + 1;
550
+ let lastError;
551
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
552
+ try {
553
+ return await fn();
554
+ } catch (err) {
555
+ lastError = err;
556
+ if (attempt >= maxAttempts - 1) break;
557
+ const classification = classifyFailure(err);
558
+ if (classification.class !== "transient") break;
559
+ const delayMs = getBackoffMs(attempt);
560
+ if (delayMs > 0) {
561
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
562
+ }
563
+ }
564
+ }
565
+ throw lastError;
566
+ }
567
+ retry = {
568
+ // Constants
569
+ MAX_RETRIES,
570
+ BACKOFF_BASE_MS,
571
+ BACKOFF_MAX_MS,
572
+ // Core functions
573
+ classifyFailure,
574
+ canRetry,
575
+ incrementRetry,
576
+ resetRetryState,
577
+ getBackoffMs,
578
+ withRetry
579
+ };
580
+ return retry;
581
+ }
582
+
174
583
  var github;
175
584
  var hasRequiredGithub;
176
585
 
@@ -178,22 +587,51 @@ function requireGithub () {
178
587
  if (hasRequiredGithub) return github;
179
588
  hasRequiredGithub = 1;
180
589
  const { execSync } = require$$0$1;
590
+ const { TimeoutError, GitHubApiError } = requireErrors();
591
+ const { withRetry } = requireRetry();
592
+ const GH_TIMEOUT_MS = 3e4;
593
+ function parseHttpStatus(stderr) {
594
+ if (!stderr) return null;
595
+ const match = stderr.match(/HTTP (\d{3})/);
596
+ return match ? parseInt(match[1], 10) : null;
597
+ }
181
598
  function run(cmd) {
182
- return execSync(cmd, {
183
- encoding: "utf-8",
184
- stdio: ["pipe", "pipe", "pipe"]
185
- }).trim();
599
+ try {
600
+ return execSync(cmd, {
601
+ encoding: "utf-8",
602
+ stdio: ["pipe", "pipe", "pipe"],
603
+ timeout: GH_TIMEOUT_MS
604
+ }).trim();
605
+ } catch (err) {
606
+ if (err.killed) {
607
+ throw new TimeoutError(
608
+ `gh command timed out after ${GH_TIMEOUT_MS / 1e3}s: ${cmd.slice(0, 80)}`,
609
+ { timeoutMs: GH_TIMEOUT_MS, operation: cmd.slice(0, 120) }
610
+ );
611
+ }
612
+ const stderr = (err.stderr || "").trim();
613
+ const httpStatus = parseHttpStatus(stderr);
614
+ const ghErr = new GitHubApiError(stderr || err.message, {
615
+ cause: err,
616
+ status: httpStatus
617
+ });
618
+ if (err.code) ghErr.code = err.code;
619
+ throw ghErr;
620
+ }
621
+ }
622
+ async function runWithRetry(cmd) {
623
+ return withRetry(async () => run(cmd));
186
624
  }
187
- function getRepo() {
188
- return run("gh repo view --json nameWithOwner -q .nameWithOwner");
625
+ async function getRepo() {
626
+ return runWithRetry("gh repo view --json nameWithOwner -q .nameWithOwner");
189
627
  }
190
- function getIssue(number) {
191
- const raw = run(
628
+ async function getIssue(number) {
629
+ const raw = await runWithRetry(
192
630
  `gh issue view ${number} --json number,title,state,labels,milestone,assignees,body`
193
631
  );
194
632
  return JSON.parse(raw);
195
633
  }
196
- function listIssues(filters) {
634
+ async function listIssues(filters) {
197
635
  const f = filters || {};
198
636
  let cmd = "gh issue list --json number,title,state,labels,milestone,assignees,createdAt,url,body,comments";
199
637
  if (f.label) cmd += ` --label ${JSON.stringify(f.label)}`;
@@ -201,16 +639,16 @@ function requireGithub () {
201
639
  if (f.assignee && f.assignee !== "all") cmd += ` --assignee ${JSON.stringify(f.assignee)}`;
202
640
  if (f.state) cmd += ` --state ${f.state}`;
203
641
  if (f.limit) cmd += ` --limit ${parseInt(f.limit, 10)}`;
204
- const raw = run(cmd);
642
+ const raw = await runWithRetry(cmd);
205
643
  return JSON.parse(raw);
206
644
  }
207
- function getMilestone(number) {
208
- const repo = getRepo();
209
- const raw = run(`gh api repos/${repo}/milestones/${number}`);
645
+ async function getMilestone(number) {
646
+ const repo = await getRepo();
647
+ const raw = await runWithRetry(`gh api repos/${repo}/milestones/${number}`);
210
648
  return JSON.parse(raw);
211
649
  }
212
- function getRateLimit() {
213
- const raw = run("gh api rate_limit");
650
+ async function getRateLimit() {
651
+ const raw = await runWithRetry("gh api rate_limit");
214
652
  const data = JSON.parse(raw);
215
653
  const core = data.resources.core;
216
654
  return {
@@ -219,21 +657,21 @@ function requireGithub () {
219
657
  reset: core.reset
220
658
  };
221
659
  }
222
- function closeMilestone(repo, number) {
223
- const raw = run(
660
+ async function closeMilestone(repo, number) {
661
+ const raw = await runWithRetry(
224
662
  `gh api repos/${repo}/milestones/${number} --method PATCH -f state=closed`
225
663
  );
226
664
  return JSON.parse(raw);
227
665
  }
228
- function createRelease(repo, tag, title, opts) {
666
+ async function createRelease(repo, tag, title, opts) {
229
667
  const o = opts || {};
230
668
  let cmd = `gh release create ${JSON.stringify(tag)} --repo ${JSON.stringify(repo)} --title ${JSON.stringify(title)}`;
231
669
  if (o.notes) cmd += ` --notes ${JSON.stringify(o.notes)}`;
232
670
  if (o.draft) cmd += " --draft";
233
671
  if (o.prerelease) cmd += " --prerelease";
234
- return run(cmd);
672
+ return runWithRetry(cmd);
235
673
  }
236
- function getProjectNodeId(owner, projectNumber) {
674
+ async function getProjectNodeId(owner, projectNumber) {
237
675
  const userQuery = `'query($login: String!, $number: Int!) { user(login: $login) { projectV2(number: $number) { id } } }'`;
238
676
  const orgQuery = `'query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { id } } }'`;
239
677
  try {
@@ -252,7 +690,7 @@ function requireGithub () {
252
690
  }
253
691
  return null;
254
692
  }
255
- function findExistingBoard(owner, titlePattern) {
693
+ async function findExistingBoard(owner, titlePattern) {
256
694
  const pattern = titlePattern.toLowerCase();
257
695
  try {
258
696
  const raw = run(
@@ -274,7 +712,7 @@ function requireGithub () {
274
712
  }
275
713
  return null;
276
714
  }
277
- function getProjectFields(owner, projectNumber) {
715
+ async function getProjectFields(owner, projectNumber) {
278
716
  const query = `'query($login: String!, $number: Int!) { user(login: $login) { projectV2(number: $number) { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } ... on ProjectV2Field { id name dataType } } } } } }'`;
279
717
  const orgQuery = `'query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } ... on ProjectV2Field { id name dataType } } } } } }'`;
280
718
  let raw;
@@ -352,19 +790,19 @@ function requireGithub () {
352
790
  }
353
791
  return Object.keys(fields).length > 0 ? fields : null;
354
792
  }
355
- function createProject(owner, title) {
356
- const raw = run(
793
+ async function createProject(owner, title) {
794
+ const raw = await runWithRetry(
357
795
  `gh project create --owner ${JSON.stringify(owner)} --title ${JSON.stringify(title)} --format json`
358
796
  );
359
797
  const data = JSON.parse(raw);
360
798
  return { number: data.number, url: data.url };
361
799
  }
362
- function addItemToProject(owner, projectNumber, issueUrl) {
363
- return run(
800
+ async function addItemToProject(owner, projectNumber, issueUrl) {
801
+ return runWithRetry(
364
802
  `gh project item-add ${projectNumber} --owner ${JSON.stringify(owner)} --url ${JSON.stringify(issueUrl)}`
365
803
  );
366
804
  }
367
- function postMilestoneStartAnnouncement(opts) {
805
+ async function postMilestoneStartAnnouncement(opts) {
368
806
  const {
369
807
  repo,
370
808
  milestoneName,
@@ -435,6 +873,8 @@ function requireGithub () {
435
873
  return { posted: false, method: "none", url: null };
436
874
  }
437
875
  github = {
876
+ GH_TIMEOUT_MS,
877
+ run,
438
878
  getRepo,
439
879
  getIssue,
440
880
  listIssues,
@@ -526,6 +966,7 @@ function requireClaude () {
526
966
  const { execSync, spawn } = require$$0$1;
527
967
  const path = require$$1;
528
968
  const fs = require$$0;
969
+ const { ClaudeNotAvailableError, TimeoutError } = requireErrors();
529
970
  function getCommandsDir() {
530
971
  const dir = path.join(__dirname, "..", "commands");
531
972
  if (!fs.existsSync(dir)) {
@@ -540,30 +981,44 @@ This may indicate a corrupted installation. Try reinstalling mgw.`
540
981
  try {
541
982
  execSync("claude --version", {
542
983
  encoding: "utf-8",
543
- stdio: ["pipe", "pipe", "pipe"]
984
+ stdio: ["pipe", "pipe", "pipe"],
985
+ timeout: 1e4
544
986
  });
545
987
  } catch (err) {
546
- if (err.code === "ENOENT") {
547
- console.error(
548
- "Error: claude CLI is not installed.\n\nInstall it with:\n npm install -g @anthropic-ai/claude-code\n\nThen run:\n claude login"
988
+ if (err.killed) {
989
+ throw new TimeoutError(
990
+ "claude --version timed out after 10s",
991
+ { timeoutMs: 1e4, operation: "claude --version" }
549
992
  );
550
- } else {
551
- console.error(
552
- "Error: claude CLI check failed.\nEnsure claude is installed and on your PATH."
993
+ }
994
+ if (err.code === "ENOENT") {
995
+ throw new ClaudeNotAvailableError(
996
+ "claude CLI is not installed.\n\nInstall it with:\n npm install -g @anthropic-ai/claude-code\n\nThen run:\n claude login",
997
+ { reason: "not-installed" }
553
998
  );
554
999
  }
555
- process.exit(1);
1000
+ throw new ClaudeNotAvailableError(
1001
+ "claude CLI check failed.\nEnsure claude is installed and on your PATH.",
1002
+ { reason: "check-failed", cause: err }
1003
+ );
556
1004
  }
557
1005
  try {
558
1006
  execSync("claude auth status", {
559
1007
  encoding: "utf-8",
560
- stdio: ["pipe", "pipe", "pipe"]
1008
+ stdio: ["pipe", "pipe", "pipe"],
1009
+ timeout: 1e4
561
1010
  });
562
- } catch {
563
- console.error(
564
- "Error: claude CLI is not authenticated.\n\nRun:\n claude login\n\nThen retry your command."
1011
+ } catch (err) {
1012
+ if (err.killed) {
1013
+ throw new TimeoutError(
1014
+ "claude auth status timed out after 10s",
1015
+ { timeoutMs: 1e4, operation: "claude auth status" }
1016
+ );
1017
+ }
1018
+ throw new ClaudeNotAvailableError(
1019
+ "claude CLI is not authenticated.\n\nRun:\n claude login\n\nThen retry your command.",
1020
+ { reason: "not-authenticated" }
565
1021
  );
566
- process.exit(1);
567
1022
  }
568
1023
  }
569
1024
  function invokeClaude(commandFile, userPrompt, opts) {
@@ -587,6 +1042,10 @@ This may indicate a corrupted installation. Try reinstalling mgw.`
587
1042
  const stdio = o.quiet ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
588
1043
  const child = spawn("claude", args, { stdio });
589
1044
  let output = "";
1045
+ const sigintHandler = () => {
1046
+ child.kill("SIGINT");
1047
+ };
1048
+ process.on("SIGINT", sigintHandler);
590
1049
  if (o.quiet) {
591
1050
  child.stdout.on("data", (chunk) => {
592
1051
  output += chunk.toString();
@@ -596,13 +1055,18 @@ This may indicate a corrupted installation. Try reinstalling mgw.`
596
1055
  });
597
1056
  }
598
1057
  child.on("error", (err) => {
1058
+ process.removeListener("SIGINT", sigintHandler);
599
1059
  if (err.code === "ENOENT") {
600
- reject(new Error("claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
1060
+ reject(new ClaudeNotAvailableError(
1061
+ "claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code",
1062
+ { reason: "not-installed" }
1063
+ ));
601
1064
  } else {
602
1065
  reject(err);
603
1066
  }
604
1067
  });
605
1068
  child.on("close", (code) => {
1069
+ process.removeListener("SIGINT", sigintHandler);
606
1070
  resolve({ exitCode: code || 0, output });
607
1071
  });
608
1072
  });
@@ -716,6 +1180,160 @@ function requireSpinner () {
716
1180
  return spinner;
717
1181
  }
718
1182
 
1183
+ var logger;
1184
+ var hasRequiredLogger;
1185
+
1186
+ function requireLogger () {
1187
+ if (hasRequiredLogger) return logger;
1188
+ hasRequiredLogger = 1;
1189
+ const path = require$$1;
1190
+ const fs = require$$0;
1191
+ function getLogDir(repoRoot) {
1192
+ const root = repoRoot || process.cwd();
1193
+ const logDir = path.join(root, ".mgw", "logs");
1194
+ if (!fs.existsSync(logDir)) {
1195
+ fs.mkdirSync(logDir, { recursive: true });
1196
+ }
1197
+ return logDir;
1198
+ }
1199
+ function getLogFile(repoRoot) {
1200
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1201
+ return path.join(getLogDir(repoRoot), `${date}.jsonl`);
1202
+ }
1203
+ function writeLog(entry) {
1204
+ const { repoRoot, ...rest } = entry;
1205
+ const record = {
1206
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1207
+ ...rest
1208
+ };
1209
+ try {
1210
+ const logFile = getLogFile(repoRoot);
1211
+ fs.appendFileSync(logFile, JSON.stringify(record) + "\n");
1212
+ } catch {
1213
+ }
1214
+ }
1215
+ function startTimer(entry) {
1216
+ const start = Date.now();
1217
+ return {
1218
+ finish(status, errorMsg) {
1219
+ writeLog({
1220
+ ...entry,
1221
+ duration_ms: Date.now() - start,
1222
+ status,
1223
+ error: errorMsg || void 0
1224
+ });
1225
+ }
1226
+ };
1227
+ }
1228
+ function readLogs(opts) {
1229
+ const o = opts || {};
1230
+ const logDir = getLogDir(o.repoRoot);
1231
+ if (!fs.existsSync(logDir)) return [];
1232
+ let sinceDate = null;
1233
+ if (o.since) {
1234
+ const relativeMatch = o.since.match(/^(\d+)d$/);
1235
+ if (relativeMatch) {
1236
+ sinceDate = /* @__PURE__ */ new Date();
1237
+ sinceDate.setDate(sinceDate.getDate() - parseInt(relativeMatch[1], 10));
1238
+ } else {
1239
+ sinceDate = new Date(o.since);
1240
+ }
1241
+ }
1242
+ let files;
1243
+ try {
1244
+ files = fs.readdirSync(logDir).filter((f) => f.endsWith(".jsonl")).sort();
1245
+ } catch {
1246
+ return [];
1247
+ }
1248
+ if (sinceDate) {
1249
+ const sinceStr = sinceDate.toISOString().slice(0, 10);
1250
+ files = files.filter((f) => f.replace(".jsonl", "") >= sinceStr);
1251
+ }
1252
+ const entries = [];
1253
+ for (const file of files) {
1254
+ const filePath = path.join(logDir, file);
1255
+ let content;
1256
+ try {
1257
+ content = fs.readFileSync(filePath, "utf-8");
1258
+ } catch {
1259
+ continue;
1260
+ }
1261
+ for (const line of content.split("\n")) {
1262
+ if (!line.trim()) continue;
1263
+ let entry;
1264
+ try {
1265
+ entry = JSON.parse(line);
1266
+ } catch {
1267
+ continue;
1268
+ }
1269
+ if (o.issue && entry.issue !== o.issue) continue;
1270
+ if (o.command && entry.command !== o.command) continue;
1271
+ if (o.stage && entry.stage !== o.stage) continue;
1272
+ entries.push(entry);
1273
+ }
1274
+ }
1275
+ entries.reverse();
1276
+ if (o.limit && entries.length > o.limit) {
1277
+ return entries.slice(0, o.limit);
1278
+ }
1279
+ return entries;
1280
+ }
1281
+ function aggregateMetrics(entries) {
1282
+ if (!entries || entries.length === 0) {
1283
+ return {
1284
+ total: 0,
1285
+ byStatus: {},
1286
+ byCommand: {},
1287
+ avgDuration: 0,
1288
+ failureRate: 0
1289
+ };
1290
+ }
1291
+ const byStatus = {};
1292
+ const byCommand = {};
1293
+ let totalDuration = 0;
1294
+ let durationCount = 0;
1295
+ let failures = 0;
1296
+ for (const e of entries) {
1297
+ byStatus[e.status] = (byStatus[e.status] || 0) + 1;
1298
+ if (e.status === "error") failures++;
1299
+ if (e.command) {
1300
+ if (!byCommand[e.command]) {
1301
+ byCommand[e.command] = { count: 0, errors: 0, totalDuration: 0 };
1302
+ }
1303
+ byCommand[e.command].count++;
1304
+ if (e.status === "error") byCommand[e.command].errors++;
1305
+ if (typeof e.duration_ms === "number") {
1306
+ byCommand[e.command].totalDuration += e.duration_ms;
1307
+ }
1308
+ }
1309
+ if (typeof e.duration_ms === "number") {
1310
+ totalDuration += e.duration_ms;
1311
+ durationCount++;
1312
+ }
1313
+ }
1314
+ for (const cmd of Object.keys(byCommand)) {
1315
+ const c = byCommand[cmd];
1316
+ c.avgDuration = c.count > 0 ? Math.round(c.totalDuration / c.count) : 0;
1317
+ }
1318
+ return {
1319
+ total: entries.length,
1320
+ byStatus,
1321
+ byCommand,
1322
+ avgDuration: durationCount > 0 ? Math.round(totalDuration / durationCount) : 0,
1323
+ failureRate: entries.length > 0 ? Math.round(failures / entries.length * 100) : 0
1324
+ };
1325
+ }
1326
+ logger = {
1327
+ getLogDir,
1328
+ getLogFile,
1329
+ writeLog,
1330
+ startTimer,
1331
+ readLogs,
1332
+ aggregateMetrics
1333
+ };
1334
+ return logger;
1335
+ }
1336
+
719
1337
  var renderer;
720
1338
  var hasRequiredRenderer;
721
1339
 
@@ -1545,7 +2163,7 @@ function requireGraceful () {
1545
2163
  let ms;
1546
2164
  try {
1547
2165
  ms = Date.now() - new Date(dateStr).getTime();
1548
- } catch (e) {
2166
+ } catch (_e) {
1549
2167
  return "-";
1550
2168
  }
1551
2169
  if (ms < 0) return "now";
@@ -1809,8 +2427,11 @@ function requireTui () {
1809
2427
 
1810
2428
  exports.getDefaultExportFromCjs = getDefaultExportFromCjs;
1811
2429
  exports.requireClaude = requireClaude;
2430
+ exports.requireErrors = requireErrors;
1812
2431
  exports.requireGithub = requireGithub;
2432
+ exports.requireLogger = requireLogger;
1813
2433
  exports.requireOutput = requireOutput;
2434
+ exports.requireRetry = requireRetry;
1814
2435
  exports.requireSpinner = requireSpinner;
1815
2436
  exports.requireState = requireState;
1816
2437
  exports.requireTui = requireTui;