santifer/career-ops · error

career-ops at is not a git checkout of its own, so git…

Error message

career-ops at ${ROOT} is not a git checkout of its own, so git operations would land in the enclosing repository at ${foreignToplevel} — this happens when the install was unpacked from a ZIP or copied without its .git directory. Nothing was changed. To make updates work, clone career-ops fresh (git clone ${CANONICAL_REPO}) and move your user-layer files (cv.md, config/, data/, reports/ — see DATA_CONTRACT.md) into the new clone.

What it means

assertOwnGitToplevel guards update-system's apply() and rollback(): before any mutating git operation it verifies that the career-ops install directory is its own git repository. If git's toplevel for ROOT resolves to an enclosing repository (foreignToplevel), running updates would commit/push into that unrelated repo, so the function throws with remediation instructions and performs no changes.

Solutions

  1. Clone career-ops fresh with git clone <CANONICAL_REPO> so it has its own .git directory.
  2. Move user-layer files (cv.md, config/, data/, reports/, jds/, output/ — see DATA_CONTRACT.md) from the old install into the new clone.
  3. Re-point any configuration (CAREER_OPS_ROOT, marker files, cron jobs) at the new clone and delete the .git-less copy.
  4. As a last resort, git init inside the install and add the canonical repo as origin — but a fresh clone is the supported path.

Example fix

# before (ZIP-unpacked install inside another repo)
~/myrepo/career-ops/   # no .git of its own

# after
git clone https://github.com/career-ops/career-ops.git ~/career-ops
cp -r ~/myrepo/career-ops/{cv.md,config,data,reports} ~/career-ops/
Defensive patterns

Strategy: validation

Validate before calling

import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
const toplevel = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: root, encoding: 'utf-8' }).trim();
if (toplevel !== root) {
  throw new Error(`career-ops install at ${root} is not its own git checkout (toplevel: ${toplevel}); re-clone before updating`);
}

Type guard

function isOwnGitCheckout(root) {
  try {
    const top = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: root, encoding: 'utf-8' }).trim();
    return top === root;
  } catch { return false; }
}

Try / catch

try {
  // apply() or rollback()
} catch (err) {
  if (err.message.includes('not a git checkout of its own')) {
    console.error('Re-clone career-ops and migrate user-layer files per DATA_CONTRACT.md before updating.');
  } else throw err;
}

Prevention

When it happens

Trigger: Calling apply() or rollback() in update-system.mjs when the install directory lacks its own .git — e.g. the tree was unzipped from a release archive or copied without .git — while sitting inside some other git repository, so gitToplevelMismatch() returns the enclosing repo path.

Common situations: Downloading the career-ops ZIP from GitHub instead of git clone and unpacking it inside a home directory or project that is itself a git repo; rsync/scp'ing an install to a new machine without the .git directory; nesting a career-ops checkout inside a dotfiles repo.

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/bdec3d2a0dadd9b0. Report an issue: GitHub.

Appendix: source

Thrown at update-system.mjs:849

  const canonicalize = realpathSync.native ?? realpathSync;
  let same;
  try {
    same = canonicalize(toplevel) === canonicalize(root);
  } catch {
    same = resolve(toplevel) === resolve(root);
  }
  return same ? null : toplevel;
}

/**
 * Throw when git operations from ROOT would land in an enclosing repository.
 * First statement of apply() and rollback(); check() reports a status instead.
 */
function assertOwnGitToplevel() {
  const foreignToplevel = gitToplevelMismatch();
  if (foreignToplevel) {
    throw new Error(
      `career-ops at ${ROOT} is not a git checkout of its own, so git operations would land in the enclosing repository at ${foreignToplevel} — this happens when the install was unpacked from a ZIP or copied without its .git directory. Nothing was changed. To make updates work, clone career-ops fresh (git clone ${CANONICAL_REPO}) and move your user-layer files (cv.md, config/, data/, reports/ — see DATA_CONTRACT.md) into the new clone.`,
    );
  }
}

/**
 * Paths the target manifest ships that did not materialize on disk.
 *
 * apply() reports success without checking that the checkout loop actually
 * produced a coherent install, so a client whose local manifest predates the
 * target's silently ends up missing every path added since — and only finds
 * out when the next script crashes with ERR_MODULE_NOT_FOUND (#1998).
 *
 * @param {string[]} targetPaths - SYSTEM_PATHS read from the target updater.
 * @returns {string[]} Entries present in FETCH_HEAD but absent locally.
 */
function missingFromTargetManifest(targetPaths) {
  const missing = [];
  for (const path of targetPaths) {

View on GitHub (pinned to e7abd431fc)