Skip to content

feat(maintainer-standup): surface contributor replies since last run#1457

Merged
Wirasm merged 1 commit intodevfrom
feat/standup-replies-section
Apr 28, 2026
Merged

feat(maintainer-standup): surface contributor replies since last run#1457
Wirasm merged 1 commit intodevfrom
feat/standup-replies-section

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Apr 28, 2026

Summary

  • Problem: Daily maintainer-standup briefs surface a lot of signal — open PRs by priority, what shipped, what's resolved — but they don't surface who replied to me since the last run. With ~30 active PRs and ongoing back-and-forth on direction questions, contributor replies got lost in the volume; the brief told me "PR #X was updated" but not "@gemmawood answered your question on PR #X."
  • Why it matters: Contributor responses are the highest-priority maintainer action — somebody is waiting on me. Burying them in the same field as label changes / force-pushes / CI-bot churn means real conversations get dropped on the floor.
  • What changed: Two new paginated GitHub API calls in the gather script (/repos/{o}/{r}/issues/comments for PR + issue conversations and /repos/{o}/{r}/pulls/comments for inline review comments), scoped by since=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.
  • What did NOT change (scope boundary): No engine changes. No new YAML schema. Other maintainer-standup outputs (P1-P4, "What you shipped", "Resolved since last run") are unchanged. Other workflows (maintainer-review-pr, repo-triage) untouched.

Validation Evidence (required)

archon validate workflows maintainer-standup     # → ok
bun run format:check                             # → clean
bun run lint                                     # → clean
bun run type-check                               # → clean across all 10 packages

# Manual test on a backdated last_run_at (proving the data flow):
$ jq '.last_run_at = "2026-04-27T08:00:00Z"' state.json > tmp && mv tmp state.json
$ bun run .archon/scripts/maintainer-standup-gh-data.ts | jq '.replies_since_last_run | length'
22
$ ... | jq '.replies_since_last_run | map(.comments[].author) | unique'
["atlas-architect", "b1skit", "cropse", "ericsoriano", "fuleinist", "gemmawood",
 "leex279", "lraphael", "popemkt", "shaun0927", "truffle-dev", "voidborne-d"]

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)

  • New permissions/capabilities? No.
  • New external network calls? Yes — two gh api paginated GETs against the existing repo (/issues/comments and /pulls/comments). Both already-allowed scopes via existing gh auth. No new tokens, no new endpoints outside what gh already accesses.
  • Secrets/tokens handling? Unchanged.
  • File system access scope? Unchanged.

Compatibility / Migration

  • Backward compatible? Yes. The new replies_since_last_run field 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 when last_run_at is missing, returning an empty array.
  • Config/env changes? No.
  • Database migration? No.

Human Verification (required)

Side Effects / Blast Radius (required)

  • Affected subsystems: only maintainer-standup workflow's gather script + command file. No engine code touched.
  • Potential unintended effects: an additional ~2 gh api calls 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.
  • Guardrails: the new field is additive; if synthesizer doesn't use it (e.g. older command file checked into another repo's .archon/), the brief degrades to its prior shape. No breakage path.

Rollback Plan (required)

  • Fast rollback: 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.
  • Feature flags: none.
  • Observable failure symptoms: if either gh api call fails (auth, network), parseJson returns [] and replies_since_last_run is 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

  • Risk: the brief's new section is too long for high-activity days, drowning out P1-P4 prioritization.
    • Mitigation: synthesizer prompt instructs to keep one line per reply, sort by recency, and surface inline-review-comment kinds (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).
  • Risk: bot filter (endsWith('[bot]')) might miss a non-conventional bot account (e.g. one that doesn't use the [bot] suffix).
    • Mitigation: GitHub's convention is reliable for App-installed bots. Edge cases (a human account that posts review-tooling output) would surface as noise but is rare. Could be tightened later by checking user.type === 'User' if the GraphQL/v3 endpoint returned that field on /comments responses (it doesn't currently — would require a separate /users lookup).
  • Risk: the maintainer doesn't actually want EVERY reply — sometimes a "thanks!" comment is just acknowledgement and doesn't need response.
    • Mitigation: synthesizer can mark such replies as informational rather than action items based on body content. Not a parser concern.

Linked Issue

Summary by CodeRabbit

  • New Features
    • Added a new "Replies waiting on you" section to standup reports that displays all contributor responses on your pull requests and issues since the last run. Includes author attribution, reply counts, comment excerpts, and direct links. Your own comments and bot interactions are automatically filtered out for cleaner, more actionable reporting.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

This PR adds a "Replies waiting on you" feature to the maintainer standup command by introducing a new replies_since_last_run data field. The TypeScript data-gathering script now fetches contributor comments on PRs and issues since the last run, filters out maintainer and bot-authored comments, groups them by PR/issue number, and surfaces them in the standup template.

Changes

Cohort / File(s) Summary
Data Contract & Template
.archon/commands/maintainer-standup.md
Added replies_since_last_run field to gh-data.output contract and introduced new "Replies waiting on you" section in brief template with PR/issue entries, comment counts, excerpts, and links.
Data Collection Logic
.archon/scripts/maintainer-standup-gh-data.ts
Implemented comment fetching from GitHub /issues/comments and /pulls/comments endpoints since last run, filtering by non-maintainer and non-bot authors, grouping by PR/issue number with sorting (newest first), and adding grouped replies to output JSON.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through GitHub's replies,
Gathering comments beneath the skies,
Filters out bots with a swift little bound,
Groups them by PRs—new discussions found!
"Waiting on you" the standup now cries! 📬

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: surfacing contributor replies since the last run, which is the main objective of the pull request.
Description check ✅ Passed The description is comprehensive and covers all required template sections: summary with problem/why/what changed/scope boundaries, validation evidence with commands and test results, security impact analysis, backward compatibility confirmation, human verification with specific scenarios, side effects assessment, rollback plan, and risks with mitigations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/standup-replies-section

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts uses comments[].author, comments[].created_at, comments[].body_excerpt, and comments[].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

📥 Commits

Reviewing files that changed from the base of the PR and between 287bb35 and 3198731.

📒 Files selected for processing (2)
  • .archon/commands/maintainer-standup.md
  • .archon/scripts/maintainer-standup-gh-data.ts

Comment on lines +239 to +285
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 ?? '');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +304 to +307
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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).

@Wirasm Wirasm merged commit 6cf9883 into dev Apr 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant