santifer/career-ops · critical

Safety violation ( ) and revert also failed ( )

Error message

Safety violation (${violation.message}) and revert also failed (${revertErr.message})

What it means

update-system.mjs enforces that an update never overwrites user-layer files. When the applied update is detected to have touched user files, it raises the safety violation, attempts to revert the touched paths, and if the revert itself fails throws this combined error (original violation chained via `cause`). Unlike the successful-abort path, here the worktree may retain modifications to user files that could not be rolled back.

Solutions

  1. Inspect err.cause (the safety violation) and the listed user file(s); back up any local edits first.
  2. Manually restore the touched files: `git checkout -- <paths>` after saving your changes.
  3. Move your customizations into the user layer (cv.md, config/, modes/_profile.md, modes/_custom.md) so updates never touch them.
  4. Re-apply the update cleanly with `node update-system.mjs apply --confirm` once user files are out of the blast radius.

Example fix

// before: hand-edited modes/_shared.md (system layer)
cp modes/_shared.md /tmp/my-shared-backup.md
git checkout -- modes/_shared.md
// after: put the rule in the user layer instead
# move the rule into modes/_custom.md, then:
node update-system.mjs apply --confirm
Defensive patterns

Strategy: validation

Validate before calling

const USER_LAYER = [/^cv\.md$/, /^config\//, /^modes\/_profile\.md$/, /^modes\/_custom\.md$/, /^modes\/_brief\.md$/, /^data\//, /^reports\//, /^output\//];
const userTouched = updatedPaths.filter(p => USER_LAYER.some(re => re.test(p)));
if (userTouched.length) throw new Error(`user files in update set: ${userTouched.join(', ')}`);

Try / catch

try {
  applyUpdate();
} catch (e) {
  if (e.message.startsWith('Safety violation')) {
    console.error('back up listed user files, git restore them, then re-apply');
  } else throw e;
}

Prevention

When it happens

Trigger: An apply run whose updated file set includes user-layer paths (violatedUserPaths), followed by revertPaths() throwing — e.g. git checkout failing due to local modifications, a lock, or permissions.

Common situations: The user edited a system-layer file so the update flow classified it as user-touched and the revert collided with uncommitted changes; .git/index.lock present; read-only checkout; file held open by a watcher on Windows.

Understand the failure class

Background: "git command failed": what it means when a tool shells out to git and git exits non-zero — this error's family across 21 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16). Data as JSON: /api/errors/291cd5757927d26f. Report an issue: GitHub.

Appendix: source

Thrown at update-system.mjs:2450

      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 {
        revertPaths([...updated], initialStatusPaths);
      } catch (revertErr) {
        // If the revert itself fails, don't lose the safety-violation
        // diagnostic — chain it via `cause` so the user sees both.
        throw new Error(
          `Safety violation (${violation.message}) and revert also failed (${revertErr.message})`,
          { cause: violation },
        );
      }
      console.error(`User file(s) left as-is (your content was NOT overwritten):`);
      for (const f of violatedUserPaths) console.error(`  ${f}`);
      // `throw` (not `process.exit`) so the outer `finally` runs and
      // .update-lock is removed. Exiting here would leak the lock and
      // permanently block subsequent updates until the user deletes
      // it manually.
      throw violation;
    }

    // 5. Install any new dependencies
    try {
      execSync('npm install --silent', { cwd: ROOT, timeout: NPM_INSTALL_TIMEOUT_MS });
    } catch {
      console.log('npm install skipped (may need manual run)');

View on GitHub (pinned to e7abd431fc)