Skip to content
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { installSkill } from "../installSkill";
import { installSkills } from "../installSkills";

vi.mock("@trigger.dev/sdk/v3", () => ({
logger: { log: vi.fn(), error: vi.fn(), warn: vi.fn() },
}));

describe("installSkill", () => {
describe("installSkills", () => {
const mockStdout = vi.fn();
const mockStderr = vi.fn();
const mockRunCommand = vi.fn();
Expand All @@ -18,48 +18,62 @@ describe("installSkill", () => {
mockStderr.mockResolvedValue("");
});

it("should not throw when skill copy fails", async () => {
// Install succeeds
it("should not throw when skills copy fails", async () => {
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});
// Copy fails
mockRunCommand.mockResolvedValueOnce({
exitCode: 1,
stdout: mockStdout,
stderr: vi.fn().mockResolvedValue("cp: no such file or directory"),
});

await expect(installSkill(sandbox, "recoupable/setup-artist")).resolves.not.toThrow();
await expect(installSkills(sandbox, "recoupable/skills")).resolves.not.toThrow();
});

it("should not throw when skill install fails", async () => {
// Install fails
it("should not throw when skills install fails", async () => {
mockRunCommand.mockResolvedValueOnce({
exitCode: 1,
stdout: mockStdout,
stderr: vi.fn().mockResolvedValue("npm ERR! 404 Not Found"),
});

await expect(installSkill(sandbox, "recoupable/setup-artist")).resolves.not.toThrow();
await expect(installSkills(sandbox, "recoupable/skills")).resolves.not.toThrow();
});

it("should succeed when both install and copy succeed", async () => {
// Install succeeds
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});
// Copy succeeds
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});

await expect(installSkill(sandbox, "recoupable/setup-artist")).resolves.not.toThrow();
await expect(installSkills(sandbox, "recoupable/skills")).resolves.not.toThrow();
});

it("should copy all skills to OpenClaw workspace", async () => {
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});

await installSkills(sandbox, "recoupable/skills");

const copyCall = mockRunCommand.mock.calls[1];
const copyArgs = copyCall[0].args[1];
expect(copyArgs).toContain("cp -r .agents/skills/* ~/.openclaw/workspace/skills/");
});
});
16 changes: 4 additions & 12 deletions src/sandboxes/ensureSetupSandbox.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { Sandbox } from "@vercel/sandbox";
import { installSkill } from "./installSkill";
import { installSkills } from "./installSkills";
import { runSetupSandboxSkill } from "./runSetupSandboxSkill";
import { runSetupArtistSkill } from "./runSetupArtistSkill";
import { logStep } from "./logStep";

/**
* Ensures the sandbox has the org/artist folder structure set up.
* Installs skills, runs setup-sandbox, then setup-artist for each artist.
* Idempotent — skips if `orgs/` directory already exists.
* Installs skills from recoupable/skills, then runs setup-sandbox.
*
* @param sandbox - The Vercel Sandbox instance
* @param accountId - The account ID for the sandbox owner
Expand All @@ -16,10 +15,7 @@ export async function ensureSetupSandbox(
accountId: string
): Promise<void> {
logStep("Installing skills");

await installSkill(sandbox, "recoupable/setup-sandbox");
await installSkill(sandbox, "recoupable/setup-artist");
await installSkill(sandbox, "recoupable/release-management");
await installSkills(sandbox, "recoupable/skills");

if (!process.env.RECOUP_API_KEY) {
throw new Error("Missing RECOUP_API_KEY environment variable");
Expand All @@ -33,8 +29,4 @@ export async function ensureSetupSandbox(
logStep("Running setup-sandbox skill");
await runSetupSandboxSkill(sandbox, env);
logStep("Setup-sandbox complete", false);

logStep("Running setup-artist skill");
await runSetupArtistSkill(sandbox, env);
logStep("Setup-artist complete", false);
}
63 changes: 0 additions & 63 deletions src/sandboxes/installSkill.ts

This file was deleted.

61 changes: 61 additions & 0 deletions src/sandboxes/installSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Sandbox } from "@vercel/sandbox";
import { logger } from "@trigger.dev/sdk/v3";

/**
* Installs all skills from a skills.sh repo into the OpenClaw workspace.
*
* Runs `npx skills add <source> -y` which installs to .agents/skills/,
* then copies everything to ~/.openclaw/workspace/skills/ so OpenClaw
* discovers them natively.
*
* @param sandbox - The Vercel Sandbox instance
* @param source - The skills.sh source (e.g. "recoupable/skills")
*/
export async function installSkills(
sandbox: Sandbox,
source: string
): Promise<void> {
logger.log("Installing skills via skills.sh", { source });

const install = await sandbox.runCommand({
cmd: "npx",
args: ["skills", "add", source, "-y"],
});

const installStdout = (await install.stdout()) || "";
const installStderr = (await install.stderr()) || "";

logger.log("skills.sh install result", {
exitCode: install.exitCode,
stdout: installStdout,
stderr: installStderr,
});

if (install.exitCode !== 0) {
logger.warn("Skills install failed, continuing without them", {
source,
stderr: installStderr,
});
return;
}

const copy = await sandbox.runCommand({
cmd: "sh",
args: [
"-c",
"mkdir -p ~/.openclaw/workspace/skills && cp -r .agents/skills/* ~/.openclaw/workspace/skills/",
],
});

const copyStderr = (await copy.stderr()) || "";

if (copy.exitCode !== 0) {
logger.warn("Failed to copy skills to OpenClaw workspace, continuing without them", {
source,
stderr: copyStderr,
});
return;
Comment on lines +34 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast when required skill setup fails.

In the current ensureSetupSandbox flow, returning here means we still try to run /setup-sandbox without the skills being available in OpenClaw. That turns an install/copy failure into a later agent failure and hides the real cause.

Proposed fix
   if (install.exitCode !== 0) {
-    logger.warn("Skills install failed, continuing without them", {
+    logger.error("Skills install failed", {
       source,
       stderr: installStderr,
     });
-    return;
+    throw new Error(`Failed to install skills from ${source}`);
   }
@@
   if (copy.exitCode !== 0) {
-    logger.warn("Failed to copy skills to OpenClaw workspace, continuing without them", {
+    logger.error("Failed to copy skills to OpenClaw workspace", {
       source,
       stderr: copyStderr,
     });
-    return;
+    throw new Error("Failed to copy skills to OpenClaw workspace");
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (install.exitCode !== 0) {
logger.warn("Skills install failed, continuing without them", {
source,
stderr: installStderr,
});
return;
}
const copy = await sandbox.runCommand({
cmd: "sh",
args: [
"-c",
"mkdir -p ~/.openclaw/workspace/skills && cp -r .agents/skills/* ~/.openclaw/workspace/skills/",
],
});
const copyStderr = (await copy.stderr()) || "";
if (copy.exitCode !== 0) {
logger.warn("Failed to copy skills to OpenClaw workspace, continuing without them", {
source,
stderr: copyStderr,
});
return;
if (install.exitCode !== 0) {
logger.error("Skills install failed", {
source,
stderr: installStderr,
});
throw new Error(`Failed to install skills from ${source}`);
}
const copy = await sandbox.runCommand({
cmd: "sh",
args: [
"-c",
"mkdir -p ~/.openclaw/workspace/skills && cp -r .agents/skills/* ~/.openclaw/workspace/skills/",
],
});
const copyStderr = (await copy.stderr()) || "";
if (copy.exitCode !== 0) {
logger.error("Failed to copy skills to OpenClaw workspace", {
source,
stderr: copyStderr,
});
throw new Error("Failed to copy skills to OpenClaw workspace");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sandboxes/installSkills.ts` around lines 34 - 57, The code currently logs
and returns when skill installation or copying fails (checking install.exitCode
and copy.exitCode after sandbox.runCommand), which lets ensureSetupSandbox
continue and later fail; instead, fail fast by throwing a descriptive Error
(including source and the captured stderr like installStderr or copyStderr) when
install.exitCode !== 0 or copy.exitCode !== 0 so the higher-level flow aborts
immediately; update the branches around install.exitCode and copy.exitCode (and
remove the current logger.warn-only behavior) to throw errors that include
context from the failing sandbox.runCommand calls.

}

logger.log("Skills installed to OpenClaw workspace", { source });
}
25 changes: 0 additions & 25 deletions src/sandboxes/runSetupArtistSkill.ts

This file was deleted.

Loading