From fdd45188ff0534bec500d4d7eb6f999a310f6a61 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 22 Mar 2026 21:02:01 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20gstack-slug=20bash=20compatibility=20?= =?UTF-8?q?=E2=80=94=20source=20to=20eval=20(#354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: replace source <(gstack-slug) with eval for bash compatibility Under bash with set -euo pipefail, source <(cmd) process substitution doesn't reliably set variables in the caller's scope. The variables stay empty and -u (nounset) crashes the script. eval "$(cmd)" works correctly in both bash and zsh. Fixes: gstack-review-read, gstack-review-log, gstack-slug comment, gen-skill-docs.ts resolver functions, and regression tests. * chore: bump version and changelog (v0.11.4.0) Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 8 ++++++++ VERSION | 2 +- autoplan/SKILL.md | 2 +- autoplan/SKILL.md.tmpl | 2 +- benchmark/SKILL.md | 2 +- benchmark/SKILL.md.tmpl | 2 +- bin/gstack-review-log | 2 +- bin/gstack-review-read | 2 +- bin/gstack-slug | 2 +- canary/SKILL.md | 4 ++-- canary/SKILL.md.tmpl | 4 ++-- design-consultation/SKILL.md | 2 +- design-consultation/SKILL.md.tmpl | 2 +- design-review/SKILL.md | 4 ++-- design-review/SKILL.md.tmpl | 2 +- land-and-deploy/SKILL.md | 2 +- land-and-deploy/SKILL.md.tmpl | 2 +- office-hours/SKILL.md | 4 ++-- office-hours/SKILL.md.tmpl | 4 ++-- plan-ceo-review/SKILL.md | 4 ++-- plan-ceo-review/SKILL.md.tmpl | 4 ++-- plan-eng-review/SKILL.md | 2 +- qa-only/SKILL.md | 4 ++-- qa-only/SKILL.md.tmpl | 4 ++-- qa/SKILL.md | 4 ++-- qa/SKILL.md.tmpl | 4 ++-- scripts/gen-skill-docs.ts | 16 +++++++++++++--- ship/SKILL.md | 6 +++--- ship/SKILL.md.tmpl | 4 ++-- test/skill-validation.test.ts | 19 +++++++++++++++++++ 30 files changed, 81 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a9b7304fe841e39aab1b0412d79d97897a127e4..3e541f40b17fdb460e25964be90fcb82b30c2626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.11.5.0] - 2026-03-23 — Bash Compatibility Fix + +### Fixed + +- **`gstack-review-read` and `gstack-review-log` no longer crash under bash.** These scripts used `source <(gstack-slug)` which silently fails to set variables under bash with `set -euo pipefail`, causing `SLUG: unbound variable` errors. Replaced with `eval "$(gstack-slug)"` which works correctly in both bash and zsh. +- **All SKILL.md templates updated.** Every template that instructed agents to run `source <(gstack-slug)` now uses `eval "$(gstack-slug)"` for cross-shell compatibility. Regenerated all SKILL.md files from templates. +- **Regression tests added.** New tests verify `eval "$(gstack-slug)"` works under bash strict mode, and guard against `source <(.*gstack-slug` patterns reappearing in templates or bin scripts. + ## [0.11.4.0] - 2026-03-22 — Codex in Office Hours ### Added diff --git a/VERSION b/VERSION index 15e91100ca43dbdd5b1b4e6a6015f2bd482ea4d4..feca3c1f4be0c31f6c65a8356eeca58a590c4655 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.11.4.0 +0.11.5.0 diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index cd9f81d1398658f4c1b53c9955870a85f0b7e619..10ba35220265f53c699342555cb2b736db7d5eb8 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -416,7 +416,7 @@ State what you examined and why nothing was flagged (1-2 sentences minimum). Before doing anything, save the plan file's current state to an external file: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-') DATETIME=$(date +%Y%m%d-%H%M%S) echo "RESTORE_PATH=$HOME/.gstack/projects/$SLUG/${BRANCH}-autoplan-restore-${DATETIME}.md" diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index 6c844d6795c1771575119d7d30c85e96e4f5a2dc..2213c8b9d689949e15dfa78b964ce087ac0dbbd8 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -107,7 +107,7 @@ State what you examined and why nothing was flagged (1-2 sentences minimum). Before doing anything, save the plan file's current state to an external file: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +{{SLUG_SETUP}} BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-') DATETIME=$(date +%Y%m%d-%H%M%S) echo "RESTORE_PATH=$HOME/.gstack/projects/$SLUG/${BRANCH}-autoplan-restore-${DATETIME}.md" diff --git a/benchmark/SKILL.md b/benchmark/SKILL.md index ac12a2801be8f84bcc308eec03b977308b3762bb..9e1a6bc34a53b26f5e81c8a38b29aae7f297bfbe 100644 --- a/benchmark/SKILL.md +++ b/benchmark/SKILL.md @@ -333,7 +333,7 @@ When the user types `/benchmark`, run this skill. ### Phase 1: Setup ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown") +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown")" mkdir -p .gstack/benchmark-reports mkdir -p .gstack/benchmark-reports/baselines ``` diff --git a/benchmark/SKILL.md.tmpl b/benchmark/SKILL.md.tmpl index d0c0ecbc33a6632d22db0a48b336dd87ff8ee0a6..f72b5a9323e1a86ae851baf3b9efe9298fc5da63 100644 --- a/benchmark/SKILL.md.tmpl +++ b/benchmark/SKILL.md.tmpl @@ -41,7 +41,7 @@ When the user types `/benchmark`, run this skill. ### Phase 1: Setup ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown") +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown")" mkdir -p .gstack/benchmark-reports mkdir -p .gstack/benchmark-reports/baselines ``` diff --git a/bin/gstack-review-log b/bin/gstack-review-log index 816cfa465cff8590743d70ab50bd3b068c5f7850..d7235bc3ac8f879e1fd0cb0ff7a1c90362494871 100755 --- a/bin/gstack-review-log +++ b/bin/gstack-review-log @@ -3,7 +3,7 @@ # Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}' set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source <("$SCRIPT_DIR/gstack-slug" 2>/dev/null) +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" diff --git a/bin/gstack-review-read b/bin/gstack-review-read index 578d748092b53d10da185bf26ff80c83c01b1c20..ccf1d70f64f60158fdf26dbd3ef4b5b0ff2eb7c6 100755 --- a/bin/gstack-review-read +++ b/bin/gstack-review-read @@ -3,7 +3,7 @@ # Usage: gstack-review-read set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source <("$SCRIPT_DIR/gstack-slug" 2>/dev/null) +eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)" GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" cat "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null || echo "NO_REVIEWS" echo "---CONFIG---" diff --git a/bin/gstack-slug b/bin/gstack-slug index 3ad303810f7eb6604f654ac2c1bac523d5a50a10..a7ae7883916f12deb7d72594d0f1627b0b14aea3 100755 --- a/bin/gstack-slug +++ b/bin/gstack-slug @@ -1,6 +1,6 @@ #!/usr/bin/env bash # gstack-slug — output project slug and sanitized branch name -# Usage: source <(gstack-slug) → sets SLUG and BRANCH variables +# Usage: eval "$(gstack-slug)" → sets SLUG and BRANCH variables # Or: gstack-slug → prints SLUG=... and BRANCH=... lines # # Security: output is sanitized to [a-zA-Z0-9._-] only, preventing diff --git a/canary/SKILL.md b/canary/SKILL.md index c71d51a9c85c2650b3d7b1062fa0ab12cb492ac4..2b2bb4038138644c56674d496c821d4d8b8ab537 100644 --- a/canary/SKILL.md +++ b/canary/SKILL.md @@ -351,7 +351,7 @@ When the user types `/canary`, run this skill. ### Phase 1: Setup ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown") +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown")" mkdir -p .gstack/canary-reports mkdir -p .gstack/canary-reports/baselines mkdir -p .gstack/canary-reports/screenshots @@ -501,7 +501,7 @@ Save report to `.gstack/canary-reports/{date}-canary.md` and `.gstack/canary-rep Log the result for the review dashboard: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" mkdir -p ~/.gstack/projects/$SLUG ``` diff --git a/canary/SKILL.md.tmpl b/canary/SKILL.md.tmpl index d0ddadfe895e99a09ffebf5aa9acb949f5535e02..eca0fd1f4529d8dfcbda261f98d5e7ad957d7d0f 100644 --- a/canary/SKILL.md.tmpl +++ b/canary/SKILL.md.tmpl @@ -42,7 +42,7 @@ When the user types `/canary`, run this skill. ### Phase 1: Setup ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown") +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown")" mkdir -p .gstack/canary-reports mkdir -p .gstack/canary-reports/baselines mkdir -p .gstack/canary-reports/screenshots @@ -192,7 +192,7 @@ Save report to `.gstack/canary-reports/{date}-canary.md` and `.gstack/canary-rep Log the result for the review dashboard: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +{{SLUG_EVAL}} mkdir -p ~/.gstack/projects/$SLUG ``` diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index d4cf72b23293d1c7ead47e2e15c1985b3820c80d..3e5b18892b419ed72749ffd880f03ff1ed51f693 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -327,7 +327,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30 Look for office-hours output: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" ls ~/.gstack/projects/$SLUG/*office-hours* 2>/dev/null | head -5 ls .context/*office-hours* .context/attachments/*office-hours* 2>/dev/null | head -5 ``` diff --git a/design-consultation/SKILL.md.tmpl b/design-consultation/SKILL.md.tmpl index 87c191df129ec059e19148f00a11972f070904f5..d8604cebff3894ec77815d615c734687bb1bcb1b 100644 --- a/design-consultation/SKILL.md.tmpl +++ b/design-consultation/SKILL.md.tmpl @@ -52,7 +52,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30 Look for office-hours output: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +{{SLUG_EVAL}} ls ~/.gstack/projects/$SLUG/*office-hours* 2>/dev/null | head -5 ls .context/*office-hours* .context/attachments/*office-hours* 2>/dev/null | head -5 ``` diff --git a/design-review/SKILL.md b/design-review/SKILL.md index ae8fa1bdb3cb14dc15a93601c423721f669e9cc3..a1abf723fa2a718b9c5bf4375c011c3da24b0bb7 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -774,7 +774,7 @@ Compare screenshots and observations across pages for: **Project-scoped:** ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG ``` Write to: `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md` @@ -1142,7 +1142,7 @@ Write the report to both local and project-scoped locations: **Project-scoped:** ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG ``` Write to `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md` diff --git a/design-review/SKILL.md.tmpl b/design-review/SKILL.md.tmpl index e54332fa8b6ecb63fb95027a1b87169656046b52..636307e87186cdff8004ac51bb1cd616f75a429a 100644 --- a/design-review/SKILL.md.tmpl +++ b/design-review/SKILL.md.tmpl @@ -224,7 +224,7 @@ Write the report to both local and project-scoped locations: **Project-scoped:** ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +{{SLUG_SETUP}} ``` Write to `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md` diff --git a/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index 25bc5e549a5eacc6d4c775044b5c8873c8757b9a..c89e2de0ce830c2eac9741eddd96434da07bdacb 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -883,7 +883,7 @@ Save report to `.gstack/deploy-reports/{date}-pr{number}-deploy.md`. Log to the review dashboard: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" mkdir -p ~/.gstack/projects/$SLUG ``` diff --git a/land-and-deploy/SKILL.md.tmpl b/land-and-deploy/SKILL.md.tmpl index 0e84d8596070f4e71100ed8079c408f77309ecf3..af902b9b0f72613221a15426714b15367933984b 100644 --- a/land-and-deploy/SKILL.md.tmpl +++ b/land-and-deploy/SKILL.md.tmpl @@ -542,7 +542,7 @@ Save report to `.gstack/deploy-reports/{date}-pr{number}-deploy.md`. Log to the review dashboard: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +{{SLUG_EVAL}} mkdir -p ~/.gstack/projects/$SLUG ``` diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index a94984bfb3cc36c188d4cec6af0556d61c3b025f..417f6be41125f72e6c09d289b2ca31387e50f701 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -331,7 +331,7 @@ You are a **YC office hours partner**. Your job is to ensure the problem is unde Understand the project and the area the user wants to change. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" ``` 1. Read `CLAUDE.md`, `TODOS.md` (if they exist). @@ -870,7 +870,7 @@ Count the signals. You'll use this count in Phase 6 to determine which tier of c Write the design document to the project directory. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG USER=$(whoami) DATETIME=$(date +%Y%m%d-%H%M%S) ``` diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 03368faf460bc4af1f2c4b2c1b945ccc3b0d214a..55e916c9e0d09d0f36cee496219e216b8532d28d 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -39,7 +39,7 @@ You are a **YC office hours partner**. Your job is to ensure the problem is unde Understand the project and the area the user wants to change. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +{{SLUG_EVAL}} ``` 1. Read `CLAUDE.md`, `TODOS.md` (if they exist). @@ -413,7 +413,7 @@ Count the signals. You'll use this count in Phase 6 to determine which tier of c Write the design document to the project directory. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +{{SLUG_SETUP}} USER=$(whoami) DATETIME=$(date +%Y%m%d-%H%M%S) ``` diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 1050ed4f5ee53014c8f01c1f93a8165f5946bf72..a9f4f67100ba25673530ba620b296e0bc72116ce 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -607,7 +607,7 @@ Rules: After the opt-in/cherry-pick ceremony, write the plan to disk so the vision and decisions survive beyond this conversation. Only run this step for EXPANSION and SELECTIVE EXPANSION modes. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG/ceo-plans +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG/ceo-plans ``` Before writing, check for existing CEO plans in the ceo-plans/ directory. If any are >30 days old or their branch has been merged/deleted, offer to archive them: @@ -1101,7 +1101,7 @@ After producing the Completion Summary, clean up any handoff notes for this bran the review is complete and the context is no longer needed. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" rm -f ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true ``` diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index 6b676a8628af422ebd23132c94b9d860cf441eaa..a94c9ffe1ccc57474213e65eadaf5517f4a25140 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -298,7 +298,7 @@ Rules: After the opt-in/cherry-pick ceremony, write the plan to disk so the vision and decisions survive beyond this conversation. Only run this step for EXPANSION and SELECTIVE EXPANSION modes. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG/ceo-plans +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG/ceo-plans ``` Before writing, check for existing CEO plans in the ceo-plans/ directory. If any are >30 days old or their branch has been merged/deleted, offer to archive them: @@ -732,7 +732,7 @@ After producing the Completion Summary, clean up any handoff notes for this bran the review is complete and the context is no longer needed. ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) +{{SLUG_EVAL}} rm -f ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true ``` diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index 435d4f09dcc275c1dd1074b626672fabf33fead0..678d42a3bc4e89bb258bf88ab804a690bb706f61 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -611,7 +611,7 @@ The plan should be complete enough that when implementation begins, every test i After producing the coverage diagram, write a test plan artifact to the project directory so `/qa` and `/qa-only` can consume it as primary test input: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG USER=$(whoami) DATETIME=$(date +%Y%m%d-%H%M%S) ``` diff --git a/qa-only/SKILL.md b/qa-only/SKILL.md index bd71461209e882d5ceed0ba7f9ceb0f7a3b0dbac..48a3983cfe29b0d75b40fcaf4a185a903c0ab09c 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -346,7 +346,7 @@ Before falling back to git diff heuristics, check for richer test plan sources: 1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` 2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation @@ -642,7 +642,7 @@ Write the report to both local and project-scoped locations: **Project-scoped:** Write test outcome artifact for cross-session context: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG ``` Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md` diff --git a/qa-only/SKILL.md.tmpl b/qa-only/SKILL.md.tmpl index 293a7b368ea2b45bb8eec1372bd263995b8bb393..ef4ae6bd6bcf14df713134f030107ac1771f0ac6 100644 --- a/qa-only/SKILL.md.tmpl +++ b/qa-only/SKILL.md.tmpl @@ -54,7 +54,7 @@ Before falling back to git diff heuristics, check for richer test plan sources: 1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + {{SLUG_EVAL}} ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` 2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation @@ -74,7 +74,7 @@ Write the report to both local and project-scoped locations: **Project-scoped:** Write test outcome artifact for cross-session context: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +{{SLUG_SETUP}} ``` Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md` diff --git a/qa/SKILL.md b/qa/SKILL.md index d59ba122b5296e91666b962256cc2c787a67b73d..2bd5d305ac9a1fab00afe1d6ef826cee894fa29c 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -549,7 +549,7 @@ Before falling back to git diff heuristics, check for richer test plan sources: 1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` 2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation @@ -1013,7 +1013,7 @@ Write the report to both local and project-scoped locations: **Project-scoped:** Write test outcome artifact for cross-session context: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG ``` Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md` diff --git a/qa/SKILL.md.tmpl b/qa/SKILL.md.tmpl index a3d02abcf8acbd043bf6d99cfa933f207f0d4caf..c569572255347b16375566e852938d898106db1b 100644 --- a/qa/SKILL.md.tmpl +++ b/qa/SKILL.md.tmpl @@ -89,7 +89,7 @@ Before falling back to git diff heuristics, check for richer test plan sources: 1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + {{SLUG_EVAL}} ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` 2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation @@ -277,7 +277,7 @@ Write the report to both local and project-scoped locations: **Project-scoped:** Write test outcome artifact for cross-session context: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +{{SLUG_SETUP}} ``` Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md` diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 1fa0507f8fa7ad87f013e02da528a7912ab80a7a..c728dfae8cf7316120d4d9b5ca03bc25cd9f6ee8 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -1219,7 +1219,7 @@ Compare screenshots and observations across pages for: **Project-scoped:** \`\`\`bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG \`\`\` Write to: \`~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md\` @@ -1815,7 +1815,7 @@ The plan should be complete enough that when implementation begins, every test i After producing the coverage diagram, write a test plan artifact to the project directory so \`/qa\` and \`/qa-only\` can consume it as primary test input: \`\`\`bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG USER=$(whoami) DATETIME=$(date +%Y%m%d-%H%M%S) \`\`\` @@ -1879,7 +1879,7 @@ Coverage line: \`Test Coverage Audit: N new code paths. M covered (X%). K tests After producing the coverage diagram, write a test plan artifact so \`/qa\` and \`/qa-only\` can consume it: \`\`\`bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG USER=$(whoami) DATETIME=$(date +%Y%m%d-%H%M%S) \`\`\` @@ -2629,7 +2629,17 @@ ${slopItems} Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4) (Mar 2026) + gstack design methodology.`; } +function generateSlugEval(ctx: TemplateContext): string { + return `eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)"`; +} + +function generateSlugSetup(ctx: TemplateContext): string { + return `eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG`; +} + const RESOLVERS: Record string> = { + SLUG_EVAL: generateSlugEval, + SLUG_SETUP: generateSlugSetup, COMMAND_REFERENCE: generateCommandReference, SNAPSHOT_FLAGS: generateSnapshotFlags, PREAMBLE: generatePreamble, diff --git a/ship/SKILL.md b/ship/SKILL.md index af138e1ddece03c4ec0f2445e8ceb86801653ed4..ce3196eaaee53d3e4f92780271d44895ba82a0d9 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -395,7 +395,7 @@ If the Eng Review is NOT "CLEAR": 1. **Check for a prior override on this branch:** ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" grep '"skill":"ship-review-override"' ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_OVERRIDE" ``` If an override exists, display the dashboard and note "Review gate previously accepted — continuing." Do NOT ask again. @@ -409,7 +409,7 @@ If the Eng Review is NOT "CLEAR": 3. **If the user chooses A or C,** persist the decision so future `/ship` runs on this branch skip the gate: ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" echo '{"skill":"ship-review-override","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","decision":"USER_CHOICE"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl ``` Substitute USER_CHOICE with "ship_anyway" or "not_relevant". @@ -969,7 +969,7 @@ Coverage line: `Test Coverage Audit: N new code paths. M covered (X%). K tests g After producing the coverage diagram, write a test plan artifact so `/qa` and `/qa-only` can consume it: ```bash -source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) && mkdir -p ~/.gstack/projects/$SLUG +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG USER=$(whoami) DATETIME=$(date +%Y%m%d-%H%M%S) ``` diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index bd74c197ae9d156ac81e7b3e70f11898cee3d57d..0308dd8be17d79732748a8c5763b204c834671d2 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -62,7 +62,7 @@ If the Eng Review is NOT "CLEAR": 1. **Check for a prior override on this branch:** ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + {{SLUG_EVAL}} grep '"skill":"ship-review-override"' ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl 2>/dev/null || echo "NO_OVERRIDE" ``` If an override exists, display the dashboard and note "Review gate previously accepted — continuing." Do NOT ask again. @@ -76,7 +76,7 @@ If the Eng Review is NOT "CLEAR": 3. **If the user chooses A or C,** persist the decision so future `/ship` runs on this branch skip the gate: ```bash - source <(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null) + {{SLUG_EVAL}} echo '{"skill":"ship-review-override","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","decision":"USER_CHOICE"}' >> ~/.gstack/projects/$SLUG/$BRANCH-reviews.jsonl ``` Substitute USER_CHOICE with "ship_anyway" or "not_relevant". diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index 2085eaec45d9554971359363a701fd37c0661e1f..3d76ae67a85f364a552065ced916eca60f65c897 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -1004,6 +1004,25 @@ describe('gstack-slug', () => { expect(slug).toMatch(/^[a-zA-Z0-9._-]+$/); expect(branch).toMatch(/^[a-zA-Z0-9._-]+$/); }); + test('eval sets variables under bash with set -euo pipefail', () => { + const result = Bun.spawnSync( + ['bash', '-c', 'set -euo pipefail; eval "$(./bin/gstack-slug 2>/dev/null)"; echo "SLUG=$SLUG"; echo "BRANCH=$BRANCH"'], + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' } + ); + expect(result.exitCode).toBe(0); + const output = result.stdout.toString(); + expect(output).toMatch(/^SLUG=.+/m); + expect(output).toMatch(/^BRANCH=.+/m); + }); + + test('no templates or bin scripts use source process substitution for gstack-slug', () => { + const result = Bun.spawnSync( + ['grep', '-r', 'source <(.*gstack-slug', '--include=*.tmpl', '--include=gstack-review-*', '.'], + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' } + ); + // grep returns exit code 1 when no matches found — that's what we want + expect(result.stdout.toString().trim()).toBe(''); + }); }); // --- Test Bootstrap validation ---