diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 8d90af1e4..3b832121f 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.10", + "version": "0.26.11", "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 272df6f6e..ded2620f0 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.11] + +### 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.10] ### Changed diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.sh b/plugins/source-control/scripts/fetch-all-pr-comments.sh index 7a3abd90a..525983723 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.sh @@ -8,7 +8,19 @@ # 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). 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 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 +# — this script's schema was the only thing missing the field; the raw API +# was always correct. # # Usage: # fetch-all-pr-comments.sh @@ -39,7 +51,7 @@ set -uo pipefail # -e omitted: gh api failures explicitly guarded with || { exit PR_NUMBER="" usage() { - sed -n '2,27p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + sed -n '2,39p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 } @@ -127,7 +139,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 @@ -151,7 +164,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 @@ -174,7 +188,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 983103b58..2582d9e09 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"