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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/{chunk-EPIM27JX.js → chunk-SJBYMBC2.js} +14 -8
  3. package/dist/cli.js +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  6. package/dist/plugin/.codex-mcp.json +1 -1
  7. package/dist/plugin/.codex-plugin/plugin.json +1 -1
  8. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  9. package/dist/plugin/.mcp.json +1 -1
  10. package/dist/plugin/hooks/README.md +1 -1
  11. package/dist/plugin/scripts/guardrail-record-stage-read.sh +2 -0
  12. package/dist/plugin/scripts/guardrails.mjs +44 -20
  13. package/dist/plugin/skills/CLAUDE.md +2 -0
  14. package/dist/plugin/skills/do/address-reviews.md +6 -0
  15. package/dist/plugin/skills/do/fix-ci.md +7 -0
  16. package/dist/plugin/skills/do/pre-flight.md +11 -0
  17. package/dist/plugin/skills/do/resolve-conflicts.md +7 -0
  18. package/dist/plugin/skills/muggle-do/SKILL.md +4 -1
  19. package/dist/release-manifest.json +3 -3
  20. package/package.json +1 -1
  21. package/plugin/.claude-plugin/plugin.json +1 -1
  22. package/plugin/.codex-mcp.json +1 -1
  23. package/plugin/.codex-plugin/plugin.json +1 -1
  24. package/plugin/.cursor-plugin/plugin.json +1 -1
  25. package/plugin/.mcp.json +1 -1
  26. package/plugin/hooks/README.md +1 -1
  27. package/plugin/scripts/guardrail-record-stage-read.sh +2 -0
  28. package/plugin/scripts/guardrails.mjs +44 -20
  29. package/plugin/skills/CLAUDE.md +2 -0
  30. package/plugin/skills/do/address-reviews.md +6 -0
  31. package/plugin/skills/do/fix-ci.md +7 -0
  32. package/plugin/skills/do/pre-flight.md +11 -0
  33. package/plugin/skills/do/resolve-conflicts.md +7 -0
  34. package/plugin/skills/muggle-do/SKILL.md +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.27.0-staging.243"
12
+ "version": "5.27.0-staging.245"
13
13
  }
14
14
  ]
15
15
  }
@@ -5375,7 +5375,7 @@ function readProcessTable(platform4, runCommand, nowMs) {
5375
5375
  function resumeOnReconcile(target, runCommand, waitForExit) {
5376
5376
  if (target.runningShortId !== void 0) {
5377
5377
  runCommand("claude", ["stop", target.runningShortId]);
5378
- if (!waitForExit(target.sessionId)) return false;
5378
+ if (!waitForExit(target.runningSessionId ?? target.sessionId)) return false;
5379
5379
  }
5380
5380
  const resumed = runCommand("claude", ["--resume", target.sessionId, "--bg", RECOVERY_RESUME_PROMPT], { cwd: target.cwd });
5381
5381
  return resumed?.exitCode === 0;
@@ -5383,17 +5383,21 @@ function resumeOnReconcile(target, runCommand, waitForExit) {
5383
5383
 
5384
5384
  // src/watch/recovery.ts
5385
5385
  function isRecoverable(assessment, ownerSession) {
5386
- const isOwnerRestartable = assessment.isOwnerSessionActive === false || assessment.isOwnerSessionActive === true && ownerSession !== void 0 && isIdleBackgroundSession(ownerSession);
5386
+ const isOwnerRestartable = ownerSession === void 0 ? assessment.isOwnerSessionActive === false : isIdleBackgroundSession(ownerSession);
5387
5387
  return assessment.isRegistered && assessment.watcher === "dead" /* Dead */ && assessment.live?.state === "OPEN" /* Open */ && assessment.ownerSessionId !== void 0 && isOwnerRestartable;
5388
5388
  }
5389
5389
  function isSlotCloseOutDue(assessment) {
5390
5390
  return isTerminalPrState(assessment.live?.state) && assessment.watcher === "dead" /* Dead */ && assessment.isOwnerSessionActive !== true;
5391
5391
  }
5392
+ function findRunningOwner(sessionsById, ownerSessionId) {
5393
+ const ownerShortId = shortSessionId(ownerSessionId);
5394
+ return sessionsById.get(ownerSessionId) ?? [...sessionsById.values()].find((session) => session.shortId === ownerShortId);
5395
+ }
5392
5396
  function planWatcherRecoveries(assessments, attemptsByOwner, nowMs, isPollingDisabled, sessionsById = /* @__PURE__ */ new Map(), isStoppedAtUsageLimit2 = () => false) {
5393
5397
  if (isPollingDisabled) return [];
5394
5398
  const slugsByOwner = /* @__PURE__ */ new Map();
5395
5399
  for (const assessment of assessments) {
5396
- const ownerSession = assessment.ownerSessionId === void 0 ? void 0 : sessionsById.get(assessment.ownerSessionId);
5400
+ const ownerSession = assessment.ownerSessionId === void 0 ? void 0 : findRunningOwner(sessionsById, assessment.ownerSessionId);
5397
5401
  if (!isRecoverable(assessment, ownerSession)) continue;
5398
5402
  const ownerSessionId = assessment.ownerSessionId;
5399
5403
  const slugs = slugsByOwner.get(ownerSessionId) ?? /* @__PURE__ */ new Set();
@@ -5405,18 +5409,20 @@ function planWatcherRecoveries(assessments, attemptsByOwner, nowMs, isPollingDis
5405
5409
  for (const ownerSessionId of [...slugsByOwner.keys()].sort()) {
5406
5410
  const recentAttemptsMs = (attemptsByOwner[ownerSessionId] ?? []).map((attemptedAt) => Date.parse(attemptedAt)).filter((attemptedAtMs) => !Number.isNaN(attemptedAtMs) && nowMs - attemptedAtMs < RECOVERY_ATTEMPT_WINDOW_MS);
5407
5411
  const lastAttemptMs = Math.max(-Infinity, ...recentAttemptsMs);
5408
- const isCapReached = recentAttemptsMs.length >= RECOVERY_MAX_ATTEMPTS && !isStoppedAtUsageLimit2(ownerSessionId);
5412
+ const runningOwner = findRunningOwner(sessionsById, ownerSessionId);
5413
+ const isRunningAtUsageLimit = runningOwner !== void 0 && [ownerSessionId, runningOwner.sessionId].some((sessionId) => isStoppedAtUsageLimit2(sessionId));
5409
5414
  let outcome = "resume-due" /* ResumeDue */;
5410
- if (isCapReached) outcome = "attempts-exhausted" /* AttemptsExhausted */;
5415
+ if (isRunningAtUsageLimit) outcome = "awaiting-usage-limit-reset" /* AwaitingUsageLimitReset */;
5416
+ else if (recentAttemptsMs.length >= RECOVERY_MAX_ATTEMPTS) outcome = "attempts-exhausted" /* AttemptsExhausted */;
5411
5417
  else if (nowMs - lastAttemptMs < RECOVERY_COOLDOWN_MS) outcome = "cooling-down" /* CoolingDown */;
5412
5418
  else if (resumesPlanned >= MAX_RESUMES_PER_TICK) outcome = "deferred-by-tick-cap" /* DeferredByTickCap */;
5413
5419
  if (outcome === "resume-due" /* ResumeDue */) resumesPlanned++;
5414
- const runningOwner = sessionsById.get(ownerSessionId);
5415
5420
  recoveries.push({
5416
5421
  ownerSessionId,
5417
5422
  slotSlugs: [...slugsByOwner.get(ownerSessionId) ?? []].sort(),
5418
5423
  outcome,
5419
- ...runningOwner?.shortId === void 0 ? {} : { ownerShortId: runningOwner.shortId }
5424
+ ...runningOwner?.shortId === void 0 ? {} : { ownerShortId: runningOwner.shortId },
5425
+ ...runningOwner === void 0 || runningOwner.sessionId === ownerSessionId ? {} : { ownerRunningSessionId: runningOwner.sessionId }
5420
5426
  });
5421
5427
  }
5422
5428
  return recoveries;
@@ -5444,7 +5450,7 @@ function executeWatcherRecoveries(planned, attemptsByOwner, deps) {
5444
5450
  return { ...recovery, cwd, outcome: "restart-refused" /* RestartRefused */ };
5445
5451
  }
5446
5452
  const isResumed = resumeOnReconcile(
5447
- { sessionId: recovery.ownerSessionId, cwd, runningShortId: recovery.ownerShortId },
5453
+ { sessionId: recovery.ownerSessionId, cwd, runningShortId: recovery.ownerShortId, runningSessionId: recovery.ownerRunningSessionId },
5448
5454
  deps.runCommand,
5449
5455
  deps.waitForExit
5450
5456
  );
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-EPIM27JX.js';
2
+ import { runCli } from './chunk-SJBYMBC2.js';
3
3
  import './chunk-FI6VHHZN.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-EPIM27JX.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-SJBYMBC2.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-FI6VHHZN.js';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
4
- "version": "5.27.0-staging.243",
4
+ "version": "5.27.0-staging.245",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.243",
7
+ "@muggleai/works@5.27.0-staging.245",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.27.0-staging.243",
3
+ "version": "5.27.0-staging.245",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
5
  "author": {
6
6
  "name": "Muggle AI",
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
- "version": "5.27.0-staging.243",
5
+ "version": "5.27.0-staging.245",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.243",
7
+ "@muggleai/works@5.27.0-staging.245",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -42,7 +42,7 @@ Pre-filters are bash builtins only (`guardrail-lib.sh`), so an irrelevant event
42
42
  | `PostToolUse` (AskUserQuestion) | `guardrail-offer-ran.sh` | record | a next-options offer ran while a terminal PR was pending | — | clear `terminalPending` — the only exit for the post-merge Stop gate |
43
43
  | `PostToolUse` (Agent, Task) | `guardrail-record-delegation.sh` | record | a `muggle:acceptance-tester` was launched into the background — its response carries no finished report yet | — | append to `e2eDelegations` with the PR URLs its brief names; for a bounded window the E2E and debug-path gates hold off, and the walkthrough gate sets those PRs aside, so the session is never steered into a second browser run alongside the agent's. Shares the AskUserQuestion group's dispatch |
44
44
  | `PostToolUse` (Skill + muggle telemetry-skill-emit) | `guardrail-skill-stages.sh` | advise | a skill this plugin ships was invoked; its `SKILL.md` frontmatter may declare `mandatoryStages`. Registered on the skill's own MCP announcement too, so the declaration still lands if the Skill payload ever stops carrying a name. A muggle-test announcement is also an E2E run (the clean SKIP verdict), recorded here alone, so the skill, its stages and the run land in one commit; a write that cannot take the session's lock in time is saved beside the state file and committed by a detached job once the lock frees | — | record `lastInvokedSkillName` + `mandatoryStages` (and `e2eRun` for a muggle-test announcement), and name the declared files as required reading at the moment of use |
45
- | `PostToolUse` (Read) | `guardrail-record-stage-read.sh` | record | a markdown file under a `skills/` directory was opened | — | append to `stagesRead` — how the stage gate tells a stage that was read from one that was skipped |
45
+ | `PostToolUse` (Read) | `guardrail-record-stage-read.sh` | record | a markdown file under a `skills/` directory was opened | — | append to `stagesRead` — how the stage gate tells a stage that was read from one that was skipped; a route entry file in the plugin's skills tree, opened while a skill runs, also adds the `mandatoryStages` its frontmatter declares |
46
46
  | `PostToolUse` (Bash + muggle telemetry-event-emit / user-feedback-create) | `guardrail-record-stage-signals.sh` | record | a Step 6f `pre-execution-classification` emit, debug-path evidence for a failed run (a `*-failure-classified\|resolved` emit or feedback naming it), or a `MUGGLE_STAGE_SKIP` / `MUGGLE_CLASSIFY_SKIP` / `MUGGLE_DEBUG_SKIP` marker | — | set `classifiedTestCaseIds` / `debuggedRuns` / the matching skip flag |
47
47
  | `PostToolUse` (Bash) | `guardrail-record-comment-replies.sh` | record | an unresolved-thread fetch named review threads still awaiting an answer (classified by the `<!-- muggle-do:bot -->` marker, never the author login), a threaded reply the provider **confirmed** answered comments in one, or a `MUGGLE_REPLY_SKIP` marker deferred them | — | claim each thread and cover the comments answered, in the per-PR ledger beside the muggle-do slot |
48
48
  | `PreToolUse` (Bash, `if` a command names `gh`/`glab`) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body — or a `gh api … issues/comments/<id>` PATCH editing one — reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
@@ -4,6 +4,8 @@ set -uo pipefail
4
4
  # stage-read observer (PostToolUse/Read, and PostToolUse/Bash through the
5
5
  # dispatcher). Records that a file in the skills tree was opened, which is how
6
6
  # the Stop gate tells a mandatory stage that was read from one that was skipped.
7
+ # A route's entry file that declares `mandatoryStages` adds that route's stages
8
+ # when it is opened during a skill's run.
7
9
  # A host with no Read tool (Codex) opens stage files through the shell — `cat`,
8
10
  # `sed -n` — so a shell command that prints one counts as a read too. Emits no
9
11
  # directive.
@@ -1464,6 +1464,7 @@ var SEQUENCE_ENTRY = /^\s*-\s*(.+?)\s*$/;
1464
1464
  var FLOW_LIST = /^\s*\[(.*)\]\s*$/;
1465
1465
  var STAGE_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_STAGE_SKIP\b/;
1466
1466
  var SKILLS_DIR_NAME = "skills";
1467
+ var SKILL_ENTRY_FILE_NAME = "SKILL.md";
1467
1468
  var SKILLS_PATH_SEGMENT = `/${SKILLS_DIR_NAME}/`;
1468
1469
  var SINGLE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;
1469
1470
  var SHELL_VARIABLE_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
@@ -1513,17 +1514,15 @@ function resolveSkillNameFromToolInput(toolInput) {
1513
1514
  }
1514
1515
  return void 0;
1515
1516
  }
1516
- function resolveSkillStagePaths(skillName, skillsRootDir) {
1517
- const skillDir = join(skillsRootDir, skillName);
1518
- const skillFile = join(skillDir, "SKILL.md");
1519
- if (!existsSync(skillFile)) return [];
1520
- const normalizedRoot = normalizeStagePath(skillsRootDir);
1521
- const staysInsideSkillsTree = (candidate) => normalizeStagePath(candidate).startsWith(`${normalizedRoot}/`);
1517
+ var isInsideDirectory = (candidatePath, directory) => normalizeStagePath(candidatePath).startsWith(`${normalizeStagePath(directory)}/`);
1518
+ function resolveDeclaredStagePaths(declaringFile, skillsRootDir) {
1519
+ if (!existsSync(declaringFile)) return [];
1520
+ const declaringDir = dirname(declaringFile);
1522
1521
  const resolved = [];
1523
- for (const declared of parseDeclaredStages(readFileSync(skillFile, "utf-8"))) {
1522
+ for (const declared of parseDeclaredStages(readFileSync(declaringFile, "utf-8"))) {
1524
1523
  if (isAbsolute(declared)) continue;
1525
- const stagePath = [resolve(skillDir, declared), resolve(skillsRootDir, declared)].find(
1526
- (candidate) => staysInsideSkillsTree(candidate) && existsSync(candidate)
1524
+ const stagePath = [resolve(declaringDir, declared), resolve(skillsRootDir, declared)].find(
1525
+ (candidate) => isInsideDirectory(candidate, skillsRootDir) && existsSync(candidate)
1527
1526
  );
1528
1527
  if (!stagePath) continue;
1529
1528
  const normalized = normalizeStagePath(stagePath);
@@ -1531,13 +1530,27 @@ function resolveSkillStagePaths(skillName, skillsRootDir) {
1531
1530
  }
1532
1531
  return resolved;
1533
1532
  }
1533
+ function resolveSkillStagePaths(skillName, skillsRootDir) {
1534
+ return resolveDeclaredStagePaths(join(skillsRootDir, skillName, SKILL_ENTRY_FILE_NAME), skillsRootDir);
1535
+ }
1536
+ function resolveRouteStagePaths(state, readPath, skillsRootDir) {
1537
+ if (!state.lastInvokedSkillName) return [];
1538
+ if (basename(readPath).toLowerCase() === SKILL_ENTRY_FILE_NAME.toLowerCase()) return [];
1539
+ if (!isInsideDirectory(readPath, skillsRootDir)) return [];
1540
+ return resolveDeclaredStagePaths(readPath, skillsRootDir);
1541
+ }
1542
+ function applyDeclaredStages(state, stagePaths) {
1543
+ const owed = state.mandatoryStages ?? [];
1544
+ const added = stagePaths.filter(
1545
+ (stagePath, index) => !owed.includes(stagePath) && stagePaths.indexOf(stagePath) === index
1546
+ );
1547
+ if (added.length === 0) return state;
1548
+ return { ...state, mandatoryStages: [...owed, ...added] };
1549
+ }
1534
1550
  function applySkillInvocation(state, skillName, stagePaths) {
1535
- const merged = [...state.mandatoryStages ?? []];
1536
- for (const stagePath of stagePaths) if (!merged.includes(stagePath)) merged.push(stagePath);
1537
- if (state.lastInvokedSkillName === skillName && merged.length === (state.mandatoryStages?.length ?? 0)) {
1538
- return state;
1539
- }
1540
- return { ...state, lastInvokedSkillName: skillName, mandatoryStages: merged };
1551
+ const declared = applyDeclaredStages(state, stagePaths);
1552
+ if (declared === state && state.lastInvokedSkillName === skillName) return state;
1553
+ return { ...declared, lastInvokedSkillName: skillName };
1541
1554
  }
1542
1555
  function isSkillTreeMarkdown(filePath) {
1543
1556
  const normalized = normalizeStagePath(filePath);
@@ -1589,8 +1602,10 @@ function detectShellStageReads(command, cwd, env = process.env) {
1589
1602
  if (!FILE_PRINTING_PROGRAMS.includes(program)) continue;
1590
1603
  for (const argument of argumentWords) {
1591
1604
  if (argument.startsWith("-") || !MARKDOWN_FILE.test(argument)) continue;
1592
- const readPath = normalizeStagePath(resolveShellPath(argument, workingDirectory));
1593
- if (isSkillTreeMarkdown(readPath) && !readPaths.includes(readPath)) readPaths.push(readPath);
1605
+ const readPath = resolveShellPath(argument, workingDirectory);
1606
+ const readKey = normalizeStagePath(readPath);
1607
+ const isRecorded = readPaths.some((recordedPath) => normalizeStagePath(recordedPath) === readKey);
1608
+ if (isSkillTreeMarkdown(readPath) && !isRecorded) readPaths.push(readPath);
1594
1609
  }
1595
1610
  }
1596
1611
  return readPaths;
@@ -1782,8 +1797,13 @@ function applyStateMutation(state, mutation) {
1782
1797
  return applyE2eDelegation(state, mutation.delegation);
1783
1798
  case "watch-follow-through" /* WatchFollowThrough */:
1784
1799
  return { ...applyWatchHolds(state, mutation.heldPrNumbers), watchFollowThroughBlockCount: mutation.blockCount };
1785
- case "stage-reads" /* StageReads */:
1786
- return mutation.readPaths.reduce(applyStageRead, state);
1800
+ case "stage-reads" /* StageReads */: {
1801
+ const withReads = mutation.readPaths.reduce(applyStageRead, state);
1802
+ const skillsRootDir = mutation.skillsRootDir;
1803
+ if (!skillsRootDir) return withReads;
1804
+ const routeStagePaths = mutation.readPaths.flatMap((readPath) => resolveRouteStagePaths(withReads, readPath, skillsRootDir));
1805
+ return applyDeclaredStages(withReads, routeStagePaths);
1806
+ }
1787
1807
  case "stage-signals" /* StageSignals */:
1788
1808
  return applyStageSignals(state, mutation);
1789
1809
  case "fields" /* Fields */:
@@ -2794,7 +2814,11 @@ function recordStageRead() {
2794
2814
  const readPaths = filePath ? [filePath].filter(isSkillTreeMarkdown) : isShellToolCall(input) ? detectShellStageReads(input.tool_input?.command ?? "", input.cwd) : [];
2795
2815
  if (readPaths.length === 0) return "{}";
2796
2816
  if (callFailed(input)) return "{}";
2797
- commitStateMutation(sessionId, { kind: "stage-reads" /* StageReads */, readPaths }, readState(sessionId));
2817
+ commitStateMutation(
2818
+ sessionId,
2819
+ { kind: "stage-reads" /* StageReads */, readPaths, skillsRootDir: resolvePluginSkillsRoot() },
2820
+ readState(sessionId)
2821
+ );
2798
2822
  return "{}";
2799
2823
  }
2800
2824
  function recordStageSignals() {
@@ -36,6 +36,8 @@ Invoking the skill names those files as required reading, and a Stop gate holds
36
36
 
37
37
  The bar is "must be **read** before the skill runs", not "must be executed". A failure protocol qualifies: by the time a run fails, a skill that never read it is already improvising. What does not qualify is a file only one branch consults for its content — the gate cannot tell that the branch was not taken, so it would block work that legitimately never needed it. Aliases declare nothing; they invoke the canonical skill, which carries the declaration.
38
38
 
39
+ A skill whose router sends runs down different routes declares per route. Its `SKILL.md` lists only what every run opens — the router — and each route's entry file carries its own `mandatoryStages` frontmatter, paths relative to that file. Opening the entry file while the skill runs adds its set; a `SKILL.md` declares only when invoked, never when read. A route lists only the files every run of that route opens, so a branch inside a route still stays out.
40
+
39
41
  ## Host compatibility
40
42
 
41
43
  Skills are written against Claude Code's tools and ship unchanged to Codex. [`_shared/host-tool-equivalents.md`](_shared/host-tool-equivalents.md) maps each Claude Code name a skill uses — `AskUserQuestion`, the `Skill` tool, `${CLAUDE_PLUGIN_ROOT}`, the PR watcher's monitor — to what plays its part in Codex. Every top-level `SKILL.md` links it right after its telemetry line, and the Codex SessionStart hook names it too.
@@ -1,3 +1,9 @@
1
+ ---
2
+ mandatoryStages:
3
+ - respawn-watcher.md
4
+ - ../_shared/telemetry-events/muggle-do-cycle.md
5
+ ---
6
+
1
7
  # Address-Reviews Orchestrator
2
8
 
3
9
  The entry procedure for `/muggle-do`'s **address-reviews** mode — invoked by the watcher loop ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md)) when new submitted reviews land on a PR. Orchestrates the cycle: read reviews → classify → execute work on actionables → escalate ambiguous → push and refresh PR → reply per comment → resolve-reminder → respawn watcher.
@@ -1,3 +1,10 @@
1
+ ---
2
+ mandatoryStages:
3
+ - ../_shared/session-state-writes.md
4
+ - respawn-watcher.md
5
+ - ../_shared/telemetry-events/muggle-do-cycle.md
6
+ ---
7
+
1
8
  # Fix-CI (watcher-dispatched)
2
9
 
3
10
  Resolve red CI on a PR's head — lint/format, typecheck, failing unit tests, and a red Muggle E2E walkthrough check — verifying green before re-push. A dumb-pipe dispatch like address-reviews: the executor receives a PR URL, slug, and the failing check names, and fixes them — it owns the fix, not the decision to dispatch.
@@ -1,3 +1,14 @@
1
+ ---
2
+ mandatoryStages:
3
+ - requirements.md
4
+ - build.md
5
+ - impact-analysis.md
6
+ - unit-tests.md
7
+ - e2e-acceptance.md
8
+ - open-prs/forward.md
9
+ - e2e-repair.md
10
+ ---
11
+
1
12
  # Pre-flight Agent (Stage 1)
2
13
 
3
14
  You are running the **only user-facing stage** of the muggle-do dev cycle. Your job is to consolidate every ambiguity — task scope, repos, validation strategy, environment, credentials, PR target — into a **single turn** so the rest of the cycle can run unattended.
@@ -1,3 +1,10 @@
1
+ ---
2
+ mandatoryStages:
3
+ - ../_shared/session-state-writes.md
4
+ - respawn-watcher.md
5
+ - ../_shared/telemetry-events/muggle-do-cycle.md
6
+ ---
7
+
1
8
  # Rebase (watcher-dispatched)
2
9
 
3
10
  Rebase a PR's branch onto its base — whether it's merely **behind** (out of date, no conflict) or actually **conflicting** — behind a verify-or-rollback gate, then force-push, so a PR doesn't sit stale or un-mergeable forever. A dumb-pipe dispatch like fix-ci: the watcher detects the branch is behind or conflicting and hands off; the executor owns the rebase (and any conflict resolution), never the decision to dispatch.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: muggle-do
3
3
  description: Take a feature request, spec, design, bug, or ticket and carry it autonomously to an opened pull request — write the code, test it, raise the PR. The PR carries real-browser proof: email-gated flows run through live test inboxes, logins through encrypted secrets, and prerequisite flows in test-graph order, with screenshots attached. Use whenever the user wants something built, implemented, added, or fixed with a PR opened, raised, or put up at the end — "implement rate limiting and raise a PR", "build the CSV export and put up a PR", "take this Jira ticket and run with it, ping me when the PR is up", "fix this bug and open a pr", or "muggle do …". Also handles the internal `address-reviews` directive the muggle-pr-followup watcher dispatches when new reviews land. Not for testing or validating existing changes (use muggle-test), nor watching a PR's incoming reviews (use muggle-pr-followup).
4
+ mandatoryStages:
5
+ - ../do/input-routing.md
4
6
  ---
5
7
 
6
8
  # Muggle Test Do
@@ -42,7 +44,7 @@ The pipeline table lists **pointers, not summaries**. Open each stage's file and
42
44
  2. Create `~/.muggle-ai/muggle-do/sessions/<slug>/` with `state.md` + `iterations/001.md` (pre-flight owns this; do it even when running unattended).
43
45
  3. `TodoWrite` one item per stage 1–8, Stage 7.5 included — these stages are the checklist; never swap in your own decomposition. Several changes → one set of stage items per change.
44
46
 
45
- **Per stage:** read the file → execute it → append a marker to `iterations/<NNN>.md` citing the evidence that file requires (jest exit code, E2E verdict + `runId`, screenshot path). A stage is done only when its evidence is written, never on recollection.
47
+ **Per stage:** read the file → execute it → append one marker line to `iterations/<NNN>.md`, always in the form `Stage <N> done — <evidence>`, citing the evidence that file requires (jest exit code, E2E verdict + `runId`, screenshot path). A stage is done only when its evidence is written, never on recollection.
46
48
 
47
49
  ### "Autonomous" / "without my intervention" collapses exactly one thing
48
50
  Best-effort the Stage-1 questionnaire and don't ask. It does **not** license skipping telemetry, session artifacts, requirements, unit tests, E2E (`autoE2ETest` defaults to `always`), browser verification, the gate below, the Stage-7.5 repair loop, or the watcher hand-off. Stage 7.5 investigates and repairs unattended too; only its user decision collapses, into an automatic waive. Run the whole pipeline silently — never a shortcut.
@@ -94,5 +96,6 @@ When invoked with the directive (PR URL + slug + review ids), routes to [`../do/
94
96
 
95
97
  - Stage 1 and Stage 7.5's user decision are the only user-facing forward stages. Stages 2–7 don't ask mid-cycle; blocker → pre-flight bug.
96
98
  - Same stage failing 3× → escalate.
99
+ - Each route's entry file declares the stage files every run of that route opens, and a Stop gate holds the turn until they are read. A turn that ends before its route's last stage — a readiness halt, an escalation, a pause for a background run — records why with `echo "MUGGLE_STAGE_SKIP: <reason>"`.
97
100
  - 3 repair iterations reach E2E with failures → waive, ship with `[E2E FAILING]`, and hand to the watcher.
98
101
  - Address-reviews escalation (ambiguous or design-adjustment) does not block the watcher; user resolves on GitHub.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "release": "5.26.0",
3
- "buildId": "run-243-1",
4
- "commitSha": "794d5f8d369c432b2843204503c101ff1ee71483",
5
- "buildTime": "2026-10-28T04:24:25Z",
3
+ "buildId": "run-245-1",
4
+ "commitSha": "d4e56d068786541b25d22819c6336d5c5f5baf6b",
5
+ "buildTime": "2026-10-29T16:50:53Z",
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.27.0-staging.243",
4
+ "version": "5.27.0-staging.245",
5
5
  "description": "13 years of Microsoft hyperscale engineering, packaged as a harness for your AI coding agent.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
4
- "version": "5.27.0-staging.243",
4
+ "version": "5.27.0-staging.245",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.243",
7
+ "@muggleai/works@5.27.0-staging.245",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.27.0-staging.243",
3
+ "version": "5.27.0-staging.245",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
5
  "author": {
6
6
  "name": "Muggle AI",
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
5
- "version": "5.27.0-staging.243",
5
+ "version": "5.27.0-staging.245",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
package/plugin/.mcp.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.27.0-staging.243",
7
+ "@muggleai/works@5.27.0-staging.245",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -42,7 +42,7 @@ Pre-filters are bash builtins only (`guardrail-lib.sh`), so an irrelevant event
42
42
  | `PostToolUse` (AskUserQuestion) | `guardrail-offer-ran.sh` | record | a next-options offer ran while a terminal PR was pending | — | clear `terminalPending` — the only exit for the post-merge Stop gate |
43
43
  | `PostToolUse` (Agent, Task) | `guardrail-record-delegation.sh` | record | a `muggle:acceptance-tester` was launched into the background — its response carries no finished report yet | — | append to `e2eDelegations` with the PR URLs its brief names; for a bounded window the E2E and debug-path gates hold off, and the walkthrough gate sets those PRs aside, so the session is never steered into a second browser run alongside the agent's. Shares the AskUserQuestion group's dispatch |
44
44
  | `PostToolUse` (Skill + muggle telemetry-skill-emit) | `guardrail-skill-stages.sh` | advise | a skill this plugin ships was invoked; its `SKILL.md` frontmatter may declare `mandatoryStages`. Registered on the skill's own MCP announcement too, so the declaration still lands if the Skill payload ever stops carrying a name. A muggle-test announcement is also an E2E run (the clean SKIP verdict), recorded here alone, so the skill, its stages and the run land in one commit; a write that cannot take the session's lock in time is saved beside the state file and committed by a detached job once the lock frees | — | record `lastInvokedSkillName` + `mandatoryStages` (and `e2eRun` for a muggle-test announcement), and name the declared files as required reading at the moment of use |
45
- | `PostToolUse` (Read) | `guardrail-record-stage-read.sh` | record | a markdown file under a `skills/` directory was opened | — | append to `stagesRead` — how the stage gate tells a stage that was read from one that was skipped |
45
+ | `PostToolUse` (Read) | `guardrail-record-stage-read.sh` | record | a markdown file under a `skills/` directory was opened | — | append to `stagesRead` — how the stage gate tells a stage that was read from one that was skipped; a route entry file in the plugin's skills tree, opened while a skill runs, also adds the `mandatoryStages` its frontmatter declares |
46
46
  | `PostToolUse` (Bash + muggle telemetry-event-emit / user-feedback-create) | `guardrail-record-stage-signals.sh` | record | a Step 6f `pre-execution-classification` emit, debug-path evidence for a failed run (a `*-failure-classified\|resolved` emit or feedback naming it), or a `MUGGLE_STAGE_SKIP` / `MUGGLE_CLASSIFY_SKIP` / `MUGGLE_DEBUG_SKIP` marker | — | set `classifiedTestCaseIds` / `debuggedRuns` / the matching skip flag |
47
47
  | `PostToolUse` (Bash) | `guardrail-record-comment-replies.sh` | record | an unresolved-thread fetch named review threads still awaiting an answer (classified by the `<!-- muggle-do:bot -->` marker, never the author login), a threaded reply the provider **confirmed** answered comments in one, or a `MUGGLE_REPLY_SKIP` marker deferred them | — | claim each thread and cover the comments answered, in the per-PR ledger beside the muggle-do slot |
48
48
  | `PreToolUse` (Bash, `if` a command names `gh`/`glab`) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body — or a `gh api … issues/comments/<id>` PATCH editing one — reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
@@ -4,6 +4,8 @@ set -uo pipefail
4
4
  # stage-read observer (PostToolUse/Read, and PostToolUse/Bash through the
5
5
  # dispatcher). Records that a file in the skills tree was opened, which is how
6
6
  # the Stop gate tells a mandatory stage that was read from one that was skipped.
7
+ # A route's entry file that declares `mandatoryStages` adds that route's stages
8
+ # when it is opened during a skill's run.
7
9
  # A host with no Read tool (Codex) opens stage files through the shell — `cat`,
8
10
  # `sed -n` — so a shell command that prints one counts as a read too. Emits no
9
11
  # directive.
@@ -1464,6 +1464,7 @@ var SEQUENCE_ENTRY = /^\s*-\s*(.+?)\s*$/;
1464
1464
  var FLOW_LIST = /^\s*\[(.*)\]\s*$/;
1465
1465
  var STAGE_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_STAGE_SKIP\b/;
1466
1466
  var SKILLS_DIR_NAME = "skills";
1467
+ var SKILL_ENTRY_FILE_NAME = "SKILL.md";
1467
1468
  var SKILLS_PATH_SEGMENT = `/${SKILLS_DIR_NAME}/`;
1468
1469
  var SINGLE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;
1469
1470
  var SHELL_VARIABLE_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
@@ -1513,17 +1514,15 @@ function resolveSkillNameFromToolInput(toolInput) {
1513
1514
  }
1514
1515
  return void 0;
1515
1516
  }
1516
- function resolveSkillStagePaths(skillName, skillsRootDir) {
1517
- const skillDir = join(skillsRootDir, skillName);
1518
- const skillFile = join(skillDir, "SKILL.md");
1519
- if (!existsSync(skillFile)) return [];
1520
- const normalizedRoot = normalizeStagePath(skillsRootDir);
1521
- const staysInsideSkillsTree = (candidate) => normalizeStagePath(candidate).startsWith(`${normalizedRoot}/`);
1517
+ var isInsideDirectory = (candidatePath, directory) => normalizeStagePath(candidatePath).startsWith(`${normalizeStagePath(directory)}/`);
1518
+ function resolveDeclaredStagePaths(declaringFile, skillsRootDir) {
1519
+ if (!existsSync(declaringFile)) return [];
1520
+ const declaringDir = dirname(declaringFile);
1522
1521
  const resolved = [];
1523
- for (const declared of parseDeclaredStages(readFileSync(skillFile, "utf-8"))) {
1522
+ for (const declared of parseDeclaredStages(readFileSync(declaringFile, "utf-8"))) {
1524
1523
  if (isAbsolute(declared)) continue;
1525
- const stagePath = [resolve(skillDir, declared), resolve(skillsRootDir, declared)].find(
1526
- (candidate) => staysInsideSkillsTree(candidate) && existsSync(candidate)
1524
+ const stagePath = [resolve(declaringDir, declared), resolve(skillsRootDir, declared)].find(
1525
+ (candidate) => isInsideDirectory(candidate, skillsRootDir) && existsSync(candidate)
1527
1526
  );
1528
1527
  if (!stagePath) continue;
1529
1528
  const normalized = normalizeStagePath(stagePath);
@@ -1531,13 +1530,27 @@ function resolveSkillStagePaths(skillName, skillsRootDir) {
1531
1530
  }
1532
1531
  return resolved;
1533
1532
  }
1533
+ function resolveSkillStagePaths(skillName, skillsRootDir) {
1534
+ return resolveDeclaredStagePaths(join(skillsRootDir, skillName, SKILL_ENTRY_FILE_NAME), skillsRootDir);
1535
+ }
1536
+ function resolveRouteStagePaths(state, readPath, skillsRootDir) {
1537
+ if (!state.lastInvokedSkillName) return [];
1538
+ if (basename(readPath).toLowerCase() === SKILL_ENTRY_FILE_NAME.toLowerCase()) return [];
1539
+ if (!isInsideDirectory(readPath, skillsRootDir)) return [];
1540
+ return resolveDeclaredStagePaths(readPath, skillsRootDir);
1541
+ }
1542
+ function applyDeclaredStages(state, stagePaths) {
1543
+ const owed = state.mandatoryStages ?? [];
1544
+ const added = stagePaths.filter(
1545
+ (stagePath, index) => !owed.includes(stagePath) && stagePaths.indexOf(stagePath) === index
1546
+ );
1547
+ if (added.length === 0) return state;
1548
+ return { ...state, mandatoryStages: [...owed, ...added] };
1549
+ }
1534
1550
  function applySkillInvocation(state, skillName, stagePaths) {
1535
- const merged = [...state.mandatoryStages ?? []];
1536
- for (const stagePath of stagePaths) if (!merged.includes(stagePath)) merged.push(stagePath);
1537
- if (state.lastInvokedSkillName === skillName && merged.length === (state.mandatoryStages?.length ?? 0)) {
1538
- return state;
1539
- }
1540
- return { ...state, lastInvokedSkillName: skillName, mandatoryStages: merged };
1551
+ const declared = applyDeclaredStages(state, stagePaths);
1552
+ if (declared === state && state.lastInvokedSkillName === skillName) return state;
1553
+ return { ...declared, lastInvokedSkillName: skillName };
1541
1554
  }
1542
1555
  function isSkillTreeMarkdown(filePath) {
1543
1556
  const normalized = normalizeStagePath(filePath);
@@ -1589,8 +1602,10 @@ function detectShellStageReads(command, cwd, env = process.env) {
1589
1602
  if (!FILE_PRINTING_PROGRAMS.includes(program)) continue;
1590
1603
  for (const argument of argumentWords) {
1591
1604
  if (argument.startsWith("-") || !MARKDOWN_FILE.test(argument)) continue;
1592
- const readPath = normalizeStagePath(resolveShellPath(argument, workingDirectory));
1593
- if (isSkillTreeMarkdown(readPath) && !readPaths.includes(readPath)) readPaths.push(readPath);
1605
+ const readPath = resolveShellPath(argument, workingDirectory);
1606
+ const readKey = normalizeStagePath(readPath);
1607
+ const isRecorded = readPaths.some((recordedPath) => normalizeStagePath(recordedPath) === readKey);
1608
+ if (isSkillTreeMarkdown(readPath) && !isRecorded) readPaths.push(readPath);
1594
1609
  }
1595
1610
  }
1596
1611
  return readPaths;
@@ -1782,8 +1797,13 @@ function applyStateMutation(state, mutation) {
1782
1797
  return applyE2eDelegation(state, mutation.delegation);
1783
1798
  case "watch-follow-through" /* WatchFollowThrough */:
1784
1799
  return { ...applyWatchHolds(state, mutation.heldPrNumbers), watchFollowThroughBlockCount: mutation.blockCount };
1785
- case "stage-reads" /* StageReads */:
1786
- return mutation.readPaths.reduce(applyStageRead, state);
1800
+ case "stage-reads" /* StageReads */: {
1801
+ const withReads = mutation.readPaths.reduce(applyStageRead, state);
1802
+ const skillsRootDir = mutation.skillsRootDir;
1803
+ if (!skillsRootDir) return withReads;
1804
+ const routeStagePaths = mutation.readPaths.flatMap((readPath) => resolveRouteStagePaths(withReads, readPath, skillsRootDir));
1805
+ return applyDeclaredStages(withReads, routeStagePaths);
1806
+ }
1787
1807
  case "stage-signals" /* StageSignals */:
1788
1808
  return applyStageSignals(state, mutation);
1789
1809
  case "fields" /* Fields */:
@@ -2794,7 +2814,11 @@ function recordStageRead() {
2794
2814
  const readPaths = filePath ? [filePath].filter(isSkillTreeMarkdown) : isShellToolCall(input) ? detectShellStageReads(input.tool_input?.command ?? "", input.cwd) : [];
2795
2815
  if (readPaths.length === 0) return "{}";
2796
2816
  if (callFailed(input)) return "{}";
2797
- commitStateMutation(sessionId, { kind: "stage-reads" /* StageReads */, readPaths }, readState(sessionId));
2817
+ commitStateMutation(
2818
+ sessionId,
2819
+ { kind: "stage-reads" /* StageReads */, readPaths, skillsRootDir: resolvePluginSkillsRoot() },
2820
+ readState(sessionId)
2821
+ );
2798
2822
  return "{}";
2799
2823
  }
2800
2824
  function recordStageSignals() {
@@ -36,6 +36,8 @@ Invoking the skill names those files as required reading, and a Stop gate holds
36
36
 
37
37
  The bar is "must be **read** before the skill runs", not "must be executed". A failure protocol qualifies: by the time a run fails, a skill that never read it is already improvising. What does not qualify is a file only one branch consults for its content — the gate cannot tell that the branch was not taken, so it would block work that legitimately never needed it. Aliases declare nothing; they invoke the canonical skill, which carries the declaration.
38
38
 
39
+ A skill whose router sends runs down different routes declares per route. Its `SKILL.md` lists only what every run opens — the router — and each route's entry file carries its own `mandatoryStages` frontmatter, paths relative to that file. Opening the entry file while the skill runs adds its set; a `SKILL.md` declares only when invoked, never when read. A route lists only the files every run of that route opens, so a branch inside a route still stays out.
40
+
39
41
  ## Host compatibility
40
42
 
41
43
  Skills are written against Claude Code's tools and ship unchanged to Codex. [`_shared/host-tool-equivalents.md`](_shared/host-tool-equivalents.md) maps each Claude Code name a skill uses — `AskUserQuestion`, the `Skill` tool, `${CLAUDE_PLUGIN_ROOT}`, the PR watcher's monitor — to what plays its part in Codex. Every top-level `SKILL.md` links it right after its telemetry line, and the Codex SessionStart hook names it too.
@@ -1,3 +1,9 @@
1
+ ---
2
+ mandatoryStages:
3
+ - respawn-watcher.md
4
+ - ../_shared/telemetry-events/muggle-do-cycle.md
5
+ ---
6
+
1
7
  # Address-Reviews Orchestrator
2
8
 
3
9
  The entry procedure for `/muggle-do`'s **address-reviews** mode — invoked by the watcher loop ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md)) when new submitted reviews land on a PR. Orchestrates the cycle: read reviews → classify → execute work on actionables → escalate ambiguous → push and refresh PR → reply per comment → resolve-reminder → respawn watcher.
@@ -1,3 +1,10 @@
1
+ ---
2
+ mandatoryStages:
3
+ - ../_shared/session-state-writes.md
4
+ - respawn-watcher.md
5
+ - ../_shared/telemetry-events/muggle-do-cycle.md
6
+ ---
7
+
1
8
  # Fix-CI (watcher-dispatched)
2
9
 
3
10
  Resolve red CI on a PR's head — lint/format, typecheck, failing unit tests, and a red Muggle E2E walkthrough check — verifying green before re-push. A dumb-pipe dispatch like address-reviews: the executor receives a PR URL, slug, and the failing check names, and fixes them — it owns the fix, not the decision to dispatch.
@@ -1,3 +1,14 @@
1
+ ---
2
+ mandatoryStages:
3
+ - requirements.md
4
+ - build.md
5
+ - impact-analysis.md
6
+ - unit-tests.md
7
+ - e2e-acceptance.md
8
+ - open-prs/forward.md
9
+ - e2e-repair.md
10
+ ---
11
+
1
12
  # Pre-flight Agent (Stage 1)
2
13
 
3
14
  You are running the **only user-facing stage** of the muggle-do dev cycle. Your job is to consolidate every ambiguity — task scope, repos, validation strategy, environment, credentials, PR target — into a **single turn** so the rest of the cycle can run unattended.
@@ -1,3 +1,10 @@
1
+ ---
2
+ mandatoryStages:
3
+ - ../_shared/session-state-writes.md
4
+ - respawn-watcher.md
5
+ - ../_shared/telemetry-events/muggle-do-cycle.md
6
+ ---
7
+
1
8
  # Rebase (watcher-dispatched)
2
9
 
3
10
  Rebase a PR's branch onto its base — whether it's merely **behind** (out of date, no conflict) or actually **conflicting** — behind a verify-or-rollback gate, then force-push, so a PR doesn't sit stale or un-mergeable forever. A dumb-pipe dispatch like fix-ci: the watcher detects the branch is behind or conflicting and hands off; the executor owns the rebase (and any conflict resolution), never the decision to dispatch.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: muggle-do
3
3
  description: Take a feature request, spec, design, bug, or ticket and carry it autonomously to an opened pull request — write the code, test it, raise the PR. The PR carries real-browser proof: email-gated flows run through live test inboxes, logins through encrypted secrets, and prerequisite flows in test-graph order, with screenshots attached. Use whenever the user wants something built, implemented, added, or fixed with a PR opened, raised, or put up at the end — "implement rate limiting and raise a PR", "build the CSV export and put up a PR", "take this Jira ticket and run with it, ping me when the PR is up", "fix this bug and open a pr", or "muggle do …". Also handles the internal `address-reviews` directive the muggle-pr-followup watcher dispatches when new reviews land. Not for testing or validating existing changes (use muggle-test), nor watching a PR's incoming reviews (use muggle-pr-followup).
4
+ mandatoryStages:
5
+ - ../do/input-routing.md
4
6
  ---
5
7
 
6
8
  # Muggle Test Do
@@ -42,7 +44,7 @@ The pipeline table lists **pointers, not summaries**. Open each stage's file and
42
44
  2. Create `~/.muggle-ai/muggle-do/sessions/<slug>/` with `state.md` + `iterations/001.md` (pre-flight owns this; do it even when running unattended).
43
45
  3. `TodoWrite` one item per stage 1–8, Stage 7.5 included — these stages are the checklist; never swap in your own decomposition. Several changes → one set of stage items per change.
44
46
 
45
- **Per stage:** read the file → execute it → append a marker to `iterations/<NNN>.md` citing the evidence that file requires (jest exit code, E2E verdict + `runId`, screenshot path). A stage is done only when its evidence is written, never on recollection.
47
+ **Per stage:** read the file → execute it → append one marker line to `iterations/<NNN>.md`, always in the form `Stage <N> done — <evidence>`, citing the evidence that file requires (jest exit code, E2E verdict + `runId`, screenshot path). A stage is done only when its evidence is written, never on recollection.
46
48
 
47
49
  ### "Autonomous" / "without my intervention" collapses exactly one thing
48
50
  Best-effort the Stage-1 questionnaire and don't ask. It does **not** license skipping telemetry, session artifacts, requirements, unit tests, E2E (`autoE2ETest` defaults to `always`), browser verification, the gate below, the Stage-7.5 repair loop, or the watcher hand-off. Stage 7.5 investigates and repairs unattended too; only its user decision collapses, into an automatic waive. Run the whole pipeline silently — never a shortcut.
@@ -94,5 +96,6 @@ When invoked with the directive (PR URL + slug + review ids), routes to [`../do/
94
96
 
95
97
  - Stage 1 and Stage 7.5's user decision are the only user-facing forward stages. Stages 2–7 don't ask mid-cycle; blocker → pre-flight bug.
96
98
  - Same stage failing 3× → escalate.
99
+ - Each route's entry file declares the stage files every run of that route opens, and a Stop gate holds the turn until they are read. A turn that ends before its route's last stage — a readiness halt, an escalation, a pause for a background run — records why with `echo "MUGGLE_STAGE_SKIP: <reason>"`.
97
100
  - 3 repair iterations reach E2E with failures → waive, ship with `[E2E FAILING]`, and hand to the watcher.
98
101
  - Address-reviews escalation (ambiguous or design-adjustment) does not block the watcher; user resolves on GitHub.