From 17660a0558da1bf7bf89ed1b57c3f5f35120a825 Mon Sep 17 00:00:00 2001 From: fedster99 Date: Tue, 14 Jul 2026 14:08:30 -0700 Subject: [PATCH 1/2] fix(setup): link shared runtime libraries --- setup | 15 +++++++++++++-- test/gen-skill-docs.test.ts | 14 +++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/setup b/setup index 275236cd3..3d1eec386 100755 --- a/setup +++ b/setup @@ -748,8 +748,9 @@ create_agents_sidecar() { local agents_gstack="$repo_root/.agents/skills/gstack" mkdir -p "$agents_gstack" - # Sidecar directories that skills reference at runtime - for asset in bin browse review qa; do + # Sidecar directories that skills reference at runtime. bin scripts import + # shared modules via ../lib, so bin and lib must always travel together. + for asset in bin lib browse review qa; do local src="$SOURCE_GSTACK_DIR/$asset" local dst="$agents_gstack/$asset" if [ -d "$src" ] || [ -f "$src" ]; then @@ -796,6 +797,9 @@ create_codex_runtime_root() { if [ -d "$gstack_dir/bin" ]; then _link_or_copy "$gstack_dir/bin" "$codex_gstack/bin" fi + if [ -d "$gstack_dir/lib" ]; then + _link_or_copy "$gstack_dir/lib" "$codex_gstack/lib" + fi if [ -d "$gstack_dir/browse/dist" ]; then _link_or_copy "$gstack_dir/browse/dist" "$codex_gstack/browse/dist" fi @@ -836,6 +840,9 @@ create_factory_runtime_root() { if [ -d "$gstack_dir/bin" ]; then _link_or_copy "$gstack_dir/bin" "$factory_gstack/bin" fi + if [ -d "$gstack_dir/lib" ]; then + _link_or_copy "$gstack_dir/lib" "$factory_gstack/lib" + fi if [ -d "$gstack_dir/browse/dist" ]; then _link_or_copy "$gstack_dir/browse/dist" "$factory_gstack/browse/dist" fi @@ -874,6 +881,9 @@ create_opencode_runtime_root() { if [ -d "$gstack_dir/bin" ]; then _link_or_copy "$gstack_dir/bin" "$opencode_gstack/bin" fi + if [ -d "$gstack_dir/lib" ]; then + _link_or_copy "$gstack_dir/lib" "$opencode_gstack/lib" + fi if [ -d "$gstack_dir/browse/dist" ]; then _link_or_copy "$gstack_dir/browse/dist" "$opencode_gstack/browse/dist" fi @@ -1115,6 +1125,7 @@ if [ "$INSTALL_KIRO" -eq 1 ]; then [ -L "$KIRO_GSTACK" ] && rm -f "$KIRO_GSTACK" mkdir -p "$KIRO_GSTACK" "$KIRO_GSTACK/browse" "$KIRO_GSTACK/gstack-upgrade" "$KIRO_GSTACK/review" _link_or_copy "$SOURCE_GSTACK_DIR/bin" "$KIRO_GSTACK/bin" + _link_or_copy "$SOURCE_GSTACK_DIR/lib" "$KIRO_GSTACK/lib" _link_or_copy "$SOURCE_GSTACK_DIR/browse/dist" "$KIRO_GSTACK/browse/dist" _link_or_copy "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin" # ETHOS.md — referenced by "Search Before Building" in all skill preambles diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd..bc9c1b29b 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -2406,6 +2406,7 @@ describe('setup script validation', () => { expect(setupContent).toContain('kiro-cli'); expect(setupContent).toContain('KIRO_SKILLS='); expect(setupContent).toContain('~/.kiro/skills/gstack'); + expect(setupContent).toContain('$KIRO_GSTACK/lib'); }); test('setup supports --host opencode with install section and OpenCode skill path vars', () => { @@ -2421,14 +2422,16 @@ describe('setup script validation', () => { expect(setupContent).toContain('qa/templates'); expect(setupContent).toContain('qa/references'); expect(setupContent).toContain('dx-hall-of-fame.md'); + expect(setupContent).toContain('$opencode_gstack/lib'); }); test('create_agents_sidecar links runtime assets', () => { - // Sidecar must link bin, browse, review, qa + // Sidecar must link bin with its shared lib modules, plus browse, review, qa const fnStart = setupContent.indexOf('create_agents_sidecar()'); const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', fnStart)); const fnBody = setupContent.slice(fnStart, fnEnd); expect(fnBody).toContain('bin'); + expect(fnBody).toContain('lib'); expect(fnBody).toContain('browse'); expect(fnBody).toContain('review'); expect(fnBody).toContain('qa'); @@ -2439,6 +2442,7 @@ describe('setup script validation', () => { const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', setupContent.indexOf('review/', fnStart))); const fnBody = setupContent.slice(fnStart, fnEnd); expect(fnBody).toContain('gstack/SKILL.md'); + expect(fnBody).toContain('$codex_gstack/lib'); expect(fnBody).toContain('browse/dist'); expect(fnBody).toContain('browse/bin'); expect(fnBody).toContain('gstack-upgrade/SKILL.md'); @@ -2450,6 +2454,14 @@ describe('setup script validation', () => { expect(fnBody).not.toContain('_link_or_copy "$gstack_dir" "$codex_gstack"'); }); + test('create_factory_runtime_root links shared lib modules beside bin', () => { + const fnStart = setupContent.indexOf('create_factory_runtime_root()'); + const fnEnd = setupContent.indexOf('create_opencode_runtime_root()', fnStart); + const fnBody = setupContent.slice(fnStart, fnEnd); + expect(fnBody).toContain('$factory_gstack/bin'); + expect(fnBody).toContain('$factory_gstack/lib'); + }); + test('direct Codex installs are migrated out of ~/.codex/skills/gstack', () => { expect(setupContent).toContain('migrate_direct_codex_install'); expect(setupContent).toContain('$HOME/.gstack/repos/gstack'); From f412cf13ee19cabf90b7c2061ba9810a73f77e51 Mon Sep 17 00:00:00 2001 From: fedster99 Date: Wed, 15 Jul 2026 16:46:34 -0700 Subject: [PATCH 2/2] test(setup): run a lib-importing bin command from every host root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review: end-to-end coverage that gstack-learnings-log (which imports ../lib/jsonl-store.ts) actually executes from each installed runtime root — agents sidecar, Codex, Factory, OpenCode, and Kiro — built by the real setup shell code, in both symlink (IS_WINDOWS=0) and Windows copy (IS_WINDOWS=1) modes. A negative control proves the probe fails on the pre-fix bin-without-lib layout. Verified: all 10 positive cells fail against upstream/main's setup and pass with the fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/setup-runtime-lib-command.test.ts | 190 +++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 test/setup-runtime-lib-command.test.ts diff --git a/test/setup-runtime-lib-command.test.ts b/test/setup-runtime-lib-command.test.ts new file mode 100644 index 000000000..d116bdfb3 --- /dev/null +++ b/test/setup-runtime-lib-command.test.ts @@ -0,0 +1,190 @@ +import { describe, test, expect } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8'); + +// gstack-learnings-log is the command from the original bug report: bin scripts +// import shared modules via `$SCRIPT_DIR/../lib`, so a runtime root that +// exposes bin/ without lib/ fails with "Cannot find module .../lib/jsonl-store.ts". +// Running it end-to-end from each installed root proves bin and lib travel together. +const PAYLOAD = JSON.stringify({ + skill: 'review', + type: 'pattern', + key: 'runtime-lib-e2e', + insight: 'bin commands resolve sibling lib modules after setup', + confidence: 8, + source: 'observed', +}); + +// Slice a named shell function out of setup by its anchors so the tests are +// resilient to line-number drift (same idiom as setup-windows-fallback.test.ts). +function extractFunction(name: string): string { + const start = SETUP_SRC.indexOf(`${name}() {`); + const end = SETUP_SRC.indexOf('\n}\n', start); + if (start < 0 || end < 0) throw new Error(`Could not locate ${name}() in setup`); + return SETUP_SRC.slice(start, end + 2); +} + +// The Kiro install is an inline block, not a function. Slice from the runtime +// root assignment through the last runtime-asset link so the extracted code is +// a complete statement list. +function extractKiroBlock(): string { + const startAnchor = 'KIRO_GSTACK="$KIRO_SKILLS/gstack"'; + const endAnchor = '_link_or_copy "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin"'; + const start = SETUP_SRC.indexOf(startAnchor); + const end = SETUP_SRC.indexOf(endAnchor, start); + if (start < 0 || end < 0) throw new Error('Could not locate the Kiro install block in setup'); + return SETUP_SRC.slice(start, end + endAnchor.length); +} + +interface CommandResult { + buildStatus: number | null; + buildStderr: string; + runStatus: number | null; + runStderr: string; + learningsWritten: boolean; + libIsSymlink: boolean | null; +} + +// Build one host runtime root inside a sandbox using the real setup shell code +// (IS_WINDOWS toggles _link_or_copy between symlink and copy), then execute +// gstack-learnings-log from the installed root and check the learning landed. +function buildRootAndRunCommand( + isWindows: '0' | '1', + buildScript: (sandbox: string) => { script: string; rootDir: string }, +): CommandResult { + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-runtime-lib-')); + try { + const home = path.join(sandbox, 'home'); + const project = path.join(sandbox, 'project'); + fs.mkdirSync(home, { recursive: true }); + fs.mkdirSync(project, { recursive: true }); + + const { script, rootDir } = buildScript(sandbox); + const build = spawnSync( + 'bash', + ['-c', `IS_WINDOWS=${isWindows}\n${extractFunction('_link_or_copy')}\n${script}`], + { encoding: 'utf-8', timeout: 30000 }, + ); + + const libLst = fs.lstatSync(path.join(rootDir, 'lib'), { throwIfNoEntry: false }); + const run = spawnSync('bash', [path.join(rootDir, 'bin', 'gstack-learnings-log'), PAYLOAD], { + cwd: project, + encoding: 'utf-8', + timeout: 30000, + env: { ...process.env, HOME: home, GSTACK_HOME: path.join(home, '.gstack') }, + }); + + const projectsDir = path.join(home, '.gstack', 'projects'); + const learningsWritten = fs.existsSync(projectsDir) + && fs.readdirSync(projectsDir).some((slug) => { + const file = path.join(projectsDir, slug, 'learnings.jsonl'); + return fs.existsSync(file) && fs.readFileSync(file, 'utf-8').includes('runtime-lib-e2e'); + }); + + return { + buildStatus: build.status, + buildStderr: build.stderr, + runStatus: run.status, + runStderr: run.stderr, + learningsWritten, + libIsSymlink: libLst ? libLst.isSymbolicLink() : null, + }; + } finally { + fs.rmSync(sandbox, { recursive: true, force: true }); + } +} + +// One builder per host root the lib fix touches. Each returns the shell that +// setup itself runs plus where the installed runtime root lands. +const HOST_ROOTS: Record { script: string; rootDir: string }> = { + 'agents sidecar': (sandbox) => ({ + script: [ + `SOURCE_GSTACK_DIR="${ROOT}"`, + extractFunction('create_agents_sidecar'), + `mkdir -p "${sandbox}/repo"`, + `create_agents_sidecar "${sandbox}/repo"`, + ].join('\n'), + rootDir: path.join(sandbox, 'repo', '.agents', 'skills', 'gstack'), + }), + codex: (sandbox) => ({ + script: [ + extractFunction('create_codex_runtime_root'), + `create_codex_runtime_root "${ROOT}" "${sandbox}/home/.codex/skills/gstack"`, + ].join('\n'), + rootDir: path.join(sandbox, 'home', '.codex', 'skills', 'gstack'), + }), + factory: (sandbox) => ({ + script: [ + extractFunction('create_factory_runtime_root'), + `create_factory_runtime_root "${ROOT}" "${sandbox}/home/.factory/skills/gstack"`, + ].join('\n'), + rootDir: path.join(sandbox, 'home', '.factory', 'skills', 'gstack'), + }), + opencode: (sandbox) => ({ + script: [ + extractFunction('create_opencode_runtime_root'), + `create_opencode_runtime_root "${ROOT}" "${sandbox}/home/.opencode/skills/gstack"`, + ].join('\n'), + rootDir: path.join(sandbox, 'home', '.opencode', 'skills', 'gstack'), + }), + kiro: (sandbox) => ({ + script: [ + `HOME="${sandbox}/home"`, + `SOURCE_GSTACK_DIR="${ROOT}"`, + `KIRO_SKILLS="$HOME/.kiro/skills"`, + `mkdir -p "$KIRO_SKILLS"`, + extractKiroBlock(), + ].join('\n'), + rootDir: path.join(sandbox, 'home', '.kiro', 'skills', 'gstack'), + }), +}; + +// The IS_WINDOWS=0 cells rely on Unix `ln -snf` semantics; on a real Windows +// runner without Developer Mode that silently degrades to a copy — the exact +// bug _link_or_copy works around — so skip there, matching the behavior-matrix +// precedent in setup-windows-fallback.test.ts. The IS_WINDOWS=1 cells exercise +// the Windows copy branch itself, which is plain `cp -R` and portable. +describe.skipIf(process.platform === 'win32')('setup: bin commands resolve sibling lib from every host root', () => { + for (const [host, buildScript] of Object.entries(HOST_ROOTS)) { + test(`${host} root (symlink install): gstack-learnings-log imports ../lib and writes the learning`, () => { + const r = buildRootAndRunCommand('0', buildScript); + expect(r.buildStatus).toBe(0); + expect(r.libIsSymlink).toBe(true); + expect(r.runStderr).not.toContain('lib/jsonl-store.ts'); + expect(r.runStatus).toBe(0); + expect(r.learningsWritten).toBe(true); + }); + + test(`${host} root (Windows copy install): gstack-learnings-log imports ../lib and writes the learning`, () => { + const r = buildRootAndRunCommand('1', buildScript); + expect(r.buildStatus).toBe(0); + // Windows branch copies: lib must be a real directory, not a symlink. + expect(r.libIsSymlink).toBe(false); + expect(r.runStderr).not.toContain('lib/jsonl-store.ts'); + expect(r.runStatus).toBe(0); + expect(r.learningsWritten).toBe(true); + }); + } + + // Negative control: a root with bin/ but no lib/ (the pre-fix layout) must + // fail on the ../lib import. Proves the positive cells actually detect the + // regression rather than passing vacuously. + test('a root missing lib/ beside bin/ fails the ../lib import (pre-fix layout)', () => { + const r = buildRootAndRunCommand('0', (sandbox) => ({ + script: [ + `mkdir -p "${sandbox}/broken"`, + `_link_or_copy "${ROOT}/bin" "${sandbox}/broken/bin"`, + ].join('\n'), + rootDir: path.join(sandbox, 'broken'), + })); + expect(r.buildStatus).toBe(0); + expect(r.runStatus).not.toBe(0); + expect(r.runStderr).toContain('lib/jsonl-store.ts'); + expect(r.learningsWritten).toBe(false); + }); +});