mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
fix(modules): gen:skill-docs refuses an exclusion render without --out-dir; render tests set up their own arm
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a919a41678
commit
1d3e05aaf9
4 files changed
+18
-3
No files matched your search
+1
-1
@@ -26,7 +26,7 @@ bin/gstack-modules render --host claude \
|
||||
|
||||
`render` writes the whole host tree into a staging directory beside `--out-dir`. It checks that no rendered file still points at an excluded module, writes `loadout.lock.json` last (loadout name or `adhoc`, the normalized spec and its sha256, host, model, every excluded id, and the sha256 and byte count of every file), then swaps the result into place. A failure leaves an earlier render at `--out-dir` untouched. Claude section links name `--out-dir`, so point the eval harness at that directory. Pass the same `--model`, `--install-root` and `--plan-mode` the install under test uses, so the arms differ only by the modules. Codex renders default to the host's generic `gpt` overlay, while `./setup` uses `gpt-6-astra`.
|
||||
|
||||
`bun run gen:skill-docs --loadout <name>` and `--modules <spec>` apply the same filter to an ordinary generation run. A non-`full` render adds `<!-- loadout: <name> -->` to each SKILL.md header.
|
||||
`bun run gen:skill-docs --loadout <name> --out-dir <dir>` and `--modules <spec> --out-dir <dir>` apply the same filter to a generation run without the staging, dangling-reference check or lock file; a selection with exclusions and no `--out-dir` is refused with `MODULE_SPEC_INVALID`, because the committed tree is always the `full` render. A non-`full` render adds `<!-- loadout: <name> -->` to each SKILL.md header.
|
||||
|
||||
## Building an eval arm
|
||||
|
||||
|
||||
@@ -5159,7 +5159,7 @@ These codes come from `bin/gstack-modules` and `gen-skill-docs --loadout/--modul
|
||||
<a id="module-spec-invalid"></a>
|
||||
### `MODULE_SPEC_INVALID: …`
|
||||
|
||||
**Meaning.** A `--modules` token is not `-<module id>` or `+<module id>`, both `--loadout` and `--modules` were given, or a render flag is missing or malformed. **Fix.** Write the spec as `<loadout>,-<id>,+<id>` (for example `full,-section:office-hours/second-opinion`) and pass the flags `bin/gstack-modules --help` lists.
|
||||
**Meaning.** A `--modules` token is not `-<module id>` or `+<module id>`, both `--loadout` and `--modules` were given, `gen:skill-docs` got a selection with exclusions but no `--out-dir`, or a render flag is missing or malformed. **Fix.** Write the spec as `<loadout>,-<id>,+<id>` (for example `full,-section:office-hours/second-opinion`) and pass the flags `bin/gstack-modules --help` lists.
|
||||
|
||||
<a id="module-unknown"></a>
|
||||
### `MODULE_UNKNOWN: "<id>" is not a module`
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { Host, TemplateContext } from './resolvers/types';
|
||||
import { HOST_PATHS } from './resolvers/types';
|
||||
import { RESOLVERS } from './resolvers/index';
|
||||
import { applySectionGuards, rewriteCarvedSectionRefs, SKILL_BYTE_CEILING, usesLazySections } from './resolvers/sections';
|
||||
import { excludedSkillDirs, selectModules } from './modules';
|
||||
import { ModuleError, excludedSkillDirs, selectModules } from './modules';
|
||||
import type { ModuleFilter } from './resolvers/types';
|
||||
import { insertRuntimePreludes } from './resolvers/runtime-root';
|
||||
import { generateSharedPreambleFile, hoistsPreamble, SHARED_PREAMBLE_FILE } from './resolvers/preamble-hoist';
|
||||
@@ -150,6 +150,9 @@ function parseGenerationArgs(args: string[]): GenerationOptions {
|
||||
const badDisabled = disabledSkills.find(n => !/^\/?[a-z0-9-]+$/.test(n));
|
||||
if (badDisabled) throw new Error(`--disabled-skills takes skill names (got ${JSON.stringify(badDisabled)})`);
|
||||
const modules = selectModules({ loadout: value('--loadout'), modules: value('--modules') });
|
||||
if (modules && outDir === undefined) {
|
||||
throw new ModuleError('MODULE_SPEC_INVALID', `--${value('--modules') === undefined ? 'loadout' : 'modules'} with exclusions needs --out-dir; the committed tree is always the full render`, 'bin/gstack-modules render --host <host> --modules <spec> --out-dir <dir>');
|
||||
}
|
||||
const installRoot = value('--install-root');
|
||||
if (installRoot !== undefined && !INSTALL_ROOT_PATTERN.test(installRoot)) {
|
||||
throw new Error(`--install-root must be an absolute path of letters, digits and . _ - + @ / (got ${JSON.stringify(installRoot)})`);
|
||||
|
||||
@@ -156,6 +156,7 @@ describe('render filter units', () => {
|
||||
describe('gstack-modules render', () => {
|
||||
const spec = 'full,-section:office-hours/second-opinion,-section:office-hours/visual-design,-preamble:writing-style,-overlay:claude,-skill:design-shotgun';
|
||||
const readLock = (dir: string) => JSON.parse(fs.readFileSync(path.join(dir, LOCK_FILE), 'utf-8')) as LoadoutLock;
|
||||
const ensureArm = async (dir: string) => { if (!fs.existsSync(path.join(dir, LOCK_FILE))) await renderLoadout({ host: 'claude', selection: selectModules({ modules: spec }), outDir: dir }); };
|
||||
|
||||
test('claude: one arm excludes one module of each kind and locks every file', async () => {
|
||||
const out = path.join(tmp, 'claude-arm');
|
||||
@@ -194,6 +195,7 @@ describe('gstack-modules render', () => {
|
||||
|
||||
test('a dangling reference is refused and the earlier render survives', async () => {
|
||||
const out = path.join(tmp, 'claude-arm');
|
||||
await ensureArm(out);
|
||||
const before = readLock(out);
|
||||
const unsafe = renderLoadout({ host: 'claude', selection: selectModules({ modules: 'full,-section:plan-design-review/review-sections' }), outDir: out });
|
||||
await expect(unsafe).rejects.toMatchObject({ code: 'MODULE_EXCLUSION_UNSAFE' });
|
||||
@@ -204,6 +206,8 @@ describe('gstack-modules render', () => {
|
||||
|
||||
test('a later render replaces an earlier one; a foreign directory is never replaced', async () => {
|
||||
const out = path.join(tmp, 'claude-arm');
|
||||
await ensureArm(out);
|
||||
expect(readLock(out).loadout).toBe('adhoc');
|
||||
const lock = await renderLoadout({ host: 'claude', selection: selectModules({ modules: 'full,-skill:design-shotgun' }), outDir: out });
|
||||
expect(readLock(out).spec_sha256).toBe(lock.spec_sha256);
|
||||
expect(fs.existsSync(path.join(out, 'office-hours/sections/second-opinion.md'))).toBe(true);
|
||||
@@ -266,6 +270,14 @@ describe('bin/gstack-modules', () => {
|
||||
expect(run('--help')).toMatchObject({ code: 0, stdout: expect.stringContaining('gstack-modules render') });
|
||||
});
|
||||
|
||||
test('gen:skill-docs refuses an exclusion render into the committed tree', () => {
|
||||
const before = spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }).stdout;
|
||||
const result = spawnSync(process.execPath, [...bunIsolationArgs(), path.join(ROOT, 'scripts/gen-skill-docs.ts'), '--host', 'claude', '--modules', 'full,-skill:design-shotgun'], { cwd: ROOT, encoding: 'utf-8', timeout: 120_000 });
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr + result.stdout).toContain('MODULE_SPEC_INVALID');
|
||||
expect(spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }).stdout).toBe(before);
|
||||
});
|
||||
|
||||
test('render prints one RENDERED line and writes the lock', () => {
|
||||
const out = path.join(tmp, 'cli-render');
|
||||
const result = run('render', '--host', 'claude', '--modules', 'full,-skill:design-shotgun', '--out-dir', out);
|
||||
|
||||
Reference in new issue
Block a user