pr-shepherd 0.2.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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/marketplace.json +8 -0
  5. package/package.json +62 -0
  6. package/skills/check/SKILL.md +70 -0
  7. package/skills/monitor/SKILL.md +108 -0
  8. package/skills/resolve/SKILL.md +85 -0
  9. package/src/cache/file-cache.mts +101 -0
  10. package/src/cache/file-cache.test.mts +91 -0
  11. package/src/cache/fix-attempts.mts +86 -0
  12. package/src/checks/classify.mts +80 -0
  13. package/src/checks/classify.test.mts +164 -0
  14. package/src/checks/triage.mock.test.mts +202 -0
  15. package/src/checks/triage.mts +88 -0
  16. package/src/cli.mts +423 -0
  17. package/src/commands/check.mts +188 -0
  18. package/src/commands/iterate.mock.test.mts +1111 -0
  19. package/src/commands/iterate.mts +371 -0
  20. package/src/commands/ready-delay.mts +117 -0
  21. package/src/commands/ready-delay.test.mts +116 -0
  22. package/src/commands/resolve.mts +92 -0
  23. package/src/commands/status.mts +173 -0
  24. package/src/comments/outdated.mts +18 -0
  25. package/src/comments/resolve.mts +179 -0
  26. package/src/config/load.mts +240 -0
  27. package/src/config.json +52 -0
  28. package/src/github/batch.mts +351 -0
  29. package/src/github/client.mts +207 -0
  30. package/src/github/client.test.mts +19 -0
  31. package/src/github/gql/batch-pr.gql +130 -0
  32. package/src/github/gql/dismiss-review.gql +7 -0
  33. package/src/github/gql/minimize-comment.gql +7 -0
  34. package/src/github/gql/multi-pr-status-paged.gql +31 -0
  35. package/src/github/gql/multi-pr-status.gql +32 -0
  36. package/src/github/gql/resolve-thread.gql +7 -0
  37. package/src/github/pagination.mts +86 -0
  38. package/src/github/pagination.test.mts +140 -0
  39. package/src/github/queries.mts +30 -0
  40. package/src/index.mts +17 -0
  41. package/src/merge-status/derive.mts +74 -0
  42. package/src/merge-status/derive.test.mts +130 -0
  43. package/src/reporters/json.mts +12 -0
  44. package/src/reporters/text.mts +140 -0
  45. package/src/types.mts +309 -0
  46. package/src/util/path-segment.mts +2 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "pr-shepherd",
3
+ "description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Jonathan Ong",
7
+ "email": "jonathanrichardong@gmail.com"
8
+ },
9
+ "homepage": "https://github.com/jonathanong/pr-shepherd",
10
+ "repository": "https://github.com/jonathanong/pr-shepherd",
11
+ "license": "MIT",
12
+ "keywords": ["github", "pull-request", "ci", "code-review", "automation"],
13
+ "skills": "./skills/"
14
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Ong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # pr-shepherd
2
+
3
+ Autonomous PR CI monitor and review-comment resolver for Claude Code.
4
+
5
+ ## Goals
6
+
7
+ - **Reduced context** — shifts more logic to the CLI instead of the agent
8
+ - **Reduced GitHub rate limit exhaustion** — all GraphQL queries are batched
9
+ - **Reduced agent tool calls** — batching comment resolutions means fewer tool calls and less context used
10
+ - **No MCP** — less reasoning and much faster than using the GitHub MCP
11
+ - **CI cancellation on failure** — avoids wasted CI runs when actionable failures exist
12
+ - **Auto-resolution of all inline comments** — including bot and AI reviewer comments
13
+ - **Automatic resolution of outdated comments** — happens before the agent is involved
14
+ - **Automatic pagination and filtering** — resolved comments never reach the agent
15
+ - **Aggressively hides bot comments** — keeps PR noise low
16
+ - **Waits for pending Copilot reviews** — avoids premature marking as ready
17
+ - **Rebases on conflict** — automatically rebases on the PR base branch when there are merge conflicts
18
+ - **4-minute watch cadence** — keeps Claude's prompt cache warm (5-minute TTL)
19
+ - **10-minute settle window** — waits after the PR is clean before exiting, in case of pending reviews
20
+ - **Draft → ready-for-review** — automatically converts draft PRs when CI passes
21
+ - **Skips non-PR CI checks** — only `pull_request` / `pull_request_target` events count toward readiness
22
+ - **Intended as a PR merge blocker** — pair with a GitHub Actions required check that verifies all threads are resolved
23
+
24
+ ## Why it's built this way
25
+
26
+ Claude's cloud autofix requires CI to verify changes for apps that can't run in the cloud. Running targeted tests locally and letting Claude Code drive is cheaper and avoids vendor lock-in. Skills are used (not subagents) because subagents load all CLAUDE.md context, increasing cost; skills inject into the main conversation instead.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install pr-shepherd
32
+ ```
33
+
34
+ ### As a Claude Code plugin
35
+
36
+ ```bash
37
+ # Install from marketplace
38
+ claude /plugin marketplace add jonathanong/pr-shepherd
39
+ claude /plugin install pr-shepherd
40
+ ```
41
+
42
+ Then use:
43
+
44
+ - `/pr-shepherd:monitor [PR]` — start continuous monitoring
45
+ - `/pr-shepherd:check [PR]` — one-shot status check
46
+ - `/pr-shepherd:resolve [PR]` — fetch, fix, and resolve review comments
47
+
48
+ ## Workflow
49
+
50
+ ```mermaid
51
+ flowchart TD
52
+ U(["/pr-shepherd:monitor PR"]) --> SC["monitor skill"]
53
+ SC -->|CronList| EX{Loop exists<br/>for this PR?}
54
+ EX -->|yes| NOW[Run iterate once<br/>inline and act]
55
+ EX -->|no| CREATE["/loop 4m --max-turns 50 --expires 8h"]
56
+ CREATE --> CRON[(cron tick every 4m)]
57
+ NOW --> ITER
58
+ CRON --> ITER["pr-shepherd iterate PR --format=json"]
59
+
60
+ ITER --> S1{1. last commit<br/>age &lt; cooldown?}
61
+ S1 -->|yes| A_COOL([action: cooldown])
62
+ S1 -->|no| S2["2. runCheck — one GraphQL batch<br/>classify + deriveMergeStatus<br/>+ autoResolveOutdated"]
63
+
64
+ S2 --> S25{2.5 state != OPEN?}
65
+ S25 -->|yes| A_CAN([action: cancel])
66
+ S25 -->|no| S3["3. updateReadyDelay<br/>ready-since.txt"]
67
+ S3 --> S3C{shouldCancel?}
68
+ S3C -->|yes| A_CAN
69
+ S3C -->|no| S4{4. CONFLICTS or actionable<br/>threads/comments/CI/reviews?}
70
+ S4 -->|yes| S4X["gh run cancel actionable runIds"]
71
+ S4X --> A_FIX([action: fix_code])
72
+ S4 -->|no| S5{5. transient<br/>timeout/infra?}
73
+ S5 -->|yes| S5X["gh run rerun runId --failed"]
74
+ S5X --> A_RR([action: rerun_ci])
75
+ S5 -->|no| S6{6. flaky + BEHIND?}
76
+ S6 -->|yes| A_REB([action: rebase])
77
+ S6 -->|no| S7{7. READY + CLEAN<br/>+ isDraft + !copilot?}
78
+ S7 -->|yes| A_MR([action: mark_ready])
79
+ S7 -->|no| A_W([action: wait])
80
+
81
+ A_COOL --> DEC{skill acts on action}
82
+ A_CAN --> DEC
83
+ A_REB --> DEC
84
+ A_FIX --> DEC
85
+ A_RR --> DEC
86
+ A_MR --> DEC
87
+ A_W --> DEC
88
+
89
+ DEC -->|cancel| STOP["/loop cancel"]
90
+ DEC -->|rebase| REB["git fetch && rebase origin/BASE &&<br/>push --force-with-lease"]
91
+ DEC -->|fix_code| FIX["Edit files →<br/>git add + commit →<br/>fetch + rebase + push →<br/>pr-shepherd resolve --require-sha HEAD"]
92
+ FIX --> NEXT[Wait for next tick]
93
+ REB --> NEXT
94
+ DEC -->|other| NEXT
95
+ NEXT --> CRON
96
+ ```
97
+
98
+ ## CLI
99
+
100
+ ```sh
101
+ pr-shepherd check [PR] # read-only PR status snapshot
102
+ pr-shepherd resolve [PR] [--fetch | --resolve-thread-ids …]
103
+ pr-shepherd iterate [PR] [--cooldown-seconds N] [--ready-delay Nm] [--last-push-time N]
104
+ pr-shepherd status PR1 [PR2 …] # multi-PR table
105
+ ```
106
+
107
+ Common flags:
108
+
109
+ | Flag | Default | Description |
110
+ | --------------------- | ------- | ------------------------------- |
111
+ | `--format text\|json` | `text` | Output format |
112
+ | `--no-cache` | false | Bypass the 5-minute file cache |
113
+ | `--cache-ttl N` | 300 | Cache TTL in seconds |
114
+ | `--ready-delay Nm` | `10m` | Settle window before loop exits |
115
+
116
+ ## Configuration
117
+
118
+ Create a `.pr-shepherdrc.yml` in your project root (or any parent directory) to override defaults:
119
+
120
+ ```yaml
121
+ iterate:
122
+ cooldownSeconds: 60 # wait longer after a push before reading CI
123
+ fixAttemptsPerThread: 5 # raise before escalating to manual review
124
+ checks:
125
+ ciTriggerEvents:
126
+ - pull_request
127
+ - pull_request_target
128
+ - merge_group # add for merge-queue repos
129
+ mergeStatus:
130
+ blockingReviewerLogins:
131
+ - copilot # add other review bots here
132
+ actions:
133
+ autoRebase: false # disable for repos that enforce merge commits
134
+ ```
135
+
136
+ See [docs/configuration.md](docs/configuration.md) for all options.
137
+
138
+ ## Requirements
139
+
140
+ - Node.js ≥ 24.0.0
141
+ - `gh` CLI authenticated (`gh auth login`)
142
+ - `git`
143
+
144
+ ## Architecture
145
+
146
+ See [docs/architecture.md](docs/architecture.md) and [docs/](docs/) for full reference docs.
147
+
148
+ ## Forking
149
+
150
+ If you want to customize pr-shepherd for your own use or team, see [docs/forking.md](docs/forking.md).
151
+
152
+ ## License
153
+
154
+ [MIT](LICENSE)
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "pr-shepherd",
3
+ "description": "Autonomous PR CI monitor and review-comment resolver",
4
+ "source": {
5
+ "source": "npm",
6
+ "package": "pr-shepherd"
7
+ }
8
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "pr-shepherd",
3
+ "version": "0.2.0",
4
+ "description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
5
+ "license": "MIT",
6
+ "author": "Jonathan Ong",
7
+ "type": "module",
8
+ "bin": {
9
+ "pr-shepherd": "./src/index.mts"
10
+ },
11
+ "files": [
12
+ "src/**/*.mts",
13
+ "src/**/*.json",
14
+ "src/**/*.gql",
15
+ "skills/**",
16
+ ".claude-plugin/**",
17
+ "marketplace.json",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=24.0.0"
23
+ },
24
+ "dependencies": {
25
+ "yaml": "^2.7.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.6.0",
29
+ "oxfmt": "latest",
30
+ "oxlint": "latest",
31
+ "typescript": "^6.0.3",
32
+ "vitest": "^4.1.4",
33
+ "@vitest/coverage-v8": "^4.1.4"
34
+ },
35
+ "scripts": {
36
+ "prepublishOnly": "npm run typecheck && npm test",
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "oxlint src/ skills/",
39
+ "format": "oxfmt src/ skills/ docs/ README.md",
40
+ "format:check": "oxfmt --check src/ skills/ docs/ README.md",
41
+ "test": "vitest run",
42
+ "test:coverage": "vitest run --coverage",
43
+ "test:watch": "vitest"
44
+ },
45
+ "keywords": [
46
+ "github",
47
+ "pull-request",
48
+ "ci",
49
+ "code-review",
50
+ "automation",
51
+ "claude",
52
+ "claude-code"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/jonathanong/pr-shepherd.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/jonathanong/pr-shepherd/issues"
60
+ },
61
+ "homepage": "https://github.com/jonathanong/pr-shepherd"
62
+ }
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: check
3
+ description: "Check GitHub CI status and review comments for the current PR"
4
+ argument-hint: "[PR number or URL ...]"
5
+ user-invocable: true
6
+ allowed-tools: ["Bash", "Read", "Grep"]
7
+ ---
8
+
9
+ # pr-shepherd check — PR Status
10
+
11
+ ## Arguments: $ARGUMENTS
12
+
13
+ ## Resolve PR number(s)
14
+
15
+ 1. If `$ARGUMENTS` contains PR numbers or GitHub PR URLs, extract the number(s).
16
+ 2. Otherwise, infer: `gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --json number --jq '.[0].number'`
17
+ 3. If no PR found, report an error and stop.
18
+
19
+ For each resolved PR number, check if it is already merged:
20
+
21
+ ```bash
22
+ gh pr view <N> --json state --jq '.state'
23
+ ```
24
+
25
+ If `MERGED`, output: `PR #N is already merged. Nothing to check.` and skip.
26
+
27
+ ## Run the check
28
+
29
+ ```bash
30
+ npx pr-shepherd check <N> --format=json
31
+ ```
32
+
33
+ ## Reporting
34
+
35
+ Parse the JSON output and report all three:
36
+
37
+ - **Merge status** (`report.mergeStatus.status`): CLEAN | BEHIND | CONFLICTS | BLOCKED | UNSTABLE | DRAFT | UNKNOWN — never omit; include `copilotReviewInProgress` when true
38
+ - **CI check results** (`report.checks`): passing count, failing names + kinds, in-progress names
39
+ - **Unresolved review comments** (`report.threads.actionable` + `report.comments.actionable`): count + details with file paths and line numbers
40
+
41
+ ## Rebase policy
42
+
43
+ ```bash
44
+ BASE_BRANCH=$(gh pr view <N> --json baseRefName --jq '.baseRefName')
45
+ ```
46
+
47
+ Rebase (`git fetch origin && git rebase origin/<BASE_BRANCH> && git push --force-with-lease`) when:
48
+
49
+ - Merge conflicts with main (`report.mergeStatus.status == 'CONFLICTS'`), OR
50
+ - About to push commits and branch is behind main, OR
51
+ - `failureKind == 'flaky'` AND branch is behind main
52
+
53
+ Do NOT rebase when nothing to push, no conflicts, and no flaky failures.
54
+
55
+ ## CI budget policy
56
+
57
+ - **actionable**: Summarize errors. Fix in next step.
58
+ - **infrastructure**: Re-run: `gh run rerun <runId> --failed`
59
+ - **timeout**: Re-run: `gh run rerun <runId> --failed`
60
+ - **flaky**: Do NOT cancel. Rebase if behind main.
61
+
62
+ ## Never declare ready to merge
63
+
64
+ Unless ALL of:
65
+
66
+ 1. `report.mergeStatus.mergeStateStatus == 'CLEAN'`
67
+ 2. `report.status == 'READY'`
68
+ 3. `report.mergeStatus.copilotReviewInProgress == false`
69
+
70
+ This is a one-shot check. For continuous monitoring, use `/pr-shepherd:monitor`.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: monitor
3
+ description: "Start continuous CI monitoring — marks PR ready for review when all checks pass"
4
+ argument-hint: "[PR number or URL] [every <interval>] [--ready-delay <duration>]"
5
+ user-invocable: true
6
+ allowed-tools:
7
+ ["Bash", "Read", "Grep", "Edit", "Write", "Glob", "Skill", "CronCreate", "CronList", "CronDelete"]
8
+ ---
9
+
10
+ # pr-shepherd monitor — Continuous PR Monitor
11
+
12
+ ## Arguments: $ARGUMENTS
13
+
14
+ ## Resolve PR number
15
+
16
+ 1. Strip any trailing `every <N> <unit>` interval clause from `$ARGUMENTS` first.
17
+ 2. Extract `--ready-delay <duration>` if present (e.g. `--ready-delay 15m`). Default: `10m`.
18
+ 3. If the remaining text contains a PR number or GitHub PR URL, extract the number.
19
+ 4. Otherwise, infer: `gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --json number --jq '.[0].number'`
20
+ 5. If no PR found, report an error and stop.
21
+
22
+ ## Detect base branch
23
+
24
+ ```bash
25
+ BASE_BRANCH=$(gh pr view <PR_NUMBER> --json baseRefName --jq '.baseRefName')
26
+ ```
27
+
28
+ Default to `main` if the command fails.
29
+
30
+ ## Start the loop
31
+
32
+ **Before starting:** List existing cron jobs with `CronList`.
33
+
34
+ - If any job's prompt contains `# pr-shepherd-loop:pr=<PR_NUMBER>`, **do not cancel+recreate** — perform one iteration now by following the logic in the CronCreate prompt below, then return.
35
+
36
+ **Run the loop:**
37
+
38
+ Invoke `/loop <INTERVAL> --max-turns 50 --expires 8h` via the Skill tool. Use the interval from the argument if provided (e.g. `every 30 minutes` → `30m`), otherwise use `4m`. The loop prompt should be:
39
+
40
+ ````
41
+ # pr-shepherd-loop:pr=<PR_NUMBER>
42
+ Run the following in a single Bash invocation (dangerouslyDisableSandbox: true):
43
+ npx pr-shepherd iterate <PR_NUMBER> --ready-delay <READY_DELAY> --no-cache --last-push-time "$(git log -1 --format=%ct HEAD)" --format=json
44
+
45
+ Exit codes 0, 1, 2, and 3 are all valid signals — always try to parse stdout as JSON first. If the command exits non-zero and stdout is not parseable JSON (e.g. a crash), log the first line of stderr and continue (do not cancel the loop).
46
+
47
+ Parse the `action` field and act:
48
+
49
+ - `cooldown` → log: `SKIP: CI still starting`
50
+ - `wait` → log: `WAIT: <summary.passing> passing, <summary.inProgress> in-progress (merge state: <mergeStateStatus>, <remainingSeconds>s cooldown remaining)`
51
+ - `rerun_ci` → log: `RERAN <N> CI checks: <reran joined by space>`
52
+ - `mark_ready` → log: `MARKED READY: PR <pr>`
53
+ - `cancel` → invoke `/loop cancel` and stop
54
+ - `rebase` → run (dangerouslyDisableSandbox: true):
55
+ ```bash
56
+ if ! git diff --quiet || ! git diff --cached --quiet; then
57
+ echo "SKIP rebase: dirty worktree (uncommitted changes present)"
58
+ exit 0
59
+ fi
60
+ git fetch origin && git rebase origin/<BASE_BRANCH> && git push --force-with-lease
61
+ ```
62
+
63
+ - `escalate` → invoke `/loop cancel` via Skill tool, then print:
64
+
65
+ ⚠️ /pr-shepherd:monitor paused — needs human direction
66
+
67
+ Triggers: <escalate.triggers joined by ", ">
68
+ <escalate.suggestion>
69
+
70
+ Items needing attention:
71
+ <for each thread in escalate.unresolvedThreads: "- threadId=<id> <path ?? '(no location)'>:<line ?? '?'> (@<author>): <body first line>">
72
+ <if escalate.changesRequestedReviews.length > 0: for each "- reviewId=<id> (@<author>): <body first line>">
73
+ <if escalate.attemptHistory: "Fix attempts: " + each "threadId=<id> attempted <N> times">
74
+
75
+ Run /pr-shepherd:check <PR> to see current state.
76
+ After fixing manually, rerun /pr-shepherd:monitor <PR> to resume.
77
+
78
+ - `fix_code` → do the following, then stop this iteration (CI needs time):
79
+ 1. For each item in `fix.threads`, `fix.comments`, `fix.checks`, and `fix.changesRequestedReviews`: read the referenced file/line and apply the fix (Edit/Write tools).
80
+ 2. If files were changed, `git add <files> && git commit -m "<appropriate commit message>"`
81
+ 3. `git fetch origin && git rebase origin/<BASE_BRANCH> && git push --force-with-lease` (dangerouslyDisableSandbox: true)
82
+ 4. `HEAD_SHA=$(git rev-parse HEAD)`
83
+ 5. `npx pr-shepherd resolve <PR_NUMBER> --resolve-thread-ids <IDs> --minimize-comment-ids <IDs> --dismiss-review-ids <IDs> --message "address review comments" --require-sha "$HEAD_SHA"` (dangerouslyDisableSandbox: true). Omit any flag whose ID list is empty.
84
+
85
+ ````
86
+
87
+ **Do NOT call ScheduleWakeup** — the cron job handles its own recurrence. Calling ScheduleWakeup with a `/loop` prompt would create a duplicate cron job on the next fire.
88
+
89
+ The default 4-minute interval is chosen for two reasons:
90
+
91
+ 1. CI checks typically take 2-3 minutes to complete so concurrent agents won't stack.
92
+ 2. 4 minutes keeps iterations within the 5-minute prompt cache TTL.
93
+
94
+ ## Each iteration
95
+
96
+ The loop prompt above handles each iteration directly — no subagent is spawned. The same iterate command can be run manually at any time:
97
+
98
+ ```bash
99
+ npx pr-shepherd iterate <PR_NUMBER> --ready-delay <READY_DELAY> --no-cache --last-push-time "$(git log -1 --format=%ct HEAD)" --format=json
100
+ ```
101
+
102
+ To stop monitoring manually, use `/loop cancel` or close the session.
103
+
104
+ ## Handling multiple PRs
105
+
106
+ To monitor several PRs simultaneously, run `/pr-shepherd:monitor <PR>` once per PR.
107
+ Each call creates its own cron job. Before creating a new loop, run `CronList`
108
+ to verify a loop for that PR doesn't already exist.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: resolve
3
+ description: "Resolve all inline review comments on the current PR"
4
+ argument-hint: "[PR number or URL] [--thread-id ID | --comment-id ID] [--require-sha SHA]"
5
+ user-invocable: true
6
+ allowed-tools: ["Bash", "Read", "Grep", "Edit", "Write", "Glob", "Skill"]
7
+ ---
8
+
9
+ # pr-shepherd resolve — Fix and Resolve Review Comments
10
+
11
+ Resolve unresolved review threads and minimize PR comments on the current PR — from ALL authors.
12
+
13
+ ## Arguments: $ARGUMENTS
14
+
15
+ ## Steps
16
+
17
+ 1. **Resolve PR number and get context:**
18
+
19
+ Parse `$ARGUMENTS`:
20
+ 1. Extract and remove any `--thread-id ID` or `--comment-id ID` flags.
21
+ 2. Extract and remove any `--require-sha SHA` flag.
22
+ 3. Look for a PR number or GitHub PR URL in the remaining text.
23
+ 4. If not found, infer: `gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --json number --jq '.[0].number'`
24
+ 5. If no PR found, report an error and stop.
25
+
26
+ ```bash
27
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
28
+ LAST_PUSH=$(git log -1 --format=%ct HEAD)
29
+ BASE_BRANCH=$(gh pr view <N> --json baseRefName --jq '.baseRefName')
30
+ ```
31
+
32
+ Check if the PR is already merged:
33
+
34
+ ```bash
35
+ gh pr view <N> --json state --jq '.state'
36
+ ```
37
+
38
+ If `MERGED`, invoke `/loop cancel` via Skill tool (to stop any active loop), output merged message, and stop.
39
+
40
+ 2. **Fetch comments:**
41
+
42
+ ```bash
43
+ npx pr-shepherd resolve <N> --fetch --last-push-time "$LAST_PUSH" --format=json
44
+ ```
45
+
46
+ The CLI auto-resolves outdated threads.
47
+ Parse the JSON for `actionableThreads`, `actionableComments`, `changesRequestedReviews`.
48
+
49
+ 3. **Triage each actionable item.** For each unresolved thread or visible comment:
50
+ - Read the comment body to understand what it's asking
51
+ - For review threads: read the referenced file and line
52
+ - Classify as: **Fixed** (already addressed), **Not relevant**, **Outdated**, or **Actionable** (real issue, not yet fixed)
53
+
54
+ 4. **Fix actionable items.** For each Actionable item:
55
+ - Read the relevant file(s) and apply the fix (Edit/Write tools)
56
+ - Re-classify as **Fixed**
57
+ - If too complex: leave as Actionable, report to user
58
+
59
+ 5. **Commit and push** (only if code was changed):
60
+ - `git add <file1> <file2> …` (NOT `git add -A`)
61
+ - `git commit -m "<appropriate commit message>"`
62
+ - `git fetch origin && git rebase origin/$BASE_BRANCH && git push --force-with-lease`
63
+ - Cancel stale CI runs: `gh run list --branch "$BRANCH" --status in_progress --json databaseId --jq '.[].databaseId' | xargs -I{} gh run cancel {}`
64
+
65
+ 6. **Resolve all verified items** — **only after the push:**
66
+
67
+ ```bash
68
+ npx pr-shepherd resolve <N> \
69
+ --resolve-thread-ids <comma-separated-IDs> \
70
+ --minimize-comment-ids <comma-separated-IDs> \
71
+ --dismiss-review-ids <comma-separated-IDs> \
72
+ --message "Addressed in $(git rev-parse HEAD)" \
73
+ --require-sha $(git rev-parse HEAD)
74
+ ```
75
+
76
+ The `--require-sha` flag ensures pr-shepherd verifies GitHub has the new commit before resolving.
77
+
78
+ 7. **Report results** from the CLI output.
79
+
80
+ ## Rules
81
+
82
+ - NEVER resolve threads before pushing fixes (use `--require-sha`).
83
+ - NEVER blindly resolve items — always read and verify first.
84
+ - Resolve from ALL authors — bots, AI reviewers, and humans alike.
85
+ - `--message` is required when using `--dismiss-review-ids`. The CLI will throw if it is missing.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Simple filesystem-based cache for shepherd batch reads.
3
+ *
4
+ * Cache entries live in `${TMPDIR}/pr-shepherd-cache/<owner>-<repo>/<pr>/<shape>.json`.
5
+ * TTL defaults to 5 minutes (configurable via PR_SHEPHERD_CACHE_TTL_SECONDS or --cache-ttl).
6
+ *
7
+ * Mutations are never cached — this module is read-path only.
8
+ */
9
+
10
+ import { readFile, writeFile, rename, mkdir, stat } from "node:fs/promises";
11
+ import { randomUUID } from "node:crypto";
12
+ import { join, dirname } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+ import { loadConfig } from "../config/load.mts";
15
+ import { SAFE_SEGMENT } from "../util/path-segment.mts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Public API
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface CacheOptions {
22
+ ttlSeconds?: number;
23
+ disabled?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Read a value from the cache. Returns null on miss or expiry.
28
+ */
29
+ export async function cacheGet<T>(key: CacheKey, opts: CacheOptions = {}): Promise<T | null> {
30
+ if (opts.disabled) return null;
31
+
32
+ const ttl = opts.ttlSeconds ?? ttlFromEnv() ?? loadConfig().cache.ttlSeconds;
33
+ // A TTL of 0 (or negative) means "always expired" — skip the filesystem read entirely.
34
+ if (ttl <= 0) return null;
35
+
36
+ try {
37
+ const path = resolvePath(key);
38
+ const stats = await stat(path);
39
+ const ageSeconds = (Date.now() - stats.mtimeMs) / 1000;
40
+ if (ageSeconds >= ttl) return null;
41
+
42
+ const raw = await readFile(path, "utf8");
43
+ return JSON.parse(raw) as T;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Write a value to the cache (fire-and-forget — never throws).
51
+ */
52
+ export async function cacheSet<T>(key: CacheKey, value: T, opts: CacheOptions = {}): Promise<void> {
53
+ if (opts.disabled) return;
54
+
55
+ try {
56
+ const path = resolvePath(key);
57
+ const tmp = `${path}.${randomUUID()}.tmp`;
58
+ await mkdir(dirname(path), { recursive: true });
59
+ await writeFile(tmp, JSON.stringify(value), "utf8");
60
+ // Atomic rename — prevents a partial read if two processes write concurrently.
61
+ await rename(tmp, path);
62
+ } catch {
63
+ // Cache writes are best-effort.
64
+ }
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Cache key
69
+ // ---------------------------------------------------------------------------
70
+
71
+ export interface CacheKey {
72
+ owner: string;
73
+ repo: string;
74
+ pr: number;
75
+ shape: string;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Internal helpers
80
+ // ---------------------------------------------------------------------------
81
+
82
+ function resolvePath(key: CacheKey): string {
83
+ for (const [field, value] of [
84
+ ["owner", key.owner],
85
+ ["repo", key.repo],
86
+ ["shape", key.shape],
87
+ ] as const) {
88
+ if (!SAFE_SEGMENT.test(value)) {
89
+ throw new Error(`Invalid cache key segment "${field}": ${value}`);
90
+ }
91
+ }
92
+ const base = process.env["PR_SHEPHERD_CACHE_DIR"] ?? join(tmpdir(), "pr-shepherd-cache");
93
+ return join(base, `${key.owner}-${key.repo}`, String(key.pr), `${key.shape}.json`);
94
+ }
95
+
96
+ function ttlFromEnv(): number | undefined {
97
+ const raw = process.env["PR_SHEPHERD_CACHE_TTL_SECONDS"];
98
+ if (!raw) return undefined;
99
+ const parsed = parseInt(raw, 10);
100
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
101
+ }