@snipcodeit/mgw 0.4.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/commands/run/execute.md +456 -24
- package/commands/run/pr-create.md +46 -0
- package/commands/run/triage.md +199 -2
- package/commands/workflows/gsd.md +71 -0
- package/commands/workflows/state.md +340 -1
- package/dist/bin/mgw.cjs +2 -2
- package/dist/{index-B-_JvYpz.cjs → index-CHrVAIMY.cjs} +197 -1
- package/dist/lib/index.cjs +524 -2
- package/package.json +9 -4
package/dist/lib/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index$1 = require('../index-
|
|
3
|
+
var index$1 = require('../index-CHrVAIMY.cjs');
|
|
4
4
|
var require$$0 = require('child_process');
|
|
5
5
|
var require$$1 = require('path');
|
|
6
6
|
var require$$3 = require('os');
|
|
@@ -145,6 +145,500 @@ function requirePipeline () {
|
|
|
145
145
|
return pipeline;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
var agentErrors;
|
|
149
|
+
var hasRequiredAgentErrors;
|
|
150
|
+
|
|
151
|
+
function requireAgentErrors () {
|
|
152
|
+
if (hasRequiredAgentErrors) return agentErrors;
|
|
153
|
+
hasRequiredAgentErrors = 1;
|
|
154
|
+
const { MgwError } = index$1.requireErrors();
|
|
155
|
+
const SEVERITY_LEVELS = Object.freeze({
|
|
156
|
+
critical: { name: "critical", weight: 4, description: "Agent produced dangerous or misleading results" },
|
|
157
|
+
high: { name: "high", weight: 3, description: "Agent failed to produce usable output" },
|
|
158
|
+
medium: { name: "medium", weight: 2, description: "Agent partially succeeded but gaps remain" },
|
|
159
|
+
low: { name: "low", weight: 1, description: "Minor issue, likely recoverable automatically" }
|
|
160
|
+
});
|
|
161
|
+
const AGENT_FAILURE_TYPES = Object.freeze({
|
|
162
|
+
timeout: Object.freeze({
|
|
163
|
+
code: "AGENT_ERR_TIMEOUT",
|
|
164
|
+
name: "timeout",
|
|
165
|
+
severity: "high",
|
|
166
|
+
description: "Agent exceeded turn limit or wall-clock timeout without completing its task",
|
|
167
|
+
recovery: "Retry with reduced scope \u2014 split the task into smaller sub-tasks or increase the turn budget",
|
|
168
|
+
retryable: true
|
|
169
|
+
}),
|
|
170
|
+
"malformed-output": Object.freeze({
|
|
171
|
+
code: "AGENT_ERR_MALFORMED_OUTPUT",
|
|
172
|
+
name: "malformed-output",
|
|
173
|
+
severity: "high",
|
|
174
|
+
description: "Agent returned output that cannot be parsed or does not match the expected format",
|
|
175
|
+
recovery: "Retry with explicit format instructions \u2014 add structured output examples to the prompt",
|
|
176
|
+
retryable: true
|
|
177
|
+
}),
|
|
178
|
+
"partial-completion": Object.freeze({
|
|
179
|
+
code: "AGENT_ERR_PARTIAL_COMPLETION",
|
|
180
|
+
name: "partial-completion",
|
|
181
|
+
severity: "medium",
|
|
182
|
+
description: "Agent completed some but not all assigned tasks \u2014 partial artifacts exist",
|
|
183
|
+
recovery: "Spawn a continuation agent for the remaining tasks using the partial output as context",
|
|
184
|
+
retryable: true
|
|
185
|
+
}),
|
|
186
|
+
hallucination: Object.freeze({
|
|
187
|
+
code: "AGENT_ERR_HALLUCINATION",
|
|
188
|
+
name: "hallucination",
|
|
189
|
+
severity: "critical",
|
|
190
|
+
description: "Agent claimed success but required artifacts are missing or do not match expectations",
|
|
191
|
+
recovery: "Reject all results and retry with a verification-first approach \u2014 require artifact proof before completion",
|
|
192
|
+
retryable: false
|
|
193
|
+
}),
|
|
194
|
+
"permission-denied": Object.freeze({
|
|
195
|
+
code: "AGENT_ERR_PERMISSION_DENIED",
|
|
196
|
+
name: "permission-denied",
|
|
197
|
+
severity: "high",
|
|
198
|
+
description: "Agent was blocked by a pre-commit hook, sandbox restriction, or file permission",
|
|
199
|
+
recovery: "Escalate to human operator \u2014 review sandbox configuration and hook settings",
|
|
200
|
+
retryable: false
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
const _codeToType = /* @__PURE__ */ new Map();
|
|
204
|
+
for (const [key, def] of Object.entries(AGENT_FAILURE_TYPES)) {
|
|
205
|
+
_codeToType.set(def.code, { key, ...def });
|
|
206
|
+
}
|
|
207
|
+
class AgentFailureError extends MgwError {
|
|
208
|
+
/**
|
|
209
|
+
* @param {string} message - Human-readable error description
|
|
210
|
+
* @param {object} [opts]
|
|
211
|
+
* @param {string} [opts.agentType] - GSD agent type (gsd-planner, gsd-executor, etc.)
|
|
212
|
+
* @param {string} [opts.failureType] - Failure type key from AGENT_FAILURE_TYPES
|
|
213
|
+
* @param {string[]} [opts.artifacts] - List of expected artifacts that are missing/malformed
|
|
214
|
+
* @param {string} [opts.stage] - Pipeline stage where failure occurred
|
|
215
|
+
* @param {number} [opts.issueNumber] - Related GitHub issue number
|
|
216
|
+
* @param {Error} [opts.cause] - Original error
|
|
217
|
+
*/
|
|
218
|
+
constructor(message, opts) {
|
|
219
|
+
const o = opts || {};
|
|
220
|
+
const failureDef = o.failureType ? AGENT_FAILURE_TYPES[o.failureType] : null;
|
|
221
|
+
const code = failureDef ? failureDef.code : "AGENT_ERR_UNKNOWN";
|
|
222
|
+
super(message, { code, stage: o.stage, issueNumber: o.issueNumber, cause: o.cause });
|
|
223
|
+
this.name = "AgentFailureError";
|
|
224
|
+
this.agentType = o.agentType || null;
|
|
225
|
+
this.failureType = o.failureType || null;
|
|
226
|
+
this.artifacts = Array.isArray(o.artifacts) ? o.artifacts : [];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the full failure type definition for this error.
|
|
230
|
+
* @returns {object|null}
|
|
231
|
+
*/
|
|
232
|
+
getFailureDefinition() {
|
|
233
|
+
if (!this.failureType) return null;
|
|
234
|
+
return AGENT_FAILURE_TYPES[this.failureType] || null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get the severity level for this error.
|
|
238
|
+
* @returns {string|null}
|
|
239
|
+
*/
|
|
240
|
+
getSeverity() {
|
|
241
|
+
const def = this.getFailureDefinition();
|
|
242
|
+
return def ? def.severity : null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Check whether this failure type is safe to auto-retry.
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
isRetryable() {
|
|
249
|
+
const def = this.getFailureDefinition();
|
|
250
|
+
return def ? def.retryable : false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const CLASSIFICATION_PATTERNS = [
|
|
254
|
+
// Timeout patterns
|
|
255
|
+
{ pattern: "turn limit", type: "timeout" },
|
|
256
|
+
{ pattern: "max turns", type: "timeout" },
|
|
257
|
+
{ pattern: "context window exhausted", type: "timeout" },
|
|
258
|
+
{ pattern: "exceeded.*timeout", type: "timeout" },
|
|
259
|
+
{ pattern: "agent timed out", type: "timeout" },
|
|
260
|
+
{ pattern: "wall.?clock.*exceeded", type: "timeout" },
|
|
261
|
+
// Malformed output patterns
|
|
262
|
+
{ pattern: "unparseable", type: "malformed-output" },
|
|
263
|
+
{ pattern: "invalid json", type: "malformed-output" },
|
|
264
|
+
{ pattern: "parse error", type: "malformed-output" },
|
|
265
|
+
{ pattern: "unexpected token", type: "malformed-output" },
|
|
266
|
+
{ pattern: "malformed output", type: "malformed-output" },
|
|
267
|
+
{ pattern: "missing required field", type: "malformed-output" },
|
|
268
|
+
{ pattern: "output format", type: "malformed-output" },
|
|
269
|
+
{ pattern: "expected.*format", type: "malformed-output" },
|
|
270
|
+
// Partial completion patterns
|
|
271
|
+
{ pattern: "partial completion", type: "partial-completion" },
|
|
272
|
+
{ pattern: "incomplete.*tasks", type: "partial-completion" },
|
|
273
|
+
{ pattern: "completed.*of.*tasks", type: "partial-completion" },
|
|
274
|
+
{ pattern: "remaining tasks", type: "partial-completion" },
|
|
275
|
+
{ pattern: "some tasks failed", type: "partial-completion" },
|
|
276
|
+
// Hallucination patterns (check before generic patterns)
|
|
277
|
+
{ pattern: "artifacts? missing", type: "hallucination" },
|
|
278
|
+
{ pattern: "claimed success.*missing", type: "hallucination" },
|
|
279
|
+
{ pattern: "file.*not found.*after", type: "hallucination" },
|
|
280
|
+
{ pattern: "verification failed.*not exist", type: "hallucination" },
|
|
281
|
+
{ pattern: "hallucination", type: "hallucination" },
|
|
282
|
+
{ pattern: "phantom", type: "hallucination" },
|
|
283
|
+
// Permission denied patterns
|
|
284
|
+
{ pattern: "permission denied", type: "permission-denied" },
|
|
285
|
+
{ pattern: "access denied", type: "permission-denied" },
|
|
286
|
+
{ pattern: "hook.*failed", type: "permission-denied" },
|
|
287
|
+
{ pattern: "pre.?commit.*rejected", type: "permission-denied" },
|
|
288
|
+
{ pattern: "sandbox.*blocked", type: "permission-denied" },
|
|
289
|
+
{ pattern: "sandbox.*violation", type: "permission-denied" },
|
|
290
|
+
{ pattern: "eacces", type: "permission-denied" }
|
|
291
|
+
];
|
|
292
|
+
function classifyAgentFailure(error, context) {
|
|
293
|
+
if (!error || typeof error !== "object") return null;
|
|
294
|
+
const message = (error.message || "").toLowerCase();
|
|
295
|
+
const code = (error.code || "").toLowerCase();
|
|
296
|
+
const ctx = context || {};
|
|
297
|
+
if (ctx.expectedArtifacts && ctx.actualArtifacts) {
|
|
298
|
+
const expected = new Set(ctx.expectedArtifacts);
|
|
299
|
+
const actual = new Set(ctx.actualArtifacts);
|
|
300
|
+
const missing = [...expected].filter((a) => !actual.has(a));
|
|
301
|
+
if (missing.length > 0 && ctx.tasksCompleted > 0) {
|
|
302
|
+
const def = AGENT_FAILURE_TYPES.hallucination;
|
|
303
|
+
return { type: "hallucination", code: def.code, severity: def.severity, confidence: "high" };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (typeof ctx.tasksTotal === "number" && typeof ctx.tasksCompleted === "number") {
|
|
307
|
+
if (ctx.tasksCompleted > 0 && ctx.tasksCompleted < ctx.tasksTotal) {
|
|
308
|
+
const def = AGENT_FAILURE_TYPES["partial-completion"];
|
|
309
|
+
return { type: "partial-completion", code: def.code, severity: def.severity, confidence: "high" };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (code === "eacces" || code === "eperm") {
|
|
313
|
+
const def = AGENT_FAILURE_TYPES["permission-denied"];
|
|
314
|
+
return { type: "permission-denied", code: def.code, severity: def.severity, confidence: "high" };
|
|
315
|
+
}
|
|
316
|
+
for (const { pattern, type } of CLASSIFICATION_PATTERNS) {
|
|
317
|
+
const regex = new RegExp(pattern, "i");
|
|
318
|
+
if (regex.test(message)) {
|
|
319
|
+
const def = AGENT_FAILURE_TYPES[type];
|
|
320
|
+
return { type, code: def.code, severity: def.severity, confidence: "medium" };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
function getRecoveryAction(failureType) {
|
|
326
|
+
const def = AGENT_FAILURE_TYPES[failureType];
|
|
327
|
+
if (!def) return null;
|
|
328
|
+
return {
|
|
329
|
+
action: def.recovery,
|
|
330
|
+
retryable: def.retryable,
|
|
331
|
+
severity: def.severity
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function isRetryable(failureType) {
|
|
335
|
+
const def = AGENT_FAILURE_TYPES[failureType];
|
|
336
|
+
return def ? def.retryable : false;
|
|
337
|
+
}
|
|
338
|
+
function compareSeverity(a, b) {
|
|
339
|
+
const weightA = (SEVERITY_LEVELS[a] || { weight: 0 }).weight;
|
|
340
|
+
const weightB = (SEVERITY_LEVELS[b] || { weight: 0 }).weight;
|
|
341
|
+
return weightA - weightB;
|
|
342
|
+
}
|
|
343
|
+
function getFailureByCode(errorCode) {
|
|
344
|
+
return _codeToType.get(errorCode) || null;
|
|
345
|
+
}
|
|
346
|
+
agentErrors = {
|
|
347
|
+
// Constants
|
|
348
|
+
AGENT_FAILURE_TYPES,
|
|
349
|
+
SEVERITY_LEVELS,
|
|
350
|
+
// Error class
|
|
351
|
+
AgentFailureError,
|
|
352
|
+
// Classification
|
|
353
|
+
classifyAgentFailure,
|
|
354
|
+
// Recovery
|
|
355
|
+
getRecoveryAction,
|
|
356
|
+
isRetryable,
|
|
357
|
+
// Utilities
|
|
358
|
+
compareSeverity,
|
|
359
|
+
getFailureByCode
|
|
360
|
+
};
|
|
361
|
+
return agentErrors;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
var modelFallback;
|
|
365
|
+
var hasRequiredModelFallback;
|
|
366
|
+
|
|
367
|
+
function requireModelFallback () {
|
|
368
|
+
if (hasRequiredModelFallback) return modelFallback;
|
|
369
|
+
hasRequiredModelFallback = 1;
|
|
370
|
+
const fs = require$$2;
|
|
371
|
+
const path = require$$1;
|
|
372
|
+
const { MgwError } = index$1.requireErrors();
|
|
373
|
+
let classifyAgentFailure = null;
|
|
374
|
+
let _AGENT_FAILURE_TYPES = null;
|
|
375
|
+
try {
|
|
376
|
+
const agentErrors = requireAgentErrors();
|
|
377
|
+
classifyAgentFailure = agentErrors.classifyAgentFailure;
|
|
378
|
+
_AGENT_FAILURE_TYPES = agentErrors.AGENT_FAILURE_TYPES;
|
|
379
|
+
} catch (_) {
|
|
380
|
+
}
|
|
381
|
+
let classifyFailure = null;
|
|
382
|
+
try {
|
|
383
|
+
const retry = index$1.requireRetry();
|
|
384
|
+
classifyFailure = retry.classifyFailure;
|
|
385
|
+
} catch (_) {
|
|
386
|
+
}
|
|
387
|
+
const DEFAULT_FALLBACK_CHAINS = Object.freeze({
|
|
388
|
+
"gsd-planner": Object.freeze(["inherit", "sonnet", "haiku"]),
|
|
389
|
+
"gsd-executor": Object.freeze(["sonnet", "haiku"]),
|
|
390
|
+
"gsd-verifier": Object.freeze(["sonnet", "haiku"]),
|
|
391
|
+
"gsd-plan-checker": Object.freeze(["sonnet", "haiku"]),
|
|
392
|
+
"general-purpose": Object.freeze(["sonnet", "haiku"])
|
|
393
|
+
});
|
|
394
|
+
const NON_FALLBACK_FAILURE_TYPES = /* @__PURE__ */ new Set([
|
|
395
|
+
"hallucination",
|
|
396
|
+
// Unsafe — different model might hallucinate differently
|
|
397
|
+
"permission-denied"
|
|
398
|
+
// Environment issue — model change won't help
|
|
399
|
+
]);
|
|
400
|
+
class ModelFallbackError extends MgwError {
|
|
401
|
+
/**
|
|
402
|
+
* @param {string} message
|
|
403
|
+
* @param {object} [opts]
|
|
404
|
+
* @param {string} [opts.failureType] - Last classified failure type
|
|
405
|
+
* @param {string} [opts.agentType] - GSD agent type
|
|
406
|
+
* @param {string[]} [opts.modelsAttempted] - Models that were tried
|
|
407
|
+
* @param {number} [opts.fallback_attempts] - Number of fallback attempts made
|
|
408
|
+
* @param {Error} [opts.cause] - Last error from final model attempt
|
|
409
|
+
* @param {string} [opts.stage] - Pipeline stage
|
|
410
|
+
* @param {number} [opts.issueNumber] - Related GitHub issue number
|
|
411
|
+
*/
|
|
412
|
+
constructor(message, opts) {
|
|
413
|
+
const o = opts || {};
|
|
414
|
+
super(message, {
|
|
415
|
+
code: "MODEL_FALLBACK_EXHAUSTED",
|
|
416
|
+
stage: o.stage,
|
|
417
|
+
issueNumber: o.issueNumber,
|
|
418
|
+
cause: o.cause
|
|
419
|
+
});
|
|
420
|
+
this.name = "ModelFallbackError";
|
|
421
|
+
this.failureType = o.failureType || null;
|
|
422
|
+
this.agentType = o.agentType || null;
|
|
423
|
+
this.modelsAttempted = Array.isArray(o.modelsAttempted) ? o.modelsAttempted : [];
|
|
424
|
+
this.fallback_attempts = typeof o.fallback_attempts === "number" ? o.fallback_attempts : 0;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
class ModelFallbackEngine {
|
|
428
|
+
/**
|
|
429
|
+
* @param {object} [opts]
|
|
430
|
+
* @param {boolean} [opts.enabled] - Master enable/disable switch (overrides config)
|
|
431
|
+
* @param {object} [opts.chains] - Per-agent-type chain overrides
|
|
432
|
+
* Format: { 'gsd-planner': ['inherit', 'sonnet'] }
|
|
433
|
+
* @param {string} [opts.configPath] - Path to .mgw/config.json (default: auto-detect)
|
|
434
|
+
*/
|
|
435
|
+
constructor(opts) {
|
|
436
|
+
const o = opts || {};
|
|
437
|
+
const fileConfig = ModelFallbackEngine.loadConfig(o.configPath || null);
|
|
438
|
+
if (typeof o.enabled === "boolean") {
|
|
439
|
+
this._enabled = o.enabled;
|
|
440
|
+
} else if (typeof fileConfig.enabled === "boolean") {
|
|
441
|
+
this._enabled = fileConfig.enabled;
|
|
442
|
+
} else {
|
|
443
|
+
this._enabled = false;
|
|
444
|
+
}
|
|
445
|
+
this._chains = Object.assign(
|
|
446
|
+
{},
|
|
447
|
+
DEFAULT_FALLBACK_CHAINS,
|
|
448
|
+
fileConfig.chains || {},
|
|
449
|
+
o.chains || {}
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
// -------------------------------------------------------------------------
|
|
453
|
+
// Public API
|
|
454
|
+
// -------------------------------------------------------------------------
|
|
455
|
+
/**
|
|
456
|
+
* Check whether model fallback is enabled.
|
|
457
|
+
* @returns {boolean}
|
|
458
|
+
*/
|
|
459
|
+
get enabled() {
|
|
460
|
+
return this._enabled;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resolve the ordered fallback chain for an agent type.
|
|
464
|
+
*
|
|
465
|
+
* If model_fallback is disabled, returns an array with only the primary
|
|
466
|
+
* model (no fallback — preserves backward-compatible behavior).
|
|
467
|
+
*
|
|
468
|
+
* @param {string} agentType - GSD agent type (e.g. 'gsd-planner')
|
|
469
|
+
* @returns {string[]} Ordered model chain (first = primary, rest = fallbacks)
|
|
470
|
+
*/
|
|
471
|
+
resolveFallbackChain(agentType) {
|
|
472
|
+
const chain = this._chains[agentType] || this._chains["general-purpose"] || ["sonnet"];
|
|
473
|
+
if (!this._enabled) {
|
|
474
|
+
return [chain[0]];
|
|
475
|
+
}
|
|
476
|
+
return [...chain];
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Execute an async function with model-tier fallback.
|
|
480
|
+
*
|
|
481
|
+
* Takes a function that receives a model name and attempts execution
|
|
482
|
+
* with each model in the fallback chain until one succeeds or the
|
|
483
|
+
* chain is exhausted.
|
|
484
|
+
*
|
|
485
|
+
* @param {(modelName: string) => Promise<*>} fn - Async function that accepts model name
|
|
486
|
+
* @param {object} [opts]
|
|
487
|
+
* @param {string} [opts.agentType] - GSD agent type for chain lookup
|
|
488
|
+
* @param {function} [opts.onFallback] - Callback on fallback: (fromModel, toModel, attempt, error) => void
|
|
489
|
+
* @param {AbortSignal} [opts.signal] - AbortSignal to cancel fallback attempts
|
|
490
|
+
* @returns {Promise<{ result: *, model: string, fallback_attempts: number, total_models_tried: number }>}
|
|
491
|
+
* @throws {ModelFallbackError} If all models in chain are exhausted
|
|
492
|
+
* @throws {Error} Original error if failure type is non-fallback-eligible
|
|
493
|
+
*/
|
|
494
|
+
async executeWithFallback(fn, opts) {
|
|
495
|
+
const o = opts || {};
|
|
496
|
+
const agentType = o.agentType || "general-purpose";
|
|
497
|
+
const onFallback = typeof o.onFallback === "function" ? o.onFallback : null;
|
|
498
|
+
const signal = o.signal || null;
|
|
499
|
+
const chain = this.resolveFallbackChain(agentType);
|
|
500
|
+
const modelsAttempted = [];
|
|
501
|
+
let lastError = null;
|
|
502
|
+
let fallbackAttempts = 0;
|
|
503
|
+
for (let i = 0; i < chain.length; i++) {
|
|
504
|
+
const model = chain[i];
|
|
505
|
+
if (signal && signal.aborted) {
|
|
506
|
+
throw lastError || new MgwError("Fallback aborted by signal", { code: "FALLBACK_ABORTED" });
|
|
507
|
+
}
|
|
508
|
+
modelsAttempted.push(model);
|
|
509
|
+
try {
|
|
510
|
+
const result = await fn(model);
|
|
511
|
+
return {
|
|
512
|
+
result,
|
|
513
|
+
model,
|
|
514
|
+
fallback_attempts: fallbackAttempts,
|
|
515
|
+
total_models_tried: modelsAttempted.length
|
|
516
|
+
};
|
|
517
|
+
} catch (err) {
|
|
518
|
+
lastError = err;
|
|
519
|
+
const failureType = this._classifyForFallback(err, agentType);
|
|
520
|
+
if (NON_FALLBACK_FAILURE_TYPES.has(failureType)) {
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
if (i < chain.length - 1) {
|
|
524
|
+
fallbackAttempts++;
|
|
525
|
+
if (onFallback) {
|
|
526
|
+
try {
|
|
527
|
+
onFallback(model, chain[i + 1], fallbackAttempts, err);
|
|
528
|
+
} catch (_) {
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
throw new ModelFallbackError(
|
|
535
|
+
`Model fallback exhausted for ${agentType}: tried ${modelsAttempted.join(", ")}`,
|
|
536
|
+
{
|
|
537
|
+
failureType: this._classifyForFallback(lastError, agentType),
|
|
538
|
+
agentType,
|
|
539
|
+
modelsAttempted,
|
|
540
|
+
fallback_attempts: fallbackAttempts,
|
|
541
|
+
cause: lastError
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
// -------------------------------------------------------------------------
|
|
546
|
+
// Internal: error classification for fallback decisions
|
|
547
|
+
// -------------------------------------------------------------------------
|
|
548
|
+
/**
|
|
549
|
+
* Classify an error to determine if model fallback should be attempted.
|
|
550
|
+
*
|
|
551
|
+
* Uses agent-errors.cjs classification first (if available), then falls
|
|
552
|
+
* back to generic retry.cjs classification.
|
|
553
|
+
*
|
|
554
|
+
* @param {Error} err - The error to classify
|
|
555
|
+
* @param {string} [agentType] - GSD agent type for context
|
|
556
|
+
* @returns {string} Failure type key (e.g. 'timeout', 'permanent', 'transient')
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
559
|
+
_classifyForFallback(err, agentType) {
|
|
560
|
+
if (classifyAgentFailure) {
|
|
561
|
+
const agentResult = classifyAgentFailure(err, { agentType });
|
|
562
|
+
if (agentResult && agentResult.type) {
|
|
563
|
+
return agentResult.type;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (classifyFailure) {
|
|
567
|
+
const genericResult = classifyFailure(err);
|
|
568
|
+
if (genericResult && genericResult.class) {
|
|
569
|
+
if (genericResult.class === "transient") return "timeout";
|
|
570
|
+
if (genericResult.class === "needs-info") return "needs-info";
|
|
571
|
+
return "permanent";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return "permanent";
|
|
575
|
+
}
|
|
576
|
+
// -------------------------------------------------------------------------
|
|
577
|
+
// Static: config loading
|
|
578
|
+
// -------------------------------------------------------------------------
|
|
579
|
+
/**
|
|
580
|
+
* Load model fallback configuration from .mgw/config.json.
|
|
581
|
+
*
|
|
582
|
+
* Looks for a `retry` section with `model_fallback` and `fallback_chains`:
|
|
583
|
+
* ```json
|
|
584
|
+
* {
|
|
585
|
+
* "retry": {
|
|
586
|
+
* "model_fallback": true,
|
|
587
|
+
* "fallback_chains": {
|
|
588
|
+
* "gsd-planner": ["inherit", "sonnet"],
|
|
589
|
+
* "gsd-executor": ["sonnet", "haiku"]
|
|
590
|
+
* }
|
|
591
|
+
* }
|
|
592
|
+
* }
|
|
593
|
+
* ```
|
|
594
|
+
*
|
|
595
|
+
* @param {string} [configPath] - Explicit path to config.json. If null,
|
|
596
|
+
* searches for .mgw/config.json relative to cwd.
|
|
597
|
+
* @returns {{ enabled?: boolean, chains?: object }}
|
|
598
|
+
*/
|
|
599
|
+
static loadConfig(configPath) {
|
|
600
|
+
const empty = {};
|
|
601
|
+
try {
|
|
602
|
+
const cfgPath = configPath || path.join(process.cwd(), ".mgw", "config.json");
|
|
603
|
+
if (!fs.existsSync(cfgPath)) return empty;
|
|
604
|
+
const raw = fs.readFileSync(cfgPath, "utf-8");
|
|
605
|
+
const config = JSON.parse(raw);
|
|
606
|
+
if (!config || typeof config !== "object") return empty;
|
|
607
|
+
const retry = config.retry;
|
|
608
|
+
if (!retry || typeof retry !== "object") return empty;
|
|
609
|
+
const result = {};
|
|
610
|
+
if (typeof retry.model_fallback === "boolean") {
|
|
611
|
+
result.enabled = retry.model_fallback;
|
|
612
|
+
}
|
|
613
|
+
if (retry.fallback_chains && typeof retry.fallback_chains === "object") {
|
|
614
|
+
const chains = {};
|
|
615
|
+
for (const [agentType, chain] of Object.entries(retry.fallback_chains)) {
|
|
616
|
+
if (Array.isArray(chain) && chain.every((m) => typeof m === "string")) {
|
|
617
|
+
chains[agentType] = chain;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (Object.keys(chains).length > 0) {
|
|
621
|
+
result.chains = chains;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return result;
|
|
625
|
+
} catch (_) {
|
|
626
|
+
return empty;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
modelFallback = {
|
|
631
|
+
// Constants
|
|
632
|
+
DEFAULT_FALLBACK_CHAINS,
|
|
633
|
+
NON_FALLBACK_FAILURE_TYPES,
|
|
634
|
+
// Error class
|
|
635
|
+
ModelFallbackError,
|
|
636
|
+
// Engine
|
|
637
|
+
ModelFallbackEngine
|
|
638
|
+
};
|
|
639
|
+
return modelFallback;
|
|
640
|
+
}
|
|
641
|
+
|
|
148
642
|
var gsdAdapter;
|
|
149
643
|
var hasRequiredGsdAdapter;
|
|
150
644
|
|
|
@@ -316,6 +810,32 @@ Set GSD_TOOLS_PATH or add gsd_path to .mgw/config.json`
|
|
|
316
810
|
}
|
|
317
811
|
return { activeMilestone, currentPhase, planCount };
|
|
318
812
|
}
|
|
813
|
+
let ModelFallbackEngine = null;
|
|
814
|
+
try {
|
|
815
|
+
const modelFallback = requireModelFallback();
|
|
816
|
+
ModelFallbackEngine = modelFallback.ModelFallbackEngine;
|
|
817
|
+
} catch (_) {
|
|
818
|
+
}
|
|
819
|
+
let _fallbackEngineSingleton = null;
|
|
820
|
+
function getModelFallbackEngine() {
|
|
821
|
+
if (!ModelFallbackEngine) return null;
|
|
822
|
+
if (!_fallbackEngineSingleton) {
|
|
823
|
+
_fallbackEngineSingleton = new ModelFallbackEngine();
|
|
824
|
+
}
|
|
825
|
+
return _fallbackEngineSingleton;
|
|
826
|
+
}
|
|
827
|
+
function resolveFallbackChain(agentType) {
|
|
828
|
+
const engine = getModelFallbackEngine();
|
|
829
|
+
if (engine && engine.enabled) {
|
|
830
|
+
return engine.resolveFallbackChain(agentType);
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
const primary = resolveModel(agentType);
|
|
834
|
+
return [primary];
|
|
835
|
+
} catch (_) {
|
|
836
|
+
return ["sonnet"];
|
|
837
|
+
}
|
|
838
|
+
}
|
|
319
839
|
gsdAdapter = {
|
|
320
840
|
resolveGsdRoot,
|
|
321
841
|
getGsdToolsPath,
|
|
@@ -326,7 +846,9 @@ Set GSD_TOOLS_PATH or add gsd_path to .mgw/config.json`
|
|
|
326
846
|
historyDigest,
|
|
327
847
|
roadmapAnalyze,
|
|
328
848
|
selectGsdRoute,
|
|
329
|
-
getGsdState
|
|
849
|
+
getGsdState,
|
|
850
|
+
getModelFallbackEngine,
|
|
851
|
+
resolveFallbackChain
|
|
330
852
|
};
|
|
331
853
|
return gsdAdapter;
|
|
332
854
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snipcodeit/mgw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "GitHub-native issue-to-PR automation for Claude Code, powered by Get Shit Done",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mgw": "./dist/bin/mgw.cjs"
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "pkgroll --clean-dist --src .",
|
|
18
18
|
"dev": "pkgroll --watch --src .",
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:node": "node --test test/*.test.cjs",
|
|
22
|
+
"test:coverage": "vitest run --coverage",
|
|
20
23
|
"lint": "eslint lib/ bin/ test/",
|
|
21
24
|
"prepublishOnly": "npm run build",
|
|
22
25
|
"completions": "node bin/generate-completions.cjs",
|
|
@@ -30,8 +33,10 @@
|
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
32
35
|
"@eslint/js": "^10.0.1",
|
|
36
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
33
37
|
"eslint": "^10.0.2",
|
|
34
|
-
"pkgroll": "^2.26.3"
|
|
38
|
+
"pkgroll": "^2.26.3",
|
|
39
|
+
"vitest": "^4.0.18"
|
|
35
40
|
},
|
|
36
41
|
"engines": {
|
|
37
42
|
"node": ">=18.0.0"
|
|
@@ -56,4 +61,4 @@
|
|
|
56
61
|
"bugs": {
|
|
57
62
|
"url": "https://github.com/snipcodeit/mgw/issues"
|
|
58
63
|
}
|
|
59
|
-
}
|
|
64
|
+
}
|