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 569c88e8..a04e1473 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 d6afffa6..a3866b38 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 5f8b5013..54a8f213 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 47128037..19a8e423 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 60247abd..23ae7f52 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 3deaca0a..07026ad6 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 b1904a8e..676c1bd9 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 52cef88a..32394b37 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 f33eabb6..2d7a5a34 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 2f64917c..55674c3b 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 655183da..becc6b1c 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 c22e99e5..acec63c2 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 bbee02fe..5ad69fbe 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 93abb1bb..c6de598f 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 76b58e81..aa5fcfb9 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 675487a2..60441158 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 71fbefde..404d1791 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 41a29f2b..e9997d84 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 b4c47e4c..b1f05a03 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 f7be4e49..d12d4284 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 15d5fe4d..0bb59c0c 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 30c00730..ab517052 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 d228b21a..0283ffc7 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 02340edb..e048a38a 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 cc4f53fa..5463d07a 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 05df971d..b06e38e2 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 a4963b13..bf09a528 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 fde799dc..da1381c2 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 6f271175..48e9c0d8 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 bc8b235c..9d5eb3a9 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 b4bd99ef..8326da97 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 6192c50b..f3f2ec01 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 a4262458..cac45ec7 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' },