pr-shepherd 0.60.1 → 1.1.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 +7 -5
- package/bin/api.d.mts +35 -24
- package/bin/api.mjs +68 -48
- package/bin/checks/classify.d.mts +1 -1
- package/bin/checks/classify.mjs +4 -4
- package/bin/checks/log-excerpt.mjs +1 -1
- package/bin/cli/arg-flags.mjs +0 -3
- package/bin/cli/clean-handler.d.mts +1 -1
- package/bin/cli/clean-handler.mjs +5 -15
- package/bin/cli/default-poll.d.mts +2 -0
- package/bin/cli/default-poll.mjs +9 -6
- package/bin/cli/formatters.d.mts +2 -3
- package/bin/cli/formatters.mjs +0 -26
- package/bin/cli/handlers.d.mts +3 -3
- package/bin/cli/handlers.mjs +45 -35
- package/bin/cli/help-command-pages.d.mts +13 -158
- package/bin/cli/help-command-pages.mjs +12 -157
- package/bin/cli/help-iterate-poll-pages.d.mts +4 -3
- package/bin/cli/help-iterate-poll-pages.mjs +76 -36
- package/bin/cli/help-log-file-page.d.mts +1 -1
- package/bin/cli/help-log-file-page.mjs +3 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +19 -20
- package/bin/cli/help.d.mts +14 -159
- package/bin/cli/iterate-checks-formatter.mjs +1 -1
- package/bin/cli/iterate-flags.d.mts +2 -4
- package/bin/cli/iterate-flags.mjs +4 -20
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +6 -6
- package/bin/cli/iterate-instructions.d.mts +1 -1
- package/bin/cli/iterate-instructions.mjs +5 -6
- package/bin/cli/iterate-lean.mjs +32 -40
- package/bin/cli/iterate-verbose.mjs +1 -1
- package/bin/cli/journal-formatter.mjs +0 -3
- package/bin/cli/journal-handler.d.mts +1 -1
- package/bin/cli/journal-handler.mjs +2 -1
- package/bin/cli/mutate-formatter.mjs +0 -9
- package/bin/cli/next-check-format.d.mts +0 -4
- package/bin/cli/next-check-format.mjs +0 -4
- package/bin/cli/poll-handler.d.mts +3 -0
- package/bin/cli/poll-handler.mjs +88 -47
- package/bin/cli/poll-summary-emitter.mjs +2 -2
- package/bin/cli/poll-summary-formatter.mjs +5 -2
- package/bin/cli/poll-targets.d.mts +2 -2
- package/bin/cli/poll-targets.mjs +15 -35
- package/bin/cli/removed-flags.d.mts +3 -0
- package/bin/cli/removed-flags.mjs +24 -0
- package/bin/cli/rule-auto-resolve-lines.d.mts +3 -0
- package/bin/cli/rule-auto-resolve-lines.mjs +12 -0
- package/bin/cli/runner.d.mts +1 -1
- package/bin/cli/runner.mjs +2 -2
- package/bin/cli/stack-layer-view.d.mts +2 -1
- package/bin/cli/stack-overview.d.mts +0 -1
- package/bin/cli/stack-overview.mjs +4 -2
- package/bin/cli/{fix-formatter-extra.d.mts → triage-formatter-extra.d.mts} +1 -2
- package/bin/cli/{fix-formatter-extra.mjs → triage-formatter-extra.mjs} +0 -6
- package/bin/cli/triage-formatter.d.mts +4 -0
- package/bin/cli/{fix-formatter.mjs → triage-formatter.mjs} +31 -42
- package/bin/cli/unknown-flags.d.mts +6 -0
- package/bin/cli/unknown-flags.mjs +29 -0
- package/bin/cli-parser.mjs +41 -62
- package/bin/commands/check-activity.d.mts +8 -0
- package/bin/commands/check-activity.mjs +26 -0
- package/bin/commands/check-annotations.d.mts +1 -1
- package/bin/commands/check-annotations.mjs +3 -2
- package/bin/commands/check-review-budget.d.mts +1 -1
- package/bin/commands/check-review-budget.mjs +1 -1
- package/bin/commands/check-wait-detectors.d.mts +1 -1
- package/bin/commands/check-wait-detectors.mjs +12 -1
- package/bin/commands/check.d.mts +1 -1
- package/bin/commands/check.mjs +13 -8
- package/bin/commands/commit-suggestion-instruction.d.mts +2 -2
- package/bin/commands/commit-suggestion-instruction.mjs +3 -3
- package/bin/commands/iterate/api-usage.mjs +8 -8
- package/bin/commands/iterate/check-instructions.d.mts +4 -4
- package/bin/commands/iterate/check-instructions.mjs +8 -8
- package/bin/commands/iterate/classify.mjs +2 -2
- package/bin/commands/iterate/escalate.mjs +6 -6
- package/bin/commands/iterate/index.mjs +20 -23
- package/bin/commands/iterate/merge-plan.mjs +3 -3
- package/bin/commands/iterate/merge-state.mjs +4 -7
- package/bin/commands/iterate/native-stack-rebase.mjs +1 -1
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +2 -2
- package/bin/commands/iterate/stall.mjs +11 -9
- package/bin/commands/iterate/{fix-loop-instruction.mjs → triage-loop-instruction.mjs} +7 -3
- package/bin/commands/iterate/{fix-code.d.mts → triage.d.mts} +2 -2
- package/bin/commands/iterate/{fix-code.mjs → triage.mjs} +14 -23
- package/bin/commands/iterate/unreported-required.mjs +2 -5
- package/bin/commands/journal/index.d.mts +1 -3
- package/bin/commands/journal/index.mjs +1 -1
- package/bin/commands/mark-files-as-viewed.d.mts +1 -3
- package/bin/commands/mark-files-as-viewed.mjs +1 -1
- package/bin/commands/next-check.d.mts +2 -2
- package/bin/commands/next-check.mjs +4 -2
- package/bin/commands/poll-progress.mjs +1 -1
- package/bin/commands/poll-summary-explicit-instructions.mjs +1 -1
- package/bin/commands/poll-summary-instructions.mjs +7 -7
- package/bin/commands/poll-summary-signature.d.mts +6 -0
- package/bin/commands/poll-summary-signature.mjs +19 -2
- package/bin/commands/poll-summary.mjs +11 -4
- package/bin/commands/poll.d.mts +1 -1
- package/bin/commands/poll.mjs +4 -4
- package/bin/commands/ready-delay.d.mts +1 -0
- package/bin/commands/ready-delay.mjs +8 -3
- package/bin/commands/resolve-mutate.d.mts +1 -1
- package/bin/commands/resolve-mutate.mjs +1 -1
- package/bin/commands/rest-stack-merge-readiness.mjs +1 -1
- package/bin/commands/rule-auto-resolve-format.d.mts +7 -0
- package/bin/commands/rule-auto-resolve-format.mjs +11 -4
- package/bin/commands/rule-auto-resolve.d.mts +14 -2
- package/bin/commands/single-tick.d.mts +5 -0
- package/bin/commands/single-tick.mjs +7 -0
- package/bin/commands/stack-drain.mjs +1 -1
- package/bin/commands/stack-merge-flag.mjs +1 -1
- package/bin/commands/stack-stall.d.mts +2 -2
- package/bin/commands/stack-stall.mjs +2 -2
- package/bin/commands/stack-work.d.mts +2 -2
- package/bin/commands/stack-work.mjs +2 -2
- package/bin/commands/suggestion-patch-item.d.mts +0 -1
- package/bin/commands/suggestion-patch-item.mjs +0 -12
- package/bin/commands/suggestion-patches.d.mts +2 -2
- package/bin/commands/suggestion-patches.mjs +1 -1
- package/bin/commands/tick-instructions.d.mts +9 -0
- package/bin/commands/tick-instructions.mjs +23 -0
- package/bin/commands/tick.d.mts +15 -0
- package/bin/commands/{event-mode.mjs → tick.mjs} +39 -55
- package/bin/comments/minimize-policy.mjs +1 -3
- package/bin/comments/resolve.d.mts +0 -23
- package/bin/comments/resolve.mjs +0 -4
- package/bin/comments/review-output-budget.d.mts +1 -1
- package/bin/comments/review-output-budget.mjs +1 -1
- package/bin/comments/sha-poll.mjs +3 -1
- package/bin/comments/thread-visibility.d.mts +6 -0
- package/bin/comments/thread-visibility.mjs +30 -15
- package/bin/config/default-bot-usernames.d.mts +2 -0
- package/bin/config/default-bot-usernames.mjs +15 -0
- package/bin/config/load.d.mts +4 -138
- package/bin/config/load.mjs +88 -443
- package/bin/config/merge-command-args.mjs +2 -2
- package/bin/config/merge-method.d.mts +1 -2
- package/bin/config/merge-method.mjs +1 -8
- package/bin/config/renames.d.mts +8 -0
- package/bin/config/renames.mjs +87 -0
- package/bin/config/resolve.d.mts +31 -0
- package/bin/config/resolve.mjs +89 -0
- package/bin/config/schema.d.mts +161 -0
- package/bin/config/schema.mjs +168 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +3 -3
- package/bin/github/batch-parse-suites.d.mts +14 -0
- package/bin/github/batch-parse-suites.mjs +4 -1
- package/bin/github/batch-parser-helpers.mjs +3 -0
- package/bin/github/check-annotation-cache.d.mts +5 -0
- package/bin/github/check-annotation-cache.mjs +6 -3
- package/bin/github/fingerprint.mjs +4 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +5 -0
- package/bin/github/poll-summary-annotation-probe.mjs +17 -3
- package/bin/github/poll-summary-annotations.d.mts +14 -0
- package/bin/github/poll-summary-annotations.mjs +75 -0
- package/bin/github/poll-summary-blocking-reviewer.d.mts +2 -0
- package/bin/github/poll-summary-blocking-reviewer.mjs +9 -0
- package/bin/github/poll-summary-check-blockers.mjs +1 -1
- package/bin/github/poll-summary-checks.d.mts +1 -0
- package/bin/github/poll-summary-checks.mjs +20 -2
- package/bin/github/poll-summary-command.mjs +8 -6
- package/bin/github/poll-summary-fingerprint.d.mts +7 -0
- package/bin/github/poll-summary-fingerprint.mjs +7 -0
- package/bin/github/poll-summary-projector.d.mts +3 -1
- package/bin/github/poll-summary-projector.mjs +14 -25
- package/bin/github/poll-summary-queue-removal.d.mts +2 -0
- package/bin/github/poll-summary-queue-removal.mjs +11 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-review-parity.d.mts +24 -0
- package/bin/github/poll-summary-review-parity.mjs +82 -0
- package/bin/github/poll-summary-review.d.mts +11 -2
- package/bin/github/poll-summary-review.mjs +88 -60
- package/bin/github/poll-summary-route.mjs +9 -9
- package/bin/github/poll-summary-row-error.d.mts +16 -0
- package/bin/github/poll-summary-row-error.mjs +74 -0
- package/bin/github/poll-summary-rule-auto-resolve.d.mts +32 -0
- package/bin/github/poll-summary-rule-auto-resolve.mjs +119 -0
- package/bin/github/poll-summary-rule-handoff.d.mts +7 -0
- package/bin/github/poll-summary-rule-handoff.mjs +20 -0
- package/bin/github/poll-summary-rule-items.d.mts +20 -0
- package/bin/github/poll-summary-rule-items.mjs +28 -0
- package/bin/github/poll-summary-unreported.mjs +1 -1
- package/bin/github/poll-summary.mjs +16 -8
- package/bin/github/queries.d.mts +1 -1
- package/bin/github/queries.mjs +1 -1
- package/bin/github/rest-reader-core.mjs +13 -2
- package/bin/github/rest-summary-read.mjs +12 -1
- package/bin/mcp/server.mjs +48 -54
- package/bin/merge-status/derive.mjs +1 -1
- package/bin/state/durable-state.d.mts +1 -3
- package/bin/state/durable-state.mjs +2 -8
- package/bin/state/fix-attempts.d.mts +2 -2
- package/bin/state/fix-attempts.mjs +1 -1
- package/bin/state/merge-request.mjs +1 -1
- package/bin/state/pr-fingerprint.mjs +4 -11
- package/bin/state/seen-comments.d.mts +4 -5
- package/bin/state/seen-comments.mjs +8 -14
- package/bin/state/stack-stall.d.mts +1 -1
- package/bin/state/stack-stall.mjs +1 -1
- package/bin/state/stall-state-store.d.mts +1 -1
- package/bin/state/stall-state-store.mjs +1 -1
- package/bin/types/escalate.d.mts +2 -2
- package/bin/types/github.d.mts +2 -0
- package/bin/types/iterate.d.mts +12 -26
- package/bin/types/merge-queue.d.mts +2 -2
- package/bin/types/next-check.d.mts +1 -1
- package/bin/types/poll-summary.d.mts +26 -6
- package/bin/types/report.d.mts +2 -2
- package/bin/types/suggestion-patch.d.mts +1 -8
- package/package.json +3 -2
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- 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 +6 -6
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/{cloud-event-loop.md → event-loop.md} +5 -5
- package/plugins/pr-shepherd/skills/pr-shepherd/references/mcp-fallback.md +3 -3
- package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/suggestion-patches.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/{fix-code-loop.md → triage-loop.md} +3 -3
- package/plugins/pr-shepherd/skills/reduce-pr-noise/SKILL.md +1 -1
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/settings.md +11 -11
- package/pr-shepherd.schema.json +361 -0
- package/bin/cli/event-state-scope.d.mts +0 -19
- package/bin/cli/event-state-scope.mjs +0 -60
- package/bin/cli/fix-formatter.d.mts +0 -4
- package/bin/commands/commit-suggestion.d.mts +0 -8
- package/bin/commands/commit-suggestion.mjs +0 -26
- package/bin/commands/event-instructions.d.mts +0 -11
- package/bin/commands/event-instructions.mjs +0 -27
- package/bin/commands/event-mode.d.mts +0 -14
- package/bin/commands/poll-mode.d.mts +0 -8
- package/bin/commands/poll-mode.mjs +0 -14
- package/bin/config.json +0 -68
- package/bin/types/protected-run.d.mts +0 -6
- package/bin/types/protected-run.mjs +0 -1
- /package/bin/commands/iterate/{fix-loop-instruction.d.mts → triage-loop-instruction.d.mts} +0 -0
|
@@ -8,11 +8,13 @@ import { missingRepositoryError } from "./errors.mjs";
|
|
|
8
8
|
import { hydratePollSummaryChecks } from "./poll-summary-check-hydration.mjs";
|
|
9
9
|
import { refreshUnknownSummaryMergeability } from "./poll-summary-mergeability.mjs";
|
|
10
10
|
import { summarizePollSummaryPr } from "./poll-summary-projector.mjs";
|
|
11
|
+
import { hydrateRowsIsolated, summarizeRowsIsolated, } from "./poll-summary-row-error.mjs";
|
|
11
12
|
import { trunkRequiredContexts } from "./poll-summary-unreported.mjs";
|
|
12
13
|
import { loadStackSizeHint, storeStackSizeHint } from "./stack-size-hint.mjs";
|
|
13
14
|
import { POLL_SUMMARY_FRAGMENT } from "./queries.mjs";
|
|
14
15
|
import { stackAncestryGaps } from "./stack-read.mjs";
|
|
15
16
|
import { readGraphqlStackSummary } from "./poll-summary-stack-page.mjs";
|
|
17
|
+
import { patchHandoffLayers } from "./poll-summary-rule-handoff.mjs";
|
|
16
18
|
import { stackHandoffSource } from "./stack-handoff.mjs";
|
|
17
19
|
const MAX_EXPLICIT_PRS_PER_QUERY = 50;
|
|
18
20
|
async function fetchSelectedPollSummary(opts, repo) {
|
|
@@ -20,24 +22,30 @@ async function fetchSelectedPollSummary(opts, repo) {
|
|
|
20
22
|
return fetchStackSummary(opts, repo);
|
|
21
23
|
const requested = [...new Set(opts.prNumbers ?? [])];
|
|
22
24
|
if (requested.length === 0) {
|
|
23
|
-
throw new ShepherdError("Aggregate poll requires at least two PRs or
|
|
25
|
+
throw new ShepherdError("Aggregate poll requires at least two PRs or a stack PR", EXIT.USAGE);
|
|
24
26
|
}
|
|
25
27
|
const raw = [];
|
|
28
|
+
const failures = new Map();
|
|
26
29
|
let viewerCanAdminister;
|
|
27
30
|
for (let offset = 0; offset < requested.length; offset += MAX_EXPLICIT_PRS_PER_QUERY) {
|
|
28
31
|
const chunk = requested.slice(offset, offset + MAX_EXPLICIT_PRS_PER_QUERY);
|
|
29
32
|
const fetched = await fetchExplicitChunk(chunk, repo);
|
|
30
33
|
viewerCanAdminister = fetched.viewerCanAdminister;
|
|
31
34
|
raw.push(...fetched.prs);
|
|
35
|
+
for (const [pr, error] of fetched.failures ?? [])
|
|
36
|
+
failures.set(pr, error);
|
|
32
37
|
}
|
|
33
38
|
return {
|
|
34
39
|
selection: { kind: "prs", requested },
|
|
35
|
-
|
|
40
|
+
// One explicit PR's failed read becomes a row; stack selections stay fail-closed.
|
|
41
|
+
prs: await summarizeRowsIsolated(raw, failures, repo, opts, (pr) => summarizePollSummaryPr(pr, repo, opts, viewerCanAdminister, undefined, null, true)),
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
44
|
/** Fresh, read-only snapshot used to bind a one-PR READY receipt to stack routing. */
|
|
39
45
|
export async function fetchRawSummaryPr(pr, repo) {
|
|
40
46
|
const fetched = await fetchExplicitChunk([pr], repo);
|
|
47
|
+
if (fetched.failures?.has(pr))
|
|
48
|
+
throw fetched.failures.get(pr);
|
|
41
49
|
return fetched.prs[0];
|
|
42
50
|
}
|
|
43
51
|
async function fetchGraphqlExplicitChunk(prs, repo) {
|
|
@@ -60,11 +68,8 @@ async function fetchGraphqlExplicitChunk(prs, repo) {
|
|
|
60
68
|
throw new ShepherdError(`PR #${pr} not found`, EXIT.UNAVAILABLE);
|
|
61
69
|
return raw;
|
|
62
70
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await refreshUnknownSummaryMergeability(raw, repo);
|
|
66
|
-
}
|
|
67
|
-
return { prs: rawPrs, viewerCanAdminister: result.data.repository.viewerCanAdminister };
|
|
71
|
+
const failures = await hydrateRowsIsolated(rawPrs, repo);
|
|
72
|
+
return { prs: rawPrs, viewerCanAdminister: result.data.repository.viewerCanAdminister, failures };
|
|
68
73
|
}
|
|
69
74
|
async function fetchExplicitChunk(prs, repo) {
|
|
70
75
|
return githubOperation("PollSummary", () => runWithGithubTransport("graphql", () => fetchGraphqlExplicitChunk(prs, repo)), async () => ({
|
|
@@ -101,9 +106,12 @@ async function fetchStackSummary(opts, repo) {
|
|
|
101
106
|
const handoffSource = getGithubTransport() === "rest" ? undefined : stackHandoffSource(read);
|
|
102
107
|
const stackAncestry = stackAncestryGaps(ordered);
|
|
103
108
|
const required = await trunkRequiredContexts(ordered, repo);
|
|
109
|
+
const prs = await Promise.all(ordered.map((pr) => summarizePollSummaryPr(pr, repo, opts, viewerCanAdminister, required, viewerLogin, true)));
|
|
110
|
+
if (handoffSource)
|
|
111
|
+
patchHandoffLayers(handoffSource.layers, prs);
|
|
104
112
|
return {
|
|
105
113
|
selection: { kind: "stack", anchor, stackNumber, stackSize },
|
|
106
|
-
prs
|
|
114
|
+
prs,
|
|
107
115
|
...(stackAncestry.length > 0 && { stackAncestry }),
|
|
108
116
|
...(allowedMergeMethods && { allowedMergeMethods }),
|
|
109
117
|
...(handoffSource && { handoffSource }),
|
package/bin/github/queries.d.mts
CHANGED
|
@@ -43,7 +43,7 @@ export declare const POLL_STACK_TOPOLOGY_QUERY: string;
|
|
|
43
43
|
export declare const UPPER_LAYER_CONFLICT_TARGET_QUERY: string;
|
|
44
44
|
/** Merged PRs whose head may be the stale base of a non-stack conflict. */
|
|
45
45
|
export declare const MERGED_BASE_PULL_REQUESTS_QUERY: string;
|
|
46
|
-
/** PR head fields plus
|
|
46
|
+
/** PR head fields plus the requested review threads for `suggest`. */
|
|
47
47
|
export declare const SUGGESTION_THREADS_QUERY: string;
|
|
48
48
|
/** Fetches additional comments for a single review thread when its nested connection paginates. */
|
|
49
49
|
export declare const REVIEW_THREAD_COMMENTS_QUERY: string;
|
package/bin/github/queries.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export const POLL_STACK_TOPOLOGY_QUERY = gql("poll-stack-topology.gql");
|
|
|
48
48
|
export const UPPER_LAYER_CONFLICT_TARGET_QUERY = gql("upper-layer-conflict-target.gql");
|
|
49
49
|
/** Merged PRs whose head may be the stale base of a non-stack conflict. */
|
|
50
50
|
export const MERGED_BASE_PULL_REQUESTS_QUERY = gql("merged-base-pull-requests.gql");
|
|
51
|
-
/** PR head fields plus
|
|
51
|
+
/** PR head fields plus the requested review threads for `suggest`. */
|
|
52
52
|
export const SUGGESTION_THREADS_QUERY = gql("suggestion-threads.gql");
|
|
53
53
|
/** Fetches additional comments for a single review thread when its nested connection paginates. */
|
|
54
54
|
export const REVIEW_THREAD_COMMENTS_QUERY = gql("review-thread-comments.gql");
|
|
@@ -101,7 +101,9 @@ function nextRestPage(link, original) {
|
|
|
101
101
|
if (!match)
|
|
102
102
|
malformedRest(`invalid pagination link for ${original}`);
|
|
103
103
|
const url = new URL(match[1], "https://api.github.com");
|
|
104
|
-
|
|
104
|
+
const originalPath = original.split("?")[0];
|
|
105
|
+
if (url.origin !== "https://api.github.com" ||
|
|
106
|
+
!samePaginationResource(url.pathname, originalPath))
|
|
105
107
|
malformedRest(`unsafe pagination link for ${original}`);
|
|
106
108
|
if (url.searchParams.get("per_page") !== "100")
|
|
107
109
|
malformedRest(`changed page size for ${original}`);
|
|
@@ -113,7 +115,16 @@ function nextRestPage(link, original) {
|
|
|
113
115
|
JSON.stringify(source.searchParams.getAll(key)))
|
|
114
116
|
malformedRest(`changed pagination filter for ${original}`);
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
// Rebuild on the requested path so the repository id in a /repositories/{id} link is never trusted.
|
|
119
|
+
return `${originalPath}${url.search}`;
|
|
120
|
+
}
|
|
121
|
+
/** GitHub may link `/repos/{owner}/{repo}/…` pages through `/repositories/{id}/…`. */
|
|
122
|
+
function samePaginationResource(linkPath, originalPath) {
|
|
123
|
+
if (linkPath === originalPath)
|
|
124
|
+
return true;
|
|
125
|
+
const linkSuffix = /^\/repositories\/\d+(\/.*)$/.exec(linkPath)?.[1];
|
|
126
|
+
const originalSuffix = /^\/repos\/[^/]+\/[^/]+(\/.*)$/.exec(originalPath)?.[1];
|
|
127
|
+
return linkSuffix !== undefined && linkSuffix === originalSuffix;
|
|
117
128
|
}
|
|
118
129
|
/** A pull request whose mergeability GitHub is still computing must be re-read, never replayed. */
|
|
119
130
|
function isSettledBody(body) {
|
|
@@ -99,7 +99,7 @@ export function restSummary(data, pull, feedback, checks, baseRef) {
|
|
|
99
99
|
nodes: checks.suites.map((suite) => ({
|
|
100
100
|
status: suite.status.toUpperCase(),
|
|
101
101
|
conclusion: suite.conclusion?.toUpperCase() ?? null,
|
|
102
|
-
workflowRun: checks.workflowRuns.find((run) => run.check_suite_id === suite.id)
|
|
102
|
+
workflowRun: restSuiteWorkflowRun(checks.workflowRuns.find((run) => run.check_suite_id === suite.id)),
|
|
103
103
|
})),
|
|
104
104
|
},
|
|
105
105
|
},
|
|
@@ -110,3 +110,14 @@ export function restSummary(data, pull, feedback, checks, baseRef) {
|
|
|
110
110
|
transportUnavailable: data.transportUnavailable,
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
/** The GraphQL `workflowRun` shape, so summary readers see one suite format on either transport. */
|
|
114
|
+
function restSuiteWorkflowRun(run) {
|
|
115
|
+
if (!run)
|
|
116
|
+
return null;
|
|
117
|
+
return {
|
|
118
|
+
databaseId: run.id,
|
|
119
|
+
event: run.event,
|
|
120
|
+
url: run.html_url,
|
|
121
|
+
workflow: { name: run.name },
|
|
122
|
+
};
|
|
123
|
+
}
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { createPrShepherd, PartialApplyError, PrShepherdValidationError, } from "../api.mjs";
|
|
6
6
|
import { isRepositoryQualifiedPrReference, normalizeRepositoryIdentity, parsePrReference, } from "../pr-reference.mjs";
|
|
7
7
|
import { formatJournalResult } from "../cli/journal-formatter.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { formatSuggestionPatchesResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, projectIterateLean, } from "../cli/formatters.mjs";
|
|
9
9
|
import { formatPollSummaryResult } from "../cli/poll-summary-formatter.mjs";
|
|
10
10
|
import { projectStackOverview } from "../cli/stack-overview.mjs";
|
|
11
11
|
import { formatCliError, serializeGitHubRequestErrorDetails } from "../cli/error-format.mjs";
|
|
@@ -13,6 +13,7 @@ import { errorToExitCode, EXIT } from "../exit-codes.mjs";
|
|
|
13
13
|
import { formatApplyMergeResult } from "../cli/apply-merge-handler.mjs";
|
|
14
14
|
import { extractShepherdJournal } from "../journal/index.mjs";
|
|
15
15
|
import { formatPlaybookResult, runPlaybook } from "../commands/playbook.mjs";
|
|
16
|
+
import { formatCheckBlockerResult } from "../commands/apply-check-blocker.mjs";
|
|
16
17
|
const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/repo#number reference";
|
|
17
18
|
const pr = z
|
|
18
19
|
.string()
|
|
@@ -20,12 +21,8 @@ const pr = z
|
|
|
20
21
|
.describe("GitHub pull-request URL or owner/repo#number; the explicit repository may differ from the server working directory");
|
|
21
22
|
const ids = z.array(z.string().min(1)).optional();
|
|
22
23
|
const transport = z.enum(["auto", "graphql", "rest"]).optional();
|
|
23
|
-
const
|
|
24
|
-
.object({
|
|
24
|
+
const tickOptionsSchema = {
|
|
25
25
|
transport,
|
|
26
|
-
pr: pr.optional(),
|
|
27
|
-
prs: z.array(pr).min(1).optional(),
|
|
28
|
-
stack: pr.optional(),
|
|
29
26
|
readyDelaySeconds: z.number().nonnegative().optional(),
|
|
30
27
|
stallTimeoutSeconds: z.number().nonnegative().optional(),
|
|
31
28
|
noAutoMarkReady: z.boolean().optional(),
|
|
@@ -33,25 +30,20 @@ const iterateInputSchema = z
|
|
|
33
30
|
instructions: z
|
|
34
31
|
.enum(["playbook", "inline"])
|
|
35
32
|
.optional()
|
|
36
|
-
.describe("
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.optional()
|
|
44
|
-
.describe("Deprecated no-op; Shepherd never cancels workflow runs."),
|
|
45
|
-
neverCancelRuns: z
|
|
46
|
-
.array(z.string())
|
|
47
|
-
.optional()
|
|
48
|
-
.describe("Deprecated per-call no-op retained for compatibility."),
|
|
33
|
+
.describe("triage instruction style. inline (default) prints every step each tick; playbook folds commit and push into the Triage loop playbook."),
|
|
34
|
+
};
|
|
35
|
+
const iterateInputSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
...tickOptionsSchema,
|
|
38
|
+
pr: pr.optional(),
|
|
39
|
+
prs: z.array(pr).min(1).optional(),
|
|
49
40
|
})
|
|
50
|
-
.refine((input) =>
|
|
51
|
-
message: "exactly one of pr
|
|
41
|
+
.refine((input) => (input.pr === undefined) !== (input.prs === undefined), {
|
|
42
|
+
message: "exactly one of pr or prs is required",
|
|
52
43
|
})
|
|
53
44
|
.refine((input) => !input.prs ||
|
|
54
45
|
new Set(input.prs.map((ref) => normalizeRepositoryIdentity(parsePrReference(ref)?.repository ?? ""))).size === 1, { message: "all prs must belong to the same repository" });
|
|
46
|
+
const stackInputSchema = z.object({ ...tickOptionsSchema, pr });
|
|
55
47
|
const reviewMutationsOperationSchema = z.object({
|
|
56
48
|
type: z.literal("review_mutations"),
|
|
57
49
|
resolveThreadIds: ids,
|
|
@@ -78,6 +70,16 @@ const acknowledgeQueueRemovalOperationSchema = z.object({
|
|
|
78
70
|
queueCommitOid: z.string().regex(/^[0-9a-f]{40}$/),
|
|
79
71
|
removedAtUnix: z.number().int().positive().safe(),
|
|
80
72
|
});
|
|
73
|
+
const checkBlockerOperationSchema = z.object({
|
|
74
|
+
type: z.literal("check_blocker"),
|
|
75
|
+
check: z.string().min(1).describe("Exact check name."),
|
|
76
|
+
blockedBy: z
|
|
77
|
+
.string()
|
|
78
|
+
.min(1)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Blocking PR/issue URL, owner/repo#N, or issue:owner/repo#N."),
|
|
81
|
+
clear: z.boolean().optional().describe("Clear the recorded blocker instead of setting one."),
|
|
82
|
+
});
|
|
81
83
|
const mergeOperationSchema = z.object({
|
|
82
84
|
type: z.literal("merge"),
|
|
83
85
|
requireSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
@@ -109,17 +111,11 @@ const applyInputSchema = z.object({
|
|
|
109
111
|
markFilesViewedOperationSchema,
|
|
110
112
|
appendJournalOperationSchema,
|
|
111
113
|
acknowledgeQueueRemovalOperationSchema,
|
|
114
|
+
checkBlockerOperationSchema,
|
|
112
115
|
]))
|
|
113
116
|
.min(1),
|
|
114
117
|
});
|
|
115
|
-
const
|
|
116
|
-
transport,
|
|
117
|
-
pr,
|
|
118
|
-
threadId: z.string().min(1),
|
|
119
|
-
message: z.string().min(1),
|
|
120
|
-
description: z.string().optional(),
|
|
121
|
-
});
|
|
122
|
-
const suggestionPatchesInputSchema = z.object({
|
|
118
|
+
const suggestInputSchema = z.object({
|
|
123
119
|
transport,
|
|
124
120
|
pr,
|
|
125
121
|
suggestions: z
|
|
@@ -136,7 +132,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
136
132
|
const getShepherd = () => (shepherd ??= createPrShepherd({ cwd: options.cwd, transport: options.transport }));
|
|
137
133
|
const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
|
|
138
134
|
server.registerTool("iterate", {
|
|
139
|
-
description: "Inspect one pull request
|
|
135
|
+
description: "Inspect one pull request (pr) or an explicit same-repository set (prs) and return one Shepherd tick.",
|
|
140
136
|
inputSchema: iterateInputSchema,
|
|
141
137
|
annotations: {
|
|
142
138
|
readOnlyHint: false,
|
|
@@ -151,14 +147,20 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
151
147
|
};
|
|
152
148
|
return runTool(() => runIterateSelector(getShepherd(), requireRepositoryQualifiedIterate(input)), (result) => isPollSummary(result)
|
|
153
149
|
? formatPollSummaryResult(result)
|
|
154
|
-
: formatIterateResult(result, opts), (result) => isPollSummary(result)
|
|
155
|
-
? result.selection.kind === "stack"
|
|
156
|
-
? projectStackOverview(result)
|
|
157
|
-
: result
|
|
158
|
-
: projectIterateLean(result, opts));
|
|
150
|
+
: formatIterateResult(result, opts), (result) => isPollSummary(result) ? result : projectIterateLean(result, opts));
|
|
159
151
|
});
|
|
152
|
+
server.registerTool("stack", {
|
|
153
|
+
description: "Inspect the native stack containing one pull request and return one aggregate Shepherd tick.",
|
|
154
|
+
inputSchema: stackInputSchema,
|
|
155
|
+
annotations: {
|
|
156
|
+
readOnlyHint: false,
|
|
157
|
+
destructiveHint: true,
|
|
158
|
+
idempotentHint: false,
|
|
159
|
+
openWorldHint: true,
|
|
160
|
+
},
|
|
161
|
+
}, async (input) => runTool(() => getShepherd().stack(requireRepositoryQualifiedPr(input)), formatPollSummaryResult, projectStackOverview));
|
|
160
162
|
server.registerTool("apply", {
|
|
161
|
-
description: "Apply ordered review, journal, and
|
|
163
|
+
description: "Apply ordered merge, review, file-view, journal, check-blocker, and queue-removal operations after prevalidation; explicit requests rely on GitHub's mutation response.",
|
|
162
164
|
inputSchema: applyInputSchema,
|
|
163
165
|
annotations: {
|
|
164
166
|
readOnlyHint: false,
|
|
@@ -202,38 +204,28 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
202
204
|
openWorldHint: false,
|
|
203
205
|
},
|
|
204
206
|
}, async (input) => runTool(async () => runPlaybook(input.name), formatPlaybookResult));
|
|
205
|
-
server.registerTool("
|
|
207
|
+
server.registerTool("suggest", {
|
|
206
208
|
description: "Build, but never apply, an ordered list of eligible review suggestion patches.",
|
|
207
|
-
inputSchema:
|
|
208
|
-
annotations: {
|
|
209
|
-
readOnlyHint: true,
|
|
210
|
-
destructiveHint: false,
|
|
211
|
-
idempotentHint: true,
|
|
212
|
-
openWorldHint: true,
|
|
213
|
-
},
|
|
214
|
-
}, async (input) => runTool(() => getShepherd().buildSuggestionPatches(requireRepositoryQualifiedPr(input)), formatSuggestionPatchesResult));
|
|
215
|
-
server.registerTool("build_suggestion_patch", {
|
|
216
|
-
description: "Deprecated: use build_suggestion_patches with a one-item suggestions array.",
|
|
217
|
-
inputSchema: suggestionPatchInputSchema,
|
|
209
|
+
inputSchema: suggestInputSchema,
|
|
218
210
|
annotations: {
|
|
219
211
|
readOnlyHint: true,
|
|
220
212
|
destructiveHint: false,
|
|
221
213
|
idempotentHint: true,
|
|
222
214
|
openWorldHint: true,
|
|
223
215
|
},
|
|
224
|
-
}, async (input) => runTool(() => getShepherd().
|
|
216
|
+
}, async (input) => runTool(() => getShepherd().suggest(requireRepositoryQualifiedPr(input)), formatSuggestionPatchesResult));
|
|
225
217
|
return server;
|
|
226
218
|
}
|
|
227
219
|
function runIterateSelector(shepherd, input) {
|
|
228
|
-
return "prs" in input
|
|
220
|
+
return "prs" in input && input.prs !== undefined
|
|
229
221
|
? shepherd.iterate(input)
|
|
230
222
|
: shepherd.iterate(input);
|
|
231
223
|
}
|
|
232
224
|
function requireRepositoryQualifiedIterate(input) {
|
|
233
|
-
if (
|
|
234
|
-
throw new PrShepherdValidationError("exactly one of pr
|
|
225
|
+
if ((input.pr === undefined) === (input.prs === undefined)) {
|
|
226
|
+
throw new PrShepherdValidationError("exactly one of pr or prs is required");
|
|
235
227
|
}
|
|
236
|
-
const refs = Array.isArray(input.prs) ? input.prs : [input.pr
|
|
228
|
+
const refs = Array.isArray(input.prs) ? input.prs : [input.pr];
|
|
237
229
|
if (refs.length === 0 || refs.some((ref) => !isRepositoryQualifiedPrReference(ref))) {
|
|
238
230
|
throw new PrShepherdValidationError(QUALIFIED_PR_ERROR);
|
|
239
231
|
}
|
|
@@ -264,7 +256,7 @@ function toolResult(structured, text) {
|
|
|
264
256
|
}
|
|
265
257
|
/**
|
|
266
258
|
* `project` mirrors the CLI's `--format=json` treatment of the same result. Tools
|
|
267
|
-
* whose CLI JSON is the raw result object (apply,
|
|
259
|
+
* whose CLI JSON is the raw result object (apply, suggest — see
|
|
268
260
|
* handlers.mts and cli-parser.mts, which JSON.stringify the result directly) omit
|
|
269
261
|
* `project` and return the result unchanged, matching their own CLI JSON output.
|
|
270
262
|
*/
|
|
@@ -290,6 +282,8 @@ function formatApplyResult(result) {
|
|
|
290
282
|
return `${heading}\n\n${formatMarkFilesAsViewedResult(operation.result)}`;
|
|
291
283
|
case "append_journal":
|
|
292
284
|
return `${heading}\n\n${formatJournalResult(operation.result)}`;
|
|
285
|
+
case "check_blocker":
|
|
286
|
+
return `${heading}\n\n${formatCheckBlockerResult(operation.result)}`;
|
|
293
287
|
case "acknowledge_queue_removal": {
|
|
294
288
|
const result = operation.result;
|
|
295
289
|
return [
|
|
@@ -70,7 +70,7 @@ function detectBlockingBotReview(pr) {
|
|
|
70
70
|
// A blocking bot review is "in progress" when:
|
|
71
71
|
// 1. Any reviewRequest has a login starting with one of the configured prefixes, OR
|
|
72
72
|
// 2. Any latestReview has such a login AND state == "PENDING"
|
|
73
|
-
const prefixes = loadConfig().
|
|
73
|
+
const prefixes = loadConfig()["mark-ready"]["blocking-reviewer-logins"].map((l) => l.toLowerCase());
|
|
74
74
|
const isBlocking = (login) => prefixes.some((p) => login.toLowerCase().startsWith(p));
|
|
75
75
|
const requested = pr.reviewRequests.some((r) => isBlocking(r.login));
|
|
76
76
|
const pendingReview = pr.latestReviews.some((r) => isBlocking(r.login) && r.state === "PENDING");
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare function runWithDurableState<T>(work: () => T): T;
|
|
3
|
-
/** Cloud sessions are always durable; other callers opt in through `runWithDurableState`. */
|
|
1
|
+
/** Cloud sessions keep state (seen markers, stall timers, reply records) beyond a temp dir. */
|
|
4
2
|
export declare function durableStateRequested(): boolean;
|
|
5
3
|
/** `<git-common-dir>/pr-shepherd-state`, or null when not requested or outside a repository. */
|
|
6
4
|
export declare function resolveDurableStateBase(): string | null;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
2
|
import { join, resolve } from "node:path";
|
|
4
3
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
5
4
|
/**
|
|
@@ -7,15 +6,10 @@ import { getEffectiveCwd } from "../execution-context.mjs";
|
|
|
7
6
|
* directory lives with the repository checkout, so state stored there survives whatever the
|
|
8
7
|
* session does with `TMPDIR`. Only a read-only `git rev-parse` is ever invoked.
|
|
9
8
|
*/
|
|
10
|
-
const durableScope = new AsyncLocalStorage();
|
|
11
9
|
const commonDirByCwd = new Map();
|
|
12
|
-
/**
|
|
13
|
-
export function runWithDurableState(work) {
|
|
14
|
-
return durableScope.run(true, work);
|
|
15
|
-
}
|
|
16
|
-
/** Cloud sessions are always durable; other callers opt in through `runWithDurableState`. */
|
|
10
|
+
/** Cloud sessions keep state (seen markers, stall timers, reply records) beyond a temp dir. */
|
|
17
11
|
export function durableStateRequested() {
|
|
18
|
-
return
|
|
12
|
+
return process.env["CLAUDE_CODE_REMOTE"] === "true";
|
|
19
13
|
}
|
|
20
14
|
/** `<git-common-dir>/pr-shepherd-state`, or null when not requested or outside a repository. */
|
|
21
15
|
export function resolveDurableStateBase() {
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Persistent attempt counter for the iterate escalation guard.
|
|
3
3
|
*
|
|
4
4
|
* Tracks how many caller-visible times each review thread has been dispatched to
|
|
5
|
-
* the
|
|
5
|
+
* the triage handler without being resolved. Body edits reset the count.
|
|
6
6
|
*
|
|
7
7
|
* State lives in `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/fix-attempts.json`.
|
|
8
8
|
*/
|
|
9
9
|
export interface FixAttemptsState {
|
|
10
10
|
/** HEAD SHA at the time the counts were last written, retained for observability/compatibility. */
|
|
11
11
|
headSha: string;
|
|
12
|
-
/** Map from thread ID → number of
|
|
12
|
+
/** Map from thread ID → number of triage dispatches that included this thread. */
|
|
13
13
|
threadAttempts: Record<string, number>;
|
|
14
14
|
/** Map from thread ID → body hash used for the associated attempt count. */
|
|
15
15
|
threadBodyHashes?: Record<string, string>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Persistent attempt counter for the iterate escalation guard.
|
|
3
3
|
*
|
|
4
4
|
* Tracks how many caller-visible times each review thread has been dispatched to
|
|
5
|
-
* the
|
|
5
|
+
* the triage handler without being resolved. Body edits reset the count.
|
|
6
6
|
*
|
|
7
7
|
* State lives in `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/fix-attempts.json`.
|
|
8
8
|
*/
|
|
@@ -100,7 +100,7 @@ export async function replaceFailedMergeRequest(key, previous, next) {
|
|
|
100
100
|
throw error;
|
|
101
101
|
persisted = parseRecord(await readFile(`${stem}.json`, "utf8"));
|
|
102
102
|
if (!persisted.replacementToken)
|
|
103
|
-
throw new Error("
|
|
103
|
+
throw new Error("Merge successor has no recovery token; reconcile its outcome");
|
|
104
104
|
if (persisted.options.requireSha !== next.options.requireSha ||
|
|
105
105
|
persisted.options.mergeAction !== next.options.mergeAction ||
|
|
106
106
|
persisted.options.mergeMethod !== next.options.mergeMethod ||
|
|
@@ -5,7 +5,7 @@ import { dirname } from "node:path";
|
|
|
5
5
|
import { resolvePrStatePath } from "./base.mjs";
|
|
6
6
|
import { discoverRuleFiles } from "../classify/loader.mjs";
|
|
7
7
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
8
|
-
const VERSION =
|
|
8
|
+
const VERSION = 5;
|
|
9
9
|
let packageVersion;
|
|
10
10
|
/** Bundled standard rules change with the package, so their digest input is its version. */
|
|
11
11
|
function readPackageVersion() {
|
|
@@ -15,18 +15,11 @@ function readPackageVersion() {
|
|
|
15
15
|
export function fingerprintInputDigest(config) {
|
|
16
16
|
const hash = createHash("sha256");
|
|
17
17
|
hash.update(JSON.stringify({
|
|
18
|
-
|
|
19
|
-
botUsernames: config.botUsernames,
|
|
18
|
+
"bot-usernames": config["bot-usernames"],
|
|
20
19
|
iterate: config.iterate,
|
|
21
|
-
watch: { readyDelayMinutes: config.watch.readyDelayMinutes },
|
|
22
20
|
checks: config.checks,
|
|
23
|
-
|
|
24
|
-
actions:
|
|
25
|
-
autoMinimizeSuppressed: config.actions.autoMinimizeSuppressed,
|
|
26
|
-
autoMarkReady: config.actions.autoMarkReady,
|
|
27
|
-
neverCancelRuns: config.actions.neverCancelRuns,
|
|
28
|
-
workWhileQueued: config.actions.workWhileQueued,
|
|
29
|
-
},
|
|
21
|
+
"mark-ready": config["mark-ready"],
|
|
22
|
+
actions: config.actions,
|
|
30
23
|
standardRules: (config.standardRules?.length ?? 0) > 0
|
|
31
24
|
? { names: config.standardRules, version: readPackageVersion() }
|
|
32
25
|
: undefined,
|
|
@@ -16,8 +16,8 @@ export declare function hashBody(body: string): string;
|
|
|
16
16
|
* - "new" — no marker exists; surface the body and write the marker.
|
|
17
17
|
* - "edited" — marker exists but stored hash differs from the current body;
|
|
18
18
|
* surface the updated body and update the marker hash.
|
|
19
|
-
* - "unchanged" — marker exists and hash matches (or marker has no hash
|
|
20
|
-
* is treated conservatively as unchanged).
|
|
19
|
+
* - "unchanged" — marker exists and hash matches (or marker has no hash and no
|
|
20
|
+
* inline thread IDs, which is treated conservatively as unchanged).
|
|
21
21
|
*/
|
|
22
22
|
export declare function classifyItem(id: string, body: string, map: Map<string, SeenMarker>): "new" | "edited" | "unchanged";
|
|
23
23
|
/**
|
|
@@ -31,9 +31,8 @@ export declare function loadSeenSet(key: StateKey): Promise<Set<string>>;
|
|
|
31
31
|
* Used when the caller needs the stored bodyHash to detect in-place edits.
|
|
32
32
|
* Returns an empty Map if the directory does not yet exist.
|
|
33
33
|
*
|
|
34
|
-
* Map keys are the stored `id` field
|
|
35
|
-
*
|
|
36
|
-
* legacy markers that predate this field.
|
|
34
|
+
* Map keys are the stored `id` field (guarding against case-insensitive
|
|
35
|
+
* filesystem collisions); markers without one are skipped.
|
|
37
36
|
*/
|
|
38
37
|
export declare function loadSeenMap(key: StateKey): Promise<Map<string, SeenMarker>>;
|
|
39
38
|
/** Return true if a "seen" marker exists for this id. */
|
|
@@ -17,8 +17,8 @@ export function hashBody(body) {
|
|
|
17
17
|
* - "new" — no marker exists; surface the body and write the marker.
|
|
18
18
|
* - "edited" — marker exists but stored hash differs from the current body;
|
|
19
19
|
* surface the updated body and update the marker hash.
|
|
20
|
-
* - "unchanged" — marker exists and hash matches (or marker has no hash
|
|
21
|
-
* is treated conservatively as unchanged).
|
|
20
|
+
* - "unchanged" — marker exists and hash matches (or marker has no hash and no
|
|
21
|
+
* inline thread IDs, which is treated conservatively as unchanged).
|
|
22
22
|
*/
|
|
23
23
|
export function classifyItem(id, body, map) {
|
|
24
24
|
const m = map.get(id);
|
|
@@ -48,9 +48,8 @@ export async function loadSeenSet(key) {
|
|
|
48
48
|
* Used when the caller needs the stored bodyHash to detect in-place edits.
|
|
49
49
|
* Returns an empty Map if the directory does not yet exist.
|
|
50
50
|
*
|
|
51
|
-
* Map keys are the stored `id` field
|
|
52
|
-
*
|
|
53
|
-
* legacy markers that predate this field.
|
|
51
|
+
* Map keys are the stored `id` field (guarding against case-insensitive
|
|
52
|
+
* filesystem collisions); markers without one are skipped.
|
|
54
53
|
*/
|
|
55
54
|
export async function loadSeenMap(key) {
|
|
56
55
|
const map = new Map();
|
|
@@ -63,14 +62,8 @@ export async function loadSeenMap(key) {
|
|
|
63
62
|
try {
|
|
64
63
|
const raw = await readFile(join(dir, entry), "utf8");
|
|
65
64
|
const marker = JSON.parse(raw);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Hash-based markers (those with an id field) take priority over legacy
|
|
69
|
-
// filename-based markers so that a stale legacy file cannot overwrite a
|
|
70
|
-
// newer hash-based entry that maps to the same key.
|
|
71
|
-
if (!map.has(mapKey) || typeof marker.id === "string") {
|
|
72
|
-
map.set(mapKey, marker);
|
|
73
|
-
}
|
|
65
|
+
if (typeof marker.id === "string")
|
|
66
|
+
map.set(marker.id, marker);
|
|
74
67
|
}
|
|
75
68
|
catch {
|
|
76
69
|
// unreadable or malformed — skip
|
|
@@ -155,7 +148,8 @@ async function writeSeenMarker(key, id, markerFields) {
|
|
|
155
148
|
// no existing marker — will create below
|
|
156
149
|
}
|
|
157
150
|
const unchanged = Object.entries(markerFields).every(([field, value]) => markerFieldEqual(existing?.[field], value));
|
|
158
|
-
|
|
151
|
+
// A marker without its `id` is ignored by loadSeenMap, so rewrite it even when unchanged.
|
|
152
|
+
if (existing !== null && unchanged && existing.id === id)
|
|
159
153
|
return;
|
|
160
154
|
const seenAt = existing?.seenAt ?? Date.now();
|
|
161
155
|
tmp = `${path}.${randomUUID()}.tmp`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persistent stall-detection state for an aggregate
|
|
2
|
+
* Persistent stall-detection state for an aggregate `stack` selection whose open layers can
|
|
3
3
|
* only wait. Keyed by native stack number because the anchor PR can merge or change.
|
|
4
4
|
*
|
|
5
5
|
* State lives in `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/stack-<number>/stack-stall.json`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persistent stall-detection state for an aggregate
|
|
2
|
+
* Persistent stall-detection state for an aggregate `stack` selection whose open layers can
|
|
3
3
|
* only wait. Keyed by native stack number because the anchor PR can merge or change.
|
|
4
4
|
*
|
|
5
5
|
* State lives in `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/stack-<number>/stack-stall.json`.
|
|
@@ -27,7 +27,7 @@ export interface StallStateStore<Key> {
|
|
|
27
27
|
clear(key: Key): Promise<void>;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* `{fingerprint, firstSeenAt}` files behind the one-PR and
|
|
30
|
+
* `{fingerprint, firstSeenAt}` files behind the one-PR and `stack` stall guards.
|
|
31
31
|
* A missing file is a miss. An unwritable directory or an unsafe key is a persistence failure
|
|
32
32
|
* so the caller can hand off instead of treating every tick as the first sighting.
|
|
33
33
|
* `ENOENT` is the only read error treated as a miss. Corrupt JSON is also a miss, so one
|
|
@@ -2,7 +2,7 @@ import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
/**
|
|
5
|
-
* `{fingerprint, firstSeenAt}` files behind the one-PR and
|
|
5
|
+
* `{fingerprint, firstSeenAt}` files behind the one-PR and `stack` stall guards.
|
|
6
6
|
* A missing file is a miss. An unwritable directory or an unsafe key is a persistence failure
|
|
7
7
|
* so the caller can hand off instead of treating every tick as the first sighting.
|
|
8
8
|
* `ENOENT` is the only read error treated as a miss. Corrupt JSON is also a miss, so one
|
package/bin/types/escalate.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentCheck, AgentComment, AgentThread } from "./report.mts";
|
|
|
2
2
|
import type { ResolveCommand } from "./iterate.mts";
|
|
3
3
|
import type { CheckStatus, Review } from "./github.mts";
|
|
4
4
|
import type { MergeQueueRemovalStatus } from "./merge-requirements.mts";
|
|
5
|
-
export type EscalateTrigger = "transport-unsupported" | "
|
|
5
|
+
export type EscalateTrigger = "transport-unsupported" | "triage-thrash" | "base-branch-unknown" | "stall-timeout" | "required-checks-unreported" | "stall-state-unavailable" | "check-follow-up-unavailable" | "authorization-required" | "merge-queue-removed" | "merge-method-unavailable";
|
|
6
6
|
export interface AgentStalledCheck {
|
|
7
7
|
name: string;
|
|
8
8
|
status: CheckStatus;
|
|
@@ -32,7 +32,7 @@ export interface EscalateDetails {
|
|
|
32
32
|
attempts: number;
|
|
33
33
|
}>;
|
|
34
34
|
/**
|
|
35
|
-
* Set only when `iterate.
|
|
35
|
+
* Set only when `iterate.review-output-max-chars` held review items back on the tick that
|
|
36
36
|
* escalated: `shown` of `total` review items appear here. The held items stay unmarked, so the
|
|
37
37
|
* next run shows them.
|
|
38
38
|
*/
|
package/bin/types/github.d.mts
CHANGED
|
@@ -60,6 +60,8 @@ export interface CheckRun {
|
|
|
60
60
|
workflowId?: string;
|
|
61
61
|
/** True when GraphQL reported at least one CheckRun annotation. Omitted when false. */
|
|
62
62
|
hasAnnotations?: boolean;
|
|
63
|
+
/** Annotation total GraphQL reported for this CheckRun. Internal: validates cached lists. */
|
|
64
|
+
annotationCount?: number;
|
|
63
65
|
/**
|
|
64
66
|
* Every annotation on this run, when BatchPr's `annotations(first: 1)` page already held all
|
|
65
67
|
* of them. Internal: lets the check path skip `CheckRunAnnotationsBatch`; never rendered.
|