#!/usr/bin/env bash # gstack-review-log — atomically log a review result # Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}' # # Before reading a diff: gstack-review-log --start review # After that pass: gstack-review-log '{...,"completed":true,"converged":true}' --finish TOKEN # Diff reviews get wtree only from a consumed, matching start capture on an # unchanged tree. Completion/convergence are reviewer-reported, not proof that # a model read the code. Caller-supplied binding fields are always discarded. # Plan-tier rows retain their legacy binding behavior. set -euo pipefail export GIT_OPTIONAL_LOCKS=0 export GIT_CONFIG_PARAMETERS="${GIT_CONFIG_PARAMETERS:+$GIT_CONFIG_PARAMETERS }'core.fsmonitor=false' 'core.untrackedCache=false'" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 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:-}" export GSTACK_REVIEW_DIR="$GSTACK_HOME/projects/$SLUG" export GSTACK_REVIEW_REPO="$(git rev-parse --show-toplevel 2>/dev/null || true)" export GSTACK_REVIEW_BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse HEAD 2>/dev/null || true)" export GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) if command -v cygpath >/dev/null 2>&1; then GSTACK_REVIEW_LIB="$(cygpath -m "$GSTACK_REVIEW_LIB")" GSTACK_REVIEW_DIR="$(cygpath -m "$GSTACK_REVIEW_DIR")" fi ;; esac export GSTACK_REVIEW_LOG="$GSTACK_REVIEW_DIR/$BRANCH-reviews.jsonl" # Compute binding fields (best-effort; empty outside a git repo). COMMIT_FULL=$(git rev-parse HEAD 2>/dev/null || true) TREE="" WTREE="" DIRTY="" if [ -n "$COMMIT_FULL" ]; then TREE=$(git rev-parse 'HEAD^{tree}' 2>/dev/null || true) # E4: the fingerprint's new objects go to a private 0700 directory this # operation owns (a read-only .git still fingerprints), and stay readable # until the consumers below (bindReview / checkSharedLibsReuse read the tree # and blobs) finish; removed on EXIT/INT/TERM. gstack-wtree's one-line # diagnostic reaches stderr; stdout stays the hash. if WTREE_OBJECTS=$(mktemp -d "${TMPDIR:-/tmp}/gstack-wtree-objects-XXXXXX" 2>/dev/null); then case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && WTREE_OBJECTS="$(cygpath -m "$WTREE_OBJECTS")" ;; esac trap 'rm -rf "$WTREE_OBJECTS"' EXIT trap 'exit 130' INT TERM WTREE=$(GSTACK_WTREE_OBJECT_DIR="$WTREE_OBJECTS" "$SCRIPT_DIR/gstack-wtree" || true) export GIT_ALTERNATE_OBJECT_DIRECTORIES="$WTREE_OBJECTS${GIT_ALTERNATE_OBJECT_DIRECTORIES:+:$GIT_ALTERNATE_OBJECT_DIRECTORIES}" else echo "gstack-review-log: cannot create a private object directory under ${TMPDIR:-/tmp}; no working-tree fingerprint. Set TMPDIR to a writable directory." >&2 fi if [ -n "$(git status --porcelain -uno 2>/dev/null | head -1)" ]; then DIRTY="true" else DIRTY="false" fi fi export GSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" GSTACK_STAMP_TREE="$TREE" GSTACK_STAMP_WTREE="$WTREE" GSTACK_STAMP_DIRTY="$DIRTY" if [ "$INPUT" = --start ]; then GSTACK_REVIEW_SKILL="${2:-}" bun -e ' const { captureReviewStart } = await import(process.env.GSTACK_REVIEW_LIB); console.log(captureReviewStart(process.env.GSTACK_REVIEW_SKILL)); ' exit $? fi if [ "$INPUT" = --check-shared-libs ] && [ "$#" -eq 2 ]; then GSTACK_REVIEW_TOKEN="$2" bun -e ' const { checkSharedLibsReuse } = await import(process.env.GSTACK_REVIEW_LIB); console.log(JSON.stringify(checkSharedLibsReuse(JSON.parse(await Bun.stdin.text()), process.env.GSTACK_REVIEW_TOKEN))); ' exit $? fi if [ "$#" -ne 1 ] && { [ "$#" -ne 3 ] || [ "${2:-}" != --finish ]; }; then echo 'Usage: gstack-review-log JSON [--finish TOKEN] | --start SKILL | --check-shared-libs TOKEN < finding.json' >&2 exit 1 fi # Validate (reject malformed or injection attempts) AND stamp in one pass. # Caller values for the binding keys are dropped before stamping. STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="${3:-}" bun -e " const { bindReview } = await import(process.env.GSTACK_REVIEW_LIB); const rec = JSON.parse(await Bun.stdin.text()); if (!rec || Array.isArray(rec) || typeof rec !== 'object') throw new Error('expected object'); console.log(JSON.stringify(bindReview(rec, process.env.GSTACK_REVIEW_TOKEN))); " 2>/dev/null) || { # Not valid JSON — refuse to append echo "gstack-review-log: invalid JSON, skipping" >&2 exit 1 } echo "$STAMPED" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" # gbrain-sync: enqueue for cross-machine sync (no-op if sync is off). "$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null &