@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.
- package/README.md +58 -1
- package/dist/{chunk-6ILUPFM5.js → chunk-63RAPKGG.js} +280 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/agents/visual-walkthrough-builder.md +3 -3
- package/dist/plugin/hooks/hooks.json +3 -3
- package/dist/plugin/scripts/guardrail-e2e-gate.sh +10 -8
- package/dist/plugin/scripts/guardrails.mjs +154 -10
- package/dist/plugin/skills/do/open-prs/forward.md +7 -3
- package/dist/plugin/skills/do/open-prs/update.md +1 -1
- package/dist/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +2 -2
- package/dist/release-manifest.json +3 -3
- package/package.json +11 -11
- package/plugin/agents/visual-walkthrough-builder.md +3 -3
- package/plugin/hooks/hooks.json +3 -3
- package/plugin/scripts/guardrail-e2e-gate.sh +10 -8
- package/plugin/scripts/guardrails.mjs +154 -10
- package/plugin/skills/do/open-prs/forward.md +7 -3
- package/plugin/skills/do/open-prs/update.md +1 -1
- package/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +2 -2
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
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. **
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
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-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-09-
|
|
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.
|
|
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.
|
|
55
|
+
"electronAppVersion": "1.10.6",
|
|
56
56
|
"downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
|
|
57
|
-
"runtimeTargetDefault": "
|
|
57
|
+
"runtimeTargetDefault": "staging",
|
|
58
58
|
"checksumsByStream": {
|
|
59
59
|
"production": {
|
|
60
|
-
"win32-x64": "
|
|
61
|
-
"linux-x64": "
|
|
62
|
-
"darwin-arm64": "
|
|
63
|
-
"darwin-x64": "
|
|
60
|
+
"win32-x64": "c20acdc2fd7a05039c62f9e5b079e3068d1b3a23a1b3b37b75e2a650b5401de6",
|
|
61
|
+
"linux-x64": "5167eaed4b82fdf57b09c0525437f9c1e0942bc05418a4f993d3b7fc9c807d1f",
|
|
62
|
+
"darwin-arm64": "f3f41d4fcd8dc9669d79215d349e9ec1e3dff82d34dce75b028ab30715530835",
|
|
63
|
+
"darwin-x64": "eeb41783e9c1e6ca8afebd480623ccc194c9aba4349dd52992f0e514fa19f168"
|
|
64
64
|
},
|
|
65
65
|
"staging": {
|
|
66
|
-
"darwin-arm64": "
|
|
67
|
-
"linux-x64": "
|
|
68
|
-
"win32-x64": "
|
|
69
|
-
"darwin-x64": "
|
|
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
|
-
**
|
|
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
|
|
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
|
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -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":
|
|
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
|
|
5
|
-
# acceptance run has happened, offer to
|
|
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
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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 '{}'
|