Skip to content

fix: don't promote URL-based groups to local folder groups#648

Merged
PureWeen merged 5 commits intomainfrom
fix/no-promote-url-groups
Apr 20, 2026
Merged

fix: don't promote URL-based groups to local folder groups#648
PureWeen merged 5 commits intomainfrom
fix/no-promote-url-groups

Conversation

@PureWeen
Copy link
Copy Markdown
Owner

Problem

ReconcileOrganization was converting URL-based groups to local folder groups when external worktrees existed, causing the "3 maui groups" bug:

  1. Promotion hijacked the URL group — setting LocalPath on the main group that had 16+ sessions on managed worktrees
  2. Session migration created a new URL groupGetOrCreateRepoGroup was called to house the displaced sessions
  3. Multiple external worktrees compounded the problem — each external path (maui2, maui3) triggered promotion, creating yet more groups

Root Cause

The external worktree promotion loop (lines 710-780) found each external worktree path, then promoted the most-recently-created URL-based group by setting its LocalPath. This was wrong when the URL group had sessions using managed worktrees (~/.polypilot/worktrees/...).

Fix

Never promote URL-based groups. Instead:

  • Create a separate local folder group for each external worktree path via GetOrCreateLocalFolderGroup
  • Skip stale external worktree entries whose paths no longer exist on disk (Directory.Exists check)
  • Leave the URL-based group and all its sessions completely untouched

Tests

  • All 3505 tests pass (0 failures)
  • Updated 4 tests that expected the old promotion behavior
  • Added 2 new tests:
    • ReconcileOrganization_MultipleExternalWorktrees_SameRepo_CreatesGroupPerPath
    • ReconcileOrganization_StaleExternalWorktree_SkippedWhenPathNotOnDisk

Fixes the recurring "multiple maui groups" issue that kept coming back after PR #638.

ReconcileOrganization was converting URL-based groups to local folder
groups when external worktrees existed, causing the '3 maui groups' bug:
1. Promotion hijacked the URL group (setting LocalPath)
2. Sessions on managed worktrees got migrated to a new URL group
3. Multiple external worktrees each triggered promotion, creating more groups

Fix: never promote URL-based groups. Instead, create a separate local
folder group for each external worktree path via GetOrCreateLocalFolderGroup.
Also skip stale external worktree entries whose paths no longer exist on disk.

Tests updated to match new behavior + new tests for:
- Multiple external worktrees creating separate groups per path
- Stale paths being skipped
- Sessions staying in URL groups untouched

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Expert Code Review — Design-Level Findings

3 independent reviewers with adversarial consensus

🟡 MODERATE — Test passes for wrong reason (false-green risk) — Flagged by 2/3 reviewers

File: PolyPilot.Tests/SessionOrganizationTests.csReconcileOrganization_NoPromotion_NestedWorktreeSession_StaysInUrlGroup (line ~1284, outside diff hunks)

The test constructs extPath = Path.Combine(Path.GetTempPath(), "MyRepo") but never calls Directory.CreateDirectory(extPath). With the new Directory.Exists(normalizedExtPath) check at CopilotService.Organization.cs:742, this path is silently skipped — no local folder group is ever created. The assertion passes trivially because the code path being tested was never exercised.

All sibling tests were updated to use GUID-suffixed paths with Directory.CreateDirectory + try/finally cleanup. This one was missed.

Suggested fix: Add Directory.CreateDirectory(extPath) + GUID suffix + try/finally cleanup, matching the pattern in sibling tests.


🟢 MINOR — Stale comment references removed behavior — Flagged by 3/3 reviewers

File: PolyPilot/Services/CopilotService.Organization.cs, line 757 (outside diff hunks)

The comment reads: "This fixes state from before the promotion migration was added." This PR removes the promotion migration from ReconcileOrganization. The healing loop is still needed (handles groups promoted by PromoteOrCreateLocalFolderGroup from the explicit "Add Existing Folder" path, or legacy state from older app versions), but the comment now references removed behavior.

Suggested fix: Update to: "This heals sessions stranded in local folder groups whose worktree path doesn't match the group's LocalPath (e.g., sessions on managed worktrees that ended up in a local folder group due to legacy promotion or explicit folder addition)."


Discarded Findings (single-reviewer only, not confirmed by follow-up)

Finding Flagged by Outcome
Double SaveOrganization() + debounce timer churn 1/3 Discarded — SaveOrganization() is debounced; only the last timer fires the disk write
PromoteOrCreateLocalFolderGroup documentation gap 1/3 Discarded — call site already has an explanatory comment
changed = true unconditional / TOCTOU concern 1/3 Discarded — GetOrCreateLocalFolderGroup rechecks under lock; spurious save is harmless

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • 192.0.2.1

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "192.0.2.1"

See Network Configuration for more information.

Generated by Expert Code Review (auto) for issue #648 ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Code Review Summary

PR: fix: don't promote URL-based groups to local folder groups
Methodology: 3 independent reviewers with adversarial consensus (initial review + targeted follow-up verification)

Findings (ranked by severity)

# Severity Finding Consensus Location
1 🟡 MODERATE Test NestedWorktreeSession_StaysInUrlGroup passes for wrong reason — missing Directory.CreateDirectory causes the Directory.Exists guard to skip the code path entirely, making the test a false green 2/3 reviewers Test file (outside diff)
2 🟡 MODERATE GetOrCreateLocalFolderGroup fires OnStateChanged mid-reconciliation before the session-healing loop runs, potentially rendering partial state in the UI 2/3 reviewers (1 disagreed, noting most callers fire OnStateChanged afterward) Production code, line 750 (inline comment)
3 🟢 MINOR Stale comment at line 757 references "promotion migration" which this PR removes 3/3 reviewers Production code (outside diff)

Overall Assessment

The core production code change is well-designed and correct. Replacing the promotion-based approach with separate local folder group creation is a clean solution to the "3 maui groups" bug. The new Directory.Exists guard for stale paths is a good defensive addition.

Test coverage is strong — 2 new tests (MultipleExternalWorktrees_SameRepo_CreatesGroupPerPath and StaleExternalWorktree_SkippedWhenPathNotOnDisk) cover important edge cases. The one false-green test (NestedWorktreeSession_StaysInUrlGroup) should be hardened with Directory.CreateDirectory to match the pattern in all sibling tests.

CI status: Tests pass (19/19 organization tests verified by reviewers).

Net code reduction: ~33 lines of complex promotion + session migration logic removed, replaced by ~6 lines of straightforward group creation. This is a significant simplification that reduces future maintenance burden.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • 192.0.2.1

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "192.0.2.1"

See Network Configuration for more information.

Generated by Expert Code Review (auto) for issue #648


// Create a dedicated local folder group for this external path.
// This never touches the existing URL-based group.
GetOrCreateLocalFolderGroup(normalizedExtPath, ext.RepoId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MODERATE — Mid-reconcile OnStateChanged fires before session healing (Flagged by 2/3 reviewers)

GetOrCreateLocalFolderGroup internally fires OnStateChanged?.Invoke() (line ~1504), which triggers a UI re-render before the session-healing loop at lines 758–791 has run. This means the UI could briefly render partially-reconciled state (new local folder group exists, but stranded sessions haven't been healed yet).

Counterargument (1/3 disagreed): Most callers of ReconcileOrganization fire OnStateChanged afterward, so the final state is delivered in the same UI batch. However, not all call sites necessarily follow this pattern.

Suggested mitigation: Consider adding a suppressNotify parameter to GetOrCreateLocalFolderGroup when called from ReconcileOrganization, or ensure the outer reconciliation always fires OnStateChanged at the end when changed is true.

Agents should run PolyPilot/relaunch.sh from their worktree, not from
the primary checkout at ~/Projects/AutoPilot/PolyPilot/. The script uses
dirname to resolve its build directory, so each worktree's copy builds
its own code automatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen
Copy link
Copy Markdown
Owner Author

PureWeen commented Apr 20, 2026

✅ PR #648 — Final Multi-Model Re-Review (Round 3)

Re-review after all fix commits (bef805646e8dca2666bf9).


Previous Findings — All Resolved

# Finding Severity Status
1 False-green NestedWorktreeSession test 🟡 MODERATE FIXED — GUID path + Directory.CreateDirectory + Assert.False(IsLocalFolder) + try/finally
2 Auto-assignment picks wrong local folder group 🟡 MODERATE FIXED — Separator-aware StartsWith + Equals + path normalization at both sites
3 Stale comment references removed promotion 🟢 MINOR FIXED — Updated to reflect no-promotion design
4 OnStateChanged/SaveOrganization mid-reconciliation 🟢 MINOR FIXEDskipNotify: true parameter, callers batch saves
5 StaleExternalWorktree test missing IsInitialized 🟢 MINOR FIXEDIsInitialized = true + allowPruning: false
6 StartsWith path-prefix collision (/maui vs /maui2) 🟡 MODERATE FIXED — Separator + exact-match + new AutoAssignment_SiblingPaths_DoesNotCrossMatch test

6/6 fully fixed.


Round 3 New Findings

No findings reached 2/3 consensus. Discarded single-reviewer findings:

Finding Flagged by Outcome
Auto-assignment fallback ?? could transiently misroute managed worktrees 1/3 Addressed — clarifying comment added explaining heal-stranded-sessions dependency. Net result is always correct.
TrimEnd arg inconsistency in test (1 arg vs 2 args) 1/3 FixedTrimEnd(DirectorySeparatorChar)TrimEnd(DirectorySeparatorChar, AltDirectorySeparatorChar)
Missing OnStateChanged at end of reconcile 1/3 Incorrect — verified all 5 callers of ReconcileOrganization call OnStateChanged?.Invoke() immediately after
g.LocalPath assumed pre-normalized 1/3 Discarded — GetOrCreateLocalFolderGroup always normalizes before storing; encapsulation is sufficient

CI Status

⚠️ No CI configured on this repo.

Test Coverage

✅ All 3506 tests pass. 8 tests directly cover the changed behavior including the new sibling-path regression test.


Recommendation

Approve and merge — All findings resolved across 3 review rounds. Core design (no-promotion → separate local folder groups) is clean and well-tested.


3 independent reviewers · Adversarial consensus · Re-review round 3

PureWeen and others added 3 commits April 20, 2026 14:04
1. Fix false-green NestedWorktreeSession test: add Directory.CreateDirectory
   with GUID suffix + try/finally cleanup + Assert.False(IsLocalFolder)

2. Path-aware auto-assignment: both sites (~579, ~612) now prefer the local
   folder group whose LocalPath matches the session's worktree path, with
   fallback to any local folder group for backward compatibility

3. Update stale comment at healing loop to reference legacy promotion and
   explicit folder addition instead of removed promotion migration

4. Suppress mid-reconciliation SaveOrganization: add skipNotify parameter
   to GetOrCreateLocalFolderGroup, used by ReconcileOrganization to batch
   saves instead of N+1 disk writes

5. Add IsInitialized to StaleExternalWorktree test for consistency with
   all sibling tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both auto-assignment sites used bare StartsWith without a trailing
Path.DirectorySeparatorChar, causing /maui to false-match /maui2.
Now mirrors the correct pattern from the heal-stranded-sessions code:
normalized paths + separator suffix + exact-match fallback.

Added AutoAssignment_SiblingPaths_DoesNotCrossMatch test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add clarifying comments on auto-assignment fallback explaining dependency
on heal-stranded-sessions block. Fix TrimEnd(DirectorySeparatorChar) →
TrimEnd(DirectorySeparatorChar, AltDirectorySeparatorChar) in test for
Windows consistency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen merged commit 17739f6 into main Apr 20, 2026
@PureWeen PureWeen deleted the fix/no-promote-url-groups branch April 20, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant