From 24af2ff139bbf1412e59f525b0b80887fa5e969c Mon Sep 17 00:00:00 2001 From: garrytan Date: Sun, 1 Nov 2026 10:20:02 +0000 Subject: [PATCH 1/3] fix(setup): bin/dev-setup succeeds in a fresh gstack clone (GST-768) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- setup | 9 +++- test/dev-setup-fresh-clone.test.ts | 71 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 test/dev-setup-fresh-clone.test.ts diff --git a/setup b/setup index f4d247792..5952e96ee 100755 --- a/setup +++ b/setup @@ -600,10 +600,15 @@ if [ "$CODEX_REPO_LOCAL" -eq 1 ]; then "$_codex_project"|"$_codex_project"/*) ;; *) echo "Refusing: project-local Codex destination resolves outside the project: $CODEX_SKILLS -> $_codex_parent. Use a separate project-local skills directory; setup will not relink it automatically." >&2; exit 1 ;; esac + _codex_self_host=0 + if [ "$_codex_project" = "$SOURCE_GSTACK_DIR" ] && [ "$_codex_parent" = "$SOURCE_GSTACK_DIR/.agents/skills" ]; then + _codex_self_host=1 + fi _codex_payload="$_codex_parent" while :; do - if [ "$_codex_payload" = "$SOURCE_GSTACK_DIR" ] \ - || { [ -f "$_codex_payload/setup" ] && [ -f "$_codex_payload/VERSION" ] && [ -f "$_codex_payload/bin/gstack-relink" ]; }; then + if { [ "$_codex_payload" = "$SOURCE_GSTACK_DIR" ] \ + || { [ -f "$_codex_payload/setup" ] && [ -f "$_codex_payload/VERSION" ] && [ -f "$_codex_payload/bin/gstack-relink" ]; }; } \ + && ! { [ "$_codex_self_host" -eq 1 ] && [ "$_codex_payload" = "$SOURCE_GSTACK_DIR" ]; }; then echo "Refusing: project-local Codex destination overlaps the source tree at $_codex_payload: $CODEX_SKILLS -> $_codex_parent. Use a separate project-local skills directory; setup will not relink it automatically." >&2 exit 1 fi diff --git a/test/dev-setup-fresh-clone.test.ts b/test/dev-setup-fresh-clone.test.ts new file mode 100644 index 000000000..5aa272641 --- /dev/null +++ b/test/dev-setup-fresh-clone.test.ts @@ -0,0 +1,71 @@ +import { afterAll, afterEach, describe, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; +import { readlinkSync, symlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import { cleanupFixtures, cleanupSeed, fixtureMkdirSync, makeFixture, makeSource, put, tree } from './helpers/install-fixture'; + +afterEach(cleanupFixtures); +afterAll(cleanupSeed); + +const stubBun = (f: ReturnType) => put(join(f.commands, 'bun'), `#!/usr/bin/env bash +case "$*" in + install|'install --production --frozen-lockfile') exit 0 ;; + 'build --help') echo 'Fixture Bun has no CSO compile flags'; exit 0 ;; + 'run build') echo 'Unexpected build in a dev-setup fixture' >&2; exit 90 ;; + *) exec '${process.execPath}' "$@" ;; +esac +`, 0o755); + +const freshClone = (rel = 'gstack') => { + const f = makeFixture(); + stubBun(f); + const source = makeSource(f, join(f.dir, rel)); + const init = spawnSync('git', ['init', '-q', source], { encoding: 'utf8', timeout: 30_000 }); + if (init.status !== 0) throw new Error(init.stderr); + return { f, source }; +}; + +describe.skipIf(process.platform === 'win32')('bin/dev-setup in a fresh gstack clone', () => { + test('links gstack into its own .claude and .agents skills and exits 0', () => { + const { f, source } = freshClone(); + for (let run = 0; run < 2; run++) { + const result = spawnSync('bash', [join(source, 'bin/dev-setup')], { cwd: source, env: f.env, encoding: 'utf8', timeout: 120_000, input: '' }); + expect(result.stderr).not.toContain('overlaps the source tree'); + expect(result.status, result.stdout + result.stderr).toBe(0); + expect(readlinkSync(join(source, '.claude/skills/gstack'))).toBe(source); + expect(readlinkSync(join(source, '.agents/skills/gstack'))).toBe(source); + } + }, 240_000); + + test('a user project whose .agents/skills resolves to the checkout\'s own .agents/skills is still refused', () => { + const { f, source } = freshClone('project/vendor/gstack'); + const project = join(f.dir, 'project'); + fixtureMkdirSync(join(source, '.agents/skills'), { recursive: true }); + fixtureMkdirSync(join(project, '.claude/skills'), { recursive: true }); + fixtureMkdirSync(join(project, '.agents')); + symlinkSync(source, join(project, '.claude/skills/gstack'), 'dir'); + symlinkSync(join(source, '.agents/skills'), join(project, '.agents/skills'), 'dir'); + const before = tree(f.dir); + const result = spawnSync('bash', [join(project, '.claude/skills/gstack/setup'), '--host', 'codex', '--no-team', '--no-plan-tune-hooks', '--no-timeline-stop-hook'], { + cwd: project, env: f.env, encoding: 'utf8', timeout: 60_000, input: '', + }); + expect(result.status, result.stdout + result.stderr).toBe(1); + expect(result.stderr).toContain(`overlaps the source tree at ${source}`); + expect(tree(f.dir)).toEqual(before); + }, 90_000); + + test('the checkout itself is still refused when its .agents/skills aliases the source root', () => { + const { f, source } = freshClone(); + fixtureMkdirSync(join(source, '.claude/skills'), { recursive: true }); + fixtureMkdirSync(join(source, '.agents')); + symlinkSync(source, join(source, '.claude/skills/gstack'), 'dir'); + symlinkSync(source, join(source, '.agents/skills'), 'dir'); + const before = tree(f.dir); + const result = spawnSync('bash', [join(source, '.claude/skills/gstack/setup'), '--host', 'codex', '--no-team', '--no-plan-tune-hooks', '--no-timeline-stop-hook'], { + cwd: source, env: f.env, encoding: 'utf8', timeout: 60_000, input: '', + }); + expect(result.status, result.stdout + result.stderr).toBe(1); + expect(result.stderr).toContain('overlaps the source tree'); + expect(tree(f.dir)).toEqual(before); + }, 90_000); +}); From 106700b26d44a14c767557d3383c240e1b75a871 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sun, 1 Nov 2026 10:30:17 +0000 Subject: [PATCH 2/3] fix: pre-landing review fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/dev-setup-fresh-clone.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/dev-setup-fresh-clone.test.ts b/test/dev-setup-fresh-clone.test.ts index 5aa272641..73d48c756 100644 --- a/test/dev-setup-fresh-clone.test.ts +++ b/test/dev-setup-fresh-clone.test.ts @@ -68,4 +68,17 @@ describe.skipIf(process.platform === 'win32')('bin/dev-setup in a fresh gstack c expect(result.stderr).toContain('overlaps the source tree'); expect(tree(f.dir)).toEqual(before); }, 90_000); + + test('setup --host codex from the checkout\'s own layout is still refused by the Codex preflight, without writes', () => { + const { f, source } = freshClone(); + const devSetup = spawnSync('bash', [join(source, 'bin/dev-setup')], { cwd: source, env: f.env, encoding: 'utf8', timeout: 120_000, input: '' }); + expect(devSetup.status, devSetup.stdout + devSetup.stderr).toBe(0); + const before = tree(f.dir); + const result = spawnSync('bash', [join(source, '.claude/skills/gstack/setup'), '--host', 'codex', '--no-team', '--no-plan-tune-hooks', '--no-timeline-stop-hook'], { + cwd: source, env: f.env, encoding: 'utf8', timeout: 60_000, input: '', + }); + expect(result.status, result.stdout + result.stderr).toBe(1); + expect(result.stderr).toContain(`Refusing: Codex sidecar root write overlaps source or escapes its namespace: ${join(source, '.agents/skills/gstack')}`); + expect(tree(f.dir)).toEqual(before); + }, 240_000); }); From 2b1e8501c2c2f5a3eb588f79a6773a13bc8dfd4a Mon Sep 17 00:00:00 2001 From: garrytan Date: Sun, 1 Nov 2026 10:37:56 +0000 Subject: [PATCH 3/3] docs: changelog fragment for GST-768 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- changelog.d/fix-gst-768-dev-setup-fresh-clone.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fix-gst-768-dev-setup-fresh-clone.md diff --git a/changelog.d/fix-gst-768-dev-setup-fresh-clone.md b/changelog.d/fix-gst-768-dev-setup-fresh-clone.md new file mode 100644 index 000000000..bc2077674 --- /dev/null +++ b/changelog.d/fix-gst-768-dev-setup-fresh-clone.md @@ -0,0 +1 @@ +- **`bin/dev-setup` works in a fresh clone again.** It installed dependencies, then setup refused with "project-local Codex destination overlaps the source tree" and exited 1, because setup's overlap guard read gstack's own `.agents/skills` as a foreign project's skills directory pointing into the checkout. Setup now allows that one layout, where the project is the gstack checkout itself and its Codex skills directory is that checkout's own `.agents/skills`. Every other overlap is still refused. That includes a user project whose `.agents/skills` leads into a gstack checkout, a checkout whose `.agents/skills` points at its own root, and `--host codex` run from a dev checkout, which the Codex preflight still stops before any write. test/dev-setup-fresh-clone.test.ts covers the fresh clone and all three refusals (GST-768).