Files
gstack/test/plan-mode-off-generation.test.ts
T
cb9ccf2e80 v1.91.158.0 feat(config): plan_mode_preamble=off drops the plan-mode preamble from installed skills (port of #3213) (#3308)
* feat(gen-skill-docs): add --plan-mode=off to omit the plan-mode preamble (#3168)

Generated skills ship a ~1.8 KB "## Plan Mode Safe Operations" +
"## Skill Invocation During Plan Mode" block on every skill, for hosts
and users who never enter plan mode. This adds a --plan-mode=off flag to
gen-skill-docs that drops that block at gen time. Default builds
(planMode undefined or true) keep the block byte-identically.

The "PLAN MODE EXCEPTION — ALWAYS RUN" markers inside skill bodies
still apply when the flag is off.

(cherry picked from commit 51edee9915)

* style(types): fix stray indentation on TemplateContext.installRoot

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

* feat(config): plan_mode_preamble key wires --plan-mode=off into setup and every installed render

gstack-config plan_mode_preamble (on|off, default on) makes ./setup pass
--plan-mode=off to every gen:skill-docs render, gbrain-refresh and the
gstack-relink router render. The default Claude install, which otherwise
serves the committed SKILL.md files, gets a plain render with the key off;
the activation record notes plan_mode=off so switching back on removes the
render, including on the gbrain=unknown keep branch. With the key on or
unset every path and record is byte-for-byte unchanged.

Fixes #3168. Finishes #3213 by @xingzihai.

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

* fix(setup,relink): plan_mode_preamble=off survives builds and failed renders; alias follows the router

Review findings on the plan_mode_preamble wiring:
- a build renders env hosts without --plan-mode=off, so with the key off
  setup no longer skips their re-render after a build;
- with the key off the gbrain-absent branch leaves the render to the plain
  re-render, so a failed render keeps the previous one;
- gstack-relink refreshes the _gstack-command alias after re-rendering the
  router (also fixes the same staleness for disabled_skills).

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

---------

Co-authored-by: xingzihai <xingzihai@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-19 23:31:59 +00:00

84 lines
3.6 KiB
TypeScript

/**
* --plan-mode=off end to end through the generator, Claude host (#3168).
*
* Default output must stay byte-identical to the committed SKILL.md files.
* With planMode false, every skill that carries the preamble's
* "## Plan Mode Safe Operations" and "## Skill Invocation During Plan Mode"
* sections loses exactly those two sections and nothing else; skills
* without them are unchanged.
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runGeneration } from '../scripts/gen-skill-docs';
const ROOT = path.resolve(import.meta.dir, '..');
const SAFE = '## Plan Mode Safe Operations\n';
const INVOCATION = '## Skill Invocation During Plan Mode\n\n';
let tmp: string;
let skills: string[];
beforeAll(async () => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-plan-mode-gen-'));
const on = await runGeneration({ host: 'claude', outputRoot: path.join(tmp, 'on'), contentLinkRoot: null });
const off = await runGeneration({ host: 'claude', outputRoot: path.join(tmp, 'off'), contentLinkRoot: null, planMode: false });
expect(on.exitCode).toBe(0);
expect(off.exitCode).toBe(0);
skills = on.artifacts.filter(a => a.kind === 'skill').map(a => a.relativePath);
});
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
const read = (root: string, rel: string) => fs.readFileSync(path.join(root, rel), 'utf8');
/** The committed text with the two plan-mode sections (each heading plus one paragraph) cut out. */
function withoutPlanMode(text: string): string {
const start = text.indexOf(SAFE);
const invocation = text.indexOf(INVOCATION, start);
expect(invocation).toBeGreaterThan(start);
const end = text.indexOf('\n\n', invocation + INVOCATION.length) + 2;
return text.slice(0, start) + text.slice(end);
}
describe('gen-skill-docs --plan-mode=off (claude host)', () => {
test('default output is byte-identical to every committed SKILL.md', () => {
expect(skills.length).toBeGreaterThan(40);
for (const rel of skills) expect(read(path.join(tmp, 'on'), rel), rel).toBe(read(ROOT, rel));
});
test('off drops exactly the two plan-mode sections and leaves other skills untouched', () => {
let stripped = 0;
for (const rel of skills) {
const committed = read(ROOT, rel);
const off = read(path.join(tmp, 'off'), rel);
expect(off, rel).not.toContain(SAFE);
expect(off, rel).not.toContain('## Skill Invocation During Plan Mode');
if (!committed.includes(SAFE)) {
expect(off, rel).toBe(committed);
continue;
}
expect(off, rel).toBe(withoutPlanMode(committed));
stripped++;
}
expect(stripped).toBeGreaterThan(40);
expect(read(path.join(tmp, 'off'), 'SKILL.md')).toContain('PLAN MODE EXCEPTION');
});
test('the CLI flag parses: --plan-mode=off renders without the sections, an unknown value is refused', () => {
const cli = (...args: string[]) => spawnSync('bun', ['run', 'scripts/gen-skill-docs.ts', '--host', 'claude', ...args], {
cwd: ROOT, encoding: 'utf8', timeout: 120_000,
});
const out = path.join(tmp, 'cli');
const ok = cli('--out-dir', out, '--plan-mode=off');
expect(ok.status, ok.stderr).toBe(0);
expect(read(out, 'SKILL.md')).toBe(read(path.join(tmp, 'off'), 'SKILL.md'));
expect(read(out, 'review/SKILL.md')).not.toContain(SAFE);
const bad = cli('--out-dir', path.join(tmp, 'bad'), '--plan-mode=maybe');
expect(bad.status).not.toBe(0);
expect(bad.stderr).toContain("Unknown plan mode: maybe. Use 'on' or 'off'.");
});
});