Repository navigation
Address PR CommentsΒ #4
Description
Activity
- addedenhancementNew feature or requestNew feature or requestand removed
on Aug 22, 2026 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-readylabel to stop it retrying.coding-agent-loop run
e81482e8-92f9-4076-9859-255ddbec7933Plan
Add PR review-comment handling: acknowledge
@coding-agentmentions 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
- 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). - 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 aPullRequestReview, so the π/π contract can't be honoured there. See Open decisions. - Reactions: ack =
eyes; done =+1(GitHub has no β reaction;+1is the conventional "done",rocketthe alternative). Both configurable. - Who may command the agent: only comment authors whose
author_associationisOWNER,MEMBER, orCOLLABORATOR, or who appear in an explicit login allowlist. Without this, any drive-by commenter on a public repo can trigger abypassPermissionsClaude run. - 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/releaseRepoandrenewLeaseare reused verbatim β one unit of work per repo at a time still holds across both work kinds. - 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,mentionmust 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.LoadusesDisallowUnknownFields, so an existing deployment'sconfig.jsonthat omits the block must still load. - Mirror the block into
config.example.jsonand the README config table.config.jsonis gitignored and generated bymake config, so nothing else to touch.
internal/gh/gh.goβ new API surfaceNew 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 .logincached by the caller; used for --authorand to skip our own commentsSearchPRs(ctx, author, owners, limit)gh search prs --state open --author <login> --owner β¦ --json number,repository,title,url,updatedAtmirrors SearchIssues, including the defensiveownedBy()filter already in this fileViewPR(ctx, repo, number)gh pr view <n> --repo <r> --json number,title,body,url,state,isDraft,headRefName,baseRefNameextend the existing PullRequeststruct withBaseRefName,IsDraftPRComments(ctx, repo, number)gh api --paginate repos/<repo>/issues/<n>/comments+β¦/pulls/<n>/commentsone merged []PRCommentsorted byCreatedAt;gh api --paginatemerges JSON arraysPRReviewBodies(ctx, repo, number)gh api --paginate repos/<repo>/pulls/<n>/reviewscontext 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 honourDryRunvialogf, exactly likeCommentandCreatePRCommentOnPR(ctx, repo, number, body)same endpoint as the existing Comment(a PR is an issue here)may just call Comment; keep the alias for readabilityAll go through the existing
run/runJSONhelpers, soCmdErrorcontext and dry-run behaviour come for free.internal/store/store.goβ migration 5Append one migration string to
migrations(the existingmigrate()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.Kindfield +runColumns+scanRun+CreateRunupdated (RunKindIssue/RunKindPRCommentconstants beside the status constants). AddAND kind = 'issue'toIssueHistory's aggregate so PR runs can never colour an issue's back-off.- New status
StatusAddressed = "addressed", added toIsTerminal. - Four small methods in the existing
ExecContext+ wrapped-error style:PRCommentTasks(ctx, repo, pr),MarkPRCommentAcked,MarkPRCommentDone,MarkPRCommentFailed(the last bumpsattempts, stampslast_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 fromtickright after the gate/capacity checks and before the issue loop; returns remaining capacity. For eachSearchPRshit:Owned/Excluded/ in-memory-busy /Store.RepoBusyguards (the same oneseligibleapplies), thenViewPR, then the head-branch-prefix check, thenpendingMentions. Nothing pending β next PR.workPRComments(ctx, cand, comments)βTryClaimon(repo, prNumber),defer ReleaseClaim, register the cancel func ino.cancels,renewLease,CreateRun{Kind: pr_comment, Issue: prNumber, Branch: headRef},Discord.RunClaimed.- Ack immediately β
GH.React(ack)on every pending comment andMarkPRCommentAcked, before any cloning. This is the user-visible promise; it must not wait on a clone. - Workspace β
repoMetadata,Git.EnsureRepo,Git.AssertRemote, thenGit.AddWorktree(ctx, repoPath, worktree, headRef, headRef). Passing the PR head branch as both branch and base makes the existing helper runworktree 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 fromGit.WorktreePath(repo, prNumber)unchanged. - Model β
Store.CooledDownModels+Registry.Ladder(models.RoleImplement, cooled)+models.Head, as in the implement phase, demoting by the task's attempt count. Runner.RunwithprCommentSystemPrompt/prCommentTaskPromptandcfg.Claude.PermissionMode; the sameOnEventsession capture,RecordUsage/recordSessionbookkeeping, andgate.DetectLimitβerrRetryablehandling asexecute, so a usage limit defers rather than fails.Git.HasWork(worktree, headRef). No work is not a failure here β a question answered in prose is a legitimate outcome. With work:Git.CommitAll(messageAddress 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.GH.CommentOnPRwith the marker-tagged summary, thenGH.React(done)+MarkPRCommentDoneper comment,SetRunStatus(StatusAddressed), events, Discord, cleanup.- Failure path β
MarkPRCommentFailedper 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.
- Ack immediately β
internal/orchestrator/phase.goAdd
markerPRComment = markerPrefix + "pr-comment -->"beside the existing markers, so the harness's own PR replies are recognised byisAgentCommentand can never be read back as a mention.internal/orchestrator/prompt.goprCommentSystemPrompt(repo, branch, worktree)β same harness contract assystemPrompt(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 fenceddiff_hunkfor inline comments), then review summary bodies as context. Reuse the existingtruncate,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.goprCommentComment(handled, summary, runID, v verify.Result, pushed bool)β marker, one line per handled comment linking itshtml_url, the agent's summary, verify status (reusingissueComment'sstore.VerifyPassed/Failedswitch), run-id footer.prCommentFailureComment(...)β the equivalent offailureCommentfor this path.
internal/discord/notifier.goOne method,
PRCommentsAddressed(r RunRef, handled int, res *claude.Result, v verify.Result, elapsed time.Duration), shaped likePROpened. Claim/fail/defer reuse the existingRunClaimed/RunFailed/RunDeferredwithRunRef{Issue: prNumber, Title: pr.Title, URL: pr.URL}.internal/server/server.goNo handler changes needed;
Run.Kindflows through/runsand/runs/:idonce it is on the struct. One README line notingkinddistinguishes the two work types.README.mdNew 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_commentsknobs 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_prefixbranch. - Only comment authors with OWNER/MEMBER/COLLABORATOR association, or an explicit login allowlist β otherwise an arbitrary GitHub user can drive a
bypassPermissionsClaude 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;
AssertRemotestill runs immediately before it. --dry-runsuppresses reactions as well as comments and pushes.
Tests
internal/orchestrator/prcomments_test.go: table test formentionsAgent(plain, mid-sentence,@Coding-Agent,@coding-agent-loopnon-match, inside a>quote, inside a fenced code block, absent);pendingMentionsfiltering (own login, marker body, disallowed association, too old, alreadydone,failedinside vs. outside the back-off).prompt_test.go/report_test.go: the prompt includes every comment body and diff hunk; the reply comment carriesmarkerPRCommentand each handled comment's URL.internal/gh/gh_test.go: reuse the existingstubGHhelper to assert exact argv forReact(both kinds),SearchPRs(owner scoping, no cross-owner leakage), andPRCommentsdecoding; assertReactis a no-op underDryRun.internal/store/store_test.go: migration applies to an existing DB; ack β done and ack β failed transitions;attemptsincrements;IssueHistoryignoreskind='pr_comment'rows.internal/config/config_test.go: defaults populated, invalid reaction rejected, a config without thepr_commentsblock still loads.
Verification
make test(go test -race ./...).go build ./...and--checkwith the new config block, then again with an oldconfig.jsonlacking it (must still boot).--once --dry-run --log-level debugagainst a repo with a live agent PR carrying an@coding-agentcomment: logs show the PR discovered, the mention matched, anddry-run: would β¦for the reaction, the push, and the reply β nothing mutated on GitHub.- 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
- Done reaction:
+1(recommended) vsrocket. Configurable either way; this only sets the default. - Review summary bodies. Excluded from triggering because REST cannot react to a
PullRequestReview. Including them means moving the read + react path to GraphQL (addReactionon anyReactable, which would also giveviewerHasReactedas 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. - 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
updatedAthasn't moved (one more column onpr_comment_tasks). Not included now. runs.kindcolumn. Recommended for legible/runsoutput and clean issue back-offs, but it does touchscanRun/runColumns/CreateRun. Say if you'd rather PR runs be indistinguishable in the run table and skip theALTER TABLE.
Reply with exactly
implementto 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, modelclaude-opus-5, cost $2.3082- Which PRs are watched: open PRs authored by the daemon's own GitHub account whose head branch starts with
- addedagent-plannedManaged by coding-agent-loopManaged by coding-agent-loopand removed
on Aug 22, 2026 implement
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- added and removedagent-plannedManaged by coding-agent-loopManaged by coding-agent-loop
on Aug 22, 2026 - added a commit that references this issue
on Aug 25, 2026
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.