feat(maintainer-standup): surface contributor replies since last run#1457
feat(maintainer-standup): surface contributor replies since last run#1457
Conversation
The brief was missing a key signal — when contributors reply on PRs or
issues, the maintainer wouldn't see it explicitly. Empirically reviewed
PR replies were buried under aggregate updatedAt timestamps with no
indication of WHO replied or WHAT they said.
This adds a new "Replies waiting on you" section to the daily brief,
sourced from two paginated GitHub API calls scoped by since=last_run_at:
- /repos/{o}/{r}/issues/comments PR + issue conversation comments
- /repos/{o}/{r}/pulls/comments inline code-review comments
Filters applied:
- Skip the maintainer's own comments (gh_handle from profile.md)
- Skip GitHub bot accounts (login ending in [bot]) — coderabbitai,
chatgpt-codex-connector, dependabot, etc. They post a constant
churn of automated review tooling that drowns out human replies;
the maintainer wants the latter.
Output is grouped by PR/issue number with kind classification:
- issue comment on a non-PR issue
- pr_conversation PR conversation-level comment
- pr_review inline code-review comment (most actionable —
usually needs a code-level response, so kind
upgrades to pr_review whenever review comments
arrive on a PR that also has conversation ones)
Sorted by recency (newest reply first). Synthesizer reads
gh-data.output.replies_since_last_run and renders a section.
Verified on a backdated state.json (last_run_at = yesterday morning):
22 human replies on 22 PRs/issues, bot noise filtered (32 → 22 after
the [bot] filter). Surfaces exactly the contributor responses to
yesterday's review comments and direction questions.
📝 WalkthroughWalkthroughThis PR adds a "Replies waiting on you" feature to the maintainer standup command by introducing a new Changes
Sequence Diagram(s)sequenceDiagram
participant Script as maintainer-standup Script
participant GH as GitHub API
participant Output as JSON Output
participant Template as Standup Template
Script->>GH: Fetch /issues/comments since lastRunAt
GH-->>Script: Comments on issues
Script->>GH: Fetch /pulls/comments since lastRunAt
GH-->>Script: Comments on PRs
Script->>Script: Filter out maintainer & bot comments
Script->>Script: Group by PR/issue number, sort newest first
Script->>Output: Add replies_since_last_run array
Output-->>Template: Provide grouped replies
Template->>Template: Render "Replies waiting on you" section
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
.archon/commands/maintainer-standup.md (1)
32-34: Make the reply-item schema explicit in the contract.The brief instructions describe the top-level grouping, but the actual payload produced by
.archon/scripts/maintainer-standup-gh-data.tsusescomments[].author,comments[].created_at,comments[].body_excerpt, andcomments[].url. Spell those out here so the renderer doesn't infer a different shape.📘 Suggested wording tweak
-`replies_since_last_run` is an array of `{ number, kind, comments }` grouping contributor replies on PRs and issues since the last run. `kind` is one of `issue` / `pr_conversation` / `pr_review`; the maintainer's own comments are filtered out. Use this as the source for the **"Replies waiting on you"** brief section (see Phase 3). +`replies_since_last_run` is an array of `{ number, kind, comments }` grouping contributor replies on PRs and issues since the last run. `kind` is one of `issue` / `pr_conversation` / `pr_review`; each `comments[]` item has `{ author, created_at, body_excerpt, url }`. The maintainer's own comments are filtered out. Use this as the source for the **"Replies waiting on you"** brief section (see Phase 3).Also applies to: 117-120
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.archon/commands/maintainer-standup.md around lines 32 - 34, Update the contract for replies_since_last_run to explicitly specify the reply-item schema to match the actual payload produced by the data generator: each reply item should be { number: number, kind: "issue" | "pr_conversation" | "pr_review", comments: Array<{ author: string, created_at: string, body_excerpt: string, url: string }> }; ensure the renderer and any consumers expect comments[].author, comments[].created_at, comments[].body_excerpt and comments[].url (and the top-level replies_since_last_run, number, kind fields) so the shape matches the output of the data generator that produces these fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.archon/scripts/maintainer-standup-gh-data.ts:
- Around line 304-307: The sort is using each group's last array element as the
"latest" (repliesSinceLastRun) but comments arrays are appended in mixed order;
instead, for each group in repliesByNumber compute the actual newest created_at
by sorting that group's comments by comment.created_at (or scanning to find the
max created_at) and use that value for the comparison. Update the comparator
that builds repliesSinceLastRun to derive aLatest and bLatest from the
sorted/max created_at of a.comments and b.comments (reference: repliesByNumber,
repliesSinceLastRun, and the comments[].created_at field).
- Around line 239-285: The code currently labels comments as 'pr_conversation'
only if the PR is currently open (openPrNumbers), which mislabels replies on PRs
closed since last run; instead, for each comment determine whether the issue
number is a pull request by fetching the issue metadata (e.g., call GH API for
repos/{owner}/{repo}/issues/{num}) and check for the pull_request field, then
set kind to 'pr_conversation' when pull_request exists (otherwise 'issue');
update the loop that processes issueComments to call this check (referencing
extractNumber, issueComments and addComment) and fall back to the openPrNumbers
check only if you want to avoid extra API calls.
---
Nitpick comments:
In @.archon/commands/maintainer-standup.md:
- Around line 32-34: Update the contract for replies_since_last_run to
explicitly specify the reply-item schema to match the actual payload produced by
the data generator: each reply item should be { number: number, kind: "issue" |
"pr_conversation" | "pr_review", comments: Array<{ author: string, created_at:
string, body_excerpt: string, url: string }> }; ensure the renderer and any
consumers expect comments[].author, comments[].created_at,
comments[].body_excerpt and comments[].url (and the top-level
replies_since_last_run, number, kind fields) so the shape matches the output of
the data generator that produces these fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b5dbd438-53f4-401d-9dd9-9ce84c906bcb
📒 Files selected for processing (2)
.archon/commands/maintainer-standup.md.archon/scripts/maintainer-standup-gh-data.ts
| const openPrNumbers = new Set( | ||
| (allOpenPrs as Array<{ number?: number }>) | ||
| .map((p) => p.number) | ||
| .filter((n): n is number => typeof n === 'number'), | ||
| ); | ||
|
|
||
| const addComment = ( | ||
| num: number, | ||
| kind: GroupedReply['kind'], | ||
| c: GhComment, | ||
| fallbackUrl: string, | ||
| ): void => { | ||
| const author = c.user?.login; | ||
| if (!author) return; | ||
| if (ghHandle && author.toLowerCase() === ghHandle.toLowerCase()) return; | ||
| // Skip GitHub bots — coderabbitai, codex-connector, dependabot, etc. The | ||
| // "[bot]" suffix is the canonical GitHub convention for bot accounts and | ||
| // is reliable across all bot integrations. Maintainer wants human replies | ||
| // worth responding to, not the constant churn of automated review tooling. | ||
| if (author.endsWith('[bot]')) return; | ||
| if (!repliesByNumber[num]) repliesByNumber[num] = { number: num, kind, comments: [] }; | ||
| // Upgrade kind toward pr_review (most actionable) when both arrive on the same PR. | ||
| if (kind === 'pr_review') repliesByNumber[num].kind = 'pr_review'; | ||
| repliesByNumber[num].comments.push({ | ||
| author, | ||
| created_at: c.created_at ?? '', | ||
| body_excerpt: (c.body ?? '').slice(0, 240).replace(/\s+/g, ' ').trim(), | ||
| url: c.html_url ?? fallbackUrl, | ||
| }); | ||
| }; | ||
|
|
||
| // /issues/comments covers PR + issue conversations under one endpoint. | ||
| // Disambiguate by checking whether the parsed number is an open PR. | ||
| const issueComments = parseJson<GhComment[]>( | ||
| exec('gh', [ | ||
| 'api', | ||
| `repos/${repoIds.owner}/${repoIds.repo}/issues/comments?since=${lastRunAt}&per_page=100`, | ||
| '--paginate', | ||
| ]), | ||
| [], | ||
| ); | ||
| for (const c of issueComments) { | ||
| const num = extractNumber(c.issue_url); | ||
| if (!num) continue; | ||
| const kind: GroupedReply['kind'] = openPrNumbers.has(num) ? 'pr_conversation' : 'issue'; | ||
| addComment(num, kind, c, c.issue_url ?? ''); | ||
| } |
There was a problem hiding this comment.
Closed PR replies will be mislabeled as issues.
openPrNumbers only covers currently open PRs, so a reply on a PR that was closed after the last run will fall through to kind: 'issue'. That drops the PR context from the new "Replies waiting on you" section exactly when the conversation is still actionable.
🔧 Suggested fix
- const openPrNumbers = new Set(
- (allOpenPrs as Array<{ number?: number }>)
- .map((p) => p.number)
- .filter((n): n is number => typeof n === 'number'),
- );
+ const prNumbers = new Set<number>();
+ // Populate this from a source that includes closed PRs too, not just open ones.
@@
- const kind: GroupedReply['kind'] = openPrNumbers.has(num) ? 'pr_conversation' : 'issue';
+ const kind: GroupedReply['kind'] = prNumbers.has(num) ? 'pr_conversation' : 'issue';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.archon/scripts/maintainer-standup-gh-data.ts around lines 239 - 285, The
code currently labels comments as 'pr_conversation' only if the PR is currently
open (openPrNumbers), which mislabels replies on PRs closed since last run;
instead, for each comment determine whether the issue number is a pull request
by fetching the issue metadata (e.g., call GH API for
repos/{owner}/{repo}/issues/{num}) and check for the pull_request field, then
set kind to 'pr_conversation' when pull_request exists (otherwise 'issue');
update the loop that processes issueComments to call this check (referencing
extractNumber, issueComments and addComment) and fall back to the openPrNumbers
check only if you want to avoid extra API calls.
| const repliesSinceLastRun = Object.values(repliesByNumber).sort((a, b) => { | ||
| const aLatest = a.comments[a.comments.length - 1]?.created_at ?? ''; | ||
| const bLatest = b.comments[b.comments.length - 1]?.created_at ?? ''; | ||
| return bLatest.localeCompare(aLatest); // newest first |
There was a problem hiding this comment.
Sort each group by created_at before deriving recency.
comments is built by appending issue comments first and review comments later, so comments[comments.length - 1] is not guaranteed to be the newest reply. That can misorder the final section and surface an old excerpt as the "latest" comment.
🛠️ Suggested fix
-const repliesSinceLastRun = Object.values(repliesByNumber).sort((a, b) => {
- const aLatest = a.comments[a.comments.length - 1]?.created_at ?? '';
- const bLatest = b.comments[b.comments.length - 1]?.created_at ?? '';
+const repliesSinceLastRun = Object.values(repliesByNumber)
+ .map((reply) => ({
+ ...reply,
+ comments: [...reply.comments].sort((a, b) => a.created_at.localeCompare(b.created_at)),
+ }))
+ .sort((a, b) => {
+ const aLatest = a.comments[a.comments.length - 1]?.created_at ?? '';
+ const bLatest = b.comments[b.comments.length - 1]?.created_at ?? '';
return bLatest.localeCompare(aLatest); // newest first
-});
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const repliesSinceLastRun = Object.values(repliesByNumber).sort((a, b) => { | |
| const aLatest = a.comments[a.comments.length - 1]?.created_at ?? ''; | |
| const bLatest = b.comments[b.comments.length - 1]?.created_at ?? ''; | |
| return bLatest.localeCompare(aLatest); // newest first | |
| const repliesSinceLastRun = Object.values(repliesByNumber) | |
| .map((reply) => ({ | |
| ...reply, | |
| comments: [...reply.comments].sort((a, b) => a.created_at.localeCompare(b.created_at)), | |
| })) | |
| .sort((a, b) => { | |
| const aLatest = a.comments[a.comments.length - 1]?.created_at ?? ''; | |
| const bLatest = b.comments[b.comments.length - 1]?.created_at ?? ''; | |
| return bLatest.localeCompare(aLatest); // newest first | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.archon/scripts/maintainer-standup-gh-data.ts around lines 304 - 307, The
sort is using each group's last array element as the "latest"
(repliesSinceLastRun) but comments arrays are appended in mixed order; instead,
for each group in repliesByNumber compute the actual newest created_at by
sorting that group's comments by comment.created_at (or scanning to find the max
created_at) and use that value for the comparison. Update the comparator that
builds repliesSinceLastRun to derive aLatest and bLatest from the sorted/max
created_at of a.comments and b.comments (reference: repliesByNumber,
repliesSinceLastRun, and the comments[].created_at field).
Summary
/repos/{o}/{r}/issues/commentsfor PR + issue conversations and/repos/{o}/{r}/pulls/commentsfor inline review comments), scoped bysince=last_run_at. Filters out the maintainer's own comments and all[bot]-suffixed authors. Groups by PR/issue number with a kind tag (issue/pr_conversation/pr_review). Plumbed through to a new "Replies waiting on you" section in the brief.Validation Evidence (required)
12 distinct human authors across 22 PRs/issues — exactly the contributors I'd want to follow up with after yesterday's review comments and direction questions. Pre-bot-filter, the count was 32 (10 of those were bots: coderabbitai, chatgpt-codex-connector, dependabot review noise).
Security Impact (required)
gh apipaginated GETs against the existing repo (/issues/commentsand/pulls/comments). Both already-allowed scopes via existingghauth. No new tokens, no new endpoints outside whatghalready accesses.Compatibility / Migration
replies_since_last_runfield is additive on the gather script's JSON output; existing consumers (synthesizer prompt) ignore unknown fields by design. First-run behavior is unchanged: the gather skips the API calls entirely whenlast_run_atis missing, returning an empty array.Human Verification (required)
state.jsonis missing or has nolast_run_at, the new block is skipped andreplies_since_last_run: []is emitted. No API calls fired..archon/workflows/maintainer/.originremote:ownerRepo()returnsnull, the new block is skipped. No crash.extractNumberregex matches both/issues/N$and/pulls/N$; cross-repo URLs would still match by number, but the API only returns this repo's comments so this isn't reachable in practice.last_run_atinvalid/empty: skipped by theif (repoIds && lastRunAt)guard.--paginateshould handle it but pagination cost grows linearly. Not a concern for this repo's volume; could be revisited if a large repo adopts the workflow.Side Effects / Blast Radius (required)
maintainer-standupworkflow's gather script + command file. No engine code touched.gh apicalls per standup run (one paginated each for issue/pull comments). At 100 comments per page, that's typically 1 page each. ~1-2 sec added to the gather phase — well within the gather's existing ~5-10s budget..archon/), the brief degrades to its prior shape. No breakage path.Rollback Plan (required)
git revert <merge-sha>— single commit, two files, fully additive. The new gather field disappears; the brief's new section disappears. No data migration needed.gh apicall fails (auth, network),parseJsonreturns[]andreplies_since_last_runis empty — degraded to silent rather than fatal. Worst case: the brief's new section is empty even when replies exist; visible in normal review.Risks and Mitigations
pr_review) first since they need code-level responses. If the section grows unwieldy in practice, easy follow-up to add a top-N cap (e.g. 10 most recent).endsWith('[bot]')) might miss a non-conventional bot account (e.g. one that doesn't use the[bot]suffix).user.type === 'User'if the GraphQL/v3 endpoint returned that field on /comments responses (it doesn't currently — would require a separate /users lookup).Linked Issue
Summary by CodeRabbit