Skip to content

fix(sdd-dispatch): resolve task→tracker via GraphQL parent#149

Merged
norrietaylor merged 2 commits into
mainfrom
fix/133-dispatch-walk-graphql-parent
May 27, 2026
Merged

fix(sdd-dispatch): resolve task→tracker via GraphQL parent#149
norrietaylor merged 2 commits into
mainfrom
fix/133-dispatch-walk-graphql-parent

Conversation

@norrietaylor
Copy link
Copy Markdown
Owner

Root cause

sdd-dispatch's close-triggered route walk seeded from payload.issue.parent_issue_url and re-fetched the closed issue via REST in the prior fix, but GitHub does not populate the child→parent pointer on either the issues.closed webhook OR on a REST GET /issues/{n} re-fetch for native sub-issues. The walk read empty, broke at hop 0, and the cascade never advanced past layer 1 (#133).

Evidence on gominimal/spectacles-test tracker #157: runs 26272436357 and 26272460894 logged walked 0 hops; ignoring for both task closures. #170/#176 REST .parent is null; webhook parent_issue_url is empty.

Fix

Replace REST seeding with GraphQL Issue.parent as the primary mechanism — it is authoritative for native sub-issues and returns the parent node with one query per hop. The walk seed is the closed issue's node_id; each hop returns the parent's node id for the next hop.

When GraphQL returns null (the closed issue is not a native sub-issue at all — e.g. linked by body reference instead of the native parent connection), fall back to a reverse lookup: list open issues in the repo carrying sdd:dispatched, walk each tracker's sub_issues → Unit sub_issues looking for the closed issue's number. Bounded by the small number of armed trackers and tolerant of races because the GraphQL path is primary.

The two-hop ceiling, the walked === 2 task-closure success condition, and the sdd:dispatched label gate on the resolved tracker are unchanged. The diagnostic walked N hops; ignoring log line now reflects the GraphQL result rather than implying a missing parent_issue_url.

Acceptance

  • Merging a task PR fires sdd-dispatch compute+dispatch and arms every newly-unblocked task within one cycle.
  • A 10-task / 5-Unit feature drains to sdd:done with no manual /dispatch.
  • The route job's existing issues: read permission is sufficient for GraphQL Issue.parent; no permission change needed.

References

🤖 Generated with Claude Code

The close-triggered route walk seeded from `payload.issue.parent_issue_url`
and re-fetched the closed issue via REST in the prior fix, but GitHub does
not populate the child→parent pointer on either the `issues.closed` webhook
OR on REST `GET /issues/{n}` for native sub-issues. The walk read empty,
broke at hop 0, and the cascade never advanced past layer 1 (issue #133).

Replace REST seeding with GraphQL `Issue.parent`, which is authoritative
for native sub-issues. When GraphQL returns null (the closed issue is not
a native sub-issue at all), fall back to scanning open `sdd:dispatched`
trackers in the repo and walking their `sub_issues` trees to locate the
closed task number. The two-hop ceiling, the `walked === 2` success
condition, and the `sdd:dispatched` gate on the resolved tracker are
unchanged.

Closes #133

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9779deb7-ea93-4167-92b1-27697d4ee742

📥 Commits

Reviewing files that changed from the base of the PR and between 5be2dd1 and 53dd004.

📒 Files selected for processing (1)
  • wrappers/sdd-dispatch.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • wrappers/sdd-dispatch.yml

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Improved issue dispatch routing to more reliably detect task closures and correctly arm cascades only for true task-level closures.
    • Added a fallback lookup for unresolved cases to reduce missed task detections.
    • Enhanced diagnostic logging to distinguish non-task closures, lookup errors, and when no tracking issue is found.

Walkthrough

Updated dispatch workflow's task-closure detection to resolve parent chains via GraphQL (up to two hops) and fall back to a paged reverse lookup across open sdd:dispatched trackers; the cascade is armed only when a two-hop tracker labeled sdd:dispatched is found.

Changes

Task-Closure Detection via GraphQL and Reverse Lookup

Layer / File(s) Summary
GraphQL parent resolution and reverse-lookup logic
wrappers/sdd-dispatch.yml
Adds helpers to resolve an issue's parent via GraphQL (returning id, number, labels and distinguishing no-parent vs error), walk up to two hops (Feature → Unit → task), and perform a paged reverse lookup across open sdd:dispatched trackers when GraphQL cannot resolve the chain.
Cascade arming and outcome logging
wrappers/sdd-dispatch.yml
After resolution, prefers the two-hop GraphQL result, falls back to reverse lookup on GraphQL error, treats fewer-than-two-hop results as non-task closures, and arms the cascade only when the tracker is exactly two hops and carries the sdd:dispatched label; otherwise logs specific outcomes.

Sequence Diagram

sequenceDiagram
  participant GHWebhook as GitHub Webhook
  participant Dispatch as Dispatch Workflow
  participant GraphQL as GraphQL Parent Walk
  participant ReverseLookup as Reverse Lookup
  participant Cascade as Cascade Arming Logic

  GHWebhook->>Dispatch: issues.closed event (closed_issue_number)
  Dispatch->>GraphQL: Resolve parent chain via GraphQL (up to 2 hops)
  alt GraphQL Parent Found
    GraphQL->>Dispatch: Return parent id, number, labels
  else GraphQL Parent Not Found or Error
    GraphQL->>Dispatch: No parent chain / error
    Dispatch->>ReverseLookup: Scan open sdd:dispatched trackers' sub-issues
    ReverseLookup->>Dispatch: Match closed_issue_number in tracker tree
  end
  Dispatch->>Cascade: Pass resolved tracker (or nil) + hop count
  alt Tracker at 2 hops with sdd:dispatched label
    Cascade->>Cascade: Arm dispatch cascade
  else Other outcome
    Cascade->>Cascade: Log non-task-closure or unresolved tracking
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • norrietaylor/spectacles#133: Directly related—addresses the same issues.closed handling bug by replacing reliance on payload.issue.parent_issue_url with GraphQL parent resolution and reverse-lookup fallback.

Possibly related PRs

  • norrietaylor/spectacles#139: Modifies the same wrappers/sdd-dispatch.yml route job handling for issues.closed parent/tracker resolution; closely related changes in the same area.

Poem

🐰 A closed task hopped out of sight,
GraphQL chased the parent light,
If queries nap and answers lack,
A reverse hunt finds the track,
Two hops up — then dispatch takes flight! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(sdd-dispatch): resolve task→tracker via GraphQL parent' clearly and specifically summarizes the main change: replacing REST-based parent resolution with GraphQL parent lookups to fix the dispatch walk mechanism.
Description check ✅ Passed The description comprehensively explains the root cause (GitHub not populating parent pointers in webhooks/REST), the fix (switching to GraphQL with reverse lookup fallback), and acceptance criteria directly related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/133-dispatch-walk-graphql-parent

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wrappers/sdd-dispatch.yml`:
- Around line 294-300: The current fallback reverse-lookup uses a single
github.rest.issues.listForRepo call (owner/repo, state/open, labels:
'sdd:dispatched', per_page:100) which only fetches the first page; change this
to paginate and aggregate all pages (e.g. use octokit.paginate or a loop with
page/per_page) so you collect all armed trackers before doing reverse
lookups—mirror the paging approach used by compute when it pages sub_issues.
Replace the single listForRepo call in the shown block and the two similar
blocks (the calls around the other occurrences referencing
github.rest.issues.listForRepo) to fetch all pages and then continue the
existing filtering/lookup logic on the aggregated results.
- Around line 258-261: The current catch around parentViaGraphql() collapses
lookup failures into a "no parent" result, incrementing gqlWalked and causing
valid task closures to be misclassified; change the error handling so lookup
errors are distinguished from a legitimate null parent (e.g., have
parentViaGraphql() return an explicit {parent: null, error: true} or rethrow and
catch upstream), do NOT increment or set gqlWalked on transient lookup errors,
and adjust the fallback logic that decides Unit/tracker vs task closure to
consider tracker unresolved OR any lookup error (not only gqlWalked === 0) so a
failed hop triggers the reverse lookup path instead of silently treating it as
no-parent.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3fe6e5cf-6591-4ef0-911b-49b1cc7fa9a3

📥 Commits

Reviewing files that changed from the base of the PR and between d80d048 and 5be2dd1.

📒 Files selected for processing (1)
  • wrappers/sdd-dispatch.yml

Comment thread wrappers/sdd-dispatch.yml Outdated
Comment thread wrappers/sdd-dispatch.yml
Two CodeRabbit findings on the cascade-arming logic:

1. parentViaGraphql() collapsed both "no parent" and lookup failure
   into null, so a transient GraphQL error on hop 2 incremented
   gqlWalked and routed the closure into the Unit/tracker-close
   branch — silently dropping a real task closure. Switch to a
   discriminated return ({ kind: 'ok' | 'none' | 'error' }) and
   surface 'stoppedBy' from walkViaGraphql(). On 'error', try the
   reverse lookup before giving up.

2. trackerViaReverseLookup() called listForRepo / sub_issues with
   per_page:100 and no paging. A closed task beyond the first page
   of armed trackers, Units, or sibling tasks was reported as having
   no resolvable tracker. Extract listSubIssuesAll() and
   listDispatchedTrackersAll() helpers that mirror the paging
   pattern compute already uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@norrietaylor
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@norrietaylor norrietaylor merged commit ade470a into main May 27, 2026
10 checks passed
@norrietaylor norrietaylor deleted the fix/133-dispatch-walk-graphql-parent branch May 27, 2026 16:18
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.

bug: sdd-dispatch re-dispatch-on-close walks 0 hops (parent_issue_url empty on close webhook)

1 participant