diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 80496ac85..4e5d47202 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -847,7 +847,11 @@ pick a new `--output`), then copy the recovery file to where you want it. **Meaning.** On Codex, Factory, OpenCode, Cursor, Copilot and Kiro every bash block finds gstack on its own: an exported `GSTACK_ROOT` (with `bin/` and `lib/`), then a repo-local install, then the host's global install -(`$CODEX_HOME/skills/gstack` on Codex). None existed. +(`$CODEX_HOME/skills/gstack` on Codex). None existed. A skill installed for one +folder (Hermes, or the Codex install `./setup` writes) is the exception for its +first block: that block runs the folder's own `bin/gstack-skill-start` and +ignores an exported `GSTACK_ROOT`. When the folder is missing, the block prints +this line and its start command then fails. **Fix.** diff --git a/test/skill-start-plain.test.ts b/test/skill-start-plain.test.ts index 753b2c663..49e58e9fe 100644 --- a/test/skill-start-plain.test.ts +++ b/test/skill-start-plain.test.ts @@ -14,7 +14,7 @@ import * as os from 'os'; import * as path from 'path'; import { ALL_HOST_CONFIGS } from '../hosts'; import { generatePreambleBash } from '../scripts/resolvers/preamble/generate-preamble-bash'; -import { insertRuntimePreludes } from '../scripts/resolvers/runtime-root'; +import { insertRuntimePreludes, runtimeRootPrelude } from '../scripts/resolvers/runtime-root'; import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types'; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gst882-')); @@ -22,8 +22,8 @@ const ctx = (host: string, installRoot: string | null) => ({ host, paths: HOST_PATHS[host], skillName: 'ship', tmplPath: '', model: 'claude', installRoot }) as TemplateContext; const startFence = (c: TemplateContext) => insertRuntimePreludes(generatePreambleBash(c), c).match(/```bash\n([\s\S]*?)\n```/)![1]; const GROUPED = /(?:^|[;&|]\s*)\{\s|exit 1;\s*\}/m; -const sh = (body: string, env: Record) => - spawnSync('bash', ['-uc', body], { cwd: tmp, encoding: 'utf8', timeout: 10_000, env: { PATH: '/usr/bin:/bin', ...env } }); +const sh = (body: string, env: Record, cwd = tmp) => + spawnSync('bash', ['-uc', body], { cwd, encoding: 'utf8', timeout: 10_000, env: { PATH: '/usr/bin:/bin', ...env } }); function stubStart(root: string) { fs.mkdirSync(path.join(root, 'bin'), { recursive: true }); fs.mkdirSync(path.join(root, 'lib'), { recursive: true }); @@ -66,5 +66,24 @@ describe('skill-start fence is plain shell on every host (GST-882)', () => { expect(r.stdout).toContain('STARTED '); expect(r.stdout).toContain('--skill ship'); }); + test(`${host.name} checked-in render: the start lookup picks the root later fences pick`, () => { + const base = path.join(tmp, `${host.name}-parity`); + const home = path.join(base, 'home'); + const repo = path.join(base, 'repo'); + const exported = path.join(base, 'exported'); + stubStart(path.join(home, host.name === 'codex' ? '.codex/skills/gstack' : host.globalRoot)); + fs.mkdirSync(repo, { recursive: true }); + spawnSync('git', ['init', '-q', repo], { timeout: 10_000 }); + const c = ctx(host.name, null); + const lookup = startFence(c).split('\n').slice(0, -1).join('\n'); + const root = (body: string, env: Record) => sh(`${body}\necho "ROOT=$GSTACK_ROOT"`, env, repo).stdout.trim().split('\n').at(-1); + for (const layout of ['global', 'repo-local', 'exported']) { + if (layout === 'repo-local') stubStart(path.join(repo, host.localSkillRoot)); + if (layout === 'exported') stubStart(exported); + const env = layout === 'exported' ? { HOME: home, GSTACK_ROOT: exported } : { HOME: home }; + expect(root(lookup, env)).toBe(root(runtimeRootPrelude(c), env)); + if (layout === 'exported') expect(root(lookup, env)).toBe(`ROOT=${exported}`); + } + }); } });