fix: pre-landing review round — 8 auto-fixes + 8 accepted findings hardened

The ship review army (4 specialists + red-team + checklist, 29 findings)
produced 8 mechanical auto-fixes and 11 decisions; the accepted set:

- win32 slug parity completed: lib/bin-context.ts gains the remote-first
  outermost walk + degraded-cache self-heal the bash side got this wave —
  the two implementations now agree on the stray-marker live-bug shape,
  pinned by shared fixtures (multi-specialist 9/10 finding).
- probe honors the plan's bounded-read decision: 256KB prefix, extraction
  semantics mirrored from parseTranscriptJsonl so probe/prepare can never
  diverge on the same file (>1MB transcript test).
- policy normalize parity: bash normalize() now matches canonicalizeRemote
  on .git/-trailing and uppercase-.GIT shapes (7-shape corpus pinned two
  ways) — a deny for those shapes could previously slip the transcript gate.
- session-update reclaim is TOCTOU-safe (atomic mv-aside on both branches).
- settings-hook: unparseable settings.json errors instead of being replaced
  with {}; ensure-event keys on (event, source) so matcher changes update
  in place — never zero or two registrations.
- dot-only slug guard at both parse sites (hostile 'url = ..' can't escape
  projects/); enqueue tmp-file janitor (1h TTL, inside the drain lock);
  brain-sync .migrating never clobbered; drop-queue/status count .migrating;
  snapshot -o warning correct + surfaced in diff mode; version-bump test
  order-dependence removed; uninstall clears the advance stamp.

Deferred with record: slug heal-probe cost sentinel (P3 TODO), FF_OK
conflation (noted, misdiagnosis-only).

270 pass / 0 fail across the 10 touched suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-17 13:20:20 -07:00
co-authored by Claude Fable 5
parent 9fecf0f16f
commit b7d44c45b4
18 changed files with 648 additions and 76 deletions
+25 -2
View File
@@ -196,6 +196,11 @@ migrate_legacy_queue() {
rm -f "$migrating" 2>/dev/null || true
fi
fi
# If the leftover STILL holds records, the conversion failed (e.g. python3
# unavailable). The mv below would overwrite it and destroy those records —
# exactly the never-destroy invariant above. Defer this run's migration;
# the next run retries both files.
[ -s "$migrating" ] && return 0
if [ -s "$QUEUE" ]; then
mkdir -p "$QUEUE_DIR" 2>/dev/null || return 0
mv -f "$QUEUE" "$migrating" 2>/dev/null || return 0
@@ -482,6 +487,13 @@ subcmd_once() {
# drain reads the spool (transition window for pre-spool writers).
migrate_legacy_queue
# Janitor: reap orphaned enqueue temp files. A writer killed between its
# tmp write and the atomic rename leaves `.tmp-*` behind forever — it never
# becomes a record and nothing else touches it. One hour is far beyond any
# live writer's write→rename window, so a fresh tmp (an in-flight enqueue)
# is never touched. Runs inside the run lock, so it can't race the drain.
find "$QUEUE_DIR" -maxdepth 1 -type f -name '.tmp-*' -mmin +60 -delete 2>/dev/null || true
local mode
mode=$("$CONFIG_BIN" get artifacts_sync_mode 2>/dev/null || echo off)
@@ -691,11 +703,13 @@ subcmd_status() {
echo '{"status":"unknown","message":"no status file yet"}'
fi
# Supplemental info (not in status file). Depth = spool record files plus
# any not-yet-migrated legacy queue lines (transition window).
# any not-yet-migrated legacy queue lines (transition window), including a
# crash-leftover .migrating file — its records are still pending too.
local queue_depth spool_depth legacy_depth
spool_depth=$(ls "$QUEUE_DIR"/*.json 2>/dev/null | wc -l | tr -d ' ')
legacy_depth=0
[ -f "$QUEUE" ] && legacy_depth=$(wc -l < "$QUEUE" | tr -d ' ')
[ -f "$QUEUE.migrating" ] && legacy_depth=$(( legacy_depth + $(wc -l < "$QUEUE.migrating" | tr -d ' ') ))
queue_depth=$(( spool_depth + legacy_depth ))
local last_push="never"
[ -f "$LAST_PUSH_FILE" ] && last_push=$(cat "$LAST_PUSH_FILE" 2>/dev/null || echo never)
@@ -727,7 +741,10 @@ subcmd_drop_queue() {
echo "Refusing: --drop-queue discards pending syncs. Pass --yes to confirm." >&2
exit 1
fi
# Remove spool record files, then truncate any legacy queue remnant.
# Remove spool record files, then truncate any legacy queue remnant
# including a crash-leftover .migrating file, whose records would otherwise
# resurrect on the next drain via migrate_legacy_queue after the user
# explicitly discarded the queue.
local n=0 f
for f in "$QUEUE_DIR"/*.json; do
[ -e "$f" ] || continue
@@ -739,6 +756,12 @@ subcmd_drop_queue() {
n=$(( n + legacy_n ))
: > "$QUEUE"
fi
if [ -f "$QUEUE.migrating" ]; then
local mig_n
mig_n=$(wc -l < "$QUEUE.migrating" | tr -d ' ')
n=$(( n + mig_n ))
rm -f "$QUEUE.migrating" 2>/dev/null || true
fi
if [ "$n" -eq 0 ]; then
echo "queue already empty"
exit 0
+2
View File
@@ -23,6 +23,7 @@
# .brain-queue.jsonl — legacy pending queue (pre-spool)
# .brain-discover-cursor — discover-new cursor
# .brain-last-push — timestamp marker
# .brain-worktree-last-advance — daily worktree-advance stamp (#2516)
# .brain-skip.txt — user-maintained skip list
# .brain-sync.lock.d/ — lock dir (if present)
# .brain-sync-status.json — health status
@@ -125,6 +126,7 @@ rm -f "$GSTACK_HOME/.brain-queue.jsonl.migrating" 2>/dev/null || true
rm -f "$GSTACK_HOME/.brain-discover-cursor" 2>/dev/null || true
rm -f "$GSTACK_HOME/.brain-last-push" 2>/dev/null || true
rm -f "$GSTACK_HOME/.brain-last-pull" 2>/dev/null || true
rm -f "$GSTACK_HOME/.brain-worktree-last-advance" 2>/dev/null || true
rm -f "$GSTACK_HOME/.brain-skip.txt" 2>/dev/null || true
rm -f "$GSTACK_HOME/.brain-sync-status.json" 2>/dev/null || true
rm -rf "$GSTACK_HOME/.brain-sync.lock.d" 2>/dev/null || true
+15 -6
View File
@@ -92,14 +92,23 @@ normalize() {
case "$head" in
*:*) url=$(printf '%s' "$url" | sed 's|:|/|') ;;
esac
# Lowercase BEFORE the suffix strips so a `.GIT` suffix still strips —
# parity with lib/gstack-memory-helpers' canonicalizeRemote, which strips
# `.git` case-insensitively. GitHub and most hosts are case-insensitive on
# paths anyway; collapsing avoids duplicate entries for "Foo/Bar" vs
# "foo/bar". (Parity is pinned by test/gbrain-repo-policy-client.test.ts:
# a key set through THIS normalize must be found via the canonicalized form
# memory-ingest passes to `get --batch`.)
url=$(printf '%s' "$url" | tr '[:upper:]' '[:lower:]')
# Strip trailing slash(es) FIRST, so ".git/" still loses its suffix (same
# order as canonicalizeRemote — slash-first, then .git, then re-strip).
while [ "${url%/}" != "$url" ]; do url="${url%/}"; done
# Strip trailing .git
url="${url%.git}"
# Strip trailing /
url="${url%/}"
# Lowercase the whole thing. GitHub and most hosts are case-insensitive on
# paths anyway; collapsing avoids duplicate entries for "Foo/Bar" vs
# "foo/bar".
printf '%s\n' "$url" | tr '[:upper:]' '[:lower:]'
# Re-strip trailing slash(es): a path remote ending in a `.git` directory
# component ("/repo/.git") exposes a new trailing slash once .git is gone.
while [ "${url%/}" != "$url" ]; do url="${url%/}"; done
printf '%s\n' "$url"
}
# ensure_file — create the policy file if missing, migrate if legacy.
+54 -22
View File
@@ -1077,17 +1077,6 @@ export function readNewFailures(
// ── Main ingest passes ─────────────────────────────────────────────────────
/**
* Lightweight attribution check: does a transcript have a resolvable git
* remote for its cwd? Extracts the cwd from the first JSONL line that has
* one (mirroring the logic in parseTranscriptJsonl) and calls
* resolveGitRemote. Avoids the full parse (body rendering, message counting)
* because probe only needs the yes/no answer.
*
* Non-transcript types (artifacts) always pass — the attribution filter in
* preparePages only applies to transcripts (#2394).
*/
/**
* The ONE attribution gate (#2394): a transcript is attributable iff its cwd
* resolves to a git remote. Both probeMode (via transcriptIsAttributable) and
@@ -1099,32 +1088,75 @@ function sessionIsAttributable(cwd: string | undefined | null): boolean {
return resolveGitRemote(cwd) !== "";
}
/**
* Bounded prefix for the probe's cheap-parse (plan C7): transcripts run to
* tens of MB, and the probe only needs the cwd, which both agent formats put
* on the FIRST records. 256KB is orders of magnitude past any real header.
*/
const TRANSCRIPT_PROBE_MAX_BYTES = 256 * 1024;
/**
* Lightweight attribution check: does a transcript have a resolvable git
* remote for its cwd? Reads a BOUNDED prefix (first 256KB, never the whole
* file — plan C7: the probe must stay a cheap parse on multi-MB transcripts),
* extracts the cwd with EXACTLY parseTranscriptJsonl's rules, and calls
* resolveGitRemote. Avoids the full parse (body rendering, message counting)
* because probe only needs the yes/no answer.
*
* Extraction MIRRORS parseTranscriptJsonl (the single source of truth for
* cwd semantics — keep the two in lockstep):
* - the first PARSEABLE line decides the format (Codex: type=session_meta
* or payload.id; else Claude Code);
* - Codex cwd comes from that FIRST record ONLY (payload.cwd || cwd) —
* a cwd appearing only on a later record is NOT used, exactly as
* parseTranscriptJsonl ignores it, so probe and prepare can never
* diverge on the same file;
* - Claude Code cwd comes from the first record that carries one;
* - unparseable lines are skipped (the truncated-tail case included).
*
* Non-transcript types (artifacts) always pass — the attribution filter in
* preparePages only applies to transcripts (#2394).
*/
function transcriptIsAttributable(path: string): boolean {
let raw: string;
try {
raw = readFileSync(path, "utf-8");
const fd = openSync(path, "r");
try {
const buf = Buffer.alloc(TRANSCRIPT_PROBE_MAX_BYTES);
const n = readSync(fd, buf, 0, buf.length, 0);
raw = buf.toString("utf-8", 0, n);
} finally {
closeSync(fd);
}
} catch {
return false;
}
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
if (lines.length === 0) return false;
// Detect format: Codex first line has type=session_meta, Claude Code
// has cwd on a user/assistant record.
let cwd = "";
let sawFirstParseable = false;
for (const line of lines) {
let rec: any;
try {
const rec = JSON.parse(line);
if (rec?.type === "session_meta") {
rec = JSON.parse(line);
} catch {
continue; // mirrors parseTranscriptJsonl: unparseable lines are skipped
}
if (!sawFirstParseable) {
sawFirstParseable = true;
// Format detection mirrors parseTranscriptJsonl's `first` record check.
const isCodex = rec?.type === "session_meta" || rec?.payload?.id != null;
if (isCodex) {
// Codex: cwd comes from the session_meta FIRST record only.
cwd = rec.payload?.cwd || rec.cwd || "";
break;
}
if (rec?.cwd) {
cwd = rec.cwd;
break;
}
} catch {
continue;
}
// Claude Code: first record with a cwd wins (the first record included).
if (rec?.cwd) {
cwd = rec.cwd;
break;
}
}
if (!cwd) return false;
+10 -3
View File
@@ -73,16 +73,23 @@ fi
[ -f "$_hb" ] || _hb="$LOCK_DIR"
[ -n "$(find "$_hb" -maxdepth 0 -mmin +$LOCK_TTL_MINUTES 2>/dev/null)" ]
}
# Reclaim is TOCTOU-safe via atomic mv-aside: `rm -rf` then `mkdir` lets TWO
# contenders both judge the lock stale, both remove it, and both win the
# mkdir (one rm can land between the other's rm and mkdir). `mv` of the lock
# dir is atomic — exactly one contender's mv succeeds; the loser's mv fails
# (ENOENT) and it backs off. The winner reaps the moved-aside dir at leisure.
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
if lock_is_expired; then
rm -rf "$LOCK_DIR" 2>/dev/null
mv "$LOCK_DIR" "$LOCK_DIR.reap.$$" 2>/dev/null || { log_entry "SKIP lock_contested"; exit 0; }
rm -rf "$LOCK_DIR.reap.$$" 2>/dev/null
mkdir "$LOCK_DIR" 2>/dev/null || { log_entry "SKIP lock_contested"; exit 0; }
log_entry "RECLAIMED lock_ttl_expired"
elif [ -f "$LOCK_DIR/pid" ]; then
LOCK_PID=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo 0)
if [ "$LOCK_PID" -gt 0 ] 2>/dev/null && ! kill -0 "$LOCK_PID" 2>/dev/null; then
# Stale lock — remove and re-acquire
rm -rf "$LOCK_DIR" 2>/dev/null
# Stale lock — mv aside atomically (see reclaim note above), re-acquire
mv "$LOCK_DIR" "$LOCK_DIR.reap.$$" 2>/dev/null || { log_entry "SKIP lock_contested"; exit 0; }
rm -rf "$LOCK_DIR.reap.$$" 2>/dev/null
mkdir "$LOCK_DIR" 2>/dev/null || { log_entry "SKIP lock_contested"; exit 0; }
else
# Live holder — or an empty/non-numeric pidfile inside the TTL
+35 -5
View File
@@ -83,7 +83,17 @@ case "$ACTION" in
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const hookCmd = process.env.GSTACK_HOOK_CMD;
let settings = {};
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); } catch {}
// An EXISTING file that does not parse must never be rewritten: the
// old catch{} folded it to {} and the atomic write below replaced the
// user permissions/env/other hooks with just ours. Refuse loudly.
if (fs.existsSync(settingsPath)) {
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
catch (e) {
console.error("error: " + settingsPath + " exists but is not valid JSON (" +
(e && e.message ? e.message : e) + "); refusing to rewrite it. Fix or move the file, then re-run.");
process.exit(1);
}
}
if (!settings.hooks) settings.hooks = {};
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
const exists = settings.hooks.SessionStart.some(entry =>
@@ -97,7 +107,7 @@ case "$ACTION" in
const tmp = settingsPath + ".tmp";
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
fs.renameSync(tmp, settingsPath);
' 2>/dev/null
'
;;
remove)
@@ -178,18 +188,34 @@ case "$ACTION" in
const ensure = process.env.GSTACK_ENSURE === "1";
let settings = {};
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); } catch {}
// An EXISTING file that does not parse must never be rewritten: the
// old catch{} folded it to {} and the atomic write below replaced the
// user permissions/env/other hooks with just ours. Refuse loudly.
if (fs.existsSync(settingsPath)) {
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
catch (e) {
console.error("error: " + settingsPath + " exists but is not valid JSON (" +
(e && e.message ? e.message : e) + "); refusing to rewrite it. Fix or move the file, then re-run.");
process.exit(1);
}
}
const before = JSON.stringify(settings, null, 2);
if (!settings.hooks) settings.hooks = {};
if (!settings.hooks[event]) settings.hooks[event] = [];
// Identity key is (event, source): any existing entry carrying OUR
// source tag for this event IS the entry to compare/update — a matcher
// change must update it in place, never push a SECOND gstack entry
// (the old key included the matcher, so a future matcher change would
// have duplicated the registration). Untagged legacy entries are still
// adopted when both matcher and command line up.
const matchesEntry = (entry) => {
if (entry._gstack_source === source) return true;
const sameMatcher = (entry.matcher || "") === matcher;
const sameCommand = entry.hooks && entry.hooks[0] && entry.hooks[0].command === cmd;
const sameSource = entry._gstack_source === source;
return sameMatcher && (sameSource || sameCommand);
return sameMatcher && sameCommand;
};
let existing = settings.hooks[event].find(matchesEntry);
@@ -202,6 +228,10 @@ case "$ACTION" in
if (existing) {
existing.hooks = [hookEntry];
existing._gstack_source = source;
// Keep the matcher current too — under the (event, source) key the
// matched entry may carry a stale matcher.
if (matcher) existing.matcher = matcher;
else delete existing.matcher;
} else {
const newEntry = { _gstack_source: source, hooks: [hookEntry] };
if (matcher) newEntry.matcher = matcher;
+6
View File
@@ -224,6 +224,12 @@ if [[ -z "$SLUG" ]]; then
if [[ -n "$REMOTE_URL" ]]; then
RAW_SLUG=$(printf '%s' "${REMOTE_URL%.git}" | sed -E 's#.*[:/]([^/]+)/([^/]+)$#\1-\2#')
SLUG=$(printf '%s' "$RAW_SLUG" | tr -cd 'a-zA-Z0-9._-')
# Dot-only / degenerate guard: a hostile origin like `url = ..` (git
# accepts it) passes sed unchanged and would become SLUG=".." — filing
# state one level ABOVE ~/.gstack/projects/. Reject empty/"."/".."/
# slash-bearing slugs and fall through to the basename fallback below.
# (tr -cd already deletes "/", so */* is belt-and-braces.)
case "$SLUG" in ""|.|..|*/*) SLUG="" ;; esac
fi
fi