garrytan/gstack · error · Error

Skill "${opts.skill.name}" missing script.ts at ${scriptPath

Error message

Skill "${opts.skill.name}" missing script.ts at ${scriptPath}

What it means

Thrown by spawnSkill (the load-bearing spawner) after a skill was resolved by readBrowserSkill but path.join(skill.dir, 'script.ts') does not exist. spawnSkill is called by handleRun and by /skillify's test-then-commit flow, so this surfaces whenever a registered skill lacks its executable entry. Distinct from SKILL.md-missing (skill wouldn't resolve) and script.test.ts-missing (only the test path checks that).

Source

Thrown at browse/src/browser-skill-commands.ts:263

 * 5. On exit/timeout, revoke the token. Always.
 */
export async function spawnSkill(opts: SpawnSkillOptions): Promise<SpawnSkillResult> {
  const spawnId = generateSpawnId();
  const tokenInfo = mintSkillToken({
    skillName: opts.skill.name,
    spawnId,
    spawnTimeoutSeconds: opts.timeoutSeconds,
  });

  try {
    const env = buildSpawnEnv({
      trusted: opts.trusted,
      port: opts.port,
      skillToken: tokenInfo.token,
    });
    const scriptPath = path.join(opts.skill.dir, 'script.ts');
    if (!fs.existsSync(scriptPath)) {
      throw new Error(`Skill "${opts.skill.name}" missing script.ts at ${scriptPath}`);
    }

    const proc = Bun.spawn(['bun', 'run', scriptPath, '--', ...opts.skillArgs], {
      cwd: opts.skill.dir,
      env,
      stdout: 'pipe',
      stderr: 'pipe',
    });

    let timedOut = false;
    const killer = setTimeout(() => {
      timedOut = true;
      try { proc.kill(); } catch {}
    }, opts.timeoutSeconds * 1000);

    const stdoutPromise = readCapped(proc.stdout, MAX_STDOUT_BYTES);
    const stderrPromise = readCapped(proc.stderr, MAX_STDOUT_BYTES);

View on GitHub (pinned to 94993f7401)

Solutions

  1. Confirm script.ts exists: `ls <skill.dir>/script.ts`.
  2. If the skill was hand-authored, add a script.ts that exports a default async function or runs the Playwright flow.
  3. Re-run /skillify to regenerate the skill end-to-end.
  4. On case-insensitive filesystems, verify the exact filename casing is `script.ts`.

Example fix

// before: <skill.dir>/ has SKILL.md only
// after: create <skill.dir>/script.ts
import { browse } from './_lib/client';
export default async function main() { /* ... */ }
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'fs';
import * as path from 'path';
import { readBrowserSkill } from './browser-skills';

function assertScriptTs(name: string): string {
  const skill = readBrowserSkill(name);
  if (!skill) throw new Error(`Skill "${name}" not found.`);
  const scriptPath = path.join(skill.dir, 'script.ts');
  if (!fs.existsSync(scriptPath)) {
    throw new Error(`Skill "${name}" missing script.ts at ${scriptPath}`);
  }
  return scriptPath;
}

Prevention

When it happens

Trigger: handleRun on a skill whose directory contains SKILL.md but no script.ts; /skillify invoked against a hand-authored skill missing the entry script; partial tombstone restore; skill directory was edited and script.ts renamed or deleted.

Common situations: Bundled skill shipped as documentation only; an agent authored SKILL.md via low-level writes but never created script.ts; file system case-sensitivity mismatch (script.ts vs Script.ts) on a case-insensitive FS mounted on Linux.

Related errors


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