LearnNewsExamplesServices
Frontmatter
id11365
titleRestore milestone-aware routing capability in gh-workflow syncers
stateClosed
labels
enhancementairefactoringarchitecture
assigneesneo-gpt
createdAtMay 14, 2026, 5:45 PM
updatedAtJun 7, 2026, 7:11 PM
githubUrlhttps://github.com/neomjs/neo/issues/11365
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 2, 2026, 6:03 PM

Restore milestone-aware routing capability in gh-workflow syncers

Closed Backlog/active-chunk-12 enhancementairefactoringarchitecture
neo-opus-ada
neo-opus-ada commented on May 14, 2026, 5:45 PM

Update 2026-05-14 (post-ADR-0004 + #11372 supersession):

  • Authority refresh: Epic #11187 framing in original body is now superseded. This ticket relates to Epic #11372 (ADR 0004 implementation) but remains parked low-prio per operator directive 2026-05-14 ("low prio for now. our full focus must stay on getting our archive and mcp server stable again").
  • Alignment note: under ADR 0004's universal ordinal-100 chunking, the original prescription's sealed-chunk-safe predicate + routeByMilestone: false config gate is slightly stale — the universal chunk-N/ primitive doesn't need a flat-vs-chunked branching. However, the underlying capability-preservation rationale (operator wants milestone-routing as future-enabled option) survives. Specific prescription refresh deferred until ticket is picked up; operator-conditional capability preserved.
  • Status: parked / not currently sub-of-#11372; reconsider sub-link decision when milestone-routing becomes operationally relevant.

Context

PR #11362 (commit 559c73d43, merged 2026-05-14) removed the milestone-based archive-bucketing fallback from IssueSyncer#planArchiveBuckets / #getIssuePath and the parallel branch in PullRequestSyncer#planArchiveBuckets. The removal was correct in the context of Epic #11187 Phase 6 — the old version || issue.milestone?.title || defaultArchiveVersion || 'unversioned' fallback chain pre-staged closed-post-latest-release items into not-yet-existing version archives, violating sealed-chunk semantics.

Operator @tobiu surfaced post-merge (2026-05-14): "the milestone logic got removed. as mentioned earlier: we might use milestones in the future. so this needs a new ticket, but low prio for now."

This ticket preserves the architectural option-space — milestone-aware routing as a future-enabled capability — without re-introducing the pre-stage bug that #11362 fixed.

The Problem

Capability lost in #11362: the syncers no longer consult issue.milestone?.title / pr.milestone?.title when deriving an archive bucket. The milestone field is still fetched (via FETCH_ISSUES_FOR_SYNC / FETCH_PRS_FOR_SYNC GraphQL queries) and persisted in .sync-metadata.json (MetadataManager.mjs:109 for issues, PullRequestSyncer.mjs:91,104 for PRs), but it no longer influences routing decisions.

Operator confirmed Neo.mjs has never used milestones operationally — release tracking has been purely time-based via ReleaseSyncer.getReleaseForDate(closedAt). The removed branch was speculative-support code. However, operator preserved the future-use option ("we MIGHT use milestones in the future") and explicitly rejected dead-code removal as the framing.

Why a verbatim restore is wrong: the pre-#11362 milestone fallback fired for items with NO matching release tag and ANY milestone title set — including milestones for not-yet-cut versions (e.g., v14.0.0 while v13 is still the current release). This pre-staged items into bucket directories that publish.mjs had not yet authored, violating the Epic #11187 mental model that archive folders for vN.M.K are created at release-cut by publish.mjs, never pre-staged.

The Architectural Reality

Touch surface (post-#11362 dev tip 559c73d43):

File Symbol Current state Removed branch (gone in #11362)
ai/services/github-workflow/sync/IssueSyncer.mjs #planArchiveBuckets if (!version) continue; (skip bucketing) if (issue.milestone?.title) { version = ... } fallback
ai/services/github-workflow/sync/IssueSyncer.mjs #getIssuePath closed branch if (!plan?.version) return active path Same fallback chain
ai/services/github-workflow/sync/PullRequestSyncer.mjs #planArchiveBuckets if (!version) continue; } else if (pr.milestone?.title) { version = ... }
ai/services/github-workflow/sync/PullRequestSyncer.mjs #getPullRequestPath Active fallback for !plan?.version Same chain
ai/services/github-workflow/queries/issueQueries.mjs All sync queries milestone { ... } still fetched (unchanged)
ai/services/github-workflow/sync/MetadataManager.mjs:109 metadata persist milestone: value.milestone still stored (unchanged)

DiscussionSyncer did not have a parallel milestone branch (Discussions don't have milestones in GitHub's GraphQL schema), so no symmetry work is needed there.

Related: #11363 (Pocket A — gh-workflow config defaultArchiveVersion: 'unversioned' cleanup; @neo-gpt's lane) will likely also remove issueSyncConfig.versionDirectoryPrefix consumers if the config field is retired entirely. This ticket should coordinate with #11363 on whether versionDirectoryPrefix survives as a milestone-routing primitive.

The Fix

Restore milestone-aware routing as an opt-in, sealed-chunk-safe capability. Two-part prescription:

Part 1 — Routing predicate (sealed-chunk-safe): milestone-derived version is only honored when the corresponding archive directory already exists on disk. This means publish.mjs has cut that release, the bucket is real, and routing into it does not create a pre-stage shape. If the milestone names a not-yet-cut release, fall through to the active path (current #11362 behavior).

Concrete shape (illustrative, IssueSyncer):

<h1 class="neo-h1" data-record-id="6">deriveMilestoneVersion(issue) {</h1>

    const title = issue.milestone?.title;
    if (!title) return null;

    const candidate = title.startsWith(issueSyncConfig.versionDirectoryPrefix)
        ? title
        : issueSyncConfig.versionDirectoryPrefix + title;

    const archiveDir = path.join(issueSyncConfig.archiveRoot, 'issues', candidate);
    // Sealed-chunk guard: only route into the bucket if publish.mjs has already cut it.
    if (!fs.existsSync(archiveDir)) return null;

    return candidate;
}

#planArchiveBuckets consults #deriveMilestoneVersion(issue) only when closedAt-derived version is null. Same shape mirrored in PullRequestSyncer.

Part 2 — Config gate (opt-in): a new issueSyncConfig.routeByMilestone: false default. Even with the sealed-chunk guard, milestone-aware routing should be opt-in until Neo.mjs operationally adopts milestones. Add to ai/mcp/server/github-workflow/config.mjs + config.template.mjs. Coordinate with #11363 (Pocket A) for atomic config-surface evolution.

Acceptance Criteria

  • IssueSyncer#deriveMilestoneVersion (private) — sealed-chunk guard via fs.existsSync(archiveDir); returns null for not-yet-cut milestone titles
  • IssueSyncer#planArchiveBuckets consults #deriveMilestoneVersion as a fallback after closedAt-derived version and only when issueSyncConfig.routeByMilestone === true
  • PullRequestSyncer parallel shape mirrored exactly
  • routeByMilestone: false default added to config.mjs + config.template.mjs (coordinate with #11363 author for atomic config-surface mutation)
  • New unit tests in IssueSyncer.spec.mjs + PullRequestSyncer.spec.mjs: (a) milestone with cut-release archive dir → routes to archive; (b) milestone with not-yet-cut release → routes to active; (c) routeByMilestone: false → milestone field ignored entirely
  • No regression on #11362 active/archive routing for items WITH a valid release-version derived from closedAt
  • PR body documents the sealed-chunk-guard rationale + cites this ticket + #11362 + Epic #11187 mental model

Out of Scope

  • Migrating Neo.mjs's release workflow to use milestones (separate ticket, separate Discussion if needed)
  • Removing the milestone { ... } fields from the GraphQL queries (those serve other consumers: metadata snapshots, frontmatter for human readers)
  • Retroactively re-bucketing existing archived items based on milestone (the sealed-chunk-safe predicate is forward-only — once-archived items stay where they are)
  • Touching DiscussionSyncer (no parallel milestone field exists)

Avoided Traps

  • Verbatim restore of removed branch: rejected — would re-introduce the Epic #11187 pre-stage bug. The removal in #11362 was correct; this ticket adds back the capability with a safer predicate, not the exact code shape.
  • defaultArchiveVersion: 'unversioned' fallback: rejected — that's the substrate rot #11363 will retire. Milestone routing must not depend on this default.
  • Routing on milestone existence alone (no sealed-chunk guard): rejected — sets up the same pre-stage failure mode #11362 fixed, just with a different trigger (milestone-set instead of closed-without-release).

Related

  • Refs PR #11362 (the cleanup that removed the original branch) — commit 559c73d43
  • Coordinate with #11363 (Pocket A — config cleanup; potential versionDirectoryPrefix retirement)
  • Related to Epic #11187 (Phase 6 substrate cleanup; this is a capability-preservation follow-up, not a Phase 6 sub-issue)
  • Sibling lanes idle pending Phase 6 stability: #11361 (recursive archive ingestion), #11364 (PR archiveVersion carry-forward retire)

Origin Session ID

cf76b29a-9cf5-4c35-a415-37d631a8a755

Handoff Retrieval Hints

  • Commit-range anchor: 8a1906221..559c73d43 (the #11362 merge that removed the milestone branches)
  • Semantic query: query_raw_memories("milestone fallback removed post-Epic-11187 Phase 6 cleanup")
  • File-level anchor: ai/services/github-workflow/sync/IssueSyncer.mjs#planArchiveBuckets + PullRequestSyncer.mjs#planArchiveBuckets
  • Operator anchor quote: "we NEVER even used milestones. it was always time based" / "NOT dead code. we MIGHT use milestones in the future." (Discussion #11359 graduation thread, 2026-05-14)

🤖 Generated with Claude Code

tobiu removed the agent-task:pending label on May 28, 2026, 12:15 AM
tobiu referenced in commit db26e6b - "feat(github-workflow): gate milestone archive routing (#11365) (#12369)" on Jun 2, 2026, 6:03 PM
tobiu closed this issue on Jun 2, 2026, 6:03 PM