diff --git a/bin/gstack-skill-start b/bin/gstack-skill-start index d63c47de3..3da95c78a 100755 --- a/bin/gstack-skill-start +++ b/bin/gstack-skill-start @@ -362,14 +362,16 @@ if [ -n "$_UPD" ]; then The update-check output above applies. If it shows \`UPGRADE_AVAILABLE \`: read \`$_ROOT_DIR/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If it shows \`JUST_UPGRADED \`: print "Running gstack v{to} (just updated!)". If \`SPAWNED_SESSION\` is true, skip feature discovery. After upgrade prompts, continue the workflow. EOI fi -# Feature discovery (one prompt per session, marker-gated). -if [ ! -f "$_ROOT_DIR/.feature-prompted-continuous-checkpoint" ]; then +# Feature discovery (one prompt per session, marker-gated). These are +# machine-local UX acknowledgements, so keep them in GSTACK_HOME rather than +# beside an install path that may resolve through a project-local symlink. +if [ ! -f "$_GH/.feature-prompted-continuous-checkpoint" ]; then _emit_block feature-checkpoint < { } }); + test('feature acknowledgement markers stay in GSTACK_HOME through a project-local bin symlink', () => { + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ss-project-')); + const projectSkillRoot = path.join(projectRoot, '.agents', 'skills', 'gstack'); + const freshGh = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ss-feature-state-')); + fs.mkdirSync(projectSkillRoot, { recursive: true }); + fs.symlinkSync(path.join(ROOT, 'bin'), path.join(projectSkillRoot, 'bin'), 'dir'); + fs.writeFileSync(path.join(freshGh, 'config.yaml'), 'update_check: false\n'); + + const localStart = path.join(projectSkillRoot, 'bin', 'gstack-skill-start'); + const env = { PATH: process.env.PATH!, HOME: tmpHome, GSTACK_HOME: freshGh }; + try { + const checkpoint = execFileSync(localStart, ['--skill', 'testskill'], { + encoding: 'utf-8', + cwd: projectRoot, + env, + }); + expect(checkpoint).toContain( + `touch "${path.join(freshGh, '.feature-prompted-continuous-checkpoint')}"`, + ); + expect(checkpoint).not.toContain( + path.join(projectSkillRoot, '.feature-prompted-continuous-checkpoint'), + ); + + fs.writeFileSync(path.join(freshGh, '.feature-prompted-continuous-checkpoint'), ''); + const overlay = execFileSync(localStart, ['--skill', 'testskill'], { + encoding: 'utf-8', + cwd: projectRoot, + env, + }); + expect(overlay).toContain( + `touch "${path.join(freshGh, '.feature-prompted-model-overlay')}"`, + ); + expect(overlay).not.toContain(path.join(projectSkillRoot, '.feature-prompted-model-overlay')); + } finally { + fs.rmSync(projectRoot, { recursive: true, force: true }); + fs.rmSync(freshGh, { recursive: true, force: true }); + } + }); + test('MODEL_OVERLAY echoes the --model argument', () => { const out = runStart(['--model', 'opus']); expect(out).toMatch(/^MODEL_OVERLAY: opus$/m);