mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 13:15:24 +02:00
f954f3449a
* feat: add bin/gstack-repo-mode — solo vs collaborative detection with caching
Detects whether a repo is solo-dev (one person does 80%+ of recent commits)
or collaborative. Uses 90-day git shortlog window with 7-day cache in
~/.gstack/projects/{SLUG}/repo-mode.json. Config override via
`gstack-config set repo_mode solo|collaborative` takes precedence over
the heuristic. Minimum 5 commits required to classify (otherwise unknown).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: test failure ownership triage — see something say something
Adds two new preamble sections to all gstack skills:
- Repo Ownership Mode: explains solo vs collaborative behavior
- See Something, Say Something: proactive issue flagging principle
Adds {{TEST_FAILURE_TRIAGE}} template variable (opt-in, used by /ship):
- Classifies test failures as in-branch vs pre-existing
- Solo mode defaults to "investigate and fix now"
- Collaborative mode offers "blame + assign GitHub issue" option
- Also offers P0 TODO and skip options
/ship Step 3 now triages test failures instead of hard-stopping on all
failures. In-branch failures still block shipping. Pre-existing failures
get user-directed triage based on repo mode.
Adds P2 TODO for gstack notes system (deferred lightweight reminder).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate SKILL.md files for Claude and Codex hosts
All 22 Claude skills and 21 Codex skills regenerated with new preamble
sections (Repo Ownership Mode, See Something Say Something) and
{{TEST_FAILURE_TRIAGE}} resolved in ship/SKILL.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: validate repo mode values to prevent shell injection
Codex adversarial review found that unvalidated config/cache values
could be injected into shell via source <(gstack-repo-mode). Added
validate_mode() that only allows solo|collaborative|unknown — anything
else becomes "unknown". Prevents persistent code execution through
malicious config.yaml or tampered cache JSON.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: shell injection via branch names + feature-branch sampling bias
Codex code review found two issues:
P1: eval $(gstack-slug) in gstack-repo-mode executes branch names as
shell. Branch names like foo$(touch${IFS}pwned) are valid git refs and
would execute arbitrary commands. Fix: compute SLUG directly with sed
instead of eval'ing gstack-slug output.
P2: git shortlog HEAD only sees current branch history. On feature
branches that haven't merged main recently, other contributors disappear
from the sample. Fix: use git shortlog on the default branch
(origin/main) instead of HEAD.
Also improved blame lookup in collaborative triage to check both the
test file and the production code it covers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: broaden codex-host stripping test to accommodate triage section
"Investigate and fix" now appears in TEST_FAILURE_TRIAGE (not just the
Codex review step). Use CODEX_REVIEWS config string as a more specific
marker for detecting the Codex review step in Codex-hosted skills.
* fix: replace template placeholder in TODOS.md with readable text
{{TEST_FAILURE_TRIAGE}} is template syntax but TODOS.md is not processed
by gen-skill-docs — replaced with human-readable reference.
* chore: bump version and changelog (v0.9.5.0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add bin/ directory to project structure in CLAUDE.md
* test: add triage resolver unit tests, plan-eng coverage audit E2E, and triage E2E
- TEST_FAILURE_TRIAGE resolver: 6 unit tests verifying all triage steps (T1-T4),
REPO_MODE branching, and safety default for ambiguous failures
- plan-eng-coverage-audit E2E: tests /plan-eng-review coverage audit codepath
(gap identified during eng review — existed on neither branch)
- ship-triage E2E: planted-bug fixture with in-branch (truncate null) and
pre-existing (divide-by-zero) failures; verifies correct classification
- Touchfile entries for diff-based test selection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate stale Codex SKILL.md for retro
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.8 KiB
Bash
Executable File
75 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-repo-mode — detect solo vs collaborative repo mode
|
|
# Usage: source <(gstack-repo-mode) → sets REPO_MODE variable
|
|
# Or: gstack-repo-mode → prints REPO_MODE=... line
|
|
#
|
|
# Detection heuristic (90-day window):
|
|
# Solo: top author >= 80% of commits
|
|
# Collaborative: top author < 80%
|
|
#
|
|
# Override: gstack-config set repo_mode solo|collaborative
|
|
# Cache: ~/.gstack/projects/$SLUG/repo-mode.json (7-day TTL)
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# Compute SLUG directly (avoid eval of gstack-slug — branch names can contain shell metacharacters)
|
|
SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
|
|
[ -z "${SLUG:-}" ] && { echo "REPO_MODE=unknown"; exit 0; }
|
|
|
|
# Validate: only allow known values (prevent shell injection via source <(...))
|
|
validate_mode() {
|
|
case "$1" in solo|collaborative|unknown) echo "$1" ;; *) echo "unknown" ;; esac
|
|
}
|
|
|
|
# Config override takes precedence
|
|
OVERRIDE=$("$SCRIPT_DIR/gstack-config" get repo_mode 2>/dev/null || true)
|
|
if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "null" ]; then
|
|
echo "REPO_MODE=$(validate_mode "$OVERRIDE")"
|
|
exit 0
|
|
fi
|
|
|
|
# Check cache (7-day TTL)
|
|
CACHE_DIR="$HOME/.gstack/projects/$SLUG"
|
|
CACHE_FILE="$CACHE_DIR/repo-mode.json"
|
|
if [ -f "$CACHE_FILE" ]; then
|
|
CACHE_AGE=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) ))
|
|
if [ "$CACHE_AGE" -lt 604800 ]; then # 7 days in seconds
|
|
MODE=$(grep -o '"mode":"[^"]*"' "$CACHE_FILE" | head -1 | cut -d'"' -f4)
|
|
[ -n "$MODE" ] && echo "REPO_MODE=$(validate_mode "$MODE")" && exit 0
|
|
fi
|
|
fi
|
|
|
|
# Compute from git history (90-day window)
|
|
# Use default branch (not HEAD) to avoid feature-branch sampling bias
|
|
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/||' || echo "origin/main")
|
|
SHORTLOG=$(git shortlog -sn --since="90 days ago" --no-merges "$DEFAULT_BRANCH" 2>/dev/null | head -20)
|
|
if [ -z "$SHORTLOG" ]; then
|
|
echo "REPO_MODE=unknown"
|
|
exit 0
|
|
fi
|
|
|
|
TOTAL=$(echo "$SHORTLOG" | awk '{s+=$1} END {print s}')
|
|
TOP=$(echo "$SHORTLOG" | head -1 | awk '{print $1}')
|
|
AUTHORS=$(echo "$SHORTLOG" | wc -l | tr -d ' ')
|
|
|
|
# Minimum sample: need at least 5 commits to classify
|
|
if [ "$TOTAL" -lt 5 ]; then
|
|
echo "REPO_MODE=unknown"
|
|
exit 0
|
|
fi
|
|
|
|
TOP_PCT=$(( TOP * 100 / TOTAL ))
|
|
|
|
# Solo: top author >= 80% of commits (occasional outside PRs don't change mode)
|
|
if [ "$TOP_PCT" -ge 80 ]; then
|
|
MODE=solo
|
|
else
|
|
MODE=collaborative
|
|
fi
|
|
|
|
# Cache result (fail silently if ~/.gstack is unwritable)
|
|
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
echo "{\"mode\":\"$MODE\",\"top_pct\":$TOP_PCT,\"authors\":$AUTHORS,\"total\":$TOTAL,\"computed\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$CACHE_FILE" 2>/dev/null || true
|
|
|
|
echo "REPO_MODE=$MODE"
|