fix(cli): resolve projects with missing workspace directories#9885

merged1
main
audit/project-removal-4851
openedSep 5, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 1d ago

Problem

A registered project cannot be removed or renamed by its stored workspace path after that directory disappears. The CLI validates the filesystem path before matching the project record, then reports ProjectNotFoundError even when the selected environment still contains that project.

Fix

Keep project-ID lookup first and preserve successful path normalization. If workspace validation fails, allow an exact match against an active project's stored workspace path. The shared lookup covers remove and rename. Existing --force handling, thread-deletion rules, IDs and environment selection are unchanged. No directories are recreated or deleted.

Related to #4851, but do not close that issue. This only fixes lookup within an environment that still has the project record. A replacement machine with an empty database cannot remove an old environment's project; forgetting orphaned client entries remains a separate product decision.

Before and after

Tested against b01771c2, committed September 4, 2026 at 20:31:16 PDT.

The tests create disposable Git directories and SQLite databases through the actual CLI parser, handlers and orchestration commands. They then move the workspace aside and assert that its original path is absent.

  • Before: direct remove by the missing stored path fails with ProjectNotFoundError, activeProjectCount: 1, caused by WorkspaceRootNotExistsError. The final regression suite has one failing missing-path case and nine passing controls.
  • After: all ten focused tests pass. Exact-path rename and force removal update the persisted project/thread records; the missing directory stays missing.
  • Controls preserve empty-project removal by ID, non-force rejection with threads, force removal by ID, rename by ID, existing path normalization and separate symlink project entries. An unregistered symlink still cannot select another project's record.
  • A replacement empty database still rejects the original ID and leaves the original database untouched.

Current web Settings and Legacy Sidebar removal callers already send force: true with the owning environment/project ID. They are unchanged. No browser or remote environment was used; CLI output and persisted-state assertions are the relevant evidence.

Checks

sh
1cd apps/server
2vp test run src/bin.test.ts src/cli/project.test.ts --maxWorkers 2 -t "project lookup with unavailable workspaces|adds, renames, and removes projects offline|force removes projects that still contain threads|maps declared server failures|preserves unexpected server failures"
3../../node_modules/.bin/tsgo --noEmit

Ten focused tests, server typecheck, two-file lint and formatting pass. Typecheck reports pre-existing Effect suggestions outside this diff. The orchestrator independently reviewed the whole two-file diff and the remove/rename/WorkspacePaths context, then reran the ten focused actual CLI/SQLite tests successfully (15 unrelated tests skipped). All current-head executed CI, Correctness, Approvability, Effect Service Conventions and Bugbot passed; all review/comment pages were read with no unresolved findings. The exact reviewed head was squash-merged on September 4, 2026 at 20:40:51 PDT as 9cb40178. The checks watcher has finished.

Prepared by GPT 6 Astra via Codex in T3 Code.


Note

Low Risk Narrow lookup fallback in shared project CLI resolution with broad integration tests; no changes to deletion semantics or filesystem side effects beyond fixing identifier resolution.

Overview Fixes project remove and project rename so they can target a project by its stored workspace path even after that directory no longer exists on disk.

In findActiveProjectTarget, ID lookup is unchanged and successful path normalization still applies. When normalization fails (e.g. missing workspace), the CLI now matches the identifier against active projects’ persisted workspaceRoot using the normalized path if available, otherwise the trimmed input string. --force, thread-deletion rules, and cross-environment behavior are unchanged; an empty replacement database still cannot delete another environment’s project.

Adds CLI integration coverage in bin.test.ts: temp git workspaces, optional threads, moved-aside directories, rename/remove by ID and path, symlink vs stored-path identity, and replacement --base-dir rejection.

Reviewed by Cursor Bugbot for commit e65c174605f0244e2752fa680c3723f67058ed71. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix findActiveProjectTarget to resolve projects with missing workspace directories

Updates workspace-path matching in findActiveProjectTarget so that when path normalization fails (because the workspace directory is gone), the resolver falls back to comparing the trimmed input against the stored path. This lets project mutations like rename and removal proceed by ID or stored path after the workspace directory has been removed. Adds a comprehensive test suite in bin.test.ts covering unavailable workspace scenarios, force removal with child threads, replacement databases, normalized paths, and symlink aliases.

  • Risk: if a stored workspace path differs from the trimmed input in trivial ways (e.g. trailing slash) and normalization fails, the path will no longer match.

Macroscope summarized e65c174.

Added 1 commit

  • fix(cli): resolve projects with missing workspace directoriese65c174
Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.6 KiB13.6 KiB−6 B (−0.0%)15.1 KiB✅
CodexThread snapshot wire7.0 KiB7.0 KiB0 B (0.0%)7.3 KiB✅
CodexLive turn WebSocket wire6.6 KiB6.6 KiB−6 B (−0.1%)7.8 KiB✅
CodexLive turn WebSocket decoded57.1 KiB57.1 KiB0 B (0.0%)66.4 KiB✅
CodexLive turn messages10100 (0.0%)21✅
ClaudeTotal thread wire13.7 KiB13.5 KiB−203 B (−1.4%)15.1 KiB✅
ClaudeThread snapshot wire7.0 KiB7.0 KiB+10 B (+0.1%)7.3 KiB✅
ClaudeLive turn WebSocket wire6.7 KiB6.5 KiB−213 B (−3.1%)7.8 KiB✅
ClaudeLive turn WebSocket decoded59.3 KiB57.8 KiB−1.5 KiB (−2.5%)66.4 KiB✅
ClaudeLive turn messages108−2 (−20.0%)21✅

Baseline: b01771c · PR result: e65c174 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Approved at e65c174

Macroscope's review found this PR approvable — The production change narrowly fixes project rename/remove lookup for stored workspace paths whose directories no longer exist, while preserving normalized and symlink-distinct behavior. Comprehensive CLI integration tests cover the new cases and no product defaults, schemas, sensitive packages, deployment settings, or static-analysis suppressions are changed.

You can add or adjust custom eligibility rules. Learn more.

Root audit complete for e65c1746: whole two-file diff plus remove/rename and WorkspacePaths context reviewed. Independently reran the ten focused actual CLI/SQLite tests; all pass, with 15 unrelated tests skipped. Exact stored-path fallback only applies when normal validation fails; successful normalization, distinct symlink entries, project IDs and force/thread rules remain unchanged. Current-head executed CI and configured reviews pass, with no unresolved review threads. Within the requested simple-fix merge scope. #4851 stays open for its different lost-environment/client-cache report.

GPT 6 Astra via Codex in T3 Code.