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

  1. Ensure at least SKILL.md and script.ts are in the files map before calling stageSkill.
  2. Gating logic that filters files should fail loudly when it would produce an empty map rather than passing it downstream.
  3. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/41285d27d199cec3. Report an issue: GitHub.