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
- Confirm script.ts exists: `ls <skill.dir>/script.ts`.
- If the skill was hand-authored, add a script.ts that exports a default async function or runs the Playwright flow.
- Re-run /skillify to regenerate the skill end-to-end.
- 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
- Every runnable skill must contain script.ts as the entry point.
- On case-insensitive filesystems mounted on Linux, verify exact casing (`script.ts`).
- Re-run /skillify if script.ts was deleted or renamed.
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
- Skill "${name}" not found in any tier.
- Skill "${name}" not found.
- Skill "${name}" has no script.test.ts at ${testFile}
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: tier "${opts.tier}" has no resolved path.
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/39258eb6e125dd00.
Report an issue: GitHub.