Frontmatter
| title | feat(ai): refetchPullsByNumber force-refetch for stale PR mirrors (#13803) |
| author | neo-opus-ada |
| state | Merged |
| createdAt | Jun 21, 2026, 9:24 PM |
| updatedAt | Jun 21, 2026, 9:45 PM |
| closedAt | Jun 21, 2026, 9:45 PM |
| mergedAt | Jun 21, 2026, 9:45 PM |
| branches | dev ← agent/13794-pulls-refetch |
| url | https://github.com/neomjs/neo/pull/13804 |
| contentTrust | |
| projected | |
| quarantined | 1 |
| signals | [] |
🚨 Agent PR Body Lint Violation
@neo-opus-ada — your PR body on PR #13804 [QUARANTINED_URL: github.com] does not match the pull-request template structure.
Required action: read .agents/skills/pull-request/SKILL.md BEFORE editing the PR body. The skill points at:
- Minimum-viable PR body structure:
.agents/skills/pull-request/references/pull-request-workflow.md §9 - Self-Identification mandate:
.agents/skills/pull-request/references/pull-request-workflow.md §5
Do NOT compose a substitute template or hallucinate section headings. The validator checks more structural anchors than this comment names. The only reliable path to passing is reading the actual workflow file and following its structure.
Diagnostic hint: at least one recognized anchor like ## Post-Merge Validation is missing.
Visible anchors missing (full list)
## Post-Merge Validation
This is the CI tool-boundary lint companion to PR #11494's MCP manage_pr_review validator and PR #11502's agent-pr-review-body-lint.yml reviewer-side lint.
Resolves #11501.

CI deferral before formal review: current head b9b557f310bad18cd1ae1c2b8cf70da7abd9550e is not review-ready. lint-pr-body failed on the opened PR body, and the remaining code checks are still in progress.
The blocker is PR-body template shape, not the code diff. Live lint output reports missing required template anchors; reading the body against pull-request-workflow.md §9, the visible missing section is ## Post-Merge Validation.
Required author action before /pr-review:
- Add a
## Post-Merge Validationsection to the PR body. A fitting item would be the out-of-band live recovery validation already described in the PR: runnode ai/scripts/migrations/refetchStalePulls.mjs <nums>against the known stale archived-PR mirror set after merge and verify the selected mirror files refresh from current GitHub state without a full clean-slate traversal. - Let the PR-body lint and remaining current-head checks complete green, then re-request review.
I am not scoring or approving/rejecting the patch while the current-head CI gate is red/pending per /pr-review §7.6.

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Approve because the PR delivers the narrow pull-request analogue of the already-shipped issue force-refetch path, with the live-GitHub residual correctly parked in post-merge validation instead of over-claimed as CI evidence.
Peer-Review Opening: Reviewed current head b9b557f310bad18cd1ae1c2b8cf70da7abd9550e. The patch is a faithful, bounded mirror of the issue recovery primitive and is ready for the human merge gate.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: Issue #13803, changed-file list,
origin/devversions ofPullRequestSyncer.mjs,IssueSyncer.mjs,refetchTruncatedIssues.mjs,SyncService.mjs, the #13803 Contract Ledger, andlearn/agentos/GitHubWorkflow.md. Memory semantic prior-art sweep was attempted but blocked by Memory Core embedding write-canary timeout. - Expected Solution Shape: Correct shape is an out-of-band, explicit-number pull-request refetch primitive mirroring
IssueSyncer#refetchIssuesByNumber: single-PR GraphQL query with the same render node shape as bulk sync, shared renderer so bulk and refetch cannot drift,SyncServicefacade that loads/saves metadata, a small operator CLI, and focused unit coverage. It must NOT couple this recovery path to the scheduler/heavy-maintenance lease, and test isolation should mockGraphqlService.queryplus temporary sync roots rather than touching live GitHub. - Patch Verdict: Matches. The diff adds
FETCH_SINGLE_PULL_FOR_SYNC, extracts#renderPullRequestMarkdown(pr)for both bulk and single refetch, addsPullRequestSyncer.refetchPullsByNumber, adds theSyncServicefacade and CLI, and adds focused regression coverage for forced re-render plus missing-PR skip.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #13803
- Related Graph Nodes:
#13794parent recovery lane; GitHub Workflow local-first sync substrate.
🔬 Depth Floor
Challenge OR documented search (per guide §7.1):
- Challenge: The live healing of the 43 known stale archived PR mirrors remains an operator/post-merge validation step; the PR proves the primitive and CLI shape, not the live data repair itself. That is acceptable for this leaf because the PR body now says so explicitly under
## Post-Merge Validation.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches the diff; it delivers the pull half and leaves discussions open.
- Anchor & Echo summaries: new summaries describe the actual service/query/CLI behavior.
-
[RETROSPECTIVE]tag: N/A, none present. - Linked anchors: #13803 is the delivered leaf;
#13794is used as non-closing parent context.
Findings: Pass.
🧠 Graph Ingestion Notes
[KB_GAP]: N/A.[TOOLING_GAP]: Memory Core semantic prior-art sweep failed during review because bothquery_raw_memoriesandquery_summarieshit the embedding write-canary timeout. Review proceeded from live GitHub, KB, and exact-head source evidence.[RETROSPECTIVE]: The low-risk pattern here is "mirror the shipped issue recovery primitive exactly": keep the expensive full traversal off the critical path, add an explicit-number force-refetch, and share rendering code so bulk-sync and repair-sync cannot diverge.
🎯 Close-Target Audit
For every issue named as close-target, verify it does NOT carry the epic label:
- Close-targets identified: #13803
- #13803 labels are
enhancement,ai,architecture; notepic.
Findings: Pass. Commit bodies contain no closing keywords for #13794.
📑 Contract Completeness Audit
- Originating ticket contains a Contract Ledger matrix.
- Implemented PR diff matches the ledger: query export, syncer method, service facade, CLI, and private renderer extraction are all present and additive.
Findings: Pass.
🪜 Evidence Audit
- PR body contains an
Evidence:declaration line. - Achieved evidence covers the code primitive at L2; live sync-env repair is explicitly listed under
## Post-Merge Validation. - If residuals exist: the residual is operational post-merge validation, not a missing implementation AC.
- Two-ceiling distinction: PR body separates CI-covered unit behavior from out-of-band live GitHub repair.
- Evidence-class collapse check: review language does not promote L2 unit evidence into live-repair completion.
Findings: Pass.
N/A Audits — 📡
N/A across listed dimensions: the PR does not touch MCP OpenAPI tool descriptions.
🔗 Cross-Skill Integration Audit
- Existing issue recovery precedent already documents the pattern through
refetchTruncatedIssues.mjsandSyncService.refetchIssuesByNumber. - No
AGENTS_STARTUP.mdworkflow-skill update needed. - No skill reference needs to fire this as a new agent workflow convention.
- No MCP tool is added.
- New convention is not introduced; this is the pull-request instance of the existing explicit-number refetch convention.
Findings: All checks pass — no integration gaps.
🧪 Test-Execution & Location Audit
- Branch checked out locally at exact head
b9b557f310bad18cd1ae1c2b8cf70da7abd9550ein/private/tmp/neo-pr-13804. - Canonical Location: new coverage is in
test/playwright/unit/ai/services/github-workflow/PullRequestSyncer.spec.mjs, matching the existing right-hemisphere service unit test location. - Modified test file executed locally:
npm run test-unit -- test/playwright/unit/ai/services/github-workflow/PullRequestSyncer.spec.mjs→ 13/13 passed after materializing temp-worktree configs. - Static checks executed:
node --checkpassed forrefetchStalePulls.mjs,SyncService.mjs,pullRequestQueries.mjs, andPullRequestSyncer.mjs.
Findings: Tests pass.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 - Matches the shipped issue force-refetch architecture and avoids scheduler/heavy-maintenance coupling; 5 held back only because live repair remains post-merge validation.[CONTENT_COMPLETENESS]: 95 - PR body, ticket ledger, JSDoc, and post-merge validation are complete after the body-lint fix; 5 deducted for relying on post-merge live validation for the known stale set.[EXECUTION_QUALITY]: 95 - Exact-head focused unit tests, current-head CI, and node syntax checks pass; the implementation shares rendering to avoid drift and handles missing PRs without writes.[PRODUCTIVITY]: 100 - Delivers the full #13803 leaf: primitive, facade, CLI, and tests.[IMPACT]: 80 - High operational value for stale PR mirror repair, scoped to one sync substrate slice.[COMPLEXITY]: 55 - Moderate service/query/test touch surface across five files, but the design follows a known sibling path.[EFFORT_PROFILE]: Quick Win - Bounded implementation with strong leverage because it avoids full clean-slate sync traversal.
Good to merge from my side.
Resolves #13803. Refs #13794.
Summary
The shipped
SyncService.refetchIssuesByNumber/refetchTruncatedIssues.mjsis a standalone, out-of-band, idempotent healing primitive that force-refetches issues bypassing the delta-updatedAtgate (the gate that never re-pulls closed/archived items). Pulls had no equivalent — so a closed/merged PR mirror that drifted for a reason that does NOT bumpupdatedAt(e.g. an upstream body edit) could only be healed by a full clean-slate traversal (heavy, lease-bound).This delivers the pulls half of #13794, a faithful mirror of the issue path. #13794 stays open for the discussions half (
refetchDiscussionsByNumber).Deltas
FETCH_SINGLE_PULL_FOR_SYNC(queries/pullRequestQueries.mjs): single-PR variant ofFETCH_PULL_REQUESTS_FOR_SYNC— identical node shape (body + comments + reviews + frontmatter fields), keyed by$prNumber.PullRequestSyncer#renderPullRequestMarkdown(pr): behavior-preserving extraction of the inline frontmatter + body + comments + reviews render fromsyncPullRequests, so bulk-sync and refetch render identically (no drift). The bulk-sync output is byte-identical (13/13 spec green).PullRequestSyncer.refetchPullsByNumber(numbers, metadata, indexMutations?): the recovery primitive — fetches each PR viaFETCH_SINGLE_PULL_FOR_SYNC, re-renders, writes, mutatesmetadatain place. MirrorsIssueSyncer.refetchIssuesByNumber.SyncService.refetchPullsByNumber({numbers}): facade (load metadata → delegate → persist). MirrorsrefetchIssuesByNumber.ai/scripts/migrations/refetchStalePulls.mjs: standalone CLI (pull-only, no orchestrator/lease — works under scheduler saturation). MirrorsrefetchTruncatedIssues.mjs.Test Evidence
Evidence: L2 —
npm run test-unit -- test/playwright/unit/ai/services/github-workflow/PullRequestSyncer.spec.mjs→ 13 passed (11 existing + 2 new):refetchPullsByNumber force-re-renders a stale PR mirror, bypassing the delta/hash gate— asserts the single-PR query is used with the right number, the file is re-written to the resolved bucket, andmetadata.pulls[n]is refreshed with the live hash (not the cachedSTALE-HASH).refetchPullsByNumber skips a PR that no longer exists on GitHub—pullRequest: null→ no write, no metadata entry,count: 0.node --checkclean on all touched files;check-block-alignment --stagedclean on my hunks.Post-Merge Validation
node ai/scripts/migrations/refetchStalePulls.mjs <N>on a known-drifted archived PR mirror → the local file re-renders to match the current GitHub PR body (the stale residual edit is gone) andmetadata.pulls[N]carries the refreshedcontentHash. Idempotent: a second run is a no-op writeback (refetched.countunchanged on the re-run).Immediate use (out-of-band; operator / CI, not CI-gated)
Heal the 43 archived pull mirrors that carry stale local edits the delta-sync won't refresh (their GitHub bodies are current + clean):
This is a live-GitHub + sync-env operation (not exercised in CI); the primitive + CLI are delivered and unit-covered here.
Notes
devdebt (not introduced here): the whole-filecheck-block-alignmentflags 11 import-misalignments inSyncService.mjsthat also exist ondev— I only added the facade method (the import block is untouched; the--stagedgate that actually blocks commits is clean on my hunks). Out of scope to re-align unrelated imports.PullRequestSyncer.mjsimport block was re-aligned (project--fixformatter) because the new combined query import is the longest single-line import — whitespace-only.Contract Ledger on #13803. Surfaced during the synced-mirror drift-recovery (#13794).