mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
Two independent Windows git-bash bugs in the bin writers, both silent
because callers invoke these scripts with 2>/dev/null and do not check
the exit status — a hard failure was indistinguishable from success.
Bug 1 — apostrophe in the checkout path breaks the bun -e program.
gstack-learnings-log, gstack-question-log and gstack-telemetry-log build
a bun -e program as a double-quoted shell string and interpolate
SCRIPT_DIR into a single-quoted JS import specifier. A path such as
C:/Users/Someone's PC/... closes the JS string literal early and Bun
fails to parse ("Expected ; but found s"). Every learning write and every
plan-tune question event no-oped; telemetry error redaction fell to its
fail-closed null path. The #1950 cygpath -m guard did not cover this —
cygpath normalises the drive form but does not remove the apostrophe.
Fixed by not interpolating the path at all: cd into the module root and
use a relative import specifier, which is immune to apostrophes, spaces,
backslashes and MSYS paths alike. The one remaining interpolated data
path in gstack-developer-profile (readFileSync of PROFILE_FILE) is passed
via the environment instead, matching do_log_session in the same file.
Bug 2 — gstack-developer-profile --derive fails on an MSYS-form
GSTACK_HOME. GSTACK_HOME defaults to $HOME/.gstack, which under git-bash
is /c/Users/..., and Bun on Windows cannot open that form (ENOENT). This
script carried no cygpath guard at all. Fixed by normalising GSTACK_HOME
once, before PROFILE_FILE / LEGACY_FILE / the events path are derived
from it, so all three pick up the normalised value.
Adds test/hostile-path-writers.test.ts, which runs the bins from a
directory whose name contains an apostrophe and asserts that rows are
ACTUALLY WRITTEN (not merely that the exit code is 0 — exit-code-only
checks are what masked bug 1). The apostrophe repro is OS-independent:
SCRIPT_DIR derives from the script's own location, so a copied checkout
under a hostile directory name reproduces bug 1 on Linux/macOS CI too.
Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (no CWD-dependent module resolution)
Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (apostrophe-safe without CWD-dependent module resolution); import-shape pin updated
92 lines
3.7 KiB
Bash
Executable File
92 lines
3.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-learnings-log — append a learning to the project learnings file
|
|
# Usage: gstack-learnings-log '{"skill":"review","type":"pitfall","key":"n-plus-one","insight":"...","confidence":8,"source":"observed"}'
|
|
# Valid types: pattern, pitfall, preference, architecture, tool, operational, investigation
|
|
#
|
|
# Append-only storage. Duplicates (same key+type) are resolved at read time
|
|
# by gstack-learnings-search ("latest winner" per key+type).
|
|
set -euo pipefail
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# Windows git-bash (#1950): pwd yields a POSIX path (/c/Users/...), which Bun
|
|
# on Windows cannot resolve as an ES module specifier in the import below.
|
|
# cygpath -m converts to C:/Users/... which Bun accepts.
|
|
case "$(uname -s)" in
|
|
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
|
|
esac
|
|
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
|
mkdir -p "$GSTACK_HOME/projects/$SLUG"
|
|
|
|
INPUT="$1"
|
|
|
|
# Validate and sanitize input. Errors surface (#1950): stderr is captured and
|
|
# printed on failure instead of swallowed — a silent exit 1 here cost Windows
|
|
# users every AI-logged learning.
|
|
TMPERR=$(mktemp)
|
|
trap 'rm -f "$TMPERR"' EXIT
|
|
set +e
|
|
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun -e "
|
|
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
|
|
const raw = await Bun.stdin.text();
|
|
let j;
|
|
try { j = JSON.parse(raw); } catch { process.stderr.write('gstack-learnings-log: invalid JSON, skipping\n'); process.exit(1); }
|
|
|
|
// Field validation: type must be from allowed list
|
|
const ALLOWED_TYPES = ['pattern', 'pitfall', 'preference', 'architecture', 'tool', 'operational', 'investigation'];
|
|
if (!j.type || !ALLOWED_TYPES.includes(j.type)) {
|
|
process.stderr.write('gstack-learnings-log: invalid type \"' + (j.type || '') + '\", must be one of: ' + ALLOWED_TYPES.join(', ') + '\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Field validation: key must be alphanumeric, hyphens, underscores (no injection surface)
|
|
if (!j.key || !/^[a-zA-Z0-9_-]+$/.test(j.key)) {
|
|
process.stderr.write('gstack-learnings-log: invalid key, must be alphanumeric with hyphens/underscores only\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Field validation: confidence must be 1-10
|
|
const conf = Number(j.confidence);
|
|
if (!Number.isInteger(conf) || conf < 1 || conf > 10) {
|
|
process.stderr.write('gstack-learnings-log: confidence must be integer 1-10\n');
|
|
process.exit(1);
|
|
}
|
|
j.confidence = conf;
|
|
|
|
// Field validation: source must be from allowed list
|
|
const ALLOWED_SOURCES = ['observed', 'user-stated', 'inferred', 'cross-model'];
|
|
if (j.source && !ALLOWED_SOURCES.includes(j.source)) {
|
|
process.stderr.write('gstack-learnings-log: invalid source, must be one of: ' + ALLOWED_SOURCES.join(', ') + '\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Content sanitization: shared injection patterns (lib/jsonl-store.ts, D2A) —
|
|
// one audited list across learnings + decisions, no drift.
|
|
if (j.insight && hasInjection(j.insight)) {
|
|
process.stderr.write('gstack-learnings-log: insight contains suspicious instruction-like content, rejected\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Inject timestamp if not present
|
|
if (!j.ts) j.ts = new Date().toISOString();
|
|
|
|
// Mark trust level based on source
|
|
// user-stated = user explicitly told the agent this. All others are AI-generated.
|
|
j.trusted = j.source === 'user-stated';
|
|
|
|
console.log(JSON.stringify(j));
|
|
" 2>"$TMPERR")
|
|
VALIDATE_RC=$?
|
|
set -e
|
|
|
|
if [ $VALIDATE_RC -ne 0 ] || [ -z "$VALIDATED" ]; then
|
|
if [ -s "$TMPERR" ]; then
|
|
cat "$TMPERR" >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
echo "$VALIDATED" >> "$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
|
|
|
|
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
|
|
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/learnings.jsonl" 2>/dev/null &
|