Skip to content

Address PR CommentsΒ #4

Description

@ableinc

Add the feature to monitor comments in PRs. If you're tagged in the comment via "@coding-agent", then react to the comment with the πŸ‘€ emoji to acknowledge that you're addressing the comment. React with the βœ… (or GH equivalent) when the comment has been addressed.

Activity

  1. ableinc commented on Aug 22, 2026

    @ableinc
    OwnerAuthor

    The coding agent could not complete this issue (attempt 1).

    claude run failed: claude exited with error: exit status 1 (stderr: )
    

    It will try again after Sat, 22 Aug 2026 10:57:42 UTC (the wait doubles with each consecutive failure). Remove the agent-ready label to stop it retrying.

    coding-agent-loop run e81482e8-92f9-4076-9859-255ddbec7933

  2. ableinc commented on Aug 22, 2026

    @ableinc
    OwnerAuthor

    Plan

    Add PR review-comment handling: acknowledge @coding-agent mentions with πŸ‘€, address them on the PR branch, then confirm with πŸ‘

    (Note: I could not write this to /home/node1/.claude/plans/… β€” that path is on a read-only filesystem. The plan is below in full.)

    Context

    Today the daemon's only input is a labelled GitHub issue: discover β†’ plan β†’ approve β†’ implement β†’ draft PR (internal/orchestrator/loop.go). Once the draft PR exists, the loop is done with it. A reviewer who leaves feedback on that PR has no way to hand it back to the agent β€” they must re-label the issue, and the agent would start from a fresh branch.

    Issue #4 asks for the missing half of that conversation: watch comments on pull requests, and when one mentions @coding-agent, react πŸ‘€ to show it has been picked up, do the work, then react with the "done" equivalent.

    Scope decision (the issue does not spell it out): "addressed" means the agent actually makes the requested change on the PR's existing branch and pushes it β€” reacting alone would be theatre. If the comment is a question rather than a change request, the agent answers it in a reply comment and still marks it done.

    The one prior automated attempt failed before producing anything (claude exited with error: exit status 1), so there is no partial work to build on.


    Design decisions taken here

    1. Which PRs are watched: open PRs authored by the daemon's own GitHub account whose head branch starts with workspace.branch_prefix. This is a hard safety rule, not a config knob: the daemon only ever pushes to branches it created itself. A mention on any other PR is ignored (documented in the README).
    2. Which comments can trigger: PR conversation comments (/issues/{n}/comments) and inline review comments (/pulls/{n}/comments). Both are reactable through the REST reactions API. Review summary bodies are read as context but cannot trigger β€” REST has no reactions endpoint for a PullRequestReview, so the πŸ‘€/πŸ‘ contract can't be honoured there. See Open decisions.
    3. Reactions: ack = eyes; done = +1 (GitHub has no βœ… reaction; +1 is the conventional "done", rocket the alternative). Both configurable.
    4. Who may command the agent: only comment authors whose author_association is OWNER, MEMBER, or COLLABORATOR, or who appear in an explicit login allowlist. Without this, any drive-by commenter on a public repo can trigger a bypassPermissions Claude run.
    5. Reuse of the existing concurrency model: GitHub numbers PRs in the same sequence as issues, so claims(repo, issue) can hold a PR number with no schema change. Store.TryClaim/RepoBusy/RenewClaim/ReleaseClaim, Orchestrator.reserveRepo/releaseRepo and renewLease are reused verbatim β€” one unit of work per repo at a time still holds across both work kinds.
    6. Priority: the PR-comment pass runs before issue discovery in each tick and consumes the same capacity budget. A reviewer waiting on a reply outranks starting a new issue.

    Changes by file

    internal/config/config.go (+ config.example.json, README)

    Add a nested block to GitHubConfig:

    // PRComments controls responding to @-mentions on the daemon's own pull requests.
    PRComments PRCommentsConfig `json:"pr_comments"`
    type PRCommentsConfig struct {
        Enabled             bool     `json:"enabled"`
        Mention             string   `json:"mention"`               // "@coding-agent"
        SearchLimit         int      `json:"search_limit"`          // 30
        MaxAge              Duration `json:"max_age"`               // "168h"; 0 = no limit
        AckReaction         string   `json:"ack_reaction"`          // "eyes"
        DoneReaction        string   `json:"done_reaction"`         // "+1"
        AllowedAuthors      []string `json:"allowed_authors"`       // empty = fall back to associations
        AllowedAssociations []string `json:"allowed_associations"`  // OWNER, MEMBER, COLLABORATOR
    }
    • Populate all of it in config.Default() (defaults above, Enabled: true).
    • Extend Config.Validate(): when enabled, mention must be non-empty and start with @; the two reaction values must be one of the eight GitHub contents (+1 -1 laugh confused heart hooray rocket eyes); search_limit β‰₯ 1.
    • Every field must be additive and default-safe β€” config.Load uses DisallowUnknownFields, so an existing deployment's config.json that omits the block must still load.
    • Mirror the block into config.example.json and the README config table. config.json is gitignored and generated by make config, so nothing else to touch.

    internal/gh/gh.go β€” new API surface

    New type, in the same "typed struct per gh JSON payload" style as Issue/Comment:

    // PRComment is one reactable comment on a pull request.
    type PRComment struct {
        ID          int64  // REST numeric id β€” what the reactions endpoint keys on
        Kind        string // CommentKindIssue | CommentKindReview
        Author      string
        Association string // author_association
        Body        string
        URL         string // html_url
        CreatedAt   time.Time
        // review-comment only, used to give Claude the anchor:
        Path, DiffHunk string
        Line           int
    }

    New methods:

    Method Underlying command Notes
    CurrentLogin(ctx) gh api user --jq .login cached by the caller; used for --author and to skip our own comments
    SearchPRs(ctx, author, owners, limit) gh search prs --state open --author <login> --owner … --json number,repository,title,url,updatedAt mirrors SearchIssues, including the defensive ownedBy() filter already in this file
    ViewPR(ctx, repo, number) gh pr view <n> --repo <r> --json number,title,body,url,state,isDraft,headRefName,baseRefName extend the existing PullRequest struct with BaseRefName, IsDraft
    PRComments(ctx, repo, number) gh api --paginate repos/<repo>/issues/<n>/comments + …/pulls/<n>/comments one merged []PRComment sorted by CreatedAt; gh api --paginate merges JSON arrays
    PRReviewBodies(ctx, repo, number) gh api --paginate repos/<repo>/pulls/<n>/reviews context only, never a trigger
    React(ctx, repo, c PRComment, content) gh api --method POST repos/<repo>/{issues|pulls}/comments/<id>/reactions -f content=<content> path segment chosen by c.Kind; must honour DryRun via logf, exactly like Comment and CreatePR
    CommentOnPR(ctx, repo, number, body) same endpoint as the existing Comment (a PR is an issue here) may just call Comment; keep the alias for readability

    All go through the existing run/runJSON helpers, so CmdError context and dry-run behaviour come for free.

    internal/store/store.go β€” migration 5

    Append one migration string to migrations (the existing migrate() loop handles versioning):

    ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'issue';
    
    CREATE TABLE IF NOT EXISTS pr_comment_tasks (
        repo            TEXT    NOT NULL,
        pr              INTEGER NOT NULL,
        comment_kind    TEXT    NOT NULL,   -- 'issue' | 'review'
        comment_id      INTEGER NOT NULL,
        status          TEXT    NOT NULL,   -- 'acked' | 'done' | 'failed'
        attempts        INTEGER NOT NULL DEFAULT 0,
        last_attempt_at INTEGER NOT NULL DEFAULT 0,
        run_id          TEXT    NOT NULL DEFAULT '',
        created_at      INTEGER NOT NULL,
        updated_at      INTEGER NOT NULL,
        PRIMARY KEY (comment_kind, comment_id)
    );
    CREATE INDEX IF NOT EXISTS pr_comment_tasks_pr ON pr_comment_tasks(repo, pr);
    • Run.Kind field + runColumns + scanRun + CreateRun updated (RunKindIssue/RunKindPRComment constants beside the status constants). Add AND kind = 'issue' to IssueHistory's aggregate so PR runs can never colour an issue's back-off.
    • New status StatusAddressed = "addressed", added to IsTerminal.
    • Four small methods in the existing ExecContext + wrapped-error style: PRCommentTasks(ctx, repo, pr), MarkPRCommentAcked, MarkPRCommentDone, MarkPRCommentFailed (the last bumps attempts, stamps last_attempt_at).

    Why a table rather than reading reactions back: it is the only thing that survives a crash between the πŸ‘€ and the run, it gives per-comment attempt counts for the back-off, and it keeps dedup independent of who else may have reacted.

    internal/orchestrator/prcomments.go (new)

    Pure, unit-testable helpers first:

    // mentionsAgent reports whether body addresses the agent by handle. Match is
    // case-insensitive, must sit on a word boundary (so "@coding-agent-loop" is not
    // a match for "@coding-agent"), and quoted lines (leading ">") are ignored so a
    // quoted mention cannot re-trigger the agent.
    func mentionsAgent(body, handle string) bool
    
    // pendingMentions filters a PR's comments to the ones this run should act on:
    // mention present, author permitted, not older than maxAge, not authored by the
    // daemon, not one of the daemon's own marker comments, and either unseen or a
    // failed task whose back-off has elapsed (reusing retryDelay from loop.go).
    func pendingMentions(...) []gh.PRComment

    Then the pass, mirroring tick/work/execute:

    • tickPRComments(ctx, capacity) int β€” called from tick right after the gate/capacity checks and before the issue loop; returns remaining capacity. For each SearchPRs hit: Owned / Excluded / in-memory-busy / Store.RepoBusy guards (the same ones eligible applies), then ViewPR, then the head-branch-prefix check, then pendingMentions. Nothing pending β†’ next PR.
    • workPRComments(ctx, cand, comments) β€” TryClaim on (repo, prNumber), defer ReleaseClaim, register the cancel func in o.cancels, renewLease, CreateRun{Kind: pr_comment, Issue: prNumber, Branch: headRef}, Discord.RunClaimed.
      1. Ack immediately β€” GH.React(ack) on every pending comment and MarkPRCommentAcked, before any cloning. This is the user-visible promise; it must not wait on a clone.
      2. Workspace β€” repoMetadata, Git.EnsureRepo, Git.AssertRemote, then Git.AddWorktree(ctx, repoPath, worktree, headRef, headRef). Passing the PR head branch as both branch and base makes the existing helper run worktree add -B <headRef> <path> origin/<headRef> β€” a clean checkout of the PR head, and a retry after a human pushed to the branch picks their commits up naturally. No new git code. Worktree path from Git.WorktreePath(repo, prNumber) unchanged.
      3. Model β€” Store.CooledDownModels + Registry.Ladder(models.RoleImplement, cooled) + models.Head, as in the implement phase, demoting by the task's attempt count.
      4. Runner.Run with prCommentSystemPrompt / prCommentTaskPrompt and cfg.Claude.PermissionMode; the same OnEvent session capture, RecordUsage/recordSession bookkeeping, and gate.DetectLimit β†’ errRetryable handling as execute, so a usage limit defers rather than fails.
      5. Git.HasWork(worktree, headRef). No work is not a failure here β€” a question answered in prose is a legitimate outcome. With work: Git.CommitAll (message Address review feedback on #<pr> + run id), Verify.Run, Git.Push(worktree, headRef, repo). The push is the existing non-force --set-upstream origin b:b; a rejected push (someone pushed meanwhile) is a normal failure and retries after the back-off from a fresh checkout.
      6. GH.CommentOnPR with the marker-tagged summary, then GH.React(done) + MarkPRCommentDone per comment, SetRunStatus(StatusAddressed), events, Discord, cleanup.
      7. Failure path β€” MarkPRCommentFailed per comment (back-off is per comment), a marker-tagged failure comment on the PR, Store.FailRun, Discord.RunFailed. The πŸ‘€ stays: the comment was seen, and the next pass retries it.

    internal/orchestrator/phase.go

    Add markerPRComment = markerPrefix + "pr-comment -->" beside the existing markers, so the harness's own PR replies are recognised by isAgentComment and can never be read back as a mention.

    internal/orchestrator/prompt.go

    • prCommentSystemPrompt(repo, branch, worktree) β€” same harness contract as systemPrompt (no push, no PR/branch creation, no history rewriting, edits confined to the worktree, nobody is watching), reframed: an open pull request already exists on this branch, you are addressing reviewer feedback on it, keep the change to what the feedback asks for.
    • prCommentTaskPrompt(repo, pr, comments, reviews) β€” PR title/body, then each triggering comment as author + body (+ path, line, and fenced diff_hunk for inline comments), then review summary bodies as context. Reuse the existing truncate, maxCommentChars, maxBodyChars. Instruct: address every comment listed; if one is a question rather than a change request, answer it in the final summary; the final summary is posted back to the PR.

    internal/orchestrator/report.go

    • prCommentComment(handled, summary, runID, v verify.Result, pushed bool) β€” marker, one line per handled comment linking its html_url, the agent's summary, verify status (reusing issueComment's store.VerifyPassed/Failed switch), run-id footer.
    • prCommentFailureComment(...) β€” the equivalent of failureComment for this path.

    internal/discord/notifier.go

    One method, PRCommentsAddressed(r RunRef, handled int, res *claude.Result, v verify.Result, elapsed time.Duration), shaped like PROpened. Claim/fail/defer reuse the existing RunClaimed/RunFailed/RunDeferred with RunRef{Issue: prNumber, Title: pr.Title, URL: pr.URL}.

    internal/server/server.go

    No handler changes needed; Run.Kind flows through /runs and /runs/:id once it is on the struct. One README line noting kind distinguishes the two work types.

    README.md

    New section "Responding to PR comments" after "Lifecycle of one issue"; the config block in the config reference; a row in Safety boundaries (author allowlist, agent-branch-only rule, never force-pushes, never merges); the pr_comments knobs in the table; and the top-of-file ASCII flow updated to show the second entry point.


    Safety boundaries this adds (please review)

    • Only PRs authored by the daemon's own account and on a workspace.branch_prefix branch.
    • Only comment authors with OWNER/MEMBER/COLLABORATOR association, or an explicit login allowlist β€” otherwise an arbitrary GitHub user can drive a bypassPermissions Claude run on your machine.
    • Own comments and marker-tagged comments are skipped; quoted (>) lines don't count as mentions β€” no self-triggering loop.
    • max_age (default 7 days) stops a first poll from stampeding through a year of comment history.
    • Push stays non-force and on the PR's own branch; AssertRemote still runs immediately before it.
    • --dry-run suppresses reactions as well as comments and pushes.

    Tests

    • internal/orchestrator/prcomments_test.go: table test for mentionsAgent (plain, mid-sentence, @Coding-Agent, @coding-agent-loop non-match, inside a > quote, inside a fenced code block, absent); pendingMentions filtering (own login, marker body, disallowed association, too old, already done, failed inside vs. outside the back-off).
    • prompt_test.go / report_test.go: the prompt includes every comment body and diff hunk; the reply comment carries markerPRComment and each handled comment's URL.
    • internal/gh/gh_test.go: reuse the existing stubGH helper to assert exact argv for React (both kinds), SearchPRs (owner scoping, no cross-owner leakage), and PRComments decoding; assert React is a no-op under DryRun.
    • internal/store/store_test.go: migration applies to an existing DB; ack β†’ done and ack β†’ failed transitions; attempts increments; IssueHistory ignores kind='pr_comment' rows.
    • internal/config/config_test.go: defaults populated, invalid reaction rejected, a config without the pr_comments block still loads.

    Verification

    1. make test (go test -race ./...).
    2. go build ./... and --check with the new config block, then again with an old config.json lacking it (must still boot).
    3. --once --dry-run --log-level debug against a repo with a live agent PR carrying an @coding-agent comment: logs show the PR discovered, the mention matched, and dry-run: would … for the reaction, the push, and the reply β€” nothing mutated on GitHub.
    4. End-to-end on a scratch repo: label an issue, let the loop open a draft PR, comment @coding-agent please rename X to Y, and confirm β€” πŸ‘€ within one poll, a commit on the PR branch, a marker-tagged reply, πŸ‘, and a second poll that does not re-process the comment. Then restart the daemon mid-run and confirm the comment is retried rather than stranded.

    Open decisions for the reviewer

    1. Done reaction: +1 (recommended) vs rocket. Configurable either way; this only sets the default.
    2. Review summary bodies. Excluded from triggering because REST cannot react to a PullRequestReview. Including them means moving the read + react path to GraphQL (addReaction on any Reactable, which would also give viewerHasReacted as a free cross-restart dedup marker). That is a materially larger change and one big embedded query string; this plan deliberately takes the smaller REST path first. Say if review bodies should be in v1.
    3. Polling cost. Each poll costs one search plus ~2 API calls per open agent PR. Negligible at a 5m interval with a handful of PRs; if a repo routinely has dozens, the follow-up is to skip PRs whose updatedAt hasn't moved (one more column on pr_comment_tasks). Not included now.
    4. runs.kind column. Recommended for legible /runs output and clean issue back-offs, but it does touch scanRun/runColumns/CreateRun. Say if you'd rather PR runs be indistinguishable in the run table and skip the ALTER TABLE.

    Reply with exactly implement to approve this plan and start the change. Reply with anything else and the plan will be revised to address it.

    coding-agent-loop run 1fdb4ab7-042a-4e44-a841-1d6e125e5c7a, model claude-opus-5, cost $2.3082

  3. ableinc commented on Aug 22, 2026

    @ableinc
    OwnerAuthor

    implement

  4. ableinc commented on Aug 22, 2026

    @ableinc
    OwnerAuthor

    Opened a draft pull request for this issue: #8

    Tests failed (make test) β€” see the PR for output.

    coding-agent-loop run 8ff203c4-9aad-4aca-a119-6c1154030178

  5. added a commit that references this issue on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions