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' },