mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
* fix(setup): read skill names without a process per skill (#2120) On Windows on ARM64, x64-emulated Git Bash intermittently deadlocks in MSYS2's fork() emulation, and ./setup hung in the loops that read every SKILL.md name through grep | sed | tr plus a basename (about four process starts per skill). A sourced helper, bin/gstack-skill-name.sh, reads the name with a bash read loop and no subshell; gstack-patch-names, setup's link and cleanup loops and gstack-uninstall's inventory use it and parameter expansion instead of basename. Codex links keep their directory names. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: fixtures that copy gstack-patch-names also copy its sourced helper Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: linker harnesses source the skill-name helper Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * changelog: fragment for fork-free skill names (#2120) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * changelog: name the host linkers that still run basename Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(qa-lazy-sections): the setup-install harness sources the skill-name helper Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
0c84f4ee14
commit
39e0148bdd
21 files changed
+212
-20
No files matched your search
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.92.67.0] - 2026-11-02
|
||||||
|
|
||||||
|
**`./setup` no longer starts a handful of processes per skill just to read its name (#2120).** On Windows 11 on ARM64, Git Bash runs under x64 emulation, and its `fork()` can deadlock when a script starts hundreds of short-lived processes in a row. Setup did exactly that: `gstack-patch-names` and setup's skill-linking loops ran `grep | sed | tr` and `basename` for every skill. The reporter counted about 220 process starts on a full install. Thanks to @habassa5, who traced the hang to those loops.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Skill names are read with bash builtins.** A new sourced helper, `bin/gstack-skill-name.sh`, reads the `name:` line with a `read` loop. It gives the same result as the old pipeline on CRLF files, files with no final newline, and files with no `name:` line. `gstack-patch-names`, setup's Claude skill linker, its two cleanup loops and `gstack-uninstall` use it. They use parameter expansion instead of `basename`. `sed` and `mv` run only for a skill that is actually renamed.
|
||||||
|
- **What is not fixed yet.** Setup still starts `ln`, `mkdir` and `cp` per skill, and the linkers for Cursor, OpenCode, Copilot, Factory, Agy and Kiro still run `basename` per skill. Nobody has confirmed on a Windows ARM64 machine that the hang is gone.
|
||||||
|
|
||||||
## [1.92.66.0] - 2026-11-02
|
## [1.92.66.0] - 2026-11-02
|
||||||
|
|
||||||
**The secrets check reads Python and Dart string context from what git diffed, and a quote inside an interpolation no longer ends a string (GST-738).** env.kv's source-syntax exemption treats a `password:` line as code only when it sits outside every string. Two inputs could make the scanner place a committed string's contents outside a string. One was a working copy that no longer matched what git diffed. The other was a quote inside an f-string or Dart `${…}` expression. A scoped review pass now applies an explicit `--diff-base` to its Red Team as well.
|
**The secrets check reads Python and Dart string context from what git diffed, and a quote inside an interpolation no longer ends a string (GST-738).** env.kv's source-syntax exemption treats a `password:` line as code only when it sits outside every string. Two inputs could make the scanner place a committed string's contents outside a string. One was a working copy that no longer matched what git diffed. The other was a quote inside an f-string or Dart `${…}` expression. A scoped review pass now applies an explicit `--diff-base` to its Red Team as well.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# gstack digest v1.92.66.0 — regenerate/re-copy after upgrading gstack
|
# gstack digest v1.92.67.0 — regenerate/re-copy after upgrading gstack
|
||||||
|
|
||||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||||
for agent hosts without a full skill install. The full skills add workflows,
|
for agent hosts without a full skill install. The full skills add workflows,
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ DO_PREFIX="$2"
|
|||||||
|
|
||||||
# Normalize prefix arg
|
# Normalize prefix arg
|
||||||
case "$DO_PREFIX" in true|1) DO_PREFIX=1 ;; *) DO_PREFIX=0 ;; esac
|
case "$DO_PREFIX" in true|1) DO_PREFIX=1 ;; *) DO_PREFIX=0 ;; esac
|
||||||
|
# Names are read with a bash builtin loop: no process per skill (#2120).
|
||||||
|
. "$(dirname "$0")/gstack-skill-name.sh"
|
||||||
|
|
||||||
PATCHED=0
|
PATCHED=0
|
||||||
for skill_dir in "$GSTACK_DIR"/*/; do
|
for skill_dir in "$GSTACK_DIR"/*/; do
|
||||||
[ -f "$skill_dir/SKILL.md" ] || continue
|
[ -f "$skill_dir/SKILL.md" ] || continue
|
||||||
dir_name="$(basename "$skill_dir")"
|
dir_name=${skill_dir%/}; dir_name=${dir_name##*/}
|
||||||
[ "$dir_name" = "node_modules" ] && continue
|
[ "$dir_name" = "node_modules" ] && continue
|
||||||
cur=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]' || true)
|
gstack_skill_md_name "$skill_dir/SKILL.md"; cur=$GSTACK_SKILL_NAME
|
||||||
[ -z "$cur" ] && continue
|
[ -z "$cur" ] && continue
|
||||||
[ "$cur" = "gstack" ] && continue # never prefix root skill
|
[ "$cur" = "gstack" ] && continue # never prefix root skill
|
||||||
if [ "$DO_PREFIX" -eq 1 ]; then
|
if [ "$DO_PREFIX" -eq 1 ]; then
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# shellcheck shell=bash
|
||||||
|
# gstack-skill-name.sh: sourced helper (bash 3.2). gstack_skill_md_name FILE
|
||||||
|
# sets GSTACK_SKILL_NAME to the first column-0 "name:" line of FILE with every
|
||||||
|
# whitespace character removed (CR included), or to "" when there is none or
|
||||||
|
# FILE cannot be read. Always returns 0.
|
||||||
|
#
|
||||||
|
# It starts no process: no subshell, pipe or command substitution, and callers
|
||||||
|
# must not wrap it in $( ). Setup reads ~55 names per run, and the
|
||||||
|
# grep | sed | tr pipeline it replaces deadlocked MSYS2's fork() under x64
|
||||||
|
# emulation on Windows ARM64 (#2120).
|
||||||
|
gstack_skill_md_name() {
|
||||||
|
GSTACK_SKILL_NAME=
|
||||||
|
[ -f "$1" ] && [ -r "$1" ] || return 0
|
||||||
|
local _gsn_line
|
||||||
|
while IFS= read -r _gsn_line || [ -n "$_gsn_line" ]; do
|
||||||
|
case "$_gsn_line" in
|
||||||
|
name:*) _gsn_line=${_gsn_line#name:}; GSTACK_SKILL_NAME=${_gsn_line//[[:space:]]/}; return 0 ;;
|
||||||
|
esac
|
||||||
|
done < "$1"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -60,6 +60,9 @@ GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|||||||
STATE_DIR="$HOME/.gstack"
|
STATE_DIR="$HOME/.gstack"
|
||||||
_STATE_DOC="https://github.com/garrytan/gstack/blob/main/docs/state-root.md"
|
_STATE_DOC="https://github.com/garrytan/gstack/blob/main/docs/state-root.md"
|
||||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade ($_STATE_DOC)" >&2; exit 1; }
|
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade ($_STATE_DOC)" >&2; exit 1; }
|
||||||
|
# Skill names without a process per skill (#2120); an older copy lacking the helper keeps the pipeline.
|
||||||
|
. "$(dirname "$0")/gstack-skill-name.sh" 2>/dev/null \
|
||||||
|
|| gstack_skill_md_name() { GSTACK_SKILL_NAME=$(grep -m1 '^name:' "$1" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]' || true); }
|
||||||
gstack_state_root_select
|
gstack_state_root_select
|
||||||
RESOLVED_ROOT="$_gstack_sr_root"
|
RESOLVED_ROOT="$_gstack_sr_root"
|
||||||
RESOLVED_VAR="$_gstack_sr_var"
|
RESOLVED_VAR="$_gstack_sr_var"
|
||||||
@@ -290,8 +293,8 @@ _INVENTORY=" _gstack-command connect-chrome gstack-connect-chrome "
|
|||||||
if [ -d "$GSTACK_DIR" ]; then
|
if [ -d "$GSTACK_DIR" ]; then
|
||||||
for _SRC in "$GSTACK_DIR"/*/; do
|
for _SRC in "$GSTACK_DIR"/*/; do
|
||||||
[ -f "$_SRC/SKILL.md" ] || continue
|
[ -f "$_SRC/SKILL.md" ] || continue
|
||||||
_SRC_NAME="$(basename "$_SRC")"
|
_SRC_NAME=${_SRC%/}; _SRC_NAME=${_SRC_NAME##*/}
|
||||||
_FM_NAME=$(grep -m1 '^name:' "$_SRC/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]' || true)
|
gstack_skill_md_name "$_SRC/SKILL.md"; _FM_NAME=$GSTACK_SKILL_NAME
|
||||||
for _N in "$_SRC_NAME" "$_FM_NAME"; do
|
for _N in "$_SRC_NAME" "$_FM_NAME"; do
|
||||||
[ -n "$_N" ] || continue
|
[ -n "$_N" ] || continue
|
||||||
case "$_INVENTORY" in *" $_N "*) ;; *) _INVENTORY="$_INVENTORY$_N gstack-$_N " ;; esac
|
case "$_INVENTORY" in *" $_N "*) ;; *) _INVENTORY="$_INVENTORY$_N gstack-$_N " ;; esac
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gstack",
|
"name": "gstack",
|
||||||
"version": "1.92.66",
|
"version": "1.92.67",
|
||||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|||||||
. "$SOURCE_GSTACK_DIR/bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $SOURCE_GSTACK_DIR/bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
. "$SOURCE_GSTACK_DIR/bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $SOURCE_GSTACK_DIR/bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||||
gstack_state_root_select; GSTACK_STATE_ROOT="$_gstack_sr_root"
|
gstack_state_root_select; GSTACK_STATE_ROOT="$_gstack_sr_root"
|
||||||
. "$SOURCE_GSTACK_DIR/bin/gstack-path-spelling.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-path-spelling.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
. "$SOURCE_GSTACK_DIR/bin/gstack-path-spelling.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-path-spelling.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
||||||
|
. "$SOURCE_GSTACK_DIR/bin/gstack-skill-name.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-skill-name.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
||||||
. "$SOURCE_GSTACK_DIR/bin/gstack-claude-root.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-claude-root.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
. "$SOURCE_GSTACK_DIR/bin/gstack-claude-root.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-claude-root.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
||||||
. "$SOURCE_GSTACK_DIR/bin/gstack-install-registry.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-install-registry.sh is missing. fix: reinstall from a fresh clone (docs/ADDING_A_HOST.md)" >&2; exit 1; }
|
. "$SOURCE_GSTACK_DIR/bin/gstack-install-registry.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-install-registry.sh is missing. fix: reinstall from a fresh clone (docs/ADDING_A_HOST.md)" >&2; exit 1; }
|
||||||
. "$SOURCE_GSTACK_DIR/bin/gstack-render-claude.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-render-claude.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
. "$SOURCE_GSTACK_DIR/bin/gstack-render-claude.sh" || { echo "$0: $SOURCE_GSTACK_DIR/bin/gstack-render-claude.sh is missing. fix: reinstall from a fresh clone" >&2; exit 1; }
|
||||||
@@ -2578,11 +2579,11 @@ link_claude_skill_dirs() {
|
|||||||
local linked=()
|
local linked=()
|
||||||
for skill_dir in "$gstack_dir"/*/; do
|
for skill_dir in "$gstack_dir"/*/; do
|
||||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||||
dir_name="$(basename "$skill_dir")"
|
dir_name=${skill_dir%/}; dir_name=${dir_name##*/}
|
||||||
# Skip node_modules
|
# Skip node_modules
|
||||||
[ "$dir_name" = "node_modules" ] && continue
|
[ "$dir_name" = "node_modules" ] && continue
|
||||||
# Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test")
|
# Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test")
|
||||||
skill_name=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]')
|
gstack_skill_md_name "$skill_dir/SKILL.md"; skill_name=$GSTACK_SKILL_NAME
|
||||||
[ -z "$skill_name" ] && skill_name="$dir_name"
|
[ -z "$skill_name" ] && skill_name="$dir_name"
|
||||||
case "${_DISABLED_SKILLS:- }" in *" $skill_name "*|*" $dir_name "*) continue ;; esac
|
case "${_DISABLED_SKILLS:- }" in *" $skill_name "*|*" $dir_name "*) continue ;; esac
|
||||||
# Apply gstack- prefix unless --no-prefix or already prefixed
|
# Apply gstack- prefix unless --no-prefix or already prefixed
|
||||||
@@ -2748,7 +2749,7 @@ cleanup_old_claude_symlinks() {
|
|||||||
# literal (empty skills_dir) is rejected by the same guard.
|
# literal (empty skills_dir) is rejected by the same guard.
|
||||||
for old_target in "$skills_dir"/*; do
|
for old_target in "$skills_dir"/*; do
|
||||||
[ -e "$old_target" ] || [ -L "$old_target" ] || continue
|
[ -e "$old_target" ] || [ -L "$old_target" ] || continue
|
||||||
skill_name="$(basename "$old_target")"
|
skill_name=${old_target##*/}
|
||||||
[ "$skill_name" = "node_modules" ] && continue
|
[ "$skill_name" = "node_modules" ] && continue
|
||||||
[ "$skill_name" = "gstack" ] && continue
|
[ "$skill_name" = "gstack" ] && continue
|
||||||
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
||||||
@@ -2788,7 +2789,7 @@ cleanup_old_claude_symlinks() {
|
|||||||
if [ "${IS_WINDOWS:-0}" -eq 1 ] && [ -d "$gstack_dir" ]; then
|
if [ "${IS_WINDOWS:-0}" -eq 1 ] && [ -d "$gstack_dir" ]; then
|
||||||
for skill_dir in "$gstack_dir"/*/; do
|
for skill_dir in "$gstack_dir"/*/; do
|
||||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||||
skill_name="$(basename "$skill_dir")"
|
skill_name=${skill_dir%/}; skill_name=${skill_name##*/}
|
||||||
[ "$skill_name" = "node_modules" ] && continue
|
[ "$skill_name" = "node_modules" ] && continue
|
||||||
case "$skill_name" in gstack-*) continue ;; esac
|
case "$skill_name" in gstack-*) continue ;; esac
|
||||||
old_target="$skills_dir/$skill_name"
|
old_target="$skills_dir/$skill_name"
|
||||||
@@ -2819,7 +2820,7 @@ cleanup_prefixed_claude_symlinks() {
|
|||||||
local removed=()
|
local removed=()
|
||||||
for skill_dir in "$gstack_dir"/*/; do
|
for skill_dir in "$gstack_dir"/*/; do
|
||||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||||
skill_name="$(basename "$skill_dir")"
|
skill_name=${skill_dir%/}; skill_name=${skill_name##*/}
|
||||||
[ "$skill_name" = "node_modules" ] && continue
|
[ "$skill_name" = "node_modules" ] && continue
|
||||||
# Only clean up prefixed entries for dirs that AREN'T already prefixed
|
# Only clean up prefixed entries for dirs that AREN'T already prefixed
|
||||||
# (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
|
# (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
|
||||||
@@ -2884,7 +2885,7 @@ link_codex_skill_dirs() {
|
|||||||
|
|
||||||
for skill_dir in "$agents_dir"/gstack*/; do
|
for skill_dir in "$agents_dir"/gstack*/; do
|
||||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||||
skill_name="$(basename "$skill_dir")"
|
skill_name=${skill_dir%/}; skill_name=${skill_name##*/}
|
||||||
case "${_DISABLED_SKILLS:- }" in *" ${skill_name#gstack-} "*) _remove_disabled_host_entry "$skills_dir/$skill_name"; continue ;; esac
|
case "${_DISABLED_SKILLS:- }" in *" ${skill_name#gstack-} "*) _remove_disabled_host_entry "$skills_dir/$skill_name"; continue ;; esac
|
||||||
# Skip the sidecar directory — it contains runtime asset symlinks (bin/,
|
# Skip the sidecar directory — it contains runtime asset symlinks (bin/,
|
||||||
# browse/), not a skill. Linking it would overwrite the root gstack
|
# browse/), not a skill. Linking it would overwrite the root gstack
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe('every installed-skill render carries the plan-mode arg', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('gstack-relink router re-render', () => {
|
describe('gstack-relink router re-render', () => {
|
||||||
const BINS = ['gstack-config', 'gstack-relink', 'gstack-patch-names', 'gstack-state-root.sh', 'gstack-install-registry.sh'];
|
const BINS = ['gstack-config', 'gstack-relink', 'gstack-patch-names', 'gstack-skill-name.sh', 'gstack-state-root.sh', 'gstack-install-registry.sh'];
|
||||||
const SAFE = '## Plan Mode Safe Operations';
|
const SAFE = '## Plan Mode Safe Operations';
|
||||||
|
|
||||||
function relinkFixture() {
|
function relinkFixture() {
|
||||||
|
|||||||
@@ -570,7 +570,8 @@ describe('installed QA pointers', () => {
|
|||||||
'set -e', `IS_WINDOWS=${copy ? 1 : 0}`, `SKILL_PREFIX=${prefix}`, 'QUIET=1',
|
'set -e', `IS_WINDOWS=${copy ? 1 : 0}`, `SKILL_PREFIX=${prefix}`, 'QUIET=1',
|
||||||
'_FOREIGN_SKIPPED_ENTRIES=()', '_BACKED_UP_SKILL_MDS=()', '_SKILL_BACKUP_ROOT="$HOME/backups"',
|
'_FOREIGN_SKIPPED_ENTRIES=()', '_BACKED_UP_SKILL_MDS=()', '_SKILL_BACKUP_ROOT="$HOME/backups"',
|
||||||
'GSTACK_USER_RENDER_DIR="$HOME/absent-render"',
|
'GSTACK_USER_RENDER_DIR="$HOME/absent-render"',
|
||||||
'GSTACK_STATE_ROOT="$HOME/.gstack"', '_SKILL_COPIES_FILE="$GSTACK_STATE_ROOT/skill-copies.tsv"', helpers,
|
'GSTACK_STATE_ROOT="$HOME/.gstack"', '_SKILL_COPIES_FILE="$GSTACK_STATE_ROOT/skill-copies.tsv"',
|
||||||
|
`. "${path.join(ROOT, 'bin/gstack-skill-name.sh')}"`, helpers,
|
||||||
'log() { :; }', '_browser_hint() { :; }', 'bun_cmd() { :; }', install,
|
'log() { :; }', '_browser_hint() { :; }', 'bun_cmd() { :; }', install,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
const runInstall = () => runBashScript(script, {
|
const runInstall = () => runBashScript(script, {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import * as os from 'os';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
const ROOT = path.resolve(import.meta.dir, '..');
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
const BINS = ['gstack-config', 'gstack-relink', 'gstack-patch-names', 'gstack-state-root.sh', 'gstack-install-registry.sh'];
|
const BINS = ['gstack-config', 'gstack-relink', 'gstack-patch-names', 'gstack-skill-name.sh', 'gstack-state-root.sh', 'gstack-install-registry.sh'];
|
||||||
|
|
||||||
let tmp: string;
|
let tmp: string;
|
||||||
let install: string;
|
let install: string;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function setupMockInstall(skills: string[]): void {
|
|||||||
fs.chmodSync(path.join(mockBin, 'gstack-config'), 0o755);
|
fs.chmodSync(path.join(mockBin, 'gstack-config'), 0o755);
|
||||||
// A real install always ships the state-root twin beside the bins that source it.
|
// A real install always ships the state-root twin beside the bins that source it.
|
||||||
fs.copyFileSync(path.join(BIN, 'gstack-state-root.sh'), path.join(mockBin, 'gstack-state-root.sh'));
|
fs.copyFileSync(path.join(BIN, 'gstack-state-root.sh'), path.join(mockBin, 'gstack-state-root.sh'));
|
||||||
|
fs.copyFileSync(path.join(BIN, 'gstack-skill-name.sh'), path.join(mockBin, 'gstack-skill-name.sh'));
|
||||||
if (fs.existsSync(path.join(BIN, 'gstack-relink'))) {
|
if (fs.existsSync(path.join(BIN, 'gstack-relink'))) {
|
||||||
fs.copyFileSync(path.join(BIN, 'gstack-relink'), path.join(mockBin, 'gstack-relink'));
|
fs.copyFileSync(path.join(BIN, 'gstack-relink'), path.join(mockBin, 'gstack-relink'));
|
||||||
fs.chmodSync(path.join(mockBin, 'gstack-relink'), 0o755);
|
fs.chmodSync(path.join(mockBin, 'gstack-relink'), 0o755);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ describe('installed skills serve rendered sections (F5, #2706)', () => {
|
|||||||
test('gstack-relink (the gbrain-refresh path) serves the render, and the checkout again once the render is gone', () => {
|
test('gstack-relink (the gbrain-refresh path) serves the render, and the checkout again once the render is gone', () => {
|
||||||
const fx = fixture();
|
const fx = fixture();
|
||||||
fs.mkdirSync(path.join(fx.install, 'bin'), { recursive: true });
|
fs.mkdirSync(path.join(fx.install, 'bin'), { recursive: true });
|
||||||
for (const f of ['gstack-relink', 'gstack-config', 'gstack-state-root.sh', 'gstack-install-registry.sh', 'gstack-patch-names']) {
|
for (const f of ['gstack-relink', 'gstack-config', 'gstack-state-root.sh', 'gstack-install-registry.sh', 'gstack-patch-names', 'gstack-skill-name.sh']) {
|
||||||
fs.copyFileSync(path.join(BIN, f), path.join(fx.install, 'bin', f));
|
fs.copyFileSync(path.join(BIN, f), path.join(fx.install, 'bin', f));
|
||||||
fs.chmodSync(path.join(fx.install, 'bin', f), 0o755);
|
fs.chmodSync(path.join(fx.install, 'bin', f), 0o755);
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ describe('installed skills serve rendered sections (F5, #2706)', () => {
|
|||||||
test('a sections/ directory the user owns is left alone by relink', () => {
|
test('a sections/ directory the user owns is left alone by relink', () => {
|
||||||
const fx = fixture();
|
const fx = fixture();
|
||||||
fs.mkdirSync(path.join(fx.install, 'bin'), { recursive: true });
|
fs.mkdirSync(path.join(fx.install, 'bin'), { recursive: true });
|
||||||
for (const f of ['gstack-relink', 'gstack-config', 'gstack-state-root.sh', 'gstack-install-registry.sh', 'gstack-patch-names']) {
|
for (const f of ['gstack-relink', 'gstack-config', 'gstack-state-root.sh', 'gstack-install-registry.sh', 'gstack-patch-names', 'gstack-skill-name.sh']) {
|
||||||
fs.copyFileSync(path.join(BIN, f), path.join(fx.install, 'bin', f));
|
fs.copyFileSync(path.join(BIN, f), path.join(fx.install, 'bin', f));
|
||||||
fs.chmodSync(path.join(fx.install, 'bin', f), 0o755);
|
fs.chmodSync(path.join(fx.install, 'bin', f), 0o755);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ beforeAll(() => {
|
|||||||
extractFn('_write_owned_marker'),
|
extractFn('_write_owned_marker'),
|
||||||
extractFn('_print_windows_copy_note_once'),
|
extractFn('_print_windows_copy_note_once'),
|
||||||
extractFn('_link_skill_runtime_assets'),
|
extractFn('_link_skill_runtime_assets'),
|
||||||
|
`. "${path.join(ROOT, 'bin/gstack-skill-name.sh')}"`,
|
||||||
extractFn('link_claude_skill_dirs'),
|
extractFn('link_claude_skill_dirs'),
|
||||||
extractFn('_install_alias_skill_md'),
|
extractFn('_install_alias_skill_md'),
|
||||||
extractFn('link_claude_root_skill_alias'),
|
extractFn('link_claude_root_skill_alias'),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function fixture(copy: boolean) {
|
|||||||
put(path.join(root, 'VERSION'), '2.0.0.0\n');
|
put(path.join(root, 'VERSION'), '2.0.0.0\n');
|
||||||
put(path.join(root, 'ETHOS.md'), 'Fixture ethos\n');
|
put(path.join(root, 'ETHOS.md'), 'Fixture ethos\n');
|
||||||
put(path.join(root, 'package.json'), JSON.stringify({ type: 'module', scripts: { 'gen:skill-docs': 'bun run scripts/gen-skill-docs.ts' } }));
|
put(path.join(root, 'package.json'), JSON.stringify({ type: 'module', scripts: { 'gen:skill-docs': 'bun run scripts/gen-skill-docs.ts' } }));
|
||||||
for (const bin of ['gstack-config', 'gstack-patch-names', 'gstack-relink', 'gstack-migrate-claude-code', 'gstack-claude-code', 'gstack-models', 'gstack-state-root.sh', 'gstack-bun-version.sh', 'gstack-path-spelling.sh', 'gstack-claude-root.sh', 'gstack-install-registry.sh', 'gstack-render-claude.sh', 'gstack-host-renders.sh']) {
|
for (const bin of ['gstack-config', 'gstack-patch-names', 'gstack-relink', 'gstack-migrate-claude-code', 'gstack-claude-code', 'gstack-models', 'gstack-state-root.sh', 'gstack-bun-version.sh', 'gstack-path-spelling.sh', 'gstack-skill-name.sh', 'gstack-claude-root.sh', 'gstack-install-registry.sh', 'gstack-render-claude.sh', 'gstack-host-renders.sh']) {
|
||||||
const target = path.join(root, 'bin', bin);
|
const target = path.join(root, 'bin', bin);
|
||||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||||
fs.copyFileSync(path.join(ROOT, 'bin', bin), target);
|
fs.copyFileSync(path.join(ROOT, 'bin', bin), target);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ beforeAll(() => {
|
|||||||
'_FOREIGN_SKIPPED_ENTRIES=()',
|
'_FOREIGN_SKIPPED_ENTRIES=()',
|
||||||
'_BACKED_UP_SKILL_MDS=()',
|
'_BACKED_UP_SKILL_MDS=()',
|
||||||
`_SKILL_BACKUP_ROOT="${os.tmpdir()}/gstack-harness-backups-${process.pid}"`,
|
`_SKILL_BACKUP_ROOT="${os.tmpdir()}/gstack-harness-backups-${process.pid}"`,
|
||||||
|
`. "${path.join(ROOT, 'bin/gstack-skill-name.sh')}"`,
|
||||||
extractFn('link_claude_skill_dirs'),
|
extractFn('link_claude_skill_dirs'),
|
||||||
`link_claude_skill_dirs "${ROOT}" "${installDir}"`,
|
`link_claude_skill_dirs "${ROOT}" "${installDir}"`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* #2120: on Windows on ARM64, x64-emulated Git Bash intermittently deadlocks
|
||||||
|
* in MSYS2's fork() emulation, and ./setup hung in the per-skill loops that
|
||||||
|
* read each SKILL.md name with a grep | sed | tr pipeline plus a basename
|
||||||
|
* (about four process starts per skill, ~55 skills a run). The deadlock
|
||||||
|
* itself needs that host; these cases pin the cause they can see anywhere:
|
||||||
|
* the loops start none of those processes per skill. Every grep, sed, tr and
|
||||||
|
* basename the code under test starts goes through a logging shim on PATH.
|
||||||
|
*/
|
||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { runBashScript } from './helpers/bash-script';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
|
||||||
|
const HELPER = path.join(ROOT, 'bin/gstack-skill-name.sh');
|
||||||
|
const SHIMMED = ['grep', 'sed', 'tr', 'basename'];
|
||||||
|
const SKILLS = ['qa', 'review', 'ship', 'office-hours', 'investigate', 'land-and-deploy', 'retro', 'cso'];
|
||||||
|
|
||||||
|
function extractFn(name: string): string {
|
||||||
|
const start = SETUP_SRC.indexOf(`\n${name}() {`);
|
||||||
|
const end = SETUP_SRC.indexOf('\n}\n', start);
|
||||||
|
if (start < 0 || end < 0) throw new Error(`function not found: ${name}`);
|
||||||
|
return SETUP_SRC.slice(start + 1, end + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A temp tree, a PATH whose grep/sed/tr/basename log each start to calls.log, and a reader for that log. */
|
||||||
|
function sandbox() {
|
||||||
|
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-fork-free-')));
|
||||||
|
const shim = path.join(tmp, 'shim');
|
||||||
|
const log = path.join(tmp, 'calls.log');
|
||||||
|
fs.mkdirSync(shim);
|
||||||
|
for (const name of SHIMMED) {
|
||||||
|
const real = spawnSync('bash', ['-c', `type -P ${name}`], { encoding: 'utf8', timeout: 10_000 }).stdout.trim();
|
||||||
|
fs.writeFileSync(path.join(shim, name), `#!/bin/sh\necho ${name} >> "${log}"\nexec "${real}" "$@"\n`, { mode: 0o755 });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tmp, env: { PATH: `${shim}${path.delimiter}${process.env.PATH ?? ''}`, HOME: tmp, GSTACK_USER_RENDER_DIR: path.join(tmp, 'no-render') },
|
||||||
|
calls: () => fs.existsSync(log) ? fs.readFileSync(log, 'utf8').split('\n').filter(Boolean) : [],
|
||||||
|
reset: () => fs.rmSync(log, { force: true }),
|
||||||
|
cleanup: () => fs.rmSync(tmp, { recursive: true, force: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillMd = (name: string, eol = '\n') => ['---', `name: ${name}`, 'description: fixture', '---', `# ${name}`, ''].join(eol);
|
||||||
|
|
||||||
|
/** A payload of SKILLS (qa in CRLF, office-hours named "oh"), then "zz-nameless" with no name: line after them. */
|
||||||
|
function payload(dir: string) {
|
||||||
|
for (const name of SKILLS) {
|
||||||
|
fs.mkdirSync(path.join(dir, name), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, name, 'SKILL.md'), skillMd(name === 'office-hours' ? 'oh' : name, name === 'qa' ? '\r\n' : '\n'));
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.join(dir, 'zz-nameless'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, 'zz-nameless', 'SKILL.md'), '---\ndescription: no name\n---\n# nameless\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.skipIf(process.platform === 'win32')('setup name reads start no process per skill (#2120)', () => {
|
||||||
|
// Value: protects=the helper returns what the old grep -m1 | sed | tr pipeline returned, and never a stale name; fails_when=it keeps the
|
||||||
|
// previous value, reads a non-column-0 name, keeps a CR or space, or returns non-zero; why_new=the helper is new; seam=none
|
||||||
|
test('gstack_skill_md_name matches the old pipeline on odd inputs, resets between files and always returns 0', () => {
|
||||||
|
const s = sandbox();
|
||||||
|
try {
|
||||||
|
const cases: Record<string, string> = {
|
||||||
|
plain: '---\nname: qa\n---\n', crlf: '---\r\nname: qa\r\n---\r\n', spaced: 'name: my skill \t\n', noeol: 'name: last',
|
||||||
|
indented: ' name: no\nname: yes\n', nameless: '---\ndescription: x\n---\n', empty: '', later: '# t\nbody\nname: deep\nname: second\n',
|
||||||
|
};
|
||||||
|
const files = Object.entries(cases).map(([k, v]) => { const f = path.join(s.tmp, k); fs.writeFileSync(f, v); return f; });
|
||||||
|
files.push(path.join(s.tmp, 'missing'));
|
||||||
|
const old = (f: string) => spawnSync('bash', ['-c', "grep -m1 '^name:' \"$1\" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]'", '_', f], { encoding: 'utf8', timeout: 10_000 }).stdout;
|
||||||
|
const script = [`set -euo pipefail; . "${HELPER}"; GSTACK_SKILL_NAME=stale`,
|
||||||
|
...files.map(f => `gstack_skill_md_name "${f}"; printf '%s\n' "[$GSTACK_SKILL_NAME]"`)].join('\n');
|
||||||
|
const r = runBashScript(script, { timeout: 10_000, env: s.env });
|
||||||
|
expect(r.status, r.stderr ?? '').toBe(0);
|
||||||
|
expect(r.stdout.trim().split('\n')).toEqual(files.map(f => `[${old(f)}]`));
|
||||||
|
expect(s.calls()).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
s.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Value: protects=no caller re-adds a fork per skill by wrapping the helper in $( ), and the helper itself stays fork-free;
|
||||||
|
// fails_when=a call site uses $(gstack_skill_md_name ...) or the helper body gains $(, a pipe or a backquote; why_new=the shims cannot see a subshell fork; seam=none
|
||||||
|
test('the helper starts no subshell and no caller wraps it in $( )', () => {
|
||||||
|
const body = fs.readFileSync(HELPER, 'utf8').split('\n').filter(l => !/^\s*#/.test(l)).join('\n');
|
||||||
|
expect(body).not.toMatch(/\$\(|`|(^|[^|])\|([^|]|$)/m);
|
||||||
|
for (const rel of ['setup', 'bin/gstack-patch-names', 'bin/gstack-uninstall']) {
|
||||||
|
expect(fs.readFileSync(path.join(ROOT, rel), 'utf8'), rel).not.toMatch(/\$\(\s*gstack_skill_md_name/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Value: protects=gstack-patch-names reads every name without a per-skill grep|sed|tr|basename (the first loop #2120 hung in);
|
||||||
|
// fails_when=the loop goes back to a process pipeline per SKILL.md; why_new=no case counted process starts; seam=PATH shims
|
||||||
|
test('gstack-patch-names: no process per skill when nothing needs a rename; a rename starts only its own sed', () => {
|
||||||
|
const s = sandbox();
|
||||||
|
try {
|
||||||
|
const dir = path.join(s.tmp, 'gstack');
|
||||||
|
payload(dir);
|
||||||
|
const off = spawnSync('bash', [path.join(ROOT, 'bin/gstack-patch-names'), dir, '0'], { encoding: 'utf8', env: s.env, timeout: 30_000 });
|
||||||
|
expect(off.status, off.stderr).toBe(0);
|
||||||
|
expect(off.stdout).toBe('');
|
||||||
|
expect(s.calls()).toEqual([]);
|
||||||
|
|
||||||
|
const on = spawnSync('bash', [path.join(ROOT, 'bin/gstack-patch-names'), dir, '1'], { encoding: 'utf8', env: s.env, timeout: 30_000 });
|
||||||
|
expect(on.status, on.stderr).toBe(0);
|
||||||
|
expect(on.stdout).toContain(`patched name: field in ${SKILLS.length} skills`);
|
||||||
|
for (const name of SKILLS) expect(fs.readFileSync(path.join(dir, name, 'SKILL.md'), 'utf8')).toContain(`name: gstack-${name === 'office-hours' ? 'oh' : name}`);
|
||||||
|
expect(fs.readFileSync(path.join(dir, 'zz-nameless', 'SKILL.md'), 'utf8')).not.toContain('name:');
|
||||||
|
expect(s.calls()).toEqual(Array(SKILLS.length).fill('sed'));
|
||||||
|
} finally {
|
||||||
|
s.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Value: protects=link_claude_skill_dirs and both cleanup loops run with no per-skill grep, sed, tr or basename, and a nameless SKILL.md links
|
||||||
|
// under its directory name, never the previous skill's name (#2120); fails_when=a loop goes back to a pipeline or basename, or the name leaks;
|
||||||
|
// why_new=link tests checked links, not process starts; seam=PATH shims
|
||||||
|
test('link_claude_skill_dirs and the two cleanup loops: no per-skill grep, sed, tr or basename; names come from frontmatter', () => {
|
||||||
|
const s = sandbox();
|
||||||
|
try {
|
||||||
|
const pl = path.join(s.tmp, 'skills', 'gstack');
|
||||||
|
payload(pl);
|
||||||
|
const script = [
|
||||||
|
'set -e', 'IS_WINDOWS=0', 'SKILL_PREFIX=0', '_FOREIGN_SKIPPED_ENTRIES=()', '_BACKED_UP_SKILL_MDS=()',
|
||||||
|
'_link_skill_runtime_assets() { :; }', '_print_windows_copy_note_once() { :; }',
|
||||||
|
...['_link_or_copy', '_gstack_link_target_abs', '_gstack_target_is_ours', '_claude_entry_is_ours', '_write_owned_marker',
|
||||||
|
'_gstack_generated_header', '_claude_entry_owned_strongly', '_backup_skill_md', '_cleanup_weak_dir', '_gstack_dir_only_links',
|
||||||
|
'_cleanup_linked_dir'].map(extractFn),
|
||||||
|
`. "${HELPER}"`,
|
||||||
|
...['link_claude_skill_dirs', 'cleanup_old_claude_symlinks', 'cleanup_prefixed_claude_symlinks'].map(extractFn),
|
||||||
|
`cleanup_old_claude_symlinks "${pl}" "${path.dirname(pl)}"`,
|
||||||
|
`cleanup_prefixed_claude_symlinks "${pl}" "${path.dirname(pl)}"`,
|
||||||
|
`link_claude_skill_dirs "${pl}" "${path.dirname(pl)}"`,
|
||||||
|
].join('\n');
|
||||||
|
const r = runBashScript(script, { timeout: 30_000, env: s.env });
|
||||||
|
expect(r.stderr ?? '').not.toContain('command not found');
|
||||||
|
expect(r.status, r.stderr ?? '').toBe(0);
|
||||||
|
expect(r.stdout).toContain('linked skills:');
|
||||||
|
for (const name of [...SKILLS.map(n => n === 'office-hours' ? 'oh' : n), 'zz-nameless']) {
|
||||||
|
expect(fs.existsSync(path.join(s.tmp, 'skills', name, 'SKILL.md')), name).toBe(true);
|
||||||
|
}
|
||||||
|
expect(fs.existsSync(path.join(s.tmp, 'skills', 'office-hours'))).toBe(false);
|
||||||
|
expect(s.calls()).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
s.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,6 +28,7 @@ const HELPERS = [
|
|||||||
'_link_skill_runtime_assets() { :; }',
|
'_link_skill_runtime_assets() { :; }',
|
||||||
'_print_windows_copy_note_once() { :; }',
|
'_print_windows_copy_note_once() { :; }',
|
||||||
extractFn('_link_or_copy'),
|
extractFn('_link_or_copy'),
|
||||||
|
`. "${path.join(ROOT, 'bin/gstack-skill-name.sh')}"`,
|
||||||
extractFn('_gstack_link_target_abs'),
|
extractFn('_gstack_link_target_abs'),
|
||||||
extractFn('_gstack_target_is_ours'),
|
extractFn('_gstack_target_is_ours'),
|
||||||
extractFn('_claude_entry_is_ours'),
|
extractFn('_claude_entry_is_ours'),
|
||||||
|
|||||||
@@ -653,6 +653,7 @@ function runLinker(opts: {
|
|||||||
'_print_windows_copy_note_once() { :; }',
|
'_print_windows_copy_note_once() { :; }',
|
||||||
'_FOREIGN_SKIPPED_ENTRIES=()',
|
'_FOREIGN_SKIPPED_ENTRIES=()',
|
||||||
extractFn('_link_or_copy'),
|
extractFn('_link_or_copy'),
|
||||||
|
`. "${path.join(ROOT, 'bin/gstack-skill-name.sh')}"`,
|
||||||
extractFn('_gstack_link_target_abs'),
|
extractFn('_gstack_link_target_abs'),
|
||||||
extractFn('_gstack_target_is_ours'),
|
extractFn('_gstack_target_is_ours'),
|
||||||
extractFn('_claude_entry_is_ours'),
|
extractFn('_claude_entry_is_ours'),
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ describe('setup --team / --no-team / -q', () => {
|
|||||||
try {
|
try {
|
||||||
for (const rel of [
|
for (const rel of [
|
||||||
'setup', 'VERSION', 'SKILL.md', 'qa/SKILL.md', 'bin/gstack-config', 'bin/gstack-patch-names',
|
'setup', 'VERSION', 'SKILL.md', 'qa/SKILL.md', 'bin/gstack-config', 'bin/gstack-patch-names',
|
||||||
'bin/gstack-state-root.sh', 'bin/gstack-bun-version.sh', 'bin/gstack-path-spelling.sh', 'bin/gstack-claude-root.sh', 'bin/gstack-install-registry.sh', 'bin/gstack-render-claude.sh', 'bin/gstack-host-renders.sh',
|
'bin/gstack-state-root.sh', 'bin/gstack-bun-version.sh', 'bin/gstack-path-spelling.sh', 'bin/gstack-skill-name.sh', 'bin/gstack-claude-root.sh', 'bin/gstack-install-registry.sh', 'bin/gstack-render-claude.sh', 'bin/gstack-host-renders.sh',
|
||||||
'scripts/resolve-codex-generation-model.ts', 'scripts/models.ts', 'scripts/preflight-codex-overlap.ts',
|
'scripts/resolve-codex-generation-model.ts', 'scripts/models.ts', 'scripts/preflight-codex-overlap.ts',
|
||||||
'scripts/discover-skills.ts', 'scripts/external-skill-names.ts', 'scripts/host-config.ts',
|
'scripts/discover-skills.ts', 'scripts/external-skill-names.ts', 'scripts/host-config.ts',
|
||||||
'lib/claude-code-migration.ts', 'lib/model-policy.ts', 'lib/model-catalog.ts',
|
'lib/claude-code-migration.ts', 'lib/model-policy.ts', 'lib/model-catalog.ts',
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ describe('link_claude_skill_dirs prefers rendered SKILL.md (behavior)', () => {
|
|||||||
'_FOREIGN_SKIPPED_ENTRIES=()',
|
'_FOREIGN_SKIPPED_ENTRIES=()',
|
||||||
'_BACKED_UP_SKILL_MDS=()',
|
'_BACKED_UP_SKILL_MDS=()',
|
||||||
`_SKILL_BACKUP_ROOT="${os.tmpdir()}/gstack-harness-backups-${process.pid}"`,
|
`_SKILL_BACKUP_ROOT="${os.tmpdir()}/gstack-harness-backups-${process.pid}"`,
|
||||||
|
`. "${path.join(ROOT, 'bin/gstack-skill-name.sh')}"`,
|
||||||
extractFn(SETUP_SRC, 'link_claude_skill_dirs'),
|
extractFn(SETUP_SRC, 'link_claude_skill_dirs'),
|
||||||
`link_claude_skill_dirs "${src}" "${skills}"`,
|
`link_claude_skill_dirs "${src}" "${skills}"`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
Reference in new issue
Block a user