From 9d47608a160180bdfba466cbffc2b8b6f6bcc5b6 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 21 May 2026 16:14:46 +0100 Subject: [PATCH 1/2] feat(references): add agent-skills reference project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minimal reference covering skills.define + skill.local — one task that loads a SKILL.md and runs a bundled shell script. Useful as a sanity test for the dev + deploy skill-bundling pipeline. --- pnpm-lock.yaml | 13 ++++++ references/agent-skills/package.json | 16 +++++++ .../src/trigger/skills/greeter/SKILL.md | 18 ++++++++ .../trigger/skills/greeter/scripts/hello.sh | 4 ++ .../agent-skills/src/trigger/test-skill.ts | 42 +++++++++++++++++++ references/agent-skills/trigger.config.ts | 9 ++++ references/agent-skills/tsconfig.json | 13 ++++++ 7 files changed, 115 insertions(+) create mode 100644 references/agent-skills/package.json create mode 100644 references/agent-skills/src/trigger/skills/greeter/SKILL.md create mode 100755 references/agent-skills/src/trigger/skills/greeter/scripts/hello.sh create mode 100644 references/agent-skills/src/trigger/test-skill.ts create mode 100644 references/agent-skills/trigger.config.ts create mode 100644 references/agent-skills/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17f73d9a252..d0bb1e5b836 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2223,6 +2223,19 @@ importers: specifier: 3.25.76 version: 3.25.76 + references/agent-skills: + dependencies: + '@trigger.dev/build': + specifier: workspace:* + version: link:../../packages/build + '@trigger.dev/sdk': + specifier: workspace:* + version: link:../../packages/trigger-sdk + devDependencies: + trigger.dev: + specifier: workspace:* + version: link:../../packages/cli-v3 + references/ai-chat: dependencies: '@ai-sdk/anthropic': diff --git a/references/agent-skills/package.json b/references/agent-skills/package.json new file mode 100644 index 00000000000..b8016c09279 --- /dev/null +++ b/references/agent-skills/package.json @@ -0,0 +1,16 @@ +{ + "name": "references-agent-skills", + "private": true, + "type": "module", + "devDependencies": { + "trigger.dev": "workspace:*" + }, + "dependencies": { + "@trigger.dev/build": "workspace:*", + "@trigger.dev/sdk": "workspace:*" + }, + "scripts": { + "dev": "trigger dev", + "deploy": "trigger deploy" + } +} diff --git a/references/agent-skills/src/trigger/skills/greeter/SKILL.md b/references/agent-skills/src/trigger/skills/greeter/SKILL.md new file mode 100644 index 00000000000..a824f138003 --- /dev/null +++ b/references/agent-skills/src/trigger/skills/greeter/SKILL.md @@ -0,0 +1,18 @@ +--- +name: greeter +description: Say hello in different styles. Use when the user asks for a greeting or a friendly message. +--- + +# Greeter + +A tiny skill used to validate that the CLI bundles `SKILL.md` plus a `scripts/` subfolder into the deploy image and that `skill.local()` can read both at runtime. + +## When to use + +- Anyone asks for "hello" — invoke `scripts/hello.sh [NAME]` and return its stdout. + +## Scripts + +### `scripts/hello.sh [NAME]` + +Prints `Hello, {NAME}!` (default `world`). Used to confirm `scripts/` is copied alongside `SKILL.md`. diff --git a/references/agent-skills/src/trigger/skills/greeter/scripts/hello.sh b/references/agent-skills/src/trigger/skills/greeter/scripts/hello.sh new file mode 100755 index 00000000000..b94fa92f76a --- /dev/null +++ b/references/agent-skills/src/trigger/skills/greeter/scripts/hello.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +NAME="${1:-world}" +echo "Hello, ${NAME}!" diff --git a/references/agent-skills/src/trigger/test-skill.ts b/references/agent-skills/src/trigger/test-skill.ts new file mode 100644 index 00000000000..6a6de46665d --- /dev/null +++ b/references/agent-skills/src/trigger/test-skill.ts @@ -0,0 +1,42 @@ +import { logger, skills, task } from "@trigger.dev/sdk"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { join } from "node:path"; +import { access, constants } from "node:fs/promises"; + +const greeterSkill = skills.define({ + id: "greeter", + path: "./skills/greeter", +}); + +const execAsync = promisify(exec); + +export const testSkillTask = task({ + id: "test-skill", + run: async (payload: { name?: string } = {}) => { + const resolved = await greeterSkill.local(); + + logger.info("Resolved skill", { + id: resolved.id, + version: resolved.version, + path: resolved.path, + frontmatterName: resolved.frontmatter.name, + frontmatterDescription: resolved.frontmatter.description, + bodyChars: resolved.body.length, + }); + + const scriptPath = join(resolved.path, "scripts", "hello.sh"); + await access(scriptPath, constants.X_OK); + + const { stdout } = await execAsync(`bash ${scriptPath} ${payload.name ?? "world"}`); + const output = stdout.trim(); + logger.info("Script output", { output }); + + return { + skillId: resolved.id, + skillPath: resolved.path, + frontmatterName: resolved.frontmatter.name, + scriptOutput: output, + }; + }, +}); diff --git a/references/agent-skills/trigger.config.ts b/references/agent-skills/trigger.config.ts new file mode 100644 index 00000000000..5a5c9779107 --- /dev/null +++ b/references/agent-skills/trigger.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "@trigger.dev/sdk"; + +export default defineConfig({ + project: "proj_zweffkxiuovfzsdtjvbe", + runtime: "node", + logLevel: "info", + maxDuration: 60, + dirs: ["./src/trigger"], +}); diff --git a/references/agent-skills/tsconfig.json b/references/agent-skills/tsconfig.json new file mode 100644 index 00000000000..2ca0ae6e2d7 --- /dev/null +++ b/references/agent-skills/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "customConditions": ["@triggerdotdev/source"], + "noEmit": true + }, + "include": ["./src/**/*.ts", "trigger.config.ts"] +} From 13f51bb4bbb5ac0effb1d681f6060ef9a5552f21 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 21 May 2026 16:15:01 +0100 Subject: [PATCH 2/2] fix(cli): stop chat.agent skills silently disappearing from trigger dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev CLI ran a separate skill-discovery indexer pass with a bare process.env, while the actual worker indexer ran with the full execution env. Task files that read process.env at module top level imported cleanly in the worker pass and threw in the skill pass — the latter swallowed the error and skipped skill copying, so skill.local() failed at runtime with ENOENT. Drop the duplicate pass. The skill registry is already part of the worker manifest, so copy skill folders from there after initialize. A bad SKILL.md now surfaces as a startup error instead of silently disappearing skills. --- .changeset/bundle-skills-single-pass.md | 5 + packages/cli-v3/src/build/bundleSkills.ts | 145 +++++++++++++--------- packages/cli-v3/src/dev/devSession.ts | 26 +--- packages/cli-v3/src/dev/devSupervisor.ts | 20 +++ 4 files changed, 116 insertions(+), 80 deletions(-) create mode 100644 .changeset/bundle-skills-single-pass.md diff --git a/.changeset/bundle-skills-single-pass.md b/.changeset/bundle-skills-single-pass.md new file mode 100644 index 00000000000..30b2c428b22 --- /dev/null +++ b/.changeset/bundle-skills-single-pass.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Fix `chat.agent` skills silently missing in `trigger dev` for projects whose task files read `process.env` at module top level (e.g. a third-party SDK client initialized at import). Skill folders now bundle into `.trigger/skills/` reliably regardless of which env vars are set when the CLI launches. diff --git a/packages/cli-v3/src/build/bundleSkills.ts b/packages/cli-v3/src/build/bundleSkills.ts index 65ad9834abe..8533d254c72 100644 --- a/packages/cli-v3/src/build/bundleSkills.ts +++ b/packages/cli-v3/src/build/bundleSkills.ts @@ -1,6 +1,5 @@ -import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; -import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path"; +import { isAbsolute, join, resolve as resolvePath } from "node:path"; import type { BuildManifest, SkillManifest } from "@trigger.dev/core/v3/schemas"; import { copyDirectoryRecursive } from "@trigger.dev/build/internal"; import { indexWorkerManifest } from "../indexing/indexWorkerManifest.js"; @@ -21,13 +20,84 @@ export type BundleSkillsResult = { skills: SkillManifest[]; }; +export type CopySkillFoldersOptions = { + skills: SkillManifest[]; + /** Root where `{destinationRoot}/{id}/` folders will be created. */ + destinationRoot: string; + /** Used to resolve relative `filePath` references in skill manifests. */ + workingDir: string; + /** Only `debug` is used. `BuildLogger` and the cli `logger` both satisfy this shape. */ + logger: { debug: (...args: unknown[]) => void }; +}; + +/** + * Copy each skill's source folder to `{destinationRoot}/{id}/`. Validates + * that `SKILL.md` exists and has the required frontmatter. Pure file IO — + * no indexer subprocess, no env handling. + * + * Used by the dev path (driven by the main worker indexer's skills list) + * and indirectly by the deploy path (via `bundleSkills` which discovers + * skills via its own indexer pass first, then delegates here). + */ +export async function copySkillFolders( + options: CopySkillFoldersOptions +): Promise { + const { skills, destinationRoot, workingDir, logger } = options; + + if (skills.length === 0) { + return []; + } + + for (const skill of skills) { + const callerDir = skill.filePath + ? resolvePath(workingDir, skill.filePath, "..") + : workingDir; + const sourcePath = isAbsolute(skill.sourcePath) + ? skill.sourcePath + : resolvePath(callerDir, skill.sourcePath); + const skillMdPath = join(sourcePath, "SKILL.md"); + + let skillMd: string; + try { + skillMd = await readFile(skillMdPath, "utf8"); + } catch { + throw new Error( + `Skill "${skill.id}": SKILL.md not found at ${skillMdPath}. ` + + `Registered via skills.define({ id: "${skill.id}", path: "${skill.sourcePath}" }) ` + + `at ${skill.filePath}.` + ); + } + + if (!/^---\r?\n[\s\S]*?\r?\n---/.test(skillMd)) { + throw new Error( + `Skill "${skill.id}": SKILL.md at ${skillMdPath} is missing a frontmatter block.` + ); + } + if (!/\bname:\s*\S/.test(skillMd) || !/\bdescription:\s*\S/.test(skillMd)) { + throw new Error( + `Skill "${skill.id}": SKILL.md at ${skillMdPath} frontmatter must include both \`name\` and \`description\`.` + ); + } + + const skillDest = join(destinationRoot, skill.id); + logger.debug(`[copySkillFolders] Copying ${sourcePath} → ${skillDest}`); + await copyDirectoryRecursive(sourcePath, skillDest); + } + + return [...skills].sort((a, b) => a.id.localeCompare(b.id)); +} + /** * Built-in skill bundler — not an extension. Runs the indexer locally - * against the bundled worker output to discover `ai.defineSkill(...)` + * against the bundled worker output to discover `skills.define(...)` * registrations, validates each skill's `SKILL.md`, and copies the * folder into `{outputPath}/.trigger/skills/{id}/` so the deploy image * picks it up via the existing Dockerfile `COPY`. * + * Used by the deploy path. The dev path uses `copySkillFolders` directly, + * driven by the main worker indexer that already runs in `BackgroundWorker.initialize` — + * no duplicate indexer pass needed there. + * * No `trigger.config.ts` changes required — discovery is side-effect * based, same mechanism as task/prompt registration. */ @@ -71,65 +141,20 @@ export async function bundleSkills( return { buildManifest, skills: [] }; } - // Destination layout differs between dev and deploy: - // - Dev: the worker runs with cwd = workingDir, so skills must live at - // {workingDir}/.trigger/skills/{id}/ for skill.local() to find them. - // - Deploy: the Dockerfile COPY picks up everything under outputPath into - // /app, so we target {outputPath}/.trigger/skills/{id}/ and the - // container's cwd (/app) resolves correctly. - const destinationRoot = - buildManifest.target === "dev" - ? join(workingDir, ".trigger", "skills") - : join(buildManifest.outputPath, ".trigger", "skills"); + // Deploy target: the Dockerfile COPY picks up everything under outputPath + // into /app, so we target {outputPath}/.trigger/skills/{id}/ and the + // container's cwd (/app) resolves correctly. + const destinationRoot = join(buildManifest.outputPath, ".trigger", "skills"); - for (const skill of skills) { - // Resolve the skill's source folder relative to the file that called - // `skills.define(...)`. Absolute paths are honored as-is. - const callerDir = skill.filePath - ? dirname(resolvePath(workingDir, skill.filePath)) - : workingDir; - const sourcePath = isAbsolute(skill.sourcePath) - ? skill.sourcePath - : resolvePath(callerDir, skill.sourcePath); - const skillMdPath = join(sourcePath, "SKILL.md"); - - let skillMd: string; - try { - skillMd = await readFile(skillMdPath, "utf8"); - } catch { - throw new Error( - `Skill "${skill.id}": SKILL.md not found at ${skillMdPath}. ` + - `Registered via ai.defineSkill({ id: "${skill.id}", path: "${skill.sourcePath}" }) ` + - `at ${skill.filePath}.` - ); - } - - if (!/^---\r?\n[\s\S]*?\r?\n---/.test(skillMd)) { - throw new Error( - `Skill "${skill.id}": SKILL.md at ${skillMdPath} is missing a frontmatter block.` - ); - } - if (!/\bname:\s*\S/.test(skillMd) || !/\bdescription:\s*\S/.test(skillMd)) { - throw new Error( - `Skill "${skill.id}": SKILL.md at ${skillMdPath} frontmatter must include both \`name\` and \`description\`.` - ); - } - - const skillDest = join(destinationRoot, skill.id); - logger.debug(`[bundleSkills] Copying ${sourcePath} → ${skillDest}`); - await copyDirectoryRecursive(sourcePath, skillDest); - } - - // Sort by id for deterministic manifest output - skills = [...skills].sort((a, b) => a.id.localeCompare(b.id)); - - // Content hash is derived from each SKILL.md's content for cache invalidation - // downstream (dashboard persistence in Phase 2). Not used in Phase 1. - void createHash; - void dirname; + const sortedSkills = await copySkillFolders({ + skills, + destinationRoot, + workingDir, + logger, + }); return { - buildManifest: { ...buildManifest, skills }, - skills, + buildManifest: { ...buildManifest, skills: sortedSkills }, + skills: sortedSkills, }; } diff --git a/packages/cli-v3/src/dev/devSession.ts b/packages/cli-v3/src/dev/devSession.ts index 2d6645cd50c..ed6290a1b86 100644 --- a/packages/cli-v3/src/dev/devSession.ts +++ b/packages/cli-v3/src/dev/devSession.ts @@ -9,7 +9,6 @@ import { logBuildFailure, logBuildWarnings, } from "../build/bundle.js"; -import { bundleSkills } from "../build/bundleSkills.js"; import { createBuildContext, notifyExtensionOnBuildComplete, @@ -119,25 +118,12 @@ export async function startDevSession({ bundle.metafile ); - // Built-in skill bundling — copies registered skill folders into - // `.trigger/skills/{id}/` so `skill.local()` works at dev runtime. - try { - const buildManifestPath = join( - workerDir?.path ?? destination.path, - "build.json" - ); - await writeJSONFile(buildManifestPath, buildManifest); - const skillsResult = await bundleSkills({ - buildManifest, - buildManifestPath, - workingDir: rawConfig.workingDir, - env: process.env, - logger: buildContext.logger, - }); - buildManifest = skillsResult.buildManifest; - } catch (err) { - logger.warn("Skill bundling failed during dev rebuild", err); - } + // Skill folder copying happens after the main worker indexer runs in + // `BackgroundWorker.initialize` — that pass already discovers skills + // via the resource catalog and reports them on `workerManifest.skills`, + // so we don't need a duplicate indexer here (which historically ran + // with a bare `process.env` and silently dropped skills on projects + // whose task files read CLI-injected vars at module top level). buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest); diff --git a/packages/cli-v3/src/dev/devSupervisor.ts b/packages/cli-v3/src/dev/devSupervisor.ts index 59b2d2a473b..0d972384c40 100644 --- a/packages/cli-v3/src/dev/devSupervisor.ts +++ b/packages/cli-v3/src/dev/devSupervisor.ts @@ -18,6 +18,7 @@ import { eventBus } from "../utilities/eventBus.js"; import { logger } from "../utilities/logger.js"; import { resolveSourceFiles } from "../utilities/sourceFiles.js"; import { BackgroundWorker } from "./backgroundWorker.js"; +import { copySkillFolders } from "../build/bundleSkills.js"; import { WorkerRuntime } from "./workerRuntime.js"; import { chalkTask, cliLink, prettyError } from "../utilities/cliOutput.js"; import { DevRunController } from "../entryPoints/dev-run-controller.js"; @@ -331,6 +332,25 @@ class DevSupervisor implements WorkerRuntime { throw new Error("Could not initialize worker"); } + // Copy registered skill folders into `${workingDir}/.trigger/skills/{id}/` + // so `skill.local()` can read them at runtime. The main indexer already + // discovered skills; we just do the file IO here. + const discoveredSkills = backgroundWorker.manifest.skills ?? []; + if (discoveredSkills.length > 0) { + try { + await copySkillFolders({ + skills: discoveredSkills, + destinationRoot: join(this.options.config.workingDir, ".trigger", "skills"), + workingDir: this.options.config.workingDir, + logger, + }); + } catch (err) { + prettyError("Skill bundling failed", (err as Error).message); + stop(); + return; + } + } + const validationIssue = validateWorkerManifest(backgroundWorker.manifest); if (validationIssue) {