pr-shepherd 0.45.0 → 0.46.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/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -5
- package/bin/checks/conclusions.d.mts +0 -16
- package/bin/checks/conclusions.mjs +0 -27
- package/bin/cli/fix-formatter.mjs +10 -4
- package/bin/cli/help-command-pages.d.mts +13 -0
- package/bin/cli/help-command-pages.mjs +13 -0
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +14 -1
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-checks-formatter.d.mts +2 -0
- package/bin/cli/iterate-checks-formatter.mjs +55 -0
- package/bin/cli/iterate-formatter.mjs +7 -2
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/journal-extract-handler.d.mts +2 -0
- package/bin/cli/journal-extract-handler.mjs +48 -0
- package/bin/cli/safe-body-file.d.mts +14 -0
- package/bin/cli/safe-body-file.mjs +42 -0
- package/bin/cli-parser.mjs +7 -0
- package/bin/commands/check-status.mjs +4 -4
- package/bin/commands/check.mjs +25 -5
- package/bin/commands/iterate/api-usage.mjs +1 -8
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +8 -32
- package/bin/commands/iterate/classify.mjs +2 -1
- package/bin/commands/iterate/escalate.mjs +63 -9
- package/bin/commands/iterate/fix-code.mjs +83 -80
- package/bin/commands/iterate/index.mjs +2 -2
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +16 -13
- package/bin/commands/poll.mjs +1 -4
- package/bin/comments/review-visibility.d.mts +1 -1
- package/bin/comments/review-visibility.mjs +3 -2
- package/bin/comments/thread-visibility.d.mts +1 -1
- package/bin/comments/thread-visibility.mjs +9 -3
- package/bin/github/client.d.mts +1 -1
- package/bin/github/client.mjs +1 -1
- package/bin/pr-reference.d.mts +1 -1
- package/bin/pr-reference.mjs +1 -1
- package/bin/types/escalate.d.mts +5 -3
- package/bin/types/report.d.mts +1 -1
- package/package.json +2 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +21 -15
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ Each tick returns exactly one action:
|
|
|
31
31
|
|
|
32
32
|
- `WAIT` — no immediate action; continue with the next poll.
|
|
33
33
|
- `MARK_READY` — the CLI converted an eligible draft PR to ready; continue polling.
|
|
34
|
-
- `FIX_CODE` — agent work is required; complete it, then continue polling.
|
|
35
|
-
- `MERGE` — run the emitted auto-merge command only when GitHub reports `viewerCanEnableAutoMerge`;
|
|
34
|
+
- `FIX_CODE` — agent work is required; complete it, push when needed, then continue polling. Push access to the PR head branch is a usage precondition.
|
|
35
|
+
- `MERGE` — run the emitted auto-merge command only when GitHub reports `viewerCanEnableAutoMerge`; missing authorization returns `ESCALATE`.
|
|
36
36
|
- `CANCEL` — stop polling because the PR merged, closed, or completed its ready-delay.
|
|
37
37
|
- `ESCALATE` — stop polling until a human provides direction.
|
|
38
38
|
|
|
@@ -77,18 +77,18 @@ Conversations Resolved: No [Not Required]
|
|
|
77
77
|
9. `[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate again with the same options; without code changes, complete the authorized review mutations and iterate again.
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
See [docs/actions.md](docs/actions.md) for the complete output contract. Iterate/poll PR outcomes use exit codes `0` and `10`–`15`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
|
|
80
|
+
See [docs/actions.md](docs/actions.md) for the complete output contract and [docs/escalations.md](docs/escalations.md) for the exact finite human-handoff boundary. Iterate/poll PR outcomes use exit codes `0` and `10`–`15`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
|
|
81
81
|
|
|
82
82
|
## Workflow Assumptions
|
|
83
83
|
|
|
84
84
|
This system is opinionated and works best with PRs that use required status checks and conversation resolution.
|
|
85
85
|
|
|
86
86
|
- A human inline thread whose original comment has `viewerDidAuthor: true` is replied to and resolved when its latest comment is unmarked. An unmarked other-human inline thread remains reply-only; a marker-ended other-human thread is already acknowledged and receives no further mutation. Human items are never minimized.
|
|
87
|
-
- Detected bots and configured `botUsernames` review threads are returned until resolved
|
|
87
|
+
- Detected bots and configured `botUsernames` review threads are returned until resolved when the required mutation is authorized and the thread has a source location. Unauthorized or unlocated items are surfaced once and then marker-gated until edited. Bot/non-human threads, PR comments, and review summaries can be resolved or minimized when eligible. Review summaries are not minimized while known inline child threads from that review remain unresolved.
|
|
88
88
|
- Shepherd identifies its own latest reply only when that comment begins `<!-- pr-shepherd -->`, not from author equality. A marked viewer-authored thread can be resolved without another reply as a retry.
|
|
89
89
|
- Every review thread/comment/review summary is surfaced at least once, even if already outdated, resolved, or minimized; edited items re-surface through seen markers.
|
|
90
90
|
- Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
|
|
91
|
-
- The CLI never performs git mutations itself — it only emits commit/push instructions for the agent to run.
|
|
91
|
+
- The CLI never performs git mutations itself — it only emits commit/push instructions for the agent to run. Push access to the PR head is a usage precondition; GitHub viewer fields do not create a separate push-authorization handoff.
|
|
92
92
|
- Every GitHub mutation is permission-aware. Shepherd uses raw viewer capability fields, omits unauthorized commands, and repeats authorization checks in direct `apply` commands. Missing capability data fails closed.
|
|
93
93
|
- `build_suggestion_patches` turns one or more ordered GitHub suggestion threads into checked patches and commit metadata, but never edits the working tree or git history. Local HEAD may be ahead when the live PR head is its ancestor.
|
|
94
94
|
|
|
@@ -156,6 +156,17 @@ complete Markdown list item with LF line endings. It fails closed for malformed
|
|
|
156
156
|
containers and ignores journal-shaped examples hidden in Markdown constructs. The full journal API,
|
|
157
157
|
including append and reconciliation helpers, is documented in [docs/api.md](docs/api.md).
|
|
158
158
|
|
|
159
|
+
For shell automation that already has a PR body, use the equivalent local-only command:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
pr-shepherd journal extract --body-file pr-body.md
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It writes one JSON line containing the typed extraction result. It never reads GitHub credentials,
|
|
166
|
+
configuration, or Shepherd logs. On POSIX, its final body-file path entry must be a readable regular
|
|
167
|
+
file in a trusted parent directory; symlinks, FIFOs, and devices are rejected with exit code 66.
|
|
168
|
+
Unsupported platforms fail closed with that same exit code.
|
|
169
|
+
|
|
159
170
|
### Clean Local State
|
|
160
171
|
|
|
161
172
|
`pr-shepherd` stores seen markers, fix-attempt counters, stall fingerprints, ready-delay markers, and logs under `$PR_SHEPHERD_STATE_DIR` (default `$TMPDIR/pr-shepherd-state`).
|
|
@@ -6,22 +6,6 @@ import type { CheckConclusion, ViewerAuthorization } from "../types.mts";
|
|
|
6
6
|
* which covers fork push access — is the right field to gate on.
|
|
7
7
|
*/
|
|
8
8
|
export declare function canRerunWorkflows(auth: ViewerAuthorization | undefined): boolean;
|
|
9
|
-
/**
|
|
10
|
-
* True when the viewer can push commits to the PR head branch — the fork, for a fork PR
|
|
11
|
-
* (own-fork PRs report `headRepositoryPermission: "ADMIN"`), or the base repo otherwise.
|
|
12
|
-
* `viewerCanEditFiles` is GitHub's direct "can the viewer commit to this PR" signal (true
|
|
13
|
-
* for fork authors and for maintainers with "allow edits from maintainers" enabled) and
|
|
14
|
-
* takes priority when present.
|
|
15
|
-
*
|
|
16
|
-
* pr-shepherd's premise is that the caller has push access to the PR it's iterating, so
|
|
17
|
-
* this defaults to pushable: it withholds the autonomous push only when GitHub
|
|
18
|
-
* affirmatively reports no head-branch access (`viewerCanEditFiles === false` and
|
|
19
|
-
* `headRepositoryPermission` in `NONE`/`READ`/`TRIAGE`). An unverifiable/unknown signal —
|
|
20
|
-
* matching the residual risk already accepted for CI reruns (see `canRerunWorkflows`) —
|
|
21
|
-
* is treated as pushable rather than pre-emptively handed off; a push without access
|
|
22
|
-
* simply fails when attempted.
|
|
23
|
-
*/
|
|
24
|
-
export declare function canPushToHead(auth: ViewerAuthorization | undefined): boolean;
|
|
25
9
|
/** Failing-check rows for formatter/instructions — excludes annotation-only carriers. */
|
|
26
10
|
export declare function isFailingAgentCheck(check: {
|
|
27
11
|
conclusion: CheckConclusion;
|
|
@@ -11,33 +11,6 @@ export function canRerunWorkflows(auth) {
|
|
|
11
11
|
const permission = auth?.repositoryPermission;
|
|
12
12
|
return permission != null && RERUN_CAPABLE_PERMISSIONS.has(permission);
|
|
13
13
|
}
|
|
14
|
-
/** Repository roles that carry push access to the head branch. */
|
|
15
|
-
const PUSH_CAPABLE_PERMISSIONS = new Set(["WRITE", "MAINTAIN", "ADMIN"]);
|
|
16
|
-
/**
|
|
17
|
-
* True when the viewer can push commits to the PR head branch — the fork, for a fork PR
|
|
18
|
-
* (own-fork PRs report `headRepositoryPermission: "ADMIN"`), or the base repo otherwise.
|
|
19
|
-
* `viewerCanEditFiles` is GitHub's direct "can the viewer commit to this PR" signal (true
|
|
20
|
-
* for fork authors and for maintainers with "allow edits from maintainers" enabled) and
|
|
21
|
-
* takes priority when present.
|
|
22
|
-
*
|
|
23
|
-
* pr-shepherd's premise is that the caller has push access to the PR it's iterating, so
|
|
24
|
-
* this defaults to pushable: it withholds the autonomous push only when GitHub
|
|
25
|
-
* affirmatively reports no head-branch access (`viewerCanEditFiles === false` and
|
|
26
|
-
* `headRepositoryPermission` in `NONE`/`READ`/`TRIAGE`). An unverifiable/unknown signal —
|
|
27
|
-
* matching the residual risk already accepted for CI reruns (see `canRerunWorkflows`) —
|
|
28
|
-
* is treated as pushable rather than pre-emptively handed off; a push without access
|
|
29
|
-
* simply fails when attempted.
|
|
30
|
-
*/
|
|
31
|
-
export function canPushToHead(auth) {
|
|
32
|
-
if (auth === undefined)
|
|
33
|
-
return true;
|
|
34
|
-
if (auth.viewerCanEditFiles === true)
|
|
35
|
-
return true;
|
|
36
|
-
const permission = auth.headRepositoryPermission;
|
|
37
|
-
if (permission != null && !PUSH_CAPABLE_PERMISSIONS.has(permission))
|
|
38
|
-
return false;
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
14
|
/** True for conclusions that belong under `## Failing checks` (not success/skipped/neutral). */
|
|
42
15
|
function isFailingCheckConclusion(conclusion) {
|
|
43
16
|
return conclusion == null || !NON_FAILING_CONCLUSIONS.has(conclusion);
|
|
@@ -9,9 +9,11 @@ import { isFailingAgentCheck } from "../checks/conclusions.mjs";
|
|
|
9
9
|
import { renderMergeCommand } from "../commands/iterate/merge.mjs";
|
|
10
10
|
export function formatFixCodeResult(header, result) {
|
|
11
11
|
const sections = [header];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const renderThreads = (heading, threads) => {
|
|
13
|
+
if (threads.length === 0)
|
|
14
|
+
return;
|
|
15
|
+
sections.push(heading);
|
|
16
|
+
for (const t of threads) {
|
|
15
17
|
const lineLabel = renderLineRange(t.startLine, t.line);
|
|
16
18
|
const loc = t.path ? `\`${t.path}:${lineLabel}\`` : "(no location)";
|
|
17
19
|
const heading = t.url ? `[threadId=${t.id}](${t.url})` : `\`threadId=${t.id}\``;
|
|
@@ -24,7 +26,11 @@ export function formatFixCodeResult(header, result) {
|
|
|
24
26
|
sections.push(renderSuggestionBlock(t.suggestion, ""));
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
}
|
|
29
|
+
};
|
|
30
|
+
const locatedThreads = result.fix.threads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
31
|
+
const unlocatedThreads = result.fix.threads.filter((thread) => thread.path === null || thread.line === null);
|
|
32
|
+
renderThreads("## Review threads", locatedThreads);
|
|
33
|
+
renderThreads("## Unlocated review threads (logged once — no mutation)", unlocatedThreads);
|
|
28
34
|
if (result.fix.resolutionOnlyThreads.length > 0) {
|
|
29
35
|
sections.push("## Review threads to resolve");
|
|
30
36
|
sections.push(result.fix.resolutionOnlyThreads
|
|
@@ -246,5 +246,18 @@ Flags:
|
|
|
246
246
|
--help, -h Print this help and exit before any GitHub I/O.
|
|
247
247
|
|
|
248
248
|
Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
|
|
249
|
+
readonly "journal extract": `pr-shepherd journal extract
|
|
250
|
+
|
|
251
|
+
Extract one visible, structurally valid Shepherd Journal from a local PR-body file.
|
|
252
|
+
This command performs no GitHub, configuration, or log I/O.
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
pr-shepherd journal extract --body-file <path>
|
|
256
|
+
|
|
257
|
+
The output is exactly one JSON line: the typed result from extractShepherdJournal.
|
|
258
|
+
Malformed or unrecognized journal content is returned as {"ok":false,...} with exit code 0.
|
|
259
|
+
On POSIX, the final body-file path entry must be a readable regular file in a trusted parent directory;
|
|
260
|
+
symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
|
|
261
|
+
--help, -h Print this help and exit before any I/O.`;
|
|
249
262
|
readonly "log-file": "pr-shepherd log-file\n\nPrint the per-worktree append-only debug log path for the current repository.\nThe log is created by the first non-help pr-shepherd command that initializes logging.\n\nUsage:\n pr-shepherd log-file [--format text|json]\n\nFlags:\n --format text|json Print a raw path or {\"path\": \"...\"} JSON. Default: text.\n --help, -h Print this help and exit before logging setup.\n\nEnvironment:\n PR_SHEPHERD_LOG_DISABLED=1 disables logging.\n PR_SHEPHERD_STATE_DIR overrides the base state directory.\n\nExit code: 0 on success; 1 if repository identity cannot be resolved.";
|
|
250
263
|
};
|
|
@@ -249,5 +249,18 @@ Flags:
|
|
|
249
249
|
--help, -h Print this help and exit before any GitHub I/O.
|
|
250
250
|
|
|
251
251
|
Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`,
|
|
252
|
+
"journal extract": `pr-shepherd journal extract
|
|
253
|
+
|
|
254
|
+
Extract one visible, structurally valid Shepherd Journal from a local PR-body file.
|
|
255
|
+
This command performs no GitHub, configuration, or log I/O.
|
|
256
|
+
|
|
257
|
+
Usage:
|
|
258
|
+
pr-shepherd journal extract --body-file <path>
|
|
259
|
+
|
|
260
|
+
The output is exactly one JSON line: the typed result from extractShepherdJournal.
|
|
261
|
+
Malformed or unrecognized journal content is returned as {"ok":false,...} with exit code 0.
|
|
262
|
+
On POSIX, the final body-file path entry must be a readable regular file in a trusted parent directory;
|
|
263
|
+
symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
|
|
264
|
+
--help, -h Print this help and exit before any I/O.`,
|
|
252
265
|
"log-file": LOG_FILE_USAGE,
|
|
253
266
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
1
|
+
export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
pr-shepherd apply review [PR] [review-flags]
|
|
11
11
|
pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]
|
|
12
12
|
pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]
|
|
13
|
+
pr-shepherd journal extract --body-file <path>
|
|
13
14
|
pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]
|
|
14
15
|
pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]
|
|
15
16
|
pr-shepherd admin log-file [--format text|json]
|
|
@@ -20,6 +21,7 @@ Commands:
|
|
|
20
21
|
apply review Apply review-state mutations after fixes.
|
|
21
22
|
apply files Select changed files; no viewed-state mutation is attempted.
|
|
22
23
|
apply journal Append a list item to the Shepherd Journal details block of a PR body.
|
|
24
|
+
journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.
|
|
23
25
|
build-suggestion-patches
|
|
24
26
|
Convert ordered GitHub suggestion threads into patches and commit instructions.
|
|
25
27
|
admin clean Remove pr-shepherd state files.
|
package/bin/cli/help.d.mts
CHANGED
|
@@ -246,8 +246,21 @@ Flags:
|
|
|
246
246
|
--help, -h Print this help and exit before any GitHub I/O.
|
|
247
247
|
|
|
248
248
|
Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
|
|
249
|
+
readonly "journal extract": `pr-shepherd journal extract
|
|
250
|
+
|
|
251
|
+
Extract one visible, structurally valid Shepherd Journal from a local PR-body file.
|
|
252
|
+
This command performs no GitHub, configuration, or log I/O.
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
pr-shepherd journal extract --body-file <path>
|
|
256
|
+
|
|
257
|
+
The output is exactly one JSON line: the typed result from extractShepherdJournal.
|
|
258
|
+
Malformed or unrecognized journal content is returned as {"ok":false,...} with exit code 0.
|
|
259
|
+
On POSIX, the final body-file path entry must be a readable regular file in a trusted parent directory;
|
|
260
|
+
symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
|
|
261
|
+
--help, -h Print this help and exit before any I/O.`;
|
|
249
262
|
readonly "log-file": "pr-shepherd log-file\n\nPrint the per-worktree append-only debug log path for the current repository.\nThe log is created by the first non-help pr-shepherd command that initializes logging.\n\nUsage:\n pr-shepherd log-file [--format text|json]\n\nFlags:\n --format text|json Print a raw path or {\"path\": \"...\"} JSON. Default: text.\n --help, -h Print this help and exit before logging setup.\n\nEnvironment:\n PR_SHEPHERD_LOG_DISABLED=1 disables logging.\n PR_SHEPHERD_STATE_DIR overrides the base state directory.\n\nExit code: 0 on success; 1 if repository identity cannot be resolved.";
|
|
250
|
-
readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
263
|
+
readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
251
264
|
};
|
|
252
265
|
/** Resolve help keys for nested public commands before any command I/O. */
|
|
253
266
|
export declare function helpKeyForArgs(args: string[]): keyof typeof USAGE;
|
package/bin/cli/help.mjs
CHANGED
|
@@ -13,6 +13,8 @@ export function helpKeyForArgs(args) {
|
|
|
13
13
|
return "apply files";
|
|
14
14
|
if (args[0] === "apply" && args[1] === "journal")
|
|
15
15
|
return "apply journal";
|
|
16
|
+
if (args[0] === "journal" && args[1] === "extract")
|
|
17
|
+
return "journal extract";
|
|
16
18
|
if (args[0] === "admin" && args[1] === "clean")
|
|
17
19
|
return "admin clean";
|
|
18
20
|
if (args[0] === "admin" && args[1] === "log-file")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { renderCheckAnnotation } from "./fix-formatter-extra.mjs";
|
|
2
|
+
export function formatRelevantChecks(checks) {
|
|
3
|
+
if (checks.length === 0)
|
|
4
|
+
return null;
|
|
5
|
+
const lines = ["## Checks", ""];
|
|
6
|
+
for (const check of checks) {
|
|
7
|
+
lines.push(...formatRelevantCheck(check));
|
|
8
|
+
}
|
|
9
|
+
return lines.join("\n");
|
|
10
|
+
}
|
|
11
|
+
function formatRelevantCheck(check) {
|
|
12
|
+
const workflow = check.workflowName ? `${check.workflowName} › ` : "";
|
|
13
|
+
const job = check.jobName ?? check.name;
|
|
14
|
+
const lines = [`- \`${workflow}${job}\` [conclusion: ${check.conclusion}]`];
|
|
15
|
+
appendCheckFields(lines, check);
|
|
16
|
+
appendLogExcerpt(lines, check.logExcerpt);
|
|
17
|
+
appendAnnotations(lines, check.annotations);
|
|
18
|
+
return lines;
|
|
19
|
+
}
|
|
20
|
+
function appendCheckFields(lines, check) {
|
|
21
|
+
const codeFields = [
|
|
22
|
+
["run", check.runId],
|
|
23
|
+
["URL", check.detailsUrl],
|
|
24
|
+
["scope", check.scope],
|
|
25
|
+
["commit", check.commitOid],
|
|
26
|
+
];
|
|
27
|
+
for (const [label, value] of codeFields) {
|
|
28
|
+
if (value)
|
|
29
|
+
lines.push(` - ${label}: \`${value}\``);
|
|
30
|
+
}
|
|
31
|
+
const textFields = [
|
|
32
|
+
["failed step", check.failedStep],
|
|
33
|
+
["summary", check.summary],
|
|
34
|
+
];
|
|
35
|
+
for (const [label, value] of textFields) {
|
|
36
|
+
if (value)
|
|
37
|
+
lines.push(` - ${label}: ${value}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function appendLogExcerpt(lines, logExcerpt) {
|
|
41
|
+
if (!logExcerpt)
|
|
42
|
+
return;
|
|
43
|
+
for (const line of logExcerpt.split("\n"))
|
|
44
|
+
lines.push(` > ${line}`);
|
|
45
|
+
}
|
|
46
|
+
function appendAnnotations(lines, annotations) {
|
|
47
|
+
if (!annotations || annotations.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
lines.push(" - annotations:");
|
|
50
|
+
for (const annotation of annotations) {
|
|
51
|
+
for (const line of renderCheckAnnotation(annotation).split("\n")) {
|
|
52
|
+
lines.push(` ${line}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatFixCodeResult } from "./fix-formatter.mjs";
|
|
2
|
+
import { formatRelevantChecks } from "./iterate-checks-formatter.mjs";
|
|
2
3
|
import { joinSections } from "../util/markdown.mjs";
|
|
3
4
|
import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, } from "./iterate-instructions.mjs";
|
|
4
5
|
import { formatMergeRequirementLines } from "../merge-status/requirements-format.mjs";
|
|
@@ -26,7 +27,8 @@ export function formatIterateResult(result, opts) {
|
|
|
26
27
|
const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
|
|
27
28
|
? ` · **reviewDecision** \`${result.reviewDecision}\``
|
|
28
29
|
: "";
|
|
29
|
-
const
|
|
30
|
+
const baseBranchSeg = verbose && result.baseBranch ? ` · **baseBranch** \`${result.baseBranch}\`` : "";
|
|
31
|
+
const baseLine = `**status** \`${result.status}\` · **merge** \`${result.mergeStateStatus}\`${reviewDecisionSeg} · **state** \`${result.state}\` · **repo** \`${result.repo}\`${baseBranchSeg}`;
|
|
30
32
|
let summaryLine;
|
|
31
33
|
if (verbose) {
|
|
32
34
|
let verboseBranch = "";
|
|
@@ -110,7 +112,8 @@ export function formatIterateResult(result, opts) {
|
|
|
110
112
|
const header = headerLines.join("\n");
|
|
111
113
|
const quotaWarning = formatQuotaWarning(result.quotaWarning);
|
|
112
114
|
const apiUsage = verbose ? formatApiUsage(result.apiUsage) : null;
|
|
113
|
-
const
|
|
115
|
+
const verboseChecks = verbose ? formatRelevantChecks(result.checks) : null;
|
|
116
|
+
const telemetrySections = [quotaWarning, apiUsage, verboseChecks];
|
|
114
117
|
switch (result.action) {
|
|
115
118
|
case "wait":
|
|
116
119
|
return joinSections([
|
|
@@ -149,6 +152,7 @@ export function formatIterateResult(result, opts) {
|
|
|
149
152
|
return joinSections([
|
|
150
153
|
cancelHeaderLines.join("\n"),
|
|
151
154
|
...(apiUsage ? [apiUsage] : []),
|
|
155
|
+
...(verboseChecks ? [verboseChecks] : []),
|
|
152
156
|
adaptIterateLog(result.log),
|
|
153
157
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
154
158
|
]);
|
|
@@ -157,6 +161,7 @@ export function formatIterateResult(result, opts) {
|
|
|
157
161
|
return joinSections([
|
|
158
162
|
header,
|
|
159
163
|
...(apiUsage ? [apiUsage] : []),
|
|
164
|
+
...(verboseChecks ? [verboseChecks] : []),
|
|
160
165
|
result.escalate.humanMessage,
|
|
161
166
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
162
167
|
]);
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -158,6 +158,10 @@ export function projectIterateLean(result, opts) {
|
|
|
158
158
|
...(result.escalate.changesRequestedReviews.length > 0 && {
|
|
159
159
|
changesRequestedReviews: result.escalate.changesRequestedReviews,
|
|
160
160
|
}),
|
|
161
|
+
...(result.escalate.checks &&
|
|
162
|
+
result.escalate.checks.length > 0 && {
|
|
163
|
+
checks: result.escalate.checks,
|
|
164
|
+
}),
|
|
161
165
|
...(result.escalate.stalledChecks &&
|
|
162
166
|
result.escalate.stalledChecks.length > 0 && {
|
|
163
167
|
stalledChecks: result.escalate.stalledChecks,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { extractShepherdJournal } from "../journal/index.mjs";
|
|
2
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
3
|
+
import { USAGE, maybePrintHelp } from "./help.mjs";
|
|
4
|
+
import { readSafeBodyFile } from "./safe-body-file.mjs";
|
|
5
|
+
/** Extracts a checked Shepherd Journal from a local PR-body file without external I/O. */
|
|
6
|
+
export async function handleJournalExtract(args) {
|
|
7
|
+
if (maybePrintHelp(args, "journal extract"))
|
|
8
|
+
return;
|
|
9
|
+
const bodyFile = parseBodyFile(args);
|
|
10
|
+
if (bodyFile === null)
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
const body = await readSafeBodyFile(bodyFile);
|
|
14
|
+
process.stdout.write(`${JSON.stringify(extractShepherdJournal(body))}\n`);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
process.stderr.write(`pr-shepherd: journal extract: ${String(error)}\n`);
|
|
18
|
+
process.exitCode = error instanceof ShepherdError ? error.exitCode : EXIT.NOINPUT;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function parseBodyFile(args) {
|
|
22
|
+
let bodyFile;
|
|
23
|
+
for (let index = 0; index < args.length; index++) {
|
|
24
|
+
const arg = args[index];
|
|
25
|
+
if (arg === "--body-file") {
|
|
26
|
+
const value = args[++index];
|
|
27
|
+
if (value === undefined || bodyFile !== undefined) {
|
|
28
|
+
return printUsage();
|
|
29
|
+
}
|
|
30
|
+
bodyFile = value;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith("--body-file=")) {
|
|
34
|
+
const value = arg.slice("--body-file=".length);
|
|
35
|
+
if (value === "" || bodyFile !== undefined)
|
|
36
|
+
return printUsage();
|
|
37
|
+
bodyFile = value;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
return printUsage();
|
|
41
|
+
}
|
|
42
|
+
return bodyFile ?? printUsage();
|
|
43
|
+
}
|
|
44
|
+
function printUsage() {
|
|
45
|
+
process.stderr.write(`${USAGE["journal extract"]}\n`);
|
|
46
|
+
process.exitCode = EXIT.USAGE;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
type FileHandle = Awaited<ReturnType<typeof open>>;
|
|
3
|
+
export interface SafeBodyFileReaderDependencies {
|
|
4
|
+
platform: NodeJS.Platform;
|
|
5
|
+
noFollow: number | undefined;
|
|
6
|
+
nonBlock: number | undefined;
|
|
7
|
+
open: (filePath: string, flags: number) => Promise<FileHandle>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads a regular file after rejecting unsafe final path entries without exposing its contents.
|
|
11
|
+
* Callers must trust parent directories: O_NOFOLLOW only protects the final path entry.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readSafeBodyFile(filePath: string, dependencies?: SafeBodyFileReaderDependencies): Promise<string>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
4
|
+
const DEFAULT_DEPENDENCIES = {
|
|
5
|
+
platform: process.platform,
|
|
6
|
+
noFollow: constants.O_NOFOLLOW,
|
|
7
|
+
nonBlock: constants.O_NONBLOCK,
|
|
8
|
+
open,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Reads a regular file after rejecting unsafe final path entries without exposing its contents.
|
|
12
|
+
* Callers must trust parent directories: O_NOFOLLOW only protects the final path entry.
|
|
13
|
+
*/
|
|
14
|
+
export async function readSafeBodyFile(filePath, dependencies = DEFAULT_DEPENDENCIES) {
|
|
15
|
+
const flags = safeOpenFlags(dependencies);
|
|
16
|
+
if (flags === null)
|
|
17
|
+
throw noInput();
|
|
18
|
+
let handle;
|
|
19
|
+
try {
|
|
20
|
+
handle = await dependencies.open(filePath, flags);
|
|
21
|
+
if (!(await handle.stat()).isFile())
|
|
22
|
+
throw new Error("not a regular file");
|
|
23
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw noInput();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await handle?.close().catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function safeOpenFlags(dependencies) {
|
|
33
|
+
if (dependencies.platform === "win32" ||
|
|
34
|
+
dependencies.noFollow === undefined ||
|
|
35
|
+
dependencies.nonBlock === undefined) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return constants.O_RDONLY | dependencies.noFollow | dependencies.nonBlock;
|
|
39
|
+
}
|
|
40
|
+
function noInput() {
|
|
41
|
+
return new ShepherdError("body file could not be read safely", EXIT.NOINPUT);
|
|
42
|
+
}
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { USAGE, helpKeyForArgs, maybePrintHelp } from "./cli/help.mjs";
|
|
|
10
10
|
import { formatMutateResult } from "./cli/formatters.mjs";
|
|
11
11
|
import { handleClean, handleCommitSuggestion, handleSuggestionPatches, handleIterate, handleMarkFilesAsViewed, } from "./cli/handlers.mjs";
|
|
12
12
|
import { handleJournal } from "./cli/journal-handler.mjs";
|
|
13
|
+
import { handleJournalExtract } from "./cli/journal-extract-handler.mjs";
|
|
13
14
|
import { handlePoll } from "./cli/poll-handler.mjs";
|
|
14
15
|
import { warnPrrcThreadIds, validateRequireSha, rejectPrrcMinimizeIds, } from "./cli/resolve-validators.mjs";
|
|
15
16
|
import { setupLog } from "./log/setup.mjs";
|
|
@@ -32,6 +33,12 @@ export async function main(argv) {
|
|
|
32
33
|
await handleLogFile(args.slice(1));
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
36
|
+
// Extraction deliberately precedes legacy warnings and logging: it is a local,
|
|
37
|
+
// GitHub/config/log-free read path for automation that already has a PR body.
|
|
38
|
+
if (subcommand === "journal" && args[1] === "extract") {
|
|
39
|
+
await handleJournalExtract(args.slice(2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
35
42
|
// The public command groups keep admin diagnostics out of the normal
|
|
36
43
|
// command namespace. Keep this before setupLog because log-file reads the
|
|
37
44
|
// log path and must not initialize/tee the log it is reporting.
|
|
@@ -9,15 +9,15 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
9
9
|
return "FAILING";
|
|
10
10
|
if (verdict.anyInProgress)
|
|
11
11
|
return "IN_PROGRESS";
|
|
12
|
-
// BLOCKED with no remaining shepherd work —
|
|
12
|
+
// BLOCKED with no remaining shepherd work — enter ready-delay regardless of why GitHub
|
|
13
13
|
// is BLOCKED (review pending, insufficient approvals, branch-protection rule, etc.).
|
|
14
14
|
// Requires hasChecks so that a PR with zero relevant checks (CI never started, or all
|
|
15
15
|
// filtered/skipped) doesn't prematurely trigger READY before any check has reported.
|
|
16
16
|
// Exception: UNSTABLE with ignored checks — UNSTABLE means only non-required checks are
|
|
17
|
-
// pending/failing, and if those are all ignored the
|
|
17
|
+
// pending/failing, and if those are all ignored the ready state is safe even with no other checks.
|
|
18
18
|
// BLOCKED is excluded from the ignoredNames extension: BLOCKED can mean required checks haven't
|
|
19
|
-
// started, and
|
|
20
|
-
// blockingBotReviewInProgress is still excluded — a bot review is
|
|
19
|
+
// started, and entering READY prematurely there risks a broken merge attempt.
|
|
20
|
+
// blockingBotReviewInProgress is still excluded — a bot review is Shepherd work, not a READY state.
|
|
21
21
|
const hasRelevantPassingChecks = verdict.hasChecks || (mergeStatus.status === "UNSTABLE" && verdict.ignoredNames.length > 0);
|
|
22
22
|
if (verdict.allPassed &&
|
|
23
23
|
hasRelevantPassingChecks &&
|
package/bin/commands/check.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
|
|
|
16
16
|
import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, } from "../comments/review-visibility.mjs";
|
|
17
17
|
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
18
18
|
import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
|
|
19
|
-
import { normalizeBotUsernames } from "../comments/authors.mjs";
|
|
19
|
+
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
20
|
+
import { buildThreadMutationRouting } from "./iterate/thread-mutation-routing.mjs";
|
|
20
21
|
import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
21
22
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
22
23
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
@@ -80,7 +81,17 @@ export async function runCheck(opts) {
|
|
|
80
81
|
const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize !== true));
|
|
81
82
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate.minimizeComments, botUsernames);
|
|
82
83
|
const deniedRuleAutoResolveThreadIds = new Set(partition.ruleAutoResolveThreadIds.filter((id) => batchData.reviewThreads.find((thread) => thread.id === id)?.viewerCanResolve !== true));
|
|
83
|
-
const
|
|
84
|
+
const visibleThreadCandidates = batchData.reviewThreads.filter((t) => !partition.suppressedThreadIds.has(t.id) || deniedRuleAutoResolveThreadIds.has(t.id));
|
|
85
|
+
const threadMutationRouting = buildThreadMutationRouting(visibleThreadCandidates, botUsernames, partition.ruleAutoResolveThreadIds);
|
|
86
|
+
const replyThreadIds = new Set(threadMutationRouting.replyThreadIds);
|
|
87
|
+
const resolveThreadIds = new Set(threadMutationRouting.resolveThreadIds);
|
|
88
|
+
const repeatableThreadIds = new Set(visibleThreadCandidates
|
|
89
|
+
.filter((thread) => thread.path !== null &&
|
|
90
|
+
thread.line !== null &&
|
|
91
|
+
(!replyThreadIds.has(thread.id) || thread.viewerCanReply === true) &&
|
|
92
|
+
(!resolveThreadIds.has(thread.id) || thread.viewerCanResolve === true))
|
|
93
|
+
.map((thread) => thread.id));
|
|
94
|
+
const threadVisibility = classifyThreadVisibility(visibleThreadCandidates, seenMap, botUsernames, repeatableThreadIds);
|
|
84
95
|
const firstLookComments = minimizedCommentCandidates.flatMap((c) => {
|
|
85
96
|
const cls = classifyItem(c.id, c.body, seenMap);
|
|
86
97
|
if (cls === "unchanged")
|
|
@@ -103,7 +114,7 @@ export async function runCheck(opts) {
|
|
|
103
114
|
else
|
|
104
115
|
seenSummaries.push(r);
|
|
105
116
|
}
|
|
106
|
-
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames);
|
|
117
|
+
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames, batchData.viewerAuthorization?.viewerCanAdminister === true);
|
|
107
118
|
const approvedReviewVisibility = classifyReviewsForDisplay(batchData.approvedReviews, seenMap);
|
|
108
119
|
if (opts.persistSeen !== false) {
|
|
109
120
|
const successfulAnnotations = [
|
|
@@ -145,12 +156,21 @@ export async function runCheck(opts) {
|
|
|
145
156
|
ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize === true),
|
|
146
157
|
};
|
|
147
158
|
const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(authorizedPartition, opts.autoMinimizeSuppressed);
|
|
159
|
+
const visibleMutationThreadIds = new Set([...threadVisibility.activeThreads, ...threadVisibility.resolutionOnlyThreads].map((thread) => thread.id));
|
|
148
160
|
const ruleAutoResolveThreadIds = [
|
|
149
161
|
...authorizedRuleAutoResolveThreadIds,
|
|
150
|
-
...deniedRuleAutoResolveThreadIds,
|
|
162
|
+
...[...deniedRuleAutoResolveThreadIds].filter((id) => visibleMutationThreadIds.has(id)),
|
|
151
163
|
];
|
|
152
164
|
const changesRequestedReviews = changesRequestedReviewVisibility.visible;
|
|
153
|
-
const
|
|
165
|
+
const visibleChangesRequestedIds = new Set(changesRequestedReviews.map((review) => review.id));
|
|
166
|
+
const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((review) => {
|
|
167
|
+
if (partition.suppressedChangesRequestedIds.has(review.id))
|
|
168
|
+
return false;
|
|
169
|
+
const isBot = !isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames);
|
|
170
|
+
return (!isBot ||
|
|
171
|
+
batchData.viewerAuthorization?.viewerCanAdminister === true ||
|
|
172
|
+
visibleChangesRequestedIds.has(review.id));
|
|
173
|
+
}).length;
|
|
154
174
|
const approvedReviews = approvedReviewVisibility.visible;
|
|
155
175
|
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount);
|
|
156
176
|
if (status === "READY" && !didRefreshMergeability) {
|