v1.93.32.0 fix(setup): bin/dev-setup works in a fresh gstack clone (GST-768) (#3789)

* 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>

* fix: pre-landing review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog fragment for GST-768

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:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-03 01:07:06 +00:00
1 parent 9514b76093
commit 923fc0a6b8
6 files changed
+98 -5

No files matched your search

+4
View File
@@ -1,5 +1,9 @@
# Changelog
## [1.93.32.0] - 2026-11-03
- **`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).
## [1.93.31.0] - 2026-11-03
**A CSO hardening test now fails at the Git step that broke instead of passing without testing anything.** `fixtureGit` in `test/cso-git-hardening.test.ts` threw only on a non-zero exit status. When Git was killed by its 30-second timeout or could not start, `spawnSync` returned a `null` status, and the helper treated that as success. If that happened to the `git replace` step, the test that checks a security snapshot ignores replaced history would pass without a replacement in place. The helper now throws on a spawn error or any status other than 0, and the message names the Git arguments, the status and the signal. The stricter helper showed that the file's warm-up step had been calling `/usr/bin/git` on Windows, where it doesn't exist, on every run. It was passing only because the error was swallowed. Every test in the file already skips on Windows, so the warm-up now skips there too.
+1 -1
View File
@@ -1 +1 @@
1.93.31.0
1.93.32.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.93.31.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.93.32.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.93.31",
"version": "1.93.32",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+7 -2
View File
@@ -641,10 +641,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
+84
View File
@@ -0,0 +1,84 @@
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<typeof makeFixture>) => 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);
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);
});