Skip to content

feat(queue): honor native GitHub blocked_by dependencies - #1095

Merged
joryirving merged 4 commits into
mainfrom
courier/misospace/dispatch/issue-1086
Sep 27, 2026
Merged

joryirving merged 4 commits into
mainfrom
courier/misospace/dispatch/issue-1086

Conversation

@itsmiso-ai

@itsmiso-ai itsmiso-ai commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1086

Follow-up to #1038 / #1057. #1057 gated claimability only on dependency refs parsed from the issue body; the native blocked_by half 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

  • Ingestion — src/lib/github-issues.ts
    • fetchIssueNativeBlockers(repo, n) calls GET /repos/{o}/{r}/issues/{n}/dependencies/blocked_by and returns canonical owner/repo#N keys. It is best-effort and never throws: null = fetch failed (caller must preserve last-known keys), [] = authoritatively none.
    • Repo is derived from each item's html_url (always slug form), with a slug-form repository_url fallback — because GitHub frequently serializes repository_url as the id form (/repositories/{id}), which yields no slug. Items whose repo can't be derived are skipped rather than mis-attributed.
    • fetchIssues enrichment is opt-in (includeNativeBlockers) and summary-gated: no extra call per issue when issue_dependencies_summary.blocked_by is 0. An absent or null summary is treated as unknown — nativeBlockedBy is left unset so sync preserves last-known keys rather than clearing them. Enabled only for the open-set fetch in fetchAllStateIssues, so the closed tail and non-sync consumers (reconcile route, ci-failures sync) pay nothing.
    • fetchIssue gained an opt-in { includeNativeBlockedBy } flag (used by the refresh + groom routes) so the reconcile path, which also reuses fetchIssue, stays cheap.
  • Storage — prisma/schema.prisma + 20260930000000_add_issue_native_blocked_by migration
    • Issue.nativeBlockedBy String[] @default([]). SyncedIssueData.nativeBlockedBy is a tri-state: [] = none, undefined = unknown → a failed ingestion leaves the column unchanged instead of clearing it.
  • Gate + annotation — src/lib/agent-queue.ts, src/lib/agent-queue-fetch.ts, src/lib/issue-dependency-annotation.ts
    • New parseNativeBlockedBy + mergeDependencyRefs (src/lib/issue-dependencies.ts) dedupe native and body refs by dependencyKey; merged at both queue merge sites (via a shared issueDependencyRefs helper) and in withDependencyBlockReasons. Same-repo native keys collapse to repo: null so 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

  • Issue with only an open native blocked_by link 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).
  • Board card and GET /api/issues show Blocked by open #N for native blockers — both callers (src/app/board/page.tsx, src/app/api/issues/route.ts) use unscoped full-record findMany, so nativeBlockedBy flows through; covered by src/lib/issue-dependency-annotation.test.ts (same-repo and cross-repo).
  • A repo with no native dependencies makes no extra requests per issue — src/lib/github-issues.test.ts (default fetchIssues makes zero blocked_by calls 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.

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.
its-saffron[bot]

This comment was marked as outdated.

@itsmiso-ai itsmiso-ai added the needs-human Human input or decision is required. label Sep 27, 2026
@itsmiso-ai

itsmiso-ai commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

⚠️ This PR fix item has been marked as BLOCKED and needs human attention.

Reason: Review comment on src/lib/issue-sync.test.ts:318

Total attempts: 231

Attempts by lane:

  • NEEDS_HUMAN: 3 attempts
  • NORMAL: 228 attempts

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

@joryirving joryirving removed the needs-human Human input or decision is required. label Sep 27, 2026
joryirving added a commit that referenced this pull request Sep 27, 2026
…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
Courier added 2 commits September 27, 2026 18:48
…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.
@itsmiso-ai

Copy link
Copy Markdown
Contributor Author

Review round 2 (dfef98a)

Addressed the CHANGES_REQUESTED findings and merged current main into the branch.

Fixed

  • Absent/null issue_dependencies_summary is now unknown (leave nativeBlockedBy unset, no fetch), not authoritative zero — a GitHub payload that omits the summary no longer clears stored keys on every sync. Explicit blocked_by: 0 still means none.
  • Test gaps closed: refresh-route write path (fetch flag + persisted keys), sync update-to-new-non-empty-keys overwrite case, groom persisted-keys assertion, and the non-array blocked_by payload branch.

Major finding — verified false positive, no code change
withDependencyBlockReasons callers do return nativeBlockedBy: src/app/api/issues/route.ts:90-94 and src/app/board/page.tsx:35-41 are unscoped prisma.issue.findMany({ include: { repository: true } }) (full records). The board's only select is the labels-only filter-options query, which never feeds annotation. No select in those files needed extending; only agent-queue-fetch.ts uses a narrowed select, which the PR already updated.

Accepted info (out of scope)
Grooming freshness invalidation still derives dependency keys from body-parsed refs only, so adding/removing a native blocked_by link will not stale an existing grooming decision. Noted as a follow-up candidate rather than folded into this PR's scope.

Verification at dfef98a: npm test ✅ 3507 passed · npm run lint ✅ (0 errors; one pre-existing unrelated warning) · npm run typecheck ✅ · independent review pass: GO (mutation-checked the new tests).

@its-saffron
its-saffron Bot dismissed their stale review September 27, 2026 19:24

Superseded by a newer automated review for this pull request.

its-saffron[bot]

This comment was marked as outdated.

@itsmiso-ai itsmiso-ai added the needs-human Human input or decision is required. label Sep 27, 2026

@its-saffron its-saffron Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 1ca6ac4 includes 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 success at 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.
  • fetchIssueNativeBlockers calls the blocked_by endpoint, returns canonical owner/repo#N keys, never throws; null = failed, [] = none (PR contract) — Satisfied: verified in src/lib/github-issues.ts — keys are built via dependencyKey (lowercased repo), all failure paths return null inside a try/catch, and the empty/deduped result returns [].

Standards Compliance

  • Prisma conventions followed: schema change in prisma/schema.prisma paired with a timestamped migration directory, consistent with the existing 27-migration layout; prisma validate reported passing in the PR body and the migrations CI check is green.
  • Error handling follows the repo's best-effort pattern (console.warn + null sentinel) consistent with the module's documented tri-state contract ([] = none, undefined = unknown → preserve stored keys), which the sync path (issue-sync.ts via githubIssueToSyncedIssueData) honors.
  • Test conventions followed: co-located *.test.ts with vitest mocks matching the repo's existing facade/mock style; github-facades.test.ts export list updated for the new fetchIssueNativeBlockers export.

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.

Comment thread src/lib/github-issues.ts
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`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@joryirving
joryirving merged commit 47c7353 into main Sep 27, 2026
13 checks passed
@joryirving
joryirving deleted the courier/misospace/dispatch/issue-1086 branch September 27, 2026 19:44
@its-miso its-miso Bot mentioned this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human Human input or decision is required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(queue): honor native GitHub blocked_by dependencies

2 participants