@muggleai/works 4.11.3 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7WRAMBOS.js → chunk-2DVZ2LYO.js} +7 -2
- package/dist/{chunk-AYWLYB3F.js → chunk-I4VLYJ7M.js} +45 -12
- 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/scripts/ensure-electron-app.sh +29 -4
- package/dist/plugin/skills/_shared/github-cli-recipes/line-comments-for-review.md +8 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/loop-user-identity.md +9 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/pr-edit.md +8 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/pr-metadata.md +12 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/push-to-branch.md +10 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/reply-line-comment.md +10 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/submitted-reviews.md +15 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/top-level-comment.md +7 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/unresolved-threads.md +33 -0
- package/dist/plugin/skills/_shared/github-cli-recipes/verify-working-tree.md +17 -0
- package/dist/plugin/skills/_shared/github-cli-recipes.md +20 -0
- package/dist/plugin/skills/_shared/pr-followup-helpers/allow-list.md +53 -0
- package/dist/plugin/skills/_shared/pr-followup-helpers/classify.md +48 -0
- package/dist/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +40 -0
- package/dist/plugin/skills/_shared/pr-followup-helpers.md +8 -146
- package/dist/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +25 -0
- package/dist/plugin/skills/_shared/telemetry-events/muggle-do-escalation.md +19 -0
- package/dist/plugin/skills/_shared/telemetry-events/muggle-do-resolve-reminder.md +19 -0
- package/dist/plugin/skills/_shared/telemetry-events/pr-followup-bootstrap.md +20 -0
- package/dist/plugin/skills/_shared/telemetry-events/pr-followup-tick.md +23 -0
- package/dist/plugin/skills/_shared/telemetry-events.md +15 -0
- package/dist/plugin/skills/do/address-reviews.md +137 -0
- package/dist/plugin/skills/do/build.md +5 -5
- package/dist/plugin/skills/do/open-prs/forward.md +80 -0
- package/dist/plugin/skills/do/open-prs/update.md +54 -0
- package/dist/plugin/skills/do/open-prs.md +6 -102
- package/dist/plugin/skills/do/per-comment-replies.md +61 -0
- package/dist/plugin/skills/do/resolve-reminder.md +70 -0
- package/dist/plugin/skills/muggle-do/SKILL.md +41 -45
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +25 -0
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +21 -43
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +88 -0
- package/dist/plugin/skills/muggle-pr-followup/contract.md +44 -161
- package/dist/plugin/skills/muggle-pr-followup/output-templates/bootstrap.md +42 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/escalation.md +32 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +17 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +17 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +12 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/watcher-log.md +21 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates.md +14 -0
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +104 -0
- package/dist/plugin/skills/muggle-test-regenerate-missing/SKILL.md +3 -3
- package/dist/release-manifest.json +4 -4
- package/dist/{src-NJUUHKPW.js → src-ARTTHWNP.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/scripts/ensure-electron-app.sh +29 -4
- package/plugin/skills/_shared/github-cli-recipes/line-comments-for-review.md +8 -0
- package/plugin/skills/_shared/github-cli-recipes/loop-user-identity.md +9 -0
- package/plugin/skills/_shared/github-cli-recipes/pr-edit.md +8 -0
- package/plugin/skills/_shared/github-cli-recipes/pr-metadata.md +12 -0
- package/plugin/skills/_shared/github-cli-recipes/push-to-branch.md +10 -0
- package/plugin/skills/_shared/github-cli-recipes/reply-line-comment.md +10 -0
- package/plugin/skills/_shared/github-cli-recipes/submitted-reviews.md +15 -0
- package/plugin/skills/_shared/github-cli-recipes/top-level-comment.md +7 -0
- package/plugin/skills/_shared/github-cli-recipes/unresolved-threads.md +33 -0
- package/plugin/skills/_shared/github-cli-recipes/verify-working-tree.md +17 -0
- package/plugin/skills/_shared/github-cli-recipes.md +20 -0
- package/plugin/skills/_shared/pr-followup-helpers/allow-list.md +53 -0
- package/plugin/skills/_shared/pr-followup-helpers/classify.md +48 -0
- package/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +40 -0
- package/plugin/skills/_shared/pr-followup-helpers.md +8 -146
- package/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +25 -0
- package/plugin/skills/_shared/telemetry-events/muggle-do-escalation.md +19 -0
- package/plugin/skills/_shared/telemetry-events/muggle-do-resolve-reminder.md +19 -0
- package/plugin/skills/_shared/telemetry-events/pr-followup-bootstrap.md +20 -0
- package/plugin/skills/_shared/telemetry-events/pr-followup-tick.md +23 -0
- package/plugin/skills/_shared/telemetry-events.md +15 -0
- package/plugin/skills/do/address-reviews.md +137 -0
- package/plugin/skills/do/build.md +5 -5
- package/plugin/skills/do/open-prs/forward.md +80 -0
- package/plugin/skills/do/open-prs/update.md +54 -0
- package/plugin/skills/do/open-prs.md +6 -102
- package/plugin/skills/do/per-comment-replies.md +61 -0
- package/plugin/skills/do/resolve-reminder.md +70 -0
- package/plugin/skills/muggle-do/SKILL.md +41 -45
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +25 -0
- package/plugin/skills/muggle-pr-followup/SKILL.md +21 -43
- package/plugin/skills/muggle-pr-followup/bootstrap.md +88 -0
- package/plugin/skills/muggle-pr-followup/contract.md +44 -161
- package/plugin/skills/muggle-pr-followup/output-templates/bootstrap.md +42 -0
- package/plugin/skills/muggle-pr-followup/output-templates/escalation.md +32 -0
- package/plugin/skills/muggle-pr-followup/output-templates/help.md +17 -0
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +17 -0
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +12 -0
- package/plugin/skills/muggle-pr-followup/output-templates/watcher-log.md +21 -0
- package/plugin/skills/muggle-pr-followup/output-templates.md +14 -0
- package/plugin/skills/muggle-pr-followup/state-schemas.md +104 -0
- package/plugin/skills/muggle-test-regenerate-missing/SKILL.md +3 -3
- package/scripts/postinstall.mjs +36 -5
|
@@ -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-I4VLYJ7M.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';
|
|
@@ -329,6 +329,11 @@ async function startStdioServer(server) {
|
|
|
329
329
|
};
|
|
330
330
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
331
331
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
332
|
+
watchForParentDeath(shutdown);
|
|
333
|
+
}
|
|
334
|
+
function watchForParentDeath(shutdown) {
|
|
335
|
+
process.stdin.on("end", () => shutdown("stdin-end"));
|
|
336
|
+
process.stdin.on("close", () => shutdown("stdin-close"));
|
|
332
337
|
}
|
|
333
338
|
|
|
334
339
|
// src/cli/pr-section/render.ts
|
|
@@ -731,7 +736,7 @@ async function resolveGsScreenshotUrls(report, opts) {
|
|
|
731
736
|
if (gsUrls.length === 0) {
|
|
732
737
|
return report;
|
|
733
738
|
}
|
|
734
|
-
const mcps = await import('./src-
|
|
739
|
+
const mcps = await import('./src-ARTTHWNP.js');
|
|
735
740
|
const credentials = await mcps.getCallerCredentialsAsync();
|
|
736
741
|
if (!credentials.bearerToken && !credentials.apiKey) {
|
|
737
742
|
stderrWrite(
|
|
@@ -1985,6 +1985,51 @@ function getRunResultStorageService() {
|
|
|
1985
1985
|
function resetRunResultStorageService() {
|
|
1986
1986
|
instance = null;
|
|
1987
1987
|
}
|
|
1988
|
+
|
|
1989
|
+
// packages/mcps/src/mcp/local/services/replay-url-rewrite.ts
|
|
1990
|
+
function rewriteActionScriptUrls(params) {
|
|
1991
|
+
const { actionScript, originalUrl, localUrl } = params;
|
|
1992
|
+
if (!originalUrl) {
|
|
1993
|
+
return actionScript;
|
|
1994
|
+
}
|
|
1995
|
+
let originalHost;
|
|
1996
|
+
let localOrigin;
|
|
1997
|
+
try {
|
|
1998
|
+
originalHost = new URL(originalUrl).hostname;
|
|
1999
|
+
localOrigin = new URL(localUrl).origin;
|
|
2000
|
+
} catch {
|
|
2001
|
+
const serialized2 = JSON.stringify(actionScript);
|
|
2002
|
+
const rewritten2 = serialized2.replace(new RegExp(escapeRegex(originalUrl), "g"), localUrl);
|
|
2003
|
+
return JSON.parse(rewritten2);
|
|
2004
|
+
}
|
|
2005
|
+
const hostPattern = new RegExp(
|
|
2006
|
+
`https?://(?:[a-z0-9-]+\\.)*${escapeRegex(originalHost)}(?=[/?#:"'\\\\]|$)`,
|
|
2007
|
+
"gi"
|
|
2008
|
+
);
|
|
2009
|
+
const serialized = JSON.stringify(actionScript);
|
|
2010
|
+
const rewritten = serialized.replace(hostPattern, localOrigin);
|
|
2011
|
+
const result = JSON.parse(rewritten);
|
|
2012
|
+
forceFirstNavigateUrl({ steps: result, localUrl });
|
|
2013
|
+
return result;
|
|
2014
|
+
}
|
|
2015
|
+
function forceFirstNavigateUrl(params) {
|
|
2016
|
+
for (const step of params.steps) {
|
|
2017
|
+
if (!isPlainObject(step)) continue;
|
|
2018
|
+
const operation = step["operation"];
|
|
2019
|
+
if (!isPlainObject(operation)) continue;
|
|
2020
|
+
if (operation["action"] !== "navigate") continue;
|
|
2021
|
+
operation["url"] = params.localUrl;
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function isPlainObject(value) {
|
|
2026
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2027
|
+
}
|
|
2028
|
+
function escapeRegex(str) {
|
|
2029
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// packages/mcps/src/mcp/local/services/execution-service.ts
|
|
1988
2033
|
var logger4 = getLogger();
|
|
1989
2034
|
var activeProcesses = /* @__PURE__ */ new Map();
|
|
1990
2035
|
function getAuthenticatedUserId() {
|
|
@@ -2606,18 +2651,6 @@ ${executionResult.stderr}`;
|
|
|
2606
2651
|
};
|
|
2607
2652
|
}
|
|
2608
2653
|
}
|
|
2609
|
-
function rewriteActionScriptUrls(params) {
|
|
2610
|
-
const { actionScript, originalUrl, localUrl } = params;
|
|
2611
|
-
if (!originalUrl) {
|
|
2612
|
-
return actionScript;
|
|
2613
|
-
}
|
|
2614
|
-
const serialized = JSON.stringify(actionScript);
|
|
2615
|
-
const rewritten = serialized.replace(new RegExp(escapeRegex(originalUrl), "g"), localUrl);
|
|
2616
|
-
return JSON.parse(rewritten);
|
|
2617
|
-
}
|
|
2618
|
-
function escapeRegex(str) {
|
|
2619
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2620
|
-
}
|
|
2621
2654
|
function cancelExecution(params) {
|
|
2622
2655
|
const process2 = activeProcesses.get(params.runId);
|
|
2623
2656
|
if (!process2) {
|
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-2DVZ2LYO.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-I4VLYJ7M.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.12.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.12.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -3,10 +3,35 @@
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
# Ensure the Electron browser test runner is installed/up to date (silent, best-effort).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
#
|
|
7
|
+
# Bounded + cached: this script runs from a SessionStart hook on every Claude
|
|
8
|
+
# session, so a hung `muggle setup` (e.g. one blocked by host security policy
|
|
9
|
+
# on Windows) must not leak a ~100 MB orphan per session start. We cap the
|
|
10
|
+
# attempt with `timeout` and skip the call entirely if we already checked
|
|
11
|
+
# within the last day.
|
|
12
|
+
ensure_marker_dir="${HOME}/.cache/muggle"
|
|
13
|
+
ensure_marker="${ensure_marker_dir}/electron-app-checked"
|
|
14
|
+
ensure_ttl=$((24 * 60 * 60))
|
|
15
|
+
|
|
16
|
+
ensure_now=$(date +%s)
|
|
17
|
+
ensure_last=0
|
|
18
|
+
if [ -f "${ensure_marker}" ]; then
|
|
19
|
+
ensure_last=$(stat -f %m "${ensure_marker}" 2>/dev/null || stat -c %Y "${ensure_marker}" 2>/dev/null || echo 0)
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ $((ensure_now - ensure_last)) -ge "${ensure_ttl}" ]; then
|
|
23
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
24
|
+
ensure_timeout="timeout -k 5 60"
|
|
25
|
+
else
|
|
26
|
+
ensure_timeout=""
|
|
27
|
+
fi
|
|
28
|
+
if command -v muggle >/dev/null 2>&1; then
|
|
29
|
+
${ensure_timeout} muggle setup >/dev/null 2>&1 || true
|
|
30
|
+
else
|
|
31
|
+
${ensure_timeout} npx -y @muggleai/works setup >/dev/null 2>&1 || true
|
|
32
|
+
fi
|
|
33
|
+
mkdir -p "${ensure_marker_dir}" 2>/dev/null || true
|
|
34
|
+
touch "${ensure_marker}" 2>/dev/null || true
|
|
10
35
|
fi
|
|
11
36
|
|
|
12
37
|
# --- Context injection ---
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Identify the loop user
|
|
2
|
+
|
|
3
|
+
The GitHub identity that owns the authenticated `gh` token. Resolve-reminder thread classification and reply attribution need this.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh api user --jq '.login'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Cache in `state.md` under `Loop user:`; re-resolve only when missing.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Refresh the PR title or body
|
|
2
|
+
|
|
3
|
+
For `open-prs/update.md` when E2E state flips (passing↔failing) or validation strategy changes.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh pr edit <pr-number> --repo <owner>/<repo> --title "<new-title>"
|
|
7
|
+
gh pr edit <pr-number> --repo <owner>/<repo> --body-file <file>
|
|
8
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# PR metadata snapshot
|
|
2
|
+
|
|
3
|
+
Fetch the fields the watcher and bootstrap need in one call.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh pr view <pr-number> --repo <owner>/<repo> \
|
|
7
|
+
--json url,number,headRefOid,headRefName,baseRefName,state,mergedAt,closedAt,body,title,author
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- `state` is one of `OPEN`, `MERGED`, `CLOSED`.
|
|
11
|
+
- `headRefOid` is the current head SHA — store as `head_sha` in `prs.json`.
|
|
12
|
+
- `headRefName` is the branch — must match the working tree's branch in bootstrap.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Push to the PR branch
|
|
2
|
+
|
|
3
|
+
After the address-reviews cycle's work.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git -C <repo-path> push origin <head-ref-name>
|
|
7
|
+
git -C <repo-path> rev-parse HEAD
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Append the new SHA to `last_seen.pushed_shas[]` so resolve-reminder can recognize it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Reply to a line comment (threaded)
|
|
2
|
+
|
|
3
|
+
Used by `/muggle-do` per-comment inline replies.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh api --method POST \
|
|
7
|
+
-H "Accept: application/vnd.github+json" \
|
|
8
|
+
repos/<owner>/<repo>/pulls/<n>/comments/<comment-id>/replies \
|
|
9
|
+
-f body="<reply-text>"
|
|
10
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Submitted reviews past a cursor
|
|
2
|
+
|
|
3
|
+
For the watcher's poll and the address-reviews fetch.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh api repos/<owner>/<repo>/pulls/<n>/reviews --paginate
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Filter client-side:
|
|
10
|
+
|
|
11
|
+
- `submitted_at != null` (skip PENDING drafts)
|
|
12
|
+
- `id > last_seen.reviewId`
|
|
13
|
+
- `id` not in `last_seen.escalated_review_ids`
|
|
14
|
+
- `user.login` in the resolved allow-list
|
|
15
|
+
- `state` in `{CHANGES_REQUESTED, COMMENTED}`, OR `APPROVED` with a non-empty body or at least one line comment
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Unresolved comment threads
|
|
2
|
+
|
|
3
|
+
For the resolve-reminder stage. GraphQL only — REST does not expose `isResolved`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh api graphql -F owner=<owner> -F name=<repo> -F number=<n> -f query='
|
|
7
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
8
|
+
repository(owner: $owner, name: $name) {
|
|
9
|
+
pullRequest(number: $number) {
|
|
10
|
+
reviewThreads(first: 100) {
|
|
11
|
+
nodes {
|
|
12
|
+
id
|
|
13
|
+
isResolved
|
|
14
|
+
comments(first: 100) {
|
|
15
|
+
nodes {
|
|
16
|
+
databaseId
|
|
17
|
+
author { login }
|
|
18
|
+
body
|
|
19
|
+
createdAt
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Filter client-side to `isResolved == false`. Classify each thread by inspecting its comments:
|
|
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.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Verify the working tree matches the PR
|
|
2
|
+
|
|
3
|
+
Bootstrap's environment check.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git rev-parse --show-toplevel # cwd is a git working tree
|
|
7
|
+
git remote get-url origin # remote matches <owner>/<repo>
|
|
8
|
+
git rev-parse --abbrev-ref HEAD # current branch matches PR's headRefName
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Accept any remote URL form for `<owner>/<repo>` (with or without trailing `.git`):
|
|
12
|
+
|
|
13
|
+
- `https://github.com/<owner>/<repo>`
|
|
14
|
+
- `git@github.com:<owner>/<repo>`
|
|
15
|
+
- `ssh://git@github.com/<owner>/<repo>`
|
|
16
|
+
|
|
17
|
+
Any mismatch → bootstrap aborts using the wrong-checkout template in [`../../muggle-pr-followup/output-templates/bootstrap.md`](../../muggle-pr-followup/output-templates/bootstrap.md).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# GitHub CLI Recipes
|
|
2
|
+
|
|
3
|
+
Reusable `gh` / `git` snippets used by `muggle-pr-followup` (watcher + bootstrap) and `/muggle-do` (address-reviews + open-prs). Each recipe is one file — load only what you need.
|
|
4
|
+
|
|
5
|
+
Skills assume a working `gh auth status`. Auth errors surface verbatim from `gh`.
|
|
6
|
+
|
|
7
|
+
## Index
|
|
8
|
+
|
|
9
|
+
| Recipe | Use case |
|
|
10
|
+
| :----- | :------- |
|
|
11
|
+
| [`pr-metadata`](github-cli-recipes/pr-metadata.md) | Snapshot PR state, head SHA, branch — watcher + bootstrap. |
|
|
12
|
+
| [`submitted-reviews`](github-cli-recipes/submitted-reviews.md) | Fetch reviews past a cursor — watcher's poll. |
|
|
13
|
+
| [`line-comments-for-review`](github-cli-recipes/line-comments-for-review.md) | Pull a review's line comments — `/muggle-do` per-comment routing. |
|
|
14
|
+
| [`unresolved-threads`](github-cli-recipes/unresolved-threads.md) | GraphQL fetch of unresolved comment threads — resolve-reminder. |
|
|
15
|
+
| [`reply-line-comment`](github-cli-recipes/reply-line-comment.md) | POST a threaded reply on a line comment. |
|
|
16
|
+
| [`top-level-comment`](github-cli-recipes/top-level-comment.md) | POST a top-level PR comment — resolve-reminder + overflow. |
|
|
17
|
+
| [`push-to-branch`](github-cli-recipes/push-to-branch.md) | Push + capture new SHA after address-reviews work. |
|
|
18
|
+
| [`verify-working-tree`](github-cli-recipes/verify-working-tree.md) | Three checks bootstrap runs before seeding state. |
|
|
19
|
+
| [`pr-edit`](github-cli-recipes/pr-edit.md) | Refresh title or body when address-reviews mode flips state. |
|
|
20
|
+
| [`loop-user-identity`](github-cli-recipes/loop-user-identity.md) | Resolve the GitHub login of the loop user. |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Reviewer allow-list
|
|
2
|
+
|
|
3
|
+
The address-reviews flow only acts on reviews submitted by users in the **allow-list** = (requested reviewers ∪ CODEOWNERS) − bots − PR author. Re-resolve every invocation — never cache across cycles.
|
|
4
|
+
|
|
5
|
+
## Step 1: requested reviewers
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gh pr view <number> --repo <owner>/<repo> --json reviewRequests,author
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`reviewRequests` is an array of `{ login? , slug? }`. User reviewers have `login`; team reviewers have `slug` (and `name`). Expand teams to member logins:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
gh api orgs/<org>/teams/<slug>/members --jq '.[].login'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Record `prAuthor = author.login` for the exclusion step.
|
|
18
|
+
|
|
19
|
+
## Step 2: CODEOWNERS
|
|
20
|
+
|
|
21
|
+
Look for the file in this order — first hit wins:
|
|
22
|
+
|
|
23
|
+
1. `.github/CODEOWNERS`
|
|
24
|
+
2. `CODEOWNERS`
|
|
25
|
+
3. `docs/CODEOWNERS`
|
|
26
|
+
|
|
27
|
+
Read from the PR's **head branch** (not master) — a PR that adds CODEOWNERS should be informational while open, load-bearing once merged:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gh api repos/<owner>/<repo>/contents/.github/CODEOWNERS?ref=<head_sha> --jq '.content' | base64 -d
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Parse line-by-line:
|
|
34
|
+
|
|
35
|
+
- Skip blank lines and lines starting with `#`.
|
|
36
|
+
- Each line is `<pattern> <owner1> <owner2> ...`.
|
|
37
|
+
- Owners are either `@user` or `@org/team`. Strip the leading `@`.
|
|
38
|
+
- Collect the union of all owners across all lines (don't match patterns against changed files — repo-level membership is enough).
|
|
39
|
+
|
|
40
|
+
Expand `@org/team` to member logins via the orgs/teams/members endpoint.
|
|
41
|
+
|
|
42
|
+
If no CODEOWNERS file exists in any location, the CODEOWNERS contribution is empty. Don't fail.
|
|
43
|
+
|
|
44
|
+
## Step 3: filter
|
|
45
|
+
|
|
46
|
+
Allow-list = (requested reviewers ∪ CODEOWNERS) − `{prAuthor}` − bot logins.
|
|
47
|
+
|
|
48
|
+
Bot logins:
|
|
49
|
+
|
|
50
|
+
- Ends with `[bot]` (e.g. `dependabot[bot]`)
|
|
51
|
+
- Exact match: `dependabot`, `github-actions`, `renovate`, `mergify`
|
|
52
|
+
|
|
53
|
+
A comment author not in the allow-list is silently ignored — do not reply, do not address.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Classify
|
|
2
|
+
|
|
3
|
+
Classify the **review as a unit** — but reply per line comment (threaded), not per review.
|
|
4
|
+
|
|
5
|
+
| Class | Signal | Action |
|
|
6
|
+
| :---- | :----- | :----- |
|
|
7
|
+
| **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). |
|
|
8
|
+
| **ambiguous** | No actionable signal — pure vibes, contradictory, or depends on knowledge the loop can't access. | Escalate once with two interpretations; pause the PR. |
|
|
9
|
+
|
|
10
|
+
Default to **actionable**. CI catches wrong attempts; reviewers correct on the next round. Escalation is a round-trip with an absent user — reserve it.
|
|
11
|
+
|
|
12
|
+
Reply shape (all replies for one review reference the same SHA):
|
|
13
|
+
|
|
14
|
+
- **threaded** (default): `Done in <sha> — <attribution>. (Review #<review_id>, cycle <status>.)`
|
|
15
|
+
- **top-level** (fallback, body-only reviews): `Re: review #<review_id> — addressed in <sha>, cycle <status>.`
|
|
16
|
+
- **ambiguous**: no bot reply.
|
|
17
|
+
|
|
18
|
+
## Worked examples — Actionable
|
|
19
|
+
|
|
20
|
+
| Review (summarized) | Why actionable |
|
|
21
|
+
| :------------------ | :------------- |
|
|
22
|
+
| 3 comments: "rename `fooBar` to `foo_bar`", "use `const` here", "fix this typo" | Three concrete edits |
|
|
23
|
+
| 1 comment: "could the procedure be simpler?" | Soft-phrased but the intent is clear — simplify |
|
|
24
|
+
| Review body: "Two things: extract validation into a helper, add a null check before the lookup." + 0 line comments | Two concrete directives in the body |
|
|
25
|
+
| 4 comments: "why this approach?", "is this called from X?", "does this need to handle empty array?", "what's the perf here?" | All questions, each answerable |
|
|
26
|
+
| 1 comment: "rewrite this module — the architecture doesn't match the spec" | Substantive rebuild, but direction is clear |
|
|
27
|
+
| 1 comment: "I'd lean toward the bar.ts pattern" | Concrete referent (bar.ts) — apply that pattern |
|
|
28
|
+
|
|
29
|
+
## Worked examples — Ambiguous
|
|
30
|
+
|
|
31
|
+
| Review (summarized) | Why ambiguous |
|
|
32
|
+
| :------------------ | :------------ |
|
|
33
|
+
| 1 comment: "👀" / "hmm" / ":thinking:" | No signal at all |
|
|
34
|
+
| 1 comment: "this is wrong" with no target or direction | Asserts a problem but doesn't propose a fix |
|
|
35
|
+
| 2 comments: "use X" + "but actually don't use X" | Self-contradicting |
|
|
36
|
+
| 1 comment: "we discussed this offline — please address" | References context the loop doesn't have |
|
|
37
|
+
| 1 comment: "won't this break the prod migration we did last week?" | Implicit change request gated on knowledge the loop can't access |
|
|
38
|
+
| Mixed: 2 concrete directives + 1 "rethink the whole approach" | The "rethink" subverts the others; escalate to confirm scope |
|
|
39
|
+
|
|
40
|
+
Escalate per the caller's procedure (add the review id to the cursor's escalated set, emit one terminal message, pause the PR).
|
|
41
|
+
|
|
42
|
+
## Borderline rule
|
|
43
|
+
|
|
44
|
+
If you can paraphrase the review's intent as **"do X"** (X concrete) → actionable. Pick the best interpretation; dispatch.
|
|
45
|
+
|
|
46
|
+
If you can only paraphrase as **"the reviewer is dissatisfied but I can't tell with what"** → ambiguous.
|
|
47
|
+
|
|
48
|
+
Mixed ("3 concrete + 1 dissatisfaction") usually splits: action the concrete; ask about the dissatisfaction in the reply summary. Pure ambiguity means *nothing* in the review is actionable.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Reply routing
|
|
2
|
+
|
|
3
|
+
GitHub's PR APIs are not uniform across comment types. Route by parent type.
|
|
4
|
+
|
|
5
|
+
## Line-level review comment (most common)
|
|
6
|
+
|
|
7
|
+
A comment attached to a specific file:line that belongs to a review thread.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gh api \
|
|
11
|
+
--method POST \
|
|
12
|
+
-H "Accept: application/vnd.github+json" \
|
|
13
|
+
/repos/<owner>/<repo>/pulls/<number>/comments/<comment_id>/replies \
|
|
14
|
+
-f body="Done in $(git rev-parse --short HEAD) — renamed \`fooBar\` to \`foo_bar\`."
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The reply lands in the same review thread with `in_reply_to_id = <comment_id>`.
|
|
18
|
+
|
|
19
|
+
## Review body (CHANGES_REQUESTED with no inline comments)
|
|
20
|
+
|
|
21
|
+
A reviewer left a summary review with `state: CHANGES_REQUESTED` and a body, but **no** inline comments. GitHub has no "reply to review body" endpoint — post a top-level PR comment that references the review:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gh pr comment <number> --repo <owner>/<repo> --body "Re: review #<review_id> — done in $(git rev-parse --short HEAD)."
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Failing CI check
|
|
28
|
+
|
|
29
|
+
No reply. The fix commit IS the response. Include the failing check name in the commit subject:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
fix(ci): typecheck — narrow type of foo
|
|
33
|
+
fix(ci): lint — remove unused import
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Never
|
|
37
|
+
|
|
38
|
+
- Never post a top-level comment in reply to a line-level comment. It loses thread context.
|
|
39
|
+
- Never `gh pr review --comment` for replies — that endpoint is for *new* reviews.
|
|
40
|
+
- Never reply twice to the same comment. The cursor in `last_seen.json` is the only re-entry guard; advance it after every reply.
|
|
@@ -1,151 +1,13 @@
|
|
|
1
1
|
# PR follow-up helpers
|
|
2
2
|
|
|
3
|
-
Generic operational guidance for
|
|
3
|
+
Generic operational guidance for PR-review follow-up. Caller-agnostic — the watcher fetches reviews, the caller (today: `/muggle-do` in address-reviews mode) reads these files to classify and decide what to do.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Each section is its own file — load only what the current step needs.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Index
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
gh pr view <number> --repo <owner>/<repo> --json reviewRequests,author
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
`reviewRequests` is an array of `{ login? , slug? }`. User reviewers have `login`; team reviewers have `slug` (and `name`). Expand teams to member logins:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
gh api orgs/<org>/teams/<slug>/members --jq '.[].login'
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Record `prAuthor = author.login` for the exclusion step.
|
|
24
|
-
|
|
25
|
-
### Step 2: CODEOWNERS
|
|
26
|
-
|
|
27
|
-
Look for the file in this order — first hit wins:
|
|
28
|
-
|
|
29
|
-
1. `.github/CODEOWNERS`
|
|
30
|
-
2. `CODEOWNERS`
|
|
31
|
-
3. `docs/CODEOWNERS`
|
|
32
|
-
|
|
33
|
-
Read from the PR's **head branch** (not master), because a PR that adds CODEOWNERS should be allowed to take effect once merged but is informational while open. In practice this means:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
gh api repos/<owner>/<repo>/contents/.github/CODEOWNERS?ref=<head_sha> --jq '.content' | base64 -d
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Parse line-by-line:
|
|
40
|
-
|
|
41
|
-
- Skip blank lines and lines starting with `#`.
|
|
42
|
-
- Each line is `<pattern> <owner1> <owner2> ...`.
|
|
43
|
-
- Owners are either `@user` or `@org/team`. Strip the leading `@`.
|
|
44
|
-
- For our purposes we don't need to match `<pattern>` against changed files — CODEOWNERS membership for the *repo* is enough. Collect the union of all owners across all lines.
|
|
45
|
-
|
|
46
|
-
Expand `@org/team` to member logins via the orgs/teams/members endpoint (same as Step 1).
|
|
47
|
-
|
|
48
|
-
If no CODEOWNERS file exists in any of the three locations, the CODEOWNERS contribution is empty. Don't fail.
|
|
49
|
-
|
|
50
|
-
### Step 3: filter
|
|
51
|
-
|
|
52
|
-
Allow-list = (requested reviewers ∪ CODEOWNERS) − `{prAuthor}` − bot logins.
|
|
53
|
-
|
|
54
|
-
Bot logins are any login matching:
|
|
55
|
-
|
|
56
|
-
- Ends with `[bot]` (e.g. `dependabot[bot]`)
|
|
57
|
-
- Exact match in the standard list: `dependabot`, `github-actions`, `renovate`, `mergify`
|
|
58
|
-
|
|
59
|
-
A comment author not in the allow-list is silently ignored — do not reply, do not address.
|
|
60
|
-
|
|
61
|
-
## Reply routing
|
|
62
|
-
|
|
63
|
-
GitHub's PR APIs are not uniform across comment types. Route by parent type.
|
|
64
|
-
|
|
65
|
-
### Line-level review comment
|
|
66
|
-
|
|
67
|
-
A comment attached to a specific file:line that belongs to a review thread. This is the **most common** path.
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
gh api \
|
|
71
|
-
--method POST \
|
|
72
|
-
-H "Accept: application/vnd.github+json" \
|
|
73
|
-
/repos/<owner>/<repo>/pulls/<number>/comments/<comment_id>/replies \
|
|
74
|
-
-f body="Done in $(git rev-parse --short HEAD) — renamed \`fooBar\` to \`foo_bar\`."
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
The reply lands in the same review thread. The reply itself becomes a new line-level comment with `in_reply_to_id = <comment_id>`.
|
|
78
|
-
|
|
79
|
-
### Review body (CHANGES_REQUESTED with no inline comments)
|
|
80
|
-
|
|
81
|
-
A reviewer left a summary review with `state: CHANGES_REQUESTED` and a body, but **no** inline comments. GitHub has no "reply to review body" endpoint — post a top-level PR comment that references the review:
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
gh pr comment <number> --repo <owner>/<repo> --body "Re: review #<review_id> — done in $(git rev-parse --short HEAD)."
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Failing CI check
|
|
88
|
-
|
|
89
|
-
No reply. The fix commit IS the response. Include the failing check name in the commit subject so the connection is obvious in `git log`:
|
|
90
|
-
|
|
91
|
-
```
|
|
92
|
-
fix(ci): typecheck — narrow type of foo
|
|
93
|
-
fix(ci): lint — remove unused import
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Never
|
|
97
|
-
|
|
98
|
-
- Never post a top-level comment in reply to a line-level comment. It loses thread context and pollutes the PR conversation tab.
|
|
99
|
-
- Never `gh pr review --comment` for replies — that endpoint is for *new* reviews, not replies.
|
|
100
|
-
- Never reply twice to the same comment. The cursor in `last_seen.json` is the only re-entry guard; advance it after every reply.
|
|
101
|
-
|
|
102
|
-
## Classify
|
|
103
|
-
|
|
104
|
-
Classify the **review as a unit** — but reply per line comment (threaded), not per review.
|
|
105
|
-
|
|
106
|
-
| Class | Signal | Action |
|
|
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 **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
|
-
| **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
|
-
|
|
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
|
-
|
|
113
|
-
Reply shape (all replies for one review reference the same SHA):
|
|
114
|
-
|
|
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>.`
|
|
117
|
-
- **ambiguous**: no bot reply.
|
|
118
|
-
|
|
119
|
-
### Worked examples — Actionable reviews
|
|
120
|
-
|
|
121
|
-
| Review (summarized) | Why actionable |
|
|
122
|
-
| :------------------ | :------------- |
|
|
123
|
-
| 3 comments: "rename `fooBar` to `foo_bar`", "use `const` here", "fix this typo" | Three concrete edits |
|
|
124
|
-
| 1 comment: "could the procedure be simpler?" | Soft-phrased but the intent is clear — simplify; pick the best interpretation, run the cycle, reply with what was changed |
|
|
125
|
-
| Review body: "Mostly looks good. Two things: extract the validation into a helper, and add a null check before the lookup." Plus 0 line comments. | Two concrete directives in the body |
|
|
126
|
-
| 4 comments: "why this approach?", "is this called from X?", "does this need to handle empty array?", "what's the perf here?" | All questions but each is answerable; cycle dispatches an "answer + maybe-fix" pass and the reply summary captures each answer |
|
|
127
|
-
| 1 comment: "rewrite this module — the architecture doesn't match the spec" | Substantive rebuild, but the direction is clear: redo the module per the spec |
|
|
128
|
-
| 1 comment: "I'd lean toward the bar.ts pattern" | Concrete referent (bar.ts) — apply that pattern |
|
|
129
|
-
|
|
130
|
-
The single review goes through one full cycle regardless of comment count.
|
|
131
|
-
|
|
132
|
-
### Worked examples — Ambiguous reviews
|
|
133
|
-
|
|
134
|
-
| Review (summarized) | Why ambiguous |
|
|
135
|
-
| :------------------ | :------------ |
|
|
136
|
-
| 1 comment: "👀" / "hmm" / ":thinking:" | No signal at all |
|
|
137
|
-
| 1 comment: "this is wrong" with no target or direction | Asserts a problem but doesn't propose a fix |
|
|
138
|
-
| 2 comments: "use X" + "but actually don't use X" | Self-contradicting — can't reconcile without the reviewer |
|
|
139
|
-
| 1 comment: "we discussed this offline — please address" | References context the loop doesn't have |
|
|
140
|
-
| 1 comment: "won't this break the prod migration we did last week?" | Implicit change request gated on knowledge the loop can't access |
|
|
141
|
-
| Mixed: 2 concrete directives + 1 comment "but also, rethink the whole approach" | The "rethink the whole approach" subverts the other two; escalate to confirm scope |
|
|
142
|
-
|
|
143
|
-
Escalate per the caller's escalation procedure (write the review id to the cursor's escalated set, emit one terminal message with both interpretations, pause the PR until the user resolves).
|
|
144
|
-
|
|
145
|
-
### Borderline rule
|
|
146
|
-
|
|
147
|
-
If you can paraphrase the review's intent as **"do X"** with X being a concrete change (one or several) — it's actionable. Pick the best interpretation and dispatch the cycle.
|
|
148
|
-
|
|
149
|
-
If you can paraphrase it only as **"the reviewer is dissatisfied but I can't tell with what"** — it's ambiguous.
|
|
150
|
-
|
|
151
|
-
When the review mixes both ("3 concrete directives + 1 dissatisfaction"), the safer move is usually to action the concrete directives and ask about the dissatisfaction in the reply summary. Pure ambiguity means *nothing* in the review is actionable.
|
|
9
|
+
| Section | Use case |
|
|
10
|
+
| :------ | :------- |
|
|
11
|
+
| [`allow-list`](pr-followup-helpers/allow-list.md) | Resolve who counts as a reviewer (requested reviewers ∪ CODEOWNERS − bots − author). |
|
|
12
|
+
| [`reply-routing`](pr-followup-helpers/reply-routing.md) | Pick the right reply endpoint per comment type (line, body-only, CI failure). |
|
|
13
|
+
| [`classify`](pr-followup-helpers/classify.md) | Per-review binary label: actionable vs ambiguous, with worked examples + borderline rule. |
|