@muggleai/works 5.18.0-staging.117 → 5.18.0-staging.119

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 CHANGED
@@ -387,7 +387,7 @@ muggle serve --e2e # Cloud E2E tools only (muggle-remote-*)
387
387
  muggle serve --local # Local E2E tools only (muggle-local-*)
388
388
 
389
389
  # Setup and Diagnostics
390
- muggle init # First-run walkthrough: explains Muggle Test, saves preferences
390
+ muggle init # First-run walkthrough: preferences, then offers the CI check
391
391
  muggle init --json # Emit the walkthrough for another front-end to render
392
392
  muggle setup # Download/update browser test runner
393
393
  muggle setup --force # Force re-download
@@ -398,6 +398,10 @@ muggle login # Manually trigger login
398
398
  muggle logout # Clear credentials
399
399
  muggle status # Show auth status
400
400
 
401
+ # Pull request checks
402
+ muggle ci-install # Add the walkthrough check to this repo's GitHub Actions
403
+ muggle ci-install --force # Replace an existing workflow file
404
+
401
405
  # Info
402
406
  muggle --version # Show version
403
407
  muggle --help # Show help
@@ -405,6 +409,59 @@ muggle --help # Show help
405
409
 
406
410
  ---
407
411
 
412
+ ## Pull request walkthrough check
413
+
414
+ When you open a pull request from a Claude session running this plugin, Muggle reserves a comment on it for the E2E visual walkthrough and holds the turn open until that comment is settled — by the walkthrough itself, or by a stated reason E2E does not apply.
415
+
416
+ A pull request opened any other way — the GitHub web UI, a teammate without the plugin — never passes through that session, so the check also runs in GitHub Actions.
417
+
418
+ `muggle init` asks whether you want it and installs it for you. To add it to another repository later:
419
+
420
+ ```bash
421
+ muggle ci-install
422
+ ```
423
+
424
+ That creates `.github/workflows/muggle-walkthrough.yml`. To add it by hand instead, create that file with:
425
+
426
+ <!-- muggle:ci-workflow-snippet -->
427
+ ```yaml
428
+ name: muggle-walkthrough
429
+
430
+ on:
431
+ pull_request:
432
+ types: [opened, synchronize, reopened, ready_for_review]
433
+ issue_comment:
434
+ types: [created, edited]
435
+
436
+ permissions:
437
+ contents: read
438
+ checks: write
439
+ pull-requests: write
440
+
441
+ concurrency:
442
+ group: muggle-walkthrough-${{ github.event.pull_request.number || github.event.issue.number }}
443
+ cancel-in-progress: true
444
+
445
+ jobs:
446
+ walkthrough-comment:
447
+ if: github.event_name == 'pull_request' || github.event.issue.pull_request
448
+ runs-on: ubuntu-latest
449
+ steps:
450
+ - name: Check the walkthrough comment
451
+ env:
452
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
453
+ run: npx -y -p @muggleai/works muggle pr-walkthrough-check --check-run
454
+ ```
455
+ <!-- /muggle:ci-workflow-snippet -->
456
+
457
+ Three things worth knowing:
458
+
459
+ - The verdict is published as a check run against the pull request's head commit, not as this job's own status. An `issue_comment` run executes against the default branch, and only a head-commit check reaches the pull request from there — which is what lets settling the comment turn the check green without a new push.
460
+ - GitHub reads the `issue_comment` trigger from the **default branch's** copy of the workflow, so comment-driven re-runs start working only once it is merged.
461
+ - On a pull request from a fork, `GITHUB_TOKEN` is read-only and the check run cannot be published. The command fails open and reports nothing rather than blocking the pull request.
462
+
463
+ ---
464
+
408
465
  ## Setup and Configuration
409
466
 
410
467
  Authentication happens automatically when you first use a tool that requires it: a browser window opens with a verification code, you log in with your Muggle AI account, and the tool call continues. Credentials persist across sessions in `~/.muggle-ai/`.
@@ -13,7 +13,7 @@ import { Command } from 'commander';
13
13
  import axios from 'axios';
14
14
  import { platform, homedir, arch } from 'os';
15
15
  import { createInterface } from 'readline/promises';
16
- import { execFile } from 'child_process';
16
+ import { execFile, execFileSync } from 'child_process';
17
17
  import { pipeline } from 'stream/promises';
18
18
  import { readFile } from 'fs/promises';
19
19
  import { verify } from 'sigstore';
@@ -893,6 +893,81 @@ async function buildPrSectionCommand(options) {
893
893
  process.exitCode = code;
894
894
  }
895
895
  }
896
+
897
+ // src/ci-workflow/constants.ts
898
+ var USER_WORKFLOW_PATH = ".github/workflows/muggle-walkthrough.yml";
899
+ var USER_WORKFLOW_COMMAND = "npx -y -p @muggleai/works muggle pr-walkthrough-check --check-run";
900
+
901
+ // src/ci-workflow/template.ts
902
+ function renderUserWorkflow() {
903
+ return `name: muggle-walkthrough
904
+
905
+ # Every PR owes a settled Muggle AI walkthrough comment: the E2E acceptance
906
+ # result, or a stated reason E2E does not apply. The verdict rides a check run
907
+ # against the PR head SHA rather than this job's own status, because the
908
+ # issue_comment trigger runs against the default branch \u2014 only a head-SHA check
909
+ # reaches the PR from there, which is what lets settling the comment turn the
910
+ # check green with no new push.
911
+ #
912
+ # GitHub reads the issue_comment trigger from the default branch's copy of this
913
+ # file, so comment-driven re-runs only start working once this is merged.
914
+ #
915
+ # On a pull request from a fork, GITHUB_TOKEN is read-only and the check run
916
+ # cannot be published; the command fails open and reports nothing.
917
+ on:
918
+ pull_request:
919
+ types: [opened, synchronize, reopened, ready_for_review]
920
+ issue_comment:
921
+ types: [created, edited]
922
+
923
+ permissions:
924
+ contents: read
925
+ checks: write
926
+ pull-requests: write
927
+
928
+ concurrency:
929
+ group: muggle-walkthrough-\${{ github.event.pull_request.number || github.event.issue.number }}
930
+ cancel-in-progress: true
931
+
932
+ jobs:
933
+ walkthrough-comment:
934
+ # issue_comment fires for plain issues too; only PR conversations matter.
935
+ if: github.event_name == 'pull_request' || github.event.issue.pull_request
936
+ runs-on: ubuntu-latest
937
+ steps:
938
+ - name: Check the walkthrough comment
939
+ env:
940
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
941
+ run: ${USER_WORKFLOW_COMMAND}
942
+ `;
943
+ }
944
+
945
+ // src/cli/ci-install.ts
946
+ function installCiWorkflow(options) {
947
+ const workflowPath = join(options.cwd, USER_WORKFLOW_PATH);
948
+ if (!existsSync(join(options.cwd, ".git"))) {
949
+ return { outcome: "not-a-repository" /* NotARepository */, workflowPath };
950
+ }
951
+ if (existsSync(workflowPath) && !options.force) {
952
+ return { outcome: "already-present" /* AlreadyPresent */, workflowPath };
953
+ }
954
+ mkdirSync(dirname(workflowPath), { recursive: true });
955
+ writeFileSync(workflowPath, renderUserWorkflow());
956
+ return { outcome: "installed" /* Installed */, workflowPath };
957
+ }
958
+ var MESSAGES = {
959
+ ["installed" /* Installed */]: (path6) => `Wrote ${path6}
960
+ Commit it, and every pull request here will owe a settled Muggle AI walkthrough comment.`,
961
+ ["already-present" /* AlreadyPresent */]: (path6) => `${path6} already exists \u2014 left untouched. Re-run with --force to replace it.`,
962
+ ["not-a-repository" /* NotARepository */]: () => "Not a git repository \u2014 run this from the repo that should carry the check."
963
+ };
964
+ function ciInstallCommand(flags) {
965
+ const report = installCiWorkflow({ cwd: process.cwd(), force: flags.force === true });
966
+ const render = MESSAGES[report.outcome];
967
+ process.stdout.write(`${render(report.workflowPath)}
968
+ `);
969
+ if (report.outcome === "not-a-repository" /* NotARepository */) process.exitCode = 1;
970
+ }
896
971
  var logger2 = getLogger();
897
972
  var ELECTRON_APP_DIR = "electron-app";
898
973
  var CURSOR_SKILLS_DIR = ".cursor";
@@ -1619,6 +1694,15 @@ function getHelpGuidance() {
1619
1694
  function helpCommand() {
1620
1695
  console.log(getHelpGuidance());
1621
1696
  }
1697
+ var QUESTION = "Make E2E acceptance a CI check in this repo? It adds a GitHub Actions workflow so pull requests\nopened outside Claude still have to carry a Muggle walkthrough comment. [y/N]: ";
1698
+ async function offerCiWorkflow(ask, cwd) {
1699
+ if (!existsSync(join(cwd, ".git"))) return "not-applicable" /* NotApplicable */;
1700
+ if (existsSync(join(cwd, USER_WORKFLOW_PATH))) return "already-installed" /* AlreadyInstalled */;
1701
+ const answer = (await ask(QUESTION)).trim().toLowerCase();
1702
+ if (answer !== "y" && answer !== "yes") return "declined" /* Declined */;
1703
+ installCiWorkflow({ cwd, force: false });
1704
+ return "installed" /* Installed */;
1705
+ }
1622
1706
  var ACCEPT_HINT = "press Enter to accept";
1623
1707
  function printPrimer() {
1624
1708
  const plan = buildOnboardingPlan();
@@ -1760,6 +1844,24 @@ function reportOutcome(result) {
1760
1844
  console.log("Preferences saved.");
1761
1845
  console.log(summary);
1762
1846
  }
1847
+ var CI_OFFER_REPORT = {
1848
+ ["installed" /* Installed */]: `Added ${USER_WORKFLOW_PATH} \u2014 commit it, and every pull request here will owe a settled walkthrough comment.`,
1849
+ ["declined" /* Declined */]: "Left CI alone. Run `muggle ci-install` whenever you want it.",
1850
+ ["already-installed" /* AlreadyInstalled */]: null,
1851
+ ["not-applicable" /* NotApplicable */]: null
1852
+ };
1853
+ async function runCiOffer() {
1854
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1855
+ try {
1856
+ const line = CI_OFFER_REPORT[await offerCiWorkflow((q) => rl.question(q), process.cwd())];
1857
+ if (line) {
1858
+ console.log("");
1859
+ console.log(line);
1860
+ }
1861
+ } finally {
1862
+ rl.close();
1863
+ }
1864
+ }
1763
1865
  async function initCommand(options) {
1764
1866
  if (options.json) {
1765
1867
  console.log(JSON.stringify(buildOnboardingPlan(), null, 2));
@@ -1785,6 +1887,7 @@ async function initCommand(options) {
1785
1887
  console.log("Muggle is already set up. Re-running the walkthrough \u2014 your current values are pre-selected.");
1786
1888
  }
1787
1889
  reportOutcome(applyOnboardingAnswers(await runTerminalWalkthrough()));
1890
+ await runCiOffer();
1788
1891
  }
1789
1892
 
1790
1893
  // src/cli/login.ts
@@ -1856,6 +1959,180 @@ async function statusCommand() {
1856
1959
  }
1857
1960
  }
1858
1961
 
1962
+ // src/pr-walkthrough/constants.ts
1963
+ var REPORT_SENTINEL = "muggle-pr-section";
1964
+ var WALKTHROUGH_SLOT_MARKER = "<!-- muggle-pr-walkthrough:v1 -->";
1965
+ var WALKTHROUGH_SKIPPED_MARKER = "<!-- muggle-pr-walkthrough-status:skipped -->";
1966
+ var WALKTHROUGH_COMMENT_HEADING = "### Muggle AI \u2014 PR visual walkthrough";
1967
+ var GH_COMMENT_TIMEOUT_MS = 1e4;
1968
+
1969
+ // src/pr-walkthrough/comment.ts
1970
+ function renderReservedComment() {
1971
+ return `${WALKTHROUGH_SLOT_MARKER}
1972
+ ${WALKTHROUGH_COMMENT_HEADING}
1973
+
1974
+ _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._`;
1975
+ }
1976
+ function classifyComment(body) {
1977
+ if (body.includes(REPORT_SENTINEL)) return "reported" /* Reported */;
1978
+ if (!body.includes(WALKTHROUGH_SLOT_MARKER)) return "not-designated" /* NotDesignated */;
1979
+ if (body.includes(WALKTHROUGH_SKIPPED_MARKER)) return "skipped" /* Skipped */;
1980
+ return "pending" /* Pending */;
1981
+ }
1982
+ function walkthroughVerdict(bodies) {
1983
+ const statuses = bodies.map(classifyComment);
1984
+ if (statuses.includes("reported" /* Reported */) || statuses.includes("skipped" /* Skipped */)) {
1985
+ return "satisfied" /* Satisfied */;
1986
+ }
1987
+ if (statuses.includes("pending" /* Pending */)) return "pending" /* Pending */;
1988
+ return "missing" /* Missing */;
1989
+ }
1990
+ var GH_CALLS_ENV = "MUGGLE_GUARDRAIL_GH_CALLS";
1991
+ var defaultGhRunner = (args, input) => {
1992
+ if (process.env[GH_CALLS_ENV] === "off") return null;
1993
+ try {
1994
+ return execFileSync("gh", args, {
1995
+ encoding: "utf-8",
1996
+ input,
1997
+ timeout: GH_COMMENT_TIMEOUT_MS,
1998
+ stdio: ["pipe", "pipe", "ignore"]
1999
+ });
2000
+ } catch {
2001
+ return null;
2002
+ }
2003
+ };
2004
+ function listPrComments(pr, run) {
2005
+ let raw;
2006
+ try {
2007
+ raw = run(["api", "--paginate", `repos/${pr.repo}/issues/${pr.prNumber}/comments`]);
2008
+ } catch {
2009
+ return null;
2010
+ }
2011
+ if (!raw) return null;
2012
+ try {
2013
+ const parsed = JSON.parse(raw);
2014
+ return Array.isArray(parsed) ? parsed : null;
2015
+ } catch {
2016
+ return null;
2017
+ }
2018
+ }
2019
+ function writeJson(args, payload, run) {
2020
+ try {
2021
+ return run(args, JSON.stringify(payload)) !== null;
2022
+ } catch {
2023
+ return false;
2024
+ }
2025
+ }
2026
+ function postPrComment(pr, body, run) {
2027
+ return writeJson(
2028
+ ["api", "--method", "POST", `repos/${pr.repo}/issues/${pr.prNumber}/comments`, "--input", "-"],
2029
+ { body },
2030
+ run
2031
+ );
2032
+ }
2033
+ function createCheckRun(repo, checkRun, run) {
2034
+ return writeJson(["api", "--method", "POST", `repos/${repo}/check-runs`, "--input", "-"], {
2035
+ name: checkRun.name,
2036
+ head_sha: checkRun.headSha,
2037
+ status: "completed",
2038
+ conclusion: checkRun.conclusion,
2039
+ output: { title: checkRun.title, summary: checkRun.summary }
2040
+ }, run);
2041
+ }
2042
+
2043
+ // src/pr-walkthrough/reserve.ts
2044
+ function reserveComment(pr, comments, run) {
2045
+ const claimed = comments.some(
2046
+ (comment) => classifyComment(comment.body) !== "not-designated" /* NotDesignated */
2047
+ );
2048
+ if (claimed) return false;
2049
+ return postPrComment(pr, renderReservedComment(), run);
2050
+ }
2051
+
2052
+ // src/cli/pr-walkthrough/check.ts
2053
+ var CHECK_RUN_NAME = "muggle-walkthrough";
2054
+ var SUMMARIES = {
2055
+ ["success" /* Success */]: "This PR's Muggle AI walkthrough comment is settled \u2014 it carries the E2E acceptance walkthrough, or states why E2E does not apply.",
2056
+ ["failure" /* Failure */]: "This PR's Muggle AI walkthrough comment is still empty. Run the E2E acceptance suite and post the walkthrough into it (/muggle:muggle-test), or record why E2E does not apply to this change.",
2057
+ ["neutral" /* Neutral */]: "The PR's comments could not be read, so the walkthrough duty could not be judged."
2058
+ };
2059
+ function conclusionFor(verdict) {
2060
+ return verdict === "satisfied" /* Satisfied */ ? "success" /* Success */ : "failure" /* Failure */;
2061
+ }
2062
+ async function runPrWalkthroughCheck(options, run = defaultGhRunner) {
2063
+ const pr = { repo: options.repo, prNumber: options.prNumber };
2064
+ const comments = listPrComments(pr, run);
2065
+ let conclusion = "neutral" /* Neutral */;
2066
+ if (comments !== null) {
2067
+ const verdict = walkthroughVerdict(comments.map((comment) => comment.body));
2068
+ if (verdict === "missing" /* Missing */) reserveComment(pr, comments, run);
2069
+ conclusion = conclusionFor(verdict);
2070
+ }
2071
+ if (options.publishCheckRun) {
2072
+ createCheckRun(
2073
+ options.repo,
2074
+ {
2075
+ name: CHECK_RUN_NAME,
2076
+ headSha: options.headSha,
2077
+ conclusion,
2078
+ title: "Muggle AI PR visual walkthrough",
2079
+ summary: SUMMARIES[conclusion]
2080
+ },
2081
+ run
2082
+ );
2083
+ }
2084
+ const failsLocally = conclusion === "failure" /* Failure */ && !options.publishCheckRun;
2085
+ return {
2086
+ conclusion,
2087
+ exitCode: failsLocally ? 1 : 0,
2088
+ summary: SUMMARIES[conclusion]
2089
+ };
2090
+ }
2091
+ function readEvent(eventPath) {
2092
+ if (!eventPath) return {};
2093
+ try {
2094
+ return JSON.parse(readFileSync(eventPath, "utf-8"));
2095
+ } catch {
2096
+ return {};
2097
+ }
2098
+ }
2099
+ function fetchHeadSha(repo, prNumber, run) {
2100
+ try {
2101
+ return run(["api", `repos/${repo}/pulls/${prNumber}`, "--jq", ".head.sha"])?.trim() ?? null;
2102
+ } catch {
2103
+ return null;
2104
+ }
2105
+ }
2106
+ function resolveCheckTarget(flags, env, run = defaultGhRunner) {
2107
+ const repo = flags.repo ?? env.GITHUB_REPOSITORY;
2108
+ if (!repo) return null;
2109
+ const event = readEvent(env.GITHUB_EVENT_PATH);
2110
+ const commentedPrNumber = event.issue?.pull_request ? event.issue.number : void 0;
2111
+ const prNumber = Number(flags.pr ?? event.pull_request?.number ?? commentedPrNumber ?? NaN);
2112
+ if (!Number.isInteger(prNumber) || prNumber <= 0) return null;
2113
+ const headSha = flags.headSha ?? event.pull_request?.head?.sha ?? fetchHeadSha(repo, prNumber, run);
2114
+ if (!headSha) return null;
2115
+ return {
2116
+ repo,
2117
+ prNumber,
2118
+ headSha,
2119
+ publishCheckRun: flags.checkRun === true
2120
+ };
2121
+ }
2122
+
2123
+ // src/cli/pr-walkthrough-check.ts
2124
+ async function prWalkthroughCheckCommand(flags) {
2125
+ const target = resolveCheckTarget(flags, process.env);
2126
+ if (!target) {
2127
+ process.stderr.write("pr-walkthrough-check: no pull request to judge\n");
2128
+ return;
2129
+ }
2130
+ const judgment = await runPrWalkthroughCheck(target);
2131
+ process.stdout.write(`${judgment.conclusion}: ${judgment.summary}
2132
+ `);
2133
+ process.exitCode = judgment.exitCode;
2134
+ }
2135
+
1859
2136
  // src/cli/disclosure/disclosure-constants.ts
1860
2137
  var DISCLOSURE_OPT_OUT_MARKER = "To opt out,";
1861
2138
  var DISCLOSURE_OPT_OUT_COPY = 'To opt out, set MUGGLE_TELEMETRY_DISABLED=1 in your environment, or set "telemetryEnabled": false at the top level of ~/.muggle-ai/preferences.json.';
@@ -2641,6 +2918,8 @@ function createProgram() {
2641
2918
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
2642
2919
  program.command("status").description("Show authentication status").action(statusCommand);
2643
2920
  program.command("build-pr-section").description("Render a muggle-do PR body evidence block from an e2e report on stdin").option("--max-body-bytes <n>", "Max UTF-8 byte budget for the PR body (default 60000)").action(buildPrSectionCommand);
2921
+ program.command("pr-walkthrough-check").description("Check that a PR's Muggle AI walkthrough comment carries a result or a stated skip").option("--repo <owner/repo>", "Repository (default: $GITHUB_REPOSITORY)").option("--pr <number>", "Pull request number (default: the Actions event payload)").option("--head-sha <sha>", "Commit the check run attaches to (default: the PR head)").option("--check-run", "Publish the verdict as a check run instead of failing the process").action(prWalkthroughCheckCommand);
2922
+ program.command("ci-install").description("Add the Muggle walkthrough check to this repository's GitHub Actions").option("--force", "Replace an existing workflow file").action(ciInstallCommand);
2644
2923
  program.action(() => {
2645
2924
  helpCommand();
2646
2925
  });
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-6ILUPFM5.js';
2
+ import { runCli } from './chunk-63RAPKGG.js';
3
3
  import './chunk-EKSYEVM2.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-6ILUPFM5.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-63RAPKGG.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-EKSYEVM2.js';
@@ -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 '{}'