pr-shepherd 0.34.0 → 0.36.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 +80 -43
- 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/checks/triage.mjs +11 -3
- 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/args.mjs +1 -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 +1 -0
- package/bin/cli/duration-flag.d.mts +2 -0
- package/bin/cli/duration.d.mts +13 -0
- 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 +11 -10
- package/bin/cli/help-command-pages.d.mts +231 -0
- package/bin/cli/help-command-pages.mjs +106 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +4 -0
- package/bin/cli/help-iterate-poll-pages.mjs +14 -7
- 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 +16 -16
- 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-flags.d.mts +11 -0
- package/bin/cli/iterate-formatter.d.mts +19 -0
- package/bin/cli/iterate-formatter.mjs +11 -2
- package/bin/cli/iterate-instructions.d.mts +6 -0
- package/bin/cli/iterate-lean.d.mts +12 -0
- package/bin/cli/iterate-lean.mjs +1 -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 +7 -20
- 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 +9 -2
- package/bin/cli/resolve-validators.d.mts +3 -0
- package/bin/cli/resolve-validators.mjs +3 -3
- 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 +72 -16
- 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 +7 -0
- package/bin/commands/check.mjs +28 -23
- 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 +20 -13
- 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/fix-code.mjs +1 -1
- package/bin/commands/iterate/helpers.d.mts +14 -0
- package/bin/commands/iterate/helpers.mjs +19 -7
- package/bin/commands/iterate/index.d.mts +2 -0
- package/bin/commands/iterate/index.mjs +8 -12
- 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 +1 -0
- package/bin/commands/poll.d.mts +12 -0
- package/bin/commands/poll.mjs +52 -17
- package/bin/commands/ready-delay.d.mts +29 -0
- package/bin/commands/ready-delay.mjs +3 -13
- package/bin/commands/ready-mergeability.d.mts +15 -0
- package/bin/commands/resolve-mutate.d.mts +4 -0
- package/bin/commands/resolve-mutate.mjs +1 -0
- 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 +129 -16
- 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/github/activity.d.mts +3 -0
- package/bin/github/activity.mjs +7 -0
- package/bin/github/batch-page-helpers.d.mts +45 -0
- package/bin/github/batch-page-helpers.mjs +63 -0
- package/bin/github/batch-page.d.mts +14 -0
- package/bin/github/batch-page.mjs +62 -0
- package/bin/github/batch-parse-suites.d.mts +4 -0
- package/bin/github/batch-parse-suites.mjs +25 -0
- package/bin/github/batch-parser-helpers.d.mts +22 -0
- package/bin/github/batch-parsers-rules.d.mts +6 -0
- package/bin/github/batch-parsers-rules.mjs +122 -0
- package/bin/github/batch-parsers.d.mts +3 -0
- package/bin/github/batch-parsers.mjs +12 -0
- package/bin/github/batch-raw-rules.d.mts +59 -0
- package/bin/github/batch-raw-rules.mjs +1 -0
- package/bin/github/batch-raw-types.d.mts +216 -0
- package/bin/github/batch-response.d.mts +4 -0
- package/bin/github/batch.d.mts +24 -0
- package/bin/github/batch.mjs +14 -120
- 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/gql/batch-pr-page.gql +189 -0
- package/bin/github/gql/batch-pr.gql +79 -21
- package/bin/github/gql/commit-suggestion-thread.gql +40 -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/http-auth.d.mts +4 -0
- package/bin/github/http-request.d.mts +7 -0
- package/bin/github/http-utils.d.mts +11 -0
- package/bin/github/http.d.mts +6 -0
- package/bin/github/http.mjs +2 -1
- package/bin/github/pagination.d.mts +46 -0
- package/bin/github/pagination.mjs +3 -2
- package/bin/github/queries.d.mts +28 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/github/rest-http.d.mts +7 -0
- package/bin/github/rest-http.mjs +25 -86
- package/bin/github/rest-text.d.mts +1 -0
- package/bin/github/rest-text.mjs +88 -0
- package/bin/github/suggestion-thread.d.mts +9 -0
- package/bin/github/suggestion-thread.mjs +45 -0
- package/bin/github/thread-comments.d.mts +2 -0
- package/bin/github/thread-comments.mjs +12 -8
- package/bin/index.d.mts +10 -0
- package/bin/index.mjs +1 -1
- 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/merge-status/derive.mjs +2 -0
- package/bin/merge-status/requirements-format.d.mts +3 -0
- package/bin/merge-status/requirements-format.mjs +88 -0
- package/bin/merge-status/requirements.d.mts +2 -0
- package/bin/merge-status/requirements.mjs +51 -0
- package/bin/reporters/agent.d.mts +23 -0
- package/bin/reporters/agent.mjs +3 -0
- package/bin/state/base.d.mts +10 -0
- package/bin/state/base.mjs +23 -0
- package/bin/state/bot-cr-seen.d.mts +51 -0
- package/bin/state/bot-cr-seen.mjs +4 -14
- package/bin/state/fix-attempts.d.mts +27 -0
- package/bin/state/fix-attempts.mjs +3 -13
- package/bin/state/iterate-stall.d.mts +27 -0
- package/bin/state/iterate-stall.mjs +3 -13
- package/bin/state/seen-comments.d.mts +62 -0
- package/bin/state/seen-comments.mjs +6 -13
- 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 +136 -0
- package/bin/types/iterate.d.mts +156 -0
- package/bin/types/merge-requirements.d.mts +82 -0
- package/bin/types/merge-requirements.mjs +2 -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 +10 -0
- package/bin/types.mjs +1 -0
- package/bin/util/markdown.d.mts +1 -0
- package/bin/util/path-segment.d.mts +4 -0
- package/bin/util/path-segment.mjs +2 -0
- package/bin/util/pool.d.mts +2 -0
- package/bin/util/pool.mjs +18 -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,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;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 9. mergeStateStatus CLEAN → CLEAN
|
|
17
17
|
*/
|
|
18
18
|
import { loadConfig } from "../config/load.mjs";
|
|
19
|
+
import { deriveMergeRequirements } from "./requirements.mjs";
|
|
19
20
|
export function deriveMergeStatus(pr) {
|
|
20
21
|
const blockingBotReviewInProgress = detectBlockingBotReview(pr);
|
|
21
22
|
let status;
|
|
@@ -55,6 +56,7 @@ export function deriveMergeStatus(pr) {
|
|
|
55
56
|
reviewDecision: pr.reviewDecision,
|
|
56
57
|
blockingBotReviewInProgress,
|
|
57
58
|
mergeStateStatus: pr.mergeStateStatus,
|
|
59
|
+
mergeRequirements: deriveMergeRequirements(pr),
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const REQUIRED = "[Required]";
|
|
2
|
+
const NOT_REQUIRED = "[Not Required]";
|
|
3
|
+
export function formatMergeRequirementLines(req) {
|
|
4
|
+
const lines = [
|
|
5
|
+
formatApprovals(req.approvals.current, req.approvals.requiredCount),
|
|
6
|
+
formatConversations(req.conversationsResolved),
|
|
7
|
+
];
|
|
8
|
+
if (req.codeOwnerReview)
|
|
9
|
+
lines.push(`Code owner review: ${REQUIRED}`);
|
|
10
|
+
if (req.lastPushApproval)
|
|
11
|
+
lines.push(`Last-push approval: ${REQUIRED}`);
|
|
12
|
+
if (req.signedCommits)
|
|
13
|
+
lines.push(`Signed commits: ${REQUIRED}`);
|
|
14
|
+
if (req.linearHistory)
|
|
15
|
+
lines.push(`Linear history: ${REQUIRED}`);
|
|
16
|
+
if (req.branchUpToDate) {
|
|
17
|
+
const value = req.branchUpToDate.current ? "Yes" : "No";
|
|
18
|
+
lines.push(`Branch up to date: ${value} ${REQUIRED}`);
|
|
19
|
+
}
|
|
20
|
+
if (req.requiredStatusChecks) {
|
|
21
|
+
const n = req.requiredStatusChecks.contexts.length;
|
|
22
|
+
lines.push(`Required status checks: ${n} ${REQUIRED}`);
|
|
23
|
+
}
|
|
24
|
+
if (req.requiredDeployments) {
|
|
25
|
+
const envs = req.requiredDeployments.environments.join(", ");
|
|
26
|
+
lines.push(`Required deployments: ${envs} ${REQUIRED}`);
|
|
27
|
+
}
|
|
28
|
+
if (req.requiredWorkflows)
|
|
29
|
+
lines.push(`Required workflows: ${REQUIRED}`);
|
|
30
|
+
if (req.codeScanning)
|
|
31
|
+
lines.push(`Code scanning: ${REQUIRED}`);
|
|
32
|
+
if (req.mergeQueue)
|
|
33
|
+
lines.push(formatMergeQueue(req.mergeQueue));
|
|
34
|
+
if (req.stack) {
|
|
35
|
+
const { number, position, size, baseRefName } = req.stack;
|
|
36
|
+
lines.push(`Stack: #${number} ${position}/${size} (base ${baseRefName})`);
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
export function blockedReasonFromRequirements(req) {
|
|
41
|
+
if (!req)
|
|
42
|
+
return null;
|
|
43
|
+
const unmet = [];
|
|
44
|
+
if (req.approvals.requiredCount > 0 && req.approvals.current < req.approvals.requiredCount) {
|
|
45
|
+
const need = req.approvals.requiredCount - req.approvals.current;
|
|
46
|
+
unmet.push(`awaiting ${need} approval${need === 1 ? "" : "s"}`);
|
|
47
|
+
}
|
|
48
|
+
if (req.conversationsResolved.required && !req.conversationsResolved.resolved) {
|
|
49
|
+
unmet.push("unresolved conversations are required");
|
|
50
|
+
}
|
|
51
|
+
if (req.branchUpToDate && !req.branchUpToDate.current) {
|
|
52
|
+
unmet.push("branch is behind base");
|
|
53
|
+
}
|
|
54
|
+
if (req.mergeQueue?.inQueue) {
|
|
55
|
+
const pos = req.mergeQueue.position != null ? ` position ${req.mergeQueue.position}` : "";
|
|
56
|
+
unmet.push(`in merge queue${pos}`);
|
|
57
|
+
}
|
|
58
|
+
else if (req.mergeQueue?.required) {
|
|
59
|
+
unmet.push("merge queue is required");
|
|
60
|
+
}
|
|
61
|
+
return unmet.length > 0 ? unmet.join("; ") : null;
|
|
62
|
+
}
|
|
63
|
+
function formatApprovals(current, requiredCount) {
|
|
64
|
+
const tag = requiredCount > 0 ? REQUIRED : NOT_REQUIRED;
|
|
65
|
+
if (current === 0 && requiredCount === 0)
|
|
66
|
+
return `Approvals: None ${NOT_REQUIRED}`;
|
|
67
|
+
if (current === 0)
|
|
68
|
+
return `Approvals: None ${REQUIRED}`;
|
|
69
|
+
if (requiredCount > 0)
|
|
70
|
+
return `Approvals: ${current}/${requiredCount} ${tag}`;
|
|
71
|
+
return `Approvals: ${current} ${NOT_REQUIRED}`;
|
|
72
|
+
}
|
|
73
|
+
function formatConversations(c) {
|
|
74
|
+
const value = c.resolved ? "Yes" : "No";
|
|
75
|
+
const tag = c.required ? REQUIRED : NOT_REQUIRED;
|
|
76
|
+
return `Conversations Resolved: ${value} ${tag}`;
|
|
77
|
+
}
|
|
78
|
+
function formatMergeQueue(q) {
|
|
79
|
+
const tag = q.required ? REQUIRED : NOT_REQUIRED;
|
|
80
|
+
if (q.inQueue) {
|
|
81
|
+
const pos = q.position != null ? `position ${q.position}` : "Yes";
|
|
82
|
+
const state = q.state ? ` ${q.state}` : "";
|
|
83
|
+
return `Merge queue: ${pos}${state} ${tag}`;
|
|
84
|
+
}
|
|
85
|
+
if (q.required || q.enabled)
|
|
86
|
+
return `Merge queue: No ${tag}`;
|
|
87
|
+
return `Merge queue: No ${NOT_REQUIRED}`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EMPTY_BRANCH_RULES } from "../github/batch-parsers-rules.mjs";
|
|
2
|
+
export function deriveMergeRequirements(pr) {
|
|
3
|
+
const rules = pr.branchRules ?? EMPTY_BRANCH_RULES;
|
|
4
|
+
const currentApprovals = pr.latestReviews.filter((r) => r.state === "APPROVED").length;
|
|
5
|
+
const unresolvedCount = pr.reviewThreads.filter((t) => !t.isResolved).length;
|
|
6
|
+
const req = {
|
|
7
|
+
approvals: {
|
|
8
|
+
current: currentApprovals,
|
|
9
|
+
requiredCount: rules.requiredApprovingReviewCount,
|
|
10
|
+
},
|
|
11
|
+
conversationsResolved: {
|
|
12
|
+
resolved: unresolvedCount === 0,
|
|
13
|
+
unresolvedCount,
|
|
14
|
+
required: rules.requiresConversationResolution,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
if (rules.requiresCodeOwnerReviews)
|
|
18
|
+
req.codeOwnerReview = { required: true };
|
|
19
|
+
if (rules.requiresLastPushApproval)
|
|
20
|
+
req.lastPushApproval = { required: true };
|
|
21
|
+
if (rules.requiresCommitSignatures)
|
|
22
|
+
req.signedCommits = { required: true };
|
|
23
|
+
if (rules.requiresLinearHistory)
|
|
24
|
+
req.linearHistory = { required: true };
|
|
25
|
+
if (rules.requiresStrictStatusChecks) {
|
|
26
|
+
req.branchUpToDate = { current: pr.mergeStateStatus !== "BEHIND", required: true };
|
|
27
|
+
}
|
|
28
|
+
if (rules.requiredStatusCheckContexts.length > 0) {
|
|
29
|
+
req.requiredStatusChecks = { contexts: rules.requiredStatusCheckContexts };
|
|
30
|
+
}
|
|
31
|
+
if (rules.requiredDeploymentEnvironments.length > 0) {
|
|
32
|
+
req.requiredDeployments = { environments: rules.requiredDeploymentEnvironments };
|
|
33
|
+
}
|
|
34
|
+
if (rules.requiresWorkflows)
|
|
35
|
+
req.requiredWorkflows = { required: true };
|
|
36
|
+
if (rules.requiresCodeScanning)
|
|
37
|
+
req.codeScanning = { required: true };
|
|
38
|
+
const queueRequired = rules.requiresMergeQueue || Boolean(pr.isMergeQueueEnabled);
|
|
39
|
+
if (queueRequired || pr.isInMergeQueue) {
|
|
40
|
+
req.mergeQueue = {
|
|
41
|
+
required: queueRequired,
|
|
42
|
+
enabled: Boolean(pr.isMergeQueueEnabled),
|
|
43
|
+
inQueue: Boolean(pr.isInMergeQueue),
|
|
44
|
+
...(pr.mergeQueueEntry?.position !== undefined && { position: pr.mergeQueueEntry.position }),
|
|
45
|
+
...(pr.mergeQueueEntry?.state !== undefined && { state: pr.mergeQueueEntry.state }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (pr.stack)
|
|
49
|
+
req.stack = pr.stack;
|
|
50
|
+
return req;
|
|
51
|
+
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
export declare function resolveStateBase(): string;
|
|
2
|
+
/**
|
|
3
|
+
* `$PR_SHEPHERD_STATE_DIR/<owner>-<repo>/<pr>/...parts`.
|
|
4
|
+
* Owner, repo, PR number, and each extra part must be a safe path segment.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolvePrStatePath(key: {
|
|
7
|
+
owner: string;
|
|
8
|
+
repo: string;
|
|
9
|
+
pr: number;
|
|
10
|
+
}, ...parts: string[]): string;
|
package/bin/state/base.mjs
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
+
import { SAFE_PR_NUMBER, SAFE_SEGMENT } from "../util/path-segment.mjs";
|
|
3
4
|
export function resolveStateBase() {
|
|
4
5
|
const envDir = process.env["PR_SHEPHERD_STATE_DIR"];
|
|
5
6
|
return envDir ? envDir : join(tmpdir(), "pr-shepherd-state");
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* `$PR_SHEPHERD_STATE_DIR/<owner>-<repo>/<pr>/...parts`.
|
|
10
|
+
* Owner, repo, PR number, and each extra part must be a safe path segment.
|
|
11
|
+
*/
|
|
12
|
+
export function resolvePrStatePath(key, ...parts) {
|
|
13
|
+
if (!SAFE_SEGMENT.test(key.owner)) {
|
|
14
|
+
throw new Error(`Invalid state key segment "owner": ${key.owner}`);
|
|
15
|
+
}
|
|
16
|
+
if (!SAFE_SEGMENT.test(key.repo)) {
|
|
17
|
+
throw new Error(`Invalid state key segment "repo": ${key.repo}`);
|
|
18
|
+
}
|
|
19
|
+
const pr = String(key.pr);
|
|
20
|
+
if (!SAFE_PR_NUMBER.test(pr)) {
|
|
21
|
+
throw new Error(`Invalid state key segment "pr": ${key.pr}`);
|
|
22
|
+
}
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (!SAFE_SEGMENT.test(part)) {
|
|
25
|
+
throw new Error(`Invalid state key segment: ${part}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return join(resolveStateBase(), `${key.owner}-${key.repo}`, pr, ...parts);
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent first-seen state for bot CHANGES_REQUESTED reviews.
|
|
3
|
+
*
|
|
4
|
+
* Bot CRs are auto-dismissed via `--dismiss-review-ids` in the post-push
|
|
5
|
+
* `apply review:` command. If the agent drops that flag, the bot CR keeps the PR in
|
|
6
|
+
* `CHANGES_REQUESTED` state. This file tracks when each bot CR was first
|
|
7
|
+
* observed so the iterate loop can escalate after `iterate.stallTimeoutMinutes`
|
|
8
|
+
* — independent of the broader fingerprint-based `stall-timeout` mechanism in
|
|
9
|
+
* `iterate-stall.mts` (which only fires when no field of the iterate result
|
|
10
|
+
* changed).
|
|
11
|
+
*
|
|
12
|
+
* State lives in
|
|
13
|
+
* `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/bot-cr-seen.json`.
|
|
14
|
+
*
|
|
15
|
+
* `bodyHash` lets us reset `firstSeenAt` when the bot re-issues the review
|
|
16
|
+
* with a different body, so a fresh review gets the full timeout window.
|
|
17
|
+
*/
|
|
18
|
+
interface BotCrSeenEntry {
|
|
19
|
+
/** Unix timestamp (seconds) when this review was first observed undismissed. */
|
|
20
|
+
firstSeenAt: number;
|
|
21
|
+
/** SHA-256 body-hash prefix; reset firstSeenAt when this changes. */
|
|
22
|
+
bodyHash: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BotCrSeenState {
|
|
25
|
+
reviews: Record<string, BotCrSeenEntry>;
|
|
26
|
+
}
|
|
27
|
+
interface StateKey {
|
|
28
|
+
owner: string;
|
|
29
|
+
repo: string;
|
|
30
|
+
pr: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function readBotCrSeenState(key: StateKey): Promise<BotCrSeenState | null>;
|
|
33
|
+
export declare function writeBotCrSeenState(key: StateKey, state: BotCrSeenState): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Update tracked entries against the current set of bot CR reviews:
|
|
36
|
+
* - Insert any new IDs with `firstSeenAt = now` and the current body hash.
|
|
37
|
+
* - Reset `firstSeenAt` for entries whose body hash changed (review re-issued).
|
|
38
|
+
* - Drop entries whose IDs are no longer in the input (review was dismissed
|
|
39
|
+
* or superseded by an approval).
|
|
40
|
+
*
|
|
41
|
+
* Pure function: returns the next state and the IDs whose age has reached the
|
|
42
|
+
* `stallTimeoutSeconds` threshold (escalate candidates).
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateBotCrSeenState(previous: BotCrSeenState | null, currentBotCrReviews: ReadonlyArray<{
|
|
45
|
+
id: string;
|
|
46
|
+
body: string;
|
|
47
|
+
}>, nowSeconds: number, stallTimeoutSeconds: number): {
|
|
48
|
+
next: BotCrSeenState;
|
|
49
|
+
staleIds: string[];
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Persistent first-seen state for bot CHANGES_REQUESTED reviews.
|
|
3
3
|
*
|
|
4
4
|
* Bot CRs are auto-dismissed via `--dismiss-review-ids` in the post-push
|
|
5
|
-
* `
|
|
5
|
+
* `apply review:` command. If the agent drops that flag, the bot CR keeps the PR in
|
|
6
6
|
* `CHANGES_REQUESTED` state. This file tracks when each bot CR was first
|
|
7
7
|
* observed so the iterate loop can escalate after `iterate.stallTimeoutMinutes`
|
|
8
8
|
* — independent of the broader fingerprint-based `stall-timeout` mechanism in
|
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { resolveStateBase } from "./base.mjs";
|
|
20
|
+
import { dirname } from "node:path";
|
|
21
|
+
import { resolvePrStatePath } from "./base.mjs";
|
|
23
22
|
import { hashBody } from "./seen-comments.mjs";
|
|
24
23
|
export async function readBotCrSeenState(key) {
|
|
25
24
|
try {
|
|
@@ -97,14 +96,5 @@ export function updateBotCrSeenState(previous, currentBotCrReviews, nowSeconds,
|
|
|
97
96
|
return { next: { reviews: next }, staleIds };
|
|
98
97
|
}
|
|
99
98
|
function resolvePath(key) {
|
|
100
|
-
|
|
101
|
-
["owner", key.owner],
|
|
102
|
-
["repo", key.repo],
|
|
103
|
-
]) {
|
|
104
|
-
if (!SAFE_SEGMENT.test(value)) {
|
|
105
|
-
throw new Error(`Invalid state key segment "${field}": ${value}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const base = resolveStateBase();
|
|
109
|
-
return join(base, `${key.owner}-${key.repo}`, String(key.pr), "bot-cr-seen.json");
|
|
99
|
+
return resolvePrStatePath(key, "bot-cr-seen.json");
|
|
110
100
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent attempt counter for the iterate escalation guard.
|
|
3
|
+
*
|
|
4
|
+
* Tracks how many times each review thread has been dispatched to the fix_code
|
|
5
|
+
* handler without being resolved. Counts are reset automatically when the HEAD
|
|
6
|
+
* commit SHA changes (i.e. a new push landed).
|
|
7
|
+
*
|
|
8
|
+
* State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
|
|
9
|
+
*/
|
|
10
|
+
export interface FixAttemptsState {
|
|
11
|
+
/** HEAD SHA at the time the counts were last written. Reset key. */
|
|
12
|
+
headSha: string;
|
|
13
|
+
/** Map from thread ID → number of fix_code dispatches that included this thread. */
|
|
14
|
+
threadAttempts: Record<string, number>;
|
|
15
|
+
/** Map from thread ID → body hash used for the associated attempt count. */
|
|
16
|
+
threadBodyHashes?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
interface StateKey {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
pr: number;
|
|
22
|
+
}
|
|
23
|
+
/** Read the current attempt state. Returns null on miss. */
|
|
24
|
+
export declare function readFixAttempts(key: StateKey): Promise<FixAttemptsState | null>;
|
|
25
|
+
/** Write attempt state (fire-and-forget — never throws). */
|
|
26
|
+
export declare function writeFixAttempts(key: StateKey, state: FixAttemptsState): Promise<void>;
|
|
27
|
+
export {};
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { resolveStateBase } from "./base.mjs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { resolvePrStatePath } from "./base.mjs";
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
16
15
|
// Public API
|
|
17
16
|
// ---------------------------------------------------------------------------
|
|
@@ -54,14 +53,5 @@ export async function writeFixAttempts(key, state) {
|
|
|
54
53
|
// Helpers
|
|
55
54
|
// ---------------------------------------------------------------------------
|
|
56
55
|
function resolvePath(key) {
|
|
57
|
-
|
|
58
|
-
["owner", key.owner],
|
|
59
|
-
["repo", key.repo],
|
|
60
|
-
]) {
|
|
61
|
-
if (!SAFE_SEGMENT.test(value)) {
|
|
62
|
-
throw new Error(`Invalid state key segment "${field}": ${value}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const base = resolveStateBase();
|
|
66
|
-
return join(base, `${key.owner}-${key.repo}`, String(key.pr), "fix-attempts.json");
|
|
56
|
+
return resolvePrStatePath(key, "fix-attempts.json");
|
|
67
57
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent stall-detection state for the iterate loop.
|
|
3
|
+
*
|
|
4
|
+
* Tracks the fingerprint of the last iterate result and when that fingerprint
|
|
5
|
+
* was first seen. If the fingerprint does not change for stallTimeoutSeconds
|
|
6
|
+
* the iterate command escalates instead of repeating the same action.
|
|
7
|
+
*
|
|
8
|
+
* State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/iterate-stall.json`.
|
|
9
|
+
*/
|
|
10
|
+
interface StallState {
|
|
11
|
+
/** Canonicalized JSON fingerprint of the material iterate inputs. */
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
/** Unix timestamp (seconds) when this fingerprint was first seen. */
|
|
14
|
+
firstSeenAt: number;
|
|
15
|
+
}
|
|
16
|
+
interface StateKey {
|
|
17
|
+
owner: string;
|
|
18
|
+
repo: string;
|
|
19
|
+
pr: number;
|
|
20
|
+
}
|
|
21
|
+
/** Read the current stall state. Returns null on miss, corrupt data, or invalid shape. */
|
|
22
|
+
export declare function readStallState(key: StateKey): Promise<StallState | null>;
|
|
23
|
+
/** Clear stall state so the next invocation starts a fresh timer (fire-and-forget — never throws). */
|
|
24
|
+
export declare function clearStallState(key: StateKey): Promise<void>;
|
|
25
|
+
/** Write stall state (fire-and-forget — never throws). */
|
|
26
|
+
export declare function writeStallState(key: StateKey, state: StallState): Promise<void>;
|
|
27
|
+
export {};
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { resolveStateBase } from "./base.mjs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { resolvePrStatePath } from "./base.mjs";
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
16
15
|
// Public API
|
|
17
16
|
// ---------------------------------------------------------------------------
|
|
@@ -70,14 +69,5 @@ export async function writeStallState(key, state) {
|
|
|
70
69
|
// Helpers
|
|
71
70
|
// ---------------------------------------------------------------------------
|
|
72
71
|
function resolvePath(key) {
|
|
73
|
-
|
|
74
|
-
["owner", key.owner],
|
|
75
|
-
["repo", key.repo],
|
|
76
|
-
]) {
|
|
77
|
-
if (!SAFE_SEGMENT.test(value)) {
|
|
78
|
-
throw new Error(`Invalid state key segment "${field}": ${value}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const base = resolveStateBase();
|
|
82
|
-
return join(base, `${key.owner}-${key.repo}`, String(key.pr), "iterate-stall.json");
|
|
72
|
+
return resolvePrStatePath(key, "iterate-stall.json");
|
|
83
73
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface SeenMarker {
|
|
2
|
+
seenAt: number;
|
|
3
|
+
id?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface StateKey {
|
|
7
|
+
owner: string;
|
|
8
|
+
repo: string;
|
|
9
|
+
pr: number;
|
|
10
|
+
}
|
|
11
|
+
/** Compute a 16-hex-char SHA-256 prefix of a comment body. */
|
|
12
|
+
export declare function hashBody(body: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Classify a candidate item against the seen map.
|
|
15
|
+
*
|
|
16
|
+
* - "new" — no marker exists; surface the body and write the marker.
|
|
17
|
+
* - "edited" — marker exists but stored hash differs from the current body;
|
|
18
|
+
* surface the updated body and update the marker hash.
|
|
19
|
+
* - "unchanged" — marker exists and hash matches (or marker has no hash, which
|
|
20
|
+
* is treated conservatively as unchanged).
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyItem(id: string, body: string, map: Map<string, SeenMarker>): "new" | "edited" | "unchanged";
|
|
23
|
+
/**
|
|
24
|
+
* Read the seen/ directory once and return a Set of already-seen IDs.
|
|
25
|
+
* Prefer this over repeated hasSeen() calls to avoid EMFILE on large PRs.
|
|
26
|
+
* Returns an empty Set if the directory does not yet exist.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadSeenSet(key: StateKey): Promise<Set<string>>;
|
|
29
|
+
/**
|
|
30
|
+
* Read the seen/ directory and return a Map from ID to SeenMarker.
|
|
31
|
+
* Used when the caller needs the stored bodyHash to detect in-place edits.
|
|
32
|
+
* Returns an empty Map if the directory does not yet exist.
|
|
33
|
+
*
|
|
34
|
+
* Map keys are the stored `id` field when present (guarding against
|
|
35
|
+
* case-insensitive filesystem collisions), falling back to the filename for
|
|
36
|
+
* legacy markers that predate this field.
|
|
37
|
+
*/
|
|
38
|
+
export declare function loadSeenMap(key: StateKey): Promise<Map<string, SeenMarker>>;
|
|
39
|
+
/** Return true if a "seen" marker exists for this id. */
|
|
40
|
+
export declare function hasSeen(key: StateKey, id: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Write (or update) a "seen" marker for this id, storing the body hash so
|
|
43
|
+
* in-place edits can be detected on future fetches.
|
|
44
|
+
*
|
|
45
|
+
* - First call (no existing marker): creates `{ seenAt: now, bodyHash, id }`.
|
|
46
|
+
* - Subsequent call, hash unchanged: no-op (skips the write).
|
|
47
|
+
* - Subsequent call, hash changed: updates `bodyHash`, preserves original `seenAt`.
|
|
48
|
+
*
|
|
49
|
+
* All errors are silently swallowed — the marker is best-effort.
|
|
50
|
+
*/
|
|
51
|
+
export declare function markSeen(key: StateKey, id: string, body: string): Promise<void>;
|
|
52
|
+
export declare function markReviewInlineThreads(key: StateKey, reviewId: string, inlineThreadIds: readonly string[]): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Write a marker after Shepherd successfully replies to a review thread.
|
|
55
|
+
*
|
|
56
|
+
* `previousBody` suppresses stale GitHub fetches that have not yet included the new reply.
|
|
57
|
+
* `body` suppresses the expected final transcript once GitHub includes Shepherd's reply.
|
|
58
|
+
*/
|
|
59
|
+
export declare function markReplySeen(key: StateKey, id: string, previousBody: string, body: string, replyBody: string): Promise<void>;
|
|
60
|
+
/** Read the full marker for inspection (returns null on miss or error). */
|
|
61
|
+
export declare function readSeenMarker(key: StateKey, id: string): Promise<SeenMarker | null>;
|
|
62
|
+
export {};
|
|
@@ -3,7 +3,7 @@ import { readFile, writeFile, rename, unlink, mkdir, access, readdir } from "nod
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { SAFE_SEGMENT } from "../util/path-segment.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { resolvePrStatePath } from "./base.mjs";
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Public API
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -57,7 +57,9 @@ export async function loadSeenMap(key) {
|
|
|
57
57
|
try {
|
|
58
58
|
const dir = resolveDir(key);
|
|
59
59
|
const entries = await readdir(dir);
|
|
60
|
-
for (const entry of entries
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.endsWith(".json") || !SAFE_SEGMENT.test(entry))
|
|
62
|
+
continue;
|
|
61
63
|
try {
|
|
62
64
|
const raw = await readFile(join(dir, entry), "utf8");
|
|
63
65
|
const marker = JSON.parse(raw);
|
|
@@ -182,16 +184,7 @@ export async function readSeenMarker(key, id) {
|
|
|
182
184
|
// Helpers
|
|
183
185
|
// ---------------------------------------------------------------------------
|
|
184
186
|
function resolveDir(key) {
|
|
185
|
-
|
|
186
|
-
["owner", key.owner],
|
|
187
|
-
["repo", key.repo],
|
|
188
|
-
]) {
|
|
189
|
-
if (!SAFE_SEGMENT.test(value)) {
|
|
190
|
-
throw new Error(`Invalid state key segment "${field}": ${value}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const base = resolveStateBase();
|
|
194
|
-
return join(base, `${key.owner}-${key.repo}`, String(key.pr), "seen");
|
|
187
|
+
return resolvePrStatePath(key, "seen");
|
|
195
188
|
}
|
|
196
189
|
function resolvePath(key, id) {
|
|
197
190
|
if (!SAFE_SEGMENT.test(id)) {
|
|
@@ -203,5 +196,5 @@ function resolvePath(key, id) {
|
|
|
203
196
|
// causing seen-markers to overwrite each other and items to re-surface every
|
|
204
197
|
// tick. SHA-256 is case-sensitive so distinct IDs get distinct files.
|
|
205
198
|
const hash = createHash("sha256").update(id, "utf8").digest("hex");
|
|
206
|
-
return
|
|
199
|
+
return resolvePrStatePath(key, "seen", `${hash}.json`);
|
|
207
200
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse GitHub review-comment "suggestion" blocks.
|
|
3
|
+
*
|
|
4
|
+
* GitHub's "Commit suggestion" UI button treats the first ```suggestion fenced
|
|
5
|
+
* block in a review comment as a replacement for the commented line range.
|
|
6
|
+
* This module extracts that block from the comment body.
|
|
7
|
+
*
|
|
8
|
+
* There is no GitHub API for applying suggestions — tools that reproduce the
|
|
9
|
+
* button (this one included) must parse + commit themselves.
|
|
10
|
+
*/
|
|
11
|
+
interface ParsedSuggestion {
|
|
12
|
+
/**
|
|
13
|
+
* Replacement lines to splice in. Empty array means "delete these lines".
|
|
14
|
+
* `[""]` means "replace with a single blank line". Array length equals the
|
|
15
|
+
* number of replacement lines in the suggestion snippet (not the number of
|
|
16
|
+
* lines in the resulting file).
|
|
17
|
+
*/
|
|
18
|
+
lines: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Return the first ```suggestion block from a review-comment body, or null if none.
|
|
22
|
+
*
|
|
23
|
+
* Handles three distinct cases:
|
|
24
|
+
* - Empty block (` ```suggestion\n``` `) → `lines: []` (deletion).
|
|
25
|
+
* - Blank-line-only body (` ```suggestion\n\n``` `) → `lines: [""]`.
|
|
26
|
+
* - Non-empty body → split into lines.
|
|
27
|
+
*
|
|
28
|
+
* The opening fence may be 3+ backticks; the closing fence must be at least
|
|
29
|
+
* as many backticks, at the start of a line (after the captured prefix). This
|
|
30
|
+
* means content lines that contain ` ``` ` in the middle (not at line-start)
|
|
31
|
+
* are treated as content rather than a closing fence — fixing the silent
|
|
32
|
+
* truncation in the original regex approach (issue #68).
|
|
33
|
+
*
|
|
34
|
+
* When the block is embedded in a quoted reply (e.g. `> ```suggestion …`),
|
|
35
|
+
* the leading prefix captured from the opening fence is stripped from each
|
|
36
|
+
* body line — but only when that exact prefix is present, so legitimate `>`
|
|
37
|
+
* characters inside the suggested code survive.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseSuggestion(body: string): ParsedSuggestion | null;
|
|
40
|
+
/**
|
|
41
|
+
* True when a parsed suggestion's replacement is safe to commit: the joined
|
|
42
|
+
* replacement contains no nested ` ```suggestion ` marker and no unmatched
|
|
43
|
+
* ` ``` ` run (odd count). Both shapes previously masked silent truncation
|
|
44
|
+
* (issue #68). Conservative by design: reviewers whose suggestion content
|
|
45
|
+
* legitimately includes these markers must apply the change manually.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isCommittableSuggestion(parsed: ParsedSuggestion): boolean;
|
|
48
|
+
export {};
|