fix: pre-landing review fixes (27 specialist findings, 3 critical)

Specialist army findings, all quote-verified before fixing:

Security: careful force-push guard now catches git's plus-refspec force
syntax (git push origin +main carried force with no flag — silently allowed
before) and refspec-form targets (HEAD:main); default-branch matching is
tokenized FIXED-STRING comparison on the full branch path (slashed defaults
like release/2.0 work; no ERE interpolation), glob-safe via noglob. HIGH rm
tier is tokenized too: trailing long options (--no-preserve-root) and /* are
root-class. Stored evidence fingerprints are 40-hex re-validated before
reaching git argv. normalizeForDetection sweeps ALL Unicode format chars
(\p{Cf}: soft hyphens, bidi marks, tag chars) instead of five enumerated
zero-widths. The wiring scanner gains flagless gh pr/issue view patterns. The
release-body banner tripwire diffs against the fetched original so a hostile
pre-existing banner string can't permanently DoS doc updates. Ship/land
evidence checks now pass --expect-cmd (a green `echo ok` recorded under the
label can never mint FRESH); package.json stays allow-listed with the
residual documented.

Performance: gstack-wtree seeds its temp index by COPYING the real index
(stat cache preserved — measured 40x faster than read-tree seeding, identical
hash) with read-tree fallback; evidence uses findLast and one gstack-slug
spawn; the stream pump honors backpressure via drain; careful's pattern block
short-circuits before slug resolution when no pattern file exists.

Testing: the gh-failure envelope test was VACUOUS (killing PATH killed the
bun shebang before the code under test ran) — replaced with a PATH gh shim
that exercises the real branch, plus shimmed happy paths (issue/pr-body/
unparseable JSON); evidence check --all + empty ledger + non-numeric
--max-age (now a usage error, was silent fail-open) covered; HIGH-tier
variants pinned; hook analytics respect GSTACK_HOME so tests stop writing the
operator's real skill-usage.jsonl.

Maintainability: dead exit ternary removed; flagValue deduped into
bin-context; sentinel defusal derived from the banner constants (no invisible
literals — \u escapes only); scratch-repo git fixture extracted to
test/helpers/scratch-repo.ts (one hermetic incantation, three consumers);
shared gstack_hook_log_fire in hook-extract.sh; the dashboard/land diff-scoped
row lists are aligned (codex-review) and drift-pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 08:19:14 -07:00
co-authored by Claude Fable 5
parent 2398fb7295
commit a171029e6b
27 changed files with 470 additions and 108 deletions
+28 -10
View File
@@ -38,7 +38,6 @@ import { mkdirSync, openSync, writeSync, closeSync, readdirSync, statSync, unlin
import { join, dirname } from "path";
import { spawnSync } from "child_process";
import { appendJsonl, readJsonl } from "../lib/jsonl-store";
import { resolveSlug } from "../lib/bin-context";
import { scan, applyRedactions } from "../lib/redact-engine";
const BIN_DIR = dirname(Bun.fileURLToPath(import.meta.url));
@@ -94,10 +93,12 @@ function currentWtree(): string | undefined {
function ledgerPath(): { dir: string; file: string; logsDir: string } {
const home = process.env.GSTACK_HOME || join(process.env.HOME || "~", ".gstack");
const slug = resolveSlug(join(BIN_DIR, "gstack-slug"));
// Same branch→filename sanitization as reviews.jsonl (gstack-slug's BRANCH).
// ONE gstack-slug spawn: its output carries both SLUG= and BRANCH= lines
// (same branch→filename sanitization as reviews.jsonl).
const slugOut = spawnSync(join(BIN_DIR, "gstack-slug"), { encoding: "utf-8" });
const sm = (slugOut.stdout || "").match(/^SLUG=(.+)$/m);
const bm = (slugOut.stdout || "").match(/^BRANCH=(.+)$/m);
const slug = sm ? sm[1].trim() : "unknown";
const branch = bm ? bm[1].trim() : "no-branch";
const dir = join(home, "projects", slug);
return { dir, file: join(dir, `${branch}-evidence.jsonl`), logsDir: join(dir, "logs") };
@@ -212,7 +213,10 @@ async function cmdRun(argv: string[]): Promise<number> {
const pump = async (stream: ReadableStream<Uint8Array> | undefined, out: NodeJS.WriteStream) => {
if (!stream) return;
for await (const chunk of stream) {
out.write(chunk);
// Honor backpressure: when the console consumer is slower than the child
// (piped into a pager/log collector), wait for drain instead of queueing
// unbounded chunks in the WriteStream buffer.
if (!out.write(chunk)) await new Promise((r) => out.once("drain", r));
teeToLog(chunk);
}
};
@@ -298,7 +302,15 @@ function cmdCheck(argv: string[]): number {
}
wanted[wanted.length - 1].expectCmd = argv[++i] ?? "";
} else if (a === "--all") all = true;
else if (a === "--max-age") maxAgeHours = Number(argv[++i]);
else if (a === "--max-age") {
maxAgeHours = Number(argv[++i]);
if (!Number.isFinite(maxAgeHours) || maxAgeHours <= 0) {
// A typo must never silently drop the age gate (fail open) on a
// freshness checker: it is a usage error.
console.error(`gstack-evidence: --max-age must be a positive number of hours, got: ${JSON.stringify(argv[i])}`);
return 2;
}
}
else if (a === "--allow-paths") allowPaths = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
}
if (!all && wanted.length === 0) {
@@ -324,7 +336,7 @@ function cmdCheck(argv: string[]): number {
const wtreeNow = currentWtree();
let allFresh = true;
for (const { label, expectCmd } of labels) {
const latest = [...records].reverse().find((r) => r.label === label);
const latest = records.findLast((r) => r.label === label);
if (!latest) {
console.log(`EVIDENCE: MISSING label=${label}`);
allFresh = false;
@@ -336,7 +348,7 @@ function cmdCheck(argv: string[]): number {
if (latest.exit !== 0) {
verdict = "STALE";
reason = "recorded run failed";
} else if (maxAgeHours !== undefined && Number.isFinite(maxAgeHours)) {
} else if (maxAgeHours !== undefined) {
const ageMs = Date.now() - Date.parse(latest.ts);
if (!(ageMs >= 0 && ageMs <= maxAgeHours * 3600 * 1000)) {
verdict = "STALE";
@@ -351,9 +363,15 @@ function cmdCheck(argv: string[]): number {
// Content binding: identical working-tree fingerprint, or a diff confined
// to the allow-list. Any git failure (gc'd tree, not a repo) → STALE —
// never an error into the calling flow.
if (!latest.wtree || !wtreeNow) {
if (!latest.wtree || !/^[0-9a-f]{40}$/.test(latest.wtree) || !wtreeNow) {
// Stored fingerprints are re-validated before reaching git argv — a
// forged/corrupt ledger line must degrade, never inject options.
verdict = "STALE";
reason = !latest.wtree ? "record has no content fingerprint" : "current fingerprint unavailable";
reason = !latest.wtree
? "record has no content fingerprint"
: !/^[0-9a-f]{40}$/.test(latest.wtree)
? "record has malformed fingerprint"
: "current fingerprint unavailable";
} else if (latest.wtree !== wtreeNow) {
const diff = git(["diff", "--name-only", latest.wtree, wtreeNow]);
if (diff === undefined) {
@@ -392,5 +410,5 @@ try {
// that breaks a skill flow: `run` propagates the child's code from inside
// cmdRun; reaching here means bookkeeping blew up outside it.
warn(`unexpected error: ${e?.message ?? e}`);
process.exit(sub === "check" ? 1 : 1);
process.exit(1);
}
+1 -5
View File
@@ -21,6 +21,7 @@
import { spawnSync } from "child_process";
import { wrapUntrustedTrackerContent } from "../lib/tracker-guard";
import { flagValue } from "../lib/bin-context";
function gh(args: string[]): { ok: boolean; out: string; err: string } {
try {
@@ -36,11 +37,6 @@ function fail(msg: string): never {
process.exit(1);
}
function flagValue(args: string[], name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
}
const [, , mode, ...rest] = process.argv;
if (mode === "--stdin") {
+28 -7
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
# gstack-wtree — print a working-tree CONTENT fingerprint (a git tree hash).
#
# Builds a temp index seeded from HEAD, stages the full working tree into it
# (`git add -A`, so .gitignore'd scratch stays out and UNTRACKED source is
# included), and prints `git write-tree` of that index. Properties that make
# this the right staleness fingerprint, vs `git rev-parse HEAD^{tree}`:
# Builds a temp index, stages the full working tree into it (`git add -A`, so
# .gitignore'd scratch stays out and UNTRACKED source is included), and prints
# `git write-tree` of that index. Properties that make this the right
# staleness fingerprint, vs `git rev-parse HEAD^{tree}`:
#
# - Committing identical content does NOT change the fingerprint, so a
# record made on a dirty tree stays valid after the exact same content is
@@ -13,15 +13,36 @@
# can't stay FRESH after a new file appears.
# - Rebase/amend/squash that preserve content do not change it.
#
# Performance: the temp index is seeded by COPYING the real index (git writes
# it atomically via rename, so the copy is a consistent snapshot). That
# preserves the stat cache, so `git add -A` only re-hashes files whose stat
# changed — measured 40x faster than a `read-tree HEAD` seed, which zeroes
# stat data and forces a full re-hash of every tracked file. Both seeds
# produce the identical write-tree hash. Fallback: `read-tree HEAD` when the
# index copy is unavailable (fresh repo, exotic index).
#
# The real repo index is never touched. Staged blobs land in the object store
# as unreachable objects and get gc'd like stash churn. Exit 1 outside a git
# repo or in a repo with no commits — callers treat that as "no fingerprint".
# as unreachable objects and get gc'd like stash churn (note: this means the
# CONTENT of untracked, non-ignored files enters .git/objects until gc — the
# same property `git stash -u` has). Exit 1 outside a git repo or in a repo
# with no commits — callers treat that as "no fingerprint".
set -euo pipefail
TOP=$(git rev-parse --show-toplevel 2>/dev/null) || exit 1
TMPIDX=$(mktemp "${TMPDIR:-/tmp}/gstack-wtree-XXXXXX")
trap 'rm -f "$TMPIDX"' EXIT
export GIT_INDEX_FILE="$TMPIDX"
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
REAL_INDEX=$(git -C "$TOP" rev-parse --git-path index 2>/dev/null || true)
# Resolve relative --git-path output against the repo root.
case "$REAL_INDEX" in
""|/*) ;;
*) REAL_INDEX="$TOP/$REAL_INDEX" ;;
esac
if [ -n "$REAL_INDEX" ] && [ -f "$REAL_INDEX" ] && cp "$REAL_INDEX" "$TMPIDX" 2>/dev/null; then
: # stat-cache-preserving seed
else
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
fi
git -C "$TOP" add -A 2>/dev/null || exit 1
git -C "$TOP" write-tree 2>/dev/null