@muggleai/works 4.3.0 → 4.4.0
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/dist/{chunk-23NOSJFH.js → chunk-PMI2DI3V.js} +277 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/skills/do/open-prs.md +68 -62
- package/dist/plugin/skills/muggle/SKILL.md +15 -15
- package/dist/plugin/skills/muggle-test/SKILL.md +56 -92
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +43 -26
- package/dist/plugin/skills/muggle-test-import/SKILL.md +13 -17
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/skills/do/open-prs.md +68 -62
- package/plugin/skills/muggle/SKILL.md +15 -15
- package/plugin/skills/muggle-test/SKILL.md +56 -92
- package/plugin/skills/muggle-test-feature-local/SKILL.md +43 -26
- package/plugin/skills/muggle-test-import/SKILL.md +13 -17
|
@@ -2837,7 +2837,7 @@ var LocalExecutionContextInputSchema = z.object({
|
|
|
2837
2837
|
electronAppVersion: z.string().optional().describe("Electron app version used for local run"),
|
|
2838
2838
|
mcpServerVersion: z.string().optional().describe("MCP server version used for local run"),
|
|
2839
2839
|
localExecutionCompletedAt: z.number().int().positive().describe("Epoch milliseconds when local run completed"),
|
|
2840
|
-
uploadedAt: z.number().int().positive().
|
|
2840
|
+
uploadedAt: z.number().int().positive().describe("Epoch milliseconds when uploaded to cloud")
|
|
2841
2841
|
});
|
|
2842
2842
|
var LocalRunUploadInputSchema = z.object({
|
|
2843
2843
|
projectId: MuggleEntityIdSchema.describe("Project ID (UUID) for the local run"),
|
|
@@ -5985,6 +5985,281 @@ async function startStdioServer(server) {
|
|
|
5985
5985
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
5986
5986
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5987
5987
|
}
|
|
5988
|
+
|
|
5989
|
+
// src/cli/pr-section/selectors.ts
|
|
5990
|
+
var ONE_LINER_BUDGET = 160;
|
|
5991
|
+
function selectHero(report) {
|
|
5992
|
+
const firstFailed = report.tests.find(
|
|
5993
|
+
(t) => t.status === "failed"
|
|
5994
|
+
);
|
|
5995
|
+
if (firstFailed) {
|
|
5996
|
+
const step = firstFailed.steps.find((s) => s.stepIndex === firstFailed.failureStepIndex);
|
|
5997
|
+
if (step) {
|
|
5998
|
+
return {
|
|
5999
|
+
screenshotUrl: step.screenshotUrl,
|
|
6000
|
+
testName: firstFailed.name,
|
|
6001
|
+
kind: "failure"
|
|
6002
|
+
};
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
const firstPassedWithSteps = report.tests.find(
|
|
6006
|
+
(t) => t.status === "passed" && t.steps.length > 0
|
|
6007
|
+
);
|
|
6008
|
+
if (firstPassedWithSteps) {
|
|
6009
|
+
const lastStep = firstPassedWithSteps.steps[firstPassedWithSteps.steps.length - 1];
|
|
6010
|
+
return {
|
|
6011
|
+
screenshotUrl: lastStep.screenshotUrl,
|
|
6012
|
+
testName: firstPassedWithSteps.name,
|
|
6013
|
+
kind: "final"
|
|
6014
|
+
};
|
|
6015
|
+
}
|
|
6016
|
+
return null;
|
|
6017
|
+
}
|
|
6018
|
+
function buildOneLiner(report) {
|
|
6019
|
+
const total = report.tests.length;
|
|
6020
|
+
if (total === 0) {
|
|
6021
|
+
return "No acceptance tests were executed.";
|
|
6022
|
+
}
|
|
6023
|
+
const failed = report.tests.filter((t) => t.status === "failed");
|
|
6024
|
+
if (failed.length === 0) {
|
|
6025
|
+
return `All ${total} acceptance tests passed.`;
|
|
6026
|
+
}
|
|
6027
|
+
const first = failed[0];
|
|
6028
|
+
const prefix = `${failed.length} of ${total} failed \u2014 "${first.name}" broke at step ${first.failureStepIndex}: `;
|
|
6029
|
+
const available = ONE_LINER_BUDGET - prefix.length - 1;
|
|
6030
|
+
const error = first.error.length > available ? first.error.slice(0, Math.max(0, available - 1)) + "\u2026" : first.error;
|
|
6031
|
+
return `${prefix}${error}.`;
|
|
6032
|
+
}
|
|
6033
|
+
|
|
6034
|
+
// src/cli/pr-section/render.ts
|
|
6035
|
+
var DASHBOARD_URL_BASE = "https://www.muggle-ai.com/muggleTestV0/dashboard/projects";
|
|
6036
|
+
var ROW_THUMB_WIDTH = 120;
|
|
6037
|
+
var DETAIL_THUMB_WIDTH = 200;
|
|
6038
|
+
var HERO_WIDTH = 480;
|
|
6039
|
+
function thumbnail(url, width) {
|
|
6040
|
+
return `<a href="${url}"><img src="${url}" width="${width}"></a>`;
|
|
6041
|
+
}
|
|
6042
|
+
function counts(report) {
|
|
6043
|
+
const passed = report.tests.filter((t) => t.status === "passed").length;
|
|
6044
|
+
const failed = report.tests.filter((t) => t.status === "failed").length;
|
|
6045
|
+
return { passed, failed, text: `**${passed} passed / ${failed} failed**` };
|
|
6046
|
+
}
|
|
6047
|
+
function renderSummary(report) {
|
|
6048
|
+
const { text: countsLine } = counts(report);
|
|
6049
|
+
const oneLiner = buildOneLiner(report);
|
|
6050
|
+
const hero = selectHero(report);
|
|
6051
|
+
const dashboard = `${DASHBOARD_URL_BASE}/${report.projectId}/scripts`;
|
|
6052
|
+
const lines = [
|
|
6053
|
+
countsLine,
|
|
6054
|
+
"",
|
|
6055
|
+
oneLiner,
|
|
6056
|
+
""
|
|
6057
|
+
];
|
|
6058
|
+
if (hero) {
|
|
6059
|
+
lines.push(
|
|
6060
|
+
`<a href="${hero.screenshotUrl}"><img src="${hero.screenshotUrl}" width="${HERO_WIDTH}" alt="${hero.testName}"></a>`,
|
|
6061
|
+
""
|
|
6062
|
+
);
|
|
6063
|
+
}
|
|
6064
|
+
lines.push(`[View project dashboard on muggle-ai.com](${dashboard})`);
|
|
6065
|
+
return lines.join("\n");
|
|
6066
|
+
}
|
|
6067
|
+
function renderRow(test) {
|
|
6068
|
+
const link = `[${test.name}](${test.viewUrl})`;
|
|
6069
|
+
if (test.status === "passed") {
|
|
6070
|
+
const lastStep = test.steps[test.steps.length - 1];
|
|
6071
|
+
const thumb2 = lastStep ? thumbnail(lastStep.screenshotUrl, ROW_THUMB_WIDTH) : "\u2014";
|
|
6072
|
+
return `| ${link} | \u2705 PASSED | ${thumb2} |`;
|
|
6073
|
+
}
|
|
6074
|
+
const failStep = test.steps.find((s) => s.stepIndex === test.failureStepIndex);
|
|
6075
|
+
const thumb = failStep ? thumbnail(failStep.screenshotUrl, ROW_THUMB_WIDTH) : "\u2014";
|
|
6076
|
+
return `| ${link} | \u274C FAILED \u2014 ${test.error} | ${thumb} |`;
|
|
6077
|
+
}
|
|
6078
|
+
function renderFailureDetails(test) {
|
|
6079
|
+
const stepCount = test.steps.length;
|
|
6080
|
+
const header2 = `<details>
|
|
6081
|
+
<summary>\u{1F4F8} <strong>${test.name}</strong> \u2014 ${stepCount} steps (failed at step ${test.failureStepIndex})</summary>
|
|
6082
|
+
|
|
6083
|
+
| # | Action | Screenshot |
|
|
6084
|
+
|---|--------|------------|`;
|
|
6085
|
+
const rows = test.steps.map((step) => renderFailureStepRow(step, test)).join("\n");
|
|
6086
|
+
return `${header2}
|
|
6087
|
+
${rows}
|
|
6088
|
+
|
|
6089
|
+
</details>`;
|
|
6090
|
+
}
|
|
6091
|
+
function renderFailureStepRow(step, test) {
|
|
6092
|
+
const isFailure = step.stepIndex === test.failureStepIndex;
|
|
6093
|
+
const marker = isFailure ? `${step.stepIndex} \u26A0\uFE0F` : String(step.stepIndex);
|
|
6094
|
+
const action = isFailure ? `${step.action} \u2014 **${test.error}**` : step.action;
|
|
6095
|
+
return `| ${marker} | ${action} | ${thumbnail(step.screenshotUrl, DETAIL_THUMB_WIDTH)} |`;
|
|
6096
|
+
}
|
|
6097
|
+
function renderRowsTable(report) {
|
|
6098
|
+
if (report.tests.length === 0) {
|
|
6099
|
+
return "_No tests were executed._";
|
|
6100
|
+
}
|
|
6101
|
+
const header2 = "| Test Case | Status | Evidence |\n|-----------|--------|----------|";
|
|
6102
|
+
const rows = report.tests.map(renderRow).join("\n");
|
|
6103
|
+
return `${header2}
|
|
6104
|
+
${rows}`;
|
|
6105
|
+
}
|
|
6106
|
+
function renderBody(report, opts) {
|
|
6107
|
+
const sections = [
|
|
6108
|
+
"## E2E Acceptance Results",
|
|
6109
|
+
"",
|
|
6110
|
+
renderSummary(report),
|
|
6111
|
+
"",
|
|
6112
|
+
renderRowsTable(report)
|
|
6113
|
+
];
|
|
6114
|
+
const failures = report.tests.filter((t) => t.status === "failed");
|
|
6115
|
+
if (failures.length > 0) {
|
|
6116
|
+
if (opts.inlineFailureDetails) {
|
|
6117
|
+
sections.push("", ...failures.map(renderFailureDetails));
|
|
6118
|
+
} else {
|
|
6119
|
+
sections.push(
|
|
6120
|
+
"",
|
|
6121
|
+
"_Full step-by-step evidence in the comment below \u2014 the PR description was too large to inline it._"
|
|
6122
|
+
);
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
return sections.join("\n");
|
|
6126
|
+
}
|
|
6127
|
+
function renderComment(report) {
|
|
6128
|
+
const failures = report.tests.filter((t) => t.status === "failed");
|
|
6129
|
+
if (failures.length === 0) {
|
|
6130
|
+
return "";
|
|
6131
|
+
}
|
|
6132
|
+
const sections = [
|
|
6133
|
+
"## E2E acceptance evidence (overflow)",
|
|
6134
|
+
"",
|
|
6135
|
+
"_This comment was posted because the full step-by-step evidence did not fit in the PR description._",
|
|
6136
|
+
"",
|
|
6137
|
+
...failures.map(renderFailureDetails)
|
|
6138
|
+
];
|
|
6139
|
+
return sections.join("\n");
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
// src/cli/pr-section/overflow.ts
|
|
6143
|
+
function splitWithOverflow(report, opts) {
|
|
6144
|
+
const inlineBody = renderBody(report, { inlineFailureDetails: true });
|
|
6145
|
+
const inlineBytes = Buffer.byteLength(inlineBody, "utf-8");
|
|
6146
|
+
if (inlineBytes <= opts.maxBodyBytes) {
|
|
6147
|
+
return { body: inlineBody, comment: null };
|
|
6148
|
+
}
|
|
6149
|
+
const spilledBody = renderBody(report, { inlineFailureDetails: false });
|
|
6150
|
+
const comment = renderComment(report);
|
|
6151
|
+
return {
|
|
6152
|
+
body: spilledBody,
|
|
6153
|
+
comment: comment.length > 0 ? comment : null
|
|
6154
|
+
};
|
|
6155
|
+
}
|
|
6156
|
+
var StepSchema = z.object({
|
|
6157
|
+
stepIndex: z.number().int().nonnegative(),
|
|
6158
|
+
action: z.string().min(1),
|
|
6159
|
+
screenshotUrl: z.string().url()
|
|
6160
|
+
});
|
|
6161
|
+
var PassedTestSchema = z.object({
|
|
6162
|
+
name: z.string().min(1),
|
|
6163
|
+
testCaseId: z.string().min(1),
|
|
6164
|
+
testScriptId: z.string().min(1).optional(),
|
|
6165
|
+
runId: z.string().min(1),
|
|
6166
|
+
viewUrl: z.string().url(),
|
|
6167
|
+
status: z.literal("passed"),
|
|
6168
|
+
steps: z.array(StepSchema)
|
|
6169
|
+
});
|
|
6170
|
+
var FailedTestSchema = z.object({
|
|
6171
|
+
name: z.string().min(1),
|
|
6172
|
+
testCaseId: z.string().min(1),
|
|
6173
|
+
testScriptId: z.string().min(1).optional(),
|
|
6174
|
+
runId: z.string().min(1),
|
|
6175
|
+
viewUrl: z.string().url(),
|
|
6176
|
+
status: z.literal("failed"),
|
|
6177
|
+
steps: z.array(StepSchema),
|
|
6178
|
+
failureStepIndex: z.number().int().nonnegative(),
|
|
6179
|
+
error: z.string().min(1),
|
|
6180
|
+
artifactsDir: z.string().min(1).optional()
|
|
6181
|
+
});
|
|
6182
|
+
var TestResultSchema = z.discriminatedUnion("status", [
|
|
6183
|
+
PassedTestSchema,
|
|
6184
|
+
FailedTestSchema
|
|
6185
|
+
]);
|
|
6186
|
+
var E2eReportSchema = z.object({
|
|
6187
|
+
projectId: z.string().min(1),
|
|
6188
|
+
tests: z.array(TestResultSchema)
|
|
6189
|
+
});
|
|
6190
|
+
|
|
6191
|
+
// src/cli/pr-section/index.ts
|
|
6192
|
+
function buildPrSection(report, opts) {
|
|
6193
|
+
return splitWithOverflow(report, opts);
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
// src/cli/build-pr-section.ts
|
|
6197
|
+
var DEFAULT_MAX_BODY_BYTES = 6e4;
|
|
6198
|
+
async function readAll(stream) {
|
|
6199
|
+
const chunks = [];
|
|
6200
|
+
for await (const chunk of stream) {
|
|
6201
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
6202
|
+
}
|
|
6203
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
6204
|
+
}
|
|
6205
|
+
function errMsg(e) {
|
|
6206
|
+
return e instanceof Error ? e.message : String(e);
|
|
6207
|
+
}
|
|
6208
|
+
async function runBuildPrSection(opts) {
|
|
6209
|
+
let raw;
|
|
6210
|
+
try {
|
|
6211
|
+
raw = await readAll(opts.stdin);
|
|
6212
|
+
} catch (err) {
|
|
6213
|
+
opts.stderrWrite(`build-pr-section: failed to read stdin: ${errMsg(err)}
|
|
6214
|
+
`);
|
|
6215
|
+
return 1;
|
|
6216
|
+
}
|
|
6217
|
+
let json;
|
|
6218
|
+
try {
|
|
6219
|
+
json = JSON.parse(raw);
|
|
6220
|
+
} catch (err) {
|
|
6221
|
+
opts.stderrWrite(`build-pr-section: failed to parse stdin as JSON: ${errMsg(err)}
|
|
6222
|
+
`);
|
|
6223
|
+
return 1;
|
|
6224
|
+
}
|
|
6225
|
+
let report;
|
|
6226
|
+
try {
|
|
6227
|
+
report = E2eReportSchema.parse(json);
|
|
6228
|
+
} catch (err) {
|
|
6229
|
+
if (err instanceof ZodError) {
|
|
6230
|
+
opts.stderrWrite(
|
|
6231
|
+
`build-pr-section: report validation failed:
|
|
6232
|
+
${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
|
|
6233
|
+
`
|
|
6234
|
+
);
|
|
6235
|
+
} else {
|
|
6236
|
+
opts.stderrWrite(`build-pr-section: report validation failed: ${errMsg(err)}
|
|
6237
|
+
`);
|
|
6238
|
+
}
|
|
6239
|
+
return 1;
|
|
6240
|
+
}
|
|
6241
|
+
const result = buildPrSection(report, { maxBodyBytes: opts.maxBodyBytes });
|
|
6242
|
+
opts.stdoutWrite(JSON.stringify({ body: result.body, comment: result.comment }));
|
|
6243
|
+
return 0;
|
|
6244
|
+
}
|
|
6245
|
+
async function buildPrSectionCommand(options) {
|
|
6246
|
+
const maxBodyBytes = options.maxBodyBytes ? Number(options.maxBodyBytes) : DEFAULT_MAX_BODY_BYTES;
|
|
6247
|
+
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) {
|
|
6248
|
+
process.stderr.write(`build-pr-section: --max-body-bytes must be a positive number
|
|
6249
|
+
`);
|
|
6250
|
+
process.exitCode = 1;
|
|
6251
|
+
return;
|
|
6252
|
+
}
|
|
6253
|
+
const code = await runBuildPrSection({
|
|
6254
|
+
stdin: process.stdin,
|
|
6255
|
+
stdoutWrite: (s) => process.stdout.write(s),
|
|
6256
|
+
stderrWrite: (s) => process.stderr.write(s),
|
|
6257
|
+
maxBodyBytes
|
|
6258
|
+
});
|
|
6259
|
+
if (code !== 0) {
|
|
6260
|
+
process.exitCode = code;
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
5988
6263
|
var logger7 = getLogger();
|
|
5989
6264
|
var ELECTRON_APP_DIR2 = "electron-app";
|
|
5990
6265
|
var CURSOR_SKILLS_DIR = ".cursor";
|
|
@@ -7362,6 +7637,7 @@ function createProgram() {
|
|
|
7362
7637
|
program.command("login").description("Authenticate with Muggle AI (uses device code flow)").option("--key-name <name>", "Name for the API key").option("--key-expiry <expiry>", "API key expiry: 30d, 90d, 1y, never", "90d").action(loginCommand);
|
|
7363
7638
|
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
7364
7639
|
program.command("status").description("Show authentication status").action(statusCommand);
|
|
7640
|
+
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);
|
|
7365
7641
|
program.action(() => {
|
|
7366
7642
|
helpCommand();
|
|
7367
7643
|
});
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { src_exports2 as commands, createChildLogger, createUnifiedMcpServer, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, server_exports as server, src_exports as shared } from './chunk-
|
|
1
|
+
export { src_exports2 as commands, createChildLogger, createUnifiedMcpServer, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, server_exports as server, src_exports as shared } from './chunk-PMI2DI3V.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.4.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -27,90 +27,96 @@ For each repo with changes:
|
|
|
27
27
|
- `## Goal` — the requirements goal
|
|
28
28
|
- `## Acceptance Criteria` — bulleted list (omit section if empty)
|
|
29
29
|
- `## Changes` — summary of what changed in this repo
|
|
30
|
-
-
|
|
30
|
+
- E2E acceptance evidence block from `muggle build-pr-section` (see "Rendering the E2E acceptance results block" below)
|
|
31
31
|
4. **Create the PR** using `gh pr create --title "..." --body "..." --head <branch>` in the repo directory.
|
|
32
32
|
5. **Capture the PR URL** and extract the PR number.
|
|
33
|
-
6. **Post
|
|
34
|
-
|
|
35
|
-
## E2E acceptance results
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
6. **Post the overflow comment only if `muggle build-pr-section` emitted one** (see "Rendering the E2E acceptance results block" below). In the common case, no comment is posted.
|
|
34
|
+
|
|
35
|
+
## Rendering the E2E acceptance results block
|
|
36
|
+
|
|
37
|
+
Do **not** hand-write the `## E2E Acceptance Results` markdown. Use the `muggle build-pr-section` CLI, which renders a deterministic block and decides whether the evidence fits in the PR description or needs to spill into an overflow comment.
|
|
38
|
+
|
|
39
|
+
### Step A: Build the report JSON
|
|
40
|
+
|
|
41
|
+
Assemble the e2e-acceptance report you collected in `e2e-acceptance.md` into a JSON object with this shape:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"projectId": "<project UUID>",
|
|
46
|
+
"tests": [
|
|
47
|
+
{
|
|
48
|
+
"name": "<test case name>",
|
|
49
|
+
"testCaseId": "<UUID>",
|
|
50
|
+
"testScriptId": "<UUID or omitted>",
|
|
51
|
+
"runId": "<UUID>",
|
|
52
|
+
"viewUrl": "<muggle-ai.com run URL>",
|
|
53
|
+
"status": "passed",
|
|
54
|
+
"steps": [
|
|
55
|
+
{ "stepIndex": 0, "action": "<action>", "screenshotUrl": "<URL>" }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "<test case name>",
|
|
60
|
+
"testCaseId": "<UUID>",
|
|
61
|
+
"runId": "<UUID>",
|
|
62
|
+
"viewUrl": "<muggle-ai.com run URL>",
|
|
63
|
+
"status": "failed",
|
|
64
|
+
"failureStepIndex": 2,
|
|
65
|
+
"error": "<error message>",
|
|
66
|
+
"artifactsDir": "<path, optional>",
|
|
67
|
+
"steps": [
|
|
68
|
+
{ "stepIndex": 0, "action": "<action>", "screenshotUrl": "<URL>" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
46
73
|
```
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
### Step B: Render the evidence block
|
|
49
76
|
|
|
50
|
-
|
|
77
|
+
Pipe the JSON into `muggle build-pr-section`. It writes `{ "body": "...", "comment": "..." | null }` to stdout:
|
|
51
78
|
|
|
52
79
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
**X passed / Y failed**
|
|
57
|
-
|
|
58
|
-
| Test Case | Status | Summary |
|
|
59
|
-
|-----------|--------|---------|
|
|
60
|
-
| [Login Flow]({viewUrl}) | ✅ PASSED | <a href="{lastStepScreenshotUrl}"><img src="{lastStepScreenshotUrl}" width="120"></a> |
|
|
61
|
-
| [Checkout]({viewUrl}) | ❌ FAILED | <a href="{failureStepScreenshotUrl}"><img src="{failureStepScreenshotUrl}" width="120"></a> |
|
|
62
|
-
|
|
63
|
-
<details>
|
|
64
|
-
<summary>📸 <strong>Login Flow</strong> — 5 steps</summary>
|
|
65
|
-
|
|
66
|
-
| # | Action | Screenshot |
|
|
67
|
-
|---|--------|------------|
|
|
68
|
-
| 1 | Navigate to `/login` | <a href="{screenshotUrl}"><img src="{screenshotUrl}" width="200"></a> |
|
|
69
|
-
| 2 | Enter username | <a href="{screenshotUrl}"><img src="{screenshotUrl}" width="200"></a> |
|
|
70
|
-
| 3 | Click "Sign In" | <a href="{screenshotUrl}"><img src="{screenshotUrl}" width="200"></a> |
|
|
80
|
+
echo "$REPORT_JSON" | muggle build-pr-section > /tmp/muggle-pr-section.json
|
|
81
|
+
```
|
|
71
82
|
|
|
72
|
-
|
|
83
|
+
The command exits nonzero on malformed input and writes a descriptive error to stderr — do not swallow that error, surface it to the user.
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
<summary>📸 <strong>Checkout</strong> — 4 steps (failed at step 3)</summary>
|
|
85
|
+
### Step C: Build the PR body
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|---|--------|------------|
|
|
79
|
-
| 1 | Add item to cart | <a href="{screenshotUrl}"><img src="{screenshotUrl}" width="200"></a> |
|
|
80
|
-
| 2 | View cart | <a href="{screenshotUrl}"><img src="{screenshotUrl}" width="200"></a> |
|
|
81
|
-
| 3 ⚠️ | Click confirm — **Element not found** | <a href="{screenshotUrl}"><img src="{screenshotUrl}" width="200"></a> |
|
|
87
|
+
Build the PR body by concatenating, in order:
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
- `## Goal` — the requirements goal
|
|
90
|
+
- `## Acceptance Criteria` — bulleted list (omit section if empty)
|
|
91
|
+
- `## Changes` — summary of what changed in this repo
|
|
92
|
+
- The `body` field from the CLI output (already contains its own `## E2E Acceptance Results` header)
|
|
87
93
|
|
|
88
|
-
###
|
|
94
|
+
### Step D: Create the PR, then post the overflow comment only if present
|
|
89
95
|
|
|
90
|
-
1.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
1. Create the PR with `gh pr create --title "..." --body "..." --head <branch>`.
|
|
97
|
+
2. Capture the PR URL and extract the PR number.
|
|
98
|
+
3. If the CLI output's `comment` field is `null`, **do not post a comment** — everything is already in the PR description.
|
|
99
|
+
4. If the CLI output's `comment` field is a non-null string, post it as a follow-up comment:
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
```bash
|
|
102
|
+
gh pr comment <PR#> --body "$(cat <<'EOF'
|
|
103
|
+
<comment field contents>
|
|
104
|
+
EOF
|
|
105
|
+
)"
|
|
106
|
+
```
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
- Use `<a href="{url}"><img src="{url}" width="N"></a>` for clickable thumbnails
|
|
102
|
-
- 120px width in summary table, 200px in details
|
|
108
|
+
### Notes on fit vs. overflow
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
- **The common case is fit**: the full evidence (summary, per-test rows, collapsible failure details) lives in the PR description, no comment is posted.
|
|
111
|
+
- **The overflow case** is triggered automatically when the full inline body would exceed the CLI's budget. In that case the PR description contains the summary, the per-test rows, and a pointer line; the full step-by-step failure details live in the follow-up comment.
|
|
112
|
+
- You do not make the fit-vs-overflow decision — the CLI does. Never post the comment speculatively.
|
|
107
113
|
|
|
108
114
|
## Output
|
|
109
115
|
|
|
110
116
|
**PRs Created:**
|
|
111
117
|
- (repo name): (PR URL)
|
|
112
118
|
|
|
113
|
-
**E2E acceptance
|
|
119
|
+
**E2E acceptance overflow comments posted:** (only include repos where an overflow comment was actually posted)
|
|
114
120
|
- (repo name): comment posted to PR #(number)
|
|
115
121
|
|
|
116
122
|
**Errors:** (any repos where PR creation or comment posting failed, with the error message)
|
|
@@ -9,24 +9,24 @@ Use this as the top-level Muggle command router.
|
|
|
9
9
|
|
|
10
10
|
## Menu
|
|
11
11
|
|
|
12
|
-
When user asks for "muggle" with no specific subcommand,
|
|
12
|
+
When user asks for "muggle" with no specific subcommand, use `AskQuestion` to present the command set as clickable options:
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
14
|
+
- "Test my changes — change-driven E2E acceptance testing (local or remote)" → `muggle-test`
|
|
15
|
+
- "Test a feature on localhost — run a single E2E test locally" → `muggle-test-feature-local`
|
|
16
|
+
- "Autonomous dev pipeline — requirements to PR" → `muggle-do`
|
|
17
|
+
- "Health check — verify installation status" → `muggle-status`
|
|
18
|
+
- "Repair — fix broken installation" → `muggle-repair`
|
|
19
|
+
- "Upgrade — update to latest version" → `muggle-upgrade`
|
|
20
20
|
|
|
21
21
|
## Routing
|
|
22
22
|
|
|
23
|
-
If the user intent clearly matches one command, route
|
|
23
|
+
If the user intent clearly matches one command, route directly — no menu needed:
|
|
24
24
|
|
|
25
|
-
- status/health/check
|
|
26
|
-
- repair/fix/install broken
|
|
27
|
-
- upgrade/update latest
|
|
28
|
-
- test my changes/acceptance test my work/test before push/post E2E acceptance results to PR/test on staging/test on preview
|
|
29
|
-
- test localhost/validate single feature
|
|
30
|
-
- build/implement from request
|
|
25
|
+
- status/health/check → `muggle-status`
|
|
26
|
+
- repair/fix/install broken → `muggle-repair`
|
|
27
|
+
- upgrade/update latest → `muggle-upgrade`
|
|
28
|
+
- test my changes/acceptance test my work/test before push/post E2E acceptance results to PR/test on staging/test on preview → `muggle-test`
|
|
29
|
+
- test localhost/validate single feature → `muggle-test-feature-local`
|
|
30
|
+
- build/implement from request → `muggle-do`
|
|
31
31
|
|
|
32
|
-
If intent is ambiguous,
|
|
32
|
+
If intent is ambiguous, use `AskQuestion` with the most likely options rather than asking the user to type a clarification.
|