@muggleai/works 5.31.0-staging.277 → 5.31.0-staging.279

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.31.0-staging.277"
12
+ "version": "5.31.0-staging.279"
13
13
  }
14
14
  ]
15
15
  }
@@ -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);
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-FVG7CYB7.js';
2
+ import { runCli } from './chunk-CGKARU5D.js';
3
3
  import './chunk-NTEBYUCA.js';
4
4
 
5
5
  // src/cli/main.ts
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-FVG7CYB7.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-CGKARU5D.js';
2
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.277",
4
+ "version": "5.31.0-staging.279",
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.277",
7
+ "@muggleai/works@5.31.0-staging.279",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.31.0-staging.277",
3
+ "version": "5.31.0-staging.279",
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.277",
5
+ "version": "5.31.0-staging.279",
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.277",
7
+ "@muggleai/works@5.31.0-staging.279",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -446,6 +446,7 @@ var USER_WAIVE_PHRASE = "SKIP E2E";
446
446
  var MIN_NOT_APPLICABLE_REASON_WORDS = 4;
447
447
  var DEV_SERVER_PROBE_TIMEOUT_MS = 2e3;
448
448
  var GIT_PROBE_TIMEOUT_MS = 5e3;
449
+ var BASE_REMOTE = "origin";
449
450
  var WEB_SERVER_COMMAND = /\b(vite|next\s+(dev|start)|react-scripts|ng\s+serve|nuxt|astro\s+dev|remix\s+dev|webpack(-dev)?-server|http-server|serve\s+-s|gatsby\s+develop|vue-cli-service\s+serve)\b/;
450
451
  var BROWSER_FRAMEWORK_PACKAGES = [
451
452
  "react-dom",
@@ -635,6 +636,68 @@ var defaultSkipProbes = {
635
636
  isReachable
636
637
  };
637
638
 
639
+ // src/git-dir/constants.ts
640
+ var DOT_GIT = ".git";
641
+ var GITDIR_POINTER = /^gitdir:\s*(.+?)\s*$/m;
642
+ var HEAD_BRANCH_REF = /^ref:\s*refs\/heads\/(.+?)\s*$/;
643
+ var REMOTE_HEAD_REF = /^ref:\s*refs\/remotes\/(.+?)\s*$/;
644
+ var CONFIG_SECTION_HEADER = /^\s*\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\]/;
645
+ var CONFIG_KEY_VALUE = /^\s*([A-Za-z][A-Za-z0-9-]*)\s*=\s*(.*)$/;
646
+ var CONFIG_INDIRECTION = /^\s*(?:\[\s*include(?:If)?\b|[^#;]*\binsteadOf\s*=)/im;
647
+
648
+ // src/git-dir/gitDir.ts
649
+ function locateGitDir(cwd, readTextFile2) {
650
+ let dir = resolve(cwd);
651
+ for (; ; ) {
652
+ const dotGit = join(dir, DOT_GIT);
653
+ const pointer = readTextFile2(dotGit)?.match(GITDIR_POINTER);
654
+ const gitDir = pointer ? resolve(dir, pointer[1]) : readTextFile2(join(dotGit, "HEAD")) !== null ? dotGit : null;
655
+ if (gitDir) {
656
+ const commonDirPointer = readTextFile2(join(gitDir, "commondir"))?.trim();
657
+ return { gitDir, commonDir: commonDirPointer ? resolve(gitDir, commonDirPointer) : gitDir };
658
+ }
659
+ const parent = dirname(dir);
660
+ if (parent === dir) return null;
661
+ dir = parent;
662
+ }
663
+ }
664
+ function readCurrentBranch(cwd, readTextFile2) {
665
+ const location = locateGitDir(cwd, readTextFile2);
666
+ const head = location ? readTextFile2(join(location.gitDir, "HEAD")) : null;
667
+ if (head === null) return null;
668
+ return head.match(HEAD_BRANCH_REF)?.[1] ?? "";
669
+ }
670
+ function parseConfigValue(rawValue) {
671
+ const value = rawValue.trim();
672
+ if (value.startsWith('"')) {
673
+ const closingQuote = value.slice(1).search(/(?<!\\)"/);
674
+ const quoted = closingQuote < 0 ? value.slice(1) : value.slice(1, closingQuote + 1);
675
+ return quoted.replace(/\\(["\\])/g, "$1");
676
+ }
677
+ return value.replace(/\s*[#;].*$/, "");
678
+ }
679
+ function readRemoteUrl(cwd, remote, readTextFile2) {
680
+ const location = locateGitDir(cwd, readTextFile2);
681
+ const config = location ? readTextFile2(join(location.commonDir, "config")) : null;
682
+ if (config === null || CONFIG_INDIRECTION.test(config)) return null;
683
+ let isInRemoteSection = false;
684
+ for (const line of config.split(/\r?\n/)) {
685
+ const header = line.match(CONFIG_SECTION_HEADER);
686
+ if (header) {
687
+ isInRemoteSection = header[1].toLowerCase() === "remote" && header[2] === remote;
688
+ continue;
689
+ }
690
+ const entry = isInRemoteSection ? line.match(CONFIG_KEY_VALUE) : null;
691
+ if (entry && entry[1].toLowerCase() === "url") return parseConfigValue(entry[2]) || null;
692
+ }
693
+ return null;
694
+ }
695
+ function readRemoteHeadBranch(cwd, remote, readTextFile2) {
696
+ const location = locateGitDir(cwd, readTextFile2);
697
+ const ref = location ? readTextFile2(join(location.commonDir, "refs", "remotes", remote, "HEAD")) : null;
698
+ return ref?.match(REMOTE_HEAD_REF)?.[1] ?? null;
699
+ }
700
+
638
701
  // src/guardrails/changeIdentity.ts
639
702
  function toRepoKey(host2, path) {
640
703
  const repoPath = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
@@ -661,21 +724,24 @@ function repoKeyOfRemoteUrl(remoteUrl) {
661
724
  return void 0;
662
725
  }
663
726
  }
664
- function resolveTreeChange(cwd, runGit2 = defaultSkipProbes.runGit) {
727
+ function resolveTreeChange(cwd, probes = defaultSkipProbes) {
665
728
  if (!cwd) return void 0;
666
- const remoteUrl = runGit2(["remote", "get-url", CHANGE_REPO_REMOTE], cwd);
667
- const repo = remoteUrl ? repoKeyOfRemoteUrl(remoteUrl) : void 0;
668
- const branch = runGit2(["branch", "--show-current"], cwd)?.trim();
669
- return repo && branch ? { repo, branch } : void 0;
729
+ const writtenUrl = readRemoteUrl(cwd, CHANGE_REPO_REMOTE, probes.readTextFile);
730
+ const writtenRepo = writtenUrl ? repoKeyOfRemoteUrl(writtenUrl) : void 0;
731
+ const remoteUrl = writtenRepo ? null : probes.runGit(["remote", "get-url", CHANGE_REPO_REMOTE], cwd);
732
+ const repo = writtenRepo ?? (remoteUrl ? repoKeyOfRemoteUrl(remoteUrl) : void 0);
733
+ if (!repo) return void 0;
734
+ const branch = (readCurrentBranch(cwd, probes.readTextFile) ?? probes.runGit(["branch", "--show-current"], cwd))?.trim();
735
+ return branch ? { repo, branch } : void 0;
670
736
  }
671
- function resolveOpenedPrChange(command, prUrl, cwd, runGit2 = defaultSkipProbes.runGit) {
737
+ function resolveOpenedPrChange(command, prUrl, cwd, probes = defaultSkipProbes) {
672
738
  const repo = repoKeyOfPrUrl(prUrl);
673
739
  if (!repo) return void 0;
674
740
  const openAt = command.search(PR_OPEN_COMMAND);
675
741
  const headFlag = openAt < 0 ? null : command.slice(openAt).match(PR_HEAD_BRANCH_FLAG);
676
742
  if (headFlag) return { repo, branch: headFlag[1].slice(headFlag[1].lastIndexOf(":") + 1) };
677
743
  if (DIRECTORY_CHANGE_COMMAND.test(command)) return void 0;
678
- const treeChange = resolveTreeChange(cwd, runGit2);
744
+ const treeChange = resolveTreeChange(cwd, probes);
679
745
  return treeChange?.repo === repo ? treeChange : void 0;
680
746
  }
681
747
  function isSameChange(left, right) {
@@ -2378,8 +2444,7 @@ var verifyDevServerUnreachable = (context, detail) => {
2378
2444
  };
2379
2445
  var verifyEmptyDiff = (context) => {
2380
2446
  if (!context.cwd) return refuted("no working directory was available to inspect");
2381
- const head = context.probe.runGit(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], context.cwd);
2382
- const base = head?.trim();
2447
+ const base = (readRemoteHeadBranch(context.cwd, BASE_REMOTE, context.probe.readTextFile) ?? context.probe.runGit(["symbolic-ref", `refs/remotes/${BASE_REMOTE}/HEAD`, "--short"], context.cwd))?.trim();
2383
2448
  if (!base) return refuted("the repo's base branch could not be resolved");
2384
2449
  const diff = context.probe.runGit(["diff", `${base}...HEAD`, "--stat"], context.cwd);
2385
2450
  if (diff === null) return refuted("the diff against the base branch could not be read");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "release": "5.26.0",
3
- "buildId": "run-277-1",
4
- "commitSha": "fea69b3dd04919c685e56559e6ceba0b6d57129e",
5
- "buildTime": "2026-11-01T19:01:29Z",
3
+ "buildId": "run-279-1",
4
+ "commitSha": "999126987b03bfc3a3b2f8fbc337415e695fbbab",
5
+ "buildTime": "2026-11-02T08:18:32Z",
6
6
  "serviceName": "muggle-ai-works-mcp"
7
7
  }
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.31.0-staging.277",
4
+ "version": "5.31.0-staging.279",
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.31.0-staging.277",
4
+ "version": "5.31.0-staging.279",
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.277",
7
+ "@muggleai/works@5.31.0-staging.279",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.31.0-staging.277",
3
+ "version": "5.31.0-staging.279",
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.277",
5
+ "version": "5.31.0-staging.279",
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.31.0-staging.277",
7
+ "@muggleai/works@5.31.0-staging.279",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -446,6 +446,7 @@ var USER_WAIVE_PHRASE = "SKIP E2E";
446
446
  var MIN_NOT_APPLICABLE_REASON_WORDS = 4;
447
447
  var DEV_SERVER_PROBE_TIMEOUT_MS = 2e3;
448
448
  var GIT_PROBE_TIMEOUT_MS = 5e3;
449
+ var BASE_REMOTE = "origin";
449
450
  var WEB_SERVER_COMMAND = /\b(vite|next\s+(dev|start)|react-scripts|ng\s+serve|nuxt|astro\s+dev|remix\s+dev|webpack(-dev)?-server|http-server|serve\s+-s|gatsby\s+develop|vue-cli-service\s+serve)\b/;
450
451
  var BROWSER_FRAMEWORK_PACKAGES = [
451
452
  "react-dom",
@@ -635,6 +636,68 @@ var defaultSkipProbes = {
635
636
  isReachable
636
637
  };
637
638
 
639
+ // src/git-dir/constants.ts
640
+ var DOT_GIT = ".git";
641
+ var GITDIR_POINTER = /^gitdir:\s*(.+?)\s*$/m;
642
+ var HEAD_BRANCH_REF = /^ref:\s*refs\/heads\/(.+?)\s*$/;
643
+ var REMOTE_HEAD_REF = /^ref:\s*refs\/remotes\/(.+?)\s*$/;
644
+ var CONFIG_SECTION_HEADER = /^\s*\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\]/;
645
+ var CONFIG_KEY_VALUE = /^\s*([A-Za-z][A-Za-z0-9-]*)\s*=\s*(.*)$/;
646
+ var CONFIG_INDIRECTION = /^\s*(?:\[\s*include(?:If)?\b|[^#;]*\binsteadOf\s*=)/im;
647
+
648
+ // src/git-dir/gitDir.ts
649
+ function locateGitDir(cwd, readTextFile2) {
650
+ let dir = resolve(cwd);
651
+ for (; ; ) {
652
+ const dotGit = join(dir, DOT_GIT);
653
+ const pointer = readTextFile2(dotGit)?.match(GITDIR_POINTER);
654
+ const gitDir = pointer ? resolve(dir, pointer[1]) : readTextFile2(join(dotGit, "HEAD")) !== null ? dotGit : null;
655
+ if (gitDir) {
656
+ const commonDirPointer = readTextFile2(join(gitDir, "commondir"))?.trim();
657
+ return { gitDir, commonDir: commonDirPointer ? resolve(gitDir, commonDirPointer) : gitDir };
658
+ }
659
+ const parent = dirname(dir);
660
+ if (parent === dir) return null;
661
+ dir = parent;
662
+ }
663
+ }
664
+ function readCurrentBranch(cwd, readTextFile2) {
665
+ const location = locateGitDir(cwd, readTextFile2);
666
+ const head = location ? readTextFile2(join(location.gitDir, "HEAD")) : null;
667
+ if (head === null) return null;
668
+ return head.match(HEAD_BRANCH_REF)?.[1] ?? "";
669
+ }
670
+ function parseConfigValue(rawValue) {
671
+ const value = rawValue.trim();
672
+ if (value.startsWith('"')) {
673
+ const closingQuote = value.slice(1).search(/(?<!\\)"/);
674
+ const quoted = closingQuote < 0 ? value.slice(1) : value.slice(1, closingQuote + 1);
675
+ return quoted.replace(/\\(["\\])/g, "$1");
676
+ }
677
+ return value.replace(/\s*[#;].*$/, "");
678
+ }
679
+ function readRemoteUrl(cwd, remote, readTextFile2) {
680
+ const location = locateGitDir(cwd, readTextFile2);
681
+ const config = location ? readTextFile2(join(location.commonDir, "config")) : null;
682
+ if (config === null || CONFIG_INDIRECTION.test(config)) return null;
683
+ let isInRemoteSection = false;
684
+ for (const line of config.split(/\r?\n/)) {
685
+ const header = line.match(CONFIG_SECTION_HEADER);
686
+ if (header) {
687
+ isInRemoteSection = header[1].toLowerCase() === "remote" && header[2] === remote;
688
+ continue;
689
+ }
690
+ const entry = isInRemoteSection ? line.match(CONFIG_KEY_VALUE) : null;
691
+ if (entry && entry[1].toLowerCase() === "url") return parseConfigValue(entry[2]) || null;
692
+ }
693
+ return null;
694
+ }
695
+ function readRemoteHeadBranch(cwd, remote, readTextFile2) {
696
+ const location = locateGitDir(cwd, readTextFile2);
697
+ const ref = location ? readTextFile2(join(location.commonDir, "refs", "remotes", remote, "HEAD")) : null;
698
+ return ref?.match(REMOTE_HEAD_REF)?.[1] ?? null;
699
+ }
700
+
638
701
  // src/guardrails/changeIdentity.ts
639
702
  function toRepoKey(host2, path) {
640
703
  const repoPath = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
@@ -661,21 +724,24 @@ function repoKeyOfRemoteUrl(remoteUrl) {
661
724
  return void 0;
662
725
  }
663
726
  }
664
- function resolveTreeChange(cwd, runGit2 = defaultSkipProbes.runGit) {
727
+ function resolveTreeChange(cwd, probes = defaultSkipProbes) {
665
728
  if (!cwd) return void 0;
666
- const remoteUrl = runGit2(["remote", "get-url", CHANGE_REPO_REMOTE], cwd);
667
- const repo = remoteUrl ? repoKeyOfRemoteUrl(remoteUrl) : void 0;
668
- const branch = runGit2(["branch", "--show-current"], cwd)?.trim();
669
- return repo && branch ? { repo, branch } : void 0;
729
+ const writtenUrl = readRemoteUrl(cwd, CHANGE_REPO_REMOTE, probes.readTextFile);
730
+ const writtenRepo = writtenUrl ? repoKeyOfRemoteUrl(writtenUrl) : void 0;
731
+ const remoteUrl = writtenRepo ? null : probes.runGit(["remote", "get-url", CHANGE_REPO_REMOTE], cwd);
732
+ const repo = writtenRepo ?? (remoteUrl ? repoKeyOfRemoteUrl(remoteUrl) : void 0);
733
+ if (!repo) return void 0;
734
+ const branch = (readCurrentBranch(cwd, probes.readTextFile) ?? probes.runGit(["branch", "--show-current"], cwd))?.trim();
735
+ return branch ? { repo, branch } : void 0;
670
736
  }
671
- function resolveOpenedPrChange(command, prUrl, cwd, runGit2 = defaultSkipProbes.runGit) {
737
+ function resolveOpenedPrChange(command, prUrl, cwd, probes = defaultSkipProbes) {
672
738
  const repo = repoKeyOfPrUrl(prUrl);
673
739
  if (!repo) return void 0;
674
740
  const openAt = command.search(PR_OPEN_COMMAND);
675
741
  const headFlag = openAt < 0 ? null : command.slice(openAt).match(PR_HEAD_BRANCH_FLAG);
676
742
  if (headFlag) return { repo, branch: headFlag[1].slice(headFlag[1].lastIndexOf(":") + 1) };
677
743
  if (DIRECTORY_CHANGE_COMMAND.test(command)) return void 0;
678
- const treeChange = resolveTreeChange(cwd, runGit2);
744
+ const treeChange = resolveTreeChange(cwd, probes);
679
745
  return treeChange?.repo === repo ? treeChange : void 0;
680
746
  }
681
747
  function isSameChange(left, right) {
@@ -2378,8 +2444,7 @@ var verifyDevServerUnreachable = (context, detail) => {
2378
2444
  };
2379
2445
  var verifyEmptyDiff = (context) => {
2380
2446
  if (!context.cwd) return refuted("no working directory was available to inspect");
2381
- const head = context.probe.runGit(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], context.cwd);
2382
- const base = head?.trim();
2447
+ const base = (readRemoteHeadBranch(context.cwd, BASE_REMOTE, context.probe.readTextFile) ?? context.probe.runGit(["symbolic-ref", `refs/remotes/${BASE_REMOTE}/HEAD`, "--short"], context.cwd))?.trim();
2383
2448
  if (!base) return refuted("the repo's base branch could not be resolved");
2384
2449
  const diff = context.probe.runGit(["diff", `${base}...HEAD`, "--stat"], context.cwd);
2385
2450
  if (diff === null) return refuted("the diff against the base branch could not be read");