fix(cli): skills bundler resolves caller-relative paths + correct dev layout

Two correctness fixes caught during end-to-end validation:

1. Resolve skill.path relative to the file that called skills.define(),
   not the project root. The resource catalog already captures filePath
   on each SkillManifest — use it to compute the source folder.

2. In dev, copy skill bundles to {workingDir}/.trigger/skills/ (where
   the worker's cwd resolves). In deploy, keep copying to
   {outputPath}/.trigger/skills/ so the Dockerfile COPY . /app lands
   them at /app/.trigger/skills/.

Also upgrade the skill-discovery failure log from debug to warn so
config mistakes surface in the dev console instead of disappearing
silently.

Finally, update the ai-chat reference's aiChat.run() to pass chatTools
through chat.toStreamTextOptions({ tools: chatTools }) instead of
spreading them after — the auto-injected loadSkill/readFile/bash tools
would otherwise get overwritten by the explicit tools: chatTools key.
This commit is contained in:
Eric Allam
2026-04-18 22:29:07 +01:00
parent 3ae6f91c0a
commit 9942ef4cd7
4 changed files with 26 additions and 8 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ export async function buildWorker(options: BuildWorkerOptions) {
});
buildManifest = skillsResult.buildManifest;
} catch (err) {
logger.debug("Skill bundling failed; continuing without skills", err);
logger.warn("Skill bundling failed; continuing without skills", err);
}
buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest);
+23 -5
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { dirname, join, resolve as resolvePath } from "node:path";
import { dirname, 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";
@@ -60,8 +60,10 @@ export async function bundleSkills(
} catch (err) {
// Skill discovery via the indexer is best-effort — if the user's
// bundle doesn't load cleanly here the downstream full indexer will
// surface the real error. Warn and continue with no skills.
logger.debug(`[bundleSkills] skill discovery failed: ${(err as Error).message}`);
// surface the real error. Warn so the user sees what went wrong.
logger.warn(
`[bundleSkills] skill discovery failed, skipping skill bundling: ${(err as Error).message}`
);
return { buildManifest, skills: [] };
}
@@ -69,10 +71,26 @@ export async function bundleSkills(
return { buildManifest, skills: [] };
}
const destinationRoot = join(buildManifest.outputPath, ".trigger", "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");
for (const skill of skills) {
const sourcePath = resolvePath(workingDir, skill.sourcePath);
// 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;
+1 -1
View File
@@ -136,7 +136,7 @@ export async function startDevSession({
});
buildManifest = skillsResult.buildManifest;
} catch (err) {
logger.debug("Skill bundling failed during dev rebuild", err);
logger.warn("Skill bundling failed during dev rebuild", err);
}
buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest);
+1 -1
View File
@@ -495,10 +495,10 @@ export const aiChat = chat
...chat.toStreamTextOptions({
registry,
telemetry: clientData?.userId ? { userId: clientData.userId } : undefined,
tools: chatTools,
}),
model: languageModelForChatTurn(modelOverride),
messages: messages,
tools: chatTools,
stopWhen: stepCountIs(10),
abortSignal: stopSignal,
providerOptions: {