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:
- Extract the push payload from local Markdown with one shared helper:
{title, body} before ## Timeline.
- When a full-file hash mismatch is detected, compare the local push payload to the current GitHub title/body before mutating GitHub.
- 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.
- 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.
- 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
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
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 forresources/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:
#7616introduced content-hash push gating, and#7793stripped 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## Timelineevents. 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:
.sync-metadata.json: 54.pushToGitHub()only scansissueSyncConfig.issuesDir, so the current push loop is the active 54.#5728and#13229: the local push payload (title + body before## Timeline) already equals GitHubtitle/body, so these are generated-only drift candidates, not real local edits.The Architectural Reality
ai/services/github-workflow/sync/IssueSyncer.mjscurrently does two incompatible things in one flow:metadata.issues[number].contentHash.## Timeline, removing the Markdown title, and sending onlytitle+bodyviaUPDATE_ISSUE.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:
{title, body}before## Timeline.UPDATE_ISSUE, update the local metadata hash for that issue, and count/log it separately from real pushes.UPDATE_ISSUEmutation and persist the new full-render hash/updated metadata so the next run does not re-push the same change.UPDATE_ISSUE, while a real body change still does.Contract Ledger Matrix
.sync-metadata.json+IssueSyncer.pullFromGitHub(){title, body}updateIssuemutation contractpushToGitHub()stats/loggingcountremains real pushes only.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 callsUPDATE_ISSUEwhen only generated/read-only Markdown sections differ from metadata and the remote title/body already match.Out of Scope
pushToGitHub()'s active scan and can be handled by a separate migration only if needed.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