archive: EPERM renaming the change directory on Windows also rolls back the spec write, silently
openspec archive fails on Windows with EPERM renaming the change directory. This looks like a regression of #197, which was closed as completed, and it now has a worse consequence than that report described: the spec write is rolled back with it, and the empty directory it leaves behind is invisible to git.
Reproduced twice in a row, on two different changes, in the same repository.
Environment
@fission-ai/openspec1.13.0- Windows 11, Node 24.19.0, npm 11.17.0
- The repository is a git worktree (
git worktree add), which may be relevant: the change directory lives under a linked working tree rather than the main checkout.
What happens
$ npx openspec archive unblock-write-dialogs --yes
Task status: ✓ Complete
Specs to update:
shell/write-feedback: create
Applying changes to openspec/specs/shell/write-feedback/spec.md:
+ 4 added
Totals: + 4, ~ 0, - 0, → 0
Specs updated successfully.
✖ Error: Could not safely stage <repo>\openspec\changes\\\unblock-write-dialogs
before the fallback archive copy (EPERM: operation not permitted, rename
'<repo>\openspec\changes\\\unblock-write-dialogs' ->
'<repo>\openspec\changes\.openspec-move-406aba35-0959-4f99-b11d-364e8b32b3ba').
No fallback copy was attempted.
Why this is worse than a failed move
"Specs updated successfully" is printed, and then undone.
After the error:
openspec/specs/<capability>/spec.mddoes not exist.openspec/specs/<capability>/does exist, as an empty directory.git statusis clean, because git does not track empty directories.openspec list --specsdoes not show the capability.
So the failure is easy to read as "the move failed, the spec landed, I will move the folder by hand" — and the spec is silently missing. It took inspecting find openspec/specs -type d to notice, because every other signal said nothing had happened.
Workaround
Complete it by hand. The only transformation needed is the delta's operation header:
# 1. delta -> main spec: "## ADDED Requirements" becomes "## Requirements"
# (copy openspec/changes/<name>/specs/<capability>/spec.md to
# openspec/specs/<capability>/spec.md with that one substitution)
# 2. move the change
cp -r openspec/changes/<name> openspec/changes/archive/<date>-<name>
rm -rf openspec/changes/<name>
git mv fails with the same EPERM, but cp -r followed by rm -rf succeeds every time. That is the part I find most suggestive: the directory can be copied and deleted, just not renamed, which points at a handle held open across the rename rather than a permission problem. #197 reported the same thing, that an equivalent PowerShell Move-Item works immediately afterwards.
Suggestions
- Do not roll back the spec write when only the directory move fails. They are independent, and the spec write is the part that matters. Leaving it in place would turn this into a cosmetic failure.
- Fall back to copy-then-delete when the rename raises
EPERM. The message says "No fallback copy was attempted" — the fallback appears to be gated behind a staging rename that is itself the thing failing. - Do not leave the empty capability directory behind, or create it only once the file is about to be written. An empty directory that git cannot see is what makes this silent.
Source: Fission-AI/OpenSpec