feat(queue): honor native GitHub blocked_by dependencies - #1095
Conversation
Refs #1086. Sync now ingests native `blocked_by` links into Issue.nativeBlockedBy and merges them with body-parsed dependency refs before the agent-queue gate and the board / GET /api/issues annotation. An issue blocked only by an open native link is withheld from the claimable queue and shows "Blocked by open #N", matching body-based deps. - Ingestion (github-issues.ts): summary-gated per-issue fetch of GET /repos/{o}/{r}/issues/{n}/dependencies/blocked_by. Opt-in on fetchIssues (enabled only for the open-set sync fetch) and opt-in on fetchIssue so the reconcile path stays cheap. Repo derived from html_url with an id-form repository_url fallback. Best-effort: a failed fetch returns null and preserves last-known keys rather than clearing them. - Storage: Issue.nativeBlockedBy String[] + additive migration. SyncedIssueData carries a tri-state ([] = none, undefined = unknown) so ingestion failures never clear stored blockers. - Gate + annotation: parseNativeBlockedBy + mergeDependencyRefs dedupe native and body refs by dependencyKey across both queue merge sites and withDependencyBlockReasons.
Reason: Review comment on src/lib/issue-sync.test.ts:318 Total attempts: 231 Attempts by lane:
Failing run(s): Last attempt: src/lib/issue-sync.test.ts:318 — Minor (bug): The sync persistence tests never exercise the empty-array case, so the path where a closed blocker causes sync to write nativeBlockedBy: [] and clear stale stored keys is untested. Automated finding from AI PR review. Posted automatically by Dispatch on 2026-09-27T19:38:07.199Z |
…h attempts (#1107) * fix(pr-fix): cap fix attempts, not evidence, and always baseline fresh attempts The PR_FIX_MAX_ATTEMPTS cap counted distinct evidence keys, and every inline review comment is its own key, so one review with 6 comments blocked an item before any fix ran. Count dispatchable attempts in a new fixAttempts column instead: returns to QUEUED and refused no-push FIXEDs count, extra evidence on already-queued work does not, and requeue resets it. Requeue, mark back to QUEUED, and the refused-FIXED rollback left attemptHeadSha null, so the guard fell back to the mutable headSha that the next sync overwrites with the worker's own push, refusing real fixes. Baseline every fresh attempt from the newest observed head, and backfill QUEUED rows. Closes #1103 Closes #1104 * test(pr-fix): cover attempt-cap edges and move migration past #1095's * test(pr-fix): assert intermediate FIXED marks in the cap tests
…ew test gaps Address AI review feedback on #1095: - enrichNativeBlockers: an absent or null issue_dependencies_summary now leaves nativeBlockedBy unset (unknown) instead of writing [] and clearing stored keys on every sync; explicit zero stays authoritative-none. - Tests: pin the summary tri-state and the non-array payload branch; cover the refresh route's nativeBlockedBy write path; cover sync overwrite-to-new-non-empty-keys; assert groom persists fetched keys.
Review round 2 (dfef98a)Addressed the CHANGES_REQUESTED findings and merged current Fixed
Major finding — verified false positive, no code change Accepted info (out of scope) Verification at dfef98a: |
Superseded by a newer automated review for this pull request.
There was a problem hiding this comment.
AI Automated Review (incremental)
Incremental review: reviewed the changes since the last managed review; unresolved findings from that review are carried forward.
Analysis engine: glm-5.3-flash@https://litellm.jory.dev/v1 (openai) — routed smart (risk match: db_or_migration_changes)
Recommendation
Approve. This incremental delta makes two small, correct changes on top of an already-reviewed PR: it adds ?per_page=100 to the native blocked_by fetch and renames the migration directory from 20260930000000_add_issue_native_blocked_by to 20261002000000_add_issue_native_blocked_by. Both are verified against the head sources; no blocker-level issues remain.
Change-by-Change Findings
1. src/lib/github-issues.ts — per_page=100 on the blocked_by fetch
Verified in the head file: fetchIssueNativeBlockers now requests .../dependencies/blocked_by?per_page=100 and the co-located test (blockedByUrl helper) pins the new URL. This is a sensible page-size bump for the per-issue dependency endpoint. However, the call is a single fetchWithRetry with no pagination loop: an issue with more than 100 native blocked_by links is silently truncated to the first page, and the missing blockers are treated as absent — the issue could be surfaced as claimable while open blockers beyond page 1 exist. This is an extreme edge case (GitHub issues rarely carry >100 native dependencies) and the failure mode is fail-open, consistent with the module's best-effort contract, so it is minor rather than blocking. A follow-up to loop on the Link header (as fetchPaginated already does elsewhere) would close it.
2. src/lib/github-issues.test.ts — URL assertion update
The only test change updates the expected URL string to include ?per_page=100. It pins the request shape but asserts no new behavior (no truncation or continuation test). Minor; acceptable for this delta given the existing suite already covers the enrichment, unknown-summary, failure, PR-skip, dedupe, and unparseable-repo branches.
3. Migration rename 20260930000000 → 20261002000000
Verified: the file content is unchanged (additive ADD COLUMN IF NOT EXISTS "nativeBlockedBy" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]), and the new timestamp 20261002000000 sorts after 20261001000000_add_pr_fix_attempt_count (from PR 1107, present in repo history), which matches the commit's stated intent to order the migration after PR 1107's. No data-loss risk: the column is additive, backfill-free, and defaults to an empty array, matching the schema.prisma nativeBlockedBy String[] @default([]) declaration.
Must-Check Items
- Review migration for data loss risk — Verified. The migration is a single additive
ADD COLUMN IF NOT EXISTS ... NOT NULL DEFAULT ARRAY[]::TEXT[]; no column drops, type changes, or backfills that could mutate existing rows. Schema and migration agree. - Test migration on a copy of production schema — The CI run for head commit
1ca6ac4includes a Database migrations check (success) and a Database integration check (success), which exercise migration application against a real PostgreSQL instance. No additional local verification is required from this review.
Requirement Coverage
- Lint/typecheck blocks CI (AGENTS.md) — Satisfied: CI reports Workflow lint, Lint, and Typecheck all
successat head. - Tokens are secrets; never logged/echoed/persisted (AGENTS.md) — Satisfied: the delta touches no auth, token, or logging code; the diff contains no secret material.
fetchIssueNativeBlockerscalls the blocked_by endpoint, returns canonicalowner/repo#Nkeys, never throws;null= failed,[]= none (PR contract) — Satisfied: verified insrc/lib/github-issues.ts— keys are built viadependencyKey(lowercased repo), all failure paths returnnullinside a try/catch, and the empty/deduped result returns[].
Standards Compliance
- Prisma conventions followed: schema change in
prisma/schema.prismapaired with a timestamped migration directory, consistent with the existing 27-migration layout;prisma validatereported passing in the PR body and the migrations CI check is green. - Error handling follows the repo's best-effort pattern (
console.warn+nullsentinel) consistent with the module's documented tri-state contract ([]= none,undefined= unknown → preserve stored keys), which the sync path (issue-sync.tsviagithubIssueToSyncedIssueData) honors. - Test conventions followed: co-located
*.test.tswith vitest mocks matching the repo's existing facade/mock style;github-facades.test.tsexport list updated for the newfetchIssueNativeBlockersexport.
Tool Harness Findings
8 tool calls executed (6 read_file, 2 git_grep). Verified: the head github-issues.ts contains the per_page=100 URL with no pagination loop; issue-dependencies.ts implements parseNativeBlockedBy/mergeDependencyRefs with dependencyKey dedupe and same-repo collapse; the migration file content is unchanged by the rename; issue-sync.ts passes { state: "open", includeNativeBlockers: true } only on the open-set fetch; grep confirms includeNativeBlockers is used only at the scheduled-sync open fetch and tests, and nativeBlockedBy flows through agent-queue-fetch.ts select, both refresh/groom route writes, and the annotation path.
Unknowns or Needs Verification
- The linked-source fetches in the corpus all failed or were skipped (they are test-fixture URLs, not real upstream dependencies), so no external evidence was needed or obtained; nothing in this delta depends on them.
- The >100-blocker truncation behavior is unexercised by tests; real-world impact is unknown but bounded by GitHub's dependency feature limits. Recommend a follow-up pagination loop rather than blocking this merge.
| issueNumber: number, | ||
| ): Promise<string[] | null> { | ||
| const [owner, repo] = repoFullName.split("/"); | ||
| const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${issueNumber}/dependencies/blocked_by?per_page=100`; |
There was a problem hiding this comment.
Minor (bug): The blocked_by fetch is capped at a single per_page=100 page with no pagination loop, so an issue with more than 100 native blockers silently drops the rest and can be reported claimable despite open blockers beyond the first page.
Automated finding from AI PR review.
| } | ||
|
|
||
| const blockedByUrl = (n: number) => | ||
| `https://api.github.com/repos/acme/app/issues/${n}/dependencies/blocked_by?per_page=100`; |
There was a problem hiding this comment.
Minor (docs): The only test delta pins the ?per_page=100 URL string but asserts no new behavior; no test covers the >100-blocker truncation or Link-header continuation case.
Automated finding from AI PR review.
Closes #1086
Follow-up to #1038 / #1057. #1057 gated claimability only on dependency refs parsed from the issue body; the native
blocked_byhalf of #1038's "Done When" never shipped, so an issue whose only blocker is a native link stayed claimable. This adds native ingestion, storage, and merges native refs into the existing gate + annotation.What changes
src/lib/github-issues.tsfetchIssueNativeBlockers(repo, n)callsGET /repos/{o}/{r}/issues/{n}/dependencies/blocked_byand returns canonicalowner/repo#Nkeys. It is best-effort and never throws:null= fetch failed (caller must preserve last-known keys),[]= authoritatively none.html_url(always slug form), with a slug-formrepository_urlfallback — because GitHub frequently serializesrepository_urlas the id form (/repositories/{id}), which yields no slug. Items whose repo can't be derived are skipped rather than mis-attributed.fetchIssuesenrichment is opt-in (includeNativeBlockers) and summary-gated: no extra call per issue whenissue_dependencies_summary.blocked_byis0. An absent or null summary is treated as unknown —nativeBlockedByis left unset so sync preserves last-known keys rather than clearing them. Enabled only for the open-set fetch infetchAllStateIssues, so the closed tail and non-sync consumers (reconcile route, ci-failures sync) pay nothing.fetchIssuegained an opt-in{ includeNativeBlockedBy }flag (used by the refresh + groom routes) so the reconcile path, which also reusesfetchIssue, stays cheap.prisma/schema.prisma+20260930000000_add_issue_native_blocked_bymigrationIssue.nativeBlockedBy String[] @default([]).SyncedIssueData.nativeBlockedByis a tri-state:[]= none,undefined= unknown → a failed ingestion leaves the column unchanged instead of clearing it.src/lib/agent-queue.ts,src/lib/agent-queue-fetch.ts,src/lib/issue-dependency-annotation.tsparseNativeBlockedBy+mergeDependencyRefs(src/lib/issue-dependencies.ts) dedupe native and body refs bydependencyKey; merged at both queue merge sites (via a sharedissueDependencyRefshelper) and inwithDependencyBlockReasons. Same-repo native keys collapse torepo: nullso they render/dedupe identically to body refs. Existing rules preserved: direct (non-transitive) blockers; a blocker in an untracked/disabled repo does not gate.Done When → coverage
blocked_bylink is not returned claimable and becomes claimable when the blocker closes —src/lib/agent-queue.test.ts(native gating) +src/lib/issue-sync.test.ts(create writes keys; update with unknown field preserves them; update with a new non-empty set overwrites).GET /api/issuesshowBlocked by open #Nfor native blockers — both callers (src/app/board/page.tsx,src/app/api/issues/route.ts) use unscoped full-recordfindMany, sonativeBlockedByflows through; covered bysrc/lib/issue-dependency-annotation.test.ts(same-repo and cross-repo).src/lib/github-issues.test.ts(defaultfetchIssuesmakes zeroblocked_bycalls even when a summary reports blockers; enrichment is opt-in + summary-gated; absent/null summary → no call, keys preserved).Review round 2
Summary-gating hardened (absent/null summary = unknown, never clears stored keys) and test gaps from the AI review closed: refresh-route write path, sync overwrite-to-new-keys case, groom persisted-keys assertion, non-array payload branch. See the round-2 comment for the false-positive verification of the major finding.
Verification
npm run typecheck✅ ·npm run lint✅ (0 errors; one pre-existing unrelated warning) ·npm test✅ 3507 passed ·npx prisma validate✅Independent review passes addressed: html_url/id-form repo parsing (B1), preserve-stored-keys-on-failure (M1), opt-in enrichment so non-sync consumers + the closed tail don't pay (M2), ingestion observability, stale comment fixes, absent-summary tri-state (round-2 M-bug), and the round-2 test gaps.