@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
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:
|
|
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
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
1
|
+
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-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
|
-
**
|
|
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
|
|
|
@@ -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 '{}'
|