Skip to content

fix(work-items): scope GitHub sub-issues by url, not the absent repository field - #3830

Merged
kyle-sexton merged 5 commits into
mainfrom
claude/3825-work-item-tracker
Sep 6, 2026
Merged

kyle-sexton merged 5 commits into
mainfrom
claude/3825-work-item-tracker

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #3825

Summary

work-item-tracker.sh list-sub-items <container> returned an empty list on GitHub even when the
container had native sub-issues, which also blinded list-frontier --parent, container rollup,
and /work-items:ship status.

The GitHub adapter asked gh issue view --json subIssues for the child numbers and then kept only
the nodes whose .repository.nameWithOwner equalled the parent's repo. That field is never there.
gh's GraphQL query does request subIssues(first:100){nodes{id,number,title,url,state,repository{nameWithOwner}}},
but its export path then projects each node down to id, number, title, url, state and
drops the repository object. The predicate compared null against owner/repo for every node,
the number set came back empty, and the verb short-circuited to {"items": []}. parent,
blockedBy and blocking are projected the same way, so the same trap exists for any future
filter on those.

Fix

adapters/github/list-sub-items.sh derives the node's repo from a field gh actually emits:

  • repository.nameWithOwner still wins where a gh build does emit it, so nothing regresses if the
    projection is widened later or the JSON comes from a raw GraphQL read.
  • Otherwise owner/repo come from the node's url, which is <host>/<owner>/<repo>/issues/<n>:
    the two path segments before issues.
  • A node attributable to neither is dropped, exactly as before. The cross-repo guard matters
    because the second read intersects by issue NUMBER against this repo's list-items output, so a
    foreign sub-issue sharing a local number would otherwise pull in an unrelated item.

Nothing else changed: same two reads, same normalized envelope, same re-parenting, same truncation
bound. lib/frontier.sh and lib/labels.sh are untouched; list-frontier --parent routes through
this verb and is fixed by it.

The adapter README gains a Gotchas bullet recording the projection, the workaround, and the gh
version the fix was checked against.

The underivable drop is no longer silent

Two different drops happen in that predicate, and only one of them is expected:

  • A node resolving to another repo is out of scope for this number-keyed intersect. It stays a
    silent drop, because that is the behaviour CONTRACT.md "Adapter contract" already documents:
    "A child in another repo is out of scope for this repo-keyed intersect (documented truncation,
    not an error)."
    Sub-issues genuinely can be cross-repo, so this is the guard, not a defect.
  • A node resolving to no repo at all means neither field parsed. That is the exact shape of
    work-items/tracker: list-sub-items returns empty because gh --json subIssues nodes carry no repository field #3825: an empty list and no signal. Exposure is low today, since every node gh emits carries a
    url, but a future narrowing of the projection would silently re-blind every lane in precisely
    the way this PR exists to fix.

The second case now emits a one-line note on stderr naming the offending node numbers. stdout stays
the machine-parseable envelope, so nothing downstream has to change. The note cannot fire on
well-formed input, and a test asserts that (see Verification).

This deliberately does not fail open. Treating an unattributable node as same-repo would defeat
the number-keyed intersect and pull an unrelated same-numbered local issue into the rollup. The drop
is kept; only its invisibility is fixed.

Verification

All foreground, from /home/user/wt-3825, on this branch.

  • The test that fails on the old predicate: list-sub-items over stubbed subIssues in
    adapters/github/list-sub-items.test.sh, specifically the cases
    url-derived filter keeps the same-repo child and
    child row is re-parented to the container. It stubs gh with the real
    --json subIssues shape (no repository key) plus a genuine cross-repo node, x/y#12, whose
    number also exists in the local repo. Reverting only list-sub-items.sh to main and rerunning
    the suite gives:
    FAIL: [8] url-derived filter keeps the same-repo child - expected github:o/r#11 got '' and
    FAIL: [9] child row is re-parented to the container - expected github:o/r#99 got null,
    suite exit 1. With the fix, 14/14 pass, exit 0. Node x/y#12 stays excluded either way, which is
    what proves the same-repo guard survived the change.
  • The stderr diagnostic is exercised, not merely added. Two cases:
    • well-formed nodes emit no derivable-repo warning runs the existing well-formed stub
      (including its cross-repo node) and asserts stderr carries no note. This is the anti-noise
      guard: the documented cross-repo truncation must stay silent.
    • A second stub serves one node with no url at all and one whose url is not an issue path.
      stderr names both unattributable nodes on one line asserts the single note names both
      numbers, unattributable nodes dropped, the attributable one kept asserts the third node still
      rolls up, and stdout stays machine-parseable, diagnostic did not leak into it asserts the
      envelope still parses.
    • Mutation-checked: suppressing the printf turns
      FAIL: [14] stderr names both unattributable nodes on one line - expected 1 got 0, so the
      assertion is load-bearing rather than vacuous.
  • scripts/affected-tests.sh --run -> All 13 selected suites passed or were skipped., exit 0.
  • Every suite under plugins/work-items/tools/work-item-tracker/ run directly: 58 suites,
    0 failures.
  • scripts/check-changelog-parity.sh --check -> exit 0.
  • scripts/check-changelog-parity.sh --check-bump origin/main -> exit 0.
  • scripts/check-changelog-parity.sh --check-preserved origin/main -> exit 0
    (177 headings compared).
  • scripts/check-changelog-parity.sh --check-order -> exit 0 (90 changelogs).
  • shellcheck -x list-sub-items.sh list-sub-items.test.sh -> exit 0.
  • shfmt -d list-sub-items.sh list-sub-items.test.sh -> exit 0, no diff.
  • markdownlint-cli2 on the changed CHANGELOG and README -> 0 issues.

The gh projection claim is source-verified, not asserted from memory: api/query_builder.go and
api/export_pr.go were read at tags v2.94.0, v2.95.0, v2.96.0, v2.97.0 and v2.98.0. The query
requests repository{nameWithOwner} at every one of them and the export drops it at every one of
them.

Live evidence, ported from #3832 (not produced on this branch)

The gh on this runner is 2.45.0, which predates subIssues entirely, so the acceptance criteria
naming live issue numbers could not be exercised here. They were exercised on a gh 2.97.0 host
by the duplicate PR #3832, and the results below are quoted from that PR's body. This is another
PR's run, not this branch's validation.
It is reproduced here because both PRs derive the repo
from the node url by the same rule, and on this input every node's url parses and resolves to
the same repo, so the two implementations produce identical output over it. Nothing below was
re-run on this branch, and no number here is restated beyond what #3832 shows.

#3832 reports gh --version as gh version 2.97.0 (2026-07-31), and
gh issue view 3799 --repo melodic-software/claude-code-plugins --json subIssues returning nodes
that carry id, number, state, title, url and no repository, e.g.
{"id":"I_kwDOTCGFQM8AAAABP7GlVw","number":3805,"state":"OPEN","title":"planning: plan the typed-ticket-body lane","url":"https://github.com/melodic-software/claude-code-plugins/issues/3805"}
with "totalCount":6. The same selection via gh api graphql, which asks for
repository { nameWithOwner } explicitly, does carry it, returning "totalCount":6 and node 3805
with "nameWithOwner":"melodic-software/claude-code-plugins". That is the live confirmation of the
projection asymmetry this PR diagnoses from source.

Against the acceptance criteria, #3832 reports list-sub-items on
github:melodic-software/claude-code-plugins#3799 returning all six sub-issues (#3824, #3823,
#3822, #3821, #3814, #3805), each with parent_id set to #3799 and blocked_by_count 1 except
#3805 at 0, where the pre-fix output was {"schema_version":"1.0","items":[]}; and
list-frontier --parent on the same container returning exactly
["github:melodic-software/claude-code-plugins#3805"], #3805 being the only child with zero open
blockers and no assignee.

A spot check on a 2.94+ host against this branch is still the honest confirmation, and is not
claimed here.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01ViPsHkL3ng9xWt2GjEQJob


Generated by Claude Code

…itory field

`gh issue view --json subIssues` projects each node down to id/number/title/
url/state and drops the `repository` object its own GraphQL query asks for.
The adapter filtered same-repo children on `.repository.nameWithOwner`, which
is never present there, so the intersect was always empty: every container
enumerated as childless, and `list-frontier --parent` and container rollup
saw nothing.

Derive owner/repo from the node `url` instead (`<host>/<owner>/<repo>/issues/
<n>`), still preferring `repository.nameWithOwner` where a gh build emits it,
and still dropping a node attributable to neither. The offline suite now stubs
gh with the real projection, including a cross-repo node whose number also
exists locally, so the same-repo filter stays proven.

Verified against the gh export path from 2.94.0 through 2.98.0; recorded in
the adapter README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViPsHkL3ng9xWt2GjEQJob
`list-sub-items` derives each sub-issue node's repo from `url`, falling
back to `repository.nameWithOwner`, and drops any node that resolves to
neither. That drop was silent, which is the same shape as the bug this
branch exists to fix: a narrowing of gh's `--json subIssues` projection
would blind every lane again with an empty list and no signal.

The node now gets named on stderr when neither field parses. Only that
case. A node resolving to a DIFFERENT repo stays a silent drop, because
sub-issues can be cross-repo and the second read intersects by issue
NUMBER against this repo's `list-items` output, so a foreign child is
out of scope by design (CONTRACT.md "Adapter contract" documents it as a
truncation, not an error). Treating an unattributable node as same-repo
instead would fail open and pull an unrelated same-numbered item into
the rollup.

stdout keeps the machine-parseable envelope; the note goes to stderr
only, and it cannot fire on well-formed input, since every node gh emits
carries a `url`.

Two test cases cover it: a well-formed node set asserts stderr stays
quiet (so the diagnostic is not noise on every read), and a stub serving
one node with no `url` plus one whose `url` is not an issue path asserts
both numbers are named on a single stderr line, the attributable sibling
still rolls up, and stdout still parses. Suppressing the printf fails
the second assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViPsHkL3ng9xWt2GjEQJob
@kyle-sexton
kyle-sexton marked this pull request as ready for review September 6, 2026 09:01
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T09:12:14.525421Z ce0289e Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 18s —— View job


Code review — PR #3830

  • Gather context (diff against origin/main, changed files, CONTRACT.md)
  • Review list-sub-items.sh fix logic (jq node_repo, unattributed-diagnostic path)
  • Review list-sub-items.test.sh new stub cases
  • Review README/CHANGELOG/plugin.json updates
  • Post findings

Scope: /review:code-review lane — correctness and maintainability only (no security lane exists on this repo's workflow set, but nothing here is security-relevant regardless).

Findings: none that clear the high-signal bar.

What I checked in list-sub-items.sh:

  • node_repo's URL-slice math ($seg[-4:-2] after confirming $seg[-2] == "issues" and length >= 5) correctly extracts owner/repo from <host>/<owner>/<repo>/issues/<n> for both scheme-qualified and bare-host URLs, and the length >= 5 guard prevents an out-of-range slice on short input — verified by tracing the segment counts by hand rather than assuming.
  • The two drop paths stay correctly separated: a node resolving to a different repo (r != $repo, r != null) is excluded from nums and from unattributed alike, so the documented cross-repo truncation (CONTRACT.md "Adapter contract") stays silent, matching the x/y#12-style stub case.
  • The .repository.nameWithOwner? // null alternative still wins when a build does emit it, so the fallback doesn't regress the case the field exists.
  • The new stderr diagnostic reads only from the unattributed set and can't fire on the well-formed stub — confirmed by reading the two test stubs side by side (one all-url, one with a missing url and a non-issue url) and matching each against node_repo's branches by hand.
  • Message-string cross-check: the printf in the script and the grep assertions in the test agree on wording (no derivable repo (number: 22, 23)), so the assertions are anchored to the real format string, not a paraphrase.
  • README/CHANGELOG/plugin.json version bump are consistent with the code change and with each other.

Not independently verified: I did not execute the test suite, shellcheck, shfmt, or scripts/affected-tests.sh myself — this lane's tool grant covers gh pr diff/view/comment/review plus Read/Glob/Grep, not general command execution. Everything in the PR body's "Verification" section (14/14 tests, shellcheck/shfmt clean, changelog-parity checks) is the author's own reported output, not something I ran — flagging per this lane's rule against restating unverified claims as confirmed.
·

Copy link
Copy Markdown
Contributor Author

Independent review and the fixes it produced — flipped to ready

Posted by the autonomous worker lane (vm-worker-202609050257). Commit ce0289e4. This flip is the lane's handoff; the merge lane owns it from here. This lane does not merge.

The bug, verified at source

list-sub-items.sh filtered sub-issues on .repository.nameWithOwner, which gh issue view --json subIssues never emits. Confirmed independently: api/query_builder.go:450 requests repository{nameWithOwner}, but api/export_pr.go is byte-identical (same md5) across v2.94.0 through v2.98.0 and its subIssues case emits only id, number, title, url and state. The LinkedIssue struct holds Repository; the exporter never copies it. Only raw gh api graphql returns it.

Consequence: every container read as childless, so list-frontier --parent and rollup returned nothing. Any lane reading through the tracker could not distinguish "no children" from "the read failed".

Verified by the reviewer

Check Result
URL parser Tested against github.com, a GHE host, GHE with a path prefix, dotted/hyphenated/underscored and leading-digit owner and repo names, http://, scheme-less, uppercase, and an owner literally named issues — all correct, tail-anchored
Re-parenting Safe — parent_id comes from the single $id argument after a number-keyed intersect; no path yields a different parent
Fixtures Faithful to the exporter's real key set and to list-items.sh:37's exact --json list, not shaped to pass
Revert probe Reproduces FAIL: [8] and FAIL: [9], suite exit 1, versus 9/9 with the fix
Cross-repo children Intended exclusion per CONTRACT.md:237-238, unchanged by this PR
Fences Clean of #3745's lib/frontier.sh/lib/labels.sh, #3740, #3783

Fix: a silent drop that reproduced the bug class

A node whose repo could not be derived was dropped by an else null branch with no diagnostic — the same "childless container with no signal" failure this PR exists to fix, just moved. It now emits one stderr line naming the unattributable node numbers and pointing at a possible projection change, with stdout untouched.

Four cases cover it, including a negative one: the original well-formed stub (cross-repo node included) asserts stderr stays quiet, so the warning cannot fire spuriously. Mutation-checked — suppressing the printf fails the suite.

The fail-open was deliberately not adopted. #3832's premise that gh scopes subIssues to the parent repo is false; sub-issues can be cross-repo, and failing open would defeat the number-keyed intersect that stops a foreign same-numbered child being mis-attributed.

Live evidence, ported with attribution

This runner has gh 2.45.0, which predates subIssues, so the acceptance criteria naming live issue numbers were never exercised here. #3832 ran them on a gh 2.97.0 host, and that evidence is now in the Verification section under a heading that says up front it is another PR's run, not this branch's validation, with a note on why it transfers and an explicit statement that a 2.94+ spot check against this branch is still not claimed.

Duplicate PR

#3832 fixes the same defect the same way off the same base SHA and touches the same three files; whichever merges second conflicts on all three. #3830 is canonical by the lowest-number convention this lane follows — a tiebreak, not a quality judgment, and the repo owner's call to overrule. #3832 bumps no version and adds no changelog, so the parity gate will fail it against a moved main. It has been commented on, never modified.

Gates: affected-tests.sh --run exit 0 (13 suites); every tracker suite 58/58; list-sub-items.test.sh 14/14; four parity modes exit 0 (177 headings preserved, 90 changelogs ordered); shellcheck -x and shfmt -d clean; markdownlint 0 issues.


Generated by Claude Code

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 44s —— View job


Security review complete

  • Gather context (PR diff, changed files)
  • Run skip gate (has security-relevant surface: this diff touches the guard that prevents a cross-repo sub-issue number from colliding with an unrelated same-numbered local issue, so a full review proceeded rather than skipping)
  • Review changed files for security-relevant surface
  • Post findings or clear result

No security issues found.

Scope: list-sub-items.sh, its test file, and the README/CHANGELOG/plugin.json edits.

What I specifically checked, given this changes a repo-scoping guard:

  • Cross-repo confusion is still prevented. The url-derived node_repo (list-sub-items.sh#L65-L78) is only used to compute child_nums, the set of numbers already scoped to target_repo. The second read (list-items.sh --repo "$target_repo") is independently scoped by the already-validated $target_repo from wit_require_github_id, so a node the new logic misattributes to the parent's repo is the only way to reintroduce the collision the PR exists to prevent — and the parsing derives owner/repo relative to the issues segment from the end of the path ($seg[-4:-2]), so it's robust to GHES installs with extra path prefixes rather than relying on a fixed absolute index.
  • The url field isn't attacker-forgeable input in a way that matters here. It's emitted by GitHub's own GraphQL export for the actual sub-issue, not user-supplied text, so there's no injection path through it into the split("/") / join("/") jq logic (pure jq, no shell interpolation of node content).
  • No injection in the new shell code. jq calls use --arg/--argjson throughout; the new stderr diagnostic (list-sub-items.sh#L82-L85) uses printf '%s' with issue numbers as arguments, not a user-controlled format string, and the numbers themselves originate from GitHub's API, not free-form input.
  • No sensitive data exposure. The new stderr note names only issue numbers and the script's own basename; nothing token/credential-shaped reaches stdout or stderr.
  • Test additions (list-sub-items.test.sh) stub gh with a local, offline script under mktemp -d — no network calls, no secrets, no eval of untrusted content.

Nothing here needed an inline comment since there's nothing to anchor.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Last security-reviewed head: ce0289e4ffd491c9f4b0d43b899248129a479727. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce0289e4ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/work-items/tools/work-item-tracker/adapters/github/list-sub-items.sh Outdated

Copy link
Copy Markdown
Contributor Author

Merge lane claim — canonical for #3825, resolving the conflict

Claimed at head ce0289e4ffd491c9f4b0d43b899248129a479727 by ccr-session-babysit-loop-20260906. No foreign activity in the preceding 30 minutes; untouched for roughly 9 hours.

Duplicate resolved

#3832 fixed the same #3825 defect by the same url-derivation approach and self-flagged the collision at 08:44:28Z, naming this PR canonical under the lowest-number tiebreak. I verified that claim against both diffs before acting on it: the overlap is five shared files, which is every file this PR touches, not the three its notice estimated. #3832 is now closed as superseded.

Its notice records an obligation that survives the closure: #3832's live gh 2.97.0 probe with GraphQL evidence and acceptance-criteria output is the better verification artifact, and porting it here must carry attribution to #3832.

What is actually holding this PR

mergeable_state: dirty. This PR conflicts with main, which moved after the branch was cut. CI on this head is otherwise green and there are no open review threads. Resolving that conflict is the whole of the remaining branch-owned work, and this lane is doing it now in an isolated worktree, recovering both sides' intent from history rather than taking either side wholesale.


Merge lane ccr-session-babysit-loop-20260906.


Generated by Claude Code

Both sides independently claimed 0.39.66 off 0.39.65. main shipped it
first with the /writing:be-concise pointers, so main's entry keeps
0.39.66 unchanged and this branch's list-sub-items fix moves up to a new
0.39.67 heading. plugin.json bumps to match. Neither side's entry is
dropped or reworded.

Both sides' intent recovered from history rather than taking either side
whole: the conflict was confined to the single 0.39.66 heading in
plugins/work-items/CHANGELOG.md; every other file merged clean.

Verification: check-changelog-parity.sh --check, --check-order,
--check-bump origin/main and --check-preserved origin/main all pass, and
list-sub-items.test.sh passes 14 of 14.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPLatLkg4329L8eyfxhuMa
Addresses the open P2 review thread on #3830.

GitHub owner and repo names are case-insensitive and the tracker id
grammar accepts any case, but the same-repo test compared the id-derived
"owner/repo" against the node's url-derived one with a case-sensitive
jq ==. An id written github:acme/widgets#99 against a repo the API
spells Acme/Widgets classified every child as foreign and returned an
empty list with no signal: the same silent blindness #3825 was, reached
by a different route, and in a verb whose whole point is that this
failure mode is now visible.

Both sides are folded with ascii_downcase before comparing. The null
guard is explicit, so an unattributable node still lands in the
unattributed bucket and still warns on stderr rather than erroring in
jq. Folding widens the match on case alone and never across repos.

Four regression cases added, stubbing a container whose api casing
(Acme/Widgets) differs from the id casing (acme/widgets): the same-repo
child is kept, a genuinely cross-repo node still drops, and no
derivable-repo warning fires. Suite goes 14 to 18 cases, all passing.

Verification: affected-tests.sh --run selected 13 suites, all passed;
check-changelog-parity.sh --check, --check-order and --check-bump
origin/main pass; check-purged-em-dashes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPLatLkg4329L8eyfxhuMa
… comments

The comment-hygiene lane rejected the previous commit: both new comment
blocks wrote the example id as `acme/widgets#99`, which matches the
owner/repo#N form the tracker-reference-form convention bans inside a
comment in a scanned extension (docs/conventions/tracker-reference-form).

The comments now describe the casing difference in words instead of
spelling an owner/repo pair, and the issue citation takes the bare
parenthesised form the convention asks for. The example ids in the stub
and the assertions are code, not comments, and keep the shape the rest of
the suite already uses.

No behaviour change; 18 of 18 cases still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPLatLkg4329L8eyfxhuMa
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

One finding from the superseded duplicate that this PR does not carry

#3832 was closed as superseded by this one, which was the right call. The two diffs agree on the substance, and this one already carries the case-insensitivity fix (ascii_downcase on both sides of the same-repo comparison). One thing from that lane's review is missing here, and it came from a reviewer finding rather than from the author, so it is worth repeating.

The fail-open fallback's justification is false as written. The comment in the superseded version said the fallback was safe because gh scopes a parent's subIssues list to that parent's own repo. The suite's own FOREIGN_PAYLOAD fixture disproves it: cross-repo children do appear, which is the entire reason the same-repo predicate exists. So a node carrying neither .url nor .repository.nameWithOwner is genuinely ambiguous, and admitting it can misattribute a foreign child whose number collides with a local issue.

The resolution taken there was to keep the fail-open behaviour, because the observed failure was the silent empty rollup, but to stop justifying it with a false invariant and to make it observable:

  • State the real reason: no known gh payload omits both fields (2.97.0 always projects .url), so this is a defensive default rather than a reasoned-about case.
  • Emit a warning on stderr when a node is unattributable, so a real occurrence surfaces instead of silently admitting a foreign child.

Two tests covered it: the warning fires on an unattributable payload, and an attributable payload stays silent on stderr.

Worth adding here, or worth an explicit decision not to. Either way the comment should not keep asserting an invariant the fixture contradicts. The full exchange is on the closed #3832 if the reasoning is useful.

@kyle-sexton
kyle-sexton merged commit c0fba15 into main Sep 6, 2026
12 checks passed
@kyle-sexton
kyle-sexton deleted the claude/3825-work-item-tracker branch September 6, 2026 21:46
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.

work-items/tracker: list-sub-items returns empty because gh --json subIssues nodes carry no repository field

2 participants