@muggleai/works 5.31.0-staging.276 → 5.31.0-staging.278

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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +4 -3
  3. package/dist/{chunk-HK7XCCNT.js → chunk-CGKARU5D.js} +99 -12
  4. package/dist/{chunk-7J7DS4SA.js → chunk-NTEBYUCA.js} +1 -1
  5. package/dist/cli.js +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  8. package/dist/plugin/.codex-mcp.json +1 -1
  9. package/dist/plugin/.codex-plugin/plugin.json +1 -1
  10. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  11. package/dist/plugin/.mcp.json +1 -1
  12. package/dist/plugin/scripts/guardrails.mjs +5 -3
  13. package/dist/plugin/skills/do/background-e2e.md +3 -3
  14. package/dist/plugin/skills/do/build.md +1 -1
  15. package/dist/plugin/skills/do/e2e-acceptance.md +3 -2
  16. package/dist/plugin/skills/do/open-prs/forward.md +126 -102
  17. package/dist/plugin/skills/do/open-prs.md +2 -2
  18. package/dist/plugin/skills/do/wave-e2e.md +14 -8
  19. package/dist/plugin/skills/muggle-do/SKILL.md +13 -6
  20. package/dist/plugin/skills/muggle-preferences/preference-gates/autoRouteBuildToMuggleDo.md +1 -1
  21. package/dist/release-manifest.json +3 -3
  22. package/dist/{src-QA7TWQ2M.js → src-LYOBYPFZ.js} +1 -1
  23. package/package.json +1 -1
  24. package/plugin/.claude-plugin/plugin.json +1 -1
  25. package/plugin/.codex-mcp.json +1 -1
  26. package/plugin/.codex-plugin/plugin.json +1 -1
  27. package/plugin/.cursor-plugin/plugin.json +1 -1
  28. package/plugin/.mcp.json +1 -1
  29. package/plugin/scripts/guardrails.mjs +5 -3
  30. package/plugin/skills/do/background-e2e.md +3 -3
  31. package/plugin/skills/do/build.md +1 -1
  32. package/plugin/skills/do/e2e-acceptance.md +3 -2
  33. package/plugin/skills/do/open-prs/forward.md +126 -102
  34. package/plugin/skills/do/open-prs.md +2 -2
  35. package/plugin/skills/do/wave-e2e.md +14 -8
  36. package/plugin/skills/muggle-do/SKILL.md +13 -6
  37. package/plugin/skills/muggle-preferences/preference-gates/autoRouteBuildToMuggleDo.md +1 -1
@@ -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.31.0-staging.276"
12
+ "version": "5.31.0-staging.278"
13
13
  }
14
14
  ]
15
15
  }
package/README.md CHANGED
@@ -40,12 +40,13 @@ Works in Claude Code, Cursor, Codex, Windsurf, and any MCP client. Powered by [M
40
40
  | 3 | Build | Delegates real design surface to a design → plan → subagent build, then commits (signed) | Conventional commits |
41
41
  | 4 | Impact analysis | Maps the diff to the user flows it can break | Affected-flow list |
42
42
  | 5 | Unit tests | Runs the suite authored in stage 3 | Exit code |
43
- | 6 | Acceptance | Drives a real browser through the affected flows | Verdict + `runId` + per-step screenshots |
44
- | 7 | Pull request | Opens the PR/MR with the evidence block in the body | PR URL |
43
+ | 5.5 | Pull request | Opens the PR/MR before the browser run, its walkthrough check red until stage 7 | PR URL |
44
+ | 6 | Acceptance | Drives a real browser through the affected flows, its progress showing on the PR | Verdict + `runId` + per-step screenshots |
45
+ | 7 | Walkthrough | Settles the PR's walkthrough comment with the run's evidence | Walkthrough comment |
45
46
  | 7.5 | Repair | Investigates and fixes acceptance failures, up to 3 iterations | Repair log |
46
47
  | 8 | Watcher | Polls reviews, CI, and base-branch drift; re-enters the cycle on each | `followup.log` |
47
48
 
48
- Stage 7 refuses to run until the Definition of Done holds. Stage 8 is dispatched only once stage 7.5 clears — a cycle with unrepaired failures never reaches a watcher.
49
+ Stage 5.5 refuses to open the PR until build and unit tests meet the Definition of Done, and stage 7 refuses to settle its walkthrough without an E2E verdict. Stage 8 is dispatched only once stage 7.5 clears — a cycle with unrepaired failures never reaches a watcher.
49
50
 
50
51
  ---
51
52
 
@@ -1,4 +1,4 @@
1
- import { __export, getLogger, TelemetryEnvironment, 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-7J7DS4SA.js';
1
+ import { __export, getLogger, TelemetryEnvironment, 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-NTEBYUCA.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';
@@ -1165,7 +1165,7 @@ async function resolveGsScreenshotUrls(report, opts) {
1165
1165
  if (gsUrls.length === 0) {
1166
1166
  return report;
1167
1167
  }
1168
- const mcps = await import('./src-QA7TWQ2M.js');
1168
+ const mcps = await import('./src-LYOBYPFZ.js');
1169
1169
  const credentials = await mcps.getCallerCredentialsAsync();
1170
1170
  if (!credentials.bearerToken && !credentials.apiKey) {
1171
1171
  stderrWrite(
@@ -2937,6 +2937,11 @@ var MUGGLE_MCP_SERVER_PATTERN = /@muggleai[\\/]works\b.*\bserve\b/;
2937
2937
  var PR_WATCH_LOOP_PATTERN = /pr-watch-loop\.sh\b.*?--slot\s+(?:"([^"]+)"|(\S+))/;
2938
2938
  var CLAUDE_SESSION_REGISTRY_DIR_NAME = "sessions";
2939
2939
  var CLAUDE_PROJECTS_DIR_NAME = "projects";
2940
+ var CLAUDE_JOBS_DIR_NAME = "jobs";
2941
+ var CLAUDE_JOB_STATE_FILE_NAME = "state.json";
2942
+ var SESSION_NAME_FLAG = "--name";
2943
+ var CLAUDE_PROMPT_NAME_MAX_LENGTH = 200;
2944
+ var CUSTOM_TITLE_ENTRY_TYPE = "custom-title";
2940
2945
  var CLAUDE_INSTALLED_PLUGINS_RELATIVE_PATH = ["plugins", "installed_plugins.json"];
2941
2946
  var CLAUDE_GLOBAL_CONFIG_FILE_NAME = ".claude.json";
2942
2947
  var CLAUDE_SETTINGS_FILE_NAME = "settings.json";
@@ -3004,6 +3009,7 @@ function resolveWatchPaths(env = process.env) {
3004
3009
  slotsDir: join(muggleDataDir, ...PR_WATCH_SLOTS_RELATIVE_PATH),
3005
3010
  claudeSessionRegistryDir: join(claudeConfigDir, CLAUDE_SESSION_REGISTRY_DIR_NAME),
3006
3011
  claudeProjectsDir: join(claudeConfigDir, CLAUDE_PROJECTS_DIR_NAME),
3012
+ claudeJobsDir: join(claudeConfigDir, CLAUDE_JOBS_DIR_NAME),
3007
3013
  claudeInstalledPluginsFile: join(claudeConfigDir, ...CLAUDE_INSTALLED_PLUGINS_RELATIVE_PATH),
3008
3014
  claudeGlobalConfigFile: join(env.CLAUDE_CONFIG_DIR || homedir(), CLAUDE_GLOBAL_CONFIG_FILE_NAME),
3009
3015
  claudeSettingsFile: join(claudeConfigDir, CLAUDE_SETTINGS_FILE_NAME),
@@ -5167,6 +5173,44 @@ function readCodexSessions(sessionsDir, nowMs) {
5167
5173
  }
5168
5174
  return sessions;
5169
5175
  }
5176
+ function jobStatePath(jobsDir, shortId) {
5177
+ return join(jobsDir, shortId, CLAUDE_JOB_STATE_FILE_NAME);
5178
+ }
5179
+ function readJobState(statePath) {
5180
+ try {
5181
+ const parsed = JSON.parse(readFileSync(statePath, "utf-8"));
5182
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
5183
+ } catch {
5184
+ return void 0;
5185
+ }
5186
+ }
5187
+ function respawnFlagsOf(state) {
5188
+ return Array.isArray(state.respawnFlags) ? state.respawnFlags.filter((flag) => typeof flag === "string") : [];
5189
+ }
5190
+ function nameFlagOf(respawnFlags) {
5191
+ const flagIndex = respawnFlags.lastIndexOf(SESSION_NAME_FLAG);
5192
+ return flagIndex === -1 ? void 0 : optionalString(respawnFlags[flagIndex + 1]);
5193
+ }
5194
+ function readClaudeJob(jobsDir, shortId) {
5195
+ const state = readJobState(jobStatePath(jobsDir, shortId));
5196
+ if (state === void 0) return void 0;
5197
+ const intent = optionalString(state.intent);
5198
+ return { respawnFlags: respawnFlagsOf(state), ...intent === void 0 ? {} : { intent } };
5199
+ }
5200
+ function pinClaudeJobName(jobsDir, shortId, name) {
5201
+ const statePath = jobStatePath(jobsDir, shortId);
5202
+ const state = readJobState(statePath);
5203
+ if (state === void 0) return false;
5204
+ const respawnFlags = respawnFlagsOf(state);
5205
+ if (nameFlagOf(respawnFlags) === name) return true;
5206
+ const otherFlags = respawnFlags.filter((flag, index) => flag !== SESSION_NAME_FLAG && respawnFlags[index - 1] !== SESSION_NAME_FLAG);
5207
+ try {
5208
+ writeFileSync(statePath, JSON.stringify({ ...state, respawnFlags: [...otherFlags, SESSION_NAME_FLAG, name] }, null, 2));
5209
+ return true;
5210
+ } catch {
5211
+ return false;
5212
+ }
5213
+ }
5170
5214
  function findTranscriptPath(projectsDir, sessionId) {
5171
5215
  let projectDirNames;
5172
5216
  try {
@@ -5211,6 +5255,23 @@ function readLimitStop(projectsDir, sessionId) {
5211
5255
  }
5212
5256
  return void 0;
5213
5257
  }
5258
+ function readCustomTitle(projectsDir, sessionId) {
5259
+ const transcriptPath = findTranscriptPath(projectsDir, sessionId);
5260
+ if (transcriptPath === void 0) return void 0;
5261
+ let transcript;
5262
+ try {
5263
+ transcript = readFileSync(transcriptPath, "utf-8");
5264
+ } catch {
5265
+ return void 0;
5266
+ }
5267
+ let customTitle;
5268
+ for (const line of transcript.split("\n")) {
5269
+ if (!line.includes(CUSTOM_TITLE_ENTRY_TYPE)) continue;
5270
+ const entry = parseTranscriptLine(line);
5271
+ if (entry?.type === CUSTOM_TITLE_ENTRY_TYPE) customTitle = optionalString(entry.customTitle) ?? customTitle;
5272
+ }
5273
+ return customTitle;
5274
+ }
5214
5275
  function findClaudeSessionCwd(projectsDir, sessionId) {
5215
5276
  const transcriptPath = findTranscriptPath(projectsDir, sessionId);
5216
5277
  if (transcriptPath === void 0) return void 0;
@@ -5430,7 +5491,7 @@ function startServiceLoops(assessments, nowMs, isPollingDisabled, ownerSessionOf
5430
5491
  }
5431
5492
 
5432
5493
  // src/watch/session-restart.ts
5433
- function resumeSession(target, runCommand, waitForExit, isSessionRunning2) {
5494
+ function resumeSession(target, runCommand, waitForExit, isSessionRunning2, pinSessionName) {
5434
5495
  const runningSessionId = target.runningSessionId ?? target.sessionId;
5435
5496
  if (target.runningShortId !== void 0) {
5436
5497
  runCommand("claude", ["stop", target.runningShortId]);
@@ -5438,6 +5499,7 @@ function resumeSession(target, runCommand, waitForExit, isSessionRunning2) {
5438
5499
  } else if (isSessionRunning2(runningSessionId)) {
5439
5500
  return { outcome: "still-running" /* StillRunning */ };
5440
5501
  }
5502
+ pinSessionName(target.sessionId, target.runningShortId ?? shortSessionId(target.sessionId));
5441
5503
  const resumed = runCommand("claude", ["--resume", target.sessionId, "--bg", target.prompt], { cwd: target.cwd, isStderrCaptured: true });
5442
5504
  if (resumed?.exitCode !== 0) return { outcome: "failed" /* Failed */ };
5443
5505
  const backgroundedShortId = resumed.stdout.match(BACKGROUNDED_SESSION_ID_PATTERN)?.[1];
@@ -5530,7 +5592,8 @@ function executeWatcherRecoveries(planned, attemptsByOwner, deps) {
5530
5592
  { sessionId: recovery.ownerSessionId, cwd, prompt, runningShortId: recovery.ownerShortId, runningSessionId: recovery.ownerRunningSessionId },
5531
5593
  deps.runCommand,
5532
5594
  deps.waitForExit,
5533
- deps.isSessionRunning
5595
+ deps.isSessionRunning,
5596
+ deps.pinSessionName
5534
5597
  );
5535
5598
  if (resumed.outcome === "continued" /* Continued */) {
5536
5599
  markServiceLoopReportDelivered(recovery.slotDir);
@@ -5889,7 +5952,8 @@ function executeLimitResumes(planned, attemptsBySession, deps) {
5889
5952
  { sessionId: decision.sessionId, cwd, prompt: LIMIT_RESUME_PROMPT, runningShortId: decision.runningShortId },
5890
5953
  deps.runCommand,
5891
5954
  deps.waitForExit,
5892
- deps.isSessionRunning
5955
+ deps.isSessionRunning,
5956
+ deps.pinSessionName
5893
5957
  );
5894
5958
  outcome = continuedOutcomeOf(resumed.outcome);
5895
5959
  message = continueMessageOf(resumed, decision.reason);
@@ -6159,7 +6223,7 @@ function planReloadFanOut(sessions, pending, watchOwnerSessionIds, maxRestarts =
6159
6223
  }
6160
6224
  return decisions;
6161
6225
  }
6162
- function executeReloadFanOut(decisions, runCommand, canRestartIn) {
6226
+ function executeReloadFanOut(decisions, runCommand, canRestartIn, pinSessionName) {
6163
6227
  const restartableByCwd = /* @__PURE__ */ new Map();
6164
6228
  const isRestartable = (cwd) => {
6165
6229
  if (!restartableByCwd.has(cwd)) restartableByCwd.set(cwd, canRestartIn(cwd));
@@ -6172,6 +6236,7 @@ function executeReloadFanOut(decisions, runCommand, canRestartIn) {
6172
6236
  report.untrustedSessionIds.push(decision.sessionId);
6173
6237
  continue;
6174
6238
  }
6239
+ pinSessionName(decision.sessionId, decision.shortId);
6175
6240
  const isRestarted = runCommand("claude", ["respawn", decision.shortId])?.exitCode === 0;
6176
6241
  (isRestarted ? report.respawnedSessionIds : report.failedSessionIds).push(decision.sessionId);
6177
6242
  }
@@ -6372,6 +6437,16 @@ function stampSessionLoads(sessions, table, tracked, installs, nowMs) {
6372
6437
  });
6373
6438
  }
6374
6439
 
6440
+ // src/watch/session-names.ts
6441
+ function isPlaceholderSessionName(name, shortId) {
6442
+ return name === void 0 || name === shortId || name === LIMIT_RESUME_PROMPT || name.startsWith("/");
6443
+ }
6444
+ function resolveSessionName(sources) {
6445
+ const promptName = sources.job?.intent?.replace(/\s+/g, " ").trim().slice(0, CLAUDE_PROMPT_NAME_MAX_LENGTH);
6446
+ const candidates = [sources.customTitle, sources.job && nameFlagOf(sources.job.respawnFlags), sources.registeredName, promptName];
6447
+ return candidates.find((candidate) => !isPlaceholderSessionName(candidate, sources.shortId));
6448
+ }
6449
+
6375
6450
  // src/watch/terminal-pr-cache.ts
6376
6451
  function isRecheckDue(record, nowMs) {
6377
6452
  if (!record) return true;
@@ -6677,12 +6752,12 @@ function reloadRecordFor(state, targetAtMs) {
6677
6752
  }
6678
6753
  function runReloadPass(record, claudeSessions, watchOwnerSessionIds, deps, at) {
6679
6754
  const decisions = planReloadFanOut(claudeSessions, record, watchOwnerSessionIds);
6680
- const report = executeReloadFanOut(decisions, deps.runCommand, canRestartVia(deps));
6755
+ const report = executeReloadFanOut(decisions, deps.runCommand, canRestartVia(deps), pinSessionNameVia(deps, claudeSessions));
6681
6756
  return { report, pendingReload: advancePendingReload(record, report), events: reloadEvents(report, at) };
6682
6757
  }
6683
6758
  function runRemoteControlPass(login, claudeSessions, restartsBySession, maxRestarts, deps, at) {
6684
6759
  const plan = planRemoteControlRestarts(claudeSessions, login, restartsBySession, deps.nowMs, maxRestarts);
6685
- const executed = executeReloadFanOut(plan.decisions, deps.runCommand, canRestartVia(deps));
6760
+ const executed = executeReloadFanOut(plan.decisions, deps.runCommand, canRestartVia(deps), pinSessionNameVia(deps, claudeSessions));
6686
6761
  return {
6687
6762
  report: { ...executed, connectedSessionIds: plan.connectedSessionIds },
6688
6763
  restartsBySession: recordRemoteControlRestarts(restartsBySession, executed, deps.nowMs),
@@ -6698,14 +6773,26 @@ function waitForExitVia(deps) {
6698
6773
  function isSessionRunningVia(deps) {
6699
6774
  return (sessionId) => isSessionRunning(deps.runCommand, deps.paths.claudeSessionRegistryDir, sessionId) !== false;
6700
6775
  }
6701
- function resumeDepsOf(deps) {
6776
+ function pinSessionNameVia(deps, claudeSessions) {
6777
+ return (sessionId, shortId) => {
6778
+ const name = resolveSessionName({
6779
+ shortId,
6780
+ customTitle: readCustomTitle(deps.paths.claudeProjectsDir, sessionId),
6781
+ job: readClaudeJob(deps.paths.claudeJobsDir, shortId),
6782
+ registeredName: claudeSessions?.find((session) => session.sessionId === sessionId)?.name
6783
+ });
6784
+ if (name !== void 0) pinClaudeJobName(deps.paths.claudeJobsDir, shortId, name);
6785
+ };
6786
+ }
6787
+ function resumeDepsOf(deps, claudeSessions) {
6702
6788
  return {
6703
6789
  runCommand: deps.runCommand,
6704
6790
  claudeProjectsDir: deps.paths.claudeProjectsDir,
6705
6791
  nowMs: deps.nowMs,
6706
6792
  canRestartIn: canRestartVia(deps),
6707
6793
  waitForExit: waitForExitVia(deps),
6708
- isSessionRunning: isSessionRunningVia(deps)
6794
+ isSessionRunning: isSessionRunningVia(deps),
6795
+ pinSessionName: pinSessionNameVia(deps, claudeSessions)
6709
6796
  };
6710
6797
  }
6711
6798
  function runLimitResumePass(claudeSessions, attemptsBySession, maxContinues, deps) {
@@ -6722,7 +6809,7 @@ function runLimitResumePass(claudeSessions, attemptsBySession, maxContinues, dep
6722
6809
  isClaudeUsageLimitInForce(deps.paths.claudeGlobalConfigFile, deps.nowMs),
6723
6810
  maxContinues
6724
6811
  );
6725
- const executed = executeLimitResumes(planned, attemptsBySession, resumeDepsOf(deps));
6812
+ const executed = executeLimitResumes(planned, attemptsBySession, resumeDepsOf(deps, claudeSessions));
6726
6813
  return { report: { decisions: executed.decisions }, attemptsBySession: executed.attemptsBySession, events: executed.events };
6727
6814
  }
6728
6815
  function reloadTargetAtMs(deps, installs) {
@@ -6787,7 +6874,7 @@ function runTickUnlocked(deps) {
6787
6874
  sessionsById,
6788
6875
  isClaudeUsageLimitInForce(deps.paths.claudeGlobalConfigFile, deps.nowMs)
6789
6876
  );
6790
- const executed = executeWatcherRecoveries(planned, recoveryAttemptsByOwner, resumeDepsOf(deps));
6877
+ const executed = executeWatcherRecoveries(planned, recoveryAttemptsByOwner, resumeDepsOf(deps, claudeSessions));
6791
6878
  recoveries = executed.recoveries;
6792
6879
  recoveryAttemptsByOwner = executed.attemptsByOwner;
6793
6880
  events.push(...executed.events);
@@ -2858,7 +2858,7 @@ var PREFERENCES_SCHEMA = {
2858
2858
  description: "When a prior session left an E2E validation context for this working tree, reuse it instead of re-asking the validation questions"
2859
2859
  },
2860
2860
  ["autoRouteBuildToMuggleDo" /* AutoRouteBuildToMuggleDo */]: {
2861
- description: "When a prompt looks like a build/implement/fix request, route it through the /muggle-do pipeline (requirements \u2192 build \u2192 impact \u2192 tests \u2192 E2E \u2192 PR \u2192 watcher)"
2861
+ description: "When a prompt looks like a build/implement/fix request, route it through the /muggle-do pipeline (requirements \u2192 build \u2192 impact \u2192 tests \u2192 PR \u2192 E2E \u2192 watcher)"
2862
2862
  },
2863
2863
  ["autoWatchPR" /* AutoWatchPR */]: {
2864
2864
  description: "After a test run opens a PR, start a muggle-pr-followup watcher that polls for new reviews and hands them to /muggle-do"
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-HK7XCCNT.js';
3
- import './chunk-7J7DS4SA.js';
2
+ import { runCli } from './chunk-CGKARU5D.js';
3
+ import './chunk-NTEBYUCA.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-HK7XCCNT.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-7J7DS4SA.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-CGKARU5D.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-NTEBYUCA.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.31.0-staging.276",
4
+ "version": "5.31.0-staging.278",
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.31.0-staging.276",
7
+ "@muggleai/works@5.31.0-staging.278",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.31.0-staging.276",
3
+ "version": "5.31.0-staging.278",
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.31.0-staging.276",
5
+ "version": "5.31.0-staging.278",
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.31.0-staging.276",
7
+ "@muggleai/works@5.31.0-staging.278",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -137,6 +137,7 @@ var RECONCILE_REPLAY_TIMEOUT_MS = 15e3;
137
137
  var GUARDRAIL_SUBCOMMAND_ASSIGNMENT = /^GUARDRAIL_SUBCOMMAND="([\w-]+)"/m;
138
138
  var HOOK_COMMAND_SCRIPTS = /^set -- ([^;]+);/;
139
139
  var GUARDRAIL_DISPATCH_SCRIPT = "guardrail-dispatch.sh";
140
+ var MUGGLE_DO_STAGES_AFTER_BUILD = "impact \u2192 unit tests \u2192 PR \u2192 E2E \u2192 watcher";
140
141
 
141
142
  // src/guardrails/store/fileLock.ts
142
143
  function isProcessAlive(pid) {
@@ -3305,7 +3306,8 @@ function watchGate() {
3305
3306
  if (state.watchSkipped !== true && prsAwaitingStateScan(unwatchedPrUrls, state, prStateScan).length > 0) {
3306
3307
  startBackgroundJob("scan-watch-pr-states" /* ScanWatchPrStates */, [sessionId]);
3307
3308
  }
3308
- const decision = watchGateDecision(state, resolveOwedWatchPrs(unwatchedPrUrls, state, prStateScan));
3309
+ const owedWatchPrUrls = resolveOwedWatchPrs(withoutDelegatedPrs(unwatchedPrUrls, state, Date.now()), state, prStateScan);
3310
+ const decision = watchGateDecision(state, owedWatchPrUrls);
3309
3311
  if (decision.action === "release" /* Release */) return releaseGate("watchReleased");
3310
3312
  if (decision.action === "none" /* None */) return "{}";
3311
3313
  recordGateBlock("watchBlockCount", decision.blockCount);
@@ -3359,7 +3361,7 @@ function buildFollowthroughGate() {
3359
3361
  if (decision.action === "release" /* Release */) return releaseGate("buildSkipped");
3360
3362
  if (decision.action === "none" /* None */) return "{}";
3361
3363
  recordGateBlock("buildBlockCount", decision.blockCount);
3362
- const reason = decision.blockCount === 1 ? `Do not end the turn yet. This session took a build/implement/fix request but no PR was opened. A root cause written into the transcript ships nothing \u2014 once the session ends it is gone, and the watcher gate never fires because it only looks at PRs that exist. Carry the work to a PR via ${muggleSkill("/muggle-do")}, which runs requirements \u2192 build \u2192 impact \u2192 unit tests \u2192 E2E \u2192 PR \u2192 watcher. If no PR is owed here (the user changed their mind, the fix landed in another repo, the answer was advice rather than a change), tell the user why and run \`echo "MUGGLE_BUILD_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Build request still unanswered \u2014 no PR opened (reminder ${decision.blockCount}/${MAX_BUILD_BLOCKS}): carry it to a PR via ${muggleSkill("/muggle-do")}, or record a legitimate skip via \`echo "MUGGLE_BUILD_SKIP: <reason>"\`.`;
3364
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. This session took a build/implement/fix request but no PR was opened. A root cause written into the transcript ships nothing \u2014 once the session ends it is gone, and the watcher gate never fires because it only looks at PRs that exist. Carry the work to a PR via ${muggleSkill("/muggle-do")}, which runs requirements \u2192 build \u2192 ${MUGGLE_DO_STAGES_AFTER_BUILD}. If no PR is owed here (the user changed their mind, the fix landed in another repo, the answer was advice rather than a change), tell the user why and run \`echo "MUGGLE_BUILD_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Build request still unanswered \u2014 no PR opened (reminder ${decision.blockCount}/${MAX_BUILD_BLOCKS}): carry it to a PR via ${muggleSkill("/muggle-do")}, or record a legitimate skip via \`echo "MUGGLE_BUILD_SKIP: <reason>"\`.`;
3363
3365
  return blockStop(reason, host);
3364
3366
  }
3365
3367
  function walkthroughGate() {
@@ -3482,7 +3484,7 @@ function buildRouter() {
3482
3484
  if (readState(sessionId).buildIntentRouted) return "{}";
3483
3485
  commitStateMutation(sessionId, { kind: "fields" /* Fields */, fields: { buildIntentRouted: true } });
3484
3486
  if (isMuggleDoInvoked) return "{}";
3485
- const ctx = `This looks like a build/implement/fix request. Per the autoRouteBuildToMuggleDo preference, route it through ${muggleSkill("/muggle-do")} \u2014 which runs requirements \u2192 build (delegated to superpowers' design\u2192plan\u2192review) \u2192 impact \u2192 unit tests \u2192 E2E \u2192 PR \u2192 watcher. If autoRouteBuildToMuggleDo=always, enter that flow; if =ask, offer it; if =never, proceed normally.`;
3487
+ const ctx = `This looks like a build/implement/fix request. Per the autoRouteBuildToMuggleDo preference, route it through ${muggleSkill("/muggle-do")} \u2014 which runs requirements \u2192 build (delegated to superpowers' design\u2192plan\u2192review) \u2192 ${MUGGLE_DO_STAGES_AFTER_BUILD}. If autoRouteBuildToMuggleDo=always, enter that flow; if =ask, offer it; if =never, proceed normally.`;
3486
3488
  return envelope("UserPromptSubmit", ctx, host);
3487
3489
  }
3488
3490
  function closingAssistantText() {
@@ -34,7 +34,7 @@ The agent knows only its brief. Pass every value itself, never a pointer to the
34
34
 
35
35
  - The change's slug and absolute session directory.
36
36
  - Repo, worktree path, branch, base branch, and the head SHA under test — for a wave, one entry per member, from its `Wave members` block.
37
- - The PR URL when the PR already exists (a repair re-run, or a PR opened outside `muggle-do`) — for a wave, every member's. Muggle reads PR URLs from the brief to know which PR's walkthrough is waiting on this run, so leave none out and add none that belong to another change.
37
+ - The change's PR URL — for a wave, every member's. Stage 5.5 opened it before this run; only an `autoCreatePR` skip leaves none. Muggle reads PR URLs from the brief to know which PR's walkthrough is waiting on this run, so leave none out and add none that belong to another change.
38
38
  - `Validation`, `localUrl`, `projectId`, and test-credential status from `state.md`.
39
39
  - The `e2eScope` from impact analysis, verbatim; a Stage 7.5 re-run passes only its affected test cases.
40
40
  - The acceptance criteria from `requirements.md`.
@@ -63,7 +63,7 @@ On the agent's completion notification, the main session:
63
63
 
64
64
  1. **Checks freshness.** Every `Tested:` line must name its branch's current head. A branch that moved since dispatch — any member's, for a wave — makes the report stale: queue the change again and post nothing.
65
65
  2. **Records it** — `E2E dispatch: reported — …` in `state.md`.
66
- 3. **Continues that change from Stage 7**, in its worktree. [`open-prs/forward.md`](open-prs/forward.md) opens its PR with this report — every member's PR for a wave, in the wave order of [`wave-e2e.md`](wave-e2e.md) Stage 7; when a PR already exists, the report settles that PR's walkthrough through Mode A with its number and repo passed explicitly — never from the current branch, since the session's cwd may be another change's worktree.
66
+ 3. **Continues that change from Stage 7**, in its worktree. [`open-prs/forward.md`](open-prs/forward.md) settles the walkthrough of the PR Stage 5.5 opened with this report — every member's PR for a wave, per [`wave-e2e.md`](wave-e2e.md) Stage 7 — through Mode A with its number and repo passed explicitly, never from the current branch, since the session's cwd may be another change's worktree.
67
67
  4. **Runs Stage 7.5** inline. A repair pass's re-run returns to this lane, behind any local run in flight.
68
68
  5. **Arms the change's watcher** once 7.5 clears (Stage 8).
69
69
  6. **Dispatches the next queued change.**
@@ -76,7 +76,7 @@ A session that fixed bugs by hand and opened each PR itself follows the same lan
76
76
 
77
77
  ## Enforcement
78
78
 
79
- - Dispatching a background acceptance-tester holds the E2E and debug-path Stop gates off for a bounded window, and for that window the walkthrough gate sets aside the PRs its brief names — the session is never steered into a second browser run alongside the agent's.
79
+ - Dispatching a background acceptance-tester holds the E2E and debug-path Stop gates off for a bounded window, and for that window the walkthrough and watch gates set aside the PRs its brief names — the session is never steered into a second browser run alongside the agent's, nor into arming a watch on a PR whose run has not come back.
80
80
  - A walkthrough posted to one PR settles that PR only; every other PR the session opened is still checked for its own.
81
81
  - A skip declaration settles only the PRs of the change it is declared in — the working tree's repo and branch: the PRs opened for that branch and its branch's PR, never another change's, wherever the session's directory sat when a PR was opened.
82
82
  - Every PR's walkthrough check stays red until its slot is settled, so a change whose run never came back cannot pass as tested.
@@ -33,7 +33,7 @@ For each affected repo:
33
33
 
34
34
  ## Delegation
35
35
 
36
- For a non-trivial change — multiple files, real design surface, or anything you would otherwise brainstorm before coding — run the implementation through superpowers' design → plan → subagent-driven build, then return to this stage's Output. That is a runtime hand-off (an action), not a doc dependency; do not encode superpowers' internals here. Routing a build request into this pipeline (the `autoRouteBuildToMuggleDo` front-door guardrail) exists to combine superpowers' design rigor with this pipeline's impact analysis, E2E, PR, and watcher — neither delivers both alone.
36
+ For a non-trivial change — multiple files, real design surface, or anything you would otherwise brainstorm before coding — run the implementation through superpowers' design → plan → subagent-driven build, then return to this stage's Output. That is a runtime hand-off (an action), not a doc dependency; do not encode superpowers' internals here. Routing a build request into this pipeline (the `autoRouteBuildToMuggleDo` front-door guardrail) exists to combine superpowers' design rigor with this pipeline's impact analysis, PR, E2E, and watcher — neither delivers both alone.
37
37
 
38
38
  ## Output
39
39
 
@@ -50,6 +50,7 @@ You receive everything from `state.md` already — pre-flight resolved it:
50
50
  - The list of changed repos, files, and a summary of changes
51
51
  - The requirements goal
52
52
  - The `e2eScope` from impact analysis — the impacted, retired, and new use cases and test cases this stage runs
53
+ - In a forward run, the PR Stage 5.5 opened from the working tree — the run's [PR progress](../_shared/dev-loop/run.md#pr-progress) shows on it, and Stage 7 settles its walkthrough
53
54
 
54
55
  ## Your Job
55
56
 
@@ -79,9 +80,9 @@ A scope with deliverable checks and no browser case runs none of these and goes
79
80
  2. **Auth** — `muggle-remote-auth-status` must be `authenticated`; else escalate.
80
81
  3. **Identity tenant/domain match** — if test credentials were marked `existing`, confirm the repo's configured identity tenant/domain matches the recorded tenant/domain. Mismatch → halt.
81
82
 
82
- **Readiness halt.** The cycle planned a browser run and still owes it, so an app that cannot be brought up halts the cycle and is never a `SKIPPED` exit: declare no skip code, write no waiver, run nothing downstream. Report the URL probed, what `muggle-test-prepare` tried (the start command and how it failed), and that no PR was opened or updated; then end the turn and wait for the user. This holds for unattended runs and for a request that asked for the PR. A forward cycle opens no PR, posts no walkthrough, runs no Stage 7.5 and arms no watcher; a watcher cycle pushes nothing and escalates through its own path, which keeps the watcher running. A run that failed because the app never answered (connection refused, page never loaded) returns to item 1, and still down is this halt. A Stop-hook reminder that E2E is still owed does not lift it.
83
+ **Readiness halt.** The cycle planned a browser run and still owes it, so an app that cannot be brought up halts the cycle and is never a `SKIPPED` exit: declare no skip code, write no waiver, run nothing downstream. Report the URL probed, what `muggle-test-prepare` tried (the start command and how it failed), and that the PR's walkthrough is unsettled; then end the turn and wait for the user. This holds for unattended runs and for a request that asked for the PR. A forward cycle leaves its PR open with the walkthrough unsettled, so its walkthrough check stays red: it posts no walkthrough, runs no Stage 7.5 and arms no watcher. Nothing more is owed that PR until the app answers, so run `echo "MUGGLE_WALKTHROUGH_SKIP: readiness halt — <PR URL>"` and `echo "MUGGLE_WATCH_SKIP: readiness halt — <PR URL>"`, which keep the Stop gates from pressing for a walkthrough or watch the halt withholds. A watcher cycle pushes nothing and escalates through its own path, which keeps the watcher running. A run that failed because the app never answered (connection refused, page never loaded) returns to item 1, and still down is this halt. A Stop-hook reminder that E2E is still owed does not lift it.
83
84
 
84
- Only the app not answering is this halt. A run the runner itself lost while readiness passes — an Electron or MCP crash, a Studio abort before the first step — is a failing run, however often it repeats: record it in the Failed block and go on to Stage 7, which opens the PR `[E2E FAILING]` for Stage 7.5 to classify.
85
+ Only the app not answering is this halt. A run the runner itself lost while readiness passes — an Electron or MCP crash, a Studio abort before the first step — is a failing run, however often it repeats: record it in the Failed block and go on to Stage 7, which retitles the PR `[E2E FAILING]` for Stage 7.5 to classify.
85
86
 
86
87
  ### Step 1: Authentication already verified
87
88