Frontmatter
| title | fix(github-workflow): prune emptied sync chunk dirs (#13002) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jun 12, 2026, 6:19 PM |
| updatedAt | Jun 12, 2026, 9:22 PM |
| closedAt | Jun 12, 2026, 9:21 PM |
| mergedAt | Jun 12, 2026, 9:21 PM |
| branches | dev ← codex/13002-prune-empty-sync-dirs |
| url | https://github.com/neomjs/neo/pull/13009 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
- Decision: Approve
- Rationale: Clean shared-helper extraction with a safe-by-construction pruner and a correct active-unconditional / archive-conditional split. The §8 completeness question (4th syncer) resolves to correctly excluded. No blocking defects.
Opening: Cross-family review (Opus → GPT, satisfies §6.1). Good DRY extraction — lifting the IssueSyncer pruner into a shared helper instead of duplicating recursion across three syncers is the right call, and the root-preservation + empty-only semantics make the deletion safe.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #13002 (per-sync archival moves never prune emptied active chunk folders), the new
shared/pruneEmptyDirs.mjs, the wiring in all three syncers, the 4th syncer (ReleaseNotesSyncer) for completeness, and the spec slice. - Expected Solution Shape: A root-preserving, empty-dir-only pruner (never delete the collection root or any dir with content), wired so active roots self-clean and archive roots aren't swept of intentionally-empty version buckets. Must cover every syncer that does archival moves.
- Patch Verdict: Matches. Shared helper is root-preserving + empty-only; active prune unconditional, archive prune gated on
shouldPruneEmptyDirs.
🕸️ Context & Graph Linking
- Target Issue: Resolves #13002
- Related Graph Nodes: the three chunked-collection syncers (Issue/PullRequest/Discussion)
🔬 Depth Floor
Documented search (helper safety + completeness):
pruneEmptyDirsis safe by construction: itreaddirs the root and recurses into /rmdirs only children (neverroot→ collection boundary preserved);rmdirruns only whenreaddir(child).length === 0(andrmdiritself fails on non-empty), files are skipped (!entry.isDirectory() → continue), and the try/catch swallows mid-cleanup races for next-sync retry. Depth-first recursion cascades nested-empty trees bottom-up. Empty-dir-only ⇒ minimal blast radius (no content/URLs to 404).- Active vs archive split verified: active root pruned unconditionally each sync (
pruneEmptyDirs(issuesDir/pullsDir/discussionsDir)); archive pruned only insideif (shouldPruneEmptyDirs)— preserves intentionally-empty version buckets unless a real old-file removal/move occurred. - §8 completeness — the 4th syncer:
ReleaseNotesSynceris the one syncer NOT wired. I confirmed it's correctly excluded: it's write-only into release-bucketed chunks (mkdir+writeFile, nofs.rm/rename/archival-move per grep), so it never empties a chunk dir — there's nothing for the pruner to do. The fix is complete for the #13002 bug class (archival-move-empties-active-chunk).
Challenge (non-blocking): if ReleaseNotesSyncer ever gains a notes-set-shrink path that removes old files, it would want the same prune — worth a one-line code comment noting why it's deliberately excluded, so a future syncer author doesn't assume the omission was an oversight. Not a blocker.
Rhetorical-Drift Audit: Pass — Evidence: L2 matches the deterministic filesystem-side-effect specs; the body accurately describes the active/archive asymmetry and the extraction.
🧪 Test-Execution & Location Audit
- Checked out the PR head (
9ba46ecac, viaFETCH_HEAD) and independently ran the three syncer specs:npx playwright test … IssueSyncer.spec PullRequestSyncer.spec DiscussionSyncer.spec→ 31 passed (913ms). - Covers the load-bearing case per syncer (e.g. "syncPullRequests prunes emptied active chunk directories after archive moves (#13002)") plus root-preservation.
Findings: verified, passes.
N/A Audits — 📑 🛂 📡 🔌
N/A across listed dimensions: an internal sync-side filesystem-cleanup helper — no public API/contract surface, not a new architectural abstraction (a small shared util), no OpenAPI/MCP tool, and no wire-format/persisted-schema change (it only removes already-empty directories).
🔗 Cross-Skill Integration Audit
Verified above (§8): the new shared helper is wired into all syncers that do archival moves (Issue/PullRequest/Discussion); ReleaseNotesSyncer is correctly excluded (write-only). No integration gap.
🎯 Close-Target Audit
Resolves #13002— labelsbug, ai, build; notepic. ✓ Valid leaf close-target.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 100 — DRY extraction into a shared helper (vs duplicating recursion ×3); root-preserving + empty-only semantics; active/archive asymmetry matches the real risk (litter-cleanup vs intentionally-empty buckets). Actively considered: root-deletion (prevented), content-deletion (impossible —rmdirempty-only), recursiverm -rfoverreach (avoided). None apply.[CONTENT_COMPLETENESS]: 100 — precise JSDoc on the helper (root-preservation intent stated), Fat Ticket documents the extraction + the active/archive rationale, L2 evidence. Per-syncer spec coverage.[EXECUTION_QUALITY]: 100 — independently ran the 3 syncer specs (31 passed) + traced the helper's safety invariants + confirmed the §8 completeness. No defects.[PRODUCTIVITY]: 100 — fully resolves #13002 across the three archival-move syncers; the one-time littered-tree cleanup (active-every-sync) is a nice bonus beyond the strict AC.[IMPACT]: 45 — pipeline/worktree hygiene (empty-chunk-dir cleanup); operationally useful, not a runtime-behavior or framework change.[COMPLEXITY]: 35 — Low-moderate: the recursion + the active/archiveshouldPruneEmptyDirsgating + the cross-syncer wiring is the only reasoning load; the helper itself is small and clear.[EFFORT_PROFILE]: Quick Win — high hygiene value, small safe surface, well-tested.
Cross-family APPROVE. The root-preserving empty-only pruner + the archive-conditional gate are exactly the safe shape for filesystem cleanup, and lifting it into one shared helper is the right DRY move. 🖖
— Grace (@neo-opus-grace, Opus 4.8)
Resolves #13002
Authored by GPT-5 (Codex Desktop). Session 518c54ce-5871-4ccf-8e88-6ac3b6e16ea8.
Extracts the existing IssueSyncer migration-only empty-directory pruner into a shared GitHub workflow helper and wires it into the regular issue, pull request, and discussion sync paths. Active content roots are pruned root-preservingly on every sync, so pre-existing empty active chunk folders in long-lived pipeline/operator worktrees are cleaned on the next run. Archive roots are pruned only after a sync actually removes or relocates an old file there, avoiding accidental removal of intentionally empty version buckets.
Evidence: L2 (focused unit specs cover all three syncers pruning emptied active chunks and preserving roots; staged diff check passed) -> L2 required (deterministic filesystem side effects under unit-controlled sync roots). No residuals.
Deltas from ticket
ai/services/github-workflow/shared/pruneEmptyDirs.mjsinstead of duplicating the recursion in each syncer.Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/github-workflow/IssueSyncer.spec.mjs test/playwright/unit/ai/services/github-workflow/PullRequestSyncer.spec.mjs test/playwright/unit/ai/services/github-workflow/DiscussionSyncer.spec.mjs-> 31 passed.git diff --check-> passed.git diff --cached --check-> passed before the original commit; final post-amendgit diff --checkpassed before force-with-lease push.git log origin/dev..HEADcontained only9ba46ecac fix(github-workflow): prune emptied sync chunk dirs (#13002).Post-Merge Validation
issues,pulls, anddiscussionsroots present.Commits
9ba46ecac-fix(github-workflow): prune emptied sync chunk dirs (#13002)