@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.
- package/dist/{chunk-JNI7INIO.js → chunk-5G7WI7IY.js} +2 -2
- package/dist/{chunk-CPF6AR2I.js → chunk-YKR2TQ24.js} +53 -37
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/skills/_shared/github-cli-recipes/submitted-reviews.md +1 -1
- package/dist/plugin/skills/_shared/github-cli-recipes/unresolved-threads.md +6 -4
- package/dist/plugin/skills/_shared/pr-followup-helpers/allow-list.md +1 -1
- package/dist/plugin/skills/_shared/pr-followup-helpers/classify.md +3 -4
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +22 -0
- package/dist/plugin/skills/_shared/pr-followup-helpers.md +1 -0
- package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +56 -0
- package/dist/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
- package/dist/plugin/skills/_shared/test-case-chain-readiness.md +41 -0
- package/dist/plugin/skills/do/address-reviews.md +14 -8
- package/dist/plugin/skills/do/e2e-acceptance.md +7 -3
- package/dist/plugin/skills/do/per-comment-replies.md +8 -2
- package/dist/plugin/skills/do/resolve-reminder.md +16 -18
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +10 -3
- package/dist/plugin/skills/muggle-pr-followup/auto-track.md +91 -0
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +11 -4
- package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +2 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +7 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +6 -3
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +12 -0
- package/dist/plugin/skills/muggle-preferences/ops/configure.md +1 -1
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoReuseValidationContext.md +11 -0
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +7 -0
- package/dist/release-manifest.json +4 -4
- package/dist/{src-YR5UKLPC.js → src-ECRJW2LY.js} +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/skills/_shared/github-cli-recipes/submitted-reviews.md +1 -1
- package/plugin/skills/_shared/github-cli-recipes/unresolved-threads.md +6 -4
- package/plugin/skills/_shared/pr-followup-helpers/allow-list.md +1 -1
- package/plugin/skills/_shared/pr-followup-helpers/classify.md +3 -4
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +22 -0
- package/plugin/skills/_shared/pr-followup-helpers.md +1 -0
- package/plugin/skills/_shared/resolve-e2e-validation-context.md +56 -0
- package/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
- package/plugin/skills/_shared/test-case-chain-readiness.md +41 -0
- package/plugin/skills/do/address-reviews.md +14 -8
- package/plugin/skills/do/e2e-acceptance.md +7 -3
- package/plugin/skills/do/per-comment-replies.md +8 -2
- package/plugin/skills/do/resolve-reminder.md +16 -18
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
- package/plugin/skills/muggle-pr-followup/SKILL.md +10 -3
- package/plugin/skills/muggle-pr-followup/auto-track.md +91 -0
- package/plugin/skills/muggle-pr-followup/bootstrap.md +11 -4
- package/plugin/skills/muggle-pr-followup/output-templates/help.md +2 -1
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +7 -1
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +6 -3
- package/plugin/skills/muggle-pr-followup/state-schemas.md +12 -0
- package/plugin/skills/muggle-preferences/ops/configure.md +1 -1
- package/plugin/skills/muggle-preferences/preference-gates/autoReuseValidationContext.md +11 -0
- 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-
|
|
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-
|
|
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
|
|
4407
|
-
const
|
|
4408
|
-
|
|
4409
|
-
|
|
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
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
totalPages,
|
|
4441
|
-
hasMore:
|
|
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: {
|
|
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
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
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`.
|
|
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
|
|
32
|
-
- **
|
|
33
|
-
- **Not addressed** —
|
|
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
|
|
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`.
|
|
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
|
-
-
|
|
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 (
|
|
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 —
|
|
30
|
+
### Step 1 — Assemble the work set
|
|
31
31
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
47
|
+
### Step 0: Consume the validation context (no user questions)
|
|
48
48
|
|
|
49
|
-
Read `state.md`.
|
|
49
|
+
Read `state.md`.
|
|
50
50
|
|
|
51
|
-
|
|
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
|
|
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
|
|
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.
|