diff --git a/bin/gstack-open b/bin/gstack-open new file mode 100644 index 000000000..cae5ffea9 --- /dev/null +++ b/bin/gstack-open @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# gstack-open — cross-platform URL/file opener +# Usage: gstack-open +# +# Replaces bare `open` calls in skill templates, which is macOS-only. +# Works on macOS, Linux (xdg-open / sensible-browser), and Windows (Git Bash / WSL). +set -euo pipefail + +TARGET="${1:-}" +if [ -z "$TARGET" ]; then + echo "gstack-open: missing argument" >&2 + exit 1 +fi + +case "$(uname -s)" in + Darwin) + open "$TARGET" + ;; + Linux) + # WSL: delegate to Windows explorer + if grep -qi microsoft /proc/version 2>/dev/null; then + explorer.exe "$TARGET" 2>/dev/null || cmd.exe /c start "" "$TARGET" 2>/dev/null || true + else + xdg-open "$TARGET" 2>/dev/null \ + || sensible-browser "$TARGET" 2>/dev/null \ + || x-www-browser "$TARGET" 2>/dev/null \ + || { echo "gstack-open: no browser launcher found (tried xdg-open, sensible-browser, x-www-browser)" >&2; exit 1; } + fi + ;; + MINGW*|MSYS*|CYGWIN*) + start "" "$TARGET" + ;; + *) + # Unknown OS — try xdg-open as a best-effort fallback + xdg-open "$TARGET" 2>/dev/null || { echo "gstack-open: unsupported OS: $(uname -s)" >&2; exit 1; } + ;; +esac diff --git a/bin/gstack-review-log b/bin/gstack-review-log index d7235bc3a..0eb9797c0 100755 --- a/bin/gstack-review-log +++ b/bin/gstack-review-log @@ -3,7 +3,35 @@ # Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}' set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Security: validate the input is a single-line JSON object before writing. +# Reject inputs containing newlines (JSONL injection) or that are not valid JSON. +INPUT="${1:-}" +if [ -z "$INPUT" ]; then + echo "gstack-review-log: missing argument" >&2 + exit 1 +fi + +# Reject multi-line input — a single JSONL entry must be one line +if printf '%s' "$INPUT" | grep -q $'\n'; then + echo "gstack-review-log: input contains newlines — rejected to prevent JSONL injection" >&2 + exit 1 +fi + +# Validate JSON structure using node or python if available; silently skip on failure +if command -v node >/dev/null 2>&1; then + if ! node -e "JSON.parse(process.argv[1])" "$INPUT" 2>/dev/null; then + echo "gstack-review-log: input is not valid JSON — rejected" >&2 + exit 1 + fi +elif command -v python3 >/dev/null 2>&1; then + if ! python3 -c "import sys,json; json.loads(sys.argv[1])" "$INPUT" 2>/dev/null; then + echo "gstack-review-log: input is not valid JSON — rejected" >&2 + exit 1 + fi +fi + eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)" GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" mkdir -p "$GSTACK_HOME/projects/$SLUG" -echo "$1" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" +printf '%s\n' "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" diff --git a/bin/gstack-telemetry-log b/bin/gstack-telemetry-log index 5cddc519f..84cdd2d0a 100755 --- a/bin/gstack-telemetry-log +++ b/bin/gstack-telemetry-log @@ -151,15 +151,37 @@ fi # ─── Construct and append JSON ─────────────────────────────── mkdir -p "$ANALYTICS_DIR" +# Sanitize a string for safe embedding in a JSONL value: +# - strip newlines/carriage returns (prevent JSONL injection — extra lines in the file) +# - escape backslashes and double-quotes (prevent JSON string corruption) +# - truncate to $2 bytes (optional, default 200) +sanitize_str() { + local val="$1" + local maxlen="${2:-200}" + printf '%s' "$val" \ + | tr -d '\n\r' \ + | head -c "$maxlen" \ + | sed 's/\\/\\\\/g; s/"/\\"/g' +} + # Escape null fields ERR_FIELD="null" -[ -n "$ERROR_CLASS" ] && ERR_FIELD="\"$ERROR_CLASS\"" +[ -n "$ERROR_CLASS" ] && ERR_FIELD="\"$(sanitize_str "$ERROR_CLASS" 100)\"" ERR_MSG_FIELD="null" -[ -n "$ERROR_MESSAGE" ] && ERR_MSG_FIELD="\"$(echo "$ERROR_MESSAGE" | head -c 200 | sed 's/"/\\"/g')\"" +[ -n "$ERROR_MESSAGE" ] && ERR_MSG_FIELD="\"$(sanitize_str "$ERROR_MESSAGE" 200)\"" STEP_FIELD="null" -[ -n "$FAILED_STEP" ] && STEP_FIELD="\"$(echo "$FAILED_STEP" | head -c 30)\"" +[ -n "$FAILED_STEP" ] && STEP_FIELD="\"$(sanitize_str "$FAILED_STEP" 30)\"" + +# Sanitize fields that appear directly in the printf format string +SKILL="$(sanitize_str "$SKILL" 60)" +SESSION_ID="$(sanitize_str "$SESSION_ID" 60)" +EVENT_TYPE="$(sanitize_str "$EVENT_TYPE" 40)" +OUTCOME="$(sanitize_str "$OUTCOME" 20)" +SOURCE="$(sanitize_str "$SOURCE" 20)" +REPO_SLUG="$(sanitize_str "$REPO_SLUG" 120)" +BRANCH="$(sanitize_str "$BRANCH" 120)" # Cap unreasonable durations if [ -n "$DURATION" ] && [ "$DURATION" -gt 86400 ] 2>/dev/null; then diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index c0b7adb1b..627cf8382 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -80,13 +80,13 @@ TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt) 2. Run the review (5-minute timeout): ```bash -codex review --base -c 'model_reasoning_effort="xhigh"' --enable web_search_cached 2>"$TMPERR" +codex review --base -c 'model_reasoning_effort="high"' --search 2>"$TMPERR" ``` Use `timeout: 300000` on the Bash call. If the user provided custom instructions (e.g., `/codex review focus on security`), pass them as the prompt argument: ```bash -codex review "focus on security" --base -c 'model_reasoning_effort="xhigh"' --enable web_search_cached 2>"$TMPERR" +codex review "focus on security" --base -c 'model_reasoning_effort="high"' --search 2>"$TMPERR" ``` 3. Capture the output. Then parse cost from stderr: @@ -95,8 +95,8 @@ grep "tokens used" "$TMPERR" 2>/dev/null || echo "tokens: unknown" ``` 4. Determine gate verdict by checking the review output for critical findings. - If the output contains `[P1]` — the gate is **FAIL**. - If no `[P1]` markers are found (only `[P2]` or no findings) — the gate is **PASS**. + If the output contains `[P0]` or `[P1]` — the gate is **FAIL**. + If no `[P0]` or `[P1]` markers are found (only `[P2]` or no findings) — the gate is **PASS**. 5. Present the output: @@ -131,7 +131,7 @@ CROSS-MODEL ANALYSIS: ``` Substitute: TIMESTAMP (ISO 8601), STATUS ("clean" if PASS, "issues_found" if FAIL), -GATE ("pass" or "fail"), findings (count of [P1] + [P2] markers), +GATE ("pass" or "fail"), findings (count of [P0] + [P1] + [P2] markers), findings_fixed (count of findings that were addressed/fixed before shipping). 8. Clean up temp files: @@ -159,7 +159,7 @@ With focus (e.g., "security"): 2. Run codex exec with **JSONL output** to capture reasoning traces and tool calls (5-minute timeout): ```bash -codex exec "" -s read-only -c 'model_reasoning_effort="xhigh"' --enable web_search_cached --json 2>/dev/null | python3 -c " +codex exec "" -s read-only -c 'model_reasoning_effort="high"' --search --json 2>/dev/null | python3 -c " import sys, json for line in sys.stdin: line = line.strip() @@ -244,7 +244,7 @@ THE PLAN: For a **new session:** ```bash -codex exec "" -s read-only -c 'model_reasoning_effort="xhigh"' --enable web_search_cached --json 2>"$TMPERR" | python3 -c " +codex exec "" -s read-only -c 'model_reasoning_effort="high"' --search --json 2>"$TMPERR" | python3 -c " import sys, json for line in sys.stdin: line = line.strip() @@ -277,7 +277,7 @@ for line in sys.stdin: For a **resumed session** (user chose "Continue"): ```bash -codex exec resume "" -s read-only -c 'model_reasoning_effort="xhigh"' --enable web_search_cached --json 2>"$TMPERR" | python3 -c " +codex exec resume "" -s read-only -c 'model_reasoning_effort="high"' --search --json 2>"$TMPERR" | python3 -c " " ``` @@ -313,10 +313,10 @@ Session saved — run /codex again to continue this conversation. agentic coding model). This means as OpenAI ships newer models, /codex automatically uses them. If the user wants a specific model, pass `-m` through to codex. -**Reasoning effort:** All modes use `xhigh` — maximum reasoning power. When reviewing code, breaking code, or consulting on architecture, you want the model thinking as hard as possible. +**Reasoning effort:** All modes use `high` — maximum reasoning power. When reviewing code, breaking code, or consulting on architecture, you want the model thinking as hard as possible. (`xhigh` is not a valid value and causes an API error.) -**Web search:** All codex commands use `--enable web_search_cached` so Codex can look up -docs and APIs during review. This is OpenAI's cached index — fast, no extra cost. +**Web search:** All codex commands use `--search` so Codex can look up +docs and APIs during review. (`--enable web_search_cached` is deprecated — use `--search`.) If the user specifies a model (e.g., `/codex review -m gpt-5.1-codex-max` or `/codex challenge -m gpt-5.2`), pass the `-m` flag through to codex. diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 93abb1bb6..8f432c6df 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -600,7 +600,7 @@ Say: Then use AskUserQuestion: "Would you consider applying to Y Combinator?" -- If yes → run `open https://ycombinator.com/apply?ref=gstack` and say: "Bring this design doc to your YC interview. It's better than most pitch decks." +- If yes → run `~/.claude/skills/gstack/bin/gstack-open https://ycombinator.com/apply?ref=gstack` and say: "Bring this design doc to your YC interview. It's better than most pitch decks." - If no → respond warmly: "Totally fair. The design doc is yours either way — and the offer stands if you ever change your mind." Then proceed to next-skill recs. No pressure, no guilt, no re-ask. **Middle tier** — emotional target: *"I might be onto something."* Validation + curiosity. diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 8d483dad2..bc0fcc696 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -235,7 +235,7 @@ thing when AI makes the marginal cost near-zero. Read more: https://garryslist.o Then offer to open the essay in their default browser: \`\`\`bash -open https://garryslist.org/posts/boil-the-ocean +~/.claude/skills/gstack/bin/gstack-open https://garryslist.org/posts/boil-the-ocean touch ~/.gstack/.completeness-intro-seen \`\`\` diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index 76573422e..4d9789e84 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -61,7 +61,7 @@ thing when AI makes the marginal cost near-zero. Read more: https://garryslist.o Then offer to open the essay in their default browser: \`\`\`bash -open https://garryslist.org/posts/boil-the-ocean +~/.claude/skills/gstack/bin/gstack-open https://garryslist.org/posts/boil-the-ocean touch ~/.gstack/.completeness-intro-seen \`\`\` diff --git a/setup b/setup index bfae87851..38893a438 100755 --- a/setup +++ b/setup @@ -3,9 +3,20 @@ set -e if ! command -v bun >/dev/null 2>&1; then - echo "Error: bun is required but not installed." >&2 - echo "Install it: curl -fsSL https://bun.sh/install | bash" >&2 - exit 1 + echo "Bun not found — installing automatically..." + if ! curl -fsSL https://bun.sh/install | bash; then + echo "Error: Bun auto-install failed." >&2 + echo "Install it manually: curl -fsSL https://bun.sh/install | bash" >&2 + exit 1 + fi + # Pick up the newly installed bun from the default location + export PATH="$HOME/.bun/bin:$PATH" + if ! command -v bun >/dev/null 2>&1; then + echo "Error: bun installed but not found in PATH." >&2 + echo "Open a new terminal and re-run setup, or add ~/.bun/bin to your PATH." >&2 + exit 1 + fi + echo "Bun installed successfully." fi INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"