diff --git a/test/audit-compliance.test.ts b/test/audit-compliance.test.ts index 6241b18bc..201ff3a3d 100644 --- a/test/audit-compliance.test.ts +++ b/test/audit-compliance.test.ts @@ -35,25 +35,30 @@ describe('Audit compliance', () => { // Fix 2: Conditional telemetry — binary calls wrapped with existence check test('preamble telemetry calls are conditional on _TEL and binary existence', () => { - // After the preamble.ts refactor (Item 9), the bash/telemetry logic lives - // in submodules under scripts/resolvers/preamble/. Concatenate all preamble - // source (root + submodules) and assert against the combined text so this - // test tracks the semantic contract, not the file layout. + // Token-reduction Phase 1: the preamble's telemetry bash moved from the + // resolvers into bin/gstack-skill-start (pending finalization) and + // bin/gstack-skill-end (end-of-skill telemetry). Assert the semantic + // contract against the scripts — the new home of the calls. + const skillStart = readFileSync(join(ROOT, 'bin/gstack-skill-start'), 'utf-8'); + // Pending finalization must check _TEL and binary existence + expect(skillStart).toContain('_TEL" != "off"'); + expect(skillStart).toContain('-x '); + expect(skillStart).toContain('gstack-telemetry-log'); + // End-of-skill telemetry (gstack-skill-end) must also be conditional + const skillEnd = readFileSync(join(ROOT, 'bin/gstack-skill-end'), 'utf-8'); + expect(skillEnd).toContain('_TEL" != "off"'); + expect(skillEnd).toContain('-x '); + expect(skillEnd).toContain('gstack-telemetry-log'); + // The render-side epilogue prose survives in the resolvers and hands off + // to gstack-skill-end. const preambleDir = join(ROOT, 'scripts/resolvers/preamble'); const submoduleFiles = existsSync(preambleDir) ? readdirSync(preambleDir).filter(f => f.endsWith('.ts')).map(f => readFileSync(join(preambleDir, f), 'utf-8')) : []; - const rootPreamble = readFileSync(join(ROOT, 'scripts/resolvers/preamble.ts'), 'utf-8'); - const preamble = [rootPreamble, ...submoduleFiles].join('\n'); - // Pending finalization must check _TEL and binary existence - expect(preamble).toContain('_TEL" != "off"'); - expect(preamble).toContain('-x '); - expect(preamble).toContain('gstack-telemetry-log'); - // End-of-skill telemetry must also be conditional + const preamble = submoduleFiles.join('\n'); const completionIdx = preamble.indexOf('Telemetry (run last)'); expect(completionIdx).toBeGreaterThan(-1); - const completionSection = preamble.slice(completionIdx); - expect(completionSection).toContain('_TEL" != "off"'); + expect(preamble.slice(completionIdx)).toContain('gstack-skill-end'); }); // Round 2 Fix 1: W012 — Bun install uses checksum verification @@ -111,11 +116,27 @@ describe('Audit compliance', () => { // Round 2 Fix 4: Chrome CDP binds to localhost only // Fix 2+6: All generated SKILL.md files with telemetry are conditional test('all generated SKILL.md files with telemetry calls use conditional pattern', () => { + // Phase 1 moved the _TEL-gated bash into the scripts. Render-side + // gstack-telemetry-log calls (route + first-task events) rely on two + // layers instead: every call line is best-effort (`|| true`), and the + // binary itself no-ops when the telemetry tier is off. + const telLog = readFileSync(join(ROOT, 'bin/gstack-telemetry-log'), 'utf-8'); + expect(telLog).toContain('if [ "$TIER" = "off" ]'); + expect(telLog).toMatch(/if \[ "\$TIER" = "off" \][\s\S]{0,200}?exit 0/); + const skills = getAllSkillMds(); + let checked = 0; for (const { name, content } of skills) { - if (content.includes('gstack-telemetry-log')) { - expect(content).toContain('_TEL" != "off"'); + for (const line of content.split('\n')) { + if (!line.includes('gstack-telemetry-log')) continue; + // Prose mentions aren't calls; only executable lines invoke the binary. + if (!line.includes('bin/gstack-telemetry-log')) continue; + checked++; + expect(line, `${name}: telemetry call must be best-effort`).toContain('|| true'); + expect(line, `${name}: telemetry call must not surface errors`).toContain('2>/dev/null'); } } + // Guard against the scan silently matching nothing. + expect(checked).toBeGreaterThan(0); }); }); diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index dab152675..9eb1e1fe2 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -28,6 +28,15 @@ function readShipUnion(): string { return readSkillUnion('ship'); } +// Token-reduction Phase 1: the preamble's inline bash (session bookkeeping, +// config echoes, telemetry producers, artifacts sync) moved into +// bin/gstack-skill-start / bin/gstack-skill-end. The render carries a one-line +// invocation fence + interpretation prose. Assertions that pinned inline-bash +// internals now pin the scripts (the new home); render-side assertions pin the +// fence + prose. Script behavior is pinned by test/gstack-skill-start.test.ts. +const SKILL_START_SCRIPT = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8'); +const SKILL_END_SCRIPT = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-end'), 'utf-8'); + function extractDescription(content: string): string { const fmEnd = content.indexOf('\n---', 4); expect(fmEnd).toBeGreaterThan(0); @@ -315,7 +324,9 @@ describe('gen-skill-docs', () => { expect(content).not.toContain('contributor-logs'); expect(content).toContain('Operational Self-Improvement'); expect(content).toContain('gstack-learnings-log'); - expect(content).toContain('gstack-learnings-search --limit 3'); + // The learnings-resurface call moved from the inline preamble bash into + // the skill-start script (Phase 1) — same command, new home. + expect(SKILL_START_SCRIPT).toContain('gstack-learnings-search" --limit 3'); }); test('generated SKILL.md with LEARNINGS_LOG contains operational type', () => { @@ -324,16 +335,20 @@ describe('gen-skill-docs', () => { expect(content).toContain('operational'); }); - test('generated SKILL.md contains session awareness', () => { + test('session awareness lives in gstack-skill-start (registry touch + stale cleanup)', () => { + // The sessions registry moved from inline preamble bash into the script: + // it records the harness pid (--parent-pid identity) and expires entries + // older than 120 minutes. + expect(SKILL_START_SCRIPT).toContain('sessions/$PARENT_PID'); + expect(SKILL_START_SCRIPT).toContain('-mmin +120'); + // The render keeps the completion-status protocol the sessions feed into. const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); - expect(content).toContain('_SESSIONS'); expect(content).toContain('RECOMMENDATION'); }); - test('generated SKILL.md contains branch detection', () => { - const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); - expect(content).toContain('_BRANCH'); - expect(content).toContain('git branch --show-current'); + test('branch detection lives in gstack-skill-start and is echoed as BRANCH', () => { + expect(SKILL_START_SCRIPT).toContain('_BRANCH=$(git branch --show-current'); + expect(SKILL_START_SCRIPT).toContain('echo "BRANCH: $_BRANCH"'); }); // #2001: update_check: false silences the binary but the upgrade-handling @@ -343,19 +358,22 @@ describe('gen-skill-docs', () => { // JUST_UPGRADED prose on it — the same echo-then-gate convention every other // flag (PROACTIVE, SKILL_PREFIX, EXPLAIN_LEVEL, QUESTION_TUNING) follows. test('update_check opt-out gates preamble echo and upgrade-handling prose (issue #2001)', () => { + // The config-echo cluster moved into gstack-skill-start: the flag must be + // read and echoed there so the render's instruction layer can act on it. + expect(SKILL_START_SCRIPT, 'script must read update_check config').toContain('_UPDATE_CHECK=$('); + expect(SKILL_START_SCRIPT, 'script must echo UPDATE_CHECK').toContain('echo "UPDATE_CHECK: $_UPDATE_CHECK"'); + // Whenever a preamble-carrying render ships the upgrade-handling prose, it + // must gate on the echoed flag — same echo-then-gate convention as + // PROACTIVE/SKILL_PREFIX. (gstack-upgrade itself is out of scope: it has + // no preamble fence and handling UPGRADE_AVAILABLE is its whole job.) let checked = 0; for (const skill of CLAUDE_GENERATED_SKILLS) { const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8'); - // Scope: only skills that render the runtime config-echo cluster. - if (!content.includes('echo "QUESTION_TUNING: $_QUESTION_TUNING"')) continue; + if (!content.includes('gstack-skill-start')) continue; + if (!content.includes('UPGRADE_AVAILABLE ')) continue; checked++; - expect(content, `${skill.dir} must echo UPDATE_CHECK`).toContain('echo "UPDATE_CHECK: $_UPDATE_CHECK"'); - expect(content, `${skill.dir} must read update_check config`).toContain('_UPDATE_CHECK=$('); - // Whenever the upgrade-handling prose ships, it must gate on the flag. - if (content.includes('UPGRADE_AVAILABLE ')) { - expect(content, `${skill.dir} upgrade prose must gate on UPDATE_CHECK`) - .toContain('If `UPDATE_CHECK` is `"false"`'); - } + expect(content, `${skill.dir} upgrade prose must gate on UPDATE_CHECK`) + .toContain('If `UPDATE_CHECK` is `"false"`'); } // Guard against the scope filter silently matching nothing. expect(checked).toBeGreaterThan(0); @@ -377,9 +395,12 @@ describe('gen-skill-docs', () => { expect(content).not.toContain('## Completeness Principle'); }); - test('generated SKILL.md contains telemetry line', () => { + test('telemetry producer lives in the scripts; render documents the analytics sink', () => { + // The skill-usage.jsonl producers moved into the scripts (Phase 1). + expect(SKILL_START_SCRIPT).toContain('analytics/skill-usage.jsonl'); + expect(SKILL_END_SCRIPT).toContain('analytics/skill-usage.jsonl'); + // The render still tells the model where telemetry lands. const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); - expect(content).toContain('skill-usage.jsonl'); expect(content).toContain('~/.gstack/analytics'); }); @@ -499,7 +520,13 @@ describe('gen-skill-docs', () => { ]; for (const skill of PREAMBLE_SKILLS) { const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8'); - expect(content).toContain(`"skill":"${skill.name}"`); + // The skill name now travels as --skill into gstack-skill-start (the + // preamble fence) and gstack-skill-end (the telemetry epilogue) — the + // scripts write it into the JSONL events. + expect(content, `${skill.dir} preamble fence must pass its own name`) + .toMatch(new RegExp(`--skill "${skill.name}" --model`)); + expect(content, `${skill.dir} epilogue must pass its own name`) + .toContain(`gstack-skill-end --skill "${skill.name}"`); } }); @@ -1563,11 +1590,13 @@ describe('parameterized resolver support', () => { describe('preamble routing injection', () => { const shipContent = readShipUnion(); - test('preamble bash checks for routing section in CLAUDE.md and AGENTS.md', () => { + test('routing probe checks CLAUDE.md and AGENTS.md (now in gstack-skill-start)', () => { // #2500: the probe iterates CLAUDE.md AND AGENTS.md — non-Claude hosts - // route skills via AGENTS.md, the cross-harness convention file. - expect(shipContent).toContain('for _RF in CLAUDE.md AGENTS.md'); - expect(shipContent).toContain('grep -q "## Skill routing" "$_RF"'); + // route skills via AGENTS.md, the cross-harness convention file. The bash + // moved into gstack-skill-start; the render acts on the echoed HAS_ROUTING. + expect(SKILL_START_SCRIPT).toContain('for _RF in CLAUDE.md AGENTS.md'); + expect(SKILL_START_SCRIPT).toContain('grep -q "## Skill routing" "$_RF"'); + expect(SKILL_START_SCRIPT).toContain('echo "HAS_ROUTING: $_HAS_ROUTING"'); expect(shipContent).toContain('HAS_ROUTING'); }); @@ -2114,7 +2143,9 @@ describe('Codex generation (--host codex)', () => { expect(override.exitCode).toBe(0); const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8'); expect(content).toContain('Model-Specific Behavioral Patch (claude)'); - expect(content).toContain('MODEL_OVERLAY: claude'); + // The overlay now travels as --model into gstack-skill-start, which + // echoes MODEL_OVERLAY at runtime. + expect(content).toContain('--model "claude"'); } finally { // Restore the host-default render — later tests and the host-config // golden read this tree. @@ -2127,6 +2158,7 @@ describe('Codex generation (--host codex)', () => { } const restored = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8'); expect(restored).toContain('Model-Specific Behavioral Patch (gpt)'); + expect(restored).toContain('--model "gpt"'); }); }); @@ -2813,13 +2845,18 @@ describe('discover-skills hidden directory filtering', () => { }); describe('telemetry', () => { - test('generated SKILL.md contains telemetry start block', () => { + test('telemetry start block lives in gstack-skill-start; render notes the handoff keys', () => { + // The start-block bash moved into the script (Phase 1): it reads the + // config, mints the session identity, and echoes the STATUS keys. + expect(SKILL_START_SCRIPT).toContain('_TEL_START=$(date +%s)'); + expect(SKILL_START_SCRIPT).toContain('_SESSION_ID='); + expect(SKILL_START_SCRIPT).toContain('echo "TELEMETRY:'); + expect(SKILL_START_SCRIPT).toContain('echo "TEL_PROMPTED:'); + expect(SKILL_START_SCRIPT).toMatch(/gstack-config" get telemetry/); + // The render must tell the model to carry SESSION_ID/TEL_START to skill end. const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); - expect(content).toContain('_TEL_START'); - expect(content).toContain('_SESSION_ID'); - expect(content).toContain('TELEMETRY:'); - expect(content).toContain('TEL_PROMPTED:'); - expect(content).toContain('gstack-config get telemetry'); + expect(content).toContain('SESSION_ID'); + expect(content).toContain('TEL_START'); }); test('generated SKILL.md contains telemetry opt-in prompt', () => { @@ -2831,21 +2868,26 @@ describe('telemetry', () => { expect(content).toContain('gstack-config set telemetry off'); }); - test('generated SKILL.md contains telemetry epilogue', () => { + test('generated SKILL.md contains telemetry epilogue (one gstack-skill-end call)', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); expect(content).toContain('Telemetry (run last)'); - expect(content).toContain('gstack-telemetry-log'); - expect(content).toContain('_TEL_END'); - expect(content).toContain('_TEL_DUR'); - expect(content).toContain('SKILL_NAME'); - expect(content).toContain('OUTCOME'); + expect(content).toContain('gstack-skill-end --skill "gstack" --outcome OUTCOME'); + expect(content).toContain('--tel-start "TEL_START"'); expect(content).toContain('PLAN MODE EXCEPTION'); + // The duration math + remote-log dispatch moved into gstack-skill-end. + expect(SKILL_END_SCRIPT).toContain('_TEL_END'); + expect(SKILL_END_SCRIPT).toContain('_TEL_DUR'); + expect(SKILL_END_SCRIPT).toContain('SKILL_NAME'); + expect(SKILL_END_SCRIPT).toContain('OUTCOME'); + expect(SKILL_END_SCRIPT).toContain('gstack-telemetry-log'); }); - test('generated SKILL.md contains pending marker handling', () => { - const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); - expect(content).toContain('.pending'); - expect(content).toContain('_pending_finalize'); + test('pending marker handling lives in the scripts', () => { + // gstack-skill-start finalizes stale markers; gstack-skill-end clears the + // session's own marker. + expect(SKILL_START_SCRIPT).toContain("-name '.pending-*'"); + expect(SKILL_START_SCRIPT).toContain('_pending_finalize'); + expect(SKILL_END_SCRIPT).toContain('.pending-$SESSION_ID'); }); test('telemetry blocks appear in all skill files that use PREAMBLE', () => { @@ -2854,8 +2896,9 @@ describe('telemetry', () => { const skillPath = path.join(ROOT, skill, 'SKILL.md'); if (fs.existsSync(skillPath)) { const content = fs.readFileSync(skillPath, 'utf-8'); - expect(content).toContain('_TEL_START'); expect(content).toContain('Telemetry (run last)'); + expect(content).toContain(`gstack-skill-end --skill "${skill}"`); + expect(content).toContain('--tel-start "TEL_START"'); } } }); @@ -3701,7 +3744,11 @@ describe('PREAMBLE resolution requires declared preamble-tier', () => { // user scope, so a correctly configured project-scoped brain was invisible. // --------------------------------------------------------------------------- describe('brain-sync block reads project-scoped MCP registrations (#2499)', () => { - const rendered = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); + // Phase 1: the artifacts-sync bash (including the MCP-scope jq probe) moved + // from the rendered SKILL.md into bin/gstack-skill-start. Pin the LIVE + // script bytes — same assertions, new home. The render carries only the + // ARTIFACTS_SYNC interpretation prose. + const rendered = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8'); test('rendered _GBRAIN_MCP_ENTRY jq resolves project scope with nearest-ancestor cwd match', () => { const line = rendered.split('\n').find((l) => l.includes('_GBRAIN_MCP_ENTRY=$(')); diff --git a/test/plan-tune.test.ts b/test/plan-tune.test.ts index 40a1465b6..9bb0230aa 100644 --- a/test/plan-tune.test.ts +++ b/test/plan-tune.test.ts @@ -475,7 +475,14 @@ describe('preamble — QUESTION_TUNING injection', () => { preambleTier: 2, }; const out = generatePreamble(ctx); - expect(out).toContain('QUESTION_TUNING: $_QUESTION_TUNING'); + // Phase 1: the config echo moved into bin/gstack-skill-start; the render's + // section gates itself on the echoed key. + const script = fs.readFileSync( + path.join(import.meta.dir, '..', 'bin', 'gstack-skill-start'), + 'utf-8', + ); + expect(script).toContain('echo "QUESTION_TUNING: $_QUESTION_TUNING"'); + expect(out).toContain('QUESTION_TUNING: false'); expect(out).toContain('## Question Tuning'); expect(out).toContain('gstack-question-preference --check'); expect(out).toContain('gstack-question-log'); diff --git a/test/preamble-compose.test.ts b/test/preamble-compose.test.ts index f2bcf8bfc..0ada67f35 100644 --- a/test/preamble-compose.test.ts +++ b/test/preamble-compose.test.ts @@ -71,12 +71,24 @@ describe('Preamble composition order', () => { }); }); -describe('Conductor signal (preamble bash)', () => { - test('claude preamble emits CONDUCTOR_SESSION, gated on != headless (Issue 8)', () => { +describe('Conductor signal (skill-start script)', () => { + // Token-reduction Phase 1 moved the preamble bash into bin/gstack-skill-start; + // the Issue-8 invariant (CONDUCTOR_SESSION emitted, gated on != headless so + // eval/CI inside Conductor BLOCKs instead of rendering prose to nobody) + // lives in the script now. The render must still invoke the script and the + // AUQ prose still branches on the echoed line. + test('skill-start script emits CONDUCTOR_SESSION, gated on != headless (Issue 8)', () => { + const fs = require('fs'); + const path = require('path'); + const script = fs.readFileSync(path.join(import.meta.dir, '..', 'bin', 'gstack-skill-start'), 'utf-8'); + expect(script).toContain('echo "CONDUCTOR_SESSION: true"'); + expect(script).toMatch(/"\$_SESSION_KIND" != "headless"[\s\S]*CONDUCTOR_WORKSPACE_PATH[\s\S]*CONDUCTOR_PORT[\s\S]*CONDUCTOR_SESSION: true/); + }); + + test('claude preamble render invokes the script and interprets CONDUCTOR_SESSION', () => { const out = generatePreamble(makeCtx('claude', 2, 'claude')); - expect(out).toContain('echo "CONDUCTOR_SESSION: true"'); - // The emission must be suppressed when the session is headless (eval/CI - // inside Conductor must BLOCK, not render prose to nobody). - expect(out).toMatch(/"\$_SESSION_KIND" != "headless"[\s\S]*CONDUCTOR_WORKSPACE_PATH[\s\S]*CONDUCTOR_PORT[\s\S]*CONDUCTOR_SESSION: true/); + expect(out).toContain('gstack-skill-start'); + // The AUQ tool-resolution prose keys off the echoed line. + expect(out).toContain('CONDUCTOR_SESSION: true'); }); }); diff --git a/test/preamble-first-task-scaffold.test.ts b/test/preamble-first-task-scaffold.test.ts index d2bd4d68d..b5fef138c 100644 --- a/test/preamble-first-task-scaffold.test.ts +++ b/test/preamble-first-task-scaffold.test.ts @@ -153,8 +153,13 @@ describe('first-run-guidance preamble wiring (generated)', () => { const md = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8'); test('detection is gated to the first-ever run only (ACTIVATED=no, not headless)', () => { - expect(md).toContain('if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ]'); - expect(md).toContain('gstack-first-task-detect'); + // Token-reduction Phase 1: the gating bash moved from the rendered + // preamble into bin/gstack-skill-start — same gate, new home. The render + // acts on the echoed FIRST_TASK/ACTIVATED keys (asserted below). + const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8'); + expect(script).toContain('if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ]'); + expect(script).toContain('gstack-first-task-detect'); + expect(md).toContain('FIRST_TASK:'); }); test('emits the unified first-run guidance section branching on ACTIVATED', () => { diff --git a/test/routing-probe.test.ts b/test/routing-probe.test.ts index fd5d13df3..3d9fb463c 100644 --- a/test/routing-probe.test.ts +++ b/test/routing-probe.test.ts @@ -38,28 +38,38 @@ function makeCtx(host: 'claude' | 'codex'): TemplateContext { }; } -/** Extract the routing-probe block from the rendered preamble bash. */ -function extractRoutingProbe(rendered: string): string { - const start = rendered.indexOf('_HAS_ROUTING="no"'); +// Token-reduction Phase 1: the probe bash moved from the rendered preamble +// into bin/gstack-skill-start (invoked by every host's preamble fence). The +// probe block under test is extracted from the LIVE script bytes. +const SKILL_START_SCRIPT = fs.readFileSync( + path.join(ROOT, 'bin', 'gstack-skill-start'), + 'utf-8', +); + +/** Extract the routing-probe block from the skill-start script. */ +function extractRoutingProbe(scriptText: string): string { + const start = scriptText.indexOf('_HAS_ROUTING="no"'); expect(start).toBeGreaterThan(-1); - const end = rendered.indexOf('done', start); + const end = scriptText.indexOf('done', start); expect(end).toBeGreaterThan(start); - return rendered.slice(start, end + 'done'.length); + return scriptText.slice(start, end + 'done'.length); } describe('routing probe checks AGENTS.md too (#2500)', () => { for (const host of ['claude', 'codex'] as const) { - test(`rendered preamble probes CLAUDE.md AND AGENTS.md (${host})`, () => { + test(`preamble reaches the CLAUDE.md AND AGENTS.md probe (${host})`, () => { + // The render must invoke the script that owns the probe... const rendered = generatePreambleBash(makeCtx(host)); - const probe = extractRoutingProbe(rendered); + expect(rendered).toContain('gstack-skill-start'); + // ...and the probe must cover both convention files. + const probe = extractRoutingProbe(SKILL_START_SCRIPT); expect(probe).toContain('CLAUDE.md'); expect(probe).toContain('AGENTS.md'); }); } test('live probe block: AGENTS.md-only repo reports HAS_ROUTING=yes', () => { - const rendered = generatePreambleBash(makeCtx('claude')); - const probe = extractRoutingProbe(rendered); + const probe = extractRoutingProbe(SKILL_START_SCRIPT); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'routing-probe-')); try { fs.writeFileSync( @@ -77,8 +87,7 @@ describe('routing probe checks AGENTS.md too (#2500)', () => { }); test('live probe block: repo with neither file reports HAS_ROUTING=no', () => { - const rendered = generatePreambleBash(makeCtx('claude')); - const probe = extractRoutingProbe(rendered); + const probe = extractRoutingProbe(SKILL_START_SCRIPT); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'routing-probe-')); try { const out = execSync( diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index 8266a4857..59c190f9a 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -277,15 +277,30 @@ describe('Update check preamble', () => { for (const skill of skillsWithUpdateCheck) { test(`${skill} update check line ends with || true`, () => { + // Token-reduction Phase 1: the inline `_UPD=$(gstack-update-check ...)` + // bash moved into bin/gstack-skill-start. The render must (a) invoke the + // script with the exact flag shape, (b) carry the exit-0 degraded-install + // fallback (the successor of the old `|| true` guard at the fence level), + // and (c) keep the UPGRADE_AVAILABLE interpretation prose that acts on + // the script's update-check STATUS output. const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8'); - // The second line of the bash block must end with || true - // to avoid exit code 1 when _UPD is empty (up to date) - const match = content.match(/\[ -n "\$_UPD" \].*$/m); - expect(match).not.toBeNull(); - expect(match![0]).toContain('|| true'); + expect(content).toContain('bin/gstack-skill-start'); + expect(content).toMatch(/--skill "[^"]+" --model "[^"]+" --parent-pid "\$PPID"/); + expect(content).toContain('|| echo "SKILL_START: unavailable'); + expect(content).toContain('UPGRADE_AVAILABLE'); }); } + test('bin/gstack-skill-start update check line ends with || true (new home of the inline guard)', () => { + // The `[ -n "$_UPD" ] ... || true` guard (empty _UPD must not exit 1 when + // up to date) moved verbatim into the consolidated preamble script. Pin it + // there so the invariant survives in its new home. + const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8'); + const match = script.match(/\[ -n "\$_UPD" \].*$/m); + expect(match).not.toBeNull(); + expect(match![0]).toContain('|| true'); + }); + test('all skills with update check are generated from .tmpl', () => { for (const skill of skillsWithUpdateCheck) { const tmplPath = path.join(ROOT, skill + '.tmpl'); @@ -294,16 +309,19 @@ describe('Update check preamble', () => { }); test('update check bash block exits 0 when up to date', () => { - // Simulate the exact preamble command from SKILL.md + // Simulate the exact update-check lines from bin/gstack-skill-start + // (per-line `|| true`, sanitize pipe included) const result = Bun.spawnSync(['bash', '-c', - '_UPD=$(echo "" || true); [ -n "$_UPD" ] && echo "$_UPD" || true' + '_sanitize() { sed "s/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g"; }; ' + + '_UPD=$(echo "" || true); [ -n "$_UPD" ] && printf "%s\\n" "$_UPD" | _sanitize || true' ], { stdout: 'pipe', stderr: 'pipe' }); expect(result.exitCode).toBe(0); }); test('update check bash block exits 0 when upgrade available', () => { const result = Bun.spawnSync(['bash', '-c', - '_UPD=$(echo "UPGRADE_AVAILABLE 0.3.3 0.4.0" || true); [ -n "$_UPD" ] && echo "$_UPD" || true' + '_sanitize() { sed "s/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g"; }; ' + + '_UPD=$(echo "UPGRADE_AVAILABLE 0.3.3 0.4.0" || true); [ -n "$_UPD" ] && printf "%s\\n" "$_UPD" | _sanitize || true' ], { stdout: 'pipe', stderr: 'pipe' }); expect(result.exitCode).toBe(0); expect(result.stdout.toString().trim()).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); @@ -617,11 +635,25 @@ describe('v0.4.1 preamble features', () => { for (const skill of skillsWithPreamble) { test(`${skill} contains session awareness`, () => { + // Token-reduction Phase 1: the inline `_SESSIONS=$(find ~/.gstack/sessions ...)` + // bash moved into bin/gstack-skill-start. The render still carries session + // identity (--parent-pid feeds the sessions dir with the harness pid) and + // the SESSION_KIND STATUS-line interpretation prose. const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8'); - expect(content).toContain('_SESSIONS'); + expect(content).toMatch(/--parent-pid "\$PPID"/); + expect(content).toContain('SESSION_KIND'); }); } + test('bin/gstack-skill-start owns the session-tracking machinery (new home of _SESSIONS)', () => { + // The sessions-dir touch + stale-session cleanup that every preamble used + // to inline now lives in the consolidated script — pin it there. + const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8'); + expect(script).toContain('mkdir -p "$_GH/sessions"'); + expect(script).toContain('touch "$_GH/sessions/$PARENT_PID"'); + expect(script).toContain('-mmin +120'); // 120-min freshness window survives the move + }); + for (const skill of skillsWithPreamble) { test(`${skill} contains escalation protocol`, () => { const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8'); @@ -1459,7 +1491,11 @@ describe('Codex skill', () => { }); test('codex integration in /plan-eng-review offers plan critique', () => { - const content = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8'); + // Carved skill: the Codex outside-voice plan critique lives in + // sections/review-sections.md — read the skeleton+sections union. (The + // skeleton alone used to match "Codex" only via an inline-bash comment + // that the gstack-skill-start consolidation removed.) + const content = readSkillUnion('plan-eng-review'); expect(content).toContain('Codex'); expect(content).toContain('codex exec'); }); @@ -1795,9 +1831,14 @@ describe('Codex skill validation', () => { describe('Repo mode preamble validation', () => { test('generated SKILL.md preamble contains REPO_MODE output', () => { + // Token-reduction Phase 1: the inline `gstack-repo-mode` call moved into + // bin/gstack-skill-start. The render pins the script invocation; the + // script pins the REPO_MODE echo + the gstack-repo-mode call. const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); - expect(content).toContain('REPO_MODE:'); - expect(content).toContain('gstack-repo-mode'); + expect(content).toContain('bin/gstack-skill-start'); + const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8'); + expect(script).toContain('REPO_MODE:'); + expect(script).toContain('gstack-repo-mode'); }); test('tier 3+ skills contain See Something Say Something section', () => { diff --git a/test/telemetry-repo-strip.test.ts b/test/telemetry-repo-strip.test.ts index db25934c8..201cbc966 100644 --- a/test/telemetry-repo-strip.test.ts +++ b/test/telemetry-repo-strip.test.ts @@ -3,12 +3,15 @@ * * The telemetry consent copy promises a user's repo name is recorded locally * only and stripped before any upload (scripts/resolvers/preamble/ - * generate-telemetry-prompt.ts). Two producers write repo/branch identity into - * the local skill-usage.jsonl: + * generate-telemetry-prompt.ts). The producers that write repo/branch identity + * into the local skill-usage.jsonl (the preamble's inline bash moved into the + * skill-start/skill-end scripts in token-reduction Phase 1): * - * - the preamble epilogue → "repo" - * (scripts/resolvers/preamble/generate-preamble-bash.ts) - * - gstack-telemetry-log → "_repo_slug", "_branch" + * - gstack-skill-start (skill_run event) → "repo" + * (bin/gstack-skill-start) + * - gstack-skill-end (completion event) → (no repo identity today, + * scanned so drift is caught) (bin/gstack-skill-end) + * - gstack-telemetry-log → "_repo_slug", "_branch" * (bin/gstack-telemetry-log) * * gstack-telemetry-sync MUST strip every one of those fields before the remote @@ -37,7 +40,8 @@ import path from 'path'; const ROOT = path.resolve(__dirname, '..'); const SYNC = path.join(ROOT, 'bin', 'gstack-telemetry-sync'); -const PREAMBLE = path.join(ROOT, 'scripts', 'resolvers', 'preamble', 'generate-preamble-bash.ts'); +const SKILL_START = path.join(ROOT, 'bin', 'gstack-skill-start'); +const SKILL_END = path.join(ROOT, 'bin', 'gstack-skill-end'); const TEL_LOG = path.join(ROOT, 'bin', 'gstack-telemetry-log'); // Fields that identify the user's repo/branch. The promise is that NONE of @@ -94,10 +98,16 @@ describe('telemetry no-repo-identity-egress invariant', () => { // Repo-identity fields the producers emit into the synced file — computed // once, asserted against BOTH strip paths (jq primary, sed fallback). Only // emission lines that target the synced file (skill-usage.jsonl) count: the - // preamble appends directly; gstack-telemetry-log builds the synced event - // with a `printf '{"v":1,...` line into $JSONL_FILE (= skill-usage.jsonl). - const preambleSynced = fs - .readFileSync(PREAMBLE, 'utf-8') + // skill-start/skill-end scripts append directly (the former inline preamble + // bash); gstack-telemetry-log builds the synced event with a + // `printf '{"v":1,...` line into $JSONL_FILE (= skill-usage.jsonl). The + // timeline log carries "branch" but is local-only and never synced. + const skillStartSynced = fs + .readFileSync(SKILL_START, 'utf-8') + .split('\n') + .filter((l) => l.includes('skill-usage.jsonl')); + const skillEndSynced = fs + .readFileSync(SKILL_END, 'utf-8') .split('\n') .filter((l) => l.includes('skill-usage.jsonl')); const telLogSynced = fs @@ -105,7 +115,8 @@ describe('telemetry no-repo-identity-egress invariant', () => { .split('\n') .filter((l) => l.includes('"v":1') || l.includes('skill-usage')); const emitted = new Set([ - ...emittedRepoFields(preambleSynced), + ...emittedRepoFields(skillStartSynced), + ...emittedRepoFields(skillEndSynced), ...emittedRepoFields(telLogSynced), ]); @@ -116,8 +127,8 @@ describe('telemetry no-repo-identity-egress invariant', () => { }); test('coverage: every repo/branch field the producers emit into skill-usage.jsonl is stripped (sed fallback path)', () => { - // The preamble must emit "repo" — guards against the test silently passing - // because a regex stopped matching the producer. + // gstack-skill-start must emit "repo" — guards against the test silently + // passing because a regex stopped matching the producer. expect(emitted.has('repo')).toBe(true); for (const field of emitted) { expect( diff --git a/test/writing-style-resolver.test.ts b/test/writing-style-resolver.test.ts index 6d7444536..25f5d0479 100644 --- a/test/writing-style-resolver.test.ts +++ b/test/writing-style-resolver.test.ts @@ -15,10 +15,20 @@ * - Tier-1 preamble does NOT include Writing Style section */ import { describe, test, expect } from 'bun:test'; +import * as fs from 'fs'; +import * as path from 'path'; import type { TemplateContext } from '../scripts/resolvers/types'; import { HOST_PATHS } from '../scripts/resolvers/types'; import { generatePreamble } from '../scripts/resolvers/preamble'; +// Token-reduction Phase 1: the EXPLAIN_LEVEL config read + echo moved from the +// inline preamble bash into bin/gstack-skill-start; the render keeps the +// interpretation prose that acts on the echoed key. +const SKILL_START_SCRIPT = fs.readFileSync( + path.join(import.meta.dir, '..', 'bin', 'gstack-skill-start'), + 'utf-8', +); + function makeCtx(host: 'claude' | 'codex', tier: 1 | 2 | 3 | 4): TemplateContext { return { skillName: 'test-skill', @@ -35,9 +45,12 @@ describe('Writing Style preamble section', () => { expect(out).toContain('## Writing Style'); }); - test('tier 2+ preamble includes EXPLAIN_LEVEL echo in bash', () => { + test('EXPLAIN_LEVEL is echoed by gstack-skill-start and read by tier 2+ prose', () => { + // The bash echo lives in the script the preamble fence invokes... + expect(SKILL_START_SCRIPT).toContain('_EXPLAIN_LEVEL=$('); + expect(SKILL_START_SCRIPT).toContain('echo "EXPLAIN_LEVEL: $_EXPLAIN_LEVEL"'); + // ...and the tier-2+ render references the echoed key. const out = generatePreamble(makeCtx('claude', 2)); - expect(out).toContain('_EXPLAIN_LEVEL'); expect(out).toContain('EXPLAIN_LEVEL:'); }); @@ -70,13 +83,18 @@ describe('Writing Style preamble section', () => { test('Codex tier-2 preamble uses host-aware path (no .claude/)', () => { const out = generatePreamble(makeCtx('codex', 2)); - // The Writing Style section shouldn't reference a Claude-specific bin path. - // Specifically check the EXPLAIN_LEVEL bash line. - const explainLine = out.split('\n').find(l => l.includes('_EXPLAIN_LEVEL=')); + // The config read moved into gstack-skill-start, which resolves its bin + // dir $0-relative ($_BIN) — host-neutral by construction. + const explainLine = SKILL_START_SCRIPT.split('\n').find(l => l.includes('_EXPLAIN_LEVEL=')); expect(explainLine).toBeDefined(); expect(explainLine).not.toMatch(/~\/\.claude\//); - // Codex uses $GSTACK_BIN - expect(explainLine).toContain('$GSTACK_BIN'); + expect(explainLine).toContain('$_BIN/'); + // The Codex render's fence must reach the script via the host path, not + // a Claude-specific one. + const fenceLine = out.split('\n').find(l => l.includes('_SS=')); + expect(fenceLine).toBeDefined(); + expect(fenceLine).not.toMatch(/~\/\.claude\//); + expect(fenceLine).toContain('$GSTACK_BIN'); }); test('tier 1 preamble does NOT include Writing Style section', () => {