@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +4 -3
- package/dist/{chunk-HK7XCCNT.js → chunk-CGKARU5D.js} +99 -12
- package/dist/{chunk-7J7DS4SA.js → chunk-NTEBYUCA.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.codex-mcp.json +1 -1
- package/dist/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/.mcp.json +1 -1
- package/dist/plugin/scripts/guardrails.mjs +5 -3
- package/dist/plugin/skills/do/background-e2e.md +3 -3
- package/dist/plugin/skills/do/build.md +1 -1
- package/dist/plugin/skills/do/e2e-acceptance.md +3 -2
- package/dist/plugin/skills/do/open-prs/forward.md +126 -102
- package/dist/plugin/skills/do/open-prs.md +2 -2
- package/dist/plugin/skills/do/wave-e2e.md +14 -8
- package/dist/plugin/skills/muggle-do/SKILL.md +13 -6
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoRouteBuildToMuggleDo.md +1 -1
- package/dist/release-manifest.json +3 -3
- package/dist/{src-QA7TWQ2M.js → src-LYOBYPFZ.js} +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-mcp.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/scripts/guardrails.mjs +5 -3
- package/plugin/skills/do/background-e2e.md +3 -3
- package/plugin/skills/do/build.md +1 -1
- package/plugin/skills/do/e2e-acceptance.md +3 -2
- package/plugin/skills/do/open-prs/forward.md +126 -102
- package/plugin/skills/do/open-prs.md +2 -2
- package/plugin/skills/do/wave-e2e.md +14 -8
- package/plugin/skills/muggle-do/SKILL.md +13 -6
- 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.
|
|
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
|
-
|
|
|
44
|
-
|
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
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-
|
|
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.
|
|
4
|
+
"version": "5.31.0-staging.278",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.31.0-staging.
|
|
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.
|
|
5
|
+
"version": "5.31.0-staging.278",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/dist/plugin/.mcp.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|