Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions bin/gstack-relink
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,46 @@ SKILLS_DIR="${GSTACK_SKILLS_DIR:-$(dirname "$INSTALL_DIR")}"
# Read prefix setting
PREFIX=$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || echo "false")

render_skill_copy() {
local source_skill="$1"
local target_skill="$2"
local target_name="$3"
local tmp_file

tmp_file="$(mktemp "${target_skill}.XXXXXX")"
sed "1,/^---$/s/^name:[[:space:]]*.*/name: ${target_name}/" "$source_skill" \
| awk '
BEGIN { frontmatter = 0; inserted = 0 }
{
print
if (!inserted && $0 == "---") {
frontmatter++
if (frontmatter == 2) {
print "<!-- gstack-managed-skill-copy -->"
inserted = 1
}
}
}
END {
if (!inserted) {
print "<!-- gstack-managed-skill-copy -->"
}
}
' > "$tmp_file"
mv "$tmp_file" "$target_skill"
}

# Helper: remove old skill entry (symlink or real directory with symlinked SKILL.md)
_cleanup_skill_entry() {
local entry="$1"
if [ -L "$entry" ]; then
rm -f "$entry"
elif [ -d "$entry" ] && [ -L "$entry/SKILL.md" ]; then
rm -rf "$entry"
elif [ -d "$entry" ]; then
if [ -L "$entry/SKILL.md" ]; then
rm -rf "$entry"
elif [ -f "$entry/SKILL.md" ] && grep -q '<!-- gstack-managed-skill-copy -->' "$entry/SKILL.md" 2>/dev/null; then
rm -rf "$entry"
fi
fi
}

Expand Down Expand Up @@ -74,15 +107,12 @@ for skill_dir in "$INSTALL_DIR"/*/; do
target="$SKILLS_DIR/$link_name"
# Upgrade old directory symlinks to real directories
[ -L "$target" ] && rm -f "$target"
# Create real directory with symlinked SKILL.md (absolute path)
# Create real directory with copied, patched SKILL.md so source skills stay unchanged.
mkdir -p "$target"
ln -snf "$INSTALL_DIR/$skill/SKILL.md" "$target/SKILL.md"
render_skill_copy "$INSTALL_DIR/$skill/SKILL.md" "$target/SKILL.md" "$link_name"
SKILL_COUNT=$((SKILL_COUNT + 1))
done

# Patch SKILL.md name: fields to match prefix setting
"$INSTALL_DIR/bin/gstack-patch-names" "$INSTALL_DIR" "$PREFIX"

if [ "$PREFIX" = "true" ]; then
echo "Relinked $SKILL_COUNT skills as gstack-*"
else
Expand Down
5 changes: 4 additions & 1 deletion bin/gstack-uninstall
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,11 @@ fi
# ─── Remove SessionStart hook from Claude Code settings ─────
SETTINGS_HOOK="$(dirname "$0")/gstack-settings-hook"
SESSION_UPDATE="$(dirname "$0")/gstack-session-update"
SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-$HOME/.claude/settings.json}"
if [ -x "$SETTINGS_HOOK" ]; then
"$SETTINGS_HOOK" remove "$SESSION_UPDATE" 2>/dev/null && REMOVED+=("SessionStart hook") || true
if [ -f "$SETTINGS_FILE" ] && grep -q 'gstack-session-update' "$SETTINGS_FILE" 2>/dev/null; then
"$SETTINGS_HOOK" remove "$SESSION_UPDATE" 2>/dev/null && REMOVED+=("SessionStart hook") || true
fi
fi

# ─── Remove global state ────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions browse/scripts/build-node-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ echo "Building Node-compatible server bundle..."
bun build "$SRC_DIR/server.ts" \
--target=node \
--outfile "$DIST_DIR/server-node.mjs" \
--external "@ngrok/ngrok" \
--external playwright \
--external playwright-core \
--external diff \
Expand Down
2 changes: 1 addition & 1 deletion openclaw/skills/gstack-openclaw-ceo-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gstack-openclaw-ceo-review
description: CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when it creates a better product. Four modes: SCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials). Use when asked to review a plan, challenge this, CEO review, poke holes, think bigger, or expand scope.
description: 'CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when it creates a better product. Four modes: SCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials). Use when asked to review a plan, challenge this, CEO review, poke holes, think bigger, or expand scope.'
version: 1.0.0
metadata: { "openclaw": { "emoji": "👑" } }
---
Expand Down
2 changes: 1 addition & 1 deletion openclaw/skills/gstack-openclaw-investigate/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gstack-openclaw-investigate
description: Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron Law: no fixes without root cause. Use when asked to debug, fix a bug, investigate an error, or root cause analysis. Proactively use when user reports errors, stack traces, unexpected behavior, or says something stopped working.
description: 'Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron Law: no fixes without root cause. Use when asked to debug, fix a bug, investigate an error, or root cause analysis. Proactively use when user reports errors, stack traces, unexpected behavior, or says something stopped working.'
version: 1.0.0
metadata: { "openclaw": { "emoji": "🔍" } }
---
Expand Down
2 changes: 1 addition & 1 deletion openclaw/skills/gstack-openclaw-office-hours/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gstack-openclaw-office-hours
description: Product interrogation with six forcing questions. Two modes: startup diagnostic (demand reality, status quo, desperate specificity, narrowest wedge, observation, future-fit) and builder brainstorm. Use when asked to brainstorm, "is this worth building", "I have an idea", "office hours", or "help me think through this". Proactively use when user describes a new product idea or wants to think through design decisions before any code is written.
description: 'Product interrogation with six forcing questions. Two modes: startup diagnostic (demand reality, status quo, desperate specificity, narrowest wedge, observation, future-fit) and builder brainstorm. Use when asked to brainstorm, "is this worth building", "I have an idea", "office hours", or "help me think through this". Proactively use when user describes a new product idea or wants to think through design decisions before any code is written.'
version: 1.0.0
metadata: { "openclaw": { "emoji": "🎯" } }
---
Expand Down
5 changes: 4 additions & 1 deletion scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { ALL_HOST_CONFIGS, ALL_HOST_NAMES, resolveHostArg, getHostConfig } from
import type { HostConfig } from './host-config';

const ROOT = path.resolve(import.meta.dir, '..');
const OUTPUT_ROOT = process.env.GSTACK_OUTPUT_ROOT
? path.resolve(process.env.GSTACK_OUTPUT_ROOT)
: ROOT;
const DRY_RUN = process.argv.includes('--dry-run');

// ─── Host Detection (config-driven) ─────────────────────────
Expand Down Expand Up @@ -357,7 +360,7 @@ function processExternalHost(
const hostConfig = getHostConfig(host);

const name = externalSkillName(skillDir === '.' ? '' : skillDir, frontmatterName);
const outputDir = path.join(ROOT, hostConfig.hostSubdir, 'skills', name);
const outputDir = path.join(OUTPUT_ROOT, hostConfig.hostSubdir, 'skills', name);
fs.mkdirSync(outputDir, { recursive: true });
const outputPath = path.join(outputDir, 'SKILL.md');

Expand Down
61 changes: 45 additions & 16 deletions setup
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fi

INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
GENERATED_GSTACK_DIR="${GSTACK_OUTPUT_ROOT:-$SOURCE_GSTACK_DIR}"
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
CODEX_SKILLS="$HOME/.codex/skills"
Expand Down Expand Up @@ -227,7 +228,7 @@ fi
# bun run build already does this, but we need it when NEEDS_BUILD=0 (binary is fresh).
# Always regenerate: generation is fast (<2s) and mtime-based staleness checks are fragile
# (miss stale files when timestamps match after clone/checkout/upgrade).
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
AGENTS_DIR="$GENERATED_GSTACK_DIR/.agents/skills"
NEEDS_AGENTS_GEN=1

if [ "$NEEDS_AGENTS_GEN" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
Expand Down Expand Up @@ -298,6 +299,34 @@ link_claude_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local linked=()
render_skill_copy() {
local source_skill="$1"
local target_skill="$2"
local target_name="$3"
local tmp_file

tmp_file="$(mktemp "${target_skill}.XXXXXX")"
sed "1,/^---$/s/^name:[[:space:]]*.*/name: ${target_name}/" "$source_skill" \
| awk '
BEGIN { frontmatter = 0; inserted = 0 }
{
print
if (!inserted && $0 == "---") {
frontmatter++
if (frontmatter == 2) {
print "<!-- gstack-managed-skill-copy -->"
inserted = 1
}
}
}
END {
if (!inserted) {
print "<!-- gstack-managed-skill-copy -->"
}
}
' > "$tmp_file"
mv "$tmp_file" "$target_skill"
}
for skill_dir in "$gstack_dir"/*/; do
if [ -f "$skill_dir/SKILL.md" ]; then
dir_name="$(basename "$skill_dir")"
Expand All @@ -320,12 +349,10 @@ link_claude_skill_dirs() {
if [ -L "$target" ]; then
rm -f "$target"
fi
# Create real directory with symlinked SKILL.md (absolute path)
# Create real directory with copied, patched SKILL.md so source skills stay unchanged.
# Use mkdir -p unconditionally (idempotent) to avoid TOCTOU race
mkdir -p "$target"
# Validate target isn't a symlink before creating the link
if [ -L "$target/SKILL.md" ]; then rm "$target/SKILL.md"; fi
ln -snf "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"
render_skill_copy "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md" "$link_name"
linked+=("$link_name")
fi
done
Expand Down Expand Up @@ -366,6 +393,9 @@ cleanup_old_claude_symlinks() {
removed+=("$skill_name")
;;
esac
elif [ -d "$old_target" ] && [ -f "$old_target/SKILL.md" ] && grep -q '<!-- gstack-managed-skill-copy -->' "$old_target/SKILL.md" 2>/dev/null; then
rm -rf "$old_target"
removed+=("$skill_name")
fi
fi
done
Expand Down Expand Up @@ -407,6 +437,9 @@ cleanup_prefixed_claude_symlinks() {
removed+=("gstack-$skill_name")
;;
esac
elif [ -d "$prefixed_target" ] && [ -f "$prefixed_target/SKILL.md" ] && grep -q '<!-- gstack-managed-skill-copy -->' "$prefixed_target/SKILL.md" 2>/dev/null; then
rm -rf "$prefixed_target"
removed+=("gstack-$skill_name")
fi
fi
done
Expand All @@ -421,12 +454,12 @@ cleanup_prefixed_claude_symlinks() {
link_codex_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local agents_dir="$gstack_dir/.agents/skills"
local agents_dir="$GENERATED_GSTACK_DIR/.agents/skills"
local linked=()

if [ ! -d "$agents_dir" ]; then
echo " Generating .agents/ skill docs..."
( cd "$gstack_dir" && bun run gen:skill-docs --host codex )
( cd "$gstack_dir" && GSTACK_OUTPUT_ROOT="$GENERATED_GSTACK_DIR" bun run gen:skill-docs --host codex )
fi

if [ ! -d "$agents_dir" ]; then
Expand Down Expand Up @@ -493,7 +526,7 @@ create_agents_sidecar() {
create_codex_runtime_root() {
local gstack_dir="$1"
local codex_gstack="$2"
local agents_dir="$gstack_dir/.agents/skills"
local agents_dir="$GENERATED_GSTACK_DIR/.agents/skills"

if [ -L "$codex_gstack" ]; then
rm -f "$codex_gstack"
Expand Down Expand Up @@ -535,7 +568,7 @@ create_codex_runtime_root() {
create_factory_runtime_root() {
local gstack_dir="$1"
local factory_gstack="$2"
local factory_dir="$gstack_dir/.factory/skills"
local factory_dir="$GENERATED_GSTACK_DIR/.factory/skills"

if [ -L "$factory_gstack" ]; then
rm -f "$factory_gstack"
Expand Down Expand Up @@ -578,7 +611,7 @@ link_factory_skill_dirs() {

if [ ! -d "$factory_dir" ]; then
echo " Generating .factory/ skill docs..."
( cd "$gstack_dir" && bun run gen:skill-docs --host factory )
( cd "$gstack_dir" && GSTACK_OUTPUT_ROOT="$GENERATED_GSTACK_DIR" bun run gen:skill-docs --host factory )
fi

if [ ! -d "$factory_dir" ]; then
Expand Down Expand Up @@ -618,9 +651,6 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then
else
cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
fi
# Patch name: fields BEFORE creating symlinks so link_claude_skill_dirs
# reads the correct (patched) name: values for symlink naming
"$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX"
link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
# Self-healing: re-run gstack-relink to ensure name: fields and directory
# names are consistent with the config. This catches cases where an interrupted
Expand Down Expand Up @@ -659,7 +689,6 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then
else
cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
fi
"$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX"
link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
GSTACK_RELINK="$SOURCE_GSTACK_DIR/bin/gstack-relink"
if [ -x "$GSTACK_RELINK" ]; then
Expand Down Expand Up @@ -701,7 +730,7 @@ fi
# 6. Install for Kiro CLI (copy from .agents/skills, rewrite paths)
if [ "$INSTALL_KIRO" -eq 1 ]; then
KIRO_SKILLS="$HOME/.kiro/skills"
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
AGENTS_DIR="$GENERATED_GSTACK_DIR/.agents/skills"
mkdir -p "$KIRO_SKILLS"

# Create gstack dir with symlinks for runtime assets, copy+sed for SKILL.md
Expand Down Expand Up @@ -768,7 +797,7 @@ fi
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs.
if [ "$INSTALL_CODEX" -eq 1 ]; then
create_agents_sidecar "$SOURCE_GSTACK_DIR"
create_agents_sidecar "$GENERATED_GSTACK_DIR"
fi

# 8. Run pending version migrations
Expand Down
40 changes: 40 additions & 0 deletions test/fixtures/golden/claude-ship-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ fi
_ROUTING_DECLINED=$(~/.claude/skills/gstack/bin/gstack-config get routing_declined 2>/dev/null || echo "false")
echo "HAS_ROUTING: $_HAS_ROUTING"
echo "ROUTING_DECLINED: $_ROUTING_DECLINED"
# Vendoring deprecation: detect if CWD has a vendored gstack copy
_VENDORED="no"
if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then
if [ -f ".claude/skills/gstack/VERSION" ] || [ -d ".claude/skills/gstack/.git" ]; then
_VENDORED="yes"
fi
fi
echo "VENDORED_GSTACK: $_VENDORED"
# Detect spawned session (OpenClaw or other orchestrator)
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
```
Expand Down Expand Up @@ -214,6 +222,38 @@ Say "No problem. You can add routing rules later by running `gstack-config set r

This only happens once per project. If `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`, skip this entirely.

If `VENDORED_GSTACK` is `yes`: This project has a vendored copy of gstack at
`.claude/skills/gstack/`. Vendoring is deprecated. We will not keep vendored copies
up to date, so this project's gstack will fall behind.

Use AskUserQuestion (one-time per project, check for `~/.gstack/.vendoring-warned-$SLUG` marker):

> This project has gstack vendored in `.claude/skills/gstack/`. Vendoring is deprecated.
> We won't keep this copy up to date, so you'll fall behind on new features and fixes.
>
> Want to migrate to team mode? It takes about 30 seconds.

Options:
- A) Yes, migrate to team mode now
- B) No, I'll handle it myself

If A:
1. Run `git rm -r .claude/skills/gstack/`
2. Run `echo '.claude/skills/gstack/' >> .gitignore`
3. Run `~/.claude/skills/gstack/bin/gstack-team-init required` (or `optional`)
4. Run `git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"`
5. Tell the user: "Done. Each developer now runs: `cd ~/.claude/skills/gstack && ./setup --team`"

If B: say "OK, you're on your own to keep the vendored copy up to date."

Always run (regardless of choice):
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true
touch ~/.gstack/.vendoring-warned-${SLUG:-unknown}
```

This only happens once per project. If the marker file exists, skip entirely.

If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an
AI orchestrator (e.g., OpenClaw). In spawned sessions:
- Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option.
Expand Down
Loading