@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.
@@ -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().optional().describe("Epoch milliseconds when uploaded to cloud")
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-23NOSJFH.js';
2
+ import { runCli } from './chunk-PMI2DI3V.js';
3
3
 
4
4
  // src/cli/main.ts
5
5
  runCli().catch((error) => {
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-23NOSJFH.js';
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.3.0",
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.3.0",
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
- - `## E2E Acceptance Results` summary table (see format below)
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 E2E acceptance evidence comment** with screenshots (see format below).
34
-
35
- ## E2E acceptance results section format (PR body)
36
-
37
- ```markdown
38
- ## E2E Acceptance Results
39
-
40
- **X passed / Y failed**
41
-
42
- | Test Case | Status | Details |
43
- |-----------|--------|---------|
44
- | [Name]({viewUrl}) | ✅ PASSED | — |
45
- | [Name]({viewUrl}) | ❌ FAILED | {error} |
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
- ## E2E acceptance evidence comment format
75
+ ### Step B: Render the evidence block
49
76
 
50
- After creating the PR, post a comment with embedded screenshots:
77
+ Pipe the JSON into `muggle build-pr-section`. It writes `{ "body": "...", "comment": "..." | null }` to stdout:
51
78
 
52
79
  ```bash
53
- gh pr comment <PR#> --body "$(cat <<'EOF'
54
- ## 🧪 E2E acceptance evidence
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
- </details>
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
- <details>
75
- <summary>📸 <strong>Checkout</strong> — 4 steps (failed at step 3)</summary>
85
+ ### Step C: Build the PR body
76
86
 
77
- | # | Action | Screenshot |
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
- </details>
84
- EOF
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
- ### Comment Building Rules
94
+ ### Step D: Create the PR, then post the overflow comment only if present
89
95
 
90
- 1. **Summary table:**
91
- - Show thumbnail (120px) of **last step** for passed tests
92
- - Show thumbnail of **failure step** for failed tests
93
- - Thumbnail links to full-size image
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
- 2. **Collapsible details per test case:**
96
- - Show all steps with 200px thumbnails
97
- - Mark failure step with ⚠️ and inline error message
98
- - Include step count in summary line
101
+ ```bash
102
+ gh pr comment <PR#> --body "$(cat <<'EOF'
103
+ <comment field contents>
104
+ EOF
105
+ )"
106
+ ```
99
107
 
100
- 3. **HTML for thumbnails:**
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
- 4. **All tests get screenshots:**
105
- - Passing tests show proof of success
106
- - Failing tests highlight the failure point
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 evidence comments posted:**
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, show this command set:
12
+ When user asks for "muggle" with no specific subcommand, use `AskQuestion` to present the command set as clickable options:
13
13
 
14
- - `/muggle:muggle-do`autonomous dev pipeline
15
- - `/muggle:muggle-test` change-driven E2E acceptance testing (local or remote, with PR posting)
16
- - `/muggle:muggle-test-feature-local`local feature E2E acceptance testing
17
- - `/muggle:muggle-status` — health check
18
- - `/muggle:muggle-repair`repair broken installation
19
- - `/muggle:muggle-upgrade`upgrade local installation
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
+ - "Repairfix broken installation" → `muggle-repair`
19
+ - "Upgradeupdate to latest version" → `muggle-upgrade`
20
20
 
21
21
  ## Routing
22
22
 
23
- If the user intent clearly matches one command, route to that command behavior:
23
+ If the user intent clearly matches one command, route directly no menu needed:
24
24
 
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`
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, ask one concise clarification question.
32
+ If intent is ambiguous, use `AskQuestion` with the most likely options rather than asking the user to type a clarification.