@muggleai/works 4.12.4 → 4.13.1

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.
Files changed (59) hide show
  1. package/dist/{chunk-JNI7INIO.js → chunk-5G7WI7IY.js} +2 -2
  2. package/dist/{chunk-CPF6AR2I.js → chunk-YKR2TQ24.js} +53 -37
  3. package/dist/cli.js +2 -2
  4. package/dist/index.js +2 -2
  5. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  6. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  7. package/dist/plugin/skills/_shared/github-cli-recipes/submitted-reviews.md +1 -1
  8. package/dist/plugin/skills/_shared/github-cli-recipes/unresolved-threads.md +6 -4
  9. package/dist/plugin/skills/_shared/pr-followup-helpers/allow-list.md +1 -1
  10. package/dist/plugin/skills/_shared/pr-followup-helpers/classify.md +3 -4
  11. package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +22 -0
  12. package/dist/plugin/skills/_shared/pr-followup-helpers.md +1 -0
  13. package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +56 -0
  14. package/dist/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
  15. package/dist/plugin/skills/_shared/test-case-chain-readiness.md +41 -0
  16. package/dist/plugin/skills/do/address-reviews.md +14 -8
  17. package/dist/plugin/skills/do/e2e-acceptance.md +7 -3
  18. package/dist/plugin/skills/do/per-comment-replies.md +8 -2
  19. package/dist/plugin/skills/do/resolve-reminder.md +16 -18
  20. package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
  21. package/dist/plugin/skills/muggle-pr-followup/SKILL.md +10 -3
  22. package/dist/plugin/skills/muggle-pr-followup/auto-track.md +91 -0
  23. package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +11 -4
  24. package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +2 -1
  25. package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +7 -1
  26. package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +6 -3
  27. package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +12 -0
  28. package/dist/plugin/skills/muggle-preferences/ops/configure.md +1 -1
  29. package/dist/plugin/skills/muggle-preferences/preference-gates/autoReuseValidationContext.md +11 -0
  30. package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +7 -0
  31. package/dist/release-manifest.json +4 -4
  32. package/dist/{src-YR5UKLPC.js → src-ECRJW2LY.js} +1 -1
  33. package/package.json +6 -6
  34. package/plugin/.claude-plugin/plugin.json +1 -1
  35. package/plugin/.cursor-plugin/plugin.json +1 -1
  36. package/plugin/skills/_shared/github-cli-recipes/submitted-reviews.md +1 -1
  37. package/plugin/skills/_shared/github-cli-recipes/unresolved-threads.md +6 -4
  38. package/plugin/skills/_shared/pr-followup-helpers/allow-list.md +1 -1
  39. package/plugin/skills/_shared/pr-followup-helpers/classify.md +3 -4
  40. package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +22 -0
  41. package/plugin/skills/_shared/pr-followup-helpers.md +1 -0
  42. package/plugin/skills/_shared/resolve-e2e-validation-context.md +56 -0
  43. package/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
  44. package/plugin/skills/_shared/test-case-chain-readiness.md +41 -0
  45. package/plugin/skills/do/address-reviews.md +14 -8
  46. package/plugin/skills/do/e2e-acceptance.md +7 -3
  47. package/plugin/skills/do/per-comment-replies.md +8 -2
  48. package/plugin/skills/do/resolve-reminder.md +16 -18
  49. package/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
  50. package/plugin/skills/muggle-pr-followup/SKILL.md +10 -3
  51. package/plugin/skills/muggle-pr-followup/auto-track.md +91 -0
  52. package/plugin/skills/muggle-pr-followup/bootstrap.md +11 -4
  53. package/plugin/skills/muggle-pr-followup/output-templates/help.md +2 -1
  54. package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +7 -1
  55. package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +6 -3
  56. package/plugin/skills/muggle-pr-followup/state-schemas.md +12 -0
  57. package/plugin/skills/muggle-preferences/ops/configure.md +1 -1
  58. package/plugin/skills/muggle-preferences/preference-gates/autoReuseValidationContext.md +11 -0
  59. package/plugin/skills/muggle-test-feature-local/SKILL.md +7 -0
@@ -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, hasShownDisclosure, getDisclosureCopy, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-CPF6AR2I.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-YKR2TQ24.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';
@@ -736,7 +736,7 @@ async function resolveGsScreenshotUrls(report, opts) {
736
736
  if (gsUrls.length === 0) {
737
737
  return report;
738
738
  }
739
- const mcps = await import('./src-YR5UKLPC.js');
739
+ const mcps = await import('./src-ECRJW2LY.js');
740
740
  const credentials = await mcps.getCallerCredentialsAsync();
741
741
  if (!credentials.bearerToken && !credentials.apiKey) {
742
742
  stderrWrite(
@@ -3611,6 +3611,15 @@ var EventName;
3611
3611
  EventName2["SystemDisclosureShown"] = "system.disclosure_shown";
3612
3612
  })(EventName || (EventName = {}));
3613
3613
  var MuggleEntityIdSchema = z.string().uuid();
3614
+ var RunEnvironment = /* @__PURE__ */ ((RunEnvironment2) => {
3615
+ RunEnvironment2["Local"] = "local";
3616
+ RunEnvironment2["Remote"] = "remote";
3617
+ return RunEnvironment2;
3618
+ })(RunEnvironment || {});
3619
+ var RunEnvironmentSchema = z.enum(["local" /* Local */, "remote" /* Remote */]);
3620
+ var RunEnvironmentInputSchema = RunEnvironmentSchema.optional().describe(
3621
+ "Environment lane for the run: 'local' (developer localhost) or 'remote' (deployed). Selects which versioned runSettings/credentials lane the cloud resolves. Omit for remote (default)."
3622
+ );
3614
3623
  var LocalExecutionContextInputSchema = z.object({
3615
3624
  originalUrl: z.string().url().describe("Original local URL used during local execution (typically localhost)"),
3616
3625
  productionUrl: z.string().url().describe("Cloud production URL for the test case"),
@@ -3627,6 +3636,7 @@ var LocalRunUploadInputSchema = z.object({
3627
3636
  useCaseId: MuggleEntityIdSchema.describe("Use case ID (UUID) for the local run"),
3628
3637
  testCaseId: MuggleEntityIdSchema.describe("Test case ID (UUID) for the local run"),
3629
3638
  runType: z.enum(["generation", "replay"]).describe("Type of local run to upload"),
3639
+ runEnvironmentType: RunEnvironmentInputSchema,
3630
3640
  productionUrl: z.string().url().describe("Cloud production URL associated with the run"),
3631
3641
  localExecutionContext: LocalExecutionContextInputSchema.describe("Local execution metadata"),
3632
3642
  actionScript: z.array(z.unknown()).min(1).describe("Generated action script steps from local execution"),
@@ -3828,6 +3838,9 @@ var TestCaseListInputSchema = z.object({
3828
3838
  var TestCaseGetInputSchema = z.object({
3829
3839
  testCaseId: IdSchema.describe("Test case ID (UUID) to retrieve")
3830
3840
  });
3841
+ var TestCaseAncestorsGetInputSchema = z.object({
3842
+ testCaseId: IdSchema.describe("Test case ID (UUID) to resolve the test-plan-graph ancestor chain for")
3843
+ });
3831
3844
  var TestCaseListByUseCaseInputSchema = z.object({
3832
3845
  useCaseId: IdSchema.describe("Use case ID (UUID) to list test cases for")
3833
3846
  });
@@ -3906,6 +3919,7 @@ var WorkflowStartTestScriptGenerationInputSchema = z.object({
3906
3919
  precondition: z.string().min(1).describe("Preconditions"),
3907
3920
  instructions: z.string().min(1).describe("Step-by-step instructions"),
3908
3921
  expectedResult: z.string().min(1).describe("Expected result"),
3922
+ runEnvironmentType: RunEnvironmentInputSchema,
3909
3923
  workflowParams: WorkflowParamsSchema
3910
3924
  });
3911
3925
  var WorkflowStartTestScriptGenerationBulkInputSchema = z.object({
@@ -4403,42 +4417,16 @@ function slimEntry(raw) {
4403
4417
  if (raw.latestWorkflowRun?.id) slim.latestWorkflowRunId = raw.latestWorkflowRun.id;
4404
4418
  return slim;
4405
4419
  }
4406
- function aggregateByStatus(entries) {
4407
- const counts = {};
4408
- for (const entry of entries) {
4409
- const status = entry.status ?? UNKNOWN_STATUS;
4410
- counts[status] = (counts[status] ?? 0) + 1;
4411
- }
4412
- return counts;
4413
- }
4414
- function sortRuns(runs, sortBy, sortOrder) {
4415
- const sign = sortOrder === "asc" ? 1 : -1;
4416
- return [...runs].sort((a, b) => {
4417
- const av = a[sortBy];
4418
- const bv = b[sortBy];
4419
- if (av === void 0 && bv === void 0) return 0;
4420
- if (av === void 0) return 1;
4421
- if (bv === void 0) return -1;
4422
- if (av < bv) return -1 * sign;
4423
- if (av > bv) return 1 * sign;
4424
- return 0;
4425
- });
4426
- }
4427
- function mapTestRunsSummary(response, input) {
4428
- const params = input;
4429
- const raw = Array.isArray(response.data) ? response.data : [];
4430
- const byStatus = aggregateByStatus(raw);
4431
- const sorted = sortRuns(raw.map(slimEntry), params.sortBy, params.sortOrder);
4432
- const total = sorted.length;
4433
- const totalPages = Math.max(1, Math.ceil(total / params.pageSize));
4434
- const start = (params.page - 1) * params.pageSize;
4435
- const runs = sorted.slice(start, start + params.pageSize);
4420
+ function mapTestRunsSummary(response) {
4421
+ const envelope = response.data ?? {};
4422
+ const rawRuns = Array.isArray(envelope.data) ? envelope.data : [];
4423
+ const runs = rawRuns.map(slimEntry);
4436
4424
  return {
4437
- totals: { total, byStatus },
4438
- page: params.page,
4439
- pageSize: params.pageSize,
4440
- totalPages,
4441
- hasMore: params.page < totalPages,
4425
+ page: envelope.page ?? 1,
4426
+ pageSize: envelope.pageSize ?? runs.length,
4427
+ totalCount: envelope.totalCount ?? runs.length,
4428
+ totalPages: envelope.totalPages ?? (runs.length === 0 ? 0 : 1),
4429
+ hasMore: envelope.hasMore ?? false,
4442
4430
  runs
4443
4431
  };
4444
4432
  }
@@ -4716,6 +4704,18 @@ var testCaseTools = [
4716
4704
  };
4717
4705
  }
4718
4706
  },
4707
+ {
4708
+ name: "muggle-remote-test-case-ancestors-get",
4709
+ description: "Resolve a test case's prerequisite chain from the project's test-plan graph. Returns { testCaseId, ancestors, orphan } where `ancestors` is an array of test case IDs ordered immediate-parent \u2192 root (empty when the case is a graph root). `orphan: true` means the case has no graph node, so it has no prerequisites. Call this before generating or replaying a script to ensure every prerequisite test case already has a ready script.",
4710
+ inputSchema: TestCaseAncestorsGetInputSchema,
4711
+ mapToUpstream: (input) => {
4712
+ const data = input;
4713
+ return {
4714
+ method: "GET",
4715
+ path: `${MUGGLE_TEST_PREFIX}/test-plan-graph/test-cases/${data.testCaseId}/ancestors`
4716
+ };
4717
+ }
4718
+ },
4719
4719
  {
4720
4720
  name: "muggle-remote-test-case-list-by-use-case",
4721
4721
  description: "List test cases for a specific use case.",
@@ -5010,6 +5010,7 @@ var workflowTools = [
5010
5010
  precondition: data.precondition,
5011
5011
  instructions: data.instructions,
5012
5012
  expectedResult: data.expectedResult,
5013
+ ...data.runEnvironmentType && { runEnvironmentType: data.runEnvironmentType },
5013
5014
  ...data.workflowParams && { workflowParams: data.workflowParams }
5014
5015
  },
5015
5016
  timeoutMs: getWorkflowTimeoutMs()
@@ -5191,6 +5192,7 @@ var workflowTools = [
5191
5192
  useCaseId: data.useCaseId,
5192
5193
  testCaseId: data.testCaseId,
5193
5194
  runType: data.runType,
5195
+ ...data.runEnvironmentType && { runEnvironmentType: data.runEnvironmentType },
5194
5196
  productionUrl: data.productionUrl,
5195
5197
  localExecutionContext: {
5196
5198
  originalUrl: data.localExecutionContext.originalUrl,
@@ -5239,13 +5241,19 @@ var reportTools = [
5239
5241
  },
5240
5242
  {
5241
5243
  name: "muggle-remote-project-test-runs-summary-get",
5242
- description: "Get a paginated, slimmed summary of latest test runs for a project. Response shape: { totals: { total, byStatus }, page, pageSize, totalPages, hasMore, runs: [{ status, testCaseId, testCaseTitle, useCaseId, useCaseTitle, lastRunAt, error, latestWorkflowRunId }] }. Returns 20 runs per page by default (max 100). `totals` covers the full project; `runs` is the page slice. Check `hasMore` to decide whether to fetch additional pages. Use muggle-remote-test-case-get / muggle-remote-wf-get-ts-replay-latest-run for full per-run detail.",
5244
+ description: "Get a paginated, slimmed summary of latest test runs for a project. Response shape: { page, pageSize, totalCount, totalPages, hasMore, runs: [{ status, testCaseId, testCaseTitle, useCaseId, useCaseTitle, lastRunAt, error, latestWorkflowRunId }] }. Returns 20 runs per page by default (max 100). `totalCount` is the project-wide total after the replay-status filter; `runs` is the page slice. Check `hasMore` to decide whether to fetch additional pages. Use muggle-remote-test-case-get / muggle-remote-wf-get-ts-replay-latest-run for full per-run detail.",
5243
5245
  inputSchema: ProjectTestRunsSummaryInputSchema,
5244
5246
  mapToUpstream: (input) => {
5245
5247
  const data = input;
5246
5248
  return {
5247
5249
  method: "GET",
5248
- path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/test-runs/summary`
5250
+ path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/test-runs/summary/paginated`,
5251
+ queryParams: {
5252
+ page: data.page,
5253
+ pageSize: data.pageSize,
5254
+ sortBy: data.sortBy,
5255
+ sortOrder: data.sortOrder
5256
+ }
5249
5257
  };
5250
5258
  },
5251
5259
  mapFromUpstream: mapTestRunsSummary
@@ -6925,6 +6933,10 @@ var publishTestScriptTool = {
6925
6933
  useCaseId: runResult.useCaseId,
6926
6934
  testCaseId: input.cloudTestCaseId,
6927
6935
  runType: runResult.runType,
6936
+ // A published run always originates from local Electron execution, so it
6937
+ // belongs to the local lane — the cloud must resolve the developer's
6938
+ // localhost credentials, not the remote managed-profile pool.
6939
+ runEnvironmentType: "local" /* Local */,
6928
6940
  productionUrl: runResult.productionUrl,
6929
6941
  localExecutionContext: {
6930
6942
  originalUrl: runResult.localExecutionContext.originalUrl,
@@ -7299,12 +7311,16 @@ __export(e2e_exports2, {
7299
7311
  ReportPreferencesUpsertInputSchema: () => ReportPreferencesUpsertInputSchema,
7300
7312
  ReportStatsSummaryInputSchema: () => ReportStatsSummaryInputSchema,
7301
7313
  RunBatchIdSchema: () => RunBatchIdSchema,
7314
+ RunEnvironment: () => RunEnvironment,
7315
+ RunEnvironmentInputSchema: () => RunEnvironmentInputSchema,
7316
+ RunEnvironmentSchema: () => RunEnvironmentSchema,
7302
7317
  SecretCreateInputSchema: () => SecretCreateInputSchema,
7303
7318
  SecretDeleteInputSchema: () => SecretDeleteInputSchema,
7304
7319
  SecretGetInputSchema: () => SecretGetInputSchema,
7305
7320
  SecretListInputSchema: () => SecretListInputSchema,
7306
7321
  SecretUpdateInputSchema: () => SecretUpdateInputSchema,
7307
7322
  StripePaymentMethodIdSchema: () => StripePaymentMethodIdSchema,
7323
+ TestCaseAncestorsGetInputSchema: () => TestCaseAncestorsGetInputSchema,
7308
7324
  TestCaseCreateInputSchema: () => TestCaseCreateInputSchema,
7309
7325
  TestCaseGenerateFromPromptInputSchema: () => TestCaseGenerateFromPromptInputSchema,
7310
7326
  TestCaseGetInputSchema: () => TestCaseGetInputSchema,
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-JNI7INIO.js';
3
- import './chunk-CPF6AR2I.js';
2
+ import { runCli } from './chunk-5G7WI7IY.js';
3
+ import './chunk-YKR2TQ24.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-JNI7INIO.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-CPF6AR2I.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-5G7WI7IY.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-YKR2TQ24.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.12.4",
4
+ "version": "4.13.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.12.4",
5
+ "version": "4.13.1",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -13,4 +13,4 @@ Filter client-side:
13
13
  - `id` not in `last_seen.escalated_review_ids`
14
14
  - `user.login` in the resolved allow-list
15
15
  - `state` in `{CHANGES_REQUESTED, COMMENTED}`, OR `APPROVED` with a non-empty body or at least one line comment
16
- - **Not a reply-wrapper.** `POST /pulls/<n>/comments/<id>/replies` creates an implicit review whose comments all have `in_reply_to_id` set. Fetch each candidate review's comments via `gh api repos/<owner>/<repo>/pulls/<n>/reviews/<id>/comments` and drop the review if every comment has a non-null `in_reply_to_id` (no new top-level critique). Without this clause, the loop's own threaded replies — submitted under the PR author's identity in single-account workflows — pass the allow-list and re-dispatch `/muggle-do` on a no-op cycle.
16
+ - **Not a loop echo.** `POST /pulls/<n>/comments/<id>/replies` creates an implicit review whose comments all have `in_reply_to_id` set. Fetch each candidate review's comments via `gh api repos/<owner>/<repo>/pulls/<n>/reviews/<id>/comments` and drop the review **only if every comment is a reply (`in_reply_to_id != null`) and carries the loop marker `<!-- muggle-do:bot -->`** (see [`../pr-followup-helpers/loop-signature.md`](../pr-followup-helpers/loop-signature.md)). A reply-only wrapper with any comment **lacking** the marker is a human follow-up — keep it; the round addresses it. Matching the marker, not structure, is what skips the loop's own replies (posted under the author's identity in single-account workflows) without dropping genuine follow-ups.
@@ -26,8 +26,10 @@ query($owner: String!, $name: String!, $number: Int!) {
26
26
  }'
27
27
  ```
28
28
 
29
- Filter client-side to `isResolved == false`. Classify each thread by inspecting its comments:
29
+ Filter client-side to `isResolved == false`. Walk each thread's comments in `createdAt` order and classify by the loop marker (see [`../pr-followup-helpers/loop-signature.md`](../pr-followup-helpers/loop-signature.md)), not by `author.login` — the login is ambiguous under a shared account:
30
30
 
31
- - **Addressed by loop** — at least one comment authored by the loop user citing a SHA in `last_seen.pushed_shas[]`.
32
- - **Addressed by human** — at least one comment authored by a non-loop user after the original, and no addressed-by-loop signal.
33
- - **Not addressed** — otherwise.
31
+ - **Addressed, awaiting resolve** — the **newest** comment carries the loop marker `<!-- muggle-do:bot -->`. The loop has replied and nothing newer is waiting. → resolve-reminder.
32
+ - **Unaddressed human comment** — the newest comment lacks the marker and is newer than the thread's newest loop-marked comment (or the thread has no loop comment yet). → actionable: the round should address it.
33
+ - **Not addressed** — indeterminate (e.g. no comments).
34
+
35
+ A loop comment also cites a `<short-sha>` from `last_seen.pushed_shas[]` in its body, which tells *which* push addressed the thread.
@@ -2,7 +2,7 @@
2
2
 
3
3
  The address-reviews flow only acts on reviews submitted by users in the **allow-list** = (requested reviewers ∪ CODEOWNERS ∪ {PR author}) − bots. Re-resolve every invocation — never cache across cycles.
4
4
 
5
- The PR author is implicitly a valid reviewer: in single-account workflows, the human running the agent and the PR's author are the same identity, and the agent must honor their reviews. Self-loop is prevented at the watcher's filter layer rather than here: `POST /pulls/<n>/comments/<id>/replies` does create an implicit review under the loop user's identity, and the watcher drops it via the reply-wrapper clause in [`../github-cli-recipes/submitted-reviews.md`](../github-cli-recipes/submitted-reviews.md). Including the PR author in this allow-list is therefore safe.
5
+ The PR author is implicitly a valid reviewer: in single-account workflows, the human running the agent and the PR's author are the same identity, and the agent must honor their reviews. Self-loop is prevented at the watcher's filter layer rather than here: `POST /pulls/<n>/comments/<id>/replies` does create an implicit review under the loop user's identity, and the watcher drops it via the loop-echo clause in [`../github-cli-recipes/submitted-reviews.md`](../github-cli-recipes/submitted-reviews.md) — matched by the loop signature ([`loop-signature.md`](loop-signature.md)), so the author's *genuine* reviews and thread replies still get through. Including the PR author in this allow-list is therefore safe.
6
6
 
7
7
  ## Step 1: requested reviewers
8
8
 
@@ -4,12 +4,11 @@ Classify the **review as a unit** — but reply per line comment (threaded), not
4
4
 
5
5
  ## Pre-check: self-loop filter
6
6
 
7
- GitHub auto-creates a synthetic review every time the agent posts `POST /comments/<id>/replies`. That review has the loop user as author, an empty body, and contains only the agent's own reply comments (`in_reply_to_id != null`). It carries no reviewer intent and must not trigger another cycle.
7
+ GitHub auto-creates a synthetic review every time the agent posts `POST /comments/<id>/replies`. In single-account workflows the loop posts under the PR author's own identity, so that synthetic review is indistinguishable from a human's thread reply by author or structure alone — both are reply-only wrappers. The **loop signature** is what separates them (see [`loop-signature.md`](loop-signature.md)).
8
8
 
9
- A review is a **self-loop** iff:
9
+ A review is a **self-loop** iff **every** line comment under it is a reply (`in_reply_to_id != null`) **and** carries the loop marker `<!-- muggle-do:bot -->`.
10
10
 
11
- - `body` is empty, AND
12
- - every line comment under it has `in_reply_to_id != null`
11
+ If any comment in a reply-only wrapper **lacks** the marker, it is a **human follow-up** on an existing thread — not a self-loop. It carries reviewer intent; treat it as actionable and address it in this round (the caller's unresolved-thread sweep picks up the thread context).
13
12
 
14
13
  Self-loops bypass the actionable/ambiguous decision entirely. Action: advance the cursor silently. No push, no reply, no resolve-reminder, no escalation, no entry in `escalated_review_ids`. Telemetry: emit one `cycle` event with `outcome: "self-loop-skip"`.
15
14
 
@@ -0,0 +1,22 @@
1
+ # Loop comment signature
2
+
3
+ Every GitHub comment `/muggle-do` posts — inline thread replies, top-level reference comments, resolve-reminders — **must** end with the signature block below. It is the only reliable way to tell loop-authored comments from human comments: in single-account workflows the loop posts under the PR author's own identity, so `author.login` cannot distinguish them. Echo-protection and addressed-by-loop classification both depend on this marker.
4
+
5
+ ## The signature
6
+
7
+ Append these two lines as the end of every loop-posted comment body:
8
+
9
+ ```
10
+ <!-- muggle-do:bot -->
11
+ 🤖 _Automated reply from muggle-do._
12
+ ```
13
+
14
+ - `<!-- muggle-do:bot -->` — hidden HTML marker; GitHub renders it invisibly and humans never type it. This is the **detection token**.
15
+ - The visible line makes the automation clear to a reader.
16
+
17
+ ## Detection
18
+
19
+ - **Loop-authored** — the comment body contains the literal `<!-- muggle-do:bot -->`.
20
+ - **Human** — the body does not contain the marker.
21
+
22
+ Classify by the marker, never by `author.login` alone — the login is ambiguous under a shared account. A comment that carries the marker is the loop's own and must never re-trigger a cycle; a comment without it is human intent to act on.
@@ -11,3 +11,4 @@ Each section is its own file — load only what the current step needs.
11
11
  | [`allow-list`](pr-followup-helpers/allow-list.md) | Resolve who counts as a reviewer (requested reviewers ∪ CODEOWNERS − bots − author). |
12
12
  | [`reply-routing`](pr-followup-helpers/reply-routing.md) | Pick the right reply endpoint per comment type (line, body-only, CI failure). |
13
13
  | [`classify`](pr-followup-helpers/classify.md) | Per-review binary label: actionable vs ambiguous, with worked examples + borderline rule. |
14
+ | [`loop-signature`](pr-followup-helpers/loop-signature.md) | The signature every loop-posted comment carries, and how to detect loop vs human comments by it. |
@@ -0,0 +1,56 @@
1
+ # Resolving the E2E Validation Context
2
+
3
+ **How-to procedure.** Followed by any seeder that prepares a session for unattended Stage 6 (E2E acceptance) runs.
4
+
5
+ **Goal:** resolve — once, while the user is present — everything Stage 6 needs to run without further prompts (validation strategy, local URL, backend status, Muggle Test project, test credentials, auth), and persist it to `state.md` so every later non-interactive run reads it instead of asking.
6
+
7
+ The sections below are the steps in order: reuse an existing context if one is on disk, else detect what's resolvable silently, ask the rest in one question, and write the result. The final [`## Persisted fields`](#persisted-fields) section doubles as the schema the cycle reads back.
8
+
9
+ ## Reuse an existing context
10
+
11
+ If a `## Pre-flight answers` block already exists for this working tree — the current session slot, or the most recent sibling session under `.muggle-do/sessions/*` — fire the [`autoReuseValidationContext`](../muggle-preferences/preference-gates/autoReuseValidationContext.md) gate before gathering anything:
12
+
13
+ - `always` → copy the existing block into this session; skip the questionnaire.
14
+ - `never` → ignore it; run the full gather below.
15
+ - `ask` → prompt reuse-vs-re-gather.
16
+
17
+ Run the gather only when no block exists or the user chose to re-gather.
18
+
19
+ ## Silent detection
20
+
21
+ Resolve without prompting; use as questionnaire defaults:
22
+
23
+ 1. Dev server + backend health — per [`dev-server-readiness.md`](dev-server-readiness.md).
24
+ 2. Muggle Test MCP auth — `muggle-remote-auth-status`.
25
+ 3. Candidate projects — `muggle-remote-project-list`, ranked against the repo's dev URL and the PR title.
26
+ 4. Existing test-user secrets — `muggle-remote-secret-list` per candidate project (`managed_profile_email` / `managed_profile_password`).
27
+ 5. Auth0 tenant for local dev — grep the repo env file for `*AUTH0_DOMAIN*`.
28
+
29
+ ## Questions
30
+
31
+ One `AskUserQuestion` for the validation subset, detected values as defaults. Canonical wording lives in [`../do/pre-flight.md`](../do/pre-flight.md) — reference, don't fork:
32
+
33
+ - Validation strategy — pre-flight Q4
34
+ - Local URL — pre-flight Q5 (defers to [`autoSelectLocalHost`](../muggle-preferences/preference-gates/autoSelectLocalHost.md))
35
+ - Backend reachable — pre-flight Q6
36
+ - Muggle Test project — pre-flight Q7 (defers to [`autoSelectProject`](../muggle-preferences/preference-gates/autoSelectProject.md))
37
+ - Test-user credentials — pre-flight Q8
38
+ - Re-auth Muggle Test MCP — pre-flight Q10
39
+
40
+ The chosen **validation strategy is the standing decision for every cycle** — no per-tick re-prompt. It subsumes the [`autoE2ETest`](../muggle-preferences/preference-gates/autoE2ETest.md) gate (pre-flight Q13), meaningless in a loop: `local-e2e` runs Stage 6 each cycle; `unit-only` / `skip` stands down. The gate's `always` default makes `local-e2e` the default when a dev server is detected.
41
+
42
+ Skip the forward-only questions (task scope, repo, branch, PR target, worktree, rebase) — the targeted repo and head branch are already checked out.
43
+
44
+ ## Persisted fields
45
+
46
+ Write to `state.md` under a `## Pre-flight answers` block:
47
+
48
+ - `Validation: <local-e2e | staging-replay | unit-only | skip>`
49
+ - `Local URL: <url | N/A>`
50
+ - `Backend status: <up | down | N/A>`
51
+ - `Muggle Test project: <name> (<uuid>)`
52
+ - `Test credentials: <existing | new | skip>`
53
+ - `Auth status: <ok | re-authed | N/A>`
54
+ - `Working tree: <path>` — the verified checkout the cycle runs against
55
+
56
+ Missing any required field is a seeding bug: escalate with the session path and halt. Never silently skip E2E.
@@ -23,4 +23,4 @@ One per address-reviews invocation, regardless of outcome.
23
23
  - `"escalated"` — all reviews were ambiguous; no push.
24
24
  - `"mixed"` — both branches happened in the same invocation.
25
25
  - `"no-op"` — every input id was already in the escalated set; no work.
26
- - `"self-loop-skip"` — review was a synthetic wrapper around the agent's own reply (empty body + all line comments are replies). Cursor advanced silently; no work, no escalation.
26
+ - `"self-loop-skip"` — review was a synthetic wrapper around the agent's own reply (every line comment is a reply carrying the loop marker `<!-- muggle-do:bot -->`). Cursor advanced silently; no work, no escalation.
@@ -0,0 +1,41 @@
1
+ # Test Case Chain Readiness
2
+
3
+ A test case may depend on prerequisite ("parent") test cases in the project's **test-plan graph** — e.g. "edit item" depends on "create item". Before generating or replaying the chosen test case, every ancestor in that chain must already have a ready script, or the run starts from missing state and fails for the wrong reason.
4
+
5
+ This is the **graph the backend owns** — do not infer the chain from titles or `precondition` text. Read it from `muggle-remote-test-case-ancestors-get`.
6
+
7
+ **Ready** = `muggle-remote-test-script-list` (with the ancestor's `testCaseId`) returns at least one replayable/succeeded script — the same bar Step 5 uses to offer replay.
8
+
9
+ ## Procedure
10
+
11
+ Run once the target `testCaseId` is chosen and the local URL + services are confirmed (the generation calls below need `localUrl` and `cwd`).
12
+
13
+ 1. **Resolve the chain.** `muggle-remote-test-case-ancestors-get` with the target `testCaseId`. Response: `{ testCaseId, ancestors, orphan }`.
14
+ - `orphan: true` **or** empty `ancestors` → no prerequisites. Skip the rest; continue to Step 5.
15
+ - Otherwise `ancestors` is ordered **immediate-parent → root**. Reverse it to **root-first** so prerequisites are satisfied bottom-up.
16
+
17
+ 2. **For each ancestor, root-first:**
18
+ - Check readiness via `muggle-remote-test-script-list` (`projectId`, `testCaseId` = ancestor). Ready → skip to the next ancestor.
19
+ - Not ready → **generate its script only (never replay):**
20
+ 1. `muggle-remote-test-case-get` for the ancestor.
21
+ 2. Determine `freshSession` for that ancestor from its own content — same rules as Step 6.
22
+ 3. `muggle-local-execute-test-generation` with the ancestor test case, `localUrl`, `cwd`, and a long `timeoutMs` (see Step 6's timeout guidance). Do **not** call `muggle-local-execute-replay`.
23
+ 4. `muggle-local-publish-test-script` (`runId`, `cloudTestCaseId` = ancestor) so the generated script is promoted as that ancestor's canonical replay script — it now reads as ready for any case downstream.
24
+
25
+ 3. **All ancestors ready** → continue to Step 5 for the target test case.
26
+
27
+ ## When an ancestor's generation fails
28
+
29
+ If an ancestor's generation does not reach `passed` (read it via `muggle-local-run-result-get`, never the execute stdout tail), the target's prerequisite state is missing. **Halt the chain** and surface which ancestor failed and why, then ask via `AskUserQuestion`:
30
+
31
+ - **Stop** — don't run the target; the chain is broken.
32
+ - **Proceed anyway** — run the target without the prerequisite (likely to fail; only if the user judges the state already exists).
33
+ - **Give feedback** — invoke the `muggle-feedback` skill with the failed ancestor's `runId`.
34
+
35
+ Do not silently skip a failed ancestor and run the target.
36
+
37
+ ## Notes
38
+
39
+ - The target test case is **not** in its own `ancestors` list — only its prerequisites are.
40
+ - Each ancestor is a single path to the root (one parent per node), so the reversed list has no duplicates; generate each at most once per session.
41
+ - This walks the **full** chain to root, not just the immediate parent — a grandparent without a ready script is generated before its child.
@@ -27,14 +27,18 @@ Read from `.muggle-do/sessions/<slug>/`:
27
27
 
28
28
  ## Procedure
29
29
 
30
- ### Step 1 — Read every listed review off GitHub
30
+ ### Step 1 — Assemble the work set
31
31
 
32
- For each review id in the input:
32
+ Two sources, combined into one batch (dedupe by comment id):
33
+
34
+ **(a) The dispatched reviews.** For each review id in the input:
33
35
 
34
36
  - Fetch reviews per [`../_shared/github-cli-recipes/submitted-reviews.md`](../_shared/github-cli-recipes/submitted-reviews.md) (cursor 0; filter to the specific id).
35
37
  - Fetch its line comments per [`../_shared/github-cli-recipes/line-comments-for-review.md`](../_shared/github-cli-recipes/line-comments-for-review.md).
36
38
 
37
- Group into one combined batch.
39
+ **(b) Unaddressed comments on every unresolved thread.** Fetch unresolved threads per [`../_shared/github-cli-recipes/unresolved-threads.md`](../_shared/github-cli-recipes/unresolved-threads.md). For each thread classified **unaddressed human comment** — newest comment lacks the loop marker `<!-- muggle-do:bot -->` ([`loop-signature.md`](../_shared/pr-followup-helpers/loop-signature.md)) and post-dates the loop's last marked reply — add it to the batch, even if its review predates the cursor. This is how a human thread follow-up (a marker-less reply) gets addressed. **Exclude** comments whose review id is in `last_seen.escalated_review_ids` — paused awaiting the user, not re-work.
40
+
41
+ Group (a) and (b) into one combined batch.
38
42
 
39
43
  ### Step 2 — Classify each review
40
44
 
@@ -57,7 +61,7 @@ The user clarifies on GitHub by submitting a new review. The next watcher tick p
57
61
 
58
62
  ### Step 4 — Handle actionables (if any)
59
63
 
60
- If `actionable_review_ids` is empty, skip Steps 4 and 5; proceed to Step 6 (cursor + respawn). Otherwise:
64
+ If `actionable_review_ids` is empty, skip the rest of Step 4 and Step 5; proceed to Step 5.5 (resolve-reminder) then Step 6. Otherwise:
61
65
 
62
66
  #### 4a. Flatten the work
63
67
 
@@ -75,7 +79,7 @@ Invoke [`unit-tests.md`](unit-tests.md). Cover the surface that just changed; re
75
79
 
76
80
  #### 4d. Run ONE E2E acceptance pass
77
81
 
78
- Invoke [`e2e-acceptance.md`](e2e-acceptance.md). One pass covering all related test cases for this PR, not one pass per comment. Use the `autoE2ETest` gate per its usual contract.
82
+ Invoke [`e2e-acceptance.md`](e2e-acceptance.md). One pass covering all related test cases for this PR, not one per comment. The stage reads the persisted validation context (seeded by pre-flight or by bootstrap Step 6.5); a poll-only session with no context (e.g. auto-track) is reported `SKIPPED`. The persisted `Validation` strategy is the standing decision — no per-tick `autoE2ETest` prompt. See [`e2e-acceptance.md`](e2e-acceptance.md) Step 0 and [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md).
79
83
 
80
84
  #### 4e. Create or update the PR
81
85
 
@@ -85,9 +89,7 @@ Invoke [`open-prs/update.md`](open-prs/update.md) (pass the PR URL + slug + exis
85
89
 
86
90
  Invoke [`per-comment-replies.md`](per-comment-replies.md) with the actionable reviews and the new SHA. One reply per comment, in its own thread, describing what was done for it.
87
91
 
88
- #### 4g. Run the resolve-reminder stage
89
-
90
- Invoke [`resolve-reminder.md`](resolve-reminder.md). Scans unresolved threads, classifies, and posts a top-level PR comment listing addressed-by-loop thread ids.
92
+ (The resolve-reminder runs once per round in Step 5.5 below — not only after a push — so a round that pushed nothing still nudges addressed-but-unresolved threads.)
91
93
 
92
94
  ### Step 5 — Update session state
93
95
 
@@ -95,6 +97,10 @@ Invoke [`resolve-reminder.md`](resolve-reminder.md). Scans unresolved threads, c
95
97
  - `last_seen.last_pushed_sha` = the new head SHA (update.md already wrote this; verify)
96
98
  - `last_seen.reviewId` = max(input review ids ∪ last_seen.reviewId)
97
99
 
100
+ ### Step 5.5 — Resolve-reminder (runs every round)
101
+
102
+ Invoke [`resolve-reminder.md`](resolve-reminder.md) once, regardless of whether this round pushed. It scans unresolved threads, finds those whose newest comment is loop-marked (addressed, awaiting resolve), and posts one top-level PR comment nudging the reviewer to resolve them — or stays silent if there are none. Threads with a newer human comment were already pulled into this round's work set in Step 1(b).
103
+
98
104
  ### Step 6 — Respawn the watcher
99
105
 
100
106
  Refresh PR state per [`../_shared/github-cli-recipes/pr-metadata.md`](../_shared/github-cli-recipes/pr-metadata.md). If the PR is now merged or closed:
@@ -44,11 +44,15 @@ You receive everything from `state.md` already — pre-flight resolved it:
44
44
 
45
45
  ## Your Job
46
46
 
47
- ### Step 0: Consume pre-flight (no user questions)
47
+ ### Step 0: Consume the validation context (no user questions)
48
48
 
49
- Read `state.md`. Resolve [`autoE2ETest`](../muggle-preferences/preference-gates/autoE2ETest.md) first — `always` (default, including when unset) runs this stage. `ask` should already have been resolved by pre-flight Q13. Use the resolved validation mode (`local-e2e`, `staging-replay`, `unit-only`, `skip`) to pick execution vs early-exit behavior.
49
+ Read `state.md`.
50
50
 
51
- Use `localUrl`, `projectId`, and `worktreePath` from `state.md`. Missing any → pre-flight bug; escalate with the session path and halt; do not ask the user.
51
+ **No `## Pre-flight answers` block at all** → the session was seeded poll-only (e.g. by auto-track, [`../muggle-pr-followup/auto-track.md`](../muggle-pr-followup/auto-track.md)). Treat `Validation` as `skip`: emit a `SKIPPED` report with reason `no validation context seeded` and exit cleanly. The watcher owns no E2E context by design; "no context" is a clean skip, not a failure.
52
+
53
+ Otherwise the block was seeded by pre-flight or bootstrap per [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md) — read it the same way regardless of seeder. The persisted `Validation` field (`local-e2e`, `staging-replay`, `unit-only`, `skip`) picks execution vs early-exit below. In a forward run, [`autoE2ETest`](../muggle-preferences/preference-gates/autoE2ETest.md) `ask` was resolved by pre-flight Q13; in a watcher cycle there is no per-tick pre-flight, so `Validation` **is** the standing decision — don't re-resolve `ask`.
54
+
55
+ For a `local-e2e` block, use `localUrl`, `projectId`, and the working-tree path from `state.md`. Missing any → seeding bug; escalate with the session path and halt; do not ask the user.
52
56
 
53
57
  ### Step 0.5: Pre-flight verification probes
54
58
 
@@ -36,9 +36,12 @@ Reply body uses the template in [`../muggle-pr-followup/output-templates/inline-
36
36
 
37
37
  ```
38
38
  Addressed in <short-sha>: <one-line summary of the change made for THIS comment>.
39
+
40
+ <!-- muggle-do:bot -->
41
+ 🤖 _Automated reply from muggle-do._
39
42
  ```
40
43
 
41
- `<short-sha>` is the first 7 chars of `new_sha`. The body must contain the literal `<short-sha>` substring — the resolve-reminder stage greps for it to classify threads as addressed-by-loop.
44
+ `<short-sha>` is the first 7 chars of `new_sha`; the body must contain that substring so the resolve-reminder stage knows which push addressed the thread. The trailing signature block is mandatory — its `<!-- muggle-do:bot -->` marker is what identifies the reply as loop-authored (see [`../_shared/pr-followup-helpers/loop-signature.md`](../_shared/pr-followup-helpers/loop-signature.md)).
42
45
 
43
46
  ### Step 3 — Handle review-body-only comments
44
47
 
@@ -46,6 +49,9 @@ If an actionable review has a non-empty `body` and **zero** line comments, GitHu
46
49
 
47
50
  ```
48
51
  Re: review #<review_id> — addressed in <short-sha>: <one-line summary>.
52
+
53
+ <!-- muggle-do:bot -->
54
+ 🤖 _Automated reply from muggle-do._
49
55
  ```
50
56
 
51
57
  Posted per [`../_shared/github-cli-recipes/top-level-comment.md`](../_shared/github-cli-recipes/top-level-comment.md). Fires at most once per actionable review-with-no-line-comments. Does not fire if the review has line comments — Step 2 covers those.
@@ -58,4 +64,4 @@ Posted per [`../_shared/github-cli-recipes/top-level-comment.md`](../_shared/git
58
64
  ## Invariants
59
65
 
60
66
  - One reply per line comment. No per-review summary reply anywhere.
61
- - Every reply body contains the new SHA's 7-char prefix — the resolve-reminder stage greps for it.
67
+ - Every reply body contains the new SHA's 7-char prefix (which push addressed it) and ends with the loop signature block — the `<!-- muggle-do:bot -->` marker, not the author login, is what identifies loop-authored comments.