M CHANGELOG.md => CHANGELOG.md +10 -0
@@ 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.
M VERSION => VERSION +1 -1
@@ 1,1 1,1 @@
-0.12.8.0
+0.12.8.1
M autoplan/SKILL.md => autoplan/SKILL.md +1 -0
@@ 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)
M codex/SKILL.md => codex/SKILL.md +1 -0
@@ 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`
M codex/SKILL.md.tmpl => codex/SKILL.md.tmpl +1 -0
@@ 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`
M cso/SKILL.md => cso/SKILL.md +4 -3
@@ 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
```
M cso/SKILL.md.tmpl => cso/SKILL.md.tmpl +4 -3
@@ 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
```
M design-consultation/SKILL.md => design-consultation/SKILL.md +1 -0
@@ 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
M design-consultation/SKILL.md.tmpl => design-consultation/SKILL.md.tmpl +1 -0
@@ 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
M design-review/SKILL.md => design-review/SKILL.md +1 -0
@@ 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"
M land-and-deploy/SKILL.md => land-and-deploy/SKILL.md +7 -5
@@ 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
M land-and-deploy/SKILL.md.tmpl => land-and-deploy/SKILL.md.tmpl +5 -3
@@ 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
```
M office-hours/SKILL.md => office-hours/SKILL.md +3 -0
@@ 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 "<keyword1>\|<keyword2>\|<keyword3>" ~/.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.
M office-hours/SKILL.md.tmpl => office-hours/SKILL.md.tmpl +3 -0
@@ 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 "<keyword1>\|<keyword2>\|<keyword3>" ~/.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.
M package.json => package.json +1 -1
@@ 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",
M plan-ceo-review/SKILL.md => plan-ceo-review/SKILL.md +4 -0
@@ 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
```
M plan-ceo-review/SKILL.md.tmpl => plan-ceo-review/SKILL.md.tmpl +3 -0
@@ 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
```
M plan-eng-review/SKILL.md => plan-eng-review/SKILL.md +3 -0
@@ 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"
M plan-eng-review/SKILL.md.tmpl => plan-eng-review/SKILL.md.tmpl +1 -0
@@ 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)
M qa-only/SKILL.md => qa-only/SKILL.md +1 -0
@@ 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
```
M qa-only/SKILL.md.tmpl => qa-only/SKILL.md.tmpl +1 -0
@@ 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
```
M qa/SKILL.md => qa/SKILL.md +2 -0
@@ 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
```
M qa/SKILL.md.tmpl => qa/SKILL.md.tmpl +1 -0
@@ 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
```
M retro/SKILL.md => retro/SKILL.md +5 -0
@@ 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))
M retro/SKILL.md.tmpl => retro/SKILL.md.tmpl +5 -0
@@ 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))
M review/SKILL.md => review/SKILL.md +2 -0
@@ 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"
M scripts/resolvers/review.ts => scripts/resolvers/review.ts +2 -0
@@ 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
M scripts/resolvers/testing.ts => scripts/resolvers/testing.ts +2 -0
@@ 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"
M scripts/resolvers/utility.ts => scripts/resolvers/utility.ts +1 -1
@@ 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
M setup-deploy/SKILL.md => setup-deploy/SKILL.md +2 -2
@@ 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
M setup-deploy/SKILL.md.tmpl => setup-deploy/SKILL.md.tmpl +2 -2
@@ 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
M ship/SKILL.md => ship/SKILL.md +3 -0
@@ 514,6 514,7 @@ git fetch origin <base> && git merge origin/<base> --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
M test/gen-skill-docs.test.ts => test/gen-skill-docs.test.ts +37 -0
@@ 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 <glob>" 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' },