@muggleai/works 5.31.0-staging.274 → 5.31.0-staging.276
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/dist/{chunk-7JQWRPFI.js → chunk-HK7XCCNT.js} +260 -52
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- 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/skills/do/input-routing.md +1 -1
- package/dist/plugin/skills/do/unit-tests.md +1 -1
- package/dist/release-manifest.json +3 -3
- 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/skills/do/input-routing.md +1 -1
- package/plugin/skills/do/unit-tests.md +1 -1
- package/scripts/muggle-watch/README.md +2 -0
- package/scripts/muggle-watch/install-muggle-watch.ps1 +4 -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.276"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -5,7 +5,7 @@ import { v4 } from 'uuid';
|
|
|
5
5
|
import { z, ZodError } from 'zod';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
|
-
import { readFileSync, existsSync, rmSync, mkdirSync, readdirSync, createWriteStream, writeFileSync, statSync, readlinkSync, realpathSync, openSync, closeSync, writeSync, unlinkSync, copyFileSync, renameSync, appendFileSync, readSync } from 'fs';
|
|
8
|
+
import { readFileSync, existsSync, rmSync, mkdirSync, readdirSync, createWriteStream, writeFileSync, statSync, readlinkSync, realpathSync, openSync, closeSync, writeSync, unlinkSync, copyFileSync, renameSync, appendFileSync, readSync, fstatSync } from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { join, dirname, resolve, win32, basename } from 'path';
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -2945,6 +2945,12 @@ var REMOTE_CONTROL_MAX_RESTARTS = 2;
|
|
|
2945
2945
|
var REMOTE_CONTROL_RESTART_WINDOW_MS = 24 * 60 * 6e4;
|
|
2946
2946
|
var REMOTE_CONTROL_RESTART_COOLDOWN_MS = 30 * 6e4;
|
|
2947
2947
|
var REMOTE_CONTROL_NAME_COLUMN_WIDTH = 32;
|
|
2948
|
+
var USAGE_LIMIT_API_ERROR = "usage_limit_reached";
|
|
2949
|
+
var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
2950
|
+
var LIMIT_RESUME_PROMPT = "The usage limit that stopped your last turn has reset. Continue where you left off.";
|
|
2951
|
+
var LIMIT_RESUME_MAX_ATTEMPTS = 2;
|
|
2952
|
+
var LIMIT_RESUME_WINDOW_MS = 24 * 60 * 6e4;
|
|
2953
|
+
var LIMIT_STOP_MAX_AGE_MS = 8 * 24 * 60 * 6e4;
|
|
2948
2954
|
var USAGE_WINDOW_EXHAUSTED_PERCENT = 100;
|
|
2949
2955
|
var RELOAD_PLUGIN_SCOPE = "user";
|
|
2950
2956
|
var MUGGLE_PLUGIN_ID = "muggleai@muggle-works";
|
|
@@ -4927,6 +4933,22 @@ function renderWatchStatus(snapshot, nowMs) {
|
|
|
4927
4933
|
lines.push(` ${decision.action.padEnd(14)} ${shortSessionId(decision.sessionId)} ${label} ${decision.reason}${outcome}`);
|
|
4928
4934
|
}
|
|
4929
4935
|
}
|
|
4936
|
+
const limitDecisions = snapshot.limitResumes?.decisions ?? [];
|
|
4937
|
+
if (limitDecisions.length > 0) {
|
|
4938
|
+
const countOutcome = (outcome) => limitDecisions.filter((decision) => decision.outcome === outcome).length;
|
|
4939
|
+
const countAction = (action) => limitDecisions.filter((decision) => decision.action === action).length;
|
|
4940
|
+
const continueCount = countAction("continue" /* Continue */);
|
|
4941
|
+
const continuedCount = countOutcome("resumed" /* Resumed */);
|
|
4942
|
+
lines.push(
|
|
4943
|
+
"",
|
|
4944
|
+
`Stopped at a usage limit: ${continuedCount} continued, ${continueCount - continuedCount} failed, ${countAction("wait" /* Wait */)} waiting, ${countAction("skip" /* Skip */)} skipped`
|
|
4945
|
+
);
|
|
4946
|
+
for (const decision of limitDecisions) {
|
|
4947
|
+
const outcome = decision.outcome === void 0 || decision.outcome === "resumed" /* Resumed */ ? "" : ` \u2014 ${decision.outcome}`;
|
|
4948
|
+
const label = (decision.name ?? "").slice(0, REMOTE_CONTROL_NAME_COLUMN_WIDTH).padEnd(REMOTE_CONTROL_NAME_COLUMN_WIDTH);
|
|
4949
|
+
lines.push(` ${decision.action.padEnd(14)} ${shortSessionId(decision.sessionId)} ${label} ${decision.reason}${outcome}`);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4930
4952
|
return `${lines.join("\n")}
|
|
4931
4953
|
`;
|
|
4932
4954
|
}
|
|
@@ -4949,6 +4971,22 @@ function readFileHead(filePath) {
|
|
|
4949
4971
|
if (fileDescriptor !== void 0) closeSync(fileDescriptor);
|
|
4950
4972
|
}
|
|
4951
4973
|
}
|
|
4974
|
+
function readFileTailLines(filePath, byteCount) {
|
|
4975
|
+
let fileDescriptor;
|
|
4976
|
+
try {
|
|
4977
|
+
fileDescriptor = openSync(filePath, "r");
|
|
4978
|
+
const fileSize = fstatSync(fileDescriptor).size;
|
|
4979
|
+
const start = Math.max(0, fileSize - byteCount);
|
|
4980
|
+
const buffer = Buffer.alloc(fileSize - start);
|
|
4981
|
+
const bytesRead = readSync(fileDescriptor, buffer, 0, buffer.length, start);
|
|
4982
|
+
const lines = buffer.subarray(0, bytesRead).toString("utf-8").split(/\r?\n/);
|
|
4983
|
+
return (start > 0 ? lines.slice(1) : lines).filter((line) => line.length > 0);
|
|
4984
|
+
} catch {
|
|
4985
|
+
return void 0;
|
|
4986
|
+
} finally {
|
|
4987
|
+
if (fileDescriptor !== void 0) closeSync(fileDescriptor);
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4952
4990
|
function cwdFromSessionHead(head) {
|
|
4953
4991
|
const parts = head.match(/"cwd"\s*:\s*("(?:[^"\\]|\\.)*")/);
|
|
4954
4992
|
if (!parts) return void 0;
|
|
@@ -5129,6 +5167,56 @@ function readCodexSessions(sessionsDir, nowMs) {
|
|
|
5129
5167
|
}
|
|
5130
5168
|
return sessions;
|
|
5131
5169
|
}
|
|
5170
|
+
function findTranscriptPath(projectsDir, sessionId) {
|
|
5171
|
+
let projectDirNames;
|
|
5172
|
+
try {
|
|
5173
|
+
projectDirNames = readdirSync(projectsDir);
|
|
5174
|
+
} catch {
|
|
5175
|
+
return void 0;
|
|
5176
|
+
}
|
|
5177
|
+
const transcriptFileName = `${sessionId}.jsonl`;
|
|
5178
|
+
for (const projectDirName of projectDirNames) {
|
|
5179
|
+
const transcriptPath = join(projectsDir, projectDirName, transcriptFileName);
|
|
5180
|
+
if (existsSync(transcriptPath)) return transcriptPath;
|
|
5181
|
+
}
|
|
5182
|
+
return void 0;
|
|
5183
|
+
}
|
|
5184
|
+
function parseTranscriptLine(line) {
|
|
5185
|
+
try {
|
|
5186
|
+
const parsed = JSON.parse(line);
|
|
5187
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
5188
|
+
} catch {
|
|
5189
|
+
return void 0;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
function isConversationTurn(entry) {
|
|
5193
|
+
return (entry.type === "user" || entry.type === "assistant") && entry.isSidechain !== true && entry.isMeta !== true;
|
|
5194
|
+
}
|
|
5195
|
+
function readLimitStop(projectsDir, sessionId) {
|
|
5196
|
+
const transcriptPath = findTranscriptPath(projectsDir, sessionId);
|
|
5197
|
+
if (transcriptPath === void 0) return void 0;
|
|
5198
|
+
const lines = readFileTailLines(transcriptPath, TRANSCRIPT_TAIL_BYTES) ?? [];
|
|
5199
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
5200
|
+
const entry = parseTranscriptLine(lines[index]);
|
|
5201
|
+
if (entry === void 0 || !isConversationTurn(entry)) continue;
|
|
5202
|
+
if (entry.type !== "assistant" || entry.apiError !== USAGE_LIMIT_API_ERROR) return void 0;
|
|
5203
|
+
const noticedAtMs = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
|
|
5204
|
+
if (typeof entry.uuid !== "string" || Number.isNaN(noticedAtMs)) return void 0;
|
|
5205
|
+
const resetsAtSeconds = entry.apiErrorParams?.rate_limit_info?.resetsAt;
|
|
5206
|
+
return {
|
|
5207
|
+
noticeUuid: entry.uuid,
|
|
5208
|
+
noticedAtMs,
|
|
5209
|
+
...typeof resetsAtSeconds === "number" ? { resetsAtMs: resetsAtSeconds * 1e3 } : {}
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
return void 0;
|
|
5213
|
+
}
|
|
5214
|
+
function findClaudeSessionCwd(projectsDir, sessionId) {
|
|
5215
|
+
const transcriptPath = findTranscriptPath(projectsDir, sessionId);
|
|
5216
|
+
if (transcriptPath === void 0) return void 0;
|
|
5217
|
+
const head = readFileHead(transcriptPath);
|
|
5218
|
+
return head === void 0 ? void 0 : cwdFromSessionHead(head);
|
|
5219
|
+
}
|
|
5132
5220
|
|
|
5133
5221
|
// src/pr-watch/constants.ts
|
|
5134
5222
|
var REBASE_KEY_SEPARATOR = "..";
|
|
@@ -5282,26 +5370,6 @@ function fetchPrLiveStatuses(prs, runCommand) {
|
|
|
5282
5370
|
}
|
|
5283
5371
|
return statusesByUrl;
|
|
5284
5372
|
}
|
|
5285
|
-
function findTranscriptPath(projectsDir, sessionId) {
|
|
5286
|
-
let projectDirNames;
|
|
5287
|
-
try {
|
|
5288
|
-
projectDirNames = readdirSync(projectsDir);
|
|
5289
|
-
} catch {
|
|
5290
|
-
return void 0;
|
|
5291
|
-
}
|
|
5292
|
-
const transcriptFileName = `${sessionId}.jsonl`;
|
|
5293
|
-
for (const projectDirName of projectDirNames) {
|
|
5294
|
-
const transcriptPath = join(projectsDir, projectDirName, transcriptFileName);
|
|
5295
|
-
if (existsSync(transcriptPath)) return transcriptPath;
|
|
5296
|
-
}
|
|
5297
|
-
return void 0;
|
|
5298
|
-
}
|
|
5299
|
-
function findClaudeSessionCwd(projectsDir, sessionId) {
|
|
5300
|
-
const transcriptPath = findTranscriptPath(projectsDir, sessionId);
|
|
5301
|
-
if (transcriptPath === void 0) return void 0;
|
|
5302
|
-
const head = readFileHead(transcriptPath);
|
|
5303
|
-
return head === void 0 ? void 0 : cwdFromSessionHead(head);
|
|
5304
|
-
}
|
|
5305
5373
|
function parseServiceLoopReport(outputText, writtenAtMs) {
|
|
5306
5374
|
const reportLines = [];
|
|
5307
5375
|
let kind = "silent" /* Silent */;
|
|
@@ -5735,6 +5803,106 @@ function readInstallHistory(installLogFile) {
|
|
|
5735
5803
|
}
|
|
5736
5804
|
return installs;
|
|
5737
5805
|
}
|
|
5806
|
+
function recentAttempts(attempts, nowMs) {
|
|
5807
|
+
return attempts.filter((attempt) => nowMs - Date.parse(attempt.at) < LIMIT_RESUME_WINDOW_MS);
|
|
5808
|
+
}
|
|
5809
|
+
function decide(session, stop, action, reason) {
|
|
5810
|
+
return {
|
|
5811
|
+
sessionId: session.sessionId,
|
|
5812
|
+
...session.shortId === void 0 ? {} : { shortId: session.shortId },
|
|
5813
|
+
...session.name === void 0 ? {} : { name: session.name },
|
|
5814
|
+
action,
|
|
5815
|
+
reason,
|
|
5816
|
+
noticeUuid: stop.noticeUuid
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5819
|
+
function planLimitResumes(sessions, limitStopsBySessionId, attemptsBySession, nowMs, isUsageLimitInForce, maxContinues) {
|
|
5820
|
+
const decisions = [];
|
|
5821
|
+
let continueCount = 0;
|
|
5822
|
+
for (const session of sessions) {
|
|
5823
|
+
const stop = limitStopsBySessionId.get(session.sessionId);
|
|
5824
|
+
if (session.agent !== "claude" /* Claude */ || stop === void 0) continue;
|
|
5825
|
+
const attempts = attemptsBySession[session.sessionId] ?? [];
|
|
5826
|
+
if (nowMs - stop.noticedAtMs > LIMIT_STOP_MAX_AGE_MS) continue;
|
|
5827
|
+
if (attempts.some((attempt) => attempt.noticeUuid === stop.noticeUuid && attempt.isContinued)) continue;
|
|
5828
|
+
const isRunning = hasLiveProcess(session);
|
|
5829
|
+
if (session.kind !== "background" /* Background */) {
|
|
5830
|
+
decisions.push(decide(session, stop, "skip" /* Skip */, "terminal session, continue it there"));
|
|
5831
|
+
} else if (session.shortId === void 0) {
|
|
5832
|
+
decisions.push(decide(session, stop, "skip" /* Skip */, "no short id to continue it by"));
|
|
5833
|
+
} else if (isRunning && session.turnStatus !== "idle" /* Idle */) {
|
|
5834
|
+
decisions.push(decide(session, stop, "skip" /* Skip */, `turn in flight (${session.turnStatus ?? "unknown"})`));
|
|
5835
|
+
} else if (stop.resetsAtMs !== void 0 && stop.resetsAtMs > nowMs) {
|
|
5836
|
+
decisions.push(decide(session, stop, "wait" /* Wait */, `limit resets at ${new Date(stop.resetsAtMs).toISOString()}`));
|
|
5837
|
+
} else if (isUsageLimitInForce) {
|
|
5838
|
+
decisions.push(decide(session, stop, "wait" /* Wait */, "the account is still at a usage limit"));
|
|
5839
|
+
} else if (recentAttempts(attempts, nowMs).length >= LIMIT_RESUME_MAX_ATTEMPTS) {
|
|
5840
|
+
decisions.push(decide(session, stop, "skip" /* Skip */, `after ${LIMIT_RESUME_MAX_ATTEMPTS} tries today`));
|
|
5841
|
+
} else if (continueCount >= maxContinues) {
|
|
5842
|
+
decisions.push(decide(session, stop, "wait" /* Wait */, "tick cap reached"));
|
|
5843
|
+
} else {
|
|
5844
|
+
continueCount++;
|
|
5845
|
+
const reason = stop.resetsAtMs === void 0 ? "usage limit cleared" : `limit reset at ${new Date(stop.resetsAtMs).toISOString()}`;
|
|
5846
|
+
decisions.push({ ...decide(session, stop, "continue" /* Continue */, reason), ...isRunning ? { runningShortId: session.shortId } : {} });
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
return decisions;
|
|
5850
|
+
}
|
|
5851
|
+
function continuedOutcomeOf(outcome) {
|
|
5852
|
+
if (outcome === "continued" /* Continued */) return "resumed" /* Resumed */;
|
|
5853
|
+
return outcome === "copied" /* Copied */ ? "resume-forked" /* ResumeForked */ : "resume-failed" /* ResumeFailed */;
|
|
5854
|
+
}
|
|
5855
|
+
function eventKindOf(outcome) {
|
|
5856
|
+
if (outcome === "resumed" /* Resumed */) return "resumed" /* Resumed */;
|
|
5857
|
+
if (outcome === "resume-forked" /* ResumeForked */) return "resume-forked" /* ResumeForked */;
|
|
5858
|
+
return outcome === "restart-refused" /* RestartRefused */ ? "restart-refused" /* RestartRefused */ : "resume-failed" /* ResumeFailed */;
|
|
5859
|
+
}
|
|
5860
|
+
function continueMessageOf(resumed, reason) {
|
|
5861
|
+
if (resumed.outcome === "continued" /* Continued */) return `continued it after its usage limit reset (${reason})`;
|
|
5862
|
+
if (resumed.outcome === "copied" /* Copied */) {
|
|
5863
|
+
const copyNote = resumed.isCopyRemoved ? "stopped and removed the copy" : `run claude rm ${resumed.copyShortId}`;
|
|
5864
|
+
return `could not continue it: Claude started copy ${resumed.copyShortId} instead; ${copyNote}`;
|
|
5865
|
+
}
|
|
5866
|
+
const cause = resumed.outcome === "still-running" /* StillRunning */ ? "it is still running" : "the resume did not start";
|
|
5867
|
+
return `could not continue it after its usage limit: ${cause}`;
|
|
5868
|
+
}
|
|
5869
|
+
function executeLimitResumes(planned, attemptsBySession, deps) {
|
|
5870
|
+
const at = new Date(deps.nowMs).toISOString();
|
|
5871
|
+
const nextAttemptsBySession = {};
|
|
5872
|
+
for (const [sessionId, attempts] of Object.entries(attemptsBySession)) {
|
|
5873
|
+
const kept = attempts.filter((attempt) => deps.nowMs - Date.parse(attempt.at) < LIMIT_STOP_MAX_AGE_MS);
|
|
5874
|
+
if (kept.length > 0) nextAttemptsBySession[sessionId] = kept;
|
|
5875
|
+
}
|
|
5876
|
+
const events = [];
|
|
5877
|
+
const decisions = planned.map((decision) => {
|
|
5878
|
+
if (decision.action !== "continue" /* Continue */) return decision;
|
|
5879
|
+
const subject = `session ${decision.shortId ?? shortSessionId(decision.sessionId)}`;
|
|
5880
|
+
const cwd = findClaudeSessionCwd(deps.claudeProjectsDir, decision.sessionId);
|
|
5881
|
+
if (!cwd || !existsSync(cwd)) return { ...decision, outcome: "transcript-missing" /* TranscriptMissing */ };
|
|
5882
|
+
let outcome;
|
|
5883
|
+
let message;
|
|
5884
|
+
if (decision.runningShortId !== void 0 && !deps.canRestartIn(cwd)) {
|
|
5885
|
+
outcome = "restart-refused" /* RestartRefused */;
|
|
5886
|
+
message = `left stopped at its usage limit: Claude would not resume it in ${cwd}`;
|
|
5887
|
+
} else {
|
|
5888
|
+
const resumed = resumeSession(
|
|
5889
|
+
{ sessionId: decision.sessionId, cwd, prompt: LIMIT_RESUME_PROMPT, runningShortId: decision.runningShortId },
|
|
5890
|
+
deps.runCommand,
|
|
5891
|
+
deps.waitForExit,
|
|
5892
|
+
deps.isSessionRunning
|
|
5893
|
+
);
|
|
5894
|
+
outcome = continuedOutcomeOf(resumed.outcome);
|
|
5895
|
+
message = continueMessageOf(resumed, decision.reason);
|
|
5896
|
+
}
|
|
5897
|
+
nextAttemptsBySession[decision.sessionId] = [
|
|
5898
|
+
...nextAttemptsBySession[decision.sessionId] ?? [],
|
|
5899
|
+
{ noticeUuid: decision.noticeUuid, at, isContinued: outcome === "resumed" /* Resumed */ }
|
|
5900
|
+
];
|
|
5901
|
+
events.push({ at, kind: eventKindOf(outcome), subject, message });
|
|
5902
|
+
return { ...decision, outcome };
|
|
5903
|
+
});
|
|
5904
|
+
return { decisions, attemptsBySession: nextAttemptsBySession, events };
|
|
5905
|
+
}
|
|
5738
5906
|
function emptyLedgerState() {
|
|
5739
5907
|
return { activeFindings: {}, recoveryAttemptsByOwner: {}, terminalPrsByUrl: {} };
|
|
5740
5908
|
}
|
|
@@ -5949,7 +6117,7 @@ function readProcessTable(platform4, runCommand, nowMs) {
|
|
|
5949
6117
|
}
|
|
5950
6118
|
|
|
5951
6119
|
// src/watch/reload-plugins.ts
|
|
5952
|
-
function
|
|
6120
|
+
function decide2(session, action, reason) {
|
|
5953
6121
|
return {
|
|
5954
6122
|
sessionId: session.sessionId,
|
|
5955
6123
|
shortId: session.shortId,
|
|
@@ -5969,24 +6137,24 @@ function planReloadFanOut(sessions, pending, watchOwnerSessionIds, maxRestarts =
|
|
|
5969
6137
|
for (const session of sessions) {
|
|
5970
6138
|
if (session.agent !== "claude" /* Claude */) continue;
|
|
5971
6139
|
if (!hasLiveProcess(session)) {
|
|
5972
|
-
decisions.push(
|
|
6140
|
+
decisions.push(decide2(session, "skip" /* Skip */, "no live process"));
|
|
5973
6141
|
} else if (hasCurrentPlugins(session, pending)) {
|
|
5974
|
-
decisions.push(
|
|
6142
|
+
decisions.push(decide2(session, "skip" /* Skip */, "already on the installed build"));
|
|
5975
6143
|
} else if (pending.respawnedSessionIds.includes(session.sessionId) || pending.skippedSessionIds.includes(session.sessionId)) {
|
|
5976
|
-
decisions.push(
|
|
6144
|
+
decisions.push(decide2(session, "skip" /* Skip */, "already handled for this install"));
|
|
5977
6145
|
} else if (session.kind === "interactive" /* Interactive */) {
|
|
5978
|
-
decisions.push(pending.notifiedSessionIds.includes(session.sessionId) ?
|
|
6146
|
+
decisions.push(pending.notifiedSessionIds.includes(session.sessionId) ? decide2(session, "skip" /* Skip */, "already asked to run /reload-plugins") : decide2(session, "notify-manual" /* NotifyManual */, "terminal session \u2014 run /reload-plugins in it"));
|
|
5979
6147
|
} else if (session.kind !== "background" /* Background */ || !session.shortId) {
|
|
5980
|
-
decisions.push(
|
|
6148
|
+
decisions.push(decide2(session, "skip" /* Skip */, "not a background session the watch can restart"));
|
|
5981
6149
|
} else if (session.turnStatus !== "idle" /* Idle */) {
|
|
5982
|
-
decisions.push(
|
|
6150
|
+
decisions.push(decide2(session, "defer" /* Defer */, `turn in flight (${session.turnStatus ?? "unknown"})`));
|
|
5983
6151
|
} else if (watchOwnerSessionIds.has(session.sessionId)) {
|
|
5984
|
-
decisions.push(
|
|
6152
|
+
decisions.push(decide2(session, "skip" /* Skip */, "owns open PR watches \u2014 left running; run /reload-plugins in it"));
|
|
5985
6153
|
} else if (restartsPlanned >= maxRestarts) {
|
|
5986
|
-
decisions.push(
|
|
6154
|
+
decisions.push(decide2(session, "defer" /* Defer */, "restart cap reached this pass"));
|
|
5987
6155
|
} else {
|
|
5988
6156
|
restartsPlanned++;
|
|
5989
|
-
decisions.push(
|
|
6157
|
+
decisions.push(decide2(session, "respawn" /* Respawn */, "idle background session"));
|
|
5990
6158
|
}
|
|
5991
6159
|
}
|
|
5992
6160
|
return decisions;
|
|
@@ -6041,7 +6209,7 @@ function isTurnOver(session) {
|
|
|
6041
6209
|
function recentRestartsMs(restartsBySession, sessionId, nowMs) {
|
|
6042
6210
|
return (restartsBySession[sessionId] ?? []).map((restartedAt) => Date.parse(restartedAt)).filter((restartedAtMs) => !Number.isNaN(restartedAtMs) && nowMs - restartedAtMs < REMOTE_CONTROL_RESTART_WINDOW_MS);
|
|
6043
6211
|
}
|
|
6044
|
-
function
|
|
6212
|
+
function decide3(session, action, reason) {
|
|
6045
6213
|
return { sessionId: session.sessionId, shortId: session.shortId, name: session.name, cwd: session.cwd, action, reason };
|
|
6046
6214
|
}
|
|
6047
6215
|
function planRemoteControlRestarts(sessions, login, restartsBySession, nowMs, maxRestarts) {
|
|
@@ -6057,20 +6225,20 @@ function planRemoteControlRestarts(sessions, login, restartsBySession, nowMs, ma
|
|
|
6057
6225
|
}
|
|
6058
6226
|
const restartsMs = recentRestartsMs(restartsBySession, session.sessionId, nowMs);
|
|
6059
6227
|
if (session.kind === "interactive" /* Interactive */) {
|
|
6060
|
-
decisions.push(
|
|
6228
|
+
decisions.push(decide3(session, "notify-manual" /* NotifyManual */, `${gap} \u2014 terminal session, run /remote-control in it`));
|
|
6061
6229
|
} else if (session.kind !== "background" /* Background */ || !session.shortId) {
|
|
6062
|
-
decisions.push(
|
|
6230
|
+
decisions.push(decide3(session, "skip" /* Skip */, `${gap} \u2014 not a background session the watch can restart`));
|
|
6063
6231
|
} else if (restartsMs.length >= REMOTE_CONTROL_MAX_RESTARTS) {
|
|
6064
|
-
decisions.push(
|
|
6232
|
+
decisions.push(decide3(session, "skip" /* Skip */, `${gap} after ${restartsMs.length} restarts today`));
|
|
6065
6233
|
} else if (nowMs - Math.max(-Infinity, ...restartsMs) < REMOTE_CONTROL_RESTART_COOLDOWN_MS) {
|
|
6066
|
-
decisions.push(
|
|
6234
|
+
decisions.push(decide3(session, "skip" /* Skip */, `${gap} \u2014 restarted recently, waiting for it to link`));
|
|
6067
6235
|
} else if (!isTurnOver(session)) {
|
|
6068
|
-
decisions.push(
|
|
6236
|
+
decisions.push(decide3(session, "defer" /* Defer */, `${gap} \u2014 turn in flight (${session.processStatus ?? session.turnStatus ?? "unknown"})`));
|
|
6069
6237
|
} else if (restartsPlanned >= maxRestarts) {
|
|
6070
|
-
decisions.push(
|
|
6238
|
+
decisions.push(decide3(session, "defer" /* Defer */, `${gap} \u2014 restart cap reached this pass`));
|
|
6071
6239
|
} else {
|
|
6072
6240
|
restartsPlanned++;
|
|
6073
|
-
decisions.push(
|
|
6241
|
+
decisions.push(decide3(session, "respawn" /* Respawn */, gap));
|
|
6074
6242
|
}
|
|
6075
6243
|
}
|
|
6076
6244
|
return { connectedSessionIds, decisions };
|
|
@@ -6455,6 +6623,7 @@ function createDefaultWatchTickDeps(options) {
|
|
|
6455
6623
|
isAutoReloadEnabled: options.isAutoReloadEnabled,
|
|
6456
6624
|
isRecoveryEnabled: options.isRecoveryEnabled,
|
|
6457
6625
|
isRemoteControlKeptOn: options.isRemoteControlKeptOn,
|
|
6626
|
+
isLimitResumeEnabled: options.isLimitResumeEnabled,
|
|
6458
6627
|
launchServiceLoop: createServiceLoopLauncher(process.platform, defaultCommandRunner)
|
|
6459
6628
|
};
|
|
6460
6629
|
}
|
|
@@ -6529,6 +6698,33 @@ function waitForExitVia(deps) {
|
|
|
6529
6698
|
function isSessionRunningVia(deps) {
|
|
6530
6699
|
return (sessionId) => isSessionRunning(deps.runCommand, deps.paths.claudeSessionRegistryDir, sessionId) !== false;
|
|
6531
6700
|
}
|
|
6701
|
+
function resumeDepsOf(deps) {
|
|
6702
|
+
return {
|
|
6703
|
+
runCommand: deps.runCommand,
|
|
6704
|
+
claudeProjectsDir: deps.paths.claudeProjectsDir,
|
|
6705
|
+
nowMs: deps.nowMs,
|
|
6706
|
+
canRestartIn: canRestartVia(deps),
|
|
6707
|
+
waitForExit: waitForExitVia(deps),
|
|
6708
|
+
isSessionRunning: isSessionRunningVia(deps)
|
|
6709
|
+
};
|
|
6710
|
+
}
|
|
6711
|
+
function runLimitResumePass(claudeSessions, attemptsBySession, maxContinues, deps) {
|
|
6712
|
+
const limitStopsBySessionId = /* @__PURE__ */ new Map();
|
|
6713
|
+
for (const session of claudeSessions) {
|
|
6714
|
+
const stop = readLimitStop(deps.paths.claudeProjectsDir, session.sessionId);
|
|
6715
|
+
if (stop !== void 0) limitStopsBySessionId.set(session.sessionId, stop);
|
|
6716
|
+
}
|
|
6717
|
+
const planned = planLimitResumes(
|
|
6718
|
+
claudeSessions,
|
|
6719
|
+
limitStopsBySessionId,
|
|
6720
|
+
attemptsBySession,
|
|
6721
|
+
deps.nowMs,
|
|
6722
|
+
isClaudeUsageLimitInForce(deps.paths.claudeGlobalConfigFile, deps.nowMs),
|
|
6723
|
+
maxContinues
|
|
6724
|
+
);
|
|
6725
|
+
const executed = executeLimitResumes(planned, attemptsBySession, resumeDepsOf(deps));
|
|
6726
|
+
return { report: { decisions: executed.decisions }, attemptsBySession: executed.attemptsBySession, events: executed.events };
|
|
6727
|
+
}
|
|
6532
6728
|
function reloadTargetAtMs(deps, installs) {
|
|
6533
6729
|
return resolveReloadTargetAtMs(installs, readMugglePluginUpdatedAtMs(deps.paths.claudeInstalledPluginsFile));
|
|
6534
6730
|
}
|
|
@@ -6591,14 +6787,7 @@ function runTickUnlocked(deps) {
|
|
|
6591
6787
|
sessionsById,
|
|
6592
6788
|
isClaudeUsageLimitInForce(deps.paths.claudeGlobalConfigFile, deps.nowMs)
|
|
6593
6789
|
);
|
|
6594
|
-
const executed = executeWatcherRecoveries(planned, recoveryAttemptsByOwner,
|
|
6595
|
-
runCommand: deps.runCommand,
|
|
6596
|
-
claudeProjectsDir: deps.paths.claudeProjectsDir,
|
|
6597
|
-
nowMs: deps.nowMs,
|
|
6598
|
-
canRestartIn: canRestartVia(deps),
|
|
6599
|
-
waitForExit: waitForExitVia(deps),
|
|
6600
|
-
isSessionRunning: isSessionRunningVia(deps)
|
|
6601
|
-
});
|
|
6790
|
+
const executed = executeWatcherRecoveries(planned, recoveryAttemptsByOwner, resumeDepsOf(deps));
|
|
6602
6791
|
recoveries = executed.recoveries;
|
|
6603
6792
|
recoveryAttemptsByOwner = executed.attemptsByOwner;
|
|
6604
6793
|
events.push(...executed.events);
|
|
@@ -6613,6 +6802,22 @@ function runTickUnlocked(deps) {
|
|
|
6613
6802
|
events.push(...started.events);
|
|
6614
6803
|
}
|
|
6615
6804
|
const restartedSessionIds = new Set(recoveries.filter(isRestartAttempted).map((recovery) => recovery.ownerSessionId));
|
|
6805
|
+
let limitResumeAttemptsBySession = state.limitResumeAttemptsBySession ?? {};
|
|
6806
|
+
let limitResumes;
|
|
6807
|
+
if (deps.isLimitResumeEnabled && claudeSessions !== null) {
|
|
6808
|
+
const pass = runLimitResumePass(
|
|
6809
|
+
claudeSessions.filter((session) => !restartedSessionIds.has(session.sessionId)),
|
|
6810
|
+
limitResumeAttemptsBySession,
|
|
6811
|
+
Math.max(0, MAX_RESUMES_PER_TICK - restartedSessionIds.size),
|
|
6812
|
+
deps
|
|
6813
|
+
);
|
|
6814
|
+
limitResumes = pass.report;
|
|
6815
|
+
limitResumeAttemptsBySession = pass.attemptsBySession;
|
|
6816
|
+
events.push(...pass.events);
|
|
6817
|
+
for (const decision of pass.report.decisions) {
|
|
6818
|
+
if (decision.outcome !== void 0) restartedSessionIds.add(decision.sessionId);
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6616
6821
|
let pendingReload = state.pendingReload;
|
|
6617
6822
|
let reload;
|
|
6618
6823
|
const targetAtMs = reloadTargetAtMs(deps, installs);
|
|
@@ -6649,7 +6854,8 @@ function runTickUnlocked(deps) {
|
|
|
6649
6854
|
terminalPrsByUrl,
|
|
6650
6855
|
pendingReload,
|
|
6651
6856
|
claudeLogin,
|
|
6652
|
-
remoteControlRestartsBySession
|
|
6857
|
+
remoteControlRestartsBySession,
|
|
6858
|
+
limitResumeAttemptsBySession
|
|
6653
6859
|
});
|
|
6654
6860
|
const snapshot = {
|
|
6655
6861
|
takenAt: at,
|
|
@@ -6663,7 +6869,8 @@ function runTickUnlocked(deps) {
|
|
|
6663
6869
|
recoveries,
|
|
6664
6870
|
serviceArms,
|
|
6665
6871
|
reload,
|
|
6666
|
-
remoteControl
|
|
6872
|
+
remoteControl,
|
|
6873
|
+
limitResumes
|
|
6667
6874
|
};
|
|
6668
6875
|
writeWatchSnapshot(deps.paths.watchDir, snapshot);
|
|
6669
6876
|
return snapshot;
|
|
@@ -6720,7 +6927,8 @@ function watchTickCommand(flags) {
|
|
|
6720
6927
|
createDefaultWatchTickDeps({
|
|
6721
6928
|
isAutoReloadEnabled: flags.autoReload,
|
|
6722
6929
|
isRecoveryEnabled: flags.recovery,
|
|
6723
|
-
isRemoteControlKeptOn: flags.remoteControl
|
|
6930
|
+
isRemoteControlKeptOn: flags.remoteControl,
|
|
6931
|
+
isLimitResumeEnabled: flags.limitResume
|
|
6724
6932
|
})
|
|
6725
6933
|
);
|
|
6726
6934
|
if (flags.json) {
|
|
@@ -6762,7 +6970,7 @@ function watchReloadPluginsCommand(flags) {
|
|
|
6762
6970
|
try {
|
|
6763
6971
|
const isDryRun = flags.dryRun === true;
|
|
6764
6972
|
const report = runReloadPluginsPass(
|
|
6765
|
-
createDefaultWatchTickDeps({ isAutoReloadEnabled: true, isRecoveryEnabled: false, isRemoteControlKeptOn: false }),
|
|
6973
|
+
createDefaultWatchTickDeps({ isAutoReloadEnabled: true, isRecoveryEnabled: false, isRemoteControlKeptOn: false, isLimitResumeEnabled: false }),
|
|
6766
6974
|
isDryRun
|
|
6767
6975
|
);
|
|
6768
6976
|
if (!report) {
|
|
@@ -6849,7 +7057,7 @@ function createProgram() {
|
|
|
6849
7057
|
stack.command("status").description("Show every lease, the line, and the services prepare tracks").option("--json", "Print the full report as JSON").action(stackStatusCommand);
|
|
6850
7058
|
program.command("ci-install").description("Add the Muggle walkthrough check to this repository's GitHub Actions").option("--force", "Replace an existing workflow file").action(ciInstallCommand);
|
|
6851
7059
|
const watch = program.command("watch").description("Standing watch over Claude/Codex sessions, their processes, and PR watchers");
|
|
6852
|
-
watch.command("tick").description("Run one watch pass (the scheduled task runs this)").option("--no-auto-reload", "Do not start a plugin reload fan-out when installed plugins change").option("--no-recovery", "Report dead watchers without resuming their owner sessions").option("--no-remote-control", "Report sessions off Remote Control without restarting them").option("--json", "Print the full snapshot as JSON").action(watchTickCommand);
|
|
7060
|
+
watch.command("tick").description("Run one watch pass (the scheduled task runs this)").option("--no-auto-reload", "Do not start a plugin reload fan-out when installed plugins change").option("--no-recovery", "Report dead watchers without resuming their owner sessions").option("--no-remote-control", "Report sessions off Remote Control without restarting them").option("--no-limit-resume", "Report sessions a usage limit cut off without continuing them").option("--json", "Print the full snapshot as JSON").action(watchTickCommand);
|
|
6853
7061
|
watch.command("status").description("Show the last watch pass: sessions, processes, PR watches, findings").option("--json", "Print the snapshot as JSON").action(watchStatusCommand);
|
|
6854
7062
|
watch.command("reload-plugins").description("Reload plugins across active Claude sessions (respawns idle background ones)").option("--dry-run", "Show what each session would get without respawning anything").option("--json", "Print the report as JSON").action(watchReloadPluginsCommand);
|
|
6855
7063
|
watch.command("install").description("Schedule the watch with this OS's scheduler, or refresh the entry in place").option("--uninstall", "Remove the task; the ledger is kept").option("--no-wait", "Return once the task is registered, without waiting for its first pass").option("--from-preference", "Act on the autoInstallWatchService preference, as an install or upgrade does").action(watchInstallCommand);
|
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-
|
|
1
|
+
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-HK7XCCNT.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-7J7DS4SA.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.276",
|
|
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.276",
|
|
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.276",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/dist/plugin/.mcp.json
CHANGED
|
@@ -6,7 +6,7 @@ How `/muggle-do` resolves `$ARGUMENTS` to a mode. Modes 1–4 are programmatic
|
|
|
6
6
|
2. **Fix-CI** — a PR/MR URL **and** a `fix ci` / `fix-ci` directive with failing check names (no review/discussion ids) → [`fix-ci.md`](fix-ci.md).
|
|
7
7
|
3. **Rebase** — a PR/MR URL **and** a `rebase` directive (or legacy `resolve conflicts` / `resolve-conflicts`; no review/discussion ids, no check names) → [`resolve-conflicts.md`](resolve-conflicts.md). Rebases the branch onto its base whether it's merely behind or actually conflicting.
|
|
8
8
|
4. **Post-merge cleanup** — a `cleanup` token and `slug=<slug>` (no PR URL, no review ids), optionally `state=<merged|closed>` (default `merged`) → [`cleanup.md`](cleanup.md).
|
|
9
|
-
5. **Empty / `help` / `menu` / `?`** → menu + session selector.
|
|
9
|
+
5. **Empty / `help` / `menu` / `?`** → menu + session selector, answered from this file alone: open no stage file. The menu offers describing a task to start the forward pipeline, plus `/muggle-status`, `/muggle-repair`, and `/muggle-upgrade`. The session selector lists each directory under `~/.muggle-ai/muggle-do/sessions/` with the PRs and states in its `prs.json`; picking one resumes that session.
|
|
10
10
|
6. **Task automation** (perform an action on a website) → `muggle:muggle-browser-task`.
|
|
11
11
|
7. **Otherwise** → forward pipeline at Stage 1.
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ You receive:
|
|
|
19
19
|
|
|
20
20
|
For each repo:
|
|
21
21
|
|
|
22
|
-
1. **Run the test command** using Bash in the repo's directory. Use the provided test command (default: `pnpm test`). Suites must run in one-shot mode (`
|
|
22
|
+
1. **Run the test command** using Bash in the repo's directory. Use the provided test command (default: `pnpm test`). Suites must run in one-shot mode (`vitest run`, jest `--watchAll=false`) — never watch mode: an orphaned watcher fork-bombs on Windows. Never set `CI=true` for a local run: repos key their local worker cap and changed-files scoping on `CI` being unset, so it runs the full suite at one worker per core. Launch through the shipped cap so the process tree is kernel-bounded and reaped if this run is killed: `node "${CLAUDE_PLUGIN_ROOT}/scripts/guard-run.mjs" -- <test command>`.
|
|
23
23
|
2. **Capture the full output** — both stdout and stderr.
|
|
24
24
|
3. **Determine pass/fail** — exit code 0 means pass, anything else means fail.
|
|
25
25
|
4. **If tests fail**, extract the specific failing test names/descriptions from the output.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"release": "5.26.0",
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-11-
|
|
3
|
+
"buildId": "run-276-1",
|
|
4
|
+
"commitSha": "b471e9f5955a787d6752dc41a20db87ceaf0a73e",
|
|
5
|
+
"buildTime": "2026-11-01T16:33:57Z",
|
|
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.
|
|
4
|
+
"version": "5.31.0-staging.276",
|
|
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.
|
|
4
|
+
"version": "5.31.0-staging.276",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
package/plugin/.codex-mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.31.0-staging.
|
|
3
|
+
"version": "5.31.0-staging.276",
|
|
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.276",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/plugin/.mcp.json
CHANGED
|
@@ -6,7 +6,7 @@ How `/muggle-do` resolves `$ARGUMENTS` to a mode. Modes 1–4 are programmatic
|
|
|
6
6
|
2. **Fix-CI** — a PR/MR URL **and** a `fix ci` / `fix-ci` directive with failing check names (no review/discussion ids) → [`fix-ci.md`](fix-ci.md).
|
|
7
7
|
3. **Rebase** — a PR/MR URL **and** a `rebase` directive (or legacy `resolve conflicts` / `resolve-conflicts`; no review/discussion ids, no check names) → [`resolve-conflicts.md`](resolve-conflicts.md). Rebases the branch onto its base whether it's merely behind or actually conflicting.
|
|
8
8
|
4. **Post-merge cleanup** — a `cleanup` token and `slug=<slug>` (no PR URL, no review ids), optionally `state=<merged|closed>` (default `merged`) → [`cleanup.md`](cleanup.md).
|
|
9
|
-
5. **Empty / `help` / `menu` / `?`** → menu + session selector.
|
|
9
|
+
5. **Empty / `help` / `menu` / `?`** → menu + session selector, answered from this file alone: open no stage file. The menu offers describing a task to start the forward pipeline, plus `/muggle-status`, `/muggle-repair`, and `/muggle-upgrade`. The session selector lists each directory under `~/.muggle-ai/muggle-do/sessions/` with the PRs and states in its `prs.json`; picking one resumes that session.
|
|
10
10
|
6. **Task automation** (perform an action on a website) → `muggle:muggle-browser-task`.
|
|
11
11
|
7. **Otherwise** → forward pipeline at Stage 1.
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ You receive:
|
|
|
19
19
|
|
|
20
20
|
For each repo:
|
|
21
21
|
|
|
22
|
-
1. **Run the test command** using Bash in the repo's directory. Use the provided test command (default: `pnpm test`). Suites must run in one-shot mode (`
|
|
22
|
+
1. **Run the test command** using Bash in the repo's directory. Use the provided test command (default: `pnpm test`). Suites must run in one-shot mode (`vitest run`, jest `--watchAll=false`) — never watch mode: an orphaned watcher fork-bombs on Windows. Never set `CI=true` for a local run: repos key their local worker cap and changed-files scoping on `CI` being unset, so it runs the full suite at one worker per core. Launch through the shipped cap so the process tree is kernel-bounded and reaped if this run is killed: `node "${CLAUDE_PLUGIN_ROOT}/scripts/guard-run.mjs" -- <test command>`.
|
|
23
23
|
2. **Capture the full output** — both stdout and stderr.
|
|
24
24
|
3. **Determine pass/fail** — exit code 0 means pass, anything else means fail.
|
|
25
25
|
4. **If tests fail**, extract the specific failing test names/descriptions from the output.
|
|
@@ -8,6 +8,7 @@ A standing per-user scheduled job that runs `muggle watch tick` every few minute
|
|
|
8
8
|
- **Closes out finished slots.** A slot whose PR GitHub reports merged or closed, with no loop and no running owner, is finalized the way a watcher's terminal tick would: `prs.json` takes the state, `followup.log` the terminal line, and `result.md` is written. Only local files change; post-merge cleanup is left to a session and recorded as not run.
|
|
9
9
|
- **Keeps sessions on the installed build.** A session loaded before the newest global install (or a later user-scope update of the muggle plugin) is behind; other plugins updating restart nothing. Idle background sessions are respawned onto the new build, up to 3 per tick, keeping their own id and name. A session that owns open PR watches is left running. Before a restart, a throwaway background launch in the session's workspace confirms Claude will bring it back; a workspace that refuses is left running and named once. Busy or waiting sessions wait for a later tick, and terminal sessions get one notification to run `/reload-plugins`. No interface runs a slash command inside another session.
|
|
10
10
|
- **Keeps sessions on Remote Control.** With `remoteControlAtStartup` on in Claude's settings, a live background session is off when its `~/.claude/sessions/<pid>.json` entry has no Remote Control link, or its process started before the current claude.ai login took effect, so its link serves another account. Such a session is respawned once its turn is over (registry status `idle` or `shell`), sharing the 3-per-tick cap with the reload. That keeps the same session id, so there is no duplicate in the agent view, and nothing is typed into it. Background commands carry over to the restarted process, PR watch loops included, so they keep reporting to that session. Each restart opens a new Remote Control session, so a session already on is never restarted. A restarted session gets 30 minutes to link, and one still off after 2 restarts in a day is left alone. Terminal sessions that are off are listed for `/remote-control`.
|
|
11
|
+
- **Continues sessions a usage limit cut off.** Claude never retries a turn a usage limit ended: the session stays stopped on the limit notice. A background session whose last turn is that notice is resumed in place with one continue prompt once the notice's own reset time has passed and Claude's usage cache shows no window still exhausted. A running session is stopped first, between turns only, and a copy Claude starts instead is removed. Each notice is continued once, a session at most twice a day, sharing the per-tick cap with owner wakes; a notice older than 8 days is left alone, and terminal sessions are listed to continue by hand.
|
|
11
12
|
- **Records and alerts.** Results go to `status.json` (last snapshot, with open-PR and Remote Control coverage), `state.json` (active findings, recovery attempts, reload progress, the login last seen, Remote Control restarts) and `events.jsonl` (raise/clear/resume/respawn/close-out history) under `~/.muggle-ai/watch/`. New alerts raise one desktop notification per tick.
|
|
12
13
|
|
|
13
14
|
## Install
|
|
@@ -45,6 +46,7 @@ On Windows the PowerShell installer also takes flags directly:
|
|
|
45
46
|
| `-NoRecovery` | Report dead watchers without resuming their owners |
|
|
46
47
|
| `-NoAutoReload` | Never restart sessions onto a new build |
|
|
47
48
|
| `-NoRemoteControl` | Report sessions off Remote Control without restarting them |
|
|
49
|
+
| `-NoLimitResume` | Report sessions a usage limit cut off without continuing them |
|
|
48
50
|
| `-NoWait` | Return once the task is registered, before its first tick |
|
|
49
51
|
| `-MuggleEntry <path>` | Run a specific `bin\muggle.js` instead of the global install |
|
|
50
52
|
| `-Uninstall` | Remove the task; the ledger is kept |
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
# Codex sessions, their processes and every PR-watch slot, alerts on dead or deaf
|
|
4
4
|
# watchers, red CI and stale branches, restarts the loop of a registered slot
|
|
5
5
|
# whose watcher died, fans plugin reloads out across sessions, and restarts
|
|
6
|
-
# sessions that are off Remote Control under the current login
|
|
6
|
+
# sessions that are off Remote Control under the current login, and continues
|
|
7
|
+
# background sessions a usage limit cut off once that limit resets.
|
|
7
8
|
#
|
|
8
9
|
# The task runs the global @muggleai/works install, whose path survives
|
|
9
10
|
# `muggle upgrade`, so the service always runs the current release. Only the
|
|
@@ -22,6 +23,7 @@ param(
|
|
|
22
23
|
[switch]$NoAutoReload,
|
|
23
24
|
[switch]$NoRecovery,
|
|
24
25
|
[switch]$NoRemoteControl,
|
|
26
|
+
[switch]$NoLimitResume,
|
|
25
27
|
# Return once the task is registered; its trigger runs the first tick a minute later.
|
|
26
28
|
[switch]$NoWait,
|
|
27
29
|
[switch]$Uninstall
|
|
@@ -64,6 +66,7 @@ $tickFlags = @()
|
|
|
64
66
|
if ($NoAutoReload) { $tickFlags += "--no-auto-reload" }
|
|
65
67
|
if ($NoRecovery) { $tickFlags += "--no-recovery" }
|
|
66
68
|
if ($NoRemoteControl) { $tickFlags += "--no-remote-control" }
|
|
69
|
+
if ($NoLimitResume) { $tickFlags += "--no-limit-resume" }
|
|
67
70
|
$launcherArguments = @("`"$installedLauncher`"", "`"$($nodeCommand.Source)`"", "`"$MuggleEntry`"") + $tickFlags
|
|
68
71
|
|
|
69
72
|
# wscript.exe rather than node.exe is what keeps every run off the desktop; see
|