@muggleai/works 5.27.0-staging.245 → 5.27.0-staging.246

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.
@@ -9,7 +9,7 @@
9
9
  "name": "muggleai",
10
10
  "source": "./plugin",
11
11
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
12
- "version": "5.27.0-staging.245"
12
+ "version": "5.27.0-staging.246"
13
13
  }
14
14
  ]
15
15
  }
@@ -5705,7 +5705,7 @@ async function checkServiceStack(params) {
5705
5705
  const snapshot = await readGenerationStore({ directory: ledgerDir(), emptyRecord: EMPTY_STACK_RECORD });
5706
5706
  const swept = sweepStackRecord({ record: snapshot.record, now: Date.now(), isAlive: isProcessAlive });
5707
5707
  const ownLease = swept.leases.find((lease) => lease.cwd === cwd) ?? null;
5708
- const base = { cwd, generation: snapshot.generation, mismatches: [], peerLease: null };
5708
+ const base = { cwd, generation: snapshot.generation, mismatches: [], peerLease: null, servedNeeds: [] };
5709
5709
  const isClaimedByCaller = ownLease?.needs.some((need) => need.port === params.port) ?? false;
5710
5710
  const peerLease = params.port === void 0 || isClaimedByCaller ? void 0 : swept.leases.find((lease) => lease.cwd !== cwd && lease.needs.some((need) => need.port === params.port));
5711
5711
  if (peerLease !== void 0) {
@@ -5734,7 +5734,12 @@ async function checkServiceStack(params) {
5734
5734
  mismatches
5735
5735
  };
5736
5736
  }
5737
- return { ...base, verdict: "VALID" /* Valid */, reason: "every claimed port serves the claimed code" };
5737
+ return {
5738
+ ...base,
5739
+ verdict: "VALID" /* Valid */,
5740
+ reason: "every claimed port serves the claimed code",
5741
+ servedNeeds: ownLease.needs
5742
+ };
5738
5743
  }
5739
5744
  async function startStackRun(params) {
5740
5745
  const cwd = await resolveCheckoutRoot({ cwd: params.cwd, runGit: params.runGit ?? runGitCommand });
@@ -9596,6 +9601,20 @@ async function findRunStackRefusal(params) {
9596
9601
  });
9597
9602
  return REFUSING_VERDICTS.has(report.verdict) ? report : null;
9598
9603
  }
9604
+ async function readServedStack(params) {
9605
+ if (!isLoopbackUrl(params.localUrl)) return [];
9606
+ const report = await checkServiceStack({ cwd: params.cwd, runGit: params.runGit, readListener: params.readListener });
9607
+ return report.servedNeeds;
9608
+ }
9609
+ function recordServedStack(params) {
9610
+ if (params.servedStack.length === 0 || params.stackChange !== null) return;
9611
+ const storage = getRunResultStorageService();
9612
+ const runResult = storage.getRunResult(params.runId);
9613
+ if (!runResult) return;
9614
+ storage.updateRunResult(params.runId, {
9615
+ localExecutionContext: { ...runResult.localExecutionContext, servedStack: params.servedStack }
9616
+ });
9617
+ }
9599
9618
  async function runUnderStackLease(params) {
9600
9619
  if (!isLoopbackUrl(params.localUrl)) return await params.execute();
9601
9620
  const runId = randomUUID();
@@ -9985,6 +10004,7 @@ var executeTestGenerationTool = {
9985
10004
  logger7.warn("Test generation refused: service stack", { verdict: stackRefusal.verdict, cwd: input.cwd });
9986
10005
  return { content: formatRunStackRefusal(stackRefusal, input.cwd), isError: true, data: stackRefusal };
9987
10006
  }
10007
+ const servedStack = await readServedStack({ cwd: input.cwd, localUrl: input.localUrl });
9988
10008
  try {
9989
10009
  const generationRun = await runUnderStackLease({
9990
10010
  cwd: input.cwd,
@@ -10012,6 +10032,7 @@ var executeTestGenerationTool = {
10012
10032
  generationRun.errorMessage ? `**Error:** ${generationRun.errorMessage}` : ""
10013
10033
  ].filter(Boolean).join("\n");
10014
10034
  const stackChange = await findStackChangedDuringRun({ cwd: input.cwd, localUrl: input.localUrl });
10035
+ recordServedStack({ runId: generationRun.id, servedStack, stackChange });
10015
10036
  const reportedContent = stackChange ? content + formatStackChangedDuringRun(stackChange) : content;
10016
10037
  return {
10017
10038
  content: reportedContent,
@@ -10051,6 +10072,7 @@ var executeReplayTool = {
10051
10072
  logger7.warn("Test replay refused: service stack", { verdict: stackRefusal.verdict, cwd: input.cwd });
10052
10073
  return { content: formatRunStackRefusal(stackRefusal, input.cwd), isError: true, data: stackRefusal };
10053
10074
  }
10075
+ const servedStack = await readServedStack({ cwd: input.cwd, localUrl: input.localUrl });
10054
10076
  try {
10055
10077
  const replayRun = await runUnderStackLease({
10056
10078
  cwd: input.cwd,
@@ -10079,6 +10101,7 @@ var executeReplayTool = {
10079
10101
  replayRun.errorMessage ? `**Error:** ${replayRun.errorMessage}` : ""
10080
10102
  ].filter(Boolean).join("\n");
10081
10103
  const stackChange = await findStackChangedDuringRun({ cwd: input.cwd, localUrl: input.localUrl });
10104
+ recordServedStack({ runId: replayRun.id, servedStack, stackChange });
10082
10105
  const reportedContent = stackChange ? content + formatStackChangedDuringRun(stackChange) : content;
10083
10106
  return {
10084
10107
  content: reportedContent,
@@ -1,4 +1,4 @@
1
- import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir2, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, resolveElectronAppExecutablePath, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, getActiveElectronAppReleaseStream, buildToolInputJsonSchema, toolRequiresAuth, getCallerCredentials, parseElectronAppDirName, getElectronAppReleaseTagPrefix, hasShownDisclosure, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, ElectronAppReleaseStream, buildElectronAppChecksumsUrl, __require, getDisclosureCopy, buildOnboardingPlan, applyOnboardingAnswers, needsOnboarding, buildLlmSourcePlan, describeLlmSourceStatus, resolveLlmSourceStatus, readRunView, RunViewError, renderRunViewText, writeRunViewHtml, openBrowserUrl, evaluateCheckoutFreshness, runGitCommand, BLOCKING_FRESHNESS_VERDICTS, claimServiceStack, BUSY_EXIT_CODE, checkServiceStack, releaseServiceStack, readServiceStackStatus, resolvePreferences, OnboardingBlanketChoice, PreferenceKey, LlmSource, applyLlmSourceSelection, describeStackMismatch, getDataDir, DEFAULT_LOCAL_LLM_BASE_URL, isValidLocalBaseUrl } from './chunk-FI6VHHZN.js';
1
+ import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir2, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, resolveElectronAppExecutablePath, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, getActiveElectronAppReleaseStream, buildToolInputJsonSchema, toolRequiresAuth, getCallerCredentials, parseElectronAppDirName, getElectronAppReleaseTagPrefix, hasShownDisclosure, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, ElectronAppReleaseStream, buildElectronAppChecksumsUrl, __require, getDisclosureCopy, buildOnboardingPlan, applyOnboardingAnswers, needsOnboarding, buildLlmSourcePlan, describeLlmSourceStatus, resolveLlmSourceStatus, readRunView, RunViewError, renderRunViewText, writeRunViewHtml, openBrowserUrl, evaluateCheckoutFreshness, runGitCommand, BLOCKING_FRESHNESS_VERDICTS, claimServiceStack, BUSY_EXIT_CODE, checkServiceStack, releaseServiceStack, readServiceStackStatus, resolvePreferences, OnboardingBlanketChoice, PreferenceKey, LlmSource, applyLlmSourceSelection, describeStackMismatch, getDataDir, DEFAULT_LOCAL_LLM_BASE_URL, isValidLocalBaseUrl, local_exports } from './chunk-6BAB7IVG.js';
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { v4 } from 'uuid';
@@ -356,6 +356,16 @@ function parentIsAlive(pid) {
356
356
  }
357
357
  }
358
358
 
359
+ // src/pr-walkthrough/constants.ts
360
+ var REPORT_SENTINEL = "muggle-pr-section";
361
+ var WALKTHROUGH_SLOT_MARKER = "<!-- muggle-pr-walkthrough:v1 -->";
362
+ var WALKTHROUGH_SKIPPED_MARKER = "<!-- muggle-pr-walkthrough-status:skipped -->";
363
+ var WALKTHROUGH_COMMENT_HEADING = "### Muggle AI \u2014 PR visual walkthrough";
364
+ var RERUN_NOTICE_OPENING = "<!-- muggle-pr-walkthrough-rerun -->";
365
+ var SHORT_SHA_LENGTH = 7;
366
+ var GH_COMMENT_TIMEOUT_MS = 1e4;
367
+ var DEPENDENCY_BOT_LOGINS = ["dependabot[bot]", "renovate[bot]"];
368
+
359
369
  // src/cli/pr-section/constants.ts
360
370
  var DETAIL_IMAGE_WIDTH = 720;
361
371
  var STEP_IMAGE_WIDTH = 640;
@@ -369,6 +379,84 @@ var SHORT_ID_LABEL_SEPARATOR = " \xB7 ";
369
379
  var TEST_DETAILS_ANCHOR_PREFIX = "muggle-test-";
370
380
  var STEP_PAIR_IMAGE_WIDTH = 420;
371
381
  var MAX_COMMENT_BYTES = 64e3;
382
+ var FULL_COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/;
383
+ var BASELINE_FAILED_STATUS = "failed";
384
+
385
+ // src/cli/pr-section/baseline.ts
386
+ var refused = (refusalReason) => ({ isCredited: false, refusalReason });
387
+ async function resolveBaseRange(deps) {
388
+ const baseRef = deps.baseBranch ? `refs/remotes/origin/${deps.baseBranch}` : await deps.runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], deps.cwd);
389
+ if (!baseRef) return null;
390
+ const baseTipSha = await deps.runGit(["rev-parse", "--verify", `${baseRef}^{commit}`], deps.cwd);
391
+ if (!baseTipSha) return null;
392
+ const mergeBaseSha = await deps.runGit(["merge-base", "HEAD", baseTipSha], deps.cwd);
393
+ return mergeBaseSha ? { mergeBaseSha, baseTipSha } : null;
394
+ }
395
+ async function isOnBaseSinceFork(sha, range, deps) {
396
+ const isAncestor = async (ancestor, descendant) => await deps.runGit(["merge-base", "--is-ancestor", ancestor, descendant], deps.cwd) !== null;
397
+ return await isAncestor(range.mergeBaseSha, sha) && await isAncestor(sha, range.baseTipSha);
398
+ }
399
+ function servedCommitsOf(baselineRun) {
400
+ const servedStack = baselineRun.localExecutionContext?.servedStack;
401
+ if (!Array.isArray(servedStack)) return [];
402
+ return servedStack.map((servedNeed) => servedNeed?.headSha).filter((sha) => typeof sha === "string" && FULL_COMMIT_SHA_PATTERN.test(sha));
403
+ }
404
+ async function judgeBaseline(test, baselineRunId, range, deps) {
405
+ if (range === null) return refused("the base branch could not be resolved in this checkout");
406
+ const baselineRun = deps.readRunResult(baselineRunId);
407
+ if (!baselineRun) return refused("no such run in the local run store");
408
+ if (baselineRun.id === test.runId) return refused("it is the failing run itself");
409
+ if (baselineRun.cloudTestCaseId !== test.testCaseId) return refused(`it ran test case ${baselineRun.cloudTestCaseId}`);
410
+ if (baselineRun.status !== BASELINE_FAILED_STATUS) return refused(`its status is ${baselineRun.status}`);
411
+ if (test.runSettingsId && baselineRun.cloudRunSettingsId && baselineRun.cloudRunSettingsId !== test.runSettingsId) {
412
+ return refused("it ran under a different runSettings record");
413
+ }
414
+ const servedCommits = servedCommitsOf(baselineRun);
415
+ if (servedCommits.length === 0) return refused("it recorded no stack it served");
416
+ for (const servedSha of servedCommits) {
417
+ if (!await isOnBaseSinceFork(servedSha, range, deps)) continue;
418
+ return {
419
+ isCredited: true,
420
+ failsOnBase: {
421
+ baselineRunId: baselineRun.id,
422
+ baselineSha: servedSha,
423
+ ...baselineRun.viewUrl && { viewUrl: baselineRun.viewUrl }
424
+ }
425
+ };
426
+ }
427
+ const span = `${range.mergeBaseSha.slice(0, SHORT_SHA_LENGTH)}..${range.baseTipSha.slice(0, SHORT_SHA_LENGTH)}`;
428
+ return refused(`none of the commits it served is on the base between ${span}`);
429
+ }
430
+ function withoutFailsOnBase(test) {
431
+ const uncredited = { ...test };
432
+ delete uncredited.failsOnBase;
433
+ return uncredited;
434
+ }
435
+ async function verifyBaselineFailures(report, deps) {
436
+ let range;
437
+ const tests = [];
438
+ for (const test of report.tests) {
439
+ if (test.status !== "failed") {
440
+ tests.push(test);
441
+ continue;
442
+ }
443
+ const uncredited = withoutFailsOnBase(test);
444
+ if (!test.baselineRunId) {
445
+ tests.push(uncredited);
446
+ continue;
447
+ }
448
+ if (range === void 0) range = await resolveBaseRange(deps);
449
+ const judgment = await judgeBaseline(uncredited, test.baselineRunId, range, deps);
450
+ if (judgment.isCredited) {
451
+ tests.push({ ...uncredited, failsOnBase: judgment.failsOnBase });
452
+ continue;
453
+ }
454
+ deps.stderrWrite(`build-pr-section: baseline ${test.baselineRunId} for "${test.name}" not credited: ${judgment.refusalReason}
455
+ `);
456
+ tests.push(uncredited);
457
+ }
458
+ return { ...report, tests };
459
+ }
372
460
 
373
461
  // src/e2e-result/types.ts
374
462
  var UnpassedCaseStatus = /* @__PURE__ */ ((UnpassedCaseStatus2) => {
@@ -400,8 +488,12 @@ function describeUnpassedCase(unpassed) {
400
488
  const outcome = unpassed.status === "failed" /* Failed */ && unpassed.failedStep !== void 0 ? `failed at step ${unpassed.failedStep}` : unpassed.status;
401
489
  return `${outcome}: ${escapeHtml(unpassed.detail)}`;
402
490
  }
491
+ function describeFailsOnBase(failsOnBase) {
492
+ const baselineLink = failsOnBase.viewUrl ? ` ([baseline run](${failsOnBase.viewUrl}))` : "";
493
+ return ` \xB7 also fails on base \`${failsOnBase.baselineSha.slice(0, SHORT_SHA_LENGTH)}\`${baselineLink}`;
494
+ }
403
495
  function renderUnpassedCaseBullet(unpassed, linkText, linkTarget) {
404
- return `- ${UNPASSED_STATUS_EMOJI[unpassed.status]} **${escapeHtml(withResolutionLabel(unpassed.name, unpassed.resolutionLabel))}** \u2014 ${describeUnpassedCase(unpassed)} \xB7 [${linkText}](${linkTarget})`;
496
+ return `- ${UNPASSED_STATUS_EMOJI[unpassed.status]} **${escapeHtml(withResolutionLabel(unpassed.name, unpassed.resolutionLabel))}** \u2014 ${describeUnpassedCase(unpassed)} \xB7 [${linkText}](${linkTarget})` + (unpassed.failsOnBase ? describeFailsOnBase(unpassed.failsOnBase) : "");
405
497
  }
406
498
  var UnpassedCaseSchema = z.object({
407
499
  name: z.string(),
@@ -411,7 +503,8 @@ var UnpassedCaseSchema = z.object({
411
503
  status: z.enum(UnpassedCaseStatus),
412
504
  failedStep: z.number().int().positive().optional(),
413
505
  detail: z.string(),
414
- viewUrl: z.string()
506
+ viewUrl: z.string(),
507
+ failsOnBase: z.object({ baselineRunId: z.string(), baselineSha: z.string(), viewUrl: z.string().optional() }).optional()
415
508
  });
416
509
  var E2eResultRecordSchema = z.object({
417
510
  total: z.number().int().nonnegative(),
@@ -467,7 +560,8 @@ function toUnpassedCase(test) {
467
560
  status: "failed" /* Failed */,
468
561
  failedStep: failureOrdinal(test, test.failureStepIndex),
469
562
  detail: oneLineDetail(test.error),
470
- viewUrl: test.viewUrl
563
+ viewUrl: test.viewUrl,
564
+ ...test.failsOnBase && { failsOnBase: test.failsOnBase }
471
565
  };
472
566
  }
473
567
  if (test.status === "inconclusive") {
@@ -532,6 +626,9 @@ var FailedTestSchema = z.object({
532
626
  steps: z.array(StepSchema),
533
627
  failureStepIndex: z.number().int().nonnegative(),
534
628
  error: z.string().min(1),
629
+ // A run of the same test case on the PR's base the author says failed too; build-pr-section verifies it before
630
+ // the walkthrough credits it.
631
+ baselineRunId: z.string().min(1).optional(),
535
632
  artifactsDir: z.string().min(1).optional(),
536
633
  useCaseName: z.string().min(1).optional(),
537
634
  description: z.string().min(1).optional(),
@@ -1054,7 +1151,7 @@ async function resolveGsScreenshotUrls(report, opts) {
1054
1151
  if (gsUrls.length === 0) {
1055
1152
  return report;
1056
1153
  }
1057
- const mcps = await import('./src-33XQK7NM.js');
1154
+ const mcps = await import('./src-MCJFH7AB.js');
1058
1155
  const credentials = await mcps.getCallerCredentialsAsync();
1059
1156
  if (!credentials.bearerToken && !credentials.apiKey) {
1060
1157
  stderrWrite(
@@ -1148,7 +1245,15 @@ ${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
1148
1245
  }
1149
1246
  return 1;
1150
1247
  }
1151
- const resolvedReport = await resolveGsScreenshotUrls(report, { stderrWrite: opts.stderrWrite });
1248
+ const verifyBaselines = opts.verifyBaselines ?? ((parsedReport) => verifyBaselineFailures(parsedReport, {
1249
+ cwd: process.cwd(),
1250
+ baseBranch: opts.baseBranch,
1251
+ runGit: runGitCommand,
1252
+ readRunResult: (runId) => local_exports.getRunResultStorageService().getRunResult(runId),
1253
+ stderrWrite: opts.stderrWrite
1254
+ }));
1255
+ const verifiedReport = await verifyBaselines(report);
1256
+ const resolvedReport = await resolveGsScreenshotUrls(verifiedReport, { stderrWrite: opts.stderrWrite });
1152
1257
  const sentinelCost = Buffer.byteLength(`${REPORT_SECTION_SENTINEL}
1153
1258
  `, "utf-8");
1154
1259
  const renderedSection = buildPrSection(resolvedReport, {
@@ -1174,7 +1279,8 @@ async function buildPrSectionCommand(options) {
1174
1279
  stdin: process.stdin,
1175
1280
  stdoutWrite: (s) => process.stdout.write(s),
1176
1281
  stderrWrite: (s) => process.stderr.write(s),
1177
- maxBodyBytes
1282
+ maxBodyBytes,
1283
+ baseBranch: options.base
1178
1284
  });
1179
1285
  if (code !== 0) {
1180
1286
  process.exitCode = code;
@@ -3192,16 +3298,6 @@ var NON_APP_PATH_PATTERNS = [
3192
3298
  /^\.gitlab-ci\.yml$/
3193
3299
  ];
3194
3300
 
3195
- // src/pr-walkthrough/constants.ts
3196
- var REPORT_SENTINEL = "muggle-pr-section";
3197
- var WALKTHROUGH_SLOT_MARKER = "<!-- muggle-pr-walkthrough:v1 -->";
3198
- var WALKTHROUGH_SKIPPED_MARKER = "<!-- muggle-pr-walkthrough-status:skipped -->";
3199
- var WALKTHROUGH_COMMENT_HEADING = "### Muggle AI \u2014 PR visual walkthrough";
3200
- var RERUN_NOTICE_OPENING = "<!-- muggle-pr-walkthrough-rerun -->";
3201
- var SHORT_SHA_LENGTH = 7;
3202
- var GH_COMMENT_TIMEOUT_MS = 1e4;
3203
- var DEPENDENCY_BOT_LOGINS = ["dependabot[bot]", "renovate[bot]"];
3204
-
3205
3301
  // src/pr-walkthrough/comment.ts
3206
3302
  function renderReservedComment() {
3207
3303
  return `${WALKTHROUGH_SLOT_MARKER}
@@ -3411,6 +3507,7 @@ var CHECK_SUMMARY_MAX_CASES = 50;
3411
3507
  var OUTCOME_CONCLUSIONS = {
3412
3508
  ["settled" /* Settled */]: "success" /* Success */,
3413
3509
  ["e2e-not-passing" /* E2eNotPassing */]: "failure" /* Failure */,
3510
+ ["e2e-fails-on-base" /* E2eFailsOnBase */]: "neutral" /* Neutral */,
3414
3511
  ["rerun-in-progress" /* RerunInProgress */]: null,
3415
3512
  ["unsettled" /* Unsettled */]: "failure" /* Failure */,
3416
3513
  ["unreadable" /* Unreadable */]: "neutral" /* Neutral */
@@ -3427,15 +3524,32 @@ function countLabel(count, status) {
3427
3524
  return count > 0 ? `${count} ${status}` : null;
3428
3525
  }
3429
3526
  function renderNotPassingSummary(record) {
3527
+ return renderCaseSummary(
3528
+ record,
3529
+ `E2E acceptance did not pass \u2014 ${unpassedTally(record)} of ${testCaseCount(record)}. Fix them, re-run the whole suite, and update the walkthrough comment; editing it re-runs this check.`
3530
+ );
3531
+ }
3532
+ function renderFailsOnBaseSummary(record) {
3533
+ return renderCaseSummary(
3534
+ record,
3535
+ `E2E acceptance: ${unpassedTally(record)} of ${testCaseCount(record)}, each verified to fail on the base too. This PR did not cause them; fix them on the base branch.`
3536
+ );
3537
+ }
3538
+ function testCaseCount(record) {
3539
+ return `${record.total} test ${record.total === 1 ? "case" : "cases"}`;
3540
+ }
3541
+ function unpassedTally(record) {
3430
3542
  const failed2 = record.unpassed.filter((c) => c.status === "failed" /* Failed */).length;
3431
- const tally = [
3543
+ return [
3432
3544
  countLabel(failed2, "failed" /* Failed */),
3433
3545
  countLabel(record.unpassed.length - failed2, "inconclusive" /* Inconclusive */)
3434
3546
  ].filter((part) => part !== null).join(", ");
3547
+ }
3548
+ function renderCaseSummary(record, headline) {
3435
3549
  const listed = record.unpassed.slice(0, CHECK_SUMMARY_MAX_CASES).map((unpassed) => renderUnpassedCaseBullet(unpassed, "open run", unpassed.viewUrl));
3436
3550
  const hidden = record.unpassed.length - listed.length;
3437
3551
  return [
3438
- `E2E acceptance did not pass \u2014 ${tally} of ${record.total} test ${record.total === 1 ? "case" : "cases"}. Fix them, re-run the whole suite, and update the walkthrough comment; editing it re-runs this check.`,
3552
+ headline,
3439
3553
  "",
3440
3554
  ...listed,
3441
3555
  ...hidden > 0 ? [`- \u2026and ${hidden} more \u2014 see the walkthrough comment.`] : []
@@ -3469,7 +3583,10 @@ function judgeComments(pr, comments, run) {
3469
3583
  }
3470
3584
  const record = latestE2eResult(bodies);
3471
3585
  if (record && record.unpassed.length > 0) {
3472
- return { outcome: "e2e-not-passing" /* E2eNotPassing */, summary: renderNotPassingSummary(record) };
3586
+ const isEveryUnpassedOnBase = record.unpassed.every(
3587
+ (unpassed) => unpassed.status === "failed" /* Failed */ && unpassed.failsOnBase !== void 0
3588
+ );
3589
+ return isEveryUnpassedOnBase ? { outcome: "e2e-fails-on-base" /* E2eFailsOnBase */, summary: renderFailsOnBaseSummary(record) } : { outcome: "e2e-not-passing" /* E2eNotPassing */, summary: renderNotPassingSummary(record) };
3473
3590
  }
3474
3591
  const outcome = verdictNow === "satisfied" /* Satisfied */ ? "settled" /* Settled */ : "unsettled" /* Unsettled */;
3475
3592
  return { outcome, summary: FIXED_SUMMARIES[outcome] };
@@ -6058,8 +6175,8 @@ function renderReloadReport(report, isDryRun) {
6058
6175
  const lines = report.decisions.map((decision) => {
6059
6176
  const label = decision.name ?? decision.shortId ?? decision.sessionId;
6060
6177
  const failed2 = report.failedSessionIds.includes(decision.sessionId) ? " (restart failed)" : "";
6061
- const refused = report.untrustedSessionIds.includes(decision.sessionId) ? ` (left running: ${decision.cwd ?? "its workspace"} refuses a background restart \u2014 run /reload-plugins in it)` : "";
6062
- return ` ${decision.action.padEnd(16)} ${label} ${decision.reason}${failed2}${refused}`;
6178
+ const refused2 = report.untrustedSessionIds.includes(decision.sessionId) ? ` (left running: ${decision.cwd ?? "its workspace"} refuses a background restart \u2014 run /reload-plugins in it)` : "";
6179
+ return ` ${decision.action.padEnd(16)} ${label} ${decision.reason}${failed2}${refused2}`;
6063
6180
  });
6064
6181
  const isRestart = (action) => action === "respawn" /* Respawn */ || action === "resume-reconcile" /* ResumeReconcile */;
6065
6182
  const respawnVerb = isDryRun ? "would restart" : "restarted";
@@ -6137,7 +6254,7 @@ function createProgram() {
6137
6254
  program.command("login").description("Authenticate with Muggle AI (uses device code flow)").option("--key-name <name>", "Name for the API key").option("--key-expiry <expiry>", "API key expiry: 30d, 90d, 1y, never", "90d").action(loginCommand);
6138
6255
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
6139
6256
  program.command("status").description("Show authentication status").action(statusCommand);
6140
- program.command("build-pr-section").description("Render a muggle-do PR body evidence block from an e2e report on stdin").option("--max-body-bytes <n>", "Max UTF-8 byte budget for the PR body (default 60000)").action(buildPrSectionCommand);
6257
+ program.command("build-pr-section").description("Render a muggle-do PR body evidence block from an e2e report on stdin").option("--max-body-bytes <n>", "Max UTF-8 byte budget for the PR body (default 60000)").option("--base <branch>", "Branch the PR merges into, for verifying cited baseline runs (default: the remote's default branch)").action(buildPrSectionCommand);
6141
6258
  program.command("run-view").argument("[runId]", "Run id, or a unique prefix of one (default: the newest run)").description("Show a local run's steps beside the frames they produced").option("--html", "Render a self-contained HTML page instead of terminal text").option("--open", "Open the rendered page in the default browser (implies --html)").option("--out-dir <dir>", "Directory to write the page into (default: current directory)").action(runViewCommand);
6142
6259
  program.command("pr-walkthrough-check").description("Check that a PR's Muggle AI walkthrough comment carries a result or a stated skip").option("--repo <owner/repo>", "Repository (default: $GITHUB_REPOSITORY)").option("--pr <number>", "Pull request number (default: the Actions event payload)").option("--head-sha <sha>", "Commit the check run attaches to (default: the PR head)").option("--check-run", "Publish the verdict as a check run instead of failing the process").action(prWalkthroughCheckCommand);
6143
6260
  program.command("checkout-freshness").description("Fetch the base branch and check that a checkout is built on its latest commit").option("--cwd <dir>", "Directory inside the checkout (default: the current directory)").option("--base <branch>", "Branch the change merges into (default: origin's default branch)").option("--no-fetch", "Check against the last fetched base instead of fetching").option("--json", "Print the full report as JSON").action(checkoutFreshnessCommand);
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-SJBYMBC2.js';
3
- import './chunk-FI6VHHZN.js';
2
+ import { runCli } from './chunk-PT5EGMBA.js';
3
+ import './chunk-6BAB7IVG.js';
4
4
 
5
5
  // src/cli/main.ts
6
6
  runCli().catch((error) => {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-SJBYMBC2.js';
2
- export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-FI6VHHZN.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-PT5EGMBA.js';
2
+ export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-6BAB7IVG.js';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
4
- "version": "5.27.0-staging.245",
4
+ "version": "5.27.0-staging.246",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.245",
7
+ "@muggleai/works@5.27.0-staging.246",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.27.0-staging.245",
3
+ "version": "5.27.0-staging.246",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
5
  "author": {
6
6
  "name": "Muggle AI",
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
- "version": "5.27.0-staging.245",
5
+ "version": "5.27.0-staging.246",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.245",
7
+ "@muggleai/works@5.27.0-staging.246",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -54,6 +54,8 @@ Map each bucket to exactly one disposition. Buckets are defined in [`../_shared/
54
54
 
55
55
  A `repair` disposition requires a named defect: the failing step, expected-versus-actual, and the code path it implicates. Without one it is `ask`, whatever the bucket said.
56
56
 
57
+ **A failure the diff cannot explain may predate it.** When a `product-defect` or `product-uxux` failure names no code path the diff touched, replay the same test case against a checkout of the base before repairing anything: a worktree at the base tip's full SHA, with its own stack claim and services, under the same runSettings record. If that run fails the same way, its disposition is `baseline` — the defect is on the base, not this change's to fix — and the entry carries that run's id as `baselineRunId` into the walkthrough report ([`../muggle-pr-visual-walkthrough/e2e-report-assembly.md`](../muggle-pr-visual-walkthrough/e2e-report-assembly.md)). If it passes, the change caused the failure and the original disposition stands. A `baseline` entry is never re-run or waived; when every remaining failure is `baseline`, the loop clears.
58
+
57
59
  An Inconclusive entry Stage 6 recorded as `prerequisites unmet` never executed. It carries no bucket and none may be derived from it — there is no run to read. Its disposition is `ask`, and the question names the missing prerequisite: what failed is the project's test-plan chain, not the diff.
58
60
 
59
61
  An Inconclusive entry Stage 6 recorded as `primary failed` never executed either: it is a record held back by its test case's failed primary. It takes no disposition of its own; the primary's entry is the one repaired, and re-running the test case runs it again.
@@ -30,6 +30,7 @@ For each result:
30
30
  2. `viewUrl` — from the run result (deep-links to the specific run).
31
31
  3. `status` — from `muggle-local-run-result-get`.
32
32
  4. If failed: also capture `failureStepIndex`, `error`, `artifactsDir` from the run result.
33
+ 5. If the caller hands over a `baselineRunId` for a failed entry — a run of the same test case against the base that failed too — set it on the entry. `build-pr-section` credits it only after checking the run itself: same test case, failed, same runSettings record, and a stack that served a base commit between the PR's fork point and the base tip. A credited failure reads "also fails on base" and stops holding the walkthrough check red; a refused one is named on stderr and stays a plain failure. Pass `--base <branch>` when the PR does not target the remote's default branch.
33
34
 
34
35
  ## Last-resort: no cloud refs on the run result
35
36
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "release": "5.26.0",
3
- "buildId": "run-245-1",
4
- "commitSha": "d4e56d068786541b25d22819c6336d5c5f5baf6b",
5
- "buildTime": "2026-10-29T16:50:53Z",
3
+ "buildId": "run-246-1",
4
+ "commitSha": "a4306b9b72399d0b48c1242356d766a492e6fc1b",
5
+ "buildTime": "2026-10-29T17:19:18Z",
6
6
  "serviceName": "muggle-ai-works-mcp"
7
7
  }
@@ -1 +1 @@
1
- export { ACTION_SCRIPT_FILE_NAME, BASE_REMOTE_NAME, BLOCKING_FRESHNESS_VERDICTS, BRANCH_CREATION_REFLOG_PREFIX, BUSY_EXIT_CODE, CLAUDE_CODE_AUTH_STATUS_ARGS, CLAUDE_CODE_CHECK_TIMEOUT_MS, CLAUDE_CODE_EXECUTABLE, CLAUDE_CODE_EXECUTABLE_NOT_FOUND_CODE, CLAUDE_CODE_SESSION_LINK_ENV_VARS, CLAUDE_CODE_VERSION_ARGS, CheckoutFreshnessVerdict, ClaudeCodeReadiness, DEFAULT_LOCAL_LLM_BASE_URL, DEFAULT_PREFERENCES, ELECTRON_APP_EXECUTABLE_PATH_SEGMENTS, ElectronAppPlatform, ElectronAppReleaseStream, HTML_VIEW_OFFER_LINE, LLM_CREDENTIALS_FILE_MODE, LLM_CREDENTIALS_FILE_NAME, LLM_SOURCE_HEADER, LLM_SOURCE_OPTIONS, LLM_SOURCE_OWNED_ENV_NAMES, LLM_SOURCE_PROMPT, LLM_SOURCE_SCOPE_NOTE, LlmSource, MASKED_API_KEY_PREFIX, MASKED_API_KEY_VISIBLE_CHARS, MAX_LISTED_INHERITED_COMMITS, ONBOARDING_BLANKET_OPTIONS, ONBOARDING_CHOICE_GROUPS, ONBOARDING_MAX_OFFERS, ONBOARDING_PRIMER_BULLETS, ONBOARDING_PRIMER_HEADLINE, ONBOARDING_TOGGLE_GROUPS, ONBOARDING_TOGGLE_PROMPT, ONBOARDING_UNSELECTED_TOGGLE_VALUE, OnboardingBlanketChoice, OnboardingGroupKind, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RUN_SESSIONS_DIR_NAME, RUN_VIEW_DASHBOARD_LINK_LABEL, RunViewError, RuntimeTarget, SCREENSHOT_DIR_RELATIVE_PATH, SCREENSHOT_FILE_EXTENSION, StackCheckVerdict, StackClaimVerdict, StackMismatchKind, StackPortAction, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, applyLlmSourceSelection, applyOnboardingAnswers, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppDirName, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, buildLlmSourcePlan, buildOnboardingPlan, buildToolInputJsonSchema, calculateFileChecksum, checkServiceStack, claimServiceStack, completeOnboarding, createApiKeyWithToken, createChildLogger, decideStackClaim, decideStackReap, decideStackRelease, decideStackRunEnd, decideStackRunStart, deleteApiKeyData, deleteCredentials, describeLlmSourceStatus, describeStackMismatch, e2e_exports as e2e, evaluateCheckoutFreshness, findVacatedNeeds, finishStackRun, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDashboardBaseUrl, getDataDir2 as getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppSignedFromVersion, getElectronAppVersion, getElectronAppVersionSource, getLlmSourceOption, getLocalQaTools, getLogger, getOnboardingToggleKeys, getPlatformKey, getQaTools, getReleaseSignerIdentityUri, getRunSessionsDir, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isElectronAppPlatform, isFirstRun, isValidLocalBaseUrl, loadApiKeyData, loadCredentials, local_exports as localQa, markOnboardingCompleted, maskLlmApiKey, mcp_exports as mcp, needsConflict, needsOnboarding, openBrowserUrl, parseElectronAppDirName, performLogin, performLogout, pollDeviceCode, portOfUrl, e2e_exports as qa, readPreferencesMetadata, readRunView, readServiceStackStatus, reapServiceStack, reconcileProjectPreferences, recordOnboardingSkip, releaseServiceStack, renderRunViewHtml, renderRunViewText, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveBlanketPreferences, resolveElectronAppExecutablePath, resolveElectronAppPathOrNull, resolveLlmSourceStatus, resolveOnboardingAnswers, resolvePreferences, resolveRunSessionPath, resolveRuntimeTarget, resolveTogglePreferences, runGitCommand, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, startStackRun, stopVacatedServices, toDisplayRef, toolRequiresAuth, validatePreference, verifyFileChecksum, writeLlmEnvBlock, writePreferences, writePreferencesWithMetadata, writeRunViewHtml } from './chunk-FI6VHHZN.js';
1
+ export { ACTION_SCRIPT_FILE_NAME, BASE_REMOTE_NAME, BLOCKING_FRESHNESS_VERDICTS, BRANCH_CREATION_REFLOG_PREFIX, BUSY_EXIT_CODE, CLAUDE_CODE_AUTH_STATUS_ARGS, CLAUDE_CODE_CHECK_TIMEOUT_MS, CLAUDE_CODE_EXECUTABLE, CLAUDE_CODE_EXECUTABLE_NOT_FOUND_CODE, CLAUDE_CODE_SESSION_LINK_ENV_VARS, CLAUDE_CODE_VERSION_ARGS, CheckoutFreshnessVerdict, ClaudeCodeReadiness, DEFAULT_LOCAL_LLM_BASE_URL, DEFAULT_PREFERENCES, ELECTRON_APP_EXECUTABLE_PATH_SEGMENTS, ElectronAppPlatform, ElectronAppReleaseStream, HTML_VIEW_OFFER_LINE, LLM_CREDENTIALS_FILE_MODE, LLM_CREDENTIALS_FILE_NAME, LLM_SOURCE_HEADER, LLM_SOURCE_OPTIONS, LLM_SOURCE_OWNED_ENV_NAMES, LLM_SOURCE_PROMPT, LLM_SOURCE_SCOPE_NOTE, LlmSource, MASKED_API_KEY_PREFIX, MASKED_API_KEY_VISIBLE_CHARS, MAX_LISTED_INHERITED_COMMITS, ONBOARDING_BLANKET_OPTIONS, ONBOARDING_CHOICE_GROUPS, ONBOARDING_MAX_OFFERS, ONBOARDING_PRIMER_BULLETS, ONBOARDING_PRIMER_HEADLINE, ONBOARDING_TOGGLE_GROUPS, ONBOARDING_TOGGLE_PROMPT, ONBOARDING_UNSELECTED_TOGGLE_VALUE, OnboardingBlanketChoice, OnboardingGroupKind, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RUN_SESSIONS_DIR_NAME, RUN_VIEW_DASHBOARD_LINK_LABEL, RunViewError, RuntimeTarget, SCREENSHOT_DIR_RELATIVE_PATH, SCREENSHOT_FILE_EXTENSION, StackCheckVerdict, StackClaimVerdict, StackMismatchKind, StackPortAction, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, applyLlmSourceSelection, applyOnboardingAnswers, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppDirName, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, buildLlmSourcePlan, buildOnboardingPlan, buildToolInputJsonSchema, calculateFileChecksum, checkServiceStack, claimServiceStack, completeOnboarding, createApiKeyWithToken, createChildLogger, decideStackClaim, decideStackReap, decideStackRelease, decideStackRunEnd, decideStackRunStart, deleteApiKeyData, deleteCredentials, describeLlmSourceStatus, describeStackMismatch, e2e_exports as e2e, evaluateCheckoutFreshness, findVacatedNeeds, finishStackRun, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDashboardBaseUrl, getDataDir2 as getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppSignedFromVersion, getElectronAppVersion, getElectronAppVersionSource, getLlmSourceOption, getLocalQaTools, getLogger, getOnboardingToggleKeys, getPlatformKey, getQaTools, getReleaseSignerIdentityUri, getRunSessionsDir, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isElectronAppPlatform, isFirstRun, isValidLocalBaseUrl, loadApiKeyData, loadCredentials, local_exports as localQa, markOnboardingCompleted, maskLlmApiKey, mcp_exports as mcp, needsConflict, needsOnboarding, openBrowserUrl, parseElectronAppDirName, performLogin, performLogout, pollDeviceCode, portOfUrl, e2e_exports as qa, readPreferencesMetadata, readRunView, readServiceStackStatus, reapServiceStack, reconcileProjectPreferences, recordOnboardingSkip, releaseServiceStack, renderRunViewHtml, renderRunViewText, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveBlanketPreferences, resolveElectronAppExecutablePath, resolveElectronAppPathOrNull, resolveLlmSourceStatus, resolveOnboardingAnswers, resolvePreferences, resolveRunSessionPath, resolveRuntimeTarget, resolveTogglePreferences, runGitCommand, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, startStackRun, stopVacatedServices, toDisplayRef, toolRequiresAuth, validatePreference, verifyFileChecksum, writeLlmEnvBlock, writePreferences, writePreferencesWithMetadata, writeRunViewHtml } from './chunk-6BAB7IVG.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@muggleai/works",
3
3
  "mcpName": "io.github.multiplex-ai/muggle",
4
- "version": "5.27.0-staging.245",
4
+ "version": "5.27.0-staging.246",
5
5
  "description": "13 years of Microsoft hyperscale engineering, packaged as a harness for your AI coding agent.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
4
- "version": "5.27.0-staging.245",
4
+ "version": "5.27.0-staging.246",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.245",
7
+ "@muggleai/works@5.27.0-staging.246",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.27.0-staging.245",
3
+ "version": "5.27.0-staging.246",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
5
  "author": {
6
6
  "name": "Muggle AI",
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
- "version": "5.27.0-staging.245",
5
+ "version": "5.27.0-staging.246",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
package/plugin/.mcp.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.245",
7
+ "@muggleai/works@5.27.0-staging.246",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -54,6 +54,8 @@ Map each bucket to exactly one disposition. Buckets are defined in [`../_shared/
54
54
 
55
55
  A `repair` disposition requires a named defect: the failing step, expected-versus-actual, and the code path it implicates. Without one it is `ask`, whatever the bucket said.
56
56
 
57
+ **A failure the diff cannot explain may predate it.** When a `product-defect` or `product-uxux` failure names no code path the diff touched, replay the same test case against a checkout of the base before repairing anything: a worktree at the base tip's full SHA, with its own stack claim and services, under the same runSettings record. If that run fails the same way, its disposition is `baseline` — the defect is on the base, not this change's to fix — and the entry carries that run's id as `baselineRunId` into the walkthrough report ([`../muggle-pr-visual-walkthrough/e2e-report-assembly.md`](../muggle-pr-visual-walkthrough/e2e-report-assembly.md)). If it passes, the change caused the failure and the original disposition stands. A `baseline` entry is never re-run or waived; when every remaining failure is `baseline`, the loop clears.
58
+
57
59
  An Inconclusive entry Stage 6 recorded as `prerequisites unmet` never executed. It carries no bucket and none may be derived from it — there is no run to read. Its disposition is `ask`, and the question names the missing prerequisite: what failed is the project's test-plan chain, not the diff.
58
60
 
59
61
  An Inconclusive entry Stage 6 recorded as `primary failed` never executed either: it is a record held back by its test case's failed primary. It takes no disposition of its own; the primary's entry is the one repaired, and re-running the test case runs it again.
@@ -30,6 +30,7 @@ For each result:
30
30
  2. `viewUrl` — from the run result (deep-links to the specific run).
31
31
  3. `status` — from `muggle-local-run-result-get`.
32
32
  4. If failed: also capture `failureStepIndex`, `error`, `artifactsDir` from the run result.
33
+ 5. If the caller hands over a `baselineRunId` for a failed entry — a run of the same test case against the base that failed too — set it on the entry. `build-pr-section` credits it only after checking the run itself: same test case, failed, same runSettings record, and a stack that served a base commit between the PR's fork point and the base tip. A credited failure reads "also fails on base" and stops holding the walkthrough check red; a refused one is named on stderr and stays a plain failure. Pass `--base <branch>` when the PR does not target the remote's default branch.
33
34
 
34
35
  ## Last-resort: no cloud refs on the run result
35
36