mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 06:58:59 +02:00
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:
co-authored by
Claude Fable 5
parent
9fecf0f16f
commit
b7d44c45b4
+96
-16
@@ -46,7 +46,7 @@ const STRONG_FILE_MARKERS = [".project.yaml", "package.json", "pyproject.toml",
|
||||
const WEAK_FILE_MARKERS = ["README.md", "README", "README.rst", "LICENSE", "LICENSE.md"];
|
||||
|
||||
/**
|
||||
* Native port of bin/gstack-slug's `_outermost_project_root` (:77-113): walk UP
|
||||
* Native port of bin/gstack-slug's `_outermost_project_root`: walk UP
|
||||
* from `startDir` tracking the OUTERMOST ancestor holding a strong marker and
|
||||
* the outermost holding a weak marker. Outermost STRONG wins; else outermost
|
||||
* WEAK; else "". Build/deploy artifacts (.vercel, node_modules, dist, ...) are
|
||||
@@ -77,6 +77,41 @@ export function outermostProjectRoot(startDir: string): string {
|
||||
return outermostStrong || outermostWeak;
|
||||
}
|
||||
|
||||
/**
|
||||
* Native port of bin/gstack-slug's `_outermost_remote_repo` (step 1a): walk UP
|
||||
* from `startDir` tracking the OUTERMOST ancestor that has a `.git` entry
|
||||
* (directory for normal clones, FILE for git-worktrees/submodules — `git -C`
|
||||
* resolves a worktree's remote through its main clone) AND whose `origin`
|
||||
* remote resolves. This is the canonical-identity walk: a marker-only
|
||||
* ancestor with no resolvable origin (stray empty ~/.git, stray package.json)
|
||||
* cannot win here, so it cannot hijack remote-derived identity the way it can
|
||||
* hijack the marker walk above. Nested-repo semantics preserved: an inner
|
||||
* repo under an outer canonical-remote repo still resolves to the OUTER
|
||||
* repo's remote (outermost wins). git spawns only at `.git`-bearing ancestors
|
||||
* — typically one. Exported for the parity tests.
|
||||
*/
|
||||
export function outermostRemoteRepo(startDir: string): { root: string; url: string } {
|
||||
let dir = startDir;
|
||||
let root = "";
|
||||
let url = "";
|
||||
let depth = 0;
|
||||
while (dir && dir !== "/" && depth < 64) {
|
||||
if (existsSync(join(dir, ".git"))) {
|
||||
const r = spawnSync("git", ["-C", dir, "remote", "get-url", "origin"], { encoding: "utf-8" });
|
||||
const u = r.status === 0 ? (r.stdout || "").trim() : "";
|
||||
if (u) {
|
||||
root = dir;
|
||||
url = u;
|
||||
}
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break; // dirname fixed point (C:\, ., //srv)
|
||||
dir = parent;
|
||||
depth += 1;
|
||||
}
|
||||
return { root, url };
|
||||
}
|
||||
|
||||
/**
|
||||
* Native port of bin/gstack-slug's resolution order, used when that script cannot be
|
||||
* spawned (see resolveSlug). Same steps, same alphabet, same cache file — so this and
|
||||
@@ -84,15 +119,28 @@ export function outermostProjectRoot(startDir: string): string {
|
||||
* Context Recovery preamble READS using the script.
|
||||
*
|
||||
* Resolution order (parity with the bash script, pinned by
|
||||
* test/bin-context-windows-slug.test.ts against test/gstack-slug-cwd-walk-up.test.ts):
|
||||
* test/bin-context-windows-slug.test.ts against test/gstack-slug-cwd-walk-up.test.ts
|
||||
* and test/gstack-slug-parity.test.ts):
|
||||
* 0. $GSTACK_PROJECT_SLUG env override — wins over everything, never cached.
|
||||
* 1. Walk UP to the OUTERMOST project root (see outermostProjectRoot). Without
|
||||
* the walk, a nested/vendored repo derived its slug from the INNERMOST
|
||||
* `git remote get-url origin`, splitting the store the bash side keeps whole.
|
||||
* 2. Cached slug is sticky — EXCEPT the provable old-bug shape (#1125): cached
|
||||
* value equals basename(cwd) while the walk-up says cwd is NOT the project
|
||||
* root; that cache came from the pre-walk-up resolver, so recompute and heal.
|
||||
* 3. Git remote AT THE PROJECT ROOT: [:/]<owner>/<repo>[.git] → owner-repo.
|
||||
* 2. Cached slug is sticky (#2212) — EXCEPT two provable bug shapes:
|
||||
* - old-bug shape (#1125): cached value equals basename(cwd) while the
|
||||
* walk-up says cwd is NOT the project root; that cache came from the
|
||||
* pre-walk-up resolver, so recompute and heal.
|
||||
* - degraded-ancestor shape (2026-08-17): cached equals the marker root's
|
||||
* basename while a remote-bearing repo BELOW the marker root exists —
|
||||
* the pre-remote-first resolver degraded to a stray ancestor's basename
|
||||
* (stray empty ~/.git → SLUG=<username>). Legit #2212 stickiness is
|
||||
* safe: there the repo that adopted the remote IS the marker root
|
||||
* (remote root == project root), so the heal never fires.
|
||||
* 3. Canonical remote-derived slug from the OUTERMOST remote-bearing repo
|
||||
* (see outermostRemoteRepo — never PROJECT_ROOT, which may be a
|
||||
* marker-only ancestor with no remote): [:/]<owner>/<repo>[.git] →
|
||||
* owner-repo, byte-parity with browse/bin/remote-slug. Degenerate slugs
|
||||
* ("", ".", "..", anything with "/") are rejected — a hostile origin
|
||||
* like `url = ..` must never escape ~/.gstack/projects/<slug>.
|
||||
* 4. Project root's basename; else basename(cwd) for plain non-project folders.
|
||||
*/
|
||||
export function slugFromEnvironment(gstackHome?: string, cwd: string = process.cwd()): string {
|
||||
@@ -108,24 +156,56 @@ export function slugFromEnvironment(gstackHome?: string, cwd: string = process.c
|
||||
// 1. outermost project root along the cwd ancestor chain (may be "").
|
||||
const projectRoot = outermostProjectRoot(cwd);
|
||||
|
||||
// Lazy, memoized remote discovery (mirrors gstack-slug's _resolve_remote):
|
||||
// needed on exactly two paths — fresh resolution and the degraded-ancestor
|
||||
// heal check — so ordinary cache hits stay git-spawn-free.
|
||||
let remote: { root: string; url: string } | null = null;
|
||||
const resolveRemote = () => (remote ??= outermostRemoteRepo(cwd));
|
||||
|
||||
let slug = "";
|
||||
// 2. cached slug is sticky (#2212), except the old-bug shape (#1125).
|
||||
// 2. cached slug is sticky (#2212), except the two provable bug shapes
|
||||
// (old-bug #1125 and degraded-ancestor 2026-08-17 — see the doc above).
|
||||
if (existsSync(cacheFile)) {
|
||||
try {
|
||||
const cached = sanitizeSlug(readFileSync(cacheFile, "utf-8").trim());
|
||||
const pwdBase = sanitizeSlug(basename(cwd));
|
||||
const oldBugShape = cached === pwdBase && projectRoot !== "" && projectRoot !== cwd;
|
||||
if (cached && !oldBugShape) slug = cached;
|
||||
if (cached) {
|
||||
const pwdBase = sanitizeSlug(basename(cwd));
|
||||
const rootBase = projectRoot ? sanitizeSlug(basename(projectRoot)) : "";
|
||||
const oldBugShape = cached === pwdBase && projectRoot !== "" && projectRoot !== cwd;
|
||||
const degradedAncestorShape =
|
||||
!oldBugShape &&
|
||||
projectRoot !== "" &&
|
||||
cached === rootBase &&
|
||||
(() => {
|
||||
const r = resolveRemote();
|
||||
return r.url !== "" && r.root !== projectRoot;
|
||||
})();
|
||||
if (!oldBugShape && !degradedAncestorShape) slug = cached;
|
||||
}
|
||||
} catch {
|
||||
slug = "";
|
||||
}
|
||||
}
|
||||
// 3. derive from the project root's git remote (a subdir without its own
|
||||
// remote inherits the parent's — same as `git -C "$PROJECT_ROOT"`).
|
||||
if (!slug && projectRoot) {
|
||||
const r = spawnSync("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf-8" });
|
||||
const m = (r.stdout || "").trim().match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
||||
if (m) slug = sanitizeSlug(m[1].replace(/\//g, "-"));
|
||||
// 3. canonical remote-derived slug from the outermost remote-bearing repo.
|
||||
// Parse mirrors bin/gstack-slug step 2 exactly (byte-parity with
|
||||
// browse/bin/remote-slug): `${REMOTE_URL%.git}` strips ONE trailing
|
||||
// ".git" (case-sensitive), then sed extracts the LAST two path segments
|
||||
// — and sed's no-match passthrough means the stripped URL itself is the
|
||||
// raw slug when no [:/]owner/repo tail exists.
|
||||
if (!slug) {
|
||||
const { url } = resolveRemote();
|
||||
if (url) {
|
||||
const stripped = url.endsWith(".git") ? url.slice(0, -4) : url;
|
||||
const m = stripped.match(/[:/]([^/]+)\/([^/]+)$/);
|
||||
const candidate = sanitizeSlug(m ? `${m[1]}-${m[2]}` : stripped);
|
||||
// Dot-only / degenerate guard (mirrors bin/gstack-slug): a hostile
|
||||
// origin like `url = ..` yields "." or ".." here, which would file
|
||||
// state OUTSIDE ~/.gstack/projects/. Reject and let the basename
|
||||
// fallback below anchor identity instead.
|
||||
if (candidate && candidate !== "." && candidate !== ".." && !candidate.includes("/")) {
|
||||
slug = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 4. project root's basename, else pwd basename for plain folders.
|
||||
if (!slug && projectRoot) slug = sanitizeSlug(basename(projectRoot));
|
||||
|
||||
Reference in New Issue
Block a user