fix(brain-sync): second review pass on the case-collision hold

- The repair loop also folds the dropped spelling's untracked (held) files
  and says to check for leftovers before removing the folder on Linux.
- The cached outgoing verdict also keys on HEAD's symbolic target.
- The outgoing check no longer needs a temp file, so a failed mktemp can
  no longer skip it.

Co-authored-by: Kkkkkuro <180382840+Kkkkkuro@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
committed 2026-10-17 17:31:41 +00:00
1 parent 26e9bca865
commit 80e0fab49e
2 files changed
+20 -19

No files matched your search

+6 -10
View File
@@ -783,27 +783,23 @@ stage_paths() {
# spellings, and a retained local commit can be pushed with an empty queue.
# Also records the tree's case-colliding names for skill start. Without
# python3 no drain can stage anything, so there is no new tree to check. A
# passing verdict is cached by the commit ids of HEAD and every ref (origin's
# included, the comparison baseline), so the empty-queue drain at every skill
# boundary skips the tree walk until one of them moves.
# passing verdict is cached by HEAD's target and the commit ids of every ref
# (origin's included, the comparison baseline), so the empty-queue drain at
# every skill boundary skips the tree walk until one of them moves.
validate_outgoing_paths() {
local out err key cached cache="$GSTACK_HOME/.brain-case-check"
local out key cached cache="$GSTACK_HOME/.brain-case-check"
command -v python3 >/dev/null 2>&1 || return 0
key=$(git -C "$GSTACK_HOME" show-ref --head 2>/dev/null | cksum)
key=$({ head -1 "$GSTACK_HOME/.git/HEAD"; git -C "$GSTACK_HOME" show-ref --head; } 2>/dev/null | cksum)
cached=$(head -1 "$cache" 2>/dev/null || true)
if [ "${cached%% *}" = "$key" ]; then
[ -n "$ST_CASE_NAMES" ] || ST_CASE_NAMES=${cached#* }
return 0
fi
err=$(mktemp "${TMPDIR:-/tmp}/brain-sync-outgoing.XXXXXX") || return 0
if ! out=$(python3 "$SCRIPT_DIR/gstack-brain-sync-index.py" --outgoing "$GSTACK_HOME" 2>"$err"); then
if ! out=$(python3 "$SCRIPT_DIR/gstack-brain-sync-index.py" --outgoing "$GSTACK_HOME"); then
ST_CASE_NAMES=${out%$'\r'}
write_status "error" "outgoing artifact path validation refused: a local commit adds a path that differs only by letter case from a path on the remote; local commits and queue preserved. Repair: https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#artifact-case-collision"
cat "$err" >&2
rm -f "$err"
return 1
fi
rm -f "$err"
out=${out%$'\r'}
printf '%s\t%s\n' "$key" "$out" > "$cache" 2>/dev/null || true
[ -n "$ST_CASE_NAMES" ] || ST_CASE_NAMES=$out
+14 -9
View File
@@ -658,19 +658,21 @@ unaffected; only the sync holds files.
2. Fold the other spelling's tracked files into the kept one. Read both
spellings from git, because a Mac's working tree shows only one of them,
and the working files too, because they hold the held, unsynced writes.
This loop folds JSONL logs (concatenated, duplicate lines dropped) and
prints every other file for you to decide:
This loop covers the dropped spelling's tracked files and its untracked
ones (held writes that never synced). It folds JSONL logs (concatenated,
duplicate lines dropped) and prints every other file for you to decide:
```bash
cd ~/.gstack
keep=projects/MyApp; drop=projects/myapp
git -c core.ignorecase=false ls-files -- "$drop" | while IFS= read -r f; do
{ git -c core.ignorecase=false ls-files -- "$drop"; git -c core.ignorecase=false ls-files --others -- "$drop"; } | sort -u | while IFS= read -r f; do
rel=${f#"$drop"/}
case "$rel" in *.jsonl) ;; *) echo "decide by hand: $f"; continue ;; esac
{ git show ":$keep/$rel"; git show ":$f"; cat "$keep/$rel" "$f"; } 2>/dev/null | awk '!seen[$0]++' > "$HOME/.gstack-fold.tmp"
mkdir -p "$keep/$(dirname "$rel")" && mv "$HOME/.gstack-fold.tmp" "$keep/$rel"
git -c core.ignorecase=false add -f -- "$keep/$rel"
git -c core.ignorecase=false rm -q --cached -- "$f"
git -c core.ignorecase=false rm -q --cached --ignore-unmatch -- "$f"
echo "folded: $keep/$rel"
done
```
@@ -678,7 +680,7 @@ unaffected; only the sync holds files.
`decisions.active.json`), compare `git show ":$keep/<file>"` with
`git show ":$drop/<file>"`, write the content to keep to `"$keep/<file>"`,
then run the same `git -c core.ignorecase=false add -f -- "$keep/<file>"`
and `git -c core.ignorecase=false rm -q --cached -- "$drop/<file>"`. Never
and `git -c core.ignorecase=false rm -q --cached --ignore-unmatch -- "$drop/<file>"`. Never
concatenate a JSON snapshot. Then commit and push:
```bash
@@ -686,12 +688,15 @@ unaffected; only the sync holds files.
git push origin HEAD
```
On Linux the two folders are separate on disk: remove the dropped folder
after the commit (`rm -r "$drop"`). On a Mac or Windows they are the same
folder, so do not remove anything.
On Linux the two folders are separate on disk: after the commit, check
that `git -c core.ignorecase=false ls-files --others -- "$drop"` lists
nothing you have not folded or moved, then remove the dropped folder
(`rm -r "$drop"`). On a Mac or Windows they are the same folder, so do
not remove anything.
3. Clear the held records of the dropped spelling and queue the kept files:
`gstack-brain-sync --skip-file <dropped path>` for each held path that
`--status` lists, then `gstack-brain-enqueue <kept path>` and
`--status` lists, then `gstack-brain-enqueue <kept path>` for each
`folded:` line and each file you moved by hand, and
`gstack-brain-sync --once`.
4. On every other machine, let the next skill start pull the repair (or run
`git -C ~/.gstack pull`). The line disappears once no tracked path