From 431a27e9334a376229e398423f58c39dfec85daa Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:19:46 -0400 Subject: [PATCH 1/3] fix(source-control): emit in_reply_to_id for threaded inline PR review replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch-all-pr-comments.sh never projected the GitHub REST field that marks an inline review comment as a threaded reply, so callers reading the script's output saw the key absent even for properly-threaded replies GraphQL confirmed were linked. Reproduced against live PR #563 data: the raw pulls//comments response correctly carries in_reply_to_id — the script's jq mapping for the inline surface simply dropped it. Adds in_reply_to_id to all three surfaces (sourced from the raw field for inline comments; null for general/review, which have no reply-parent concept). Additive schema change, no breaking impact on existing consumers. Closes #587 Co-Authored-By: Claude Sonnet 5 --- .../source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 16 ++++++++++++++ .../scripts/fetch-all-pr-comments.sh | 17 +++++++++++---- .../scripts/fetch-all-pr-comments.test.sh | 21 ++++++++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 22a43617c..4453decff 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.26.2", + "version": "0.26.3", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and merge-rung raises binding from the team-tracked layer only), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 126931f3f..011906798 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.26.3] + +### Fixed + +- **`fetch-all-pr-comments.sh` now emits `in_reply_to_id` for inline review comments (#587).** The + script's unified schema never projected the GitHub REST field that marks an inline review comment + as a threaded reply, so any caller reading the script's own output saw the key absent (surfacing as + `None`/`null` in downstream tooling) even for comments GraphQL confirmed were properly threaded + replies. Reproduced against live PR #563 data: the raw `pulls//comments` response correctly + carries `in_reply_to_id` on reply comments — the script's `jq` projection for the inline surface + simply dropped it. Added `in_reply_to_id: .in_reply_to_id` to the inline mapping (sourced from the + same raw field GraphQL cross-checks against) and `in_reply_to_id: null` to the general/review + mappings, which have no reply-parent concept on their surfaces. Additive schema change — existing + consumers that don't read the new key are unaffected. Regression-tested with a threaded-reply + fixture. + ## [0.26.2] ### Fixed diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.sh b/plugins/source-control/scripts/fetch-all-pr-comments.sh index d92a648f5..7d68a10fb 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.sh @@ -8,7 +8,13 @@ # 3. Inline review comments (line-anchored on the diff) # # Output: unified JSON array sorted by creation date. Each object: -# {"id":N,"type":"general|review|inline","author":"login","body":"...","path":"...","line":N,"created_at":"ISO"} +# {"id":N,"type":"general|review|inline","author":"login","body":"...","path":"...","line":N,"created_at":"ISO","in_reply_to_id":N|null} +# +# in_reply_to_id is populated ONLY for type "inline" (the only GitHub REST +# surface that threads via this field — general/review comments have no +# reply-parent concept and always carry null). A non-null value is the id of +# the inline comment this one replies to; use it to confirm thread membership +# instead of a raw `gh api pulls//comments` cross-check. # # Usage: # fetch-all-pr-comments.sh @@ -118,7 +124,8 @@ if ! GENERAL=$(printf '%s' "$GENERAL_RAW" | jq -c ' body: .body, path: null, line: null, - created_at: .created_at + created_at: .created_at, + in_reply_to_id: null } ' 2>/dev/null); then printf 'fetch-all-pr-comments: jq failed parsing issues/%s/comments response\n' "$PR_NUMBER" >&2 @@ -142,7 +149,8 @@ if ! REVIEWS=$(printf '%s' "$REVIEWS_RAW" | jq -c ' body: .body, path: null, line: null, - created_at: (.submitted_at // .created_at) + created_at: (.submitted_at // .created_at), + in_reply_to_id: null } ' 2>/dev/null); then printf 'fetch-all-pr-comments: jq failed parsing pulls/%s/reviews response\n' "$PR_NUMBER" >&2 @@ -165,7 +173,8 @@ if ! INLINE=$(printf '%s' "$INLINE_RAW" | jq -c ' body: .body, path: .path, line: (.line // .original_line), - created_at: .created_at + created_at: .created_at, + in_reply_to_id: .in_reply_to_id } ' 2>/dev/null); then printf 'fetch-all-pr-comments: jq failed parsing pulls/%s/comments response\n' "$PR_NUMBER" >&2 diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.test.sh b/plugins/source-control/scripts/fetch-all-pr-comments.test.sh index 351a9937c..cc57e956a 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.test.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.test.sh @@ -68,15 +68,17 @@ cat >"$FIXTURE_INLINE" <<'JSON' "path": "src/Auth.cs", "line": 42, "original_line": null, + "in_reply_to_id": null, "created_at": "2026-05-20T09:30:00Z" }, { "id": 3002, - "user": {"login": "codex[bot]"}, + "user": {"login": "kyle-sexton"}, "body": "Stale count reference", "path": "src/Counter.cs", "line": null, "original_line": 15, + "in_reply_to_id": 3001, "created_at": "2026-05-20T12:00:00Z" } ] @@ -158,6 +160,23 @@ assert_eq "inline comments have path" "2" "$inline_with_path" fallback_line=$(printf '%s' "$out" | jq '[.[] | select(.id == 3002)] | .[0].line') assert_eq "original_line fallback" "15" "$fallback_line" +# Case 6a: a top-level inline comment reports in_reply_to_id null (regression +# guard for #587 — the field must be sourced from the raw API response, not +# silently dropped by the jq projection) +top_level_reply_id=$(printf '%s' "$out" | jq '[.[] | select(.id == 3001)] | .[0].in_reply_to_id') +assert_eq "top-level inline comment has null in_reply_to_id" "null" "$top_level_reply_id" + +# Case 6b: a threaded inline reply reports the true parent id +threaded_reply_id=$(printf '%s' "$out" | jq '[.[] | select(.id == 3002)] | .[0].in_reply_to_id') +assert_eq "threaded inline reply reports parent id" "3001" "$threaded_reply_id" + +# Case 6c: general and review comments (no reply-parent concept) carry null +general_reply_id=$(printf '%s' "$out" | jq '[.[] | select(.id == 1001)] | .[0].in_reply_to_id') +assert_eq "general comment has null in_reply_to_id" "null" "$general_reply_id" + +review_reply_id=$(printf '%s' "$out" | jq '[.[] | select(.id == 2001)] | .[0].in_reply_to_id') +assert_eq "review comment has null in_reply_to_id" "null" "$review_reply_id" + # Case 7: gh api failure exits 2 FAIL_STUB="$TEST_TMPDIR/bin-fail/gh" mkdir -p "$TEST_TMPDIR/bin-fail" From 43f488e7a0a3df31e4f5ddfe2db9b8e6298bb4a4 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:26:43 -0400 Subject: [PATCH 2/3] fix(source-control): correct in_reply_to_id docstring semantics + usage() range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from review of 431a27e9: - The in_reply_to_id docstring asserted "the id of the inline comment this one replies to" and steered callers away from the REST cross-check that reference/review-discipline.md and skills/pull-request/SKILL.md already prescribe. Live PR #563 data shows GitHub sets the field to the THREAD-OPENING comment id, not the immediately-preceding reply (every parent referenced in that thread set was itself a root comment) — the docstring now states that correctly and stops contradicting the two canonical reference docs, which were never wrong. - usage()'s `sed -n '2,18p'` range was written against the old (pre-fix) header length. Adding the in_reply_to_id block pushed the Usage/Env overrides sections past line 18 without updating the range, so --help silently truncated before showing them. Range corrected to 2,30p and verified against actual --help output. Co-Authored-By: Claude Sonnet 5 --- .../scripts/fetch-all-pr-comments.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.sh b/plugins/source-control/scripts/fetch-all-pr-comments.sh index 7d68a10fb..5353c4f26 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.sh @@ -12,9 +12,15 @@ # # in_reply_to_id is populated ONLY for type "inline" (the only GitHub REST # surface that threads via this field — general/review comments have no -# reply-parent concept and always carry null). A non-null value is the id of -# the inline comment this one replies to; use it to confirm thread membership -# instead of a raw `gh api pulls//comments` cross-check. +# reply-parent concept and always carry null). GitHub sets it to the id of +# the THREAD-OPENING comment, not the immediately-preceding reply: every +# reply in a thread carries the same in_reply_to_id (empirically verified +# against live PR #563 data — every referenced parent in that thread set was +# itself a root comment), so grouping by `in_reply_to_id // id` recovers +# thread membership directly. This does not change the documented REST +# cross-check in reference/review-discipline.md / skills/pull-request/SKILL.md +# — this script's schema was the only thing missing the field; the raw API +# was always correct. # # Usage: # fetch-all-pr-comments.sh @@ -36,7 +42,7 @@ set -uo pipefail # -e omitted: gh api failures explicitly guarded with || { exit PR_NUMBER="" usage() { - sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 } From 85912400c5b1d5c7238a68a7b536a4d1c72e50c7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:09:15 -0400 Subject: [PATCH 3/3] fix(source-control): drop the PR back-reference from the fetch-comments header The comment-hygiene gate flags a tracker/PR reference in a code comment, and the repo's convention reserves back-references for TODO(#issue). The empirical claim is what matters, not which pull request it was observed on, so the sentence now cites live review-thread data without naming one. --- plugins/source-control/scripts/fetch-all-pr-comments.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.sh b/plugins/source-control/scripts/fetch-all-pr-comments.sh index 0bf32bc90..525983723 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.sh @@ -15,7 +15,7 @@ # reply-parent concept and always carry null). GitHub sets it to the id of # the THREAD-OPENING comment, not the immediately-preceding reply: every # reply in a thread carries the same in_reply_to_id (empirically verified -# against live PR #563 data — every referenced parent in that thread set was +# against live review-thread data — every referenced parent in that set was # itself a root comment), so grouping by `in_reply_to_id // id` recovers # thread membership directly. This does not change the documented REST # cross-check in reference/review-discipline.md / skills/pull-request/SKILL.md