fix: [#830] project_scan 32% failure rate — defensive spawn + degraded tracking

Merged
AN
5mo ago
+310-51

What does this PR do?

Fixes project_scan failing 32% of the time across 1,171 distinct users (telemetry-2026-05-21, 2,503 machines, 14 days). It's the first-interaction tool, so 32% failure at boot is a major conversion killer for new installs.

Two root causes (both addressed):

  1. detectGit() had no try/catch around Bun.spawnSync(["git", ...]). Bun's spawnSync THROWS when the binary is missing from PATH, not just when it exits non-zero. Any environment without git installed (minimal Docker images, fresh Windows installs without Git for Windows, some CI runners) crashed the entire tool — even though the other sub-detections didn't depend on git.

  2. The PII masker shredded the diagnostic. Bun's error Executable not found in $PATH: "git" became Executable not found in $PATH: ? in telemetry (quoted strings → ?), making the bug look like a generic shell failure and hiding it for two months.

Fix:

  • New safeSpawnSync helper that catches the throw and returns null so callers see a deterministic "command failed" instead of an exception. Applied to every Bun.spawnSync call in detectGit().
  • Every detection in ProjectScanTool.execute() now wraps with .catch() recording the failure to a degraded: string[] field and continuing with a sentinel result.
  • New "Degraded Detections" section in the formatted output + degraded field in metadata lets the LLM see exactly what was unavailable.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Issue for this PR

Closes #830

How did you verify your code works?

  • tsgo --noEmit: pass, zero errors
  • bun test test/tool/project-scan.test.ts: 78 pass / 0 fail (1 new test)
  • New test "does not throw when git binary is missing from PATH" pins the regression — sets PATH to an empty directory, calls detectGit(), asserts it returns { isRepo: false } without throwing
  • Marker check: pass — no upstream-shared files touched

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes
  • Marker check passes
  • Linked to issue #830

Files changed (2):

  • packages/opencode/src/altimate/tools/project-scan.ts — safeSpawnSync helper, degraded tracking on every detection, Config.get() guarded, output surfaces degraded list
  • packages/opencode/test/tool/project-scan.test.ts — +1 test for the missing-git path

Expected telemetry impact: project_scan error rate drops from 32% to near-zero. Users without git get a successful scan with degraded: ["git"] instead of an opaque tool failure. Same defensive pattern protects against future Dispatcher/Config/Skill regressions that previously could crash the whole tool.

Related: Issue #827 / PR #828 fixed the finops_* 100% failure rate. This PR fixes the next P0 from the same triage doc (docs/internal/2026-05-21-p0-triage.md).


Summary by cubic

Make project_scan fail-safe by degrading partial detections instead of crashing, and now clearly distinguishes missing git, clean non-repo, and degraded git errors. Fixes #830 and should drop the 32% error rate to near-zero.

  • Bug Fixes
    • Added and exported safeSpawnSync; routed all git and tool-version checks through it with timeout support.
    • detectGit() now returns gitAvailable and optional gitError; output shows "git not found" or "git error (exit N)" vs "Not a git repository"; metadata.git includes both.
    • Wrapped all sub-detections and Dispatcher calls with .catch(); track degraded as a Set, sort it, always emit metadata.degraded, and append (<n> degraded) to the title; added a "Degraded Detections (n)" section in output.
    • Guarded Config.get(); mark "skills", "post-connect-suggestions", "python-engine", "warehouse.list", "dbt.profiles", "warehouse.discover", "schema.cache_status", and "dbt.manifest" on failure.
    • Tests: added safeSpawnSync cases (missing binary, success, non-zero, timeout) and detectGit contracts (gitAvailable true in normal runs; clean non-repo leaves gitError undefined); removed PATH-mutation approach.

Summary by CodeRabbit

  • Bug Fixes

    • Scan tolerates partial failures across sub-detections and records degraded items instead of aborting.
  • New Features

    • Adds a "Degraded Detections" section, appends a degraded-count suffix to the title, and includes degraded details in scan metadata.
  • Improvements

    • More clearly reports VCS availability vs. "not a repository" and treats failed tool checks as not-installed.
  • Tests

    • Added tests for missing system binaries, non-zero exits, timeouts, and degraded-scan scenarios.

Review Change Stack