@muggleai/works 4.11.0 → 4.11.2

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.
@@ -1,4 +1,4 @@
1
- import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, isFirstRun, writePreferences, DEFAULT_PREFERENCES, getDataDir, PREFERENCES_FILE_NAME, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, performLogin, toolRequiresAuth, getCallerCredentials, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-ZKRHFTIC.js';
1
+ import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, isFirstRun, writePreferences, DEFAULT_PREFERENCES, getDataDir, PREFERENCES_FILE_NAME, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, getDisclosureCopy, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-AYWLYB3F.js';
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { v4 } from 'uuid';
@@ -731,7 +731,7 @@ async function resolveGsScreenshotUrls(report, opts) {
731
731
  if (gsUrls.length === 0) {
732
732
  return report;
733
733
  }
734
- const mcps = await import('./src-2G7ENRGI.js');
734
+ const mcps = await import('./src-NJUUHKPW.js');
735
735
  const credentials = await mcps.getCallerCredentialsAsync();
736
736
  if (!credentials.bearerToken && !credentials.apiKey) {
737
737
  stderrWrite(
@@ -1638,6 +1638,15 @@ async function statusCommand() {
1638
1638
  }
1639
1639
 
1640
1640
  // src/cli/serve.ts
1641
+ var APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=0e28df42-b98e-4089-b3b5-6decefb99447;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=b64a932f-85ec-40cd-a74b-e181a3660f6c";
1642
+ function showDisclosureIfNeeded() {
1643
+ try {
1644
+ if (hasShownDisclosure()) return;
1645
+ process.stderr.write(getDisclosureCopy() + "\n");
1646
+ markDisclosureShown();
1647
+ } catch {
1648
+ }
1649
+ }
1641
1650
  var logger5 = getLogger();
1642
1651
  async function serveCommand(options) {
1643
1652
  const config = getConfig();
@@ -1650,7 +1659,12 @@ async function serveCommand(options) {
1650
1659
  transport: "stdio"
1651
1660
  });
1652
1661
  try {
1653
- initTelemetry({ serviceName: ServiceName.MuggleMcp, surface: Surface.McpLocal });
1662
+ showDisclosureIfNeeded();
1663
+ initTelemetry({
1664
+ serviceName: ServiceName.MuggleMcp,
1665
+ surface: Surface.McpLocal,
1666
+ connectionString: APPLICATIONINSIGHTS_CONNECTION_STRING
1667
+ });
1654
1668
  track({ name: EventName.SystemStartup, props: { serviceName: ServiceName.MuggleMcp } });
1655
1669
  } catch (err) {
1656
1670
  logger5.warn("Telemetry init skipped", {
@@ -94,7 +94,7 @@ __export(src_exports, {
94
94
  toolRequiresAuth: () => toolRequiresAuth,
95
95
  validatePreference: () => validatePreference,
96
96
  verifyFileChecksum: () => verifyFileChecksum,
97
- writePreferences: () => writePreferences
97
+ writePreferences: () => writePreferences2
98
98
  });
99
99
  var DATA_DIR_NAME = ".muggle-ai";
100
100
  function getDataDir() {
@@ -3198,6 +3198,19 @@ function readPreferences() {
3198
3198
  return {};
3199
3199
  }
3200
3200
  }
3201
+ function writePreferences(prefs) {
3202
+ const filePath = prefsFilePath();
3203
+ mkdirSync(dirname(filePath), { recursive: true });
3204
+ writeFileSync(filePath, JSON.stringify(prefs, null, 2) + "\n", "utf8");
3205
+ }
3206
+ function updatePreference(key, value) {
3207
+ const prefs = readPreferences();
3208
+ prefs[key] = value;
3209
+ writePreferences(prefs);
3210
+ }
3211
+ function preferencesFileExists() {
3212
+ return existsSync(prefsFilePath());
3213
+ }
3201
3214
 
3202
3215
  function isTelemetryEnabled() {
3203
3216
  if (process.env[ENV_TELEMETRY_DISABLED] === "1")
@@ -3223,6 +3236,21 @@ function track(event) {
3223
3236
  }
3224
3237
  }
3225
3238
 
3239
+ var DISCLOSURE_COPY = `Muggle Test collects anonymous usage telemetry \u2014 which features you use, how long they take, and whether they succeed \u2014 to help us improve the product. We attach an opaque install ID, your account ID (when signed in), and your IP. We do not collect file contents, OS username, hostname, or file paths. To opt out, set telemetryEnabled to false in ~/.muggle-ai/preferences.json or run \`muggle preferences set telemetryEnabled false\`.`;
3240
+
3241
+ function getDisclosureCopy() {
3242
+ return DISCLOSURE_COPY;
3243
+ }
3244
+ function hasShownDisclosure() {
3245
+ if (!preferencesFileExists())
3246
+ return false;
3247
+ const prefs = readPreferences();
3248
+ return typeof prefs.disclosureShownAt === "string" && prefs.disclosureShownAt.length > 0;
3249
+ }
3250
+ function markDisclosureShown(now = /* @__PURE__ */ new Date()) {
3251
+ updatePreference("disclosureShownAt", now.toISOString());
3252
+ }
3253
+
3226
3254
  var Trigger;
3227
3255
  (function(Trigger2) {
3228
3256
  Trigger2["UserSlash"] = "user-slash";
@@ -6018,7 +6046,7 @@ function resolvePreferences(dataDirOverride2, cwd) {
6018
6046
  const project = readProjectPreferences(cwd);
6019
6047
  return { ...global, ...project };
6020
6048
  }
6021
- function writePreferences(prefs, scope, dataDirOverride2, cwd) {
6049
+ function writePreferences2(prefs, scope, dataDirOverride2, cwd) {
6022
6050
  const dir = scope === "project" && cwd ? path2.join(cwd, PREFERENCES_PROJECT_DIR_NAME) : dataDirOverride2 ?? getDataDir();
6023
6051
  fs5.mkdirSync(dir, { recursive: true });
6024
6052
  const filePath = path2.join(dir, PREFERENCES_FILE_NAME);
@@ -6584,7 +6612,7 @@ var preferencesSetTool = {
6584
6612
  const logger6 = createChildLogger2(ctx.correlationId);
6585
6613
  logger6.info("Executing muggle-local-preferences-set");
6586
6614
  const input = PreferencesSetInputSchema.parse(ctx.input);
6587
- writePreferences(
6615
+ writePreferences2(
6588
6616
  { [input.key]: input.value },
6589
6617
  input.scope,
6590
6618
  void 0,
@@ -7056,4 +7084,4 @@ var plugins_exports = {};
7056
7084
  // packages/mcps/src/mcp/agents/index.ts
7057
7085
  var agents_exports = {};
7058
7086
 
7059
- export { DEFAULT_PREFERENCES, EventName, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ServiceName, Surface, __export, __require, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports2 as e2e_exports, formatPreferencesOneLiner, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir2 as getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, initTelemetry, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports2 as local_exports, mcp_exports, openBrowserUrl, performLogin, performLogout, pollDeviceCode, readGlobalPreferences, readProjectPreferences, resetConfig, resetLogger, resetPreference, resolveElectronAppPathOrNull, resolvePreferences, saveApiKey, saveApiKeyData, saveCredentials, src_exports, startDeviceCodeFlow, toolRequiresAuth, track, validatePreference, verifyFileChecksum, writePreferences };
7087
+ export { DEFAULT_PREFERENCES, EventName, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ServiceName, Surface, __export, __require, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports2 as e2e_exports, formatPreferencesOneLiner, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir2 as getDataDir, getDisclosureCopy, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, hasShownDisclosure, initTelemetry, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports2 as local_exports, markDisclosureShown, mcp_exports, openBrowserUrl, performLogin, performLogout, pollDeviceCode, readGlobalPreferences, readProjectPreferences, resetConfig, resetLogger, resetPreference, resolveElectronAppPathOrNull, resolvePreferences, saveApiKey, saveApiKeyData, saveCredentials, src_exports, startDeviceCodeFlow, toolRequiresAuth, track, validatePreference, verifyFileChecksum, writePreferences2 as writePreferences };
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-Q7WNEMN4.js';
3
- import './chunk-ZKRHFTIC.js';
2
+ import { runCli } from './chunk-7WRAMBOS.js';
3
+ import './chunk-AYWLYB3F.js';
4
4
 
5
5
  // src/cli/main.ts
6
6
  runCli().catch((error) => {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-Q7WNEMN4.js';
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-ZKRHFTIC.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-7WRAMBOS.js';
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-AYWLYB3F.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.11.0",
4
+ "version": "4.11.1",
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.11.0",
5
+ "version": "4.11.1",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -101,18 +101,19 @@ fix(ci): lint — remove unused import
101
101
 
102
102
  ## Classify
103
103
 
104
- Classify the **review as a unit**, not individual comments.
104
+ Classify the **review as a unit** — but reply per line comment (threaded), not per review.
105
105
 
106
106
  | Class | Signal | Action |
107
107
  | :---- | :----- | :----- |
108
- | **actionable** | Review names at least one concrete change or asks an answerable question. Soft phrasing counts when there's a concrete referent. | Treat as amended requirements; run the caller's implementation cycle; reply with one summary referencing the new SHA. |
108
+ | **actionable** | Review names at least one concrete change or asks an answerable question. Soft phrasing counts when there's a concrete referent. | Treat as amended requirements; run **one** implementation cycle for the whole review; reply **threaded per line comment** referencing the new SHA (top-level only when the review is body-only). |
109
109
  | **ambiguous** | No actionable signal — pure vibes, contradictory, or depends on knowledge the loop can't access. | Escalate once with two interpretations; pause the PR. |
110
110
 
111
111
  Default to **actionable**. CI catches wrong attempts; reviewers correct on the next round. Escalation is a round-trip with an absent user — reserve it.
112
112
 
113
- Reply summary shape:
113
+ Reply shape (all replies for one review reference the same SHA):
114
114
 
115
- - **actionable**: `Addressed review <review_id> in <sha> — cycle ran clean (or: with <N> failures, see walkthrough).`
115
+ - **threaded** (default): `Done in <sha> — <attribution>. (Review #<review_id>, cycle <status>.)`
116
+ - **top-level** (fallback, body-only reviews): `Re: review #<review_id> — addressed in <sha>, cycle <status>.`
116
117
  - **ambiguous**: no bot reply.
117
118
 
118
119
  ### Worked examples — Actionable reviews
@@ -86,10 +86,7 @@ When the review is actionable:
86
86
  2. **Amend `requirements.md`** in the session dir with a new `## Amendment — review <review_id> by <login> (<timestamp>)` section pasting the review body and each comment (with `<file>:<line>` context).
87
87
  3. **Invoke the implementation cycle** declared in the caller's `cycle.json`. Iterate the `steps[]` in order. Each step is either a markdown file to follow, a skill to invoke, or a shell command (per the `cycle.json` schema in SKILL.md). When a step fails, the cycle returns `failed: <step-name>`; the loop escalates per Step 8 with the failure as the reason.
88
88
  4. **Push** via `cycle.json`'s `pushHandler`. Set `last_seen.last_pushed_sha` to the new HEAD.
89
- 5. **Reply** with one summary via `gh pr comment <n>`:
90
- ```
91
- Addressed review <review_id> in <sha> — cycle ran clean (or: with <N> failures, see walkthrough).
92
- ```
89
+ 5. **Reply** per [helpers § Reply routing](../_shared/pr-followup-helpers.md#reply-routing) and [§ Classify](../_shared/pr-followup-helpers.md#classify) (reply shape). For each line comment in the review, derive `<attribution>` from `git diff <last_pushed_sha>..HEAD -- <comment.path>` near `comment.line` ±5 (fall back to `addressed indirectly — see walkthrough` if empty). `<status>` = `ran clean` or `had <N> failures, see walkthrough`. If the review is body-only (no line comments), post the top-level fallback shape; if both body and line comments, threaded replies cover it — no top-level.
93
90
  6. **Resume polling**: clear `cycling: true`, increment `cycles_completed`, advance `last_seen.reviewId` past this review.
94
91
  7. Emit per-cycle telemetry.
95
92
 
@@ -133,9 +130,10 @@ Emit one tick event per `muggle-local-telemetry-skill-emit`. Exit the turn.
133
130
 
134
131
  ## Reply routing
135
132
 
136
- - **Summary reply on a review**: `gh pr comment <number> --body "..."` referencing the review id and the new SHA. There's no "reply to a review" endpoint.
137
- - **Reply to a specific line comment** (optional): `POST /repos/{owner}/{repo}/pulls/{n}/comments/{comment_id}/replies`.
138
- - **Never post the same summary twice** — `last_seen.reviewId` is the only re-entry guard.
133
+ - **Threaded reply per line comment** (default): `POST /repos/{owner}/{repo}/pulls/{n}/comments/{comment_id}/replies`. Use for every line comment in the review so each thread can be resolved in GitHub's UI.
134
+ - **Top-level summary on a body-only review** (fallback): `gh pr comment <number> --body "..."` referencing the review id and the new SHA. Used only when the review has body content and zero line comments — GitHub has no "reply to a review body" endpoint.
135
+ - **Never post the same reply twice** — `last_seen.reviewId` is the only re-entry guard.
136
+ - **Never post a top-level summary alongside threaded replies** — duplication pollutes the Conversation tab.
139
137
 
140
138
  ## Telemetry
141
139
 
@@ -187,5 +185,6 @@ This stage produces no console output beyond:
187
185
  - [ ] `followup.log` has at minimum a heartbeat or per-review line for this tick.
188
186
  - [ ] Telemetry events emitted (per-cycle when applicable + per-tick).
189
187
  - [ ] If pushed, `last_pushed_sha` is set and `cycles_completed` incremented.
188
+ - [ ] If actionable, one threaded reply posted per line comment (or one top-level reply for body-only reviews) — never both, never zero.
190
189
  - [ ] If escalated, `escalated_review_ids` contains the review id.
191
190
  - [ ] If terminal, the loop is NOT continued.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release": "4.11.0",
3
- "buildId": "run-33-1",
4
- "commitSha": "f704789e0836e91f5e24f364d0d8eccf1d1a5439",
5
- "buildTime": "2026-05-15T07:07:24Z",
2
+ "release": "4.11.1",
3
+ "buildId": "run-35-1",
4
+ "commitSha": "91afe33fb96f46115bd399141f165832253b4809",
5
+ "buildTime": "2026-05-19T23:10:43Z",
6
6
  "serviceName": "muggle-ai-works-mcp"
7
7
  }
@@ -1 +1 @@
1
- export { DEFAULT_PREFERENCES, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports as e2e, formatPreferencesOneLiner, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports as localQa, mcp_exports as mcp, openBrowserUrl, performLogin, performLogout, pollDeviceCode, e2e_exports as qa, readGlobalPreferences, readProjectPreferences, resetConfig, resetLogger, resetPreference, resolveElectronAppPathOrNull, resolvePreferences, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, toolRequiresAuth, validatePreference, verifyFileChecksum, writePreferences } from './chunk-ZKRHFTIC.js';
1
+ export { DEFAULT_PREFERENCES, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports as e2e, formatPreferencesOneLiner, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports as localQa, mcp_exports as mcp, openBrowserUrl, performLogin, performLogout, pollDeviceCode, e2e_exports as qa, readGlobalPreferences, readProjectPreferences, resetConfig, resetLogger, resetPreference, resolveElectronAppPathOrNull, resolvePreferences, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, toolRequiresAuth, validatePreference, verifyFileChecksum, writePreferences } from './chunk-AYWLYB3F.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@muggleai/works",
3
3
  "mcpName": "io.github.multiplex-ai/muggle",
4
- "version": "4.11.0",
4
+ "version": "4.11.2",
5
5
  "description": "Ship quality products with AI-powered E2E acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -41,14 +41,14 @@
41
41
  "test:watch": "vitest"
42
42
  },
43
43
  "muggleConfig": {
44
- "electronAppVersion": "1.0.84",
44
+ "electronAppVersion": "1.0.87",
45
45
  "downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
46
46
  "runtimeTargetDefault": "production",
47
47
  "checksums": {
48
- "darwin-arm64": "da978efea8e3c8633f96c5245b932f98988eb60dc94cced62cac20f4bdefbb20",
49
- "darwin-x64": "9aa611d446494ab1d48674813cead2d669f8aea6869efea6f4f2beb13d18e4ac",
50
- "win32-x64": "5220df3c394f2406201955f04e78d8faf764ea565fc59611484c8bbaad24635e",
51
- "linux-x64": "5c519a473800b2726eef7a55c50d1d2c2e0ff78ab2d860dc869e2cd42fa59d74"
48
+ "darwin-arm64": "df04c98269e22ae427bb863956215b3f20cbf2ff0b51c037647d6e734a7b7502",
49
+ "darwin-x64": "e9f5b8a61f4a9f47743aaabb27d6e4c462dca5aa922ba66ae04f0a0133b8a7eb",
50
+ "win32-x64": "75e552672757d280259637bdcbe0aae0eecc2417ed9a6e5bd49f42d3466f66cf",
51
+ "linux-x64": "2c8b64cf549f9a6682db959b17c5f04578d632d5bcdba1ac9efbb45d6a097802"
52
52
  }
53
53
  },
54
54
  "dependencies": {
@@ -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.11.0",
4
+ "version": "4.11.1",
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.11.0",
5
+ "version": "4.11.1",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -101,18 +101,19 @@ fix(ci): lint — remove unused import
101
101
 
102
102
  ## Classify
103
103
 
104
- Classify the **review as a unit**, not individual comments.
104
+ Classify the **review as a unit** — but reply per line comment (threaded), not per review.
105
105
 
106
106
  | Class | Signal | Action |
107
107
  | :---- | :----- | :----- |
108
- | **actionable** | Review names at least one concrete change or asks an answerable question. Soft phrasing counts when there's a concrete referent. | Treat as amended requirements; run the caller's implementation cycle; reply with one summary referencing the new SHA. |
108
+ | **actionable** | Review names at least one concrete change or asks an answerable question. Soft phrasing counts when there's a concrete referent. | Treat as amended requirements; run **one** implementation cycle for the whole review; reply **threaded per line comment** referencing the new SHA (top-level only when the review is body-only). |
109
109
  | **ambiguous** | No actionable signal — pure vibes, contradictory, or depends on knowledge the loop can't access. | Escalate once with two interpretations; pause the PR. |
110
110
 
111
111
  Default to **actionable**. CI catches wrong attempts; reviewers correct on the next round. Escalation is a round-trip with an absent user — reserve it.
112
112
 
113
- Reply summary shape:
113
+ Reply shape (all replies for one review reference the same SHA):
114
114
 
115
- - **actionable**: `Addressed review <review_id> in <sha> — cycle ran clean (or: with <N> failures, see walkthrough).`
115
+ - **threaded** (default): `Done in <sha> — <attribution>. (Review #<review_id>, cycle <status>.)`
116
+ - **top-level** (fallback, body-only reviews): `Re: review #<review_id> — addressed in <sha>, cycle <status>.`
116
117
  - **ambiguous**: no bot reply.
117
118
 
118
119
  ### Worked examples — Actionable reviews
@@ -86,10 +86,7 @@ When the review is actionable:
86
86
  2. **Amend `requirements.md`** in the session dir with a new `## Amendment — review <review_id> by <login> (<timestamp>)` section pasting the review body and each comment (with `<file>:<line>` context).
87
87
  3. **Invoke the implementation cycle** declared in the caller's `cycle.json`. Iterate the `steps[]` in order. Each step is either a markdown file to follow, a skill to invoke, or a shell command (per the `cycle.json` schema in SKILL.md). When a step fails, the cycle returns `failed: <step-name>`; the loop escalates per Step 8 with the failure as the reason.
88
88
  4. **Push** via `cycle.json`'s `pushHandler`. Set `last_seen.last_pushed_sha` to the new HEAD.
89
- 5. **Reply** with one summary via `gh pr comment <n>`:
90
- ```
91
- Addressed review <review_id> in <sha> — cycle ran clean (or: with <N> failures, see walkthrough).
92
- ```
89
+ 5. **Reply** per [helpers § Reply routing](../_shared/pr-followup-helpers.md#reply-routing) and [§ Classify](../_shared/pr-followup-helpers.md#classify) (reply shape). For each line comment in the review, derive `<attribution>` from `git diff <last_pushed_sha>..HEAD -- <comment.path>` near `comment.line` ±5 (fall back to `addressed indirectly — see walkthrough` if empty). `<status>` = `ran clean` or `had <N> failures, see walkthrough`. If the review is body-only (no line comments), post the top-level fallback shape; if both body and line comments, threaded replies cover it — no top-level.
93
90
  6. **Resume polling**: clear `cycling: true`, increment `cycles_completed`, advance `last_seen.reviewId` past this review.
94
91
  7. Emit per-cycle telemetry.
95
92
 
@@ -133,9 +130,10 @@ Emit one tick event per `muggle-local-telemetry-skill-emit`. Exit the turn.
133
130
 
134
131
  ## Reply routing
135
132
 
136
- - **Summary reply on a review**: `gh pr comment <number> --body "..."` referencing the review id and the new SHA. There's no "reply to a review" endpoint.
137
- - **Reply to a specific line comment** (optional): `POST /repos/{owner}/{repo}/pulls/{n}/comments/{comment_id}/replies`.
138
- - **Never post the same summary twice** — `last_seen.reviewId` is the only re-entry guard.
133
+ - **Threaded reply per line comment** (default): `POST /repos/{owner}/{repo}/pulls/{n}/comments/{comment_id}/replies`. Use for every line comment in the review so each thread can be resolved in GitHub's UI.
134
+ - **Top-level summary on a body-only review** (fallback): `gh pr comment <number> --body "..."` referencing the review id and the new SHA. Used only when the review has body content and zero line comments — GitHub has no "reply to a review body" endpoint.
135
+ - **Never post the same reply twice** — `last_seen.reviewId` is the only re-entry guard.
136
+ - **Never post a top-level summary alongside threaded replies** — duplication pollutes the Conversation tab.
139
137
 
140
138
  ## Telemetry
141
139
 
@@ -187,5 +185,6 @@ This stage produces no console output beyond:
187
185
  - [ ] `followup.log` has at minimum a heartbeat or per-review line for this tick.
188
186
  - [ ] Telemetry events emitted (per-cycle when applicable + per-tick).
189
187
  - [ ] If pushed, `last_pushed_sha` is set and `cycles_completed` incremented.
188
+ - [ ] If actionable, one threaded reply posted per line comment (or one top-level reply for body-only reviews) — never both, never zero.
190
189
  - [ ] If escalated, `escalated_review_ids` contains the review id.
191
190
  - [ ] If terminal, the loop is NOT continued.