Conversation
🔍 Multi-Model Consensus Review — PR #405 (Round 1)PR: feat: Add existing folder as repository Findings (consensus ≥ 2 models)🟡 F1: 🟡 F2: Bare catch { remoteUrl = ""; }
🟡 F3: Same bare catch { return false; }A cancelled 🟢 F4: Fragile state reset on form open (SessionSidebar.razor:341) ✅ Positive Notes
Test Coverage
Verdict:
|
F1 (test race): RunProcess now sets EnableRaisingEvents=true and subscribes Exited BEFORE Process.Start(), eliminating the window where a fast process (like 'git init') exits before the event handler is registered. F2 (bare catch swallows cancellation): AddRepositoryFromLocalAsync now uses 'catch (Exception ex) when (ex is not OperationCanceledException)' so cancellation propagates instead of yielding a misleading 'No origin remote' error. F3 (bare catch in IsGitRepositoryAsync): Added explicit re-throw for OperationCanceledException so cancellation propagates from the git rev-parse call instead of returning 'false' (not a git repo). F4 (stale form state on re-open): Replaced inline 'showAddRepo = true' handler with OpenAddRepoForm() method that resets addRepoFolderMode, addRepoFolderPath, addRepoError, confirmRepoReplace, confirmRepoName, and newRepoUrl on every open. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
F1 (test race): RunProcess now sets EnableRaisingEvents=true and subscribes Exited BEFORE Process.Start(), eliminating the window where a fast process (like 'git init') exits before the event handler is registered. F2 (bare catch swallows cancellation): AddRepositoryFromLocalAsync now uses 'catch (Exception ex) when (ex is not OperationCanceledException)' so cancellation propagates instead of yielding a misleading 'No origin remote' error. F3 (bare catch in IsGitRepositoryAsync): Added explicit re-throw for OperationCanceledException so cancellation propagates from the git rev-parse call instead of returning 'false' (not a git repo). F4 (stale form state on re-open): Replaced inline 'showAddRepo = true' handler with OpenAddRepoForm() method that resets addRepoFolderMode, addRepoFolderPath, addRepoError, confirmRepoReplace, confirmRepoName, and newRepoUrl on every open. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
775ea1b to
c4e2e56
Compare
Replace `Dictionary` with `ConcurrentDictionary` for `_cache` in ExternalSessionScanner. The cache is read/written from a Timer callback (ThreadPool thread) in `Scan()`, and `Dictionary` is not thread-safe for concurrent operations. Found during PR #405 code review — the scanner code is from PR #370. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0760d4d to
76ac48e
Compare
Re-Review: Add existing folder as repository (#405) — Round 2Latest commit: 775ea1b (3 commits total) Round 1 Findings Status
All 4 Round 1 findings confirmed fixed by 4/4 models (5th agent timed out). New Findings (Round 2, consensus 2+ models)
Test CoverageNew Recommended Action: ✅ ApproveAll blocking issues are fixed. The two remaining minor items (process leak and stdout drain) are test infrastructure concerns that don't affect production behavior. |
Skip dead event-stream disk fallback when SessionId is empty to avoid reading from an invalid path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allows users to add an already-cloned local folder as a managed repository in PolyPilot, giving it full feature parity with repos cloned through the app (worktrees, branch creation, PR checkout, squad discovery, sessions, etc.). ## RepoManager.AddRepositoryFromLocalAsync (enhanced) - Added Directory.Exists check with clear error message - Added IsGitRepositoryAsync helper (runs git rev-parse --git-dir) - Added origin-remote check with actionable error message - Added Action<string>? onProgress parameter forwarded to the clone step - All three validations throw InvalidOperationException with user-friendly messages ## CopilotService.AddRepoFromLocalFolderAsync (new) - Public surface that delegates to RepoManager then creates the repo group - Desktop (local) mode only — throws clearly when connected to a remote server ## SessionSidebar UI (updated) - '+ Repo' form now shows URL / Existing Folder tabs on desktop - Folder tab: path text input + Browse (…) button that opens FolderPickerService - Browse button is platform-gated (#if MACCATALYST || WINDOWS) - CloseAddRepoForm resets both modes; AddRepositoryFromFolder wires progress display - CSS: .add-repo-mode-tabs, .add-repo-tab, .add-repo-folder-row, .btn-repo-browse ## Tests (3 new in RepoManagerTests) - AddRepositoryFromLocal_NonExistentFolder_ThrowsWithClearMessage - AddRepositoryFromLocal_FolderWithNoGit_ThrowsWithClearMessage - AddRepositoryFromLocal_GitRepoWithNoOrigin_ThrowsWithClearMessage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…into existing repo) Root cause of ongoing confusion: when the user added ~/Projects/maui3, the code extracted its origin (https://github.com/dotnet/maui) and merged it into the existing 'maui' group. The user saw no visible change. The user's expectation: adding a local folder should create a NEW, DISTINCT entry in the sidebar -- like '📁 maui3' -- that they can see and use. Fix: complete redesign of the local-folder flow: 1. SessionGroup.LocalPath (new field): marks a group as a 'pinned local folder'. IsLocalFolder computed property. Not linked to a PolyPilot bare clone. 2. GetOrCreateLocalFolderGroup() (new): creates a distinct group named after the folder (e.g. 'maui3'), idempotent by path. Un-collapses if already exists. 3. AddRepoFromLocalFolderAsync: now does path normalization (including ~ expansion) itself, validates the folder, ALSO registers the bare clone (for worktree support), then creates a LOCAL FOLDER GROUP -- not the existing repo group. 4. Sidebar rendering: local-folder groups show a 📁 icon, have their own '...' menu with 'New Session Here' (creates CWD=localPath session) and 'Remove Folder'. 5. QuickCreateSessionForFolder: creates a timestamped session in the local folder group, with working directory set to the folder path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ncileOrganization When the user creates a session from a '📁 maui3' local folder group, ReconcileOrganization was detecting that the session's workingDirectory matched a registered external worktree, then moving the session to the corresponding repo group (maui) instead of leaving it in the local folder group. Fix: skip the group-reassignment when the session is already in a local folder group (IsLocalFolder == true). The worktree link (WorktreeId) is still set for metadata purposes, but the GroupId is not overridden. Also: manually fixed the already-misplaced maui3-20260318-124507 session in organization.json to be in the correct local folder group. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…of creating redundant 📁 group When a user adds a local folder whose origin URL matches a repo already in the sidebar, we now: 1. Register the folder as an external worktree (visible in '📂 Existing' tab) 2. Un-collapse the existing repo group so the user can see it 3. Show a contextual success message: '✅ Registered in the <name> group. Use ⋯ → New Session → 📂 Existing to open it.' 4. Do NOT create a redundant 📁 local folder group When the repo is genuinely new (no existing group), we still create a 📁 group but now pass RepoId so it can offer the full ⋯ menu: - ➕ New Session Here (CWD = local path) - ⚡ Quick Branch + Session - ⑂ Named Branch + Session This makes 'Add Existing Folder' a first-class citizen that integrates with PolyPilot's full worktree infrastructure rather than being a bookmark. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tale groups Three fixes: 1. CreateSessionWithWorktreeAsync now accepts targetGroupId -- when 'Quick Branch' or 'Named Branch' is clicked from a 📁 group, the new session lands IN that local folder group (not the URL-based repo group). 2. ReconcileOrganization migration: groups created by old code (before LocalPath field existed) are auto-detected by matching their name against registered external worktrees. LocalPath and RepoId are back-filled on load, so 📁 groups created in earlier sessions now get the correct menu. 3. RepoManager.GetWorktreesDir() exposes the managed worktrees directory path so ReconcileOrganization can distinguish external worktrees (user-owned) from PolyPilot-managed ones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…inside repo)
When a user adds an existing folder via 'Add Existing Folder' and creates a
new branch (⚡ Quick Branch or ⑂ Named Branch) from the 📁 group menu,
worktrees are now created nested inside their repo at:
{localPath}/.polypilot/worktrees/{branchName}/
instead of the centralized ~/.polypilot/worktrees/{repoId}-{guid8}/.
Changes:
- RepoManager.CreateWorktreeAsync: new optional localPath param; when
provided, uses nested strategy and adds .polypilot/ to .gitignore
- EnsureGitIgnoreEntry: new private helper — idempotent, handles existing
entries with/without trailing slash, creates .gitignore if missing
- RemoveWorktreeAsync: safety guard now also accepts nested worktrees
(paths containing /.polypilot/worktrees/) in addition to centralized ones
- CopilotService.CreateSessionWithWorktreeAsync: threads localPath through
to CreateWorktreeAsync
- SessionSidebar: QuickCreateSessionForRepo, StartQuickBranch, CommitQuickBranch
all pass group.LocalPath for 📁 local folder groups
- Tests: 4 new unit tests for EnsureGitIgnoreEntry edge cases; updated
FakeRepoManager/FailingRepoManager to match new signature
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bug 1 (path traversal): After computing the nested worktree path, validate that Path.GetFullPath(worktreePath) stays inside the managed .polypilot/worktrees/ directory. A branch name like '../../evil' would otherwise escape and create a worktree outside the user's repo. Bug 2 (delete dialog): SessionListItem.ShowCloseConfirm isManaged check now also accepts nested worktrees (paths containing /.polypilot/worktrees/) in addition to centralized ~/.polypilot/worktrees/ paths — consistent with RemoveWorktreeAsync. Tests: 6 new path traversal tests (3 malicious, 3 safe branch names) validating the guard logic works correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on Windows Git creates read-only files in .git/objects/ on Windows. Directory.Delete throws UnauthorizedAccessException in test cleanup for tests that run git commands (RegisterExternalWorktree_AddsWorktreeToState and _Idempotent). Add ForceDeleteDirectory helper that strips FileAttributes.ReadOnly from all files before deletion, and replace all 14 Directory.Delete call sites in RepoManagerTests.cs with it. Fixes deterministic CI failures on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, AddRepoFromLocalFolderAsync would silently merge the local folder into an existing URL-based repo group if the origin matched. This caused confusion when the user added a separate local copy of a repo that was already managed via URL -- it would show 'Registered as existing worktree' instead of creating a new sidebar entry. Remove the merge-into-existing-URL-group logic. A local folder always gets its own dedicated sidebar group, regardless of whether the origin matches a repo already managed via URL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d5f62ad to
2003de8
Compare
🔍 Multi-Model Consensus Review — PR #405 (Round 3)PR: feat: Add existing folder as repository (new commits since Round 2 approval) Round 1+2 Findings Status (all fixed ✅)F1 EnableRaisingEvents race, F2/F3 bare catch swallowing cancellation, F4 addRepoFolderMode reset — all confirmed fixed in Round 2. New Findings (Round 3, consensus 2+ models)🔴 C1 — Data Loss:
|
C1 - External folder deletion (data loss):
RemoveWorktreeAsync now applies the managed-location guard (isCentralized ||
isNested) in the catch block before Directory.Delete. External folders added
via 'Add Existing Folder' have BareClonePath set (same as linked worktrees),
so without this guard they bypassed the else-branch safety check and got
recursively deleted. Now only directories inside ~/.polypilot/worktrees/ or
{repo}/.polypilot/worktrees/ are ever deleted.
C2 - Empty branch name destroys nested worktrees container (data loss):
Two-part fix:
1. SessionSidebar.razor: after Regex sanitization, validate that branchName
is non-empty before calling CreateWorktreeAsync. Shows user-friendly error.
2. RepoManager.cs: remove the 'resolved.Equals(repoWorktreesDir)' escape
hatch from the path traversal guard. An empty/dot branch name that resolves
to the container dir itself now throws instead of letting git fail and the
catch block delete the entire .polypilot/worktrees/ directory.
M1 - TOCTOU duplicate worktree registration:
Re-check for existing matching path inside the second lock(_stateLock) block
in RegisterExternalWorktreeAsync, immediately before Worktrees.Add. Eliminates
the race window between the first idempotency check and the Add.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔄 Re-Review Round 4 — PR #405Reviewing commit Previous Findings Status
New Issue (Minor)Test quality gap on C2: The new [InlineData("")]
[InlineData(".")]would complete the regression coverage for C2's server-side guard. Tests2830 passed / 1 failed The one failure ( No new test failures introduced by this PR. VerdictThe two critical data-loss bugs (C1, C2) and the TOCTOU race (M1) are all genuinely fixed. The only outstanding item is M2 (migration folder-name collision), which was a moderate finding from Round 3, and a minor test coverage gap for the C2 server-side guard. Suggested before merging:
If M2 is intentionally left for a follow-up, I'd accept this PR — the critical data-loss issues are fixed. |
…production guard CopilotService.Organization.cs: Migration heuristic now collects all external worktrees matching a group folder name. Only backfills when exactly one match exists - skips if ambiguous (multiple repos share the same folder name). RepoManagerTests.cs: Update path traversal tests to use the single-condition guard that matches production code. Add empty-string and dot branch name cases to the rejection theory - both resolve to repoWorktreesDir itself and should be rejected, closing the C2 regression coverage gap from Round 4 review. Tests: 72/72 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔄 Re-Review Round 5 — PR #405Reviewing commit Previous Findings Status
New IssuesNone found. Tests2831 passed / 2 failed / 2833 total (+2 new tests vs Round 4) Both failures are pre-existing flaky timing tests ( Verdict✅ Approve All 5 findings (2 critical, 2 moderate, 1 minor) are now fixed. No new issues introduced. The migration guard is correct and conservative (skip-if-ambiguous is the safe choice). Test coverage is complete. Ready to merge. |
🔄 Re-Review Round 6 — PR #405No new commits since Round 5 ( All 5 findings resolved (C1, C2, M1, M2, N1), tests at 2831/2833 passing with 2 pre-existing flaky failures unrelated to this PR. Ready to merge. |
When a user adds both a URL-based repo group and a 📁 local folder group for the same RepoId, GetOrCreateRepoGroup was incorrectly returning the local folder group because it matched on RepoId without distinguishing between the two group types. This caused sessions created without an explicit targetGroupId (e.g. from the URL-based repo group menu's 'Quick Branch + Session') to land in the local folder group instead of the URL-based repo group. Fix: add && !g.IsLocalFolder to the FirstOrDefault predicate in GetOrCreateRepoGroup so it only returns dedicated URL-based repo groups. Local folder groups continue to be returned by GetOrCreateLocalFolderGroup which already filters by IsLocalFolder. Tests: added 4 new tests in RepoGroupingTests covering: - GetOrCreateRepoGroup_DoesNotReturnLocalFolderGroup - GetOrCreateRepoGroup_WhenBothGroupTypesExist_ReturnsUrlBasedGroup - GetOrCreateRepoGroup_LocalFolderGroupFirst_ThenUrlGroup_ReturnsUrlBasedGroup - ReconcileOrganization_SessionInLocalFolderGroup_DoesNotMoveToUrlGroup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔄 Re-Review Round 7 — PR #405Reviewing new commit Previous Findings Status
New Commit
|
Add Existing Folder as Repository
Allows users to point PolyPilot at an already-cloned local git repo, giving it full sidebar integration with worktree-based branch creation.
Two-Path Add Flow
Nested Worktrees
For 📁 groups, worktrees created inside the repo at {localPath}/.polypilot/worktrees/{branchName}/. Auto .gitignore. Path traversal protection. URL repos use ~/.polypilot/worktrees/.
Delete Session Flow
Non-managed sessions skip worktree deletion prompt. Nested worktrees show delete option.
Migration
ReconcileOrganization backfills LocalPath/RepoId on stale groups.
Key Files
TODOs
Tests
49 RepoManager tests + all WorktreeStrategy tests passing
Commits