garrytan/gstack · error · Error
stageSkill: files map is empty.
Error message
stageSkill: files map is empty.
What it means
Thrown by stageSkill after validateSkillName passed but opts.files.size === 0. stageSkill is the entry point of /skillify's write path; an empty files map means there is nothing to write (no SKILL.md, no script.ts), so it bails before creating the per-spawn wrapper dir under ~/.gstack/.tmp/.
Source
Thrown at browse/src/browser-skill-write.ts:70
files: Map<string, string | Buffer>;
/** Optional override (tests pass synthetic spawn ids). */
spawnId?: string;
/** Optional override (tests pass a fake tmp root). */
tmpRoot?: string;
}
/**
* Stage a skill into the staging tree:
* <tmpRoot>/.gstack/.tmp/skillify-<spawnId>/<name>/
*
* The leaf <name> directory is what gets renamed during commit. The wrapper
* skillify-<spawnId>/ is per-spawn so concurrent /skillify invocations don't
* collide. Returns the absolute path to the staged skill dir (ending in <name>).
*/
export function stageSkill(opts: StageSkillOptions): string {
validateSkillName(opts.name);
if (opts.files.size === 0) {
throw new Error('stageSkill: files map is empty.');
}
const spawnId = opts.spawnId ?? generateSpawnId();
const tmpRoot = opts.tmpRoot ?? path.join(os.homedir(), '.gstack', '.tmp');
const wrapperDir = path.join(tmpRoot, `skillify-${spawnId}`);
const stagedDir = path.join(wrapperDir, opts.name);
mkdirSecure(wrapperDir);
mkdirSecure(stagedDir);
for (const [relPath, contents] of opts.files) {
if (relPath.startsWith('/') || relPath.includes('..')) {
// Defense in depth: validateSkillName above bounds the leaf, but a
// bad relPath in files could still write outside the staged dir.
throw new Error(`Invalid file path in stageSkill: "${relPath}".`);
}
const filePath = path.join(stagedDir, relPath);
const fileDir = path.dirname(filePath);View on GitHub (pinned to 94993f7401)
Solutions
- Ensure at least SKILL.md and script.ts are in the files map before calling stageSkill.
- Gating logic that filters files should fail loudly when it would produce an empty map rather than passing it downstream.
- Add a pre-check: `if (files.size === 0) throw new Error('no files generated');` at the source.
Example fix
// before
stageSkill({ name: 'foo', files: new Map() });
// after
const files = new Map([['SKILL.md', '---\nname: foo\nhost: example.com\n---'], ['script.ts', '...']]);
stageSkill({ name: 'foo', files }); Defensive patterns
Strategy: validation
Validate before calling
function assertFilesNonEmpty(files: Map<string, string | Buffer>): void {
if (files.size === 0) {
throw new Error('stageSkill: files map is empty. Generate at least SKILL.md + script.ts.');
}
}
// before stageSkill:
assertFilesNonEmpty(opts.files); Prevention
- Fail loud at the source if a scrape/template produced no files.
- Ensure SKILL.md and script.ts are always populated before calling stageSkill.
- Add a unit test that stageSkill throws on an empty map.
When it happens
Trigger: stageSkill({name: 'foo', files: new Map()}); or a caller that built the files map conditionally and skipped every entry. Common in agent flows that gate file production on scraped content that came back empty.
Common situations: Agent's /skillify flow produced zero files because the scrape returned nothing; a wrapper filtered out every file via an allowlist; map constructor was called without entries by mistake; programmatic caller forgot to populate files.
Related errors
- Skill name is empty.
- Invalid skill name "${name}". Must be lowercase letters/digi
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: staged path "${opts.stagedDir}" is not a direct
- Usage: $B skill show <name>
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/41285d27d199cec3.
Report an issue: GitHub.