From 60061d0b6dfaadc504728abc6ec9db5dbd85f5d1 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 27 Mar 2026 00:23:37 -0600 Subject: [PATCH] fix: zsh glob compatibility across all skill templates (v0.12.8.1) (#559) * fix: replace zsh-incompatible raw globs with find-based alternatives and setopt guards Zsh's NOMATCH option (on by default) causes raw globs like `*.yaml` and `*deploy*` to throw errors when no files match, instead of silently expanding to nothing as bash does. The preamble resolver already handled this correctly with find, but 38 glob instances across 13 templates and 2 resolvers still used raw shell globs. Two fix approaches based on complexity: - find-based replacement for cat/for/ls-with-pipes patterns (.github/workflows/) - setopt +o nomatch guard for simple ls -t patterns (~/.gstack/, ~/.claude/) Co-Authored-By: Claude Opus 4.6 (1M context) * chore: regenerate SKILL.md files from updated templates Co-Authored-By: Claude Opus 4.6 (1M context) * chore: bump version and changelog (v0.12.8.1) Co-Authored-By: Claude Opus 4.6 * test: add zsh glob safety test + fix 2 missed resolver globs Adds a test that scans all generated SKILL.md bash blocks for raw glob patterns and verifies they have either a find-based replacement or a setopt +o nomatch guard. The test immediately caught 2 unguarded blocks in review.ts (design doc re-check and plan file discovery). Also syncs package.json version to 0.12.8.1. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 10 +++++++++ VERSION | 2 +- autoplan/SKILL.md | 1 + codex/SKILL.md | 1 + codex/SKILL.md.tmpl | 1 + cso/SKILL.md | 7 +++--- cso/SKILL.md.tmpl | 7 +++--- design-consultation/SKILL.md | 1 + design-consultation/SKILL.md.tmpl | 1 + design-review/SKILL.md | 1 + land-and-deploy/SKILL.md | 12 +++++----- land-and-deploy/SKILL.md.tmpl | 8 ++++--- office-hours/SKILL.md | 3 +++ office-hours/SKILL.md.tmpl | 3 +++ package.json | 2 +- plan-ceo-review/SKILL.md | 4 ++++ plan-ceo-review/SKILL.md.tmpl | 3 +++ plan-eng-review/SKILL.md | 3 +++ plan-eng-review/SKILL.md.tmpl | 1 + qa-only/SKILL.md | 1 + qa-only/SKILL.md.tmpl | 1 + qa/SKILL.md | 2 ++ qa/SKILL.md.tmpl | 1 + retro/SKILL.md | 5 +++++ retro/SKILL.md.tmpl | 5 +++++ review/SKILL.md | 2 ++ scripts/resolvers/review.ts | 2 ++ scripts/resolvers/testing.ts | 2 ++ scripts/resolvers/utility.ts | 2 +- setup-deploy/SKILL.md | 4 ++-- setup-deploy/SKILL.md.tmpl | 4 ++-- ship/SKILL.md | 3 +++ test/gen-skill-docs.test.ts | 37 +++++++++++++++++++++++++++++++ 33 files changed, 121 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569c88e8f..a04e1473c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.12.8.1] - 2026-03-27 — zsh Glob Compatibility + +Skill scripts now work correctly in zsh. Previously, bash code blocks in skill templates used raw glob patterns like `.github/workflows/*.yaml` and `ls ~/.gstack/projects/$SLUG/*-design-*.md` that would throw "no matches found" errors in zsh when no files matched. Fixed 38 instances across 13 templates and 2 resolvers using two approaches: `find`-based alternatives for complex patterns, and `setopt +o nomatch` guards for simple `ls` commands. + +### Fixed + +- **`.github/workflows/` globs replaced with `find`.** `cat .github/workflows/*deploy*`, `for f in .github/workflows/*.yml`, and `ls .github/workflows/*.yaml` patterns in `/land-and-deploy`, `/setup-deploy`, `/cso`, and the deploy bootstrap resolver now use `find ... -name` instead of raw globs. +- **`~/.gstack/` and `~/.claude/` globs guarded with `setopt`.** Design doc lookups, eval result listings, test plan discovery, and retro history checks across 10 skills now prepend `setopt +o nomatch 2>/dev/null || true` (no-op in bash, disables NOMATCH in zsh). +- **Test framework detection globs guarded.** `ls jest.config.* vitest.config.*` in the testing resolver now has a setopt guard. + ## [0.12.8.0] - 2026-03-27 — Codex No Longer Reviews the Wrong Project When you run gstack in Conductor with multiple workspaces open, Codex could silently review the wrong project. The `codex exec -C` flag resolved the repo root inline via `$(git rev-parse --show-toplevel)`, which evaluates in whatever cwd the background shell inherits. In multi-workspace environments, that cwd might be a different project entirely. diff --git a/VERSION b/VERSION index d6afffa6d..a3866b38c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.12.8.0 +0.12.8.1 diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 5f8b5013f..54a8f213d 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -408,6 +408,7 @@ If the Read fails (file not found), say: After /office-hours completes, re-run the design doc check: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) diff --git a/codex/SKILL.md b/codex/SKILL.md index 471280374..19a8e423e 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -650,6 +650,7 @@ TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt) 3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan, or if plan files exist and the user said `/codex` with no arguments: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1 ``` If no project-scoped match, fall back to `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1` diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 60247abd9..23ae7f52d 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -245,6 +245,7 @@ TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt) 3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan, or if plan files exist and the user said `/codex` with no arguments: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1 ``` If no project-scoped match, fall back to `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1` diff --git a/cso/SKILL.md b/cso/SKILL.md index 3deaca0ab..07026ad65 100644 --- a/cso/SKILL.md +++ b/cso/SKILL.md @@ -358,7 +358,7 @@ ls go.mod 2>/dev/null && echo "STACK: Go" ls Cargo.toml 2>/dev/null && echo "STACK: Rust" ls pom.xml build.gradle 2>/dev/null && echo "STACK: JVM" ls composer.json 2>/dev/null && echo "STACK: PHP" -ls *.csproj *.sln 2>/dev/null && echo "STACK: .NET" +find . -maxdepth 1 \( -name '*.csproj' -o -name '*.sln' \) 2>/dev/null | grep -q . && echo "STACK: .NET" ``` **Framework detection:** @@ -395,7 +395,8 @@ Map what an attacker sees — both code surface and infrastructure surface. **Infrastructure surface:** ```bash -ls .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml 2>/dev/null | wc -l +setopt +o nomatch 2>/dev/null || true # zsh compat +{ find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null; [ -f .gitlab-ci.yml ] && echo .gitlab-ci.yml; } | wc -l find . -maxdepth 4 -name "Dockerfile*" -o -name "docker-compose*.yml" 2>/dev/null find . -maxdepth 4 -name "*.tf" -o -name "*.tfvars" -o -name "kustomization.yaml" 2>/dev/null ls .env .env.* 2>/dev/null @@ -445,7 +446,7 @@ grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored **CI configs with inline secrets (not using secret stores):** ```bash -for f in .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml .circleci/config.yml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do [ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.' done 2>/dev/null ``` diff --git a/cso/SKILL.md.tmpl b/cso/SKILL.md.tmpl index b1904a8e7..676c1bd94 100644 --- a/cso/SKILL.md.tmpl +++ b/cso/SKILL.md.tmpl @@ -73,7 +73,7 @@ ls go.mod 2>/dev/null && echo "STACK: Go" ls Cargo.toml 2>/dev/null && echo "STACK: Rust" ls pom.xml build.gradle 2>/dev/null && echo "STACK: JVM" ls composer.json 2>/dev/null && echo "STACK: PHP" -ls *.csproj *.sln 2>/dev/null && echo "STACK: .NET" +find . -maxdepth 1 \( -name '*.csproj' -o -name '*.sln' \) 2>/dev/null | grep -q . && echo "STACK: .NET" ``` **Framework detection:** @@ -110,7 +110,8 @@ Map what an attacker sees — both code surface and infrastructure surface. **Infrastructure surface:** ```bash -ls .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml 2>/dev/null | wc -l +setopt +o nomatch 2>/dev/null || true # zsh compat +{ find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null; [ -f .gitlab-ci.yml ] && echo .gitlab-ci.yml; } | wc -l find . -maxdepth 4 -name "Dockerfile*" -o -name "docker-compose*.yml" 2>/dev/null find . -maxdepth 4 -name "*.tf" -o -name "*.tfvars" -o -name "kustomization.yaml" 2>/dev/null ls .env .env.* 2>/dev/null @@ -160,7 +161,7 @@ grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored **CI configs with inline secrets (not using secret stores):** ```bash -for f in .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml .circleci/config.yml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do [ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.' done 2>/dev/null ``` diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 52cef88ac..32394b37d 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -356,6 +356,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30 Look for office-hours output: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat 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 f33eabb6d..2d7a5a342 100644 --- a/design-consultation/SKILL.md.tmpl +++ b/design-consultation/SKILL.md.tmpl @@ -53,6 +53,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30 Look for office-hours output: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat {{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 2f64917ce..55674c3b3 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -401,6 +401,7 @@ If `NEEDS_SETUP`: **Detect existing test framework and project runtime:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" diff --git a/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index 655183dab..becc6b1cc 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -470,7 +470,7 @@ else SAVED_HASH=$(cat ~/.gstack/projects/$SLUG/land-deploy-confirmed 2>/dev/null) CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1) # Also hash workflow files that affect deploy behavior - WORKFLOW_HASH=$(cat .github/workflows/*deploy* .github/workflows/*cd* 2>/dev/null | shasum -a 256 | cut -d' ' -f1) + WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1) COMBINED_HASH="${CURRENT_HASH}-${WORKFLOW_HASH}" if [ "$SAVED_HASH" != "$COMBINED_HASH" ] && [ -n "$SAVED_HASH" ]; then echo "CONFIG_CHANGED" @@ -527,7 +527,7 @@ fi ([ -f railway.json ] || [ -f railway.toml ]) && echo "PLATFORM:railway" # Detect deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done @@ -613,7 +613,7 @@ grep -i "staging" CLAUDE.md 2>/dev/null | head -3 2. **GitHub Actions staging workflow:** Check for workflow files with "staging" in the name or content: ```bash -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done ``` @@ -663,7 +663,7 @@ Save the deploy config fingerprint so we can detect future changes: ```bash mkdir -p ~/.gstack/projects/$SLUG CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1) -WORKFLOW_HASH=$(cat .github/workflows/*deploy* .github/workflows/*cd* 2>/dev/null | shasum -a 256 | cut -d' ' -f1) +WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1) echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > ~/.gstack/projects/$SLUG/land-deploy-confirmed ``` Continue to Step 2. @@ -805,6 +805,7 @@ If tests fail: **BLOCKER.** Cannot merge with failing tests. **E2E tests — check recent results:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack-dev/evals/*-e2e-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -20 ``` @@ -820,6 +821,7 @@ If E2E results exist but have failures: **WARNING — N tests failed.** List the **LLM judge evals — check recent results:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack-dev/evals/*-llm-judge-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -5 ``` @@ -1025,7 +1027,7 @@ fi ([ -f railway.json ] || [ -f railway.toml ]) && echo "PLATFORM:railway" # Detect deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done diff --git a/land-and-deploy/SKILL.md.tmpl b/land-and-deploy/SKILL.md.tmpl index c22e99e5e..acec63c2e 100644 --- a/land-and-deploy/SKILL.md.tmpl +++ b/land-and-deploy/SKILL.md.tmpl @@ -113,7 +113,7 @@ else SAVED_HASH=$(cat ~/.gstack/projects/$SLUG/land-deploy-confirmed 2>/dev/null) CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1) # Also hash workflow files that affect deploy behavior - WORKFLOW_HASH=$(cat .github/workflows/*deploy* .github/workflows/*cd* 2>/dev/null | shasum -a 256 | cut -d' ' -f1) + WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1) COMBINED_HASH="${CURRENT_HASH}-${WORKFLOW_HASH}" if [ "$SAVED_HASH" != "$COMBINED_HASH" ] && [ -n "$SAVED_HASH" ]; then echo "CONFIG_CHANGED" @@ -223,7 +223,7 @@ grep -i "staging" CLAUDE.md 2>/dev/null | head -3 2. **GitHub Actions staging workflow:** Check for workflow files with "staging" in the name or content: ```bash -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done ``` @@ -273,7 +273,7 @@ Save the deploy config fingerprint so we can detect future changes: ```bash mkdir -p ~/.gstack/projects/$SLUG CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1) -WORKFLOW_HASH=$(cat .github/workflows/*deploy* .github/workflows/*cd* 2>/dev/null | shasum -a 256 | cut -d' ' -f1) +WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1) echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > ~/.gstack/projects/$SLUG/land-deploy-confirmed ``` Continue to Step 2. @@ -415,6 +415,7 @@ If tests fail: **BLOCKER.** Cannot merge with failing tests. **E2E tests — check recent results:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack-dev/evals/*-e2e-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -20 ``` @@ -430,6 +431,7 @@ If E2E results exist but have failures: **WARNING — N tests failed.** List the **LLM judge evals — check recent results:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack-dev/evals/*-llm-judge-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -5 ``` diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index bbee02fea..5ad69fbeb 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -368,6 +368,7 @@ eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 3. Use Grep/Glob to map the codebase areas most relevant to the user's request. 4. **List existing design docs for this project:** ```bash + setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null ``` If design docs exist, list them: "Prior designs for this project: [titles + dates]" @@ -598,6 +599,7 @@ After the user states the problem (first question in Phase 2A or 2B), search exi Extract 3-5 significant keywords from the user's problem statement and grep across design docs: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat grep -li "\|\|" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null ``` @@ -909,6 +911,7 @@ DATETIME=$(date +%Y%m%d-%H%M%S) **Design lineage:** Before writing, check for existing design docs on this branch: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat PRIOR=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) ``` If `$PRIOR` exists, the new doc gets a `Supersedes:` field referencing it. This creates a revision chain — you can trace how a design evolved across office hours sessions. diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 93abb1bb6..c6de598f2 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -48,6 +48,7 @@ Understand the project and the area the user wants to change. 3. Use Grep/Glob to map the codebase areas most relevant to the user's request. 4. **List existing design docs for this project:** ```bash + setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null ``` If design docs exist, list them: "Prior designs for this project: [titles + dates]" @@ -278,6 +279,7 @@ After the user states the problem (first question in Phase 2A or 2B), search exi Extract 3-5 significant keywords from the user's problem statement and grep across design docs: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat grep -li "\|\|" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null ``` @@ -422,6 +424,7 @@ DATETIME=$(date +%Y%m%d-%H%M%S) **Design lineage:** Before writing, check for existing design docs on this branch: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat PRIOR=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) ``` If `$PRIOR` exists, the new doc gets a `Supersedes:` field referencing it. This creates a revision chain — you can trace how a design evolved across office hours sessions. diff --git a/package.json b/package.json index 76b58e818..aa5fcfb9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "0.12.8.0", + "version": "0.12.8.1", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 675487a20..604411582 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -445,6 +445,7 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs. **Design doc check:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -455,6 +456,7 @@ If a design doc exists (from `/office-hours`), read it. Use it as the source of **Handoff note check** (reuses $SLUG and $BRANCH from the design doc check above): ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat HANDOFF=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null | head -1) [ -n "$HANDOFF" ] && echo "HANDOFF_FOUND: $HANDOFF" || echo "NO_HANDOFF" ``` @@ -509,6 +511,7 @@ If the Read fails (file not found), say: After /office-hours completes, re-run the design doc check: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -1270,6 +1273,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 +setopt +o nomatch 2>/dev/null || true # zsh compat 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 71fbefde1..404d1791a 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -105,6 +105,7 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs. **Design doc check:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -115,6 +116,7 @@ If a design doc exists (from `/office-hours`), read it. Use it as the source of **Handoff note check** (reuses $SLUG and $BRANCH from the design doc check above): ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat HANDOFF=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null | head -1) [ -n "$HANDOFF" ] && echo "HANDOFF_FOUND: $HANDOFF" || echo "NO_HANDOFF" ``` @@ -703,6 +705,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 +setopt +o nomatch 2>/dev/null || true # zsh compat {{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 41a29f2b5..e9997d842 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -371,6 +371,7 @@ When evaluating architecture, think "boring by default." When reviewing tests, t ### Design Doc Check ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -420,6 +421,7 @@ If the Read fails (file not found), say: After /office-hours completes, re-run the design doc check: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -497,6 +499,7 @@ Before analyzing coverage, detect the project's test framework: 2. **If CLAUDE.md has no testing section, auto-detect:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index b4c47e4ca..b1f05a03d 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -68,6 +68,7 @@ When evaluating architecture, think "boring by default." When reviewing tests, t ### Design Doc Check ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) diff --git a/qa-only/SKILL.md b/qa-only/SKILL.md index f7be4e490..d12d4284f 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -375,6 +375,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 + setopt +o nomatch 2>/dev/null || true # zsh compat eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` diff --git a/qa-only/SKILL.md.tmpl b/qa-only/SKILL.md.tmpl index 15d5fe4d0..0bb59c0c0 100644 --- a/qa-only/SKILL.md.tmpl +++ b/qa-only/SKILL.md.tmpl @@ -55,6 +55,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 + setopt +o nomatch 2>/dev/null || true # zsh compat {{SLUG_EVAL}} ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` diff --git a/qa/SKILL.md b/qa/SKILL.md index 30c007300..ab517052d 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -442,6 +442,7 @@ If `NEEDS_SETUP`: **Detect existing test framework and project runtime:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" @@ -604,6 +605,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 + setopt +o nomatch 2>/dev/null || true # zsh compat eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` diff --git a/qa/SKILL.md.tmpl b/qa/SKILL.md.tmpl index d228b21a5..0283ffc7c 100644 --- a/qa/SKILL.md.tmpl +++ b/qa/SKILL.md.tmpl @@ -96,6 +96,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 + setopt +o nomatch 2>/dev/null || true # zsh compat {{SLUG_EVAL}} ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` diff --git a/retro/SKILL.md b/retro/SKILL.md index 02340edbd..e048a38a2 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -629,6 +629,7 @@ Count backward from today — how many consecutive days have at least one commit Before saving the new snapshot, check for prior retro history: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t .context/retros/*.json 2>/dev/null ``` @@ -655,6 +656,7 @@ mkdir -p .context/retros Determine the next sequence number for today (substitute the actual date for `$(date +%Y-%m-%d)`): ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Count existing retros for today to get next sequence number today=$(date +%Y-%m-%d) existing=$(ls .context/retros/${today}-*.json 2>/dev/null | wc -l | tr -d ' ') @@ -778,6 +780,7 @@ Narrative covering: Check review JSONL logs for plan completion data from /ship runs this period: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" cat ~/.gstack/projects/$SLUG/*-reviews.jsonl 2>/dev/null | grep '"skill":"ship"' | grep '"plan_items_total"' || echo "NO_PLAN_DATA" ``` @@ -1079,6 +1082,7 @@ Considering the full cross-project picture. ### Global Step 8: Load history & compare ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack/retros/global-*.json 2>/dev/null | head -5 ``` @@ -1096,6 +1100,7 @@ mkdir -p ~/.gstack/retros Determine the next sequence number for today: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat today=$(date +%Y-%m-%d) existing=$(ls ~/.gstack/retros/global-${today}-*.json 2>/dev/null | wc -l | tr -d ' ') next=$((existing + 1)) diff --git a/retro/SKILL.md.tmpl b/retro/SKILL.md.tmpl index cc4f53fad..5463d07a9 100644 --- a/retro/SKILL.md.tmpl +++ b/retro/SKILL.md.tmpl @@ -307,6 +307,7 @@ Count backward from today — how many consecutive days have at least one commit Before saving the new snapshot, check for prior retro history: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t .context/retros/*.json 2>/dev/null ``` @@ -333,6 +334,7 @@ mkdir -p .context/retros Determine the next sequence number for today (substitute the actual date for `$(date +%Y-%m-%d)`): ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Count existing retros for today to get next sequence number today=$(date +%Y-%m-%d) existing=$(ls .context/retros/${today}-*.json 2>/dev/null | wc -l | tr -d ' ') @@ -456,6 +458,7 @@ Narrative covering: Check review JSONL logs for plan completion data from /ship runs this period: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" cat ~/.gstack/projects/$SLUG/*-reviews.jsonl 2>/dev/null | grep '"skill":"ship"' | grep '"plan_items_total"' || echo "NO_PLAN_DATA" ``` @@ -757,6 +760,7 @@ Considering the full cross-project picture. ### Global Step 8: Load history & compare ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack/retros/global-*.json 2>/dev/null | head -5 ``` @@ -774,6 +778,7 @@ mkdir -p ~/.gstack/retros Determine the next sequence number for today: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat today=$(date +%Y-%m-%d) existing=$(ls ~/.gstack/retros/global-${today}-*.json 2>/dev/null | wc -l | tr -d ' ') next=$((existing + 1)) diff --git a/review/SKILL.md b/review/SKILL.md index 05df971d3..b06e38e25 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -394,6 +394,7 @@ Before reviewing code quality, check: **did they build what was requested — no 2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-') REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") # Search common plan file locations @@ -650,6 +651,7 @@ Before analyzing coverage, detect the project's test framework: 2. **If CLAUDE.md has no testing section, auto-detect:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" diff --git a/scripts/resolvers/review.ts b/scripts/resolvers/review.ts index a4963b133..bf09a528b 100644 --- a/scripts/resolvers/review.ts +++ b/scripts/resolvers/review.ts @@ -233,6 +233,7 @@ If the Read fails (file not found), say: After /${first} completes, re-run the design doc check: \`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -614,6 +615,7 @@ function generatePlanFileDiscovery(): string { 2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content: \`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-') REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") # Search common plan file locations diff --git a/scripts/resolvers/testing.ts b/scripts/resolvers/testing.ts index fde799dc0..da1381c20 100644 --- a/scripts/resolvers/testing.ts +++ b/scripts/resolvers/testing.ts @@ -6,6 +6,7 @@ export function generateTestBootstrap(_ctx: TemplateContext): string { **Detect existing test framework and project runtime:** \`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" @@ -200,6 +201,7 @@ Before analyzing coverage, detect the project's test framework: 2. **If CLAUDE.md has no testing section, auto-detect:** \`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" diff --git a/scripts/resolvers/utility.ts b/scripts/resolvers/utility.ts index 6f2711755..48e9c0d82 100644 --- a/scripts/resolvers/utility.ts +++ b/scripts/resolvers/utility.ts @@ -72,7 +72,7 @@ fi ([ -f railway.json ] || [ -f railway.toml ]) && echo "PLATFORM:railway" # Detect deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \\( -name '*.yml' -o -name '*.yaml' \\) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done diff --git a/setup-deploy/SKILL.md b/setup-deploy/SKILL.md index bc8b235ce..9d5eb3a96 100644 --- a/setup-deploy/SKILL.md +++ b/setup-deploy/SKILL.md @@ -349,13 +349,13 @@ Run the platform detection from the deploy bootstrap: [ -f railway.json ] || [ -f railway.toml ] && echo "PLATFORM:railway" # GitHub Actions deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|staging|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" done # Project type [ -f package.json ] && grep -q '"bin"' package.json 2>/dev/null && echo "PROJECT_TYPE:cli" -ls *.gemspec 2>/dev/null && echo "PROJECT_TYPE:library" +find . -maxdepth 1 -name '*.gemspec' 2>/dev/null | grep -q . && echo "PROJECT_TYPE:library" ``` ### Step 3: Platform-specific setup diff --git a/setup-deploy/SKILL.md.tmpl b/setup-deploy/SKILL.md.tmpl index b4bd99efd..8326da977 100644 --- a/setup-deploy/SKILL.md.tmpl +++ b/setup-deploy/SKILL.md.tmpl @@ -64,13 +64,13 @@ Run the platform detection from the deploy bootstrap: [ -f railway.json ] || [ -f railway.toml ] && echo "PLATFORM:railway" # GitHub Actions deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|staging|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" done # Project type [ -f package.json ] && grep -q '"bin"' package.json 2>/dev/null && echo "PROJECT_TYPE:cli" -ls *.gemspec 2>/dev/null && echo "PROJECT_TYPE:library" +find . -maxdepth 1 -name '*.gemspec' 2>/dev/null | grep -q . && echo "PROJECT_TYPE:library" ``` ### Step 3: Platform-specific setup diff --git a/ship/SKILL.md b/ship/SKILL.md index 6192c50bf..f3f2ec013 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -514,6 +514,7 @@ git fetch origin && git merge origin/ --no-edit **Detect existing test framework and project runtime:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" @@ -866,6 +867,7 @@ Before analyzing coverage, detect the project's test framework: 2. **If CLAUDE.md has no testing section, auto-detect:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" @@ -1124,6 +1126,7 @@ Repo: {owner/repo} 2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-') REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") # Search common plan file locations diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index a42624584..cac45ec77 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -263,6 +263,43 @@ describe('gen-skill-docs', () => { } }); + test('bash blocks with shell globs are zsh-safe (setopt guard or find)', () => { + for (const skill of ALL_SKILLS) { + const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8'); + const bashBlocks = [...content.matchAll(/```bash\n([\s\S]*?)```/g)].map(m => m[1]); + + for (const block of bashBlocks) { + const lines = block.split('\n'); + + for (const line of lines) { + const trimmed = line.trimStart(); + if (trimmed.startsWith('#')) continue; + if (!trimmed.includes('*')) continue; + // Skip lines where * is inside find -name, git pathspecs, or $(find) + if (/\bfind\b/.test(trimmed)) continue; + if (/\bgit\b/.test(trimmed)) continue; + if (/\$\(find\b/.test(trimmed)) continue; + + // Check 1: "for VAR in " must use $(find ...) — caught above by the + // $(find check, so any surviving for-in with a glob pattern is a violation + if (/\bfor\s+\w+\s+in\b/.test(trimmed) && /\*\./.test(trimmed)) { + throw new Error( + `Unsafe for-in glob in ${skill.dir}/SKILL.md: "${trimmed}". ` + + `Use \`for f in $(find ... -name '*.ext')\` for zsh compatibility.` + ); + } + + // Check 2: ls/cat/rm/grep with glob file args must have setopt guard + const isGlobCmd = /\b(?:ls|cat|rm|grep)\b/.test(trimmed) && + /(?:\/\*[a-z.*]|\*\.[a-z])/.test(trimmed); + if (isGlobCmd) { + expect(block).toContain('setopt +o nomatch'); + } + } + } + } + }); + test('preamble-using skills have correct skill name in telemetry', () => { const PREAMBLE_SKILLS = [ { dir: '.', name: 'gstack' },