pr-shepherd 0.33.0 → 0.35.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 +3 -2
- package/.grok-plugin/marketplace.json +17 -0
- package/README.md +63 -41
- package/bin/api.d.mts +80 -0
- package/bin/api.mjs +236 -0
- package/bin/checks/classify.d.mts +41 -0
- package/bin/checks/startup-failures.d.mts +2 -0
- package/bin/checks/superseded.d.mts +21 -0
- package/bin/checks/triage.d.mts +4 -0
- package/bin/classify/apply.d.mts +18 -0
- package/bin/classify/apply.mjs +4 -0
- package/bin/classify/loader.d.mts +10 -0
- package/bin/classify/types.d.mts +35 -0
- package/bin/cli/args.d.mts +18 -0
- package/bin/cli/clean-formatter.d.mts +2 -0
- package/bin/cli/default-poll.d.mts +2 -0
- package/bin/cli/default-poll.mjs +2 -1
- package/bin/cli/duration-flag.d.mts +2 -0
- package/bin/cli/duration-flag.mjs +5 -4
- package/bin/cli/duration.d.mts +13 -0
- package/bin/cli/{exit-codes.mjs → duration.mjs} +0 -26
- package/bin/cli/fence.d.mts +1 -0
- package/bin/cli/fix-formatter-extra.d.mts +3 -0
- package/bin/cli/fix-formatter.d.mts +2 -0
- package/bin/cli/fix-formatter.mjs +6 -6
- package/bin/cli/formatters.d.mts +7 -0
- package/bin/cli/handlers.d.mts +4 -0
- package/bin/cli/handlers.mjs +20 -18
- package/bin/cli/help-command-pages.d.mts +231 -0
- package/bin/cli/help-command-pages.mjs +116 -72
- package/bin/cli/help-iterate-poll-pages.d.mts +4 -0
- package/bin/cli/help-iterate-poll-pages.mjs +74 -0
- package/bin/cli/help-log-file-page.d.mts +1 -0
- package/bin/cli/help-top-page.d.mts +1 -0
- package/bin/cli/help-top-page.mjs +22 -20
- package/bin/cli/help.d.mts +236 -0
- package/bin/cli/help.mjs +17 -0
- package/bin/cli/iterate-emitter.d.mts +8 -0
- package/bin/cli/iterate-emitter.mjs +2 -2
- package/bin/cli/iterate-flags.d.mts +11 -0
- package/bin/cli/iterate-flags.mjs +1 -1
- package/bin/cli/iterate-formatter.d.mts +19 -0
- package/bin/cli/iterate-instructions.d.mts +6 -0
- package/bin/cli/iterate-lean.d.mts +12 -0
- package/bin/cli/journal-formatter.d.mts +2 -0
- package/bin/cli/journal-formatter.mjs +15 -0
- package/bin/cli/journal-handler.d.mts +1 -0
- package/bin/cli/journal-handler.mjs +13 -25
- package/bin/cli/list-formatters.d.mts +76 -0
- package/bin/cli/list-formatters.mjs +9 -9
- package/bin/cli/mark-files-as-viewed-flags.d.mts +11 -0
- package/bin/cli/mark-files-as-viewed-formatter.d.mts +2 -0
- package/bin/cli/mutate-formatter.d.mts +2 -0
- package/bin/cli/poll-handler.d.mts +1 -0
- package/bin/cli/poll-handler.mjs +3 -3
- package/bin/cli/resolve-validators.d.mts +3 -0
- package/bin/cli/resolve-validators.mjs +6 -5
- package/bin/cli/runner.d.mts +7 -0
- package/bin/cli/suggestion-renderer.d.mts +3 -0
- package/bin/cli/validate-default-args.d.mts +6 -0
- package/bin/cli-parser.d.mts +2 -0
- package/bin/cli-parser.mjs +77 -20
- package/bin/commands/check-annotations.d.mts +5 -0
- package/bin/commands/check-status.d.mts +3 -0
- package/bin/commands/check-terminal-report.d.mts +5 -0
- package/bin/commands/check.d.mts +6 -0
- package/bin/commands/check.mjs +4 -2
- package/bin/commands/clean.d.mts +21 -0
- package/bin/commands/commit-suggestion-instruction.d.mts +8 -0
- package/bin/commands/commit-suggestion-instruction.mjs +3 -3
- package/bin/commands/commit-suggestion.d.mts +8 -0
- package/bin/commands/commit-suggestion.mjs +28 -26
- package/bin/commands/iterate/check-instructions.d.mts +16 -0
- package/bin/commands/iterate/check-instructions.mjs +3 -3
- package/bin/commands/iterate/classify.d.mts +18 -0
- package/bin/commands/iterate/classify.mjs +4 -4
- package/bin/commands/iterate/escalate.d.mts +31 -0
- package/bin/commands/iterate/escalate.mjs +7 -4
- package/bin/commands/iterate/fix-code.d.mts +25 -0
- package/bin/commands/iterate/helpers.d.mts +13 -0
- package/bin/commands/iterate/helpers.mjs +4 -1
- package/bin/commands/iterate/index.d.mts +2 -0
- package/bin/commands/iterate/index.mjs +10 -5
- package/bin/commands/iterate/render.d.mts +5 -0
- package/bin/commands/iterate/render.mjs +8 -6
- package/bin/commands/iterate/reruns.d.mts +20 -0
- package/bin/commands/iterate/stall.d.mts +6 -0
- package/bin/commands/journal/index.d.mts +14 -0
- package/bin/commands/journal/index.mjs +1 -0
- package/bin/commands/journal/transform.d.mts +22 -0
- package/bin/commands/log-file.d.mts +5 -0
- package/bin/commands/mark-files-as-viewed.d.mts +26 -0
- package/bin/commands/mark-files-as-viewed.mjs +8 -5
- package/bin/commands/poll.d.mts +10 -0
- package/bin/commands/poll.mjs +1 -0
- package/bin/commands/ready-delay.d.mts +29 -0
- package/bin/commands/ready-mergeability.d.mts +15 -0
- package/bin/commands/resolve-mutate.d.mts +4 -0
- package/bin/commands/resolve-mutate.mjs +3 -1
- package/bin/commands/resolve.d.mts +4 -0
- package/bin/commands/shepherd-journal.d.mts +7 -0
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/comments/authors.d.mts +14 -0
- package/bin/comments/marker.d.mts +2 -0
- package/bin/comments/minimize-policy.d.mts +4 -0
- package/bin/comments/pending-ops.d.mts +15 -0
- package/bin/comments/rate-limit.d.mts +18 -0
- package/bin/comments/resolve.d.mts +34 -0
- package/bin/comments/resolve.mjs +1 -0
- package/bin/comments/review-thread-markers.d.mts +8 -0
- package/bin/comments/review-visibility.d.mts +28 -0
- package/bin/comments/sha-poll.d.mts +2 -0
- package/bin/comments/thread-visibility.d.mts +11 -0
- package/bin/comments/visible-comments.d.mts +11 -0
- package/bin/config/load.d.mts +60 -0
- package/bin/config/load.mjs +72 -1
- package/bin/config.json +0 -2
- package/bin/execution-context.d.mts +9 -0
- package/bin/execution-context.mjs +19 -0
- package/bin/exit-codes.d.mts +51 -0
- package/bin/exit-codes.mjs +74 -0
- package/bin/github/activity.d.mts +3 -0
- package/bin/github/activity.mjs +7 -0
- package/bin/github/batch-parser-helpers.d.mts +22 -0
- package/bin/github/batch-parsers.d.mts +3 -0
- package/bin/github/batch-parsers.mjs +6 -0
- package/bin/github/batch-raw-types.d.mts +207 -0
- package/bin/github/batch-response.d.mts +4 -0
- package/bin/github/batch-response.mjs +8 -3
- package/bin/github/batch.d.mts +22 -0
- package/bin/github/branch-protection.d.mts +3 -0
- package/bin/github/check-annotations.d.mts +2 -0
- package/bin/github/client.d.mts +46 -0
- package/bin/github/client.mjs +7 -2
- package/bin/github/errors.d.mts +25 -0
- package/bin/github/errors.mjs +26 -2
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/review-thread-comments.gql +1 -0
- package/bin/github/graphql-http.d.mts +19 -0
- package/bin/github/graphql-response.d.mts +7 -0
- package/bin/github/graphql-response.mjs +5 -0
- package/bin/github/http-auth.d.mts +4 -0
- package/bin/github/http-auth.mjs +2 -1
- package/bin/github/http-request.d.mts +7 -0
- package/bin/github/http-utils.d.mts +11 -0
- package/bin/github/http.d.mts +5 -0
- package/bin/github/pagination.d.mts +45 -0
- package/bin/github/queries.d.mts +24 -0
- package/bin/github/rest-http.d.mts +2 -0
- package/bin/github/rest-http.mjs +19 -5
- package/bin/github/thread-comments.d.mts +2 -0
- package/bin/index.d.mts +10 -0
- package/bin/index.mjs +3 -2
- package/bin/log/log-file.d.mts +28 -0
- package/bin/log/session.d.mts +31 -0
- package/bin/log/setup.d.mts +6 -0
- package/bin/mcp/index.d.mts +5 -0
- package/bin/mcp/index.mjs +8 -0
- package/bin/mcp/server.d.mts +8 -0
- package/bin/mcp/server.mjs +157 -0
- package/bin/mcp-stdio.d.mts +2 -0
- package/bin/mcp-stdio.mjs +7 -0
- package/bin/merge-status/derive.d.mts +19 -0
- package/bin/reporters/agent.d.mts +23 -0
- package/bin/reporters/agent.mjs +3 -0
- package/bin/state/base.d.mts +1 -0
- package/bin/state/bot-cr-seen.d.mts +51 -0
- package/bin/state/bot-cr-seen.mjs +1 -1
- package/bin/state/fix-attempts.d.mts +27 -0
- package/bin/state/iterate-stall.d.mts +27 -0
- package/bin/state/seen-comments.d.mts +62 -0
- package/bin/suggestions/extract.d.mts +8 -0
- package/bin/suggestions/parse.d.mts +48 -0
- package/bin/suggestions/patch.d.mts +14 -0
- package/bin/threads/transcript.d.mts +14 -0
- package/bin/threads/transcript.mjs +4 -0
- package/bin/types/activity.d.mts +30 -0
- package/bin/types/agent-thread.d.mts +9 -0
- package/bin/types/check-annotations.d.mts +14 -0
- package/bin/types/check-classification.d.mts +19 -0
- package/bin/types/github.d.mts +139 -0
- package/bin/types/iterate.d.mts +157 -0
- package/bin/types/protected-run.d.mts +6 -0
- package/bin/types/report.d.mts +176 -0
- package/bin/types/review-thread.d.mts +12 -0
- package/bin/types.d.mts +9 -0
- package/bin/util/markdown.d.mts +1 -0
- package/bin/util/path-segment.d.mts +2 -0
- package/bin/util/sleep.d.mts +1 -0
- package/bin/util/worktree.d.mts +9 -0
- package/bin/util/worktree.mjs +4 -1
- package/package.json +51 -37
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +3 -2
- package/plugins/pr-shepherd/.codex.mcp.json +8 -0
- package/plugins/pr-shepherd/.mcp.json +6 -0
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +5 -19
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -15
- package/src/classify/types.mts +12 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type GitHubGraphQlError } from "./errors.mts";
|
|
2
|
+
import { type RateLimitInfo } from "./http-utils.mts";
|
|
3
|
+
interface GraphQlResult<T = unknown> {
|
|
4
|
+
data: T;
|
|
5
|
+
rateLimit?: RateLimitInfo;
|
|
6
|
+
retryAfterSeconds?: number;
|
|
7
|
+
errors?: GitHubGraphQlError[];
|
|
8
|
+
}
|
|
9
|
+
export interface GraphQlRequestOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Mutation batches use partial GraphQL data to preserve per-alias successes.
|
|
12
|
+
* Read paths must keep the strict default so incomplete snapshots never drive
|
|
13
|
+
* PR state transitions.
|
|
14
|
+
*/
|
|
15
|
+
allowPartialData?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function graphql<T = unknown>(query: string, vars?: Record<string, unknown>, opts?: GraphQlRequestOptions): Promise<GraphQlResult<T>>;
|
|
18
|
+
export declare function graphqlWithRateLimit<T = unknown>(query: string, vars?: Record<string, unknown>, opts?: GraphQlRequestOptions): Promise<GraphQlResult<T>>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type GitHubGraphQlError } from "./errors.mts";
|
|
2
|
+
import type { RateLimitInfo } from "./http-utils.mts";
|
|
3
|
+
export declare function parseGraphQlPayload<T>(parsed: unknown, status: number, rateLimit: RateLimitInfo | null, retryAfterSeconds: number | undefined): {
|
|
4
|
+
data: T | null;
|
|
5
|
+
errors?: GitHubGraphQlError[];
|
|
6
|
+
};
|
|
7
|
+
export declare function formatGraphQlErrors(errors: GitHubGraphQlError[] | undefined): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
1
2
|
import { GitHubRequestError } from "./errors.mjs";
|
|
2
3
|
export function parseGraphQlPayload(parsed, status, rateLimit, retryAfterSeconds) {
|
|
3
4
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
@@ -34,9 +35,13 @@ export function formatGraphQlErrors(errors) {
|
|
|
34
35
|
.join("; ");
|
|
35
36
|
}
|
|
36
37
|
function malformedGraphQlResponse(detail, status, rateLimit, retryAfterSeconds) {
|
|
38
|
+
// A response that fails to parse as valid GraphQL shape is an internal/unexpected
|
|
39
|
+
// failure, not a precondition or permission problem — force EX_SOFTWARE rather
|
|
40
|
+
// than letting the (likely 200) status fall through to EX_UNAVAILABLE.
|
|
37
41
|
return new GitHubRequestError(`Malformed GitHub GraphQL response: ${detail}`, {
|
|
38
42
|
status,
|
|
39
43
|
rateLimit: rateLimit ?? undefined,
|
|
40
44
|
retryAfterSeconds,
|
|
45
|
+
exitCodeOverride: EXIT.SOFTWARE,
|
|
41
46
|
});
|
|
42
47
|
}
|
package/bin/github/http-auth.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile as execFileCb } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
3
4
|
const execFile = promisify(execFileCb);
|
|
4
5
|
let _token;
|
|
5
6
|
export function _resetTokenCache() {
|
|
@@ -35,7 +36,7 @@ async function resolveToken() {
|
|
|
35
36
|
_token = codexToken;
|
|
36
37
|
return _token;
|
|
37
38
|
}
|
|
38
|
-
throw new
|
|
39
|
+
throw new ShepherdError("No GitHub token found. Set GH_TOKEN, GITHUB_TOKEN, or GITHUB_PERSONAL_ACCESS_TOKEN, or run `gh auth login`.", EXIT.NOPERM);
|
|
39
40
|
}
|
|
40
41
|
export async function makeHeaders() {
|
|
41
42
|
return {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function isTransportError(err: unknown): boolean;
|
|
2
|
+
export interface RateLimitInfo {
|
|
3
|
+
remaining: number;
|
|
4
|
+
limit: number;
|
|
5
|
+
resetAt: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function sanitizeBody(body: string): string;
|
|
8
|
+
export declare function redactToken(body: string): string;
|
|
9
|
+
export declare function redactUrl(url: string): string;
|
|
10
|
+
export declare function parseRateLimit(headers: Headers): RateLimitInfo | null;
|
|
11
|
+
export declare function parseRetryAfter(headers: Headers): number | undefined;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { GitHubRequestError } from "./errors.mts";
|
|
2
|
+
export { _resetTokenCache } from "./http-auth.mts";
|
|
3
|
+
export { graphql, graphqlWithRateLimit } from "./graphql-http.mts";
|
|
4
|
+
export { rest, restText } from "./rest-http.mts";
|
|
5
|
+
export type { RateLimitInfo } from "./http-utils.mts";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic GraphQL cursor-based paginator for shepherd.
|
|
3
|
+
*
|
|
4
|
+
* Both paginators accept a `fetchFn` instead of calling `graphql` directly,
|
|
5
|
+
* which makes them testable without any mocking — tests supply a pure function
|
|
6
|
+
* that returns pages of fake data.
|
|
7
|
+
*
|
|
8
|
+
* GitHub's GraphQL connections support two cursor directions:
|
|
9
|
+
* - Forward (`after` + `first`) — used by check contexts.
|
|
10
|
+
* - Backward (`before` + `last`) — used by reviewThreads (default GitHub order).
|
|
11
|
+
*/
|
|
12
|
+
interface PageInfo {
|
|
13
|
+
hasNextPage?: boolean;
|
|
14
|
+
hasPreviousPage?: boolean;
|
|
15
|
+
endCursor?: string | null;
|
|
16
|
+
startCursor?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface Connection<T> {
|
|
19
|
+
pageInfo: PageInfo;
|
|
20
|
+
nodes: T[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Paginate forward through a GraphQL connection (`first` / `after` cursors).
|
|
24
|
+
*
|
|
25
|
+
* @param fetchFn Called once per page. Receives the cursor (or null for
|
|
26
|
+
* the very first page) and returns a Connection<T>.
|
|
27
|
+
* @param initialCursor Start from this cursor instead of null. Pass the
|
|
28
|
+
* `endCursor` of an already-fetched page to fetch only
|
|
29
|
+
* the pages *after* it, avoiding a duplicate re-fetch.
|
|
30
|
+
*/
|
|
31
|
+
export declare function paginateForward<T>(fetchFn: (cursor: string | null) => Promise<Connection<T>>, initialCursor?: string | null): Promise<T[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Paginate backward through a GraphQL connection (`last` / `before` cursors).
|
|
34
|
+
*
|
|
35
|
+
* Used for `reviewThreads(last: 100, before: $before)`.
|
|
36
|
+
*
|
|
37
|
+
* @param fetchFn Called once per page. Receives the cursor (or null for
|
|
38
|
+
* the very first page). Returns a Connection<T> with
|
|
39
|
+
* `hasPreviousPage` and `startCursor` in pageInfo.
|
|
40
|
+
* @param initialCursor Start from this cursor instead of null. Pass the
|
|
41
|
+
* `startCursor` of an already-fetched page to avoid
|
|
42
|
+
* re-fetching it (fetch only the pages *before* it).
|
|
43
|
+
*/
|
|
44
|
+
export declare function paginateBackward<T>(fetchFn: (cursor: string | null) => Promise<Connection<T>>, initialCursor?: string | null): Promise<T[]>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL query strings used by pr-shepherd.
|
|
3
|
+
*
|
|
4
|
+
* Static query strings live in src/github/gql/*.gql and are loaded here.
|
|
5
|
+
* Dynamic documents whose content varies per call (e.g. BulkApply in
|
|
6
|
+
* src/comments/resolve.mts) are built at runtime and are exempt — they
|
|
7
|
+
* cannot be expressed as static files.
|
|
8
|
+
*/
|
|
9
|
+
/** The primary batch query that fetches CI + comments + merge status in one round-trip. */
|
|
10
|
+
export declare const BATCH_PR_QUERY: string;
|
|
11
|
+
/** Fetches additional comments for a single review thread when its nested connection paginates. */
|
|
12
|
+
export declare const REVIEW_THREAD_COMMENTS_QUERY: string;
|
|
13
|
+
/** Fetch inline annotations for a single CheckRun by node ID. */
|
|
14
|
+
export declare const CHECK_RUN_ANNOTATIONS_QUERY: string;
|
|
15
|
+
/** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
|
|
16
|
+
export declare const GET_PR_HEAD_SHA_QUERY: string;
|
|
17
|
+
/** Look up PR number by branch name (for getCurrentPrNumber). */
|
|
18
|
+
export declare const PR_NUMBER_BY_BRANCH_QUERY: string;
|
|
19
|
+
/** Convert a draft PR to ready for review. */
|
|
20
|
+
export declare const MARK_PR_READY_MUTATION: string;
|
|
21
|
+
/** Fetch the PR body and node ID. */
|
|
22
|
+
export declare const GET_PR_BODY_QUERY: string;
|
|
23
|
+
/** Update the PR body. */
|
|
24
|
+
export declare const UPDATE_PR_BODY_MUTATION: string;
|
package/bin/github/rest-http.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { appendEntry, nextEntry } from "../log/log-file.mjs";
|
|
2
2
|
import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
3
|
+
import { GitHubRequestError } from "./errors.mjs";
|
|
3
4
|
import { makeHeaders } from "./http-auth.mjs";
|
|
4
5
|
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
5
|
-
import { redactToken, redactUrl, sanitizeBody } from "./http-utils.mjs";
|
|
6
|
+
import { parseRateLimit, parseRetryAfter, redactToken, redactUrl, sanitizeBody, } from "./http-utils.mjs";
|
|
6
7
|
const BASE_URL = "https://api.github.com";
|
|
7
8
|
export async function rest(method, path, body) {
|
|
8
9
|
const url = `${BASE_URL}${path}`;
|
|
@@ -28,7 +29,11 @@ export async function rest(method, path, body) {
|
|
|
28
29
|
textBody: redactToken(text),
|
|
29
30
|
attempt: attempt > 1 ? attempt : undefined,
|
|
30
31
|
}));
|
|
31
|
-
throw new
|
|
32
|
+
throw new GitHubRequestError(`GitHub REST ${method} ${path} failed: ${res.status} ${sanitizeBody(text)}`, {
|
|
33
|
+
status: res.status,
|
|
34
|
+
rateLimit: parseRateLimit(res.headers) ?? undefined,
|
|
35
|
+
retryAfterSeconds: parseRetryAfter(res.headers),
|
|
36
|
+
});
|
|
32
37
|
}
|
|
33
38
|
if (ct.includes("application/json")) {
|
|
34
39
|
const json = (await res.json());
|
|
@@ -80,7 +85,11 @@ export async function restText(path) {
|
|
|
80
85
|
durationMs,
|
|
81
86
|
attempt: attempt > 1 ? attempt : undefined,
|
|
82
87
|
}));
|
|
83
|
-
throw new
|
|
88
|
+
throw new GitHubRequestError(`GitHub REST GET ${path} failed: ${res.status} ${sanitizeBody(text)}`, {
|
|
89
|
+
status: res.status,
|
|
90
|
+
rateLimit: parseRateLimit(res.headers) ?? undefined,
|
|
91
|
+
retryAfterSeconds: parseRetryAfter(res.headers),
|
|
92
|
+
});
|
|
84
93
|
}
|
|
85
94
|
appendEntry(formatResponseEntry({
|
|
86
95
|
n,
|
|
@@ -121,8 +130,13 @@ async function followRestTextRedirect(res, entry) {
|
|
|
121
130
|
durationMs: Math.round(performance.now() - t1),
|
|
122
131
|
contentLength: parseContentLength(redirectRes.headers),
|
|
123
132
|
}));
|
|
124
|
-
if (!redirectRes.ok)
|
|
125
|
-
throw new
|
|
133
|
+
if (!redirectRes.ok) {
|
|
134
|
+
throw new GitHubRequestError(`redirect target ${location} failed: ${redirectRes.status}`, {
|
|
135
|
+
status: redirectRes.status,
|
|
136
|
+
rateLimit: parseRateLimit(redirectRes.headers) ?? undefined,
|
|
137
|
+
retryAfterSeconds: parseRetryAfter(redirectRes.headers),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
126
140
|
return redirectRes.text();
|
|
127
141
|
}
|
|
128
142
|
function parseContentLength(headers) {
|
package/bin/index.d.mts
ADDED
package/bin/index.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* pr-shepherd [PR]
|
|
7
|
-
* pr-shepherd
|
|
7
|
+
* pr-shepherd apply review [PR]
|
|
8
8
|
* pr-shepherd iterate [PR]
|
|
9
9
|
*/
|
|
10
10
|
import { main } from "./cli-parser.mjs";
|
|
11
|
+
import { errorToExitCode } from "./exit-codes.mjs";
|
|
11
12
|
function formatCause(cause, seen = new Set(), depth = 0) {
|
|
12
13
|
if (depth > 5 || seen.has(cause))
|
|
13
14
|
return "[circular or deep cause chain]";
|
|
@@ -23,5 +24,5 @@ main(process.argv).catch((err) => {
|
|
|
23
24
|
const msg = err instanceof Error ? err.message : String(err);
|
|
24
25
|
const causeStr = err instanceof Error && err.cause != null ? formatCause(err.cause) : null;
|
|
25
26
|
process.stderr.write(`pr-shepherd error: ${msg}${causeStr !== null ? ` (cause: ${causeStr})` : ""}\n`);
|
|
26
|
-
process.exit(
|
|
27
|
+
process.exit(errorToExitCode(err));
|
|
27
28
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only per-worktree markdown log.
|
|
3
|
+
*
|
|
4
|
+
* Log path: $PR_SHEPHERD_STATE_DIR/<owner>-<repo>/worktrees/<basename>-<sha8>.md
|
|
5
|
+
*
|
|
6
|
+
* Always-on by default. Set PR_SHEPHERD_LOG_DISABLED=1 or CI=true to disable.
|
|
7
|
+
* Write failures flip an internal disabled flag so the CLI never crashes because
|
|
8
|
+
* logging failed.
|
|
9
|
+
*/
|
|
10
|
+
/** Returns the next monotonically-increasing entry number for the current session. */
|
|
11
|
+
export declare function nextEntry(): number;
|
|
12
|
+
interface RepoKey {
|
|
13
|
+
owner: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function getLogFilePath(key: RepoKey): string;
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the log for this process. Must be called before appendEntry().
|
|
19
|
+
* Silently disables logging on any error (no git repo, bad repo name, etc.).
|
|
20
|
+
*/
|
|
21
|
+
export declare function initLog(repoKey: RepoKey): Promise<string | null>;
|
|
22
|
+
/** Append a pre-formatted markdown chunk to the log. No-op if disabled. */
|
|
23
|
+
export declare function appendEntry(markdown: string): void;
|
|
24
|
+
/** Resolve the log path without initializing (for the log-file subcommand). */
|
|
25
|
+
export declare function resolveLogPath(repoKey: RepoKey): Promise<string>;
|
|
26
|
+
/** Exposed for tests to reset module state. */
|
|
27
|
+
export declare function _resetLogState(): void;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Builds the session header markdown block. */
|
|
2
|
+
export declare function buildSessionHeader(argv: string[]): {
|
|
3
|
+
markdown: string;
|
|
4
|
+
};
|
|
5
|
+
interface HttpRequestEntry {
|
|
6
|
+
n: number;
|
|
7
|
+
kind: "GraphQL" | "REST" | "restText";
|
|
8
|
+
method: string;
|
|
9
|
+
url: string;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface HttpResponseEntry {
|
|
13
|
+
n: number;
|
|
14
|
+
kind: "GraphQL" | "REST" | "restText";
|
|
15
|
+
method: string;
|
|
16
|
+
url: string;
|
|
17
|
+
status: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
/** Parsed response body. Omit for restText. */
|
|
20
|
+
body?: unknown;
|
|
21
|
+
/** Raw text response body. Omit for restText. */
|
|
22
|
+
textBody?: string;
|
|
23
|
+
contentType?: string;
|
|
24
|
+
contentLength?: number;
|
|
25
|
+
/** Set on 401-retry invocations. */
|
|
26
|
+
attempt?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function formatRequestEntry(entry: HttpRequestEntry): string;
|
|
29
|
+
export declare function formatResponseEntry(entry: HttpResponseEntry): string;
|
|
30
|
+
export declare function formatOutputEntry(text: string, format: "text" | "json"): string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize the per-worktree log, write the session header, and install a
|
|
3
|
+
* stdout tee that routes all CLI output to the log. No-op after the first call.
|
|
4
|
+
* Silently skips logging when not in a git repo or on any other error.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setupLog(argv: string[]): Promise<void>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CreatePrShepherdMcpServerOptions } from "./server.mts";
|
|
2
|
+
export { createPrShepherdMcpServer } from "./server.mts";
|
|
3
|
+
export type { CreatePrShepherdMcpServerOptions } from "./server.mts";
|
|
4
|
+
/** Starts the local stdio-only MCP transport. */
|
|
5
|
+
export declare function runPrShepherdMcpStdio(options?: CreatePrShepherdMcpServerOptions): Promise<void>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createPrShepherdMcpServer } from "./server.mjs";
|
|
3
|
+
export { createPrShepherdMcpServer } from "./server.mjs";
|
|
4
|
+
/** Starts the local stdio-only MCP transport. */
|
|
5
|
+
export async function runPrShepherdMcpStdio(options = {}) {
|
|
6
|
+
const server = createPrShepherdMcpServer(options);
|
|
7
|
+
await server.connect(new StdioServerTransport());
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type CreatePrShepherdOptions, type PrShepherd } from "../api.mts";
|
|
3
|
+
export interface CreatePrShepherdMcpServerOptions extends CreatePrShepherdOptions {
|
|
4
|
+
/** Optional injection point for embedding hosts and focused tests. */
|
|
5
|
+
shepherd?: PrShepherd;
|
|
6
|
+
}
|
|
7
|
+
/** Creates a local-only MCP server with the three public Shepherd operations. */
|
|
8
|
+
export declare function createPrShepherdMcpServer(options?: CreatePrShepherdMcpServerOptions): McpServer;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createPrShepherd, PartialApplyError, PrShepherdValidationError, } from "../api.mjs";
|
|
6
|
+
import { formatJournalResult } from "../cli/journal-formatter.mjs";
|
|
7
|
+
import { formatCommitSuggestionResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, } from "../cli/formatters.mjs";
|
|
8
|
+
import { errorToExitCode, EXIT } from "../exit-codes.mjs";
|
|
9
|
+
const pr = z.union([z.number().int().positive(), z.string().url()]).optional();
|
|
10
|
+
const ids = z.array(z.string().min(1)).optional();
|
|
11
|
+
const iterateInputSchema = z.object({
|
|
12
|
+
pr,
|
|
13
|
+
readyDelaySeconds: z.number().nonnegative().optional(),
|
|
14
|
+
stallTimeoutSeconds: z.number().nonnegative().optional(),
|
|
15
|
+
noAutoMarkReady: z.boolean().optional(),
|
|
16
|
+
noAutoCancelActionable: z.boolean().optional(),
|
|
17
|
+
neverCancelRuns: z.array(z.string()).optional(),
|
|
18
|
+
});
|
|
19
|
+
const reviewMutationsOperationSchema = z.object({
|
|
20
|
+
type: z.literal("review_mutations"),
|
|
21
|
+
resolveThreadIds: ids,
|
|
22
|
+
replyThreadIds: ids,
|
|
23
|
+
minimizeCommentIds: ids,
|
|
24
|
+
dismissReviewIds: ids,
|
|
25
|
+
message: z.string().optional(),
|
|
26
|
+
requireSha: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
const markFilesViewedOperationSchema = z.object({
|
|
29
|
+
type: z.literal("mark_files_viewed"),
|
|
30
|
+
files: z.array(z.string().min(1)).optional(),
|
|
31
|
+
tests: z.boolean().optional(),
|
|
32
|
+
matchPatterns: z.array(z.string().min(1)).optional(),
|
|
33
|
+
});
|
|
34
|
+
const appendJournalOperationSchema = z.object({
|
|
35
|
+
type: z.literal("append_journal"),
|
|
36
|
+
item: z.string(),
|
|
37
|
+
dryRun: z.boolean().optional(),
|
|
38
|
+
});
|
|
39
|
+
const applyInputSchema = z.object({
|
|
40
|
+
pr,
|
|
41
|
+
operations: z
|
|
42
|
+
.array(z.discriminatedUnion("type", [
|
|
43
|
+
reviewMutationsOperationSchema,
|
|
44
|
+
markFilesViewedOperationSchema,
|
|
45
|
+
appendJournalOperationSchema,
|
|
46
|
+
]))
|
|
47
|
+
.min(1),
|
|
48
|
+
});
|
|
49
|
+
const suggestionPatchInputSchema = z.object({
|
|
50
|
+
pr,
|
|
51
|
+
threadId: z.string().min(1),
|
|
52
|
+
message: z.string().min(1),
|
|
53
|
+
description: z.string().optional(),
|
|
54
|
+
});
|
|
55
|
+
/** Creates a local-only MCP server with the three public Shepherd operations. */
|
|
56
|
+
export function createPrShepherdMcpServer(options = {}) {
|
|
57
|
+
const shepherd = options.shepherd ?? createPrShepherd({ cwd: options.cwd });
|
|
58
|
+
const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
|
|
59
|
+
server.registerTool("iterate", {
|
|
60
|
+
description: "Inspect the current pull request and return the next Shepherd state.",
|
|
61
|
+
inputSchema: iterateInputSchema,
|
|
62
|
+
annotations: {
|
|
63
|
+
readOnlyHint: false,
|
|
64
|
+
destructiveHint: true,
|
|
65
|
+
idempotentHint: false,
|
|
66
|
+
openWorldHint: true,
|
|
67
|
+
},
|
|
68
|
+
}, async (input) => runTool(() => shepherd.iterate(input), formatIterateResult));
|
|
69
|
+
server.registerTool("apply", {
|
|
70
|
+
description: "Apply ordered review, file-view, and journal operations after prevalidation.",
|
|
71
|
+
inputSchema: applyInputSchema,
|
|
72
|
+
annotations: {
|
|
73
|
+
readOnlyHint: false,
|
|
74
|
+
destructiveHint: true,
|
|
75
|
+
idempotentHint: false,
|
|
76
|
+
openWorldHint: true,
|
|
77
|
+
},
|
|
78
|
+
}, async (input) => runTool(() => shepherd.apply(input), formatApplyResult));
|
|
79
|
+
server.registerTool("build_suggestion_patch", {
|
|
80
|
+
description: "Build, but never apply, a patch from an eligible review suggestion.",
|
|
81
|
+
inputSchema: suggestionPatchInputSchema,
|
|
82
|
+
annotations: {
|
|
83
|
+
readOnlyHint: true,
|
|
84
|
+
destructiveHint: false,
|
|
85
|
+
idempotentHint: true,
|
|
86
|
+
openWorldHint: true,
|
|
87
|
+
},
|
|
88
|
+
}, async (input) => runTool(() => shepherd.buildSuggestionPatch(input), formatCommitSuggestionResult));
|
|
89
|
+
return server;
|
|
90
|
+
}
|
|
91
|
+
function readPackageVersion() {
|
|
92
|
+
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
93
|
+
return packageJson.version;
|
|
94
|
+
}
|
|
95
|
+
function toolResult(result, text) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text }],
|
|
98
|
+
structuredContent: result,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function runTool(work, format) {
|
|
102
|
+
try {
|
|
103
|
+
const result = await work();
|
|
104
|
+
return toolResult(result, format(result));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return toolError(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function formatApplyResult(result) {
|
|
111
|
+
return result.operations
|
|
112
|
+
.map((operation, index) => {
|
|
113
|
+
const heading = `## Operation ${index + 1}: ${operation.type}`;
|
|
114
|
+
switch (operation.type) {
|
|
115
|
+
case "review_mutations":
|
|
116
|
+
return `${heading}\n\n${formatMutateResult(operation.result)}`;
|
|
117
|
+
case "mark_files_viewed":
|
|
118
|
+
return `${heading}\n\n${formatMarkFilesAsViewedResult(operation.result)}`;
|
|
119
|
+
case "append_journal":
|
|
120
|
+
return `${heading}\n\n${formatJournalResult(operation.result)}`;
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.join("\n\n");
|
|
124
|
+
}
|
|
125
|
+
function toolError(error) {
|
|
126
|
+
const code = error instanceof PrShepherdValidationError
|
|
127
|
+
? EXIT.USAGE
|
|
128
|
+
: error instanceof PartialApplyError
|
|
129
|
+
? errorToExitCode(error.cause)
|
|
130
|
+
: errorToExitCode(error);
|
|
131
|
+
const details = error instanceof PartialApplyError
|
|
132
|
+
? { failedIndex: error.failedIndex, completed: redactValue(error.completed) }
|
|
133
|
+
: error instanceof PrShepherdValidationError
|
|
134
|
+
? { validation: true }
|
|
135
|
+
: {};
|
|
136
|
+
const message = redactErrorMessage(error instanceof Error ? error.message : String(error));
|
|
137
|
+
return {
|
|
138
|
+
isError: true,
|
|
139
|
+
content: [{ type: "text", text: `pr-shepherd error (${code}): ${message}` }],
|
|
140
|
+
structuredContent: { code, message, details },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function redactErrorMessage(message) {
|
|
144
|
+
return message
|
|
145
|
+
.replace(/(?:ghp|github_pat)_[A-Za-z0-9_]+/g, "[redacted]")
|
|
146
|
+
.replace(/(authorization:\s*bearer\s+)[^\s]+/gi, "$1[redacted]");
|
|
147
|
+
}
|
|
148
|
+
function redactValue(value) {
|
|
149
|
+
if (typeof value === "string")
|
|
150
|
+
return redactErrorMessage(value);
|
|
151
|
+
if (Array.isArray(value))
|
|
152
|
+
return value.map(redactValue);
|
|
153
|
+
if (value !== null && typeof value === "object") {
|
|
154
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactValue(item)]));
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runPrShepherdMcpStdio } from "./mcp/index.mjs";
|
|
3
|
+
runPrShepherdMcpStdio().catch((error) => {
|
|
4
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5
|
+
process.stderr.write(`pr-shepherd-mcp error: ${message}\n`);
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives a shepherd `MergeStatusResult` from raw PR data.
|
|
3
|
+
*
|
|
4
|
+
* `pr.state` is passed through unchanged; `iterate` handles the cancel action
|
|
5
|
+
* for non-OPEN (merged/closed) PRs — this function does not branch on it.
|
|
6
|
+
*
|
|
7
|
+
* Interpretation order for `status` — first match wins:
|
|
8
|
+
* 1. mergeable == CONFLICTING → CONFLICTS (hard conflict even for drafts)
|
|
9
|
+
* 2. blockingBotReviewInProgress → BLOCKED
|
|
10
|
+
* 3. mergeStateStatus DIRTY → CONFLICTS (GitHub merge conflicts, even for drafts)
|
|
11
|
+
* 4. isDraft → DRAFT
|
|
12
|
+
* 5. mergeStateStatus BEHIND → BEHIND
|
|
13
|
+
* 6. mergeStateStatus BLOCKED / HAS_HOOKS → BLOCKED
|
|
14
|
+
* 7. mergeStateStatus UNSTABLE → UNSTABLE
|
|
15
|
+
* 8. mergeStateStatus UNKNOWN → UNKNOWN
|
|
16
|
+
* 9. mergeStateStatus CLEAN → CLEAN
|
|
17
|
+
*/
|
|
18
|
+
import type { BatchPrData, MergeStatusResult } from "../types.mts";
|
|
19
|
+
export declare function deriveMergeStatus(pr: BatchPrData): MergeStatusResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projections for the agent-facing iterate output.
|
|
3
|
+
*
|
|
4
|
+
* These strip fields that are always-false by the time items reach iterate
|
|
5
|
+
* (isResolved, isOutdated, isMinimized, createdAtUnix) and check metadata the
|
|
6
|
+
* agent/iterate prompt never reads (event, status, category).
|
|
7
|
+
* conclusion is preserved on AgentCheck so the formatter can branch on run-level conclusions.
|
|
8
|
+
* detailsUrl is preserved in AgentCheck as a fallback for external status checks.
|
|
9
|
+
* The original domain types are preserved as internal snapshot types.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReviewThread, PrComment, TriagedCheck, ClassifiedCheck, AgentThread, AgentComment, AgentCheck, AgentStalledCheck } from "../types.mts";
|
|
12
|
+
export declare function toAgentThread(t: ReviewThread): AgentThread;
|
|
13
|
+
export declare function toAgentComment(c: PrComment & {
|
|
14
|
+
edited?: boolean;
|
|
15
|
+
}): AgentComment;
|
|
16
|
+
export declare function toAgentCheck(c: TriagedCheck): AgentCheck;
|
|
17
|
+
export declare function toAgentStalledCheck(c: ClassifiedCheck, nowSeconds: number): AgentStalledCheck;
|
|
18
|
+
/**
|
|
19
|
+
* Project failing checks for the agent. Deduplicates only null-runId external
|
|
20
|
+
* checks by name — when runId is present each check may have a distinct job and
|
|
21
|
+
* log tail, so they are all kept.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toAgentChecks(checks: TriagedCheck[]): AgentCheck[];
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -21,6 +21,7 @@ export function toAgentThread(t) {
|
|
|
21
21
|
t.startLine !== t.line && { startLine: t.startLine }),
|
|
22
22
|
author: t.author,
|
|
23
23
|
...(t.authorType !== undefined && { authorType: t.authorType }),
|
|
24
|
+
...(t.authorAssociation !== undefined && { authorAssociation: t.authorAssociation }),
|
|
24
25
|
body: t.body,
|
|
25
26
|
url: t.url,
|
|
26
27
|
...(t.edited === true && { edited: true }),
|
|
@@ -29,6 +30,7 @@ export function toAgentThread(t) {
|
|
|
29
30
|
id: c.id,
|
|
30
31
|
author: c.author,
|
|
31
32
|
...(c.authorType !== undefined && { authorType: c.authorType }),
|
|
33
|
+
...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
|
|
32
34
|
body: c.body,
|
|
33
35
|
url: c.url,
|
|
34
36
|
})),
|
|
@@ -41,6 +43,7 @@ export function toAgentComment(c) {
|
|
|
41
43
|
id: c.id,
|
|
42
44
|
author: c.author,
|
|
43
45
|
...(c.authorType !== undefined && { authorType: c.authorType }),
|
|
46
|
+
...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
|
|
44
47
|
body: c.body,
|
|
45
48
|
url: c.url,
|
|
46
49
|
...(c.edited === true && { edited: true }),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveStateBase(): string;
|