fix(desktop,host-service): classify environmental git failures as typed non-500s

Merged
KI
3mo ago
+540-240

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 funnel translateRawGitError in git-task-worker.ts only classified "not a git repository"; the cwd-unreadable variants crossed the boundary as raw GitError and missed the domain catches in branches.ts/status.ts.
  • desktop, in-process paths (DESKTOP-1A: changes.commit/push/pull/sync/fetch/createPR, changes.stageFiles/discardChanges/discardFiles/stash via security/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.ts only has the existsSync pre-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):

  • classifyEnvironmentalGitError message-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 in branches.ts/status.ts translate to the existing typed non-500 TRPCErrors (PRECONDITION_FAILED/{kind:"GIT_ENVIRONMENT"}, BAD_REQUEST/{kind:"NOT_GIT_REPO"}).
  • rethrowEnvironmentalGitError applies the same classification at the in-process boundaries (git-operations.ts procedures, security/git-commands.ts command helpers, staging.ts status helpers, projects.initGitAndOpen), rethrowing the same typed TRPCErrors directly. Domain errors are matched by name (never instanceof) since worker serialization strips prototypes.
  • NotGitRepoError moved from git.ts into git-errors.ts (re-exported from git.ts, so all existing importers and instanceof checks are unchanged) to avoid an import cycle.

host-service (trpc/router/git/utils/classify-git-error.ts, matching the #6199 pattern):

  • rethrowEnvironmentalGitError applied at the git.getStatus boundary: 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 classifyEnvironmentalGitError and rethrowEnvironmentalGitError; applies via new runGitOperation/runGit wrappers across commit/push/pull/sync/fetch/createPR, staging status reads, and projects.initGitAndOpen.
    • Push path classifies environmental failures before push-specific PRECONDITION_FAILED mapping to preserve cause.kind.
    • Worker funnels raw errors through classifyEnvironmentalGitError; moves NotGitRepoError to git-errors.ts and re-exports. Tests added in git-errors.test.ts; tightens no-main-process-blocking.test.ts after consolidation.
  • Host Service

    • Wraps git.getStatus with rethrowEnvironmentalGitError: 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 in classify-git-error.test.ts.

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.