mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* fix(ship-budget): calibrate projections from the run's own meter, settle dispatch reservations when reviewers return (GST-101) The /ship budget projected every step and dispatch from fixed constants (7.43M per dispatch, 2.25M per default step) plus a fixed 4.48M reserve, so real caps refused work that cost 3-20x less (GSTA-152 PR B, GSTA-2). Each settled reservation is now measured on the next reading, and the run projects with its own factor (max actual/prior of its last three measurements x 1.5), the prior only before the first measurement. The default tail reserve scales with the step factor; the stop overhead keeps its fit. Reservations of reviewers admitted one at a time no longer pile up: check --returned settles named dispatches, --finish settles the pass's dispatches of that step, and a step change settles consumed dispatches. DRIFT and BUDGET_DRIFT lines print projected against actual. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-101) Unseen charges (settle --unknown, a Claude-metered outside voice) are booked at their reservation and never measured, so they cannot shrink the dispatch factor. The handoff holds back the same calibrated reserve as check, so a delegate verdict is one the handoff admits. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship-budget): handoff sub-cap subtracts unknown charges and the calibrated reserve Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: changelog (Unreleased) for GST-101 ship-budget calibration Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ship-budget): give the GSTA-152 PR B replay an explicit 120s timeout It spawns about 100 CLI processes; on windows-latest it ran past the shard runner's 30s per-test timeout (windows-free-shard 5, run 40917695070), and on 2 cores it sat at bare bun test's 5s default. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
173 lines
9.8 KiB
Bash
Executable File
173 lines
9.8 KiB
Bash
Executable File
#!/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
|
|
# Under a /ship token cap (docs/ship-budget.md): --start SKILL --reservation ID
|
|
# with the RESERVATION id gstack-ship-budget check printed for this dispatch
|
|
# batch; refuses (exit 1, BUDGET_CHECK_SKIPPED) without an unspent one, and
|
|
# --finish settles it with the pass's other dispatch reservations of that
|
|
# step. Without a cap no marker exists and nothing changes.
|
|
# Fix-range repeat (#3339): gstack-review-log --start review --fix-range
|
|
# prints the token, then FIX_BASE=<sha>; refuses (exit 1) unless the latest
|
|
# pass completed with fixes on unchanged content and its commit is an
|
|
# ancestor of HEAD. Its row is never CURRENT; a later full pass must follow.
|
|
# After that pass: gstack-review-log '{...,"completed":true,"converged":true}' --finish TOKEN
|
|
# With a findings file: gstack-review-log '{...}' --findings findings.jsonl
|
|
# status/unresolved/issues_found/critical_gaps are DERIVED from the file (B7);
|
|
# a claimed value that disagrees is rejected with REVIEW_STATUS_MISMATCH.
|
|
# Exit: 0 written · 1 rejected (binding or validation failed; stderr says why) · 2 usage.
|
|
# 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
|
|
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
|
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"
|
|
export GSTACK_SHIP_BUDGET_LIB="$SCRIPT_DIR/../lib/ship-budget.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_SHIP_BUDGET_LIB="$(cygpath -m "$GSTACK_SHIP_BUDGET_LIB")"
|
|
GSTACK_REVIEW_DIR="$(cygpath -m "$GSTACK_REVIEW_DIR")"
|
|
fi
|
|
;;
|
|
esac
|
|
export GSTACK_REVIEW_LOG="$GSTACK_REVIEW_DIR/$BRANCH-reviews.jsonl"
|
|
export GSTACK_SHIP_BUDGET_MARKER="$GSTACK_REVIEW_DIR/$BRANCH-ship-budget.json"
|
|
|
|
# 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
|
|
|
|
# Gate findings get `actor` from the session classifier, never from the caller;
|
|
# a non-interactive session's gate findings are forced to action "open".
|
|
SESSION_KIND=$("$SCRIPT_DIR/gstack-session-kind" 2>/dev/null || echo interactive)
|
|
export GSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" GSTACK_STAMP_TREE="$TREE" GSTACK_STAMP_WTREE="$WTREE" GSTACK_STAMP_DIRTY="$DIRTY" GSTACK_STAMP_SESSION_KIND="$SESSION_KIND"
|
|
if [ "$INPUT" = --start ]; then
|
|
START_SKILL="${2:-}" START_FIX="" START_RESERVATION=""
|
|
if [ "$#" -ge 2 ]; then shift 2; else shift "$#"; fi
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--fix-range) START_FIX=--fix-range; shift ;;
|
|
--reservation) [ "$#" -ge 2 ] || { echo 'Usage: gstack-review-log --start SKILL [--fix-range] [--reservation ID]' >&2; exit 2; }; START_RESERVATION="$2"; shift 2 ;;
|
|
*) echo 'Usage: gstack-review-log --start SKILL [--fix-range] [--reservation ID]' >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
if [ ! -f "$GSTACK_SHIP_BUDGET_MARKER" ]; then
|
|
GSTACK_REVIEW_SKILL="$START_SKILL" GSTACK_REVIEW_FIX_RANGE="$START_FIX" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
|
const { captureReviewStart } = await import(process.env.GSTACK_REVIEW_LIB);
|
|
try { console.log(captureReviewStart(process.env.GSTACK_REVIEW_SKILL, process.env, process.env.GSTACK_REVIEW_FIX_RANGE === "--fix-range")); }
|
|
catch (e) { console.error(e.message); process.exit(1); }
|
|
'
|
|
exit $?
|
|
fi
|
|
GSTACK_REVIEW_SKILL="$START_SKILL" GSTACK_REVIEW_FIX_RANGE="$START_FIX" GSTACK_REVIEW_RESERVATION="$START_RESERVATION" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
|
const { captureReviewStart } = await import(process.env.GSTACK_REVIEW_LIB);
|
|
const { admitReviewStart } = await import(process.env.GSTACK_SHIP_BUDGET_LIB);
|
|
const capture = () => captureReviewStart(process.env.GSTACK_REVIEW_SKILL, process.env, process.env.GSTACK_REVIEW_FIX_RANGE === "--fix-range");
|
|
try { console.log(admitReviewStart(process.env.GSTACK_SHIP_BUDGET_MARKER, process.env.GSTACK_REVIEW_RESERVATION || undefined, capture)); }
|
|
catch (e) { console.error(e.message); process.exit(1); }
|
|
'
|
|
exit $?
|
|
fi
|
|
if [ "$INPUT" = --check-shared-libs ] && [ "$#" -eq 2 ]; then
|
|
GSTACK_REVIEW_TOKEN="$2" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -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
|
|
usage() {
|
|
echo 'Usage: gstack-review-log JSON [--finish TOKEN] [--findings FILE] | --start SKILL [--fix-range] [--reservation ID] | --check-shared-libs TOKEN < finding.json' >&2
|
|
echo 'Exit codes: 0 written · 1 rejected · 2 usage' >&2
|
|
exit 2
|
|
}
|
|
[ "$#" -ge 1 ] || usage
|
|
TOKEN=""
|
|
FINDINGS=""
|
|
shift
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--finish) [ "$#" -ge 2 ] || usage; TOKEN="$2"; shift 2 ;;
|
|
--findings) [ "$#" -ge 2 ] || usage; FINDINGS="$2"; shift 2 ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
case "$INPUT" in --*) usage ;; esac
|
|
|
|
# Validate (reject malformed or injection attempts) AND stamp in one pass.
|
|
# Caller values for the binding keys are dropped before stamping. A binding or
|
|
# validation failure reaches stderr as its own line (never folded into
|
|
# "invalid JSON"), and exits 1 with nothing written.
|
|
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
|
const { bindReview } = await import(process.env.GSTACK_REVIEW_LIB);
|
|
let rec;
|
|
try { rec = JSON.parse(await Bun.stdin.text()); } catch (e) { console.error('gstack-review-log: invalid JSON, skipping (' + e.message + ')'); process.exit(1); }
|
|
if (!rec || Array.isArray(rec) || typeof rec !== 'object') { console.error('gstack-review-log: invalid JSON, skipping (expected an object)'); process.exit(1); }
|
|
try {
|
|
console.log(JSON.stringify(bindReview(rec, process.env.GSTACK_REVIEW_TOKEN, process.env, process.env.GSTACK_REVIEW_FINDINGS || undefined)));
|
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
") || exit 1
|
|
|
|
echo "$STAMPED" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
|
|
|
|
# A finished pass settles the budget reservation its start consumed (its
|
|
# completion receipt) and every other dispatch reservation started in the same
|
|
# step; stdout stays unchanged and a failure never undoes the row.
|
|
if [ -n "$TOKEN" ] && [ -f "$GSTACK_SHIP_BUDGET_MARKER" ]; then
|
|
GSTACK_REVIEW_TOKEN="$TOKEN" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
|
const { settleReviewFinish } = await import(process.env.GSTACK_SHIP_BUDGET_LIB);
|
|
try { const id = settleReviewFinish(process.env.GSTACK_SHIP_BUDGET_MARKER, process.env.GSTACK_REVIEW_TOKEN); if (id) console.error("review-log: settled budget reservations " + id); }
|
|
catch (e) { console.error("review-log: budget reservation not settled: " + e.message); }
|
|
' || true
|
|
fi
|
|
|
|
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off). Plan B1:
|
|
# an unattended session never enqueues, whatever the inherited sync mode.
|
|
if [ "$("$SCRIPT_DIR/gstack-session-kind" 2>/dev/null || echo interactive)" = "unattended" ]; then
|
|
echo "review-log: sync skipped (unattended session)" >&2
|
|
else
|
|
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null &
|
|
fi
|