From 69fa5a7a6835dd14109300c0731c9482225dea13 Mon Sep 17 00:00:00 2001 From: Simon Altit Date: Mon, 31 Aug 2026 13:02:23 +0100 Subject: [PATCH] fix: align feature marker seeding with GStack state Wave-amended: seeding relocation re-applied to the composite action (v1.77 moved CI seeding out of the inline workflow steps the original commit edited); wiring tripwire re-pointed accordingly; stale marker comment updated --- .../actions/register-gstack-skills/action.yml | 8 ++---- test/gstack-skill-start.test.ts | 13 +++++++++ test/helpers/e2e-helpers.ts | 10 ++++++- test/helpers/hermetic-env.ts | 10 ++++++- test/hermetic-wiring.test.ts | 28 +++++++++++++++++++ test/setup-claude-skill-assets.test.ts | 2 +- 6 files changed, 63 insertions(+), 8 deletions(-) diff --git a/.github/actions/register-gstack-skills/action.yml b/.github/actions/register-gstack-skills/action.yml index 1a96953d1..479bb723f 100644 --- a/.github/actions/register-gstack-skills/action.yml +++ b/.github/actions/register-gstack-skills/action.yml @@ -70,11 +70,9 @@ runs: "$HOME/.gstack/.telemetry-prompted" \ "$HOME/.gstack/.proactive-prompted" \ "$HOME/.gstack/.completeness-intro-seen" \ - "$HOME/.gstack/.plan-tune-nudge-shown" - # These two resolve through the gstack root symlink into $REPO — - # untracked scratch in the CI checkout, exactly where the preamble looks. - touch "$SKILLS_DIR/gstack/.feature-prompted-continuous-checkpoint" \ - "$SKILLS_DIR/gstack/.feature-prompted-model-overlay" + "$HOME/.gstack/.plan-tune-nudge-shown" \ + "$HOME/.gstack/.feature-prompted-continuous-checkpoint" \ + "$HOME/.gstack/.feature-prompted-model-overlay" echo "--- registry under $SKILLS_DIR ---" ls -la "$SKILLS_DIR/gstack" # ── Fail-fast verification ────────────────────────────────────────── diff --git a/test/gstack-skill-start.test.ts b/test/gstack-skill-start.test.ts index 46acf6792..ff68cbffc 100644 --- a/test/gstack-skill-start.test.ts +++ b/test/gstack-skill-start.test.ts @@ -321,6 +321,7 @@ describe('gstack-skill-start behavior', () => { expect(checkpoint).toContain( `touch "${path.join(freshGh, '.feature-prompted-continuous-checkpoint')}"`, ); + expect(checkpoint).not.toContain('GSTACK_INSTRUCTION_BEGIN: feature-overlay'); expect(checkpoint).not.toContain( path.join(projectSkillRoot, '.feature-prompted-continuous-checkpoint'), ); @@ -334,7 +335,19 @@ describe('gstack-skill-start behavior', () => { expect(overlay).toContain( `touch "${path.join(freshGh, '.feature-prompted-model-overlay')}"`, ); + expect(overlay).not.toContain('GSTACK_INSTRUCTION_BEGIN: feature-checkpoint'); expect(overlay).not.toContain(path.join(projectSkillRoot, '.feature-prompted-model-overlay')); + + fs.writeFileSync(path.join(freshGh, '.feature-prompted-model-overlay'), ''); + const acknowledged = execFileSync(localStart, ['--skill', 'testskill'], { + encoding: 'utf-8', + cwd: projectRoot, + env, + }); + expect(acknowledged).not.toContain('GSTACK_INSTRUCTION_BEGIN: feature-checkpoint'); + expect(acknowledged).not.toContain('GSTACK_INSTRUCTION_BEGIN: feature-overlay'); + expect(acknowledged).not.toContain('.feature-prompted-continuous-checkpoint'); + expect(acknowledged).not.toContain('.feature-prompted-model-overlay'); } finally { fs.rmSync(projectRoot, { recursive: true, force: true }); fs.rmSync(freshGh, { recursive: true, force: true }); diff --git a/test/helpers/e2e-helpers.ts b/test/helpers/e2e-helpers.ts index 87e5f2c54..e4ed03672 100644 --- a/test/helpers/e2e-helpers.ts +++ b/test/helpers/e2e-helpers.ts @@ -324,7 +324,15 @@ if (evalsEnabled) { fs.mkdirSync(gstackDir, { recursive: true }); // Marker list kept at parity with hermetic-env.ts's child-GSTACK_HOME seed // (the canonical set for the emission layer's gates). - for (const f of ['.activated', '.completeness-intro-seen', '.telemetry-prompted', '.proactive-prompted', '.first-loop-tip-shown']) { + for (const f of [ + '.activated', + '.completeness-intro-seen', + '.telemetry-prompted', + '.proactive-prompted', + '.first-loop-tip-shown', + '.feature-prompted-continuous-checkpoint', + '.feature-prompted-model-overlay', + ]) { const p = path.join(gstackDir, f); if (!fs.existsSync(p)) fs.writeFileSync(p, ''); } diff --git a/test/helpers/hermetic-env.ts b/test/helpers/hermetic-env.ts index 5e3229c3c..820e3a8dc 100644 --- a/test/helpers/hermetic-env.ts +++ b/test/helpers/hermetic-env.ts @@ -217,7 +217,15 @@ export function getHermeticDirs(): HermeticDirs { // children — without these, the emission layer fires lake-intro/telemetry // prompts that burn turns and can stall PTY tests waiting on an answer. // Tests that exercise onboarding itself override GSTACK_HOME per-test. - for (const f of ['.activated', '.completeness-intro-seen', '.telemetry-prompted', '.proactive-prompted', '.first-loop-tip-shown']) { + for (const f of [ + '.activated', + '.completeness-intro-seen', + '.telemetry-prompted', + '.proactive-prompted', + '.first-loop-tip-shown', + '.feature-prompted-continuous-checkpoint', + '.feature-prompted-model-overlay', + ]) { fs.writeFileSync(path.join(gstackHome, f), ''); } // The privacy stop-gate is config-keyed, not marker-keyed: on machines diff --git a/test/hermetic-wiring.test.ts b/test/hermetic-wiring.test.ts index 87fe13523..2ed9a2d1f 100644 --- a/test/hermetic-wiring.test.ts +++ b/test/hermetic-wiring.test.ts @@ -60,6 +60,34 @@ describe('hermetic wiring tripwire', () => { } }); + test('feature prompt acknowledgements are seeded in GSTACK_HOME everywhere', () => { + const markers = [ + '.feature-prompted-continuous-checkpoint', + '.feature-prompted-model-overlay', + ]; + // CI seeding lives in the composite action (v1.77 moved it out of the + // inline workflow steps) — the workflows call the action, so one seeding + // site covers every lane. + const sources: Array<[string, number]> = [ + ['test/helpers/hermetic-env.ts', 1], + ['test/helpers/e2e-helpers.ts', 1], + ['.github/actions/register-gstack-skills/action.yml', 1], + ]; + + for (const [rel, expectedCount] of sources) { + const src = read(rel); + for (const marker of markers) { + expect(src.split(marker).length - 1, `${rel}: ${marker}`).toBe(expectedCount); + } + } + + for (const rel of ['.github/actions/register-gstack-skills/action.yml']) { + const src = read(rel); + expect(src).not.toContain('$SKILLS_DIR/gstack/.feature-prompted-'); + for (const marker of markers) expect(src).toContain(`$HOME/.gstack/${marker}`); + } + }); + test('claude runners gate --strict-mcp-config on isHermeticEnabled()', () => { // Zero MCP servers for hermetic children; EVALS_HERMETIC=0 must restore // operator MCP along with the operator env (the flag may not be diff --git a/test/setup-claude-skill-assets.test.ts b/test/setup-claude-skill-assets.test.ts index f8dcf4796..2599a0b6d 100644 --- a/test/setup-claude-skill-assets.test.ts +++ b/test/setup-claude-skill-assets.test.ts @@ -196,7 +196,7 @@ describe('two-class referenced-paths (ENG-OV7)', () => { const missing: string[] = []; for (const { fromSkill, skillName, rel } of collectRefs()) { if (skillName !== 'gstack') continue; // class 1 - if (rel.startsWith('.')) continue; // runtime state markers (.feature-prompted-*, .git) + if (rel.startsWith('.')) continue; // runtime state (.git; feature markers now live in ~/.gstack — #2728) if (BUILT_ARTIFACT_ALLOWLIST.some((a) => rel === a || rel.startsWith(a))) continue; if (KNOWN_BROKEN_CLASS2[rel]) continue; if (!fs.existsSync(path.join(ROOT, rel))) {