* fix: add recovery script for #1364 .gsd/ data-loss regression
Adds scripts/recover-gsd-1364.sh to help users whose .gsd/ files were
deleted by the ensureGitignore bug in v2.33.x–v2.35.x.
The script handles both damage scenarios:
- Scenario A: .gsd files deleted in working tree but not yet committed
- Scenario B: git rm --cached .gsd/ was committed (files gone from HEAD)
Steps performed:
1. Detects whether the repo is affected (symlink check, .gitignore scan,
git history scan)
2. Finds the last clean commit before ".gsd" was added to .gitignore
3. Restores all deleted .gsd/ files via git checkout <clean-commit> -- .gsd/
4. Removes the bare ".gsd" line from .gitignore
5. Stages both changes and prints the ready-to-commit command
Supports --dry-run to preview without making changes.
Safe to run on unaffected repos — exits early with no modifications.
Closes#1364
* fix: add Windows PowerShell recovery script for #1364
Adds scripts/recover-gsd-1364.ps1, a PowerShell equivalent of the bash
recovery script for users on Windows.
Windows-specific differences handled:
- Junction detection: GSD's migrateToExternalState() uses symlinkSync()
with type "junction" on Windows instead of a POSIX symlink. The script
checks Get-Item.LinkType for both "SymbolicLink" and "Junction" so
migrated repos exit cleanly on step 1.
- .gitignore rewrite uses [System.IO.File]::WriteAllLines() with UTF-8
no-BOM encoding to match git's expectations on Windows, rather than
shell redirection which can introduce BOM or CRLF issues.
- All git invocations use execFileSync-style array args via Invoke-Git
helper — no shell string eval, no quoting edge cases.
- Colour output uses Write-Host -ForegroundColor instead of ANSI escapes.
- -DryRun is a proper PowerShell switch parameter.
Also updates recover-gsd-1364.sh header to:
- Clarify it is Linux/macOS only
- Point Windows users to the .ps1
- Correct the affected version range to v2.30.0-v2.35.x (was 2.33.x)
- Reference the three residual vectors on v2.36.0-v2.38.0 (PR #1635)
Usage on Windows:
powershell -ExecutionPolicy Bypass -File scripts\recover-gsd-1364.ps1
powershell -ExecutionPolicy Bypass -File scripts\recover-gsd-1364.ps1 -DryRun
* fix(gsd): close residual #1364 data-loss vectors on v2.36.0+
Two targeted fixes that close the three remaining paths where .gsd/
tracked files can still be silently deleted after the v2.36.0 fix.
--- Path 1: hasGitTrackedGsdFiles fails open on git error (gitignore.ts)
nativeLsFiles() swallows git failures via allowFailure=true and returns
[], making hasGitTrackedGsdFiles() indistinguishable between "nothing
tracked" and "git failed". On any transient git failure (locked index,
binary not on PATH, corrupted .git/index), the function returned false
and .gsd was added to .gitignore, deleting all tracked state.
Fix: after nativeLsFiles returns [], verify git is reachable with a
cheap rev-parse call. If git is unavailable, return true (fail safe —
assume tracked). The outer catch also returns true instead of false.
--- Path 2: migration never cleans git index (migrate-external.ts)
migrateToExternalState() correctly creates the .gsd symlink/junction but
never ran `git rm -r --cached .gsd/`. All previously tracked .gsd/* files
remained in the git index pointing through the new symlink, which git
cannot follow — causing PROJECT.md, milestones/, REQUIREMENTS.md etc. to
appear as deleted in git status immediately after every migration.
Fix: after the symlink is verified, run:
git rm -r --cached --ignore-unmatch .gsd
--ignore-unmatch makes this a no-op on fresh/untracked projects.
--- Path 3: race between migration and ensureGitignore
Resolved by Path 2. If migration always cleans the index, the race
window (another process converting .gsd/ to a symlink between the
migrateToExternalState() and ensureGitignore() calls) is harmless —
the index is already clean and there is nothing to lose.
--- Tests added (gitignore-tracked-gsd.test.ts)
- hasGitTrackedGsdFiles returns true (fail-safe) when git is unavailable
(simulated via .git/index.lock to force git ls-files failure)
- migrateToExternalState cleans git index so tracked files don't show
as deleted after successful migration
Fixes residual vectors from #1364 (original fix: #1367, v2.36.0)
* fix(recovery): add Scenario C support to recover-gsd-1364 scripts
Scenario C: .gsd/ is already a symlink/junction (migration succeeded on
the filesystem) but `git rm -r --cached .gsd/` was never run, leaving
tracked .gsd/* files appearing as deleted in git status.
Both bash and PowerShell scripts previously exited early at Step 1 when
they detected a symlink. Now they continue with a dedicated Scenario C
path through all steps:
- Step 1: sets GSD_IS_SYMLINK flag, continues instead of exiting
- Step 2: inverted .gitignore check — warns if .gsd is MISSING (should
be present for external-state layout) rather than if it's present
- Step 3: skips commit-history scan (index issue only, no file restore
needed); exits clean if no stale entries found
- Step 4: skips damage-commit search (nothing to restore from history)
- Step 5: runs `git rm -r --cached --ignore-unmatch .gsd` to clean the
stale index entries instead of restoring files from a prior commit
- Step 6: appends .gsd to .gitignore instead of removing it
- Step 7: stages only .gitignore (not .gsd/) to avoid the "gitignored
path" error; the index cleanup from Step 5 is already staged
- Summary: uses a distinct commit message for Scenario C
Smoke-tested against a synthetic repo that replicates the exact Scenario
C failure mode (symlink in place, git rm --cached never run).