Fission-AI/OpenSpec · error

A concurrent file appeared at

Error message

A concurrent file appeared at ${update.target} while archive was retiring it.

What it means

Thrown during deferred retirement in the specs-apply retire flow: after renaming the spec file aside to a displaced path, an lstat on the original target unexpectedly succeeded — meaning another process created a file at that path between the rename and the check. The retirement aborts rather than clobbering the newcomer.

Solutions

  1. Identify the concurrent writer (editor sync, another openspec process) and stop it, then rerun the archive
  2. Manually reconcile the concurrently-created file at the target path
  3. Re-run the archive after the concurrent operation completes
Defensive patterns

Strategy: try-catch

When it happens

Trigger: Thrown at src/core/specs-apply.ts:843 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of Fission-AI/OpenSpec@6926ccb18a (2026-08-25). Data as JSON: /api/errors/edd83a8fd5437fc0. Report an issue: GitHub.

Appendix: source

Thrown at src/core/specs-apply.ts:843

      throw new Error(
        `Could not retire capability '${update.id}': ${update.target} resolves outside ` +
          `${mainSpecsDir}. Remove the external file by hand, or replace the symlink and rerun.`
      );
    }
  }

  let displacedPath: string | undefined;
  try {
    await options.beforeMutate?.();
    if (options.verifyDisplaced) {
      const displaced = `${update.target}.openspec-retire-${randomUUID()}`;
      displacedPath = displaced;
      await fs.rename(update.target, displaced);
      try {
        await options.verifyDisplaced(displaced);
        try {
          await fs.lstat(update.target);
          throw new Error(
            `A concurrent file appeared at ${update.target} while archive was retiring it.`
          );
        } catch (targetError) {
          if ((targetError as NodeJS.ErrnoException).code !== 'ENOENT') throw targetError;
        }
        if (!options.deferDelete) await fs.unlink(displaced);
      } catch (error) {
        try {
          await fs.lstat(update.target);
          throw new Error(
            `${error instanceof Error ? error.message : String(error)} ` +
              `A concurrent file now occupies ${update.target}; the displaced spec was retained at ${displaced}.`
          );
        } catch (targetError) {
          if ((targetError as NodeJS.ErrnoException).code !== 'ENOENT') throw targetError;
        }
        await fs.rename(displaced, update.target);
        throw error;

View on GitHub (pinned to 6926ccb18a)