@muggleai/works 5.17.0 → 5.18.0-staging.118

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.
@@ -12,6 +12,7 @@ var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
12
12
  var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
13
13
  var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
14
14
  var MAX_PR_TERMINAL_BLOCKS = 3;
15
+ var SHELL_TOOL_NAMES = ["Bash", "PowerShell"];
15
16
  var MAX_WATCH_BLOCKS = 3;
16
17
  var MAX_BUILD_BLOCKS = 3;
17
18
  var MAX_WALKTHROUGH_BLOCKS = 3;
@@ -153,13 +154,18 @@ function markPrHandled(sessionId2, prUrl, dirOverride) {
153
154
  writeState(state, dirOverride);
154
155
  }
155
156
 
157
+ // src/guardrails/shellTool.ts
158
+ function isShellToolCall(input2) {
159
+ return SHELL_TOOL_NAMES.includes(input2.tool_name ?? "");
160
+ }
161
+
156
162
  // src/guardrails/prOpened.ts
157
163
  var PR_URL = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+/;
158
164
  var MR_URL = /https?:\/\/[^/\s]+\/[^\s]+\/-\/merge_requests\/\d+/;
159
165
  var CREATE_CMD = /\bgh\s+pr\s+(create|ready)\b/;
160
166
  var MR_CREATE_CMD = /\bglab\s+mr\s+create\b|\bglab\s+mr\s+update\b.*--ready\b/;
161
167
  function detectPrOpened(input2) {
162
- if (input2.tool_name !== "Bash") return null;
168
+ if (!isShellToolCall(input2)) return null;
163
169
  const cmd = input2.tool_input?.command ?? "";
164
170
  if (!CREATE_CMD.test(cmd) && !MR_CREATE_CMD.test(cmd)) return null;
165
171
  const out = `${input2.tool_response?.stdout ?? ""}
@@ -178,7 +184,7 @@ function terminalProvenance(input2) {
178
184
  };
179
185
  }
180
186
  function detectPrTerminal(input2) {
181
- if (input2.tool_name !== "Bash" && input2.tool_name !== "Monitor") return null;
187
+ if (!isShellToolCall(input2) && input2.tool_name !== "Monitor") return null;
182
188
  const response = input2.tool_response;
183
189
  const provenance = terminalProvenance(input2);
184
190
  const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
@@ -200,7 +206,7 @@ function detectPrTerminal(input2) {
200
206
  return null;
201
207
  }
202
208
  function detectPrReopened(input2) {
203
- if (input2.tool_name !== "Bash") return null;
209
+ if (!isShellToolCall(input2)) return null;
204
210
  if (!terminalProvenance(input2).acceptsForgeLine) return null;
205
211
  const response = input2.tool_response;
206
212
  const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
@@ -521,7 +527,8 @@ function isE2ERun(input2) {
521
527
  // src/guardrails/shouldRunE2E.ts
522
528
  var MAX_E2E_BLOCKS = 3;
523
529
  function shouldRunE2E(state) {
524
- return state.unitTestsGreen === true && state.e2eRun !== true && state.e2eSkipped !== true;
530
+ const owed = state.unitTestsGreen === true || state.prsHandled.length > 0;
531
+ return owed && state.e2eRun !== true && state.e2eSkipped !== true;
525
532
  }
526
533
  function applyRecordedRun(state, run) {
527
534
  let next = state;
@@ -603,7 +610,15 @@ function buildFollowthroughDecision(state, maxBlocks = MAX_BUILD_BLOCKS) {
603
610
  }
604
611
  return { action: "block" /* Block */, blockCount: blockCount + 1 };
605
612
  }
613
+
614
+ // src/pr-walkthrough/constants.ts
606
615
  var REPORT_SENTINEL = "muggle-pr-section";
616
+ var WALKTHROUGH_SLOT_MARKER = "<!-- muggle-pr-walkthrough:v1 -->";
617
+ var WALKTHROUGH_SKIPPED_MARKER = "<!-- muggle-pr-walkthrough-status:skipped -->";
618
+ var WALKTHROUGH_COMMENT_HEADING = "### Muggle AI \u2014 PR visual walkthrough";
619
+ var GH_COMMENT_TIMEOUT_MS = 1e4;
620
+
621
+ // src/guardrails/prReportPost.ts
607
622
  var PR_PROSE_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
608
623
  var GH_API_CMD = /\bgh\s+api\b/;
609
624
  var ISSUE_COMMENT_PATH = /\bissues\/comments\/\d+/;
@@ -652,7 +667,7 @@ function isWalkthroughSkipMarker(cmd) {
652
667
  return WALKTHROUGH_SKIP_MARKER.test(cmd);
653
668
  }
654
669
  function detectWalkthroughPost(input2, read = defaultFileReader) {
655
- if (input2.tool_name !== "Bash") return false;
670
+ if (!isShellToolCall(input2)) return false;
656
671
  const cmd = input2.tool_input?.command ?? "";
657
672
  if (!isPrReportPostCommand(cmd)) return false;
658
673
  if (callFailed(input2)) return false;
@@ -715,6 +730,125 @@ function walkthroughGateDecision(state, owedPrUrls, maxBlocks = MAX_WALKTHROUGH_
715
730
  return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedPrUrls };
716
731
  }
717
732
 
733
+ // src/pr-walkthrough/comment.ts
734
+ function renderReservedComment() {
735
+ return `${WALKTHROUGH_SLOT_MARKER}
736
+ ${WALKTHROUGH_COMMENT_HEADING}
737
+
738
+ _Awaiting the E2E acceptance run. Muggle edits this comment in place when the run finishes \u2014 or records why E2E does not apply to this change._`;
739
+ }
740
+ function renderSkippedComment(reason) {
741
+ const stated = reason.trim();
742
+ if (!stated) return renderReservedComment();
743
+ return `${WALKTHROUGH_SLOT_MARKER}
744
+ ${WALKTHROUGH_SKIPPED_MARKER}
745
+ ${WALKTHROUGH_COMMENT_HEADING}
746
+
747
+ **E2E skipped** \u2014 ${stated}`;
748
+ }
749
+ function classifyComment(body) {
750
+ if (body.includes(REPORT_SENTINEL)) return "reported" /* Reported */;
751
+ if (!body.includes(WALKTHROUGH_SLOT_MARKER)) return "not-designated" /* NotDesignated */;
752
+ if (body.includes(WALKTHROUGH_SKIPPED_MARKER)) return "skipped" /* Skipped */;
753
+ return "pending" /* Pending */;
754
+ }
755
+ var GITHUB_PR_URL = /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/;
756
+ var GH_CALLS_ENV = "MUGGLE_GUARDRAIL_GH_CALLS";
757
+ var defaultGhRunner = (args, input2) => {
758
+ if (process.env[GH_CALLS_ENV] === "off") return null;
759
+ try {
760
+ return execFileSync("gh", args, {
761
+ encoding: "utf-8",
762
+ input: input2,
763
+ timeout: GH_COMMENT_TIMEOUT_MS,
764
+ stdio: ["pipe", "pipe", "ignore"]
765
+ });
766
+ } catch {
767
+ return null;
768
+ }
769
+ };
770
+ function parsePrUrl(prUrl) {
771
+ const parts = prUrl.match(GITHUB_PR_URL);
772
+ if (!parts) return null;
773
+ return { repo: `${parts[1]}/${parts[2]}`, prNumber: Number(parts[3]) };
774
+ }
775
+ function listPrComments(pr, run) {
776
+ let raw;
777
+ try {
778
+ raw = run(["api", "--paginate", `repos/${pr.repo}/issues/${pr.prNumber}/comments`]);
779
+ } catch {
780
+ return null;
781
+ }
782
+ if (!raw) return null;
783
+ try {
784
+ const parsed = JSON.parse(raw);
785
+ return Array.isArray(parsed) ? parsed : null;
786
+ } catch {
787
+ return null;
788
+ }
789
+ }
790
+ function writeJson(args, payload, run) {
791
+ try {
792
+ return run(args, JSON.stringify(payload)) !== null;
793
+ } catch {
794
+ return false;
795
+ }
796
+ }
797
+ function postPrComment(pr, body, run) {
798
+ return writeJson(
799
+ ["api", "--method", "POST", `repos/${pr.repo}/issues/${pr.prNumber}/comments`, "--input", "-"],
800
+ { body },
801
+ run
802
+ );
803
+ }
804
+ function patchPrComment(pr, commentId, body, run) {
805
+ return writeJson(
806
+ ["api", "--method", "PATCH", `repos/${pr.repo}/issues/comments/${commentId}`, "--input", "-"],
807
+ { body },
808
+ run
809
+ );
810
+ }
811
+
812
+ // src/pr-walkthrough/reserve.ts
813
+ function commentWithStatus(comments, status) {
814
+ return comments.find((comment) => classifyComment(comment.body) === status);
815
+ }
816
+ function reserveComment(pr, comments, run) {
817
+ const claimed = comments.some(
818
+ (comment) => classifyComment(comment.body) !== "not-designated" /* NotDesignated */
819
+ );
820
+ if (claimed) return false;
821
+ return postPrComment(pr, renderReservedComment(), run);
822
+ }
823
+ function reserveWalkthroughComment(prUrl, run = defaultGhRunner) {
824
+ const pr = parsePrUrl(prUrl);
825
+ if (!pr) return false;
826
+ const comments = listPrComments(pr, run);
827
+ if (comments === null) return false;
828
+ return reserveComment(pr, comments, run);
829
+ }
830
+ function settleWalkthroughCommentAsSkipped(prUrl, reason, run = defaultGhRunner) {
831
+ if (!reason.trim()) return false;
832
+ const pr = parsePrUrl(prUrl);
833
+ if (!pr) return false;
834
+ const comments = listPrComments(pr, run);
835
+ if (comments === null) return false;
836
+ if (commentWithStatus(comments, "reported" /* Reported */)) return false;
837
+ if (commentWithStatus(comments, "skipped" /* Skipped */)) return false;
838
+ const body = renderSkippedComment(reason);
839
+ const reserved = commentWithStatus(comments, "pending" /* Pending */);
840
+ return reserved ? patchPrComment(pr, reserved.id, body, run) : postPrComment(pr, body, run);
841
+ }
842
+
843
+ // src/guardrails/skipReason.ts
844
+ var SKIP_DECLARATION = /^\s*echo\s+["']?MUGGLE_(?:E2E|WALKTHROUGH)_SKIP:\s*(.+)$/;
845
+ function skipReasonFrom(cmd) {
846
+ const declared = cmd.match(SKIP_DECLARATION);
847
+ if (!declared) return null;
848
+ const reason = declared[1].replace(/["']\s*$/, "").trim();
849
+ return reason || null;
850
+ }
851
+
718
852
  // src/guardrails/ledger/constants.ts
719
853
  var LEDGER_FILE_NAME = "comment-ledger.json";
720
854
  var LEDGER_VERSION = 1;
@@ -874,14 +1008,14 @@ function gitlabThread(thread) {
874
1008
  };
875
1009
  }
876
1010
  function detectUnansweredThreads(input2) {
877
- if (input2.tool_name !== "Bash") return [];
1011
+ if (!isShellToolCall(input2)) return [];
878
1012
  if (!REVIEW_THREAD_FETCH_COMMAND.test(input2.tool_input?.command ?? "")) return [];
879
1013
  const threads = [];
880
1014
  collectThreads(parsedResponse(input2), threads);
881
1015
  return threads.map((thread) => Array.isArray(thread.notes) ? gitlabThread(thread) : githubThread(thread)).filter((thread) => thread !== void 0);
882
1016
  }
883
1017
  function detectConfirmedReplies(input2) {
884
- if (input2.tool_name !== "Bash") return [];
1018
+ if (!isShellToolCall(input2)) return [];
885
1019
  const command = input2.tool_input?.command ?? "";
886
1020
  const targets = [...command.matchAll(THREADED_REPLY_TARGET)].map(
887
1021
  ([, githubCommentId, gitlabDiscussionId]) => githubCommentId ?? gitlabDiscussionId
@@ -976,7 +1110,7 @@ function looksLikeE2EReport(text) {
976
1110
  return resultsStructure && muggleContext;
977
1111
  }
978
1112
  function evaluateReportPost(input2, read = defaultFileReader) {
979
- if (input2.tool_name !== "Bash") return { deny: false };
1113
+ if (!isShellToolCall(input2)) return { deny: false };
980
1114
  const cmd = input2.tool_input?.command ?? "";
981
1115
  if (!isPrReportPostCommand(cmd)) return { deny: false };
982
1116
  const text = collectPrPostText(cmd, input2.cwd, read);
@@ -997,7 +1131,7 @@ function detectResolveCall(command) {
997
1131
  }
998
1132
  var RESOLVE_DENIAL = "Blocked: resolving a review thread is the reviewer's call, not the loop's. Reply to the thread instead \u2014 the `<!-- muggle-do:bot -->` marker on that reply is what retires it (a thread is actionable only while it is unresolved AND its newest comment is unmarked), so resolving buys no echo protection and costs the reviewer their record of what is still unverified. The loop has twice hidden threads carrying fixes that were partly wrong. If a thread genuinely warrants resolving, say so and let the reviewer close it in the UI; to nudge, run the resolve-reminder stage, which lists addressed-but-open threads without touching them.";
999
1133
  function evaluateReviewThreadResolve(input2) {
1000
- if (input2.tool_name !== "Bash") return { deny: false };
1134
+ if (!isShellToolCall(input2)) return { deny: false };
1001
1135
  const provider = detectResolveCall(input2.tool_input?.command ?? "");
1002
1136
  if (!provider) return { deny: false };
1003
1137
  return { deny: true, reason: RESOLVE_DENIAL };
@@ -1052,7 +1186,9 @@ function prOpened() {
1052
1186
  if (!url) return "{}";
1053
1187
  if (readState(sessionId).prsHandled.includes(url)) return "{}";
1054
1188
  markPrHandled(sessionId, url);
1189
+ reserveWalkthroughComment(url);
1055
1190
  const ctx = `A pull request was just opened: ${url}
1191
+ Its Muggle AI visual-walkthrough comment is reserved and empty \u2014 settle it by posting the walkthrough there once E2E runs, or by recording why E2E does not apply.
1056
1192
  Per the autoWatchPR preference, a muggle-pr-followup watcher should handle its incoming reviews. If autoWatchPR=always, start it now by invoking /muggle:muggle-pr-followup with the PR URL; if =ask, offer it to the user; if =never, do nothing.`;
1057
1193
  return envelope("PostToolUse", ctx, host);
1058
1194
  }
@@ -1106,8 +1242,16 @@ function recordTests() {
1106
1242
  const failedRunId = detectFailedRunId(input);
1107
1243
  const next = failedRunId ? applyFailedRun(withWalkthroughSkip, failedRunId) : withWalkthroughSkip;
1108
1244
  if (next !== state) writeState(next);
1245
+ recordSkipReasonOnPrs(next, cmd);
1109
1246
  return "{}";
1110
1247
  }
1248
+ function recordSkipReasonOnPrs(state, cmd) {
1249
+ const reason = skipReasonFrom(cmd);
1250
+ if (!reason) return;
1251
+ for (const prUrl of state.prsHandled) {
1252
+ settleWalkthroughCommentAsSkipped(prUrl, reason);
1253
+ }
1254
+ }
1111
1255
  function skillStages() {
1112
1256
  const skillName = resolveSkillNameFromToolInput(input.tool_input);
1113
1257
  if (!skillName) return "{}";
@@ -1185,7 +1329,7 @@ function e2eGate() {
1185
1329
  if (decision.action === "none" /* None */) return "{}";
1186
1330
  state.e2eBlockCount = decision.blockCount;
1187
1331
  writeState(state);
1188
- const reason = decision.blockCount === 1 ? `Do not end the turn yet. Unit tests passed this session but no E2E acceptance run has happened. Per the autoE2ETest preference (default: always), run change-driven E2E now via /muggle:muggle-test, then finish. If E2E genuinely cannot run here (no app to drive, services down, no PR), tell the user why and run \`echo "MUGGLE_E2E_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `E2E acceptance run still owed (reminder ${decision.blockCount}/${MAX_E2E_BLOCKS}): run /muggle:muggle-test, or record a legitimate skip via \`echo "MUGGLE_E2E_SKIP: <reason>"\`.`;
1332
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. This session went unit-green or opened a PR, but no E2E acceptance run has happened. Per the autoE2ETest preference (default: always), run change-driven E2E now via /muggle:muggle-test, then finish. If E2E genuinely cannot run here (no app to drive, services down, no PR), tell the user why and run \`echo "MUGGLE_E2E_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `E2E acceptance run still owed (reminder ${decision.blockCount}/${MAX_E2E_BLOCKS}): run /muggle:muggle-test, or record a legitimate skip via \`echo "MUGGLE_E2E_SKIP: <reason>"\`.`;
1189
1333
  return blockStop(reason, host);
1190
1334
  }
1191
1335
  function watchGate() {
@@ -32,14 +32,18 @@ Forward pipeline's Stage 7. Invoked by `/muggle-do` after stages 1–6 of a fres
32
32
  - `## Acceptance Criteria` — bulleted; omit if empty.
33
33
  - `## Changes` — summary of what changed in this repo.
34
34
  - `## Validation` — one line: link to E2E report, `unit-only`, or `skip — <reason>`.
35
- - **Walkthrough block** — only when an E2E report exists. Fire [`postPRVisualWalkthrough`](../../muggle-preferences/preference-gates/postPRVisualWalkthrough.md); on skip, omit this block. Otherwise invoke [`../../muggle-pr-visual-walkthrough/SKILL.md`](../../muggle-pr-visual-walkthrough/SKILL.md) Mode B and embed the returned `body` verbatim. No report → skip the block.
35
+ - The walkthrough does **not** go in the body. It settles the PR's designated walkthrough comment in Step 6, so a rerun updates one comment instead of rewriting the description.
36
36
  - **Signature** — write the assembled body to a file and sign it with `--command /muggle-do --mode editable` per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md). The signature lands last, after the walkthrough block; `editable` is the mode a description carries so later refreshes replace it instead of stacking.
37
37
 
38
38
  5. **Create:** resolve the provider per [`../../_shared/vcs/detect-vcs.md`](../../_shared/vcs/detect-vcs.md).
39
39
  - `github` → `gh pr create --title "..." --body-file <signed-file> --head <branch>`, passing the file signed in Step 4. Capture the PR URL and number.
40
40
  - `gitlab` → open the change via [`../../_shared/vcs/gitlab/mr-create.md`](../../_shared/vcs/gitlab/mr-create.md): `glab mr create --source-branch <branch> --target-branch <base> --title "..." --description "..."`. Capture the MR URL and iid.
41
41
 
42
- 6. **Overflow comment:** if the walkthrough skill returned a non-null `comment`, post it once using the provider resolved in Step 5 — `github` per [`../../_shared/vcs/github/top-level-comment.md`](../../_shared/vcs/github/top-level-comment.md), `gitlab` per [`../../_shared/vcs/gitlab/mr-note.md`](../../_shared/vcs/gitlab/mr-note.md). End the posted body with the signature line (command `/muggle-do`) per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md). Never post when `comment` is `null`.
42
+ 6. **Settle the designated walkthrough comment.** Creating the PR reserves one comment for the walkthrough, marked `muggle-pr-walkthrough` and empty until settled — the PR's walkthrough check fails while it stays that way.
43
+ - E2E report exists → fire [`postPRVisualWalkthrough`](../../muggle-preferences/preference-gates/postPRVisualWalkthrough.md); on skip, leave the comment to the gate's own skip record. Otherwise invoke [`../../muggle-pr-visual-walkthrough/SKILL.md`](../../muggle-pr-visual-walkthrough/SKILL.md) Mode A with the PR number — it fills the reserved comment in place.
44
+ - No E2E report (validation was `unit-only` or `skip`) → state the reason on the PR with `echo "MUGGLE_E2E_SKIP: <reason>"`, which settles the comment as a declared skip. A PR whose title says `[UNIT-ONLY]` still owes reviewers the why.
45
+
46
+ 7. **Overflow comment:** if the walkthrough skill returned a non-null `comment`, post it once using the provider resolved in Step 5 — `github` per [`../../_shared/vcs/github/top-level-comment.md`](../../_shared/vcs/github/top-level-comment.md), `gitlab` per [`../../_shared/vcs/gitlab/mr-note.md`](../../_shared/vcs/gitlab/mr-note.md). End the posted body with the signature line (command `/muggle-do`) per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md). Never post when `comment` is `null`.
43
47
 
44
48
  ## Stage 7.5 gate
45
49
 
@@ -68,7 +72,7 @@ If `prs.json` is empty, **do not dispatch** — record the reason in `result.md`
68
72
 
69
73
  ## Invariants
70
74
 
71
- - Branch synced with its base before the push, never after; PR creation per non-skipped repo; walkthrough block via Mode B; `prs.json`+`last_seen.json` seeded (no `cycle.json`, no `requirements.md`); Stage 7.5 cleared before the dispatch; `/loop` dispatch is the last action.
75
+ - Branch synced with its base before the push, never after; PR creation per non-skipped repo; designated walkthrough comment settled via Mode A, or its skip reason stated; `prs.json`+`last_seen.json` seeded (no `cycle.json`, no `requirements.md`); Stage 7.5 cleared before the dispatch; `/loop` dispatch is the last action.
72
76
 
73
77
  ## Output
74
78
 
@@ -40,7 +40,7 @@ Resolve the provider once per [`../../_shared/vcs/detect-vcs.md`](../../_shared/
40
40
 
41
41
  4. **Refresh body when validation outcome changed** — only when the `## Validation` section's content differs from what's in the body. Use the `--body-file` form in [`../../_shared/vcs/github/pr-edit.md`](../../_shared/vcs/github/pr-edit.md). Preserve `## Goal` and `## Acceptance Criteria` verbatim. Re-stamp the signature: delete the existing block from the `<!-- muggle-works:signature -->` marker to the end, then append the editable-body signature (command `/muggle-do`) as the last thing in the body per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md) — this keeps exactly one signature across refreshes.
42
42
 
43
- 5. **Visual walkthrough comment** — only when an E2E report exists. Fire [`postPRVisualWalkthrough`](../../muggle-preferences/preference-gates/postPRVisualWalkthrough.md) (PR number from `prs.json`); on skip, record `skipped (gate)` and continue. Otherwise invoke [`../../muggle-pr-visual-walkthrough/SKILL.md`](../../muggle-pr-visual-walkthrough/SKILL.md) Mode A — a fresh comment per cycle; do not edit prior walkthrough comments.
43
+ 5. **Visual walkthrough comment** — only when an E2E report exists. Fire [`postPRVisualWalkthrough`](../../muggle-preferences/preference-gates/postPRVisualWalkthrough.md) (PR number from `prs.json`); on skip, record `skipped (gate)` and continue. Otherwise invoke [`../../muggle-pr-visual-walkthrough/SKILL.md`](../../muggle-pr-visual-walkthrough/SKILL.md) Mode A, which settles the PR's designated walkthrough comment in place — one walkthrough per PR, always reflecting the latest run.
44
44
 
45
45
  6. **Overflow comment** — same rule as forward mode: post when the walkthrough skill returns non-null `comment`, via [`../../_shared/vcs/github/top-level-comment.md`](../../_shared/vcs/github/top-level-comment.md). End the posted body with the signature line (command `/muggle-do`) per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md).
46
46
 
@@ -18,7 +18,7 @@ This is the **canonical PR-walkthrough workflow** shared across every Muggle Tes
18
18
  | :--- | :--- | :--- |
19
19
  | `muggle-test` | **Mode A** (post to existing PR) | After publishing results, user opts in via `AskUserQuestion` |
20
20
  | `muggle-test-feature-local` | **Mode A** (post to existing PR) | After publishing the run, user opts in via `AskUserQuestion` |
21
- | `muggle-do` / `open-prs.md` | **Mode B** (render-only for embedding) | During PR creation — caller embeds `body` in the PR create call and posts `comment` as follow-up |
21
+ | `muggle-do` / `open-prs.md` | **Mode A** (settle the designated comment) | Right after PR creation — the walkthrough fills the comment the PR reserved for it, and `comment` posts as follow-up |
22
22
  | `muggle-test` Mode C / `acceptance-tester` agent | **Mode C** (embed in verdict comment) | Inside an open-PR sweep orchestrator — caller folds the rendered body into a single per-PR verdict comment |
23
23
 
24
24
  ## Preferences
@@ -27,7 +27,7 @@ Callers consult the `postPRVisualWalkthrough` gate **before** invoking this skil
27
27
 
28
28
  ## Procedure
29
29
 
30
- 1. **Resolve the mode.** Chosen by the caller, never the user: top-level `muggle-test`/`muggle-test-feature-local` → `post` (Mode A); `muggle-do` PR creation → `render-for-new-pr` (Mode B); an orchestrator passing `mode: "embed"` → Mode C.
30
+ 1. **Resolve the mode.** Chosen by the caller, never the user: top-level `muggle-test`/`muggle-test-feature-local` and `muggle-do` PR creation → `post` (Mode A); a caller that needs the block rendered without posting → `render-for-new-pr` (Mode B); an orchestrator passing `mode: "embed"` → Mode C.
31
31
  2. **Mode A only — find the PR** with `gh pr view --json number,url,title`. No PR on the branch → `AskUserQuestion`: create a new PR with the walkthrough in the body (switch to Mode B and hand the rendered block back to the caller), or skip posting. `gh` missing/unauthenticated → tell the user, suggest `gh auth login`, stop. This is the skill's only interactive branch — resolve it **before** dispatching.
32
32
  3. **Gather the inputs.** The `E2eReport` JSON if the caller already assembled it (see [`e2e-report-assembly.md`](e2e-report-assembly.md)), else the run identifiers (`projectId`, per-test `runId`/`testCaseId`) the agent needs to assemble it.
33
33
  4. **Dispatch** the `visual-walkthrough-builder` agent (subagent type `muggle:visual-walkthrough-builder`; bare `visual-walkthrough-builder` where the plugin namespace is absent), synchronously, passing: mode, PR number + repo (Mode A), and the report JSON or identifiers. In a harness with no agent/subagent facility, execute `plugin/agents/visual-walkthrough-builder.md` inline instead.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "release": "5.13.1",
3
- "buildId": "run-116-1",
4
- "commitSha": "4739cd18232aea59096ce70e569d1abf4674dc5c",
5
- "buildTime": "2026-09-16T19:07:37Z",
3
+ "buildId": "run-118-1",
4
+ "commitSha": "f5fd018bf455519cab67f84dc8840339e1c4a061",
5
+ "buildTime": "2026-09-18T06:00:32Z",
6
6
  "serviceName": "muggle-ai-works-mcp"
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@muggleai/works",
3
3
  "mcpName": "io.github.multiplex-ai/muggle",
4
- "version": "5.17.0",
4
+ "version": "5.18.0-staging.118",
5
5
  "description": "Ship quality products with AI-powered E2E acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -52,21 +52,21 @@
52
52
  "typecheck:browser-bench": "tsc --noEmit -p internal/browser-bench/tsconfig.json"
53
53
  },
54
54
  "muggleConfig": {
55
- "electronAppVersion": "1.10.3",
55
+ "electronAppVersion": "1.10.6",
56
56
  "downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
57
- "runtimeTargetDefault": "production",
57
+ "runtimeTargetDefault": "staging",
58
58
  "checksumsByStream": {
59
59
  "production": {
60
- "win32-x64": "58e2238b1609ceff65b836b4b724b1c4c341d737a15debaa8a71af21b70bc436",
61
- "linux-x64": "731c2ab093ef77202191d0fec8e98f19ef895019d3e7817bacef197c7197a358",
62
- "darwin-arm64": "a3f8229678d4c6cd7514726b480fa97dc9dffed7ac45c2ff76e6abf220e1709f",
63
- "darwin-x64": "b193c2fc02174636bee3fbb3b1041b301639315f2af55fb0352f09e7454a5694"
60
+ "win32-x64": "c20acdc2fd7a05039c62f9e5b079e3068d1b3a23a1b3b37b75e2a650b5401de6",
61
+ "linux-x64": "5167eaed4b82fdf57b09c0525437f9c1e0942bc05418a4f993d3b7fc9c807d1f",
62
+ "darwin-arm64": "f3f41d4fcd8dc9669d79215d349e9ec1e3dff82d34dce75b028ab30715530835",
63
+ "darwin-x64": "eeb41783e9c1e6ca8afebd480623ccc194c9aba4349dd52992f0e514fa19f168"
64
64
  },
65
65
  "staging": {
66
- "darwin-arm64": "0769b8b146b393fbf582b995a1bf7d2e1b9a8ee8fbc08a2108ad9be283291d37",
67
- "linux-x64": "5bdf0e98f3eb95c6cd9461cbddefa19e5361354f3faefaba7bead9ea4feabca7",
68
- "win32-x64": "d029700622445ab2dab931b2cd206922cf20cf16137ccc7f164ef5c195b45ac5",
69
- "darwin-x64": "db29f8fbddd4e1f1f3098aca33e3ecf6ea1a0647de6d17a0ac809a09524e7464"
66
+ "darwin-arm64": "58fab257672df1fe34c9bd787a4892c6bf4a62dc930794bd5b51b2491080967d",
67
+ "linux-x64": "e2daf1ac47d33fb7a3c02b59abf0a29eaa9e346966fc21cd5ffa3dbd62494244",
68
+ "win32-x64": "978328942eea108396917e9a0796141b588983118c45034c97f3db6431637ddc",
69
+ "darwin-x64": "8892a29a91b318811012c1d74bb3f5668ed4481ce1e739e7283cb783e0b54e54"
70
70
  }
71
71
  },
72
72
  "electronAppSignedFromVersion": "1.10.0",
@@ -33,18 +33,18 @@ echo "$REPORT_JSON" | muggle build-pr-section > /tmp/muggle-pr-section.json
33
33
 
34
34
  **Mode A (`post`)** — deliver `body`, then `comment` only if non-null. Sign each posted body per [`../skills/_shared/vcs/post-signature.md`](../skills/_shared/vcs/post-signature.md) with `--mode plain` — this post is the walkthrough's own, so the command it names is `/muggle-pr-visual-walkthrough`.
35
35
 
36
- **Update in place when this PR already carries a walkthrough.** A rerun after a failure must leave the PR with **one** walkthrough reflecting latest state, not a comment per attempt. Resolve which comment to update by reading the PR — never by remembering an id — so the behavior is idempotent across sessions and survives a lost session or a forgotten handle:
36
+ **Settle the designated comment.** Every PR carries one comment reserved for this walkthrough — posted the moment the PR opened, marked `muggle-pr-walkthrough`, and empty until a run settles it. Fill that comment rather than adding another: a rerun after a failure must leave the PR with **one** walkthrough reflecting latest state, and a fresh post would strand the reserved slot pending, which is what the PR's walkthrough check fails on. Resolve which comment to fill by reading the PR — never by remembering an id — so the behavior is idempotent across sessions and survives a lost session or a forgotten handle:
37
37
 
38
38
  ```bash
39
39
  sign() { bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-pr-visual-walkthrough --mode plain; }
40
40
  existing=$(gh api "repos/<owner>/<repo>/issues/<prNumber>/comments" \
41
- --jq '[.[] | select(.body | contains("muggle-pr-section")) | .id] | join(" ")')
41
+ --jq '[.[] | select(.body | contains("muggle-pr-section") or contains("muggle-pr-walkthrough")) | .id] | join(" ")')
42
42
  ```
43
43
 
44
44
  - `existing` empty → post fresh: `jq -r '.body' … | sign | gh pr comment <prNumber> --body-file -`, then the same for `.comment` when non-null.
45
45
  - `existing` non-empty → update the first id with `body` via `gh api --method PATCH repos/<owner>/<repo>/issues/comments/<id> -F body=@-`, feeding the same signed text on stdin. Handle `comment` against the second id when both exist; post it fresh when the overflow is new, and delete a now-surplus overflow comment (`gh api --method DELETE …`) so a stale tail never outlives the run it described.
46
46
 
47
- Match only comments carrying the sentinel — never every comment the loop user wrote — so an unrelated reply is never overwritten.
47
+ Match only comments carrying one of those markers — never every comment the loop user wrote — so an unrelated reply is never overwritten.
48
48
 
49
49
  Report back: PR URL, whether an overflow comment was involved, and whether this was a fresh post or an update.
50
50
 
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "PreToolUse": [
29
29
  {
30
- "matcher": "Bash",
30
+ "matcher": "Bash|PowerShell",
31
31
  "hooks": [
32
32
  {
33
33
  "type": "command",
@@ -57,13 +57,13 @@
57
57
  ],
58
58
  "PostToolUse": [
59
59
  {
60
- "matcher": "Bash",
60
+ "matcher": "Bash|PowerShell",
61
61
  "hooks": [
62
62
  {
63
63
  "type": "command",
64
64
  "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-pr-opened.sh\"",
65
65
  "async": false,
66
- "timeout": 10
66
+ "timeout": 25
67
67
  },
68
68
  {
69
69
  "type": "command",
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env bash
2
2
  set -uo pipefail
3
3
 
4
- # tests-green → E2E gate (Stop). When unit tests passed this session and no E2E
5
- # acceptance run has happened, offer to run change-driven E2E (gated by
6
- # autoE2ETest). Fires once per session.
4
+ # tests-green-or-PR-opened → E2E gate (Stop). When unit tests passed this
5
+ # session or a PR was opened, and no E2E acceptance run has happened, offer to
6
+ # run change-driven E2E (gated by autoE2ETest). Fires once per session.
7
7
  #
8
8
  # This must stay synchronous (only a sync Stop hook can block the turn end), and
9
9
  # it fires on EVERY turn end. There is no command payload to key off, so the
10
10
  # pre-filter reads the same per-session state file guardrails.mjs uses and only
11
- # spawns Node when the gate could actually fire — i.e. shouldRunE2E: unit tests
12
- # went green and no E2E run is recorded yet. On the overwhelming majority of
13
- # turns (no test run this session) the state file is absent or unitTestsGreen is
14
- # unset, so we return {} in-shell and never pay Node cold-start. Degrades to {}.
11
+ # spawns Node when the gate could actually fire — i.e. shouldRunE2E's two
12
+ # triggers, with no E2E run recorded yet. It must track that predicate exactly:
13
+ # a pre-filter narrower than the gate retires the gate silently. On the
14
+ # overwhelming majority of turns (no test run, no PR) the state file is absent,
15
+ # or unitTestsGreen is unset while prsHandled is empty, so we return {} in-shell
16
+ # and never pay Node cold-start. Degrades to {}.
15
17
  payload="$(cat)"
16
18
 
17
19
  raw_sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/')"
@@ -28,7 +30,7 @@ fi
28
30
 
29
31
  state_file="$home/.muggle-ai/guardrails/$sid.json"
30
32
  if [ ! -f "$state_file" ] \
31
- || ! grep -q '"unitTestsGreen": true' "$state_file" \
33
+ || { ! grep -q '"unitTestsGreen": true' "$state_file" && grep -q '"prsHandled": \[\]' "$state_file"; } \
32
34
  || grep -q '"e2eReleased": true' "$state_file" \
33
35
  || grep -q '"e2eRun": true' "$state_file"; then
34
36
  printf '{}'