jackwener/OpenCLI · error · Error

Stale .git/index.lock found — remove it first

Error message

Stale .git/index.lock found — remove it first

What it means

toEnvelope is the CLI's central error-to-envelope mapper in src/errors.ts. Known CliError instances keep their own code/message; anything else falls into this UNKNOWN branch at line 277, where the envelope's message is just whatever getErrorMessage(err) recovered (e.g. err.message or String(err)) and the exit code is EXIT_CODES.GENERIC_ERROR. Hitting this means a non-CliError exception (a raw TypeError, network error, JSON.parse failure, etc.) escaped a command without being wrapped.

Source

Thrown at autoresearch/engine.ts:98

  }

  private log(msg: string): void {
    this.callbacks.onStatus?.(msg);
  }

  /** Phase 0: Precondition checks */
  private checkPreconditions(): void {
    // Git repo exists
    try { execStrict('git rev-parse --git-dir'); }
    catch { throw new Error('Not a git repository'); }

    // Clean working tree
    const status = exec('git status --porcelain');
    if (status) throw new Error(`Working tree not clean:\n${status}`);

    // No stale locks
    if (existsSync(join(ROOT, '.git', 'index.lock'))) {
      throw new Error('Stale .git/index.lock found — remove it first');
    }

    // Not detached HEAD
    try { execStrict('git symbolic-ref HEAD'); }
    catch { throw new Error('Detached HEAD — checkout a branch first'); }
  }

  /** Phase 5: Run verify command and extract metric */
  private runVerify(): number | null {
    this.log('  verify...');
    const output = exec(this.config.verify, { timeout: 300_000 });
    return extractMetric(output);
  }

  /** Phase 5.5: Run guard command */
  private runGuard(): boolean {
    if (!this.config.guard) return true;
    this.log('  guard...');

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the envelope's message (and optional cause/trace fields) to identify the underlying raw exception, then fix that root cause.
  2. Wrap known failure modes in CliError with a specific code so users get actionable codes instead of UNKNOWN.
  3. Use the trace fields (traceId/summaryPath) if present to inspect what the CLI was doing when the raw error escaped.
  4. If the message is empty or 'undefined', the thrown value wasn't an Error — add logging of the raw thrown value in the command path.

Example fix

// before
const data = JSON.parse(raw); // raw TypeError escapes as UNKNOWN
// after
let data;
try {
  data = JSON.parse(raw);
} catch (err) {
  throw new CliError('PARSE_ERROR', `Invalid JSON: ${getErrorMessage(err)}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (err instanceof CliError) {
  // known, coded failure — envelope preserves err.code
} else {
  // will surface as code 'UNKNOWN'; log raw value now before mapping
  console.error('raw thrown value:', typeof err, err);
}

Type guard

function isCliError(err: unknown): err is CliError {
  return err instanceof CliError;
}

Try / catch

try {
  await runCommand(argv);
} catch (err) {
  const envelope = toEnvelope(err);
  if (envelope.error.code === 'UNKNOWN') {
    logger.debug({ raw: err }, 'unwrapped error surfaced as UNKNOWN');
  }
  process.exitCode = envelope.error.exitCode;
  console.error(JSON.stringify(envelope, null, 2));
}

Prevention

When it happens

Trigger: Any command path that lets a plain Error (or non-Error value) reach the top-level envelope() call instead of throwing a CliError — e.g. unhandled TypeError inside a command, JSON.parse failure, uncaught promise rejection from a dependency.

Common situations: A library call throws a native error the CLI forgot to wrap; a bug (undefined property access) surfaces in production; an async callback rejects outside the CliError-wrapping layer; a non-Error value like a string is thrown.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/26d1404ba3f39593. Report an issue: GitHub.