mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
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) <noreply@anthropic.com> * chore: regenerate SKILL.md files from updated templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.12.8.1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
+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
|
||||
```
|
||||
|
||||
+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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+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",
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user