santifer/career-ops · error · Error

Validation failed (${err.message}) and revert also failed ($

Error message

Validation failed (${err.message}) and revert also failed (${revertErr.message})

What it means

Thrown only when TWO independent failures stack inside apply()'s post-update safety validation. First, the loop that iterates gitStatusEntries() to confirm no user-layer files were touched throws (so the safety invariant cannot be evaluated — the code fails closed rather than assume safety). Second, the immediate remedy revertPaths(updated, initialStatusPaths) — which runs `git checkout --` over the system paths just applied — also throws. The original validation error is preserved via `cause` so neither diagnostic is lost; the comment notes whatever broke `git status` usually also breaks `git checkout --`.

Source

Thrown at update-system.mjs:1290

        for (const userPath of USER_PATHS) {
          if (file.startsWith(userPath)) {
            console.error(`SAFETY VIOLATION: User file was modified: ${file}`);
            violatedUserPaths.add(file);
          }
        }
      }
    } catch (err) {
      // Fail closed: if we can't validate the safety invariant we must
      // not silently proceed — that would let a real violation slip
      // through. Revert what we already applied and abort.
      console.error(`Aborting: could not validate user-layer safety (${err.message}).`);
      try {
        revertPaths(updated, initialStatusPaths);
      } catch (revertErr) {
        // If the revert itself fails (likely whatever broke `git
        // status` also broke `git checkout --`), don't lose the
        // original validation error — chain it via `cause`.
        throw new Error(
          `Validation failed (${err.message}) and revert also failed (${revertErr.message})`,
          { cause: err },
        );
      }
      throw err;
    }

    if (violatedUserPaths.size > 0) {
      console.error('Aborting: user files were touched. Rolling back system files...');
      // Revert ONLY the system-layer updates — never `git checkout` the
      // violated user paths back to HEAD. Doing so would overwrite the
      // user's working-tree content (accumulated STAR+R stories, local
      // edits) with whatever is committed upstream, causing data loss.
      // The user files were flagged as touched by the update, not by the
      // user; leaving them as-is is the safe choice — the user decides
      // what to do with them.
      const violation = new Error('Update aborted: user files were touched.');
      try {

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Inspect err.cause first — it holds the original validation failure (usually the git error); that root cause is what both steps share.
  2. Remove a stale lock if present: `rm -f .git/index.lock` (only if no git process is running), then re-run `node update-system.mjs apply`.
  3. Repair a corrupt index: `git read-tree HEAD` or, if that fails, `rm .git/index && git reset` (mixed reset to HEAD, preserving working tree) to rebuild it, then retry the updater.
  4. Free disk space if full, then retry — checkout writes can leave the tree half-applied; run `git status` + `git checkout -- .` to reconcile before re-running apply.
  5. Verify git is on PATH and not mid-upgrade (`git --version`); a broken git install makes both status and checkout fail identically.
  6. If .git ownership changed, run `git config --global --add safe.directory <ROOT>` to clear the 'dubious ownership' refusal.

Example fix

# before: apply aborts with the combined error, repo left mid-update
node update-system.mjs apply
# after: repair the shared root cause (stale lock), then re-run
rm -f .git/index.lock
git status   # confirm git works again
node update-system.mjs apply
Defensive patterns

Strategy: try-catch

Validate before calling

// Before applying, sanity-check git is operational on ROOT — the same two
// operations that fail (status + checkout) are what the validator and revert need.
import { execFileSync } from 'node:child_process';
function gitHealthy(root) {
  const probes = [
    ['status', '--porcelain'],
    ['checkout', '-q', '--', '.'], // no-op on a clean tree; reveals lock/corruption
  ];
  for (const args of probes) {
    try { execFileSync('git', args, { cwd: root, stdio: 'ignore', timeout: 30000 }); }
    catch (e) { return { ok: false, args, error: e.message }; }
  }
  return { ok: true };
}
// gate the update: const h = gitHealthy(ROOT); if (!h.ok) -> repair before apply.

Try / catch

// Distinguish the double-failure from a plain update error so repair logic runs.
try {
  await import('./update-system.mjs'); // or spawn apply
} catch (err) {
  const msg = String(err?.message || err);
  if (msg.startsWith('Validation failed (') && msg.includes('and revert also failed')) {
    // err.cause is the original git error; repair git state, then re-run.
    console.error('Repo in broken git state:', err.cause?.message || err.cause);
    console.error('Repair path: rm -f .git/index.lock; git read-tree HEAD');
    process.exit(2);
  }
  throw err;
}

Prevention

When it happens

Trigger: apply() has already checked out system paths from the upstream ref when git enters a broken state mid-update: a corrupt .git/index, a stale .git/index.lock left by a killed prior process, a permissions/ownership change on .git, a full disk, or the git binary disappearing from PATH. gitStatusEntries() then throws, the revert via git checkout throws for the same root cause, and this combined error is raised.

Common situations: Disk filled mid-update (writes succeed for checkout, then index read fails); a previous update-system.mjs was SIGKILLed and left .git/index.lock behind; running as a different user than owns .git (permissions); .git/index corruption after a crash or fsync failure; antivirus/FS monitor locking .git on Windows; git upgraded/downgraded across a format-version boundary.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/3bc4152e4c451063. Report an issue: GitHub.