LearnNewsExamplesServices
Frontmatter
id13958
titleStop issue sync re-pushing generated-only drift
stateClosed
labels
bugairegressionmodel-experience
assigneesneo-gpt
createdAtJun 24, 2026, 6:11 PM
updatedAtJun 24, 2026, 6:27 PM
githubUrlhttps://github.com/neomjs/neo/issues/13958
authorneo-gpt
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 24, 2026, 6:27 PM

Stop issue sync re-pushing generated-only drift

Closed v13.1.0/archive-v13-1-0-chunk-6 bugairegressionmodel-experience
neo-gpt
neo-gpt commented on Jun 24, 2026, 6:11 PM

Context

A maintainer sync log showed every GitHub Workflow sync run pushing the same local issue changes back to GitHub: Pushed 54 local change(s) to GitHub. This started after a sensitive-name scrub touched synced Markdown/public artifacts. Local Markdown edits should be an extreme edge case for resources/content/issues/**; a normal generated-content sync must not repeatedly mutate public GitHub issues.

Live latest-open sweep: checked the latest 20 open issues on 2026-06-24; no equivalent active ticket found. A2A in-flight sweep: checked the latest 30 messages on 2026-06-24; no overlapping lane claim found. KB/local sweep found adjacent historical work, not this bug: #7616 introduced content-hash push gating, and #7793 stripped activity-log content from the body pushed back to GitHub.

Release classification: boardless operational regression. This is urgent sync hygiene for the agent/orchestrator substrate, but it is not a v13 feature-board item.

The Problem

IssueSyncer.pushToGitHub() uses a full-file Markdown hash to decide whether an active issue file represents a local edit. The file includes generated/read-only sections such as frontmatter, content-trust projection metadata, relationship summaries, and ## Timeline events. The push payload then strips the timeline and only sends title/body back to GitHub.

That means generated-only drift can look like a local edit even when the GitHub title/body already match. When the GraphQL mutation is a no-op or the later delta pull does not rewrite the same generated sections, the metadata hash remains misaligned and the next sync pushes the same issue again.

Empirical evidence from the current checkout:

  • Active issue files whose full-file SHA-256 differs from .sync-metadata.json: 54.
  • Archived issue files also have historical hash drift, but pushToGitHub() only scans issueSyncConfig.issuesDir, so the current push loop is the active 54.
  • Live spot-checks for #5728 and #13229: the local push payload (title + body before ## Timeline) already equals GitHub title/body, so these are generated-only drift candidates, not real local edits.

The Architectural Reality

ai/services/github-workflow/sync/IssueSyncer.mjs currently does two incompatible things in one flow:

  • Lines 969-980 hash the entire local Markdown file and compare it against metadata.issues[number].contentHash.
  • Lines 1003-1022 derive a much smaller push payload by stripping ## Timeline, removing the Markdown title, and sending only title + body via UPDATE_ISSUE.
  • Lines 1246-1273 intentionally scan only active issue Markdown files, which is why the observed 54 active mismatches become repeated public pushes.

The sync metadata is still valuable for pull/render integrity; the bug is using that full-render hash as the sole local-edit detector for a remote surface that only accepts title/body.

The Fix

Keep full-render hashes for pull-side metadata, but make push-side detection payload-aware:

  1. Extract the push payload from local Markdown with one shared helper: {title, body} before ## Timeline.
  2. When a full-file hash mismatch is detected, compare the local push payload to the current GitHub title/body before mutating GitHub.
  3. If title/body already match, treat it as generated-only drift: skip UPDATE_ISSUE, update the local metadata hash for that issue, and count/log it separately from real pushes.
  4. If title/body differ, perform the existing UPDATE_ISSUE mutation and persist the new full-render hash/updated metadata so the next run does not re-push the same change.
  5. Add focused unit coverage that proves generated-only frontmatter/timeline drift does not call UPDATE_ISSUE, while a real body change still does.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Local issue Markdown full-render hash .sync-metadata.json + IssueSyncer.pullFromGitHub() Remains the pull/render cache hash for generated Markdown integrity. If absent, skip unsafe push as today. JSDoc on push detection helper. Existing pull/refetch tests plus new regression.
Push payload {title, body} GitHub updateIssue mutation contract Only title/body deltas are considered remote-edit intent. Generated-only drift is not pushed. Live fetch failure leaves the issue unmutated and records a failure/skip. JSDoc on helper. Unit: generated-only drift skips mutation.
pushToGitHub() stats/logging This ticket Separately report real pushes vs generated-only drift skipped/healed. Existing count remains real pushes only. Logger message. Unit or static assertion in test.

Decision Record impact

None. This aligns the implementation with the existing GitHub Workflow architecture: local Markdown is a generated mirror, and local-to-GitHub writes are exceptional, title/body-only updates.

Acceptance Criteria

  • pushToGitHub() no longer calls UPDATE_ISSUE when only generated/read-only Markdown sections differ from metadata and the remote title/body already match.
  • Real local title/body edits still push exactly once.
  • Metadata is updated after generated-only drift is recognized, so the same active issues are not reprocessed on the next sync run.
  • Unit coverage reproduces the 54-item failure shape with generated-only drift and verifies zero GraphQL update mutations for that case.
  • A sync dry-run or focused diagnostic shows the current active mismatch set would no longer produce 54 issue pushes.

Out of Scope

  • Rewriting archived issue metadata drift. Archived files are outside pushToGitHub()'s active scan and can be handled by a separate migration only if needed.
  • Changing pull/refetch timeline rendering semantics.
  • Syncing local edits to comments/timeline events. GitHub issue body mutations cannot represent that surface.
  • Any public mention of private deployment names or sensitive scrub terms.

Related

  • #7616 — content-hash push gating introduced the full-file comparison.
  • #7793 — push payload strips activity/timeline content.
  • #10090 — timeline/comment drift recovery context.

Handoff Retrieval Hint: IssueSyncer pushToGitHub generated-only drift contentHash metadata UPDATE_ISSUE 54 local changes

tobiu closed this issue on Jun 24, 2026, 6:27 PM
tobiu referenced in commit 4db12fd - "fix(ai): skip generated-only issue sync pushes (#13958) (#13959)" on Jun 24, 2026, 6:27 PM