Files
gstack/bin/gstack-learnings-log
T
garrytan cd7485b0b4 W1: guarded migration of every executable state-root site; uninstall deletes only ~/.gstack
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design,
ios-qa daemon, lib and scripts resolve the state root through
bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the
twin and stop with a reinstall message when it is missing; hooks source it
and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts
delegate to resolveStateRoot. Analytics writers and readers move together
so the usage log stays one file. gstack-uninstall deletes state only at
~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor,
the checkout or the git root, and leaves any other resolved root in place
with the removal command. Fixtures that copy single bins now copy the twin.
2026-09-30 06:38:50 +00:00

94 lines
4.0 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)"
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
gstack_state_root_select
GSTACK_HOME="$_gstack_sr_root"
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 &