fix(desktop,host-service): classify environmental git failures as typed non-500s
Problem
Environmental git failures — the worktree directory deleted out from under a running git process, macOS folder permissions denying reads (fatal: Unable to read current working directory: Operation not permitted / No such file or directory), or a directory that is not a git repository — propagate out of simple-git as plain GitErrors. The tRPC Sentry middleware reports every INTERNAL_SERVER_ERROR, so these routine lifecycle states report as bugs. DESKTOP-1A alone is ~5,300 events/day and the largest quota driver on the desktop project.
Root cause
The typed classification from #6164/#6199 only covers existsSync pre-checks and the exec-based getStatusNoLock. simple-git's own GitError for these environmental failures maps to nothing:
- desktop, worker path (
changes.getBranches→ DESKTOP-HQ): the worker funneltranslateRawGitErroringit-task-worker.tsonly classified "not a git repository"; the cwd-unreadable variants crossed the boundary as rawGitErrorand missed the domain catches inbranches.ts/status.ts. - desktop, in-process paths (DESKTOP-1A:
changes.commit/push/pull/sync/fetch/createPR,changes.stageFiles/discardChanges/discardFiles/stashviasecurity/git-commands.ts,projects.initGitAndOpen): simple-git runs in the main process with no message classification at all. - host-service (
git.getStatus→ HOST-SERVICE-X/Y):resolve-worktree.tsonly has theexistsSyncpre-check; the worktree can vanish between that check and the git spawn, and permission walls pass the check entirely.
Fix
Classification at throw/capture sites only — no Sentry-side filtering. Existing error messages are preserved verbatim; genuine unexpected git failures keep reporting as 500s.
desktop (workspaces/utils/git-errors.ts, one shared helper pair):
classifyEnvironmentalGitErrormessage-checks a caught git error ("Unable to read current working directory" either variant →GitEnvironmentError; "not a git repository" →NotGitRepoError). The worker funnel now uses it, so every worker task crosses the boundary in the domain vocabulary and the existing catches inbranches.ts/status.tstranslate to the existing typed non-500 TRPCErrors (PRECONDITION_FAILED/{kind:"GIT_ENVIRONMENT"},BAD_REQUEST/{kind:"NOT_GIT_REPO"}).rethrowEnvironmentalGitErrorapplies the same classification at the in-process boundaries (git-operations.tsprocedures,security/git-commands.tscommand helpers,staging.tsstatus helpers,projects.initGitAndOpen), rethrowing the same typed TRPCErrors directly. Domain errors are matched by name (neverinstanceof) since worker serialization strips prototypes.NotGitRepoErrormoved fromgit.tsintogit-errors.ts(re-exported fromgit.ts, so all existing importers andinstanceofchecks are unchanged) to avoid an import cycle.
host-service (trpc/router/git/utils/classify-git-error.ts, matching the #6199 pattern):
rethrowEnvironmentalGitErrorapplied at thegit.getStatusboundary: cwd gone →NOT_FOUND/{kind:"WORKTREE_MISSING"}, permission wall →PRECONDITION_FAILED/{kind:"GIT_ENVIRONMENT"}, not a repo →BAD_REQUEST/{kind:"NOT_GIT_REPO"}.
Unit tests cover both helpers (all three message variants, name-checked domain errors, and the no-op path for genuine failures); verified they fail under mutation of the classification.
Verification
bunx biome check <11 changed files>:
Checked 11 files in 15ms. No fixes applied.
cd apps/desktop && bun run typecheck:
$ tsc --noEmit
(exit 0)
cd packages/host-service && bun run typecheck:
$ tsc --noEmit --emitDeclarationOnly false
(exit 0)
bun test (desktop: new git-errors.test.ts + existing git-task-handlers.test.ts; host-service: new classify-git-error.test.ts + existing get-check-job-logs.test.ts):
19 pass / 0 fail (desktop, 2 files)
8 pass / 0 fail (host-service, 2 files)
Refs DESKTOP-HQ
Refs DESKTOP-1A
Refs HOST-SERVICE-X
Refs HOST-SERVICE-Y
https://claude.ai/code/session_9a505eb2-10a1-4f40-910d-5ee0d7f2e0b4
Summary by cubic
Classifies environmental simple-git failures as typed non-500 @trpc/server errors in Desktop and Host Service. Previously these routine states returned 500s and were reported to Sentry; now they return BAD_REQUEST, PRECONDITION_FAILED, or NOT_FOUND with cause kinds, while unexpected failures stay 500s. Messages are preserved verbatim.
-
Desktop
- Adds
classifyEnvironmentalGitErrorandrethrowEnvironmentalGitError; applies via newrunGitOperation/runGitwrappers across commit/push/pull/sync/fetch/createPR, staging status reads, andprojects.initGitAndOpen. - Push path classifies environmental failures before push-specific PRECONDITION_FAILED mapping to preserve
cause.kind. - Worker funnels raw errors through
classifyEnvironmentalGitError; movesNotGitRepoErrortogit-errors.tsand re-exports. Tests added ingit-errors.test.ts; tightensno-main-process-blocking.test.tsafter consolidation.
- Adds
-
Host Service
- Wraps
git.getStatuswithrethrowEnvironmentalGitError: CWD gone → NOT_FOUND/{kind:"WORKTREE_MISSING"}; permission wall → PRECONDITION_FAILED/{kind:"GIT_ENVIRONMENT"}; not-a-repo → BAD_REQUEST/{kind:"NOT_GIT_REPO"}. Tests added inclassify-git-error.test.ts.
- Wraps
No client migration required; codes and cause.kind are now consistent across worker and in-process paths.
Summary by CodeRabbit
-
Bug Fixes
- Git operations now provide clearer, actionable errors for missing repositories, deleted worktrees, and inaccessible working directories.
- Commit, push, pull, sync, fetch, branch, staging, reset, discard, stash, and pull request workflows handle environmental failures more reliably while preserving existing behavior.
- Git status retrieval now validates worktrees consistently.
-
Tests
- Added coverage for Git error classification across common repository and filesystem failure scenarios.