• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 25260030712

02 May 2026 07:31PM UTC coverage: 64.412% (-0.3%) from 64.699%
25260030712

push

github

web-flow
chore: Add GitHub PR review workflow (#594)

Introduces an end-to-end AI-assisted pull request review workflow,
spanning a new `jp_attachment_github` crate, extensions to `jp_github`,
new project tooling, and a dedicated `pr-reviewer` persona.

**New `jp_attachment_github` crate**

Adds a `gh://` URI attachment handler with two resource types:

- `gh:pull/N/diff` — fetches a PR's title, description, and unified diff
as a single text attachment. The diff is filtered through a configurable
exclusion list (snapshots, lockfiles, minified JS, …) so generated noise
is kept out of the LLM context. Override via `?exclude=glob1,glob2` or
disable the defaults with `?no_defaults=true`.

- `gh:pull/N/reviews` — fetches all reviews and inline comments on a PR,
rendered as structured Markdown grouped by file and anchor line. Pending
(draft) reviews and comments are attributed to "you" rather than
exposing the reviewer's login.

Both resource types accept a shortform (`gh:pull/N/diff`) that is
project-rooted to `dcdpr/jp`, or a canonical form
(`gh://owner/repo/pull/N/diff`) for any repository.

**`jp_github` extensions**

Added new models (`Side`, `ReviewState`, `Review`, `DraftReviewComment`,
`GitRef`) and new `PullsHandler` methods:

- `list_reviews` — lists all reviews on a PR.
- `create_review` — builder API that creates a draft (`PENDING`) review.
- `delete_review` — deletes a pending review.
- `add_review_thread` — appends a single inline comment to an existing
pending review via the GraphQL `addPullRequestReviewThread` mutation,
since the REST API does not support appending to an existing draft.

`PullRequest` gains `node_id`, `head`, and `base` fields required for
fetching file content at the correct SHA.

**`github_pr_review_add_comment` tool**

A new project tool that queues one inline comment at a time to the
authenticated user's pending review. Before each comment is posted, the
tool renders a syntax-highlighted snippet from the PR's HEAD file plus
the p... (continued)

539 of 1036 new or added lines in 8 files covered. (52.03%)

2 existing lines in 1 file now uncovered.

24568 of 38142 relevant lines covered (64.41%)

194.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/.config/jp/tools/src/github.rs
1
use crate::{
2
    Context, Error, Tool,
3
    util::{ToolResult, unknown_tool},
4
};
5

6
mod create_issue_bug;
7
mod create_issue_enhancement;
8
mod create_issue_rfd_tracking;
9
mod issues;
10
mod pulls;
11
mod repo;
12
mod review;
13

14
use create_issue_bug::github_create_issue_bug;
15
use create_issue_enhancement::github_create_issue_enhancement;
16
use create_issue_rfd_tracking::github_create_issue_rfd_tracking;
17
use issues::github_issues;
18
use pulls::github_pulls;
19
use repo::{github_code_search, github_list_files, github_read_file};
20
use review::github_pr_review_add_comment;
21

22
const ORG: &str = "dcdpr";
23
const REPO: &str = "jp";
24

NEW
25
pub async fn run(ctx: Context, t: Tool) -> ToolResult {
×
26
    match t.name.trim_start_matches("github_") {
×
27
        "issues" => github_issues(t.opt_or_empty("number")?)
×
28
            .await
×
29
            .map(Into::into),
×
30

31
        "create_issue_bug" => github_create_issue_bug(
×
32
            t.req("title")?,
×
33
            t.req("description")?,
×
34
            t.req("expected_behavior")?,
×
35
            t.req("actual_behavior")?,
×
36
            t.req("complexity")?,
×
37
            t.opt("reproduce")?,
×
38
            t.opt("proposed_solution")?,
×
39
            t.opt("tasks")?,
×
40
            t.opt("resource_links")?,
×
41
            t.opt("labels")?,
×
42
            t.opt("assignees")?,
×
43
        )
44
        .await
×
45
        .map(Into::into),
×
46

47
        "create_issue_enhancement" => github_create_issue_enhancement(
×
48
            t.req("title")?,
×
49
            t.req("description")?,
×
50
            t.req("context")?,
×
51
            t.req("complexity")?,
×
52
            t.opt("alternatives")?,
×
53
            t.opt("proposed_implementation")?,
×
54
            t.opt("tasks")?,
×
55
            t.opt("resource_links")?,
×
56
            t.opt("labels")?,
×
57
            t.opt("assignees")?,
×
58
        )
59
        .await
×
60
        .map(Into::into),
×
61

62
        "create_issue_rfd_tracking" => github_create_issue_rfd_tracking(
×
63
            t.req("rfd_number")?,
×
64
            t.req("rfd_title")?,
×
65
            t.req("rfd_slug")?,
×
66
            t.req("tasks")?,
×
67
        )
68
        .await
×
69
        .map(Into::into),
×
70

71
        "pulls" => github_pulls(t.opt("number")?, t.opt("state")?, t.opt("file_diffs")?)
×
72
            .await
×
73
            .map(Into::into),
×
74

NEW
75
        "pr_review_add_comment" => {
×
NEW
76
            github_pr_review_add_comment(
×
NEW
77
                ctx,
×
NEW
78
                t.req("pull_number")?,
×
NEW
79
                t.req("path")?,
×
NEW
80
                t.req("line")?,
×
NEW
81
                t.req("body")?,
×
NEW
82
                t.opt("side")?,
×
NEW
83
                t.opt("start_line")?,
×
NEW
84
                t.opt("start_side")?,
×
85
            )
86
            .await
×
87
        }
88

NEW
89
        "code_search" => github_code_search(t.opt("repository")?, t.req("query")?)
×
90
            .await
×
91
            .map(Into::into),
×
92

NEW
93
        "read_file" => github_read_file(
×
NEW
94
            t.opt("repository")?,
×
NEW
95
            t.opt("ref")?,
×
NEW
96
            t.req("path")?,
×
NEW
97
            t.opt("start_line")?,
×
NEW
98
            t.opt("end_line")?,
×
99
        )
NEW
100
        .await
×
NEW
101
        .map(Into::into),
×
102

103
        "list_files" => github_list_files(t.opt("repository")?, t.opt("ref")?, t.opt("path")?)
×
104
            .await
×
105
            .map(Into::into),
×
106

107
        _ => unknown_tool(t),
×
108
    }
109
}
×
110

111
async fn auth() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
×
112
    let token = std::env::var("JP_GITHUB_TOKEN")
×
113
        .or_else(|_| std::env::var("GITHUB_TOKEN"))
×
114
        .map_err(|_| {
×
115
            "unable to get auth token. Set `JP_GITHUB_TOKEN` or `GITHUB_TOKEN` to a valid token."
×
116
        })?;
×
117

118
    let octocrab = jp_github::Octocrab::builder()
×
119
        .personal_token(token)
×
120
        .build()
×
121
        .map_err(|err| format!("unable to create github client: {err:#}"))?;
×
122

123
    jp_github::initialise(octocrab);
×
124

125
    if jp_github::instance().current().user().await.is_err() {
×
126
        return Err(
×
127
            "Unable to authenticate with github. This might be because the token is expired. \
×
128
             Either set `JP_GITHUB_TOKEN` or `GITHUB_TOKEN` to a valid token."
×
129
                .into(),
×
130
        );
×
131
    }
×
132

133
    Ok(())
×
134
}
×
135

136
fn handle_404(error: jp_github::Error, msg: impl Into<String>) -> Error {
×
137
    match error {
×
138
        jp_github::Error::GitHub { source, .. } if source.status_code.as_u16() == 404 => {
×
139
            msg.into().into()
×
140
        }
141
        _ => Box::new(error) as Error,
×
142
    }
143
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc